diff --git a/app/package.json b/app/package.json index cebace3..0f09fb6 100644 --- a/app/package.json +++ b/app/package.json @@ -18,19 +18,21 @@ "clsx": "^2.1.1", "echarts": "^5.6.0", "lucide-react": "^0.475.0", + "mammoth": "^1.13.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.0", - "tailwind-merge": "^3.3.0" + "tailwind-merge": "^3.3.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@tailwindcss/vite": "^4.1.0", + "@tauri-apps/cli": "^2.5.0", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "@vitejs/plugin-react": "^4.5.0", "tailwindcss": "^4.1.0", "typescript": "^5.8.0", - "vite": "^6.3.5", - "@tauri-apps/cli": "^2.5.0" + "vite": "^6.3.5" } } diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 18c260d..c9f525d 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: lucide-react: specifier: ^0.475.0 version: 0.475.0(react@19.3.1) + mammoth: + specifier: ^1.13.0 + version: 1.13.0 react: specifier: ^19.1.0 version: 19.3.1 @@ -38,6 +41,9 @@ importers: tailwind-merge: specifier: ^3.3.0 version: 3.7.0 + xlsx: + specifier: ^0.18.5 + version: 0.18.5 devDependencies: '@tailwindcss/vite': specifier: ^4.1.0 @@ -679,6 +685,20 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} + engines: {node: '>=10.0.0'} + + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.11.28: resolution: {integrity: sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==} engines: {node: '>=6.0.0'} @@ -692,6 +712,10 @@ packages: caniuse-lite@1.0.30001816: resolution: {integrity: sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -699,6 +723,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -706,6 +734,14 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -722,6 +758,12 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dingbat-to-unicode@1.0.2: + resolution: {integrity: sha512-R0bRerYzy/EZP6QzD3Hkl6JjUDA1Mnn+pd928w0nWjPzihnbAR6dcjGJGiRyznj5E4S1duEKzWrTqw02DPcdzg==} + + duck@0.1.12: + resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==} + echarts@5.6.0: resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} @@ -750,6 +792,10 @@ packages: picomatch: optional: true + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -762,6 +808,15 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -779,6 +834,12 @@ packages: engines: {node: '>=6'} hasBin: true + jszip@3.10.2: + resolution: {integrity: sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -853,6 +914,9 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lop@0.4.2: + resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -864,6 +928,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mammoth@1.13.0: + resolution: {integrity: sha512-2cawcYFcP7ISfXbBcL9lmVgTNWo88v/52ND1gnywHtVT/pX0//puPnf0iMPV9ZgmuvNOE+zkIhDgZ9M2c2dDDg==} + engines: {node: '>=12.0.0'} + hasBin: true + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -876,6 +945,12 @@ packages: resolution: {integrity: sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==} engines: {node: '>=18'} + option@0.2.4: + resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -887,6 +962,9 @@ packages: resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} engines: {node: ^10 || ^12 || >=14} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + react-dom@19.3.1: resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==} peerDependencies: @@ -917,11 +995,17 @@ packages: resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + rollup@4.64.7: resolution: {integrity: sha512-aCiJONoZp0lNGJ6sYqspLkh6UXaWEey/7Ko2TZyXQ4kgzAhDUxAgbt/oulXKmD+zFHXw1k+RF6ZZWOrjp7DZnQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + scheduler@0.28.0: resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} @@ -932,10 +1016,23 @@ packages: set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + source-map-js@1.2.2: resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + tailwind-merge@3.7.0: resolution: {integrity: sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==} @@ -958,12 +1055,18 @@ packages: engines: {node: '>=14.17'} hasBin: true + underscore@1.13.8: + resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} + update-browserslist-db@1.3.4: resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite@6.4.4: resolution: {integrity: sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1004,6 +1107,23 @@ packages: yaml: optional: true + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + + xmlbuilder@10.1.1: + resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} + engines: {node: '>=4.0'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1468,6 +1588,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@xmldom/xmldom@0.8.15': {} + + adler-32@1.3.1: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + base64-js@1.5.1: {} + baseline-browser-mapping@2.11.28: {} browserslist@4.29.3: @@ -1480,16 +1610,27 @@ snapshots: caniuse-lite@1.0.30001816: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 clsx@2.1.1: {} + codepage@1.15.0: {} + convert-source-map@2.0.0: {} cookie@1.1.1: {} + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + csstype@3.2.3: {} debug@4.4.3: @@ -1498,6 +1639,12 @@ snapshots: detect-libc@2.1.2: {} + dingbat-to-unicode@1.0.2: {} + + duck@0.1.12: + dependencies: + underscore: 1.13.8 + echarts@5.6.0: dependencies: tslib: 2.3.0 @@ -1545,6 +1692,8 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + frac@1.1.2: {} + fsevents@2.3.3: optional: true @@ -1552,6 +1701,12 @@ snapshots: graceful-fs@4.2.11: {} + immediate@3.0.6: {} + + inherits@2.0.4: {} + + isarray@1.0.0: {} + jiti@2.7.0: {} js-tokens@4.0.0: {} @@ -1560,6 +1715,17 @@ snapshots: json5@2.2.3: {} + jszip@3.10.2: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.32.0: optional: true @@ -1609,6 +1775,12 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lop@0.4.2: + dependencies: + duck: 0.1.12 + option: 0.2.4 + underscore: 1.13.8 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -1621,12 +1793,27 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + mammoth@1.13.0: + dependencies: + '@xmldom/xmldom': 0.8.15 + argparse: 1.0.10 + base64-js: 1.5.1 + dingbat-to-unicode: 1.0.2 + jszip: 3.10.2 + lop: 0.4.2 + underscore: 1.13.8 + xmlbuilder: 10.1.1 + ms@2.1.3: {} nanoid@3.3.20: {} node-releases@2.0.59: {} + option@0.2.4: {} + + pako@1.0.11: {} + picocolors@1.1.1: {} picomatch@4.0.7: {} @@ -1637,6 +1824,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.2 + process-nextick-args@2.0.1: {} + react-dom@19.3.1(react@19.3.1): dependencies: react: 19.3.1 @@ -1660,6 +1849,16 @@ snapshots: react@19.3.1: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + rollup@4.64.7: dependencies: '@types/estree': 1.0.9 @@ -1692,14 +1891,28 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.64.7 fsevents: 2.3.3 + safe-buffer@5.1.2: {} + scheduler@0.28.0: {} semver@6.3.1: {} set-cookie-parser@2.7.2: {} + setimmediate@1.0.5: {} + source-map-js@1.2.2: {} + sprintf-js@1.0.3: {} + + ssf@0.11.2: + dependencies: + frac: 1.1.2 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + tailwind-merge@3.7.0: {} tailwindcss@4.3.3: {} @@ -1715,12 +1928,16 @@ snapshots: typescript@5.9.3: {} + underscore@1.13.8: {} + update-browserslist-db@1.3.4(browserslist@4.29.3): dependencies: browserslist: 4.29.3 escalade: 3.2.0 picocolors: 1.1.1 + util-deprecate@1.0.2: {} + vite@6.4.4(jiti@2.7.0)(lightningcss@1.32.0): dependencies: esbuild: 0.25.12 @@ -1734,6 +1951,22 @@ snapshots: jiti: 2.7.0 lightningcss: 1.32.0 + wmf@1.0.2: {} + + word@0.3.0: {} + + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + + xmlbuilder@10.1.1: {} + yallist@3.1.1: {} zrender@5.6.1: diff --git a/app/src/components/common/AttachmentPreview.tsx b/app/src/components/common/AttachmentPreview.tsx new file mode 100644 index 0000000..e2b384e --- /dev/null +++ b/app/src/components/common/AttachmentPreview.tsx @@ -0,0 +1,132 @@ +import { useEffect, useState } from 'react'; +import { Download, Loader2 } from 'lucide-react'; +import { Button, Dialog } from '@/components/ui'; +import { readFull, b64ToObjectUrl } from '@/lib/att-cache'; +import type { Attachment } from '@/lib/types'; + +export type AttKind = 'image' | 'pdf' | 'text' | 'audio' | 'video' | 'docx' | 'xlsx' | 'other'; + +export function attKind(a: { file_name: string; mime?: string | null }): AttKind { + const mime = (a.mime || '').toLowerCase(); + const ext = (a.file_name.split('.').pop() || '').toLowerCase(); + if (mime.startsWith('image/') || ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg'].includes(ext)) return 'image'; + if (mime === 'application/pdf' || ext === 'pdf') return 'pdf'; + if (ext === 'docx' || mime.includes('wordprocessingml')) return 'docx'; + if (ext === 'xlsx' || ext === 'xls' || mime.includes('spreadsheetml') || mime === 'application/vnd.ms-excel') return 'xlsx'; + if (mime.startsWith('audio/') || ['mp3', 'wav', 'm4a', 'flac', 'ogg'].includes(ext)) return 'audio'; + if (mime.startsWith('video/') || ['mp4', 'mov', 'webm', 'mkv'].includes(ext)) return 'video'; + if (mime.startsWith('text/') || ['txt', 'md', 'csv', 'json', 'log', 'srt'].includes(ext)) return 'text'; + return 'other'; +} + +function b64ToUint8(dataB64: string): Uint8Array { + return Uint8Array.from(atob(dataB64), c => c.charCodeAt(0)) as Uint8Array; +} + +function forceDownload(fileName: string, mime: string | undefined | null, dataB64: string) { + const url = URL.createObjectURL(new Blob([b64ToUint8(dataB64)], { type: mime || 'application/octet-stream' })); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + a.click(); + setTimeout(() => URL.revokeObjectURL(url), 60_000); +} + +const KIND_LABEL: Record = { + image: '图片', pdf: 'PDF 文档', text: '文本文件', audio: '音频', video: '视频', + docx: 'Word 文档(.docx)', xlsx: 'Excel 表格(.xlsx)', other: '该类型暂不支持在线预览', +}; + +export function AttachmentPreview({ att, onClose }: { att: Attachment | null; onClose: () => void }) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [content, setContent] = useState<{ kind: AttKind; objectUrl?: string; html?: string; text?: string } | null>(null); + + useEffect(() => { + if (!att) { setContent(null); setError(''); return; } + let cancelled = false; + const created: string[] = []; + setLoading(true); setError(''); setContent(null); + (async () => { + try { + const kind = attKind(att); + const { data_b64, mime } = await readFull(att); + if (cancelled) return; + if (kind === 'image' || kind === 'pdf' || kind === 'audio' || kind === 'video') { + const url = b64ToObjectUrl(data_b64, mime); + if (cancelled) { URL.revokeObjectURL(url); return; } + created.push(url); + setContent({ kind, objectUrl: url }); + } else if (kind === 'text') { + const text = new TextDecoder().decode(b64ToUint8(data_b64)); + setContent({ kind, text: text.length > 400_000 ? text.slice(0, 400_000) + '\n…(已截断)' : text }); + } else if (kind === 'docx') { + const mammoth = await import('mammoth/mammoth.browser') as any; + const html = (await mammoth.convertToHtml({ arrayBuffer: b64ToUint8(data_b64).buffer })).value as string; + if (cancelled) return; + setContent({ kind, html }); + } else if (kind === 'xlsx') { + const XLSX = await import('xlsx'); + const wb = XLSX.read(b64ToUint8(data_b64).buffer, { type: 'array' }); + const html = XLSX.utils.sheet_to_html(wb.Sheets[wb.SheetNames[0]]); + if (cancelled) return; + setContent({ kind, html }); + } else { + setContent({ kind: 'other' }); + } + } catch (e) { + if (!cancelled) setError((e as Error).message || '预览加载失败'); + } finally { + if (!cancelled) setLoading(false); + } + })(); + return () => { cancelled = true; created.forEach(u => URL.revokeObjectURL(u)); }; + }, [att?.id, att?.file_name]); + + const kind = att ? attKind(att) : 'other'; + + return ( + + + + + ) : null} + > + {loading && ( +
+ 正在加载预览… +
+ )} + {!loading && !!error &&
{error}
} + {!loading && !error && content && ( + <> + {content.kind === 'image' && {att?.file_name}} + {content.kind === 'pdf' &&