From c73d9defc1a9f3cfcabe5df13d3a2a66b0c84468 Mon Sep 17 00:00:00 2001 From: MoeexT Date: Fri, 6 Mar 2026 15:33:00 +0800 Subject: [PATCH 1/6] :tada: preview pdf, docx, md --- frontend/package-lock.json | 562 +++++++++++++++++- frontend/package.json | 2 + .../components/file-preview/DocxPreview.tsx | 91 +++ .../components/file-preview/ImagePreview.tsx | 32 + .../file-preview/MarkdownPreview.tsx | 100 ++++ .../components/file-preview/PdfPreview.tsx | 159 +++++ .../components/file-preview/TextPreview.tsx | 27 + .../src/components/file-preview/index.tsx | 76 +++ frontend/src/index.css | 178 ++++++ .../Detail/components/Overview.tsx | 47 +- .../Detail/useFilesOperation.ts | 55 +- frontend/src/utils/request.ts | 14 +- 12 files changed, 1291 insertions(+), 52 deletions(-) create mode 100644 frontend/src/components/file-preview/DocxPreview.tsx create mode 100644 frontend/src/components/file-preview/ImagePreview.tsx create mode 100644 frontend/src/components/file-preview/MarkdownPreview.tsx create mode 100644 frontend/src/components/file-preview/PdfPreview.tsx create mode 100644 frontend/src/components/file-preview/TextPreview.tsx create mode 100644 frontend/src/components/file-preview/index.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9ab17a993..5c8359453 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,12 +14,14 @@ "i18next": "^25.8.0", "jssha": "^3.3.1", "lucide-react": "^0.539.0", + "mammoth": "^1.11.0", "react": "^18.1.1", "react-dom": "^18.1.1", "react-force-graph-2d": "^1.29.0", "react-force-graph-3d": "^1.29.0", "react-i18next": "^16.5.4", "react-markdown": "^10.1.0", + "react-pdf": "^10.4.1", "react-redux": "^9.2.0", "react-router": "^7.8.0", "react-syntax-highlighter": "^16.1.0", @@ -1204,6 +1206,256 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.96.tgz", + "integrity": "sha512-6NNmNxvoJKeucVjxaaRUt3La2i5jShgiAbaY3G/72s1Vp3U06XPrAIxkAjBxpDcamEn/t+WJ4OOlGmvILo4/Ew==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.96", + "@napi-rs/canvas-darwin-arm64": "0.1.96", + "@napi-rs/canvas-darwin-x64": "0.1.96", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.96", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.96", + "@napi-rs/canvas-linux-arm64-musl": "0.1.96", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.96", + "@napi-rs/canvas-linux-x64-gnu": "0.1.96", + "@napi-rs/canvas-linux-x64-musl": "0.1.96", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.96", + "@napi-rs/canvas-win32-x64-msvc": "0.1.96" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.96.tgz", + "integrity": "sha512-ew1sPrN3dGdZ3L4FoohPfnjq0f9/Jk7o+wP7HkQZokcXgIUD6FIyICEWGhMYzv53j63wUcPvZeAwgewX58/egg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.96.tgz", + "integrity": "sha512-Q/wOXZ5PzTqpdmA5eUOcegCf4Go/zz3aZ5DlzSeDpOjFmfwMKh8EzLAoweQ+mJVagcHQyzoJhaTEnrO68TNyNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.96.tgz", + "integrity": "sha512-UrXiQz28tQEvGM1qvyptewOAfmUrrd5+wvi6Rzjj2VprZI8iZ2KIvBD2lTTG1bVF95AbeDeG7PJA0D9sLKaOFA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.96.tgz", + "integrity": "sha512-I90ODxweD8aEP6XKU/NU+biso95MwCtQ2F46dUvhec1HesFi0tq/tAJkYic/1aBSiO/1kGKmSeD1B0duOHhEHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.96.tgz", + "integrity": "sha512-Dx/0+RFV++w3PcRy+4xNXkghhXjA5d0Mw1bs95emn5Llinp1vihMaA6WJt3oYv2LAHc36+gnrhIBsPhUyI2SGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.96.tgz", + "integrity": "sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.96.tgz", + "integrity": "sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.96.tgz", + "integrity": "sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.96.tgz", + "integrity": "sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.96.tgz", + "integrity": "sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.96.tgz", + "integrity": "sha512-UYGdTltVd+Z8mcIuoqGmAXXUvwH5CLf2M6mIB5B0/JmX5J041jETjqtSYl7gN+aj3k1by/SG6sS0hAwCqyK7zw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2615,6 +2867,15 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@xyflow/react": { "version": "12.8.3", "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.3.tgz", @@ -2865,6 +3126,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bezier-js": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", @@ -2888,6 +3169,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/bn.js": { "version": "4.12.2", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", @@ -3304,6 +3591,12 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3678,6 +3971,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -3688,6 +3987,15 @@ "csstype": "^3.0.2" } }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4773,6 +5081,12 @@ "dev": true, "license": "ISC" }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immer": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz", @@ -4823,7 +5137,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/inline-style-parser": { @@ -4967,6 +5280,12 @@ "dev": true, "license": "MIT" }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5090,6 +5409,18 @@ "node": "*" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/kapsule": { "version": "1.16.3", "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", @@ -5139,6 +5470,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", @@ -5435,6 +5775,17 @@ "loose-envify": "cli.js" } }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "node_modules/lowlight": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", @@ -5478,6 +5829,57 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/make-cancellable-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/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-event-props": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/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/mammoth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.11.0.tgz", + "integrity": "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mammoth/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -5803,6 +6205,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-refs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6718,6 +7137,12 @@ "wrappy": "1" } }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6768,6 +7193,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/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.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6826,6 +7257,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6847,6 +7287,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6951,6 +7403,12 @@ "node": ">=6" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7839,6 +8297,35 @@ "react": ">=18" } }, + "node_modules/react-pdf": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.4.1.tgz", + "integrity": "sha512-kS/35staVCBqS29verTQJQZXw7RfsRCPO3fdJoW1KXylcv7A9dw6DZ3vJXC2w+bIBgLw5FN4pOFvKSQtkQhPfA==", + "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.4.296", + "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-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", @@ -7945,6 +8432,27 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -8365,6 +8873,12 @@ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "license": "MIT" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -8524,6 +9038,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -8534,6 +9054,21 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", @@ -8954,6 +9489,12 @@ "dev": true, "license": "MIT" }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", @@ -9122,7 +9663,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/vary": { @@ -9297,6 +9837,15 @@ "node": ">=0.10.0" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9343,6 +9892,15 @@ "typedarray-to-buffer": "^3.1.5" } }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 21901e36a..e517ad485 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,12 +17,14 @@ "i18next": "^25.8.0", "jssha": "^3.3.1", "lucide-react": "^0.539.0", + "mammoth": "^1.11.0", "react": "^18.1.1", "react-dom": "^18.1.1", "react-force-graph-2d": "^1.29.0", "react-force-graph-3d": "^1.29.0", "react-i18next": "^16.5.4", "react-markdown": "^10.1.0", + "react-pdf": "^10.4.1", "react-redux": "^9.2.0", "react-router": "^7.8.0", "react-syntax-highlighter": "^16.1.0", diff --git a/frontend/src/components/file-preview/DocxPreview.tsx b/frontend/src/components/file-preview/DocxPreview.tsx new file mode 100644 index 000000000..eb2cd2d8a --- /dev/null +++ b/frontend/src/components/file-preview/DocxPreview.tsx @@ -0,0 +1,91 @@ +import React, { useEffect, useState } from 'react'; +import mammoth from 'mammoth'; + +export interface DocxPreviewProps { + blob?: Blob; + fileName?: string; +} + +export const DocxPreview: React.FC = ({ + blob, + fileName +}) => { + const [html, setHtml] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + if (!blob) { + setError('No file content'); + setLoading(false); + return; + } + + const convertDocx = async () => { + try { + setLoading(true); + setError(''); + + const arrayBuffer = await blob.arrayBuffer(); + const result = await mammoth.convertToHtml({ arrayBuffer }); + setHtml(result.value); + + // 显示转换消息(如果有) + if (result.messages && result.messages.length > 0) { + console.warn('DOCX conversion messages:', result.messages); + } + } catch (err) { + console.error('Failed to convert DOCX:', err); + setError('Failed to convert Word document'); + } finally { + setLoading(false); + } + }; + + convertDocx(); + }, [blob]); + + if (loading) { + return ( +
+
+
+ Converting document... +
+
+ ); + } + + if (error) { + return ( +
+
+

⚠️ {error}

+

Please download the file to view

+
+
+ ); + } + + if (!html) { + return ( +
+ No content available +
+ ); + } + + return ( +
+
+
+ ); +}; diff --git a/frontend/src/components/file-preview/ImagePreview.tsx b/frontend/src/components/file-preview/ImagePreview.tsx new file mode 100644 index 000000000..675abf4cb --- /dev/null +++ b/frontend/src/components/file-preview/ImagePreview.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +export interface ImagePreviewProps { + blobUrl?: string; + fileName?: string; + alt?: string; +} + +export const ImagePreview: React.FC = ({ + blobUrl, + fileName = 'Preview', + alt = 'Preview' +}) => { + if (!blobUrl) { + return ( +
+ No image available +
+ ); + } + + return ( +
+ {alt} +
+ ); +}; diff --git a/frontend/src/components/file-preview/MarkdownPreview.tsx b/frontend/src/components/file-preview/MarkdownPreview.tsx new file mode 100644 index 000000000..d4b6b3527 --- /dev/null +++ b/frontend/src/components/file-preview/MarkdownPreview.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +export interface MarkdownPreviewProps { + content?: string; + fileName?: string; +} + +export const MarkdownPreview: React.FC = ({ + content = '', + fileName +}) => { + if (!content) { + return ( +
+ No content available +
+ ); + } + + return ( +
+
+ + {children} + + ); + } + + if (match) { + return ( +
+
+                      
+                        {codeString}
+                      
+                    
+
+ ); + } + + return ( +
+                  
+                    {codeString}
+                  
+                
+ ); + }, + // 自定义链接样式 + a: ({ href, children }) => ( + + {children} + + ), + // 自定义表格样式 + table: ({ children }) => ( +
+ + {children} +
+
+ ), + thead: ({ children }) => ( + {children} + ), + th: ({ children }) => ( + + {children} + + ), + td: ({ children }) => ( + + {children} + + ), + }} + > + {content} +
+
+
+ ); +}; diff --git a/frontend/src/components/file-preview/PdfPreview.tsx b/frontend/src/components/file-preview/PdfPreview.tsx new file mode 100644 index 000000000..3ddea4689 --- /dev/null +++ b/frontend/src/components/file-preview/PdfPreview.tsx @@ -0,0 +1,159 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { Document, Page, pdfjs } from 'react-pdf'; +import { DownloadOutlined } from '@ant-design/icons'; + +// 配置 PDF.js worker - 使用本地文件 +pdfjs.GlobalWorkerOptions.workerSrc = '/pdfjs/pdf.worker.min.mjs'; + +export interface PdfPreviewProps { + blob?: Blob; + fileName?: string; +} + +export const PdfPreview: React.FC = ({ + blob, + fileName = 'document.pdf' +}) => { + const [numPages, setNumPages] = useState(0); + const [pageNumber, setPageNumber] = useState(1); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + // 使用 useMemo 创建稳定的 Blob 对象(带有正确的 MIME 类型) + const pdfBlob = useMemo(() => { + if (!blob) return null; + return new Blob([blob], { type: 'application/pdf' }); + }, [blob]); + + // 当 blob 变化时重置状态 + useEffect(() => { + if (blob) { + setLoading(true); + setError(''); + setPageNumber(1); + setNumPages(0); + } + }, [blob]); + + const onDocumentLoadSuccess = ({ numPages: pages }: { numPages: number }) => { + setNumPages(pages); + setPageNumber(1); + setLoading(false); + }; + + const onDocumentLoadError = (error: any) => { + setError(`Failed to load PDF: ${error?.message || 'Unknown error'}`); + setLoading(false); + }; + + const onSourceError = (error: any) => { + setError(`PDF source error: ${error?.message || 'Unknown error'}`); + setLoading(false); + }; + + const changePage = (offset: number) => { + setPageNumber((prevPageNumber) => prevPageNumber + offset); + }; + + const previousPage = () => changePage(-1); + const nextPage = () => changePage(1); + + const handleDownload = () => { + if (!blob) return; + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + if (!blob) { + return ( +
+ No PDF content available +
+ ); + } + + // 显示错误状态 + if (error) { + return ( +
+
+

⚠️ PDF Preview Failed

+

{error}

+ +
+
+ ); + } + + return ( +
+ {/* 工具栏 */} +
+
+ Page {pageNumber} of {numPages} +
+
+ + + +
+
+ + {/* PDF 预览区域 */} +
+ {loading && ( +
+
+
+ Loading PDF... +
+
+ )} + + + +
+
+ ); +}; diff --git a/frontend/src/components/file-preview/TextPreview.tsx b/frontend/src/components/file-preview/TextPreview.tsx new file mode 100644 index 000000000..c462346cc --- /dev/null +++ b/frontend/src/components/file-preview/TextPreview.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +export interface TextPreviewProps { + content?: string; + fileName?: string; +} + +export const TextPreview: React.FC = ({ + content = '', + fileName +}) => { + if (!content) { + return ( +
+ No content available +
+ ); + } + + return ( +
+
+        {content}
+      
+
+ ); +}; diff --git a/frontend/src/components/file-preview/index.tsx b/frontend/src/components/file-preview/index.tsx new file mode 100644 index 000000000..b430baa94 --- /dev/null +++ b/frontend/src/components/file-preview/index.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { ImagePreview } from './ImagePreview'; +import { MarkdownPreview } from './MarkdownPreview'; +import { DocxPreview } from './DocxPreview'; +import { PdfPreview } from './PdfPreview'; +import { TextPreview } from './TextPreview'; + +export interface FilePreviewProps { + fileName?: string; + content?: string; + blobUrl?: string; + blob?: Blob; + loading?: boolean; + error?: string; +} + +export const FilePreview: React.FC = ({ + fileName = '', + content, + blobUrl, + blob, + loading = false, + error +}) => { + const ext = fileName?.toLowerCase().split('.').pop() || ''; + + // 错误状态 + if (error) { + return ( +
+
+

⚠️ Preview Failed

+

{error}

+
+
+ ); + } + + // 加载状态 + if (loading) { + return ( +
+
+
+ Loading preview... +
+
+ ); + } + + // 根据文件扩展名选择预览器 + // 图片文件 + if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'].includes(ext)) { + return ; + } + + // Markdown 文件 + if (ext === 'md' || ext === 'markdown') { + return ; + } + + // Word 文档 + if (ext === 'docx') { + return ; + } + + // PDF 文档 + if (ext === 'pdf') { + return ; + } + + // 其他文件(纯文本) + return ; +}; + +export default FilePreview; diff --git a/frontend/src/index.css b/frontend/src/index.css index 7afc2691a..43988a80a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -116,3 +116,181 @@ white-space: nowrap; } } + +/* Markdown 内容样式 */ +.markdown-content { + /* 标题样式 */ + h1 { + font-size: 1.5rem; + font-weight: 700; + margin-top: 1.5rem; + margin-bottom: 1rem; + color: #1a202c; + border-bottom: 2px solid #e2e8f0; + padding-bottom: 0.5rem; + } + + h2 { + font-size: 1.25rem; + font-weight: 600; + margin-top: 1.25rem; + margin-bottom: 0.75rem; + color: #2d3748; + } + + h3 { + font-size: 1.125rem; + font-weight: 600; + margin-top: 1rem; + margin-bottom: 0.5rem; + color: #2d3748; + } + + h4, h5, h6 { + font-size: 1rem; + font-weight: 600; + margin-top: 0.75rem; + margin-bottom: 0.5rem; + color: #4a5568; + } + + /* 段落 */ + p { + margin-bottom: 1rem; + line-height: 1.7; + } + + /* 列表 */ + ul, ol { + margin-left: 1.5rem; + margin-bottom: 1rem; + } + + li { + margin-bottom: 0.5rem; + } + + /* 引用块 */ + blockquote { + border-left: 4px solid #3b82f6; + padding-left: 1rem; + margin: 1rem 0; + color: #64748b; + background-color: #f7fafc; + padding: 0.75rem 1rem; + border-radius: 0.375rem; + } + + /* 代码块 */ + code { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.875rem; + } + + pre { + background-color: #1e293b; + color: #e2e8f0; + padding: 1rem; + border-radius: 0.375rem; + overflow-x: auto; + margin-bottom: 1rem; + } + + pre code { + background-color: transparent; + padding: 0; + color: inherit; + } + + /* 表格 */ + table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; + } + + th, td { + padding: 0.5rem 1rem; + text-align: left; + border: 1px solid #e2e8f0; + } + + th { + background-color: #f7fafc; + font-weight: 600; + } + + /* 水平线 */ + hr { + border-color: #e2e8f0; + margin: 2rem 0; + } +} + +/* Word 文档转换内容样式 */ +.docx-content { + /* 段落 */ + p { + margin-bottom: 0.5em; + line-height: 1.6; + } + + /* 标题 */ + h1, h2, h3, h4, h5, h6 { + font-weight: 600; + margin-top: 1em; + margin-bottom: 0.5em; + } + + /* 列表 */ + ul, ol { + margin-left: 2em; + margin-bottom: 0.5em; + } + + /* 表格 */ + table { + border-collapse: collapse; + margin: 1em 0; + } + + td, th { + border: 1px solid #dfe2e5; + padding: 0.375rem 0.5rem; + } + + /* 代码 */ + code { + font-family: monospace; + background-color: #f3f4f6; + padding: 0.125rem 0.25rem; + border-radius: 0.25rem; + font-size: 0.875em; + } + + pre { + background-color: #1f2937; + color: #f9fafb; + padding: 1rem; + border-radius: 0.5rem; + overflow-x: auto; + } + + pre code { + background-color: transparent; + padding: 0; + } +} + +/* PDF 预览样式 */ +.pdf-preview { + .pdf-document { + canvas { + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + } + } + + .pdf-page { + margin: 0 auto; + } +} diff --git a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx index f8d22ac00..eb095ae04 100644 --- a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx +++ b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx @@ -3,6 +3,7 @@ import { formatBytes, formatDateTime } from "@/utils/unit"; import { Download, Trash2, Folder, File } from "lucide-react"; import { getDatasetTypeMap } from "../../dataset.const"; import DeleteConfirmModal from "@/components/DeleteConfirmModal"; +import FilePreview from "@/components/file-preview"; import { useTranslation } from "react-i18next"; import { useState } from "react"; @@ -33,8 +34,10 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) { previewFileName, previewContent, previewUrl, + previewBlob, previewFileDetail, previewLoading, + handlePreviewFile, setPreviewVisible, handleDeleteFile, handleDownloadFile, @@ -45,7 +48,6 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) { handleDeleteDirectory, handleRenameFile, handleRenameDirectory, - handlePreviewFile, } = filesOperation; // 文件列表多选配置 @@ -518,40 +520,19 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) { open={previewVisible} onCancel={() => setPreviewVisible(false)} footer={null} - width={1000} + width={1200} + styles={{ body: { padding: 0 } }} > -
+
{/* 左侧预览区域 */} -
- {previewLoading ? ( - - ) : previewUrl ? ( - {previewFileName} - ) : previewContent ? ( -
-                {previewContent}
-              
- ) : ( - - {t("dataManagement.detail.previewEmpty")} - - )} +
+
{/* 右侧文件信息(来自 t_dm_dataset_files) */} diff --git a/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts index 6b7dac99a..ee882ed57 100644 --- a/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts +++ b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts @@ -37,13 +37,14 @@ export function useFilesOperation(dataset: Dataset) { const [previewContent, setPreviewContent] = useState(""); const [previewFileName, setPreviewFileName] = useState(""); const [previewUrl, setPreviewUrl] = useState(); + const [previewBlob, setPreviewBlob] = useState(); const [previewFileDetail, setPreviewFileDetail] = useState(); const [previewLoading, setPreviewLoading] = useState(false); const fetchFiles = async (prefix?: string, current?, pageSize?) => { // 如果明确传了 prefix(包括空字符串),使用传入的值;否则使用当前 pagination.prefix const targetPrefix = prefix !== undefined ? prefix : (pagination.prefix || ''); - + const params: any = { page: current !== undefined ? current : pagination.current, size: pageSize !== undefined ? pageSize : pagination.pageSize, @@ -93,15 +94,34 @@ export function useFilesOperation(dataset: Dataset) { setSelectedFiles([]); // 清空选中状态 }; + const getFileExtension = (fileName?: string) => { + if (!fileName) return ''; + const parts = fileName.toLowerCase().split('.'); + return parts.length > 1 ? parts[parts.length - 1] : ''; + }; + const isImageFile = (fileName?: string, fileType?: string) => { const lowerType = (fileType || "").toLowerCase(); if (lowerType.includes("image")) return true; const name = (fileName || "").toLowerCase(); - return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"].some((ext) => + return [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"].some((ext) => name.endsWith(ext) ); }; + const isMarkdownFile = (fileName?: string) => { + const ext = getFileExtension(fileName); + return ext === 'md' || ext === 'markdown'; + }; + + const isDocxFile = (fileName?: string) => { + return getFileExtension(fileName) === 'docx'; + }; + + const isPdfFile = (fileName?: string) => { + return getFileExtension(fileName) === 'pdf'; + }; + const handlePreviewFile = async (file: any) => { if (!file || !file.id) return; const datasetId = dataset.id; @@ -110,7 +130,9 @@ export function useFilesOperation(dataset: Dataset) { setPreviewFileName(file.fileName || ""); setPreviewContent(""); setPreviewUrl(undefined); + setPreviewBlob(undefined); setPreviewFileDetail(undefined); + try { // 获取文件元信息(来自 t_dm_dataset_files) const prefix = pagination.prefix || ""; @@ -118,12 +140,28 @@ export function useFilesOperation(dataset: Dataset) { const detail = detailRes?.data || detailRes; setPreviewFileDetail(detail); - const image = isImageFile(detail?.fileName || file.fileName, detail?.fileType); + const fileName = detail?.fileName || file.fileName; const { blob, blobUrl } = await downloadFileByIdUsingGet(datasetId, prefix, file.id, file.fileName, "preview"); - if (image) { + // 图片文件 + if (isImageFile(fileName, detail?.fileType)) { setPreviewUrl(blobUrl); - } else { + } + // Markdown 文件 + else if (isMarkdownFile(fileName)) { + const text = await blob.text(); + setPreviewContent(text); + } + // Word 文档 + else if (isDocxFile(fileName)) { + setPreviewBlob(blob); + } + // PDF 文档 + else if (isPdfFile(fileName)) { + setPreviewBlob(blob); + } + // 其他文件(纯文本) + else { const text = await blob.text(); setPreviewContent(text); } @@ -223,9 +261,10 @@ export function useFilesOperation(dataset: Dataset) { setPreviewVisible, previewContent, previewFileName, - previewUrl, - previewFileDetail, - previewLoading, + previewUrl, + previewBlob, + previewFileDetail, + previewLoading, setPreviewContent, setPreviewFileName, fetchFiles, diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index c80f663ef..5e5aff216 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -433,17 +433,13 @@ class Request { // 使用fetch const response = await fetch(fullURL, processedConfig); - // 执行响应拦截器 - const processedResponse = await this.executeResponseInterceptors( - response, - processedConfig - ); - - if (!processedResponse.ok) { - throw new Error(`HTTP error! status: ${processedResponse.status}`); + // 文件下载不需要执行响应拦截器(因为响应是二进制数据,不是JSON) + // 直接检查响应状态 + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } - blob = await processedResponse.blob(); + blob = await response.blob(); name = name || response.headers.get("Content-Disposition")?.split("filename=")[1] || From 6ee8c3ef110ce28d5e6cf1120ffd404b53568554 Mon Sep 17 00:00:00 2001 From: MoeexT Date: Fri, 6 Mar 2026 15:43:30 +0800 Subject: [PATCH 2/6] :fire: remove debug code --- .../src/components/file-preview/DocxPreview.tsx | 11 ----------- frontend/src/index.css | 17 +++-------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/file-preview/DocxPreview.tsx b/frontend/src/components/file-preview/DocxPreview.tsx index eb2cd2d8a..2249aac53 100644 --- a/frontend/src/components/file-preview/DocxPreview.tsx +++ b/frontend/src/components/file-preview/DocxPreview.tsx @@ -29,13 +29,7 @@ export const DocxPreview: React.FC = ({ const arrayBuffer = await blob.arrayBuffer(); const result = await mammoth.convertToHtml({ arrayBuffer }); setHtml(result.value); - - // 显示转换消息(如果有) - if (result.messages && result.messages.length > 0) { - console.warn('DOCX conversion messages:', result.messages); - } } catch (err) { - console.error('Failed to convert DOCX:', err); setError('Failed to convert Word document'); } finally { setLoading(false); @@ -80,11 +74,6 @@ export const DocxPreview: React.FC = ({
); diff --git a/frontend/src/index.css b/frontend/src/index.css index 43988a80a..1753f0fb1 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -229,10 +229,12 @@ /* Word 文档转换内容样式 */ .docx-content { + color: #333; + line-height: 1.6; + /* 段落 */ p { margin-bottom: 0.5em; - line-height: 1.6; } /* 标题 */ @@ -281,16 +283,3 @@ padding: 0; } } - -/* PDF 预览样式 */ -.pdf-preview { - .pdf-document { - canvas { - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - } - } - - .pdf-page { - margin: 0 auto; - } -} From 88526861d4a2ba5a355059788bb0862553de3063 Mon Sep 17 00:00:00 2001 From: MoeexT Date: Fri, 6 Mar 2026 16:21:05 +0800 Subject: [PATCH 3/6] :tada: add pdf/code/img/md preview --- .../components/file-preview/CodePreview.tsx | 111 ++++++++++++++++++ .../components/file-preview/ImagePreview.tsx | 3 - .../components/file-preview/JsonPreview.tsx | 69 +++++++++++ .../file-preview/MarkdownPreview.tsx | 30 +++-- .../components/file-preview/PdfPreview.tsx | 5 +- .../components/file-preview/TextPreview.tsx | 4 +- .../src/components/file-preview/index.tsx | 17 ++- frontend/src/index.css | 1 + 8 files changed, 219 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/file-preview/CodePreview.tsx create mode 100644 frontend/src/components/file-preview/JsonPreview.tsx diff --git a/frontend/src/components/file-preview/CodePreview.tsx b/frontend/src/components/file-preview/CodePreview.tsx new file mode 100644 index 000000000..22e69b79e --- /dev/null +++ b/frontend/src/components/file-preview/CodePreview.tsx @@ -0,0 +1,111 @@ +import React, { useMemo } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +export interface CodePreviewProps { + content?: string; + fileName?: string; +} + +// 文件扩展名到 Prism 语言映射 +const LANGUAGE_MAP: Record = { + // JavaScript/TypeScript + 'js': 'javascript', + 'jsx': 'jsx', + 'ts': 'typescript', + 'tsx': 'tsx', + 'mjs': 'javascript', + + // Python + 'py': 'python', + 'pyw': 'python', + + // Java + 'java': 'java', + + // C/C++ + 'c': 'c', + 'cpp': 'cpp', + 'cc': 'cpp', + 'cxx': 'cpp', + 'h': 'c', + 'hpp': 'cpp', + + // C# + 'cs': 'csharp', + + // Go + 'go': 'go', + + // Rust + 'rs': 'rust', + + // PHP + 'php': 'php', + + // Ruby + 'rb': 'ruby', + + // Shell + 'sh': 'bash', + 'bash': 'bash', + 'zsh': 'bash', + + // SQL + 'sql': 'sql', + + // HTML/CSS + 'html': 'html', + 'htm': 'html', + 'css': 'css', + 'scss': 'scss', + 'less': 'less', + + // XML + 'xml': 'xml', + + // YAML + 'yaml': 'yaml', + 'yml': 'yaml', + + // 其他 + 'txt': 'clike', // 通用类 C 语言 +}; + +export const CodePreview: React.FC = ({ + content = '', + fileName +}) => { + // 根据文件扩展名检测语言 + const language = useMemo(() => { + if (!fileName) return 'clike'; + const ext = fileName.toLowerCase().split('.').pop() || ''; + return LANGUAGE_MAP[ext] || 'clike'; + }, [fileName]); + + if (!content) { + return ( +
+ No content available +
+ ); + } + + return ( +
+ + {content} + +
+ ); +}; diff --git a/frontend/src/components/file-preview/ImagePreview.tsx b/frontend/src/components/file-preview/ImagePreview.tsx index 675abf4cb..bb0b54a08 100644 --- a/frontend/src/components/file-preview/ImagePreview.tsx +++ b/frontend/src/components/file-preview/ImagePreview.tsx @@ -2,13 +2,11 @@ import React from 'react'; export interface ImagePreviewProps { blobUrl?: string; - fileName?: string; alt?: string; } export const ImagePreview: React.FC = ({ blobUrl, - fileName = 'Preview', alt = 'Preview' }) => { if (!blobUrl) { @@ -25,7 +23,6 @@ export const ImagePreview: React.FC = ({ src={blobUrl} alt={alt} className="max-w-full max-h-[600px] object-contain" - style={{ maxHeight: '600px' }} />
); diff --git a/frontend/src/components/file-preview/JsonPreview.tsx b/frontend/src/components/file-preview/JsonPreview.tsx new file mode 100644 index 000000000..46321d4b8 --- /dev/null +++ b/frontend/src/components/file-preview/JsonPreview.tsx @@ -0,0 +1,69 @@ +import React, { useMemo } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; + +export interface JsonPreviewProps { + content?: string; + fileName?: string; +} + +export const JsonPreview: React.FC = ({ + content = '', + fileName +}) => { + // 尝试解析并格式化 JSON + const { formattedContent, error } = useMemo(() => { + if (!content) { + return { formattedContent: '', error: 'No content available' }; + } + + try { + const data = JSON.parse(content); + const formatted = JSON.stringify(data, null, 2); + return { formattedContent: formatted, error: null }; + } catch (err) { + return { + formattedContent: '', + error: err instanceof Error ? err.message : 'Invalid JSON format' + }; + } + }, [content]); + + if (error) { + return ( +
+
+

⚠️ JSON Parse Failed

+

{error}

+

Displaying as plain text

+
+
+ ); + } + + if (!formattedContent) { + return ( +
+ No content available +
+ ); + } + + return ( +
+ + {formattedContent} + +
+ ); +}; diff --git a/frontend/src/components/file-preview/MarkdownPreview.tsx b/frontend/src/components/file-preview/MarkdownPreview.tsx index d4b6b3527..e167b544f 100644 --- a/frontend/src/components/file-preview/MarkdownPreview.tsx +++ b/frontend/src/components/file-preview/MarkdownPreview.tsx @@ -1,6 +1,8 @@ import React from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; export interface MarkdownPreviewProps { content?: string; @@ -25,8 +27,8 @@ export const MarkdownPreview: React.FC = ({ = ({ if (match) { return ( -
-
-                      
-                        {codeString}
-                      
-                    
-
+ + {codeString} + ); } @@ -58,7 +66,7 @@ export const MarkdownPreview: React.FC = ({ ); }, - // 自定义链接样式 + // 链接 a: ({ href, children }) => ( = ({ {children} ), - // 自定义表格样式 + // 表格 table: ({ children }) => (
diff --git a/frontend/src/components/file-preview/PdfPreview.tsx b/frontend/src/components/file-preview/PdfPreview.tsx index 3ddea4689..263796bcd 100644 --- a/frontend/src/components/file-preview/PdfPreview.tsx +++ b/frontend/src/components/file-preview/PdfPreview.tsx @@ -2,8 +2,9 @@ import React, { useState, useEffect, useMemo } from 'react'; import { Document, Page, pdfjs } from 'react-pdf'; import { DownloadOutlined } from '@ant-design/icons'; -// 配置 PDF.js worker - 使用本地文件 -pdfjs.GlobalWorkerOptions.workerSrc = '/pdfjs/pdf.worker.min.mjs'; +// 配置 PDF.js worker - 使用 Vite 的 ?url 语法处理 npm 包 +import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'; +pdfjs.GlobalWorkerOptions.workerSrc = workerUrl; export interface PdfPreviewProps { blob?: Blob; diff --git a/frontend/src/components/file-preview/TextPreview.tsx b/frontend/src/components/file-preview/TextPreview.tsx index c462346cc..35b0cfc78 100644 --- a/frontend/src/components/file-preview/TextPreview.tsx +++ b/frontend/src/components/file-preview/TextPreview.tsx @@ -2,12 +2,10 @@ import React from 'react'; export interface TextPreviewProps { content?: string; - fileName?: string; } export const TextPreview: React.FC = ({ - content = '', - fileName + content = '' }) => { if (!content) { return ( diff --git a/frontend/src/components/file-preview/index.tsx b/frontend/src/components/file-preview/index.tsx index b430baa94..fa773c0c0 100644 --- a/frontend/src/components/file-preview/index.tsx +++ b/frontend/src/components/file-preview/index.tsx @@ -3,6 +3,8 @@ import { ImagePreview } from './ImagePreview'; import { MarkdownPreview } from './MarkdownPreview'; import { DocxPreview } from './DocxPreview'; import { PdfPreview } from './PdfPreview'; +import { JsonPreview } from './JsonPreview'; +import { CodePreview } from './CodePreview'; import { TextPreview } from './TextPreview'; export interface FilePreviewProps { @@ -51,7 +53,7 @@ export const FilePreview: React.FC = ({ // 根据文件扩展名选择预览器 // 图片文件 if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'].includes(ext)) { - return ; + return ; } // Markdown 文件 @@ -69,8 +71,19 @@ export const FilePreview: React.FC = ({ return ; } + // JSON 文件 + if (ext === 'json') { + return ; + } + + // 代码文件(JavaScript, Python, Java, 等) + const codeExtensions = ['js', 'jsx', 'ts', 'tsx', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'php', 'rb', 'sh', 'sql', 'html', 'css', 'scss', 'xml', 'yaml', 'yml']; + if (codeExtensions.includes(ext)) { + return ; + } + // 其他文件(纯文本) - return ; + return ; }; export default FilePreview; diff --git a/frontend/src/index.css b/frontend/src/index.css index 1753f0fb1..8d7d6522f 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -283,3 +283,4 @@ padding: 0; } } + From 1ecfd683c49a5aa6a13ef86863e609b4aeca22ab Mon Sep 17 00:00:00 2001 From: MoeexT Date: Fri, 6 Mar 2026 16:25:04 +0800 Subject: [PATCH 4/6] :tada: add csv preview --- .../components/file-preview/CsvPreview.tsx | 139 ++++++++++++++++++ .../src/components/file-preview/index.tsx | 6 + 2 files changed, 145 insertions(+) create mode 100644 frontend/src/components/file-preview/CsvPreview.tsx diff --git a/frontend/src/components/file-preview/CsvPreview.tsx b/frontend/src/components/file-preview/CsvPreview.tsx new file mode 100644 index 000000000..c5ab19cc3 --- /dev/null +++ b/frontend/src/components/file-preview/CsvPreview.tsx @@ -0,0 +1,139 @@ +import React, { useMemo } from 'react'; +import { Table } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; + +export interface CsvPreviewProps { + content?: string; + fileName?: string; +} + +interface DataType { + key: number; + [key: string]: string; +} + +export const CsvPreview: React.FC = ({ + content = '', + fileName +}) => { + // 解析 CSV + const { headers, rows, error } = useMemo(() => { + if (!content) { + return { headers: [], rows: [], error: 'No content available' }; + } + + try { + const lines = content.split('\n').filter(line => line.trim()); + + if (lines.length === 0) { + return { headers: [], rows: [], error: 'Empty file' }; + } + + // 解析 CSV 处理引号 + const parseCSVLine = (line: string): string[] => { + const result: string[] = []; + let current = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + const nextChar = line[i + 1]; + + if (char === '"') { + if (inQuotes && nextChar === '"') { + // 转义的引号 + current += '"'; + i++; // 跳过下一个引号 + } else { + inQuotes = !inQuotes; + } + } else if (char === ',' && !inQuotes) { + result.push(current); + current = ''; + } else { + current += char; + } + } + + result.push(current); + return result.map(cell => cell.trim()); + }; + + const headers = parseCSVLine(lines[0]); + const dataRows = lines.slice(1).map((line, idx) => { + const values = parseCSVLine(line); + const row: DataType = { + key: idx, + }; + headers.forEach((header, index) => { + row[header] = values[index] || ''; + }); + return row; + }); + + return { headers, rows: dataRows, error: null }; + } catch (err) { + return { + headers: [], + rows: [], + error: err instanceof Error ? err.message : 'Failed to parse CSV' + }; + } + }, [content]); + + if (error) { + return ( +
+
+

⚠️ CSV Parse Failed

+

{error}

+

Displaying as plain text

+
+
+ ); + } + + if (headers.length === 0) { + return ( +
+ No content available +
+ ); + } + + // 动态生成列定义 + const columns: ColumnsType = headers.map(header => ({ + title: header, + dataIndex: header, + key: header, + width: 150, + ellipsis: true, + render: (text: string) => text || '-' + })); + + return ( +
+ {/* 文件信息 */} + {fileName && ( +
+ + 📄 {fileName} ({rows.length} rows × {headers.length} columns) + +
+ )} + + {/* 表格 */} +
+
record.key} + /> + + + ); +}; diff --git a/frontend/src/components/file-preview/index.tsx b/frontend/src/components/file-preview/index.tsx index fa773c0c0..b2825681b 100644 --- a/frontend/src/components/file-preview/index.tsx +++ b/frontend/src/components/file-preview/index.tsx @@ -4,6 +4,7 @@ import { MarkdownPreview } from './MarkdownPreview'; import { DocxPreview } from './DocxPreview'; import { PdfPreview } from './PdfPreview'; import { JsonPreview } from './JsonPreview'; +import { CsvPreview } from './CsvPreview'; import { CodePreview } from './CodePreview'; import { TextPreview } from './TextPreview'; @@ -76,6 +77,11 @@ export const FilePreview: React.FC = ({ return ; } + // CSV 文件 + if (ext === 'csv') { + return ; + } + // 代码文件(JavaScript, Python, Java, 等) const codeExtensions = ['js', 'jsx', 'ts', 'tsx', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs', 'php', 'rb', 'sh', 'sql', 'html', 'css', 'scss', 'xml', 'yaml', 'yml']; if (codeExtensions.includes(ext)) { From 4f7591bd2d763ee047443daa5cc7e1d98c087169 Mon Sep 17 00:00:00 2001 From: MoeexT Date: Tue, 10 Mar 2026 17:25:03 +0800 Subject: [PATCH 5/6] :lipstick: perf tag UI --- frontend/src/components/DetailHeader.tsx | 232 +++++++++++------- .../Detail/components/Overview.tsx | 10 +- .../Detail/useFilesOperation.ts | 5 +- 3 files changed, 147 insertions(+), 100 deletions(-) diff --git a/frontend/src/components/DetailHeader.tsx b/frontend/src/components/DetailHeader.tsx index 24a69642f..bf2ee7ad1 100644 --- a/frontend/src/components/DetailHeader.tsx +++ b/frontend/src/components/DetailHeader.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useRef, useState } from "react"; +import React, { useLayoutEffect, useEffect, useRef, useState, useCallback, useMemo } from "react"; import { Database } from "lucide-react"; -import { Card, Button, Tag, Tooltip, Popconfirm } from "antd"; +import { Card, Button, Tag, Tooltip, Popconfirm, Popover } from "antd"; import type { ItemType } from "antd/es/menu/interface"; import AddTagPopover from "./AddTagPopover"; import ActionDropdown from "./ActionDropdown"; @@ -40,119 +40,169 @@ interface DetailHeaderProps { // 标签单行渲染组件 const TagsInline = ({ tags }: { tags: Array<{ id: number; name: string; color: string } | string> }) => { - const [visibleTags, setVisibleTags] = useState([]); - const [hiddenCount, setHiddenCount] = useState(0); const containerRef = useRef(null); + const tagsAreaRef = useRef(null); + const [visibleTags, setVisibleTags] = useState([]); + const [hiddenCount, setHiddenCount] = useState(0); + const [initialized, setInitialized] = useState(false); - useEffect(() => { - if (!tags || tags.length === 0) return; + const calculateVisibleTags = useCallback(() => { + if (!tags || tags.length === 0) { + setVisibleTags([]); + setHiddenCount(0); + return; + } - const calculateVisibleTags = () => { - if (!containerRef.current) return; + if (!containerRef.current) return; - const container = containerRef.current; + // 创建测量容器 + const measureContainer = document.createElement("div"); + measureContainer.style.position = "absolute"; + measureContainer.style.visibility = "hidden"; + measureContainer.style.pointerEvents = "none"; + measureContainer.style.display = "inline-flex"; + measureContainer.style.alignItems = "center"; + measureContainer.style.gap = "4px"; + measureContainer.style.whiteSpace = "nowrap"; + measureContainer.style.zIndex = "-1"; + document.body.appendChild(measureContainer); - // 创建一个隐藏的测量容器 - const measureContainer = document.createElement("div"); - measureContainer.style.position = "absolute"; - measureContainer.style.visibility = "hidden"; - measureContainer.style.pointerEvents = "none"; - measureContainer.style.top = "0"; - measureContainer.style.left = "0"; - measureContainer.style.display = "inline-flex"; - measureContainer.style.alignItems = "center"; - measureContainer.style.gap = "4px"; - measureContainer.style.whiteSpace = "nowrap"; - measureContainer.style.flexWrap = "nowrap"; - measureContainer.style.zIndex = "-1"; + // 测量 "+n" 标签 + const plusTag = document.createElement("span"); + plusTag.className = "ant-tag ant-tag-default"; + plusTag.textContent = "+99"; + measureContainer.appendChild(plusTag); + const plusWidth = plusTag.offsetWidth; - // 创建 "+n" 标签来测量 - const plusTag = document.createElement("span"); - plusTag.className = "ant-tag ant-tag-default cursor-pointer bg-gray-100 border-gray-300 text-gray-600 hover:bg-gray-200"; - plusTag.textContent = "+99"; - measureContainer.appendChild(plusTag); - const plusWidth = plusTag.offsetWidth; + // 总容器宽度 + const totalWidth = 300; + // 预留间距和"+n"的空间 + const availableWidth = totalWidth - plusWidth - 12; - // 暂时插入到 DOM 中测量 - if (container.parentElement) { - container.parentElement.style.position = "relative"; - container.parentElement.appendChild(measureContainer); + // 先计算所有标签的总宽度 + let tagsTotalWidth = 0; + tags.forEach((tag) => { + const tagEl = document.createElement("span"); + tagEl.className = "ant-tag ant-tag-default"; + const tagName = typeof tag === "string" ? tag : tag.name; + tagEl.textContent = tagName; + measureContainer.appendChild(tagEl); + tagsTotalWidth = measureContainer.offsetWidth; + }); - const containerWidth = container.offsetWidth; - const availableWidth = containerWidth - 8; // 安全边距 + // 如果所有标签都能放下,直接显示全部 + if (tagsTotalWidth <= availableWidth) { + setVisibleTags(tags); + setHiddenCount(0); + if (measureContainer.parentNode) { + measureContainer.parentNode.removeChild(measureContainer); + } + return; + } - let visibleCount = 0; + // 如果放不下,需要计算可见标签数量 + while (measureContainer.firstChild) { + measureContainer.removeChild(measureContainer.firstChild); + } - tags.forEach((tag, index) => { - const tagEl = document.createElement("span"); - tagEl.className = "ant-tag ant-tag-default shrink-0"; - const tagName = typeof tag === "string" ? tag : tag.name; - const tagColor = typeof tag === "string" ? undefined : tag.color; - if (tagColor) tagEl.style.color = tagColor; - tagEl.textContent = tagName; - measureContainer.appendChild(tagEl); + let visibleCount = 0; - // 测量当前容器宽度 - const currentWidth = measureContainer.offsetWidth; - const needsPlus = index < tags.length - 1; - const targetWidth = availableWidth - (needsPlus ? plusWidth : 0); + tags.forEach((tag, index) => { + const tagEl = document.createElement("span"); + tagEl.className = "ant-tag ant-tag-default"; + const tagName = typeof tag === "string" ? tag : tag.name; + tagEl.textContent = tagName; + measureContainer.appendChild(tagEl); - if (currentWidth <= targetWidth) { - visibleCount++; - } else { - // 移除这个标签,因为它放不下 - measureContainer.removeChild(tagEl); - return false; // 停止循环 - } + const currentWidth = measureContainer.offsetWidth; - return true; - }); + if (currentWidth <= availableWidth) { + visibleCount++; + } else { + measureContainer.removeChild(tagEl); + return false; + } + return true; + }); - // 移除测量容器 - container.parentElement.removeChild(measureContainer); + if (measureContainer.parentNode) { + measureContainer.parentNode.removeChild(measureContainer); + } - setVisibleTags(tags.slice(0, visibleCount)); - setHiddenCount(tags.length - visibleCount); - } - }; + setVisibleTags(tags.slice(0, visibleCount)); + setHiddenCount(tags.length - visibleCount); + }, [tags]); - const timer = setTimeout(calculateVisibleTags, 0); - const handleResize = () => calculateVisibleTags(); + useLayoutEffect(() => { + calculateVisibleTags(); + setInitialized(true); + }, [calculateVisibleTags]); + useLayoutEffect(() => { + const handleResize = () => calculateVisibleTags(); window.addEventListener("resize", handleResize); - return () => { - clearTimeout(timer); - window.removeEventListener("resize", handleResize); - }; - }, [tags]); + return () => window.removeEventListener("resize", handleResize); + }, [calculateVisibleTags]); if (!tags || tags.length === 0) return null; + const displayTags = initialized ? visibleTags : tags; + + // 获取隐藏的标签名称 + const hiddenTagNames = useMemo(() => { + if (!tags || hiddenCount === 0) return []; + const visibleSet = new Set(visibleTags.map(t => typeof t === 'string' ? t : t.name)); + return tags.filter(t => { + const name = typeof t === 'string' ? t : t.name; + return !visibleSet.has(name); + }).map(t => typeof t === 'string' ? t : t.name); + }, [tags, visibleTags, hiddenCount]); + return ( -
- {visibleTags.map((tag, index) => { - const tagName = typeof tag === "string" ? tag : tag.name; - const tagColor = typeof tag === "string" ? undefined : tag.color; - return ( - - {tagName} - - ); - })} - {hiddenCount > 0 && ( - - +
+
+ {displayTags.map((tag, index) => { + const tagName = typeof tag === "string" ? tag : tag.name; + const tagColor = typeof tag === "string" ? undefined : tag.color; + return ( + + {tagName} + + ); + })} +
+ {initialized && hiddenCount > 0 && ( + +
+ {hiddenTagNames.map((name, i) => ( + + {name} + + ))} +
+
+ } + title="更多标签" + trigger="hover" + placement="topLeft" + > + +{hiddenCount} -
+ )}
); diff --git a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx index eb095ae04..ff1853f03 100644 --- a/frontend/src/pages/DataManagement/Detail/components/Overview.tsx +++ b/frontend/src/pages/DataManagement/Detail/components/Overview.tsx @@ -54,11 +54,6 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) { const rowSelection = { onChange: (selectedRowKeys: React.Key[], selectedRows: any[]) => { setSelectedFiles(selectedRowKeys as number[]); - console.log( - `selectedRowKeys: ${selectedRowKeys}`, - "selectedRows: ", - selectedRows - ); }, }; @@ -505,8 +500,11 @@ export default function Overview({ dataset, filesOperation, fetchDataset }) { // rowSelection={rowSelection} scroll={{ x: "max-content", y: 600 }} pagination={{ - ...pagination, + current: pagination.current, + pageSize: pagination.pageSize, + total: pagination.total, showTotal: (total) => t("dataManagement.detail.totalItems", { total }), + showSizeChanger: true, onChange: (page, pageSize) => { filesOperation.fetchFiles(filesOperation.pagination.prefix, page, pageSize); } diff --git a/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts index ee882ed57..06be5bc78 100644 --- a/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts +++ b/frontend/src/pages/DataManagement/Detail/useFilesOperation.ts @@ -232,7 +232,7 @@ export function useFilesOperation(dataset: Dataset) { try { await deleteDatasetFileUsingDelete(dataset.id, file.id, directoryPath); } catch (e) { - console.error("删除文件失败", file, e); + // Continue deleting other files even if one fails } } @@ -247,7 +247,7 @@ export function useFilesOperation(dataset: Dataset) { try { await deleteDirectoryUsingDelete(dataset.id, directoryPath); } catch (e) { - console.error("删除目录失败", directoryPath, e); + // Directory deletion failed, may still have contents } }; @@ -305,7 +305,6 @@ export function useFilesOperation(dataset: Dataset) { await fetchFiles(currentPrefix, 1, pagination.pageSize); message.success({ content: `文件夹 ${directoryName} 已删除` }); } catch (error) { - console.error("删除文件夹失败", error); message.error({ content: `文件夹 ${directoryName} 删除失败` }); } }, From aef5c152ae0b323ab01e693c542db7f87592c015 Mon Sep 17 00:00:00 2001 From: MoeexT Date: Tue, 10 Mar 2026 17:46:57 +0800 Subject: [PATCH 6/6] :lipstick: perf tag UI --- frontend/src/components/DetailHeader.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/DetailHeader.tsx b/frontend/src/components/DetailHeader.tsx index bf2ee7ad1..001c8b474 100644 --- a/frontend/src/components/DetailHeader.tsx +++ b/frontend/src/components/DetailHeader.tsx @@ -75,9 +75,10 @@ const TagsInline = ({ tags }: { tags: Array<{ id: number; name: string; color: s const plusWidth = plusTag.offsetWidth; // 总容器宽度 - const totalWidth = 300; - // 预留间距和"+n"的空间 - const availableWidth = totalWidth - plusWidth - 12; + const totalWidth = 450; + // 预留"+n"标签的完整空间(使用更保守的估计) + // "+n"标签的实际宽度 ≈ 35-50px,预留 60px 确保安全 + const availableWidth = totalWidth - 60; // 先计算所有标签的总宽度 let tagsTotalWidth = 0; @@ -159,7 +160,7 @@ const TagsInline = ({ tags }: { tags: Array<{ id: number; name: string; color: s }, [tags, visibleTags, hiddenCount]); return ( -
+