feat: unified in-app attachment preview (image/PDF/text/docx via mammoth/xlsx via SheetJS/audio/video) with lazy-loaded renderers
This commit is contained in:
+5
-3
@@ -18,19 +18,21 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
|
"mammoth": "^1.13.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^7.6.0",
|
"react-router-dom": "^7.6.0",
|
||||||
"tailwind-merge": "^3.3.0"
|
"tailwind-merge": "^3.3.0",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.0",
|
"@tailwindcss/vite": "^4.1.0",
|
||||||
|
"@tauri-apps/cli": "^2.5.0",
|
||||||
"@types/react": "^19.1.0",
|
"@types/react": "^19.1.0",
|
||||||
"@types/react-dom": "^19.1.0",
|
"@types/react-dom": "^19.1.0",
|
||||||
"@vitejs/plugin-react": "^4.5.0",
|
"@vitejs/plugin-react": "^4.5.0",
|
||||||
"tailwindcss": "^4.1.0",
|
"tailwindcss": "^4.1.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5"
|
||||||
"@tauri-apps/cli": "^2.5.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+233
@@ -26,6 +26,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.475.0
|
specifier: ^0.475.0
|
||||||
version: 0.475.0(react@19.3.1)
|
version: 0.475.0(react@19.3.1)
|
||||||
|
mammoth:
|
||||||
|
specifier: ^1.13.0
|
||||||
|
version: 1.13.0
|
||||||
react:
|
react:
|
||||||
specifier: ^19.1.0
|
specifier: ^19.1.0
|
||||||
version: 19.3.1
|
version: 19.3.1
|
||||||
@@ -38,6 +41,9 @@ importers:
|
|||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.3.0
|
specifier: ^3.3.0
|
||||||
version: 3.7.0
|
version: 3.7.0
|
||||||
|
xlsx:
|
||||||
|
specifier: ^0.18.5
|
||||||
|
version: 0.18.5
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
@@ -679,6 +685,20 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
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:
|
baseline-browser-mapping@2.11.28:
|
||||||
resolution: {integrity: sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==}
|
resolution: {integrity: sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -692,6 +712,10 @@ packages:
|
|||||||
caniuse-lite@1.0.30001816:
|
caniuse-lite@1.0.30001816:
|
||||||
resolution: {integrity: sha512-+8Yx7BXCoqk6IF8yTicjfUR6D/ldJzdlsodlEOiOSPmhK844WIgk67/zIQaGAOxNkZyyvBlrRFnLvqKIoqbAFw==}
|
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:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
@@ -699,6 +723,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
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:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
@@ -706,6 +734,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||||
engines: {node: '>=18'}
|
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:
|
csstype@3.2.3:
|
||||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
@@ -722,6 +758,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
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:
|
echarts@5.6.0:
|
||||||
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
|
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
|
||||||
|
|
||||||
@@ -750,6 +792,10 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
frac@1.1.2:
|
||||||
|
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
|
||||||
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -762,6 +808,15 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
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:
|
jiti@2.7.0:
|
||||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -779,6 +834,12 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
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:
|
lightningcss-android-arm64@1.32.0:
|
||||||
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -853,6 +914,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
lop@0.4.2:
|
||||||
|
resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
@@ -864,6 +928,11 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
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:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -876,6 +945,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==}
|
resolution: {integrity: sha512-5qr2okC4ItAI0hG5SaV0HoYMpnM/Y6+Oqr7AmPYy/2uEoGvS+GbSKLeRxSS1uAdu59yz0ouzcuXievSgg///WA==}
|
||||||
engines: {node: '>=18'}
|
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:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -887,6 +962,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==}
|
resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
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:
|
react-dom@19.3.1:
|
||||||
resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==}
|
resolution: {integrity: sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -917,11 +995,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==}
|
resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
readable-stream@2.3.8:
|
||||||
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
|
|
||||||
rollup@4.64.7:
|
rollup@4.64.7:
|
||||||
resolution: {integrity: sha512-aCiJONoZp0lNGJ6sYqspLkh6UXaWEey/7Ko2TZyXQ4kgzAhDUxAgbt/oulXKmD+zFHXw1k+RF6ZZWOrjp7DZnQ==}
|
resolution: {integrity: sha512-aCiJONoZp0lNGJ6sYqspLkh6UXaWEey/7Ko2TZyXQ4kgzAhDUxAgbt/oulXKmD+zFHXw1k+RF6ZZWOrjp7DZnQ==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
safe-buffer@5.1.2:
|
||||||
|
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||||
|
|
||||||
scheduler@0.28.0:
|
scheduler@0.28.0:
|
||||||
resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
|
resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
|
||||||
|
|
||||||
@@ -932,10 +1016,23 @@ packages:
|
|||||||
set-cookie-parser@2.7.2:
|
set-cookie-parser@2.7.2:
|
||||||
resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
|
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:
|
source-map-js@1.2.2:
|
||||||
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
|
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
|
||||||
engines: {node: '>=0.10.0'}
|
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:
|
tailwind-merge@3.7.0:
|
||||||
resolution: {integrity: sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==}
|
resolution: {integrity: sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==}
|
||||||
|
|
||||||
@@ -958,12 +1055,18 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
underscore@1.13.8:
|
||||||
|
resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==}
|
||||||
|
|
||||||
update-browserslist-db@1.3.4:
|
update-browserslist-db@1.3.4:
|
||||||
resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==}
|
resolution: {integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
|
|
||||||
|
util-deprecate@1.0.2:
|
||||||
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
vite@6.4.4:
|
vite@6.4.4:
|
||||||
resolution: {integrity: sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==}
|
resolution: {integrity: sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==}
|
||||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
@@ -1004,6 +1107,23 @@ packages:
|
|||||||
yaml:
|
yaml:
|
||||||
optional: true
|
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:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
@@ -1468,6 +1588,16 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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: {}
|
baseline-browser-mapping@2.11.28: {}
|
||||||
|
|
||||||
browserslist@4.29.3:
|
browserslist@4.29.3:
|
||||||
@@ -1480,16 +1610,27 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001816: {}
|
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:
|
class-variance-authority@0.7.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
|
codepage@1.15.0: {}
|
||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
cookie@1.1.1: {}
|
cookie@1.1.1: {}
|
||||||
|
|
||||||
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
|
crc-32@1.2.2: {}
|
||||||
|
|
||||||
csstype@3.2.3: {}
|
csstype@3.2.3: {}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
@@ -1498,6 +1639,12 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
dingbat-to-unicode@1.0.2: {}
|
||||||
|
|
||||||
|
duck@0.1.12:
|
||||||
|
dependencies:
|
||||||
|
underscore: 1.13.8
|
||||||
|
|
||||||
echarts@5.6.0:
|
echarts@5.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.3.0
|
tslib: 2.3.0
|
||||||
@@ -1545,6 +1692,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.7
|
picomatch: 4.0.7
|
||||||
|
|
||||||
|
frac@1.1.2: {}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1552,6 +1701,12 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
immediate@3.0.6: {}
|
||||||
|
|
||||||
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
jiti@2.7.0: {}
|
jiti@2.7.0: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
@@ -1560,6 +1715,17 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
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:
|
lightningcss-android-arm64@1.32.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1609,6 +1775,12 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.32.0
|
lightningcss-win32-arm64-msvc: 1.32.0
|
||||||
lightningcss-win32-x64-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:
|
lru-cache@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
@@ -1621,12 +1793,27 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.6.0
|
'@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: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.20: {}
|
nanoid@3.3.20: {}
|
||||||
|
|
||||||
node-releases@2.0.59: {}
|
node-releases@2.0.59: {}
|
||||||
|
|
||||||
|
option@0.2.4: {}
|
||||||
|
|
||||||
|
pako@1.0.11: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.7: {}
|
picomatch@4.0.7: {}
|
||||||
@@ -1637,6 +1824,8 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.2
|
source-map-js: 1.2.2
|
||||||
|
|
||||||
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
react-dom@19.3.1(react@19.3.1):
|
react-dom@19.3.1(react@19.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.3.1
|
react: 19.3.1
|
||||||
@@ -1660,6 +1849,16 @@ snapshots:
|
|||||||
|
|
||||||
react@19.3.1: {}
|
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:
|
rollup@4.64.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.9
|
'@types/estree': 1.0.9
|
||||||
@@ -1692,14 +1891,28 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc': 4.64.7
|
'@rollup/rollup-win32-x64-msvc': 4.64.7
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
safe-buffer@5.1.2: {}
|
||||||
|
|
||||||
scheduler@0.28.0: {}
|
scheduler@0.28.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
set-cookie-parser@2.7.2: {}
|
set-cookie-parser@2.7.2: {}
|
||||||
|
|
||||||
|
setimmediate@1.0.5: {}
|
||||||
|
|
||||||
source-map-js@1.2.2: {}
|
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: {}
|
tailwind-merge@3.7.0: {}
|
||||||
|
|
||||||
tailwindcss@4.3.3: {}
|
tailwindcss@4.3.3: {}
|
||||||
@@ -1715,12 +1928,16 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
underscore@1.13.8: {}
|
||||||
|
|
||||||
update-browserslist-db@1.3.4(browserslist@4.29.3):
|
update-browserslist-db@1.3.4(browserslist@4.29.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.29.3
|
browserslist: 4.29.3
|
||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
vite@6.4.4(jiti@2.7.0)(lightningcss@1.32.0):
|
vite@6.4.4(jiti@2.7.0)(lightningcss@1.32.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
@@ -1734,6 +1951,22 @@ snapshots:
|
|||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
lightningcss: 1.32.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: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
zrender@5.6.1:
|
zrender@5.6.1:
|
||||||
|
|||||||
@@ -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<ArrayBuffer> {
|
||||||
|
return Uint8Array.from(atob(dataB64), c => c.charCodeAt(0)) as Uint8Array<ArrayBuffer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<AttKind, string> = {
|
||||||
|
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 (
|
||||||
|
<Dialog
|
||||||
|
open={!!att}
|
||||||
|
onClose={onClose}
|
||||||
|
width={kind === 'image' || kind === 'pdf' ? 880 : 680}
|
||||||
|
title={(att ? '附件预览 · ' + att.file_name : '附件预览') + (att ? '(' + KIND_LABEL[kind] + ')' : '')}
|
||||||
|
footer={att ? (
|
||||||
|
<>
|
||||||
|
<Button variant="secondary" className="mr-auto" onClick={() => { void readFull(att).then(r => forceDownload(att.file_name, att.mime, r.data_b64)); }}>
|
||||||
|
<Download size={13} /> 下载
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={onClose}>关闭</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
>
|
||||||
|
{loading && (
|
||||||
|
<div className="flex items-center justify-center gap-2 py-16 text-[13px] text-ink-3">
|
||||||
|
<Loader2 className="animate-spin" size={16} /> 正在加载预览…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && !!error && <div className="py-10 text-center text-[13px] text-danger">{error}</div>}
|
||||||
|
{!loading && !error && content && (
|
||||||
|
<>
|
||||||
|
{content.kind === 'image' && <img src={content.objectUrl} alt={att?.file_name} className="mx-auto max-h-[72vh] rounded-md" />}
|
||||||
|
{content.kind === 'pdf' && <iframe src={content.objectUrl} title={att?.file_name} className="h-[72vh] w-full rounded-md border border-line bg-white" />}
|
||||||
|
{content.kind === 'audio' && <audio controls src={content.objectUrl} className="w-full" />}
|
||||||
|
{content.kind === 'video' && <video controls src={content.objectUrl} className="max-h-[70vh] w-full rounded-md bg-black" />}
|
||||||
|
{content.kind === 'text' && <pre className="max-h-[68vh] overflow-auto whitespace-pre-wrap rounded-md bg-surface-2 p-3 text-[12.5px] leading-relaxed text-ink-1">{content.text}</pre>}
|
||||||
|
{(content.kind === 'docx' || content.kind === 'xlsx') && (
|
||||||
|
<div
|
||||||
|
className="max-h-[70vh] overflow-auto rounded-md border border-line bg-white p-4 text-[13px] leading-relaxed [&_img]:max-w-full [&_table]:border-collapse [&_td]:border [&_td]:border-line [&_td]:px-2 [&_td]:py-1 [&_th]:border [&_th]:border-line [&_th]:px-2 [&_th]:py-1"
|
||||||
|
dangerouslySetInnerHTML={{ __html: content.html || '' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{content.kind === 'other' && (
|
||||||
|
<div className="py-10 text-center">
|
||||||
|
<p className="text-[13px] text-ink-2">该文件类型暂不支持在线预览</p>
|
||||||
|
<p className="mt-1 text-[12px] text-ink-3">请点击左下角「下载」,用本地应用打开</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,13 +5,12 @@ import { attachmentRead, attachmentThumb, footprintGet, listDimensions, listType
|
|||||||
import type { Attachment, FormField } from '@/lib/types';
|
import type { Attachment, FormField } from '@/lib/types';
|
||||||
import { Badge, Button, Confirm, Dialog, useToast } from '@/components/ui';
|
import { Badge, Button, Confirm, Dialog, useToast } from '@/components/ui';
|
||||||
import { StatusBadge, LevelBadge, GradeBadge, RoleBadge, NegativeBadge } from '@/components/common/badges';
|
import { StatusBadge, LevelBadge, GradeBadge, RoleBadge, NegativeBadge } from '@/components/common/badges';
|
||||||
import { openOrDownload } from '@/lib/file';
|
import { AttachmentPreview } from '@/components/common/AttachmentPreview';
|
||||||
import { makeThumb } from '@/lib/img';
|
import { makeThumb } from '@/lib/img';
|
||||||
import { fmtRange, fmtDateTime, fmtSize } from '@/lib/format';
|
import { fmtRange, fmtDateTime, fmtSize } from '@/lib/format';
|
||||||
|
|
||||||
// 缩略图/原图会话级缓存:同一附件一次会话只读一次
|
// 缩略图会话级缓存:同一附件一次会话只读一次(原图缓存统一在 lib/att-cache)
|
||||||
const thumbCache = new Map<number, string>();
|
const thumbCache = new Map<number, string>();
|
||||||
const fullCache = new Map<number, string>();
|
|
||||||
|
|
||||||
export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
|
export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -36,6 +35,7 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
|
|||||||
.filter(Boolean) as string[];
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
const [previewAtt, setPreviewAtt] = useState<Attachment | null>(null);
|
||||||
const delMut = useMutation({
|
const delMut = useMutation({
|
||||||
mutationFn: () => import('@/lib/api').then(m => m.footprintDelete(footprintId!)),
|
mutationFn: () => import('@/lib/api').then(m => m.footprintDelete(footprintId!)),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -48,19 +48,7 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const openAtt = async (a: Attachment) => {
|
|
||||||
try {
|
|
||||||
let b64 = fullCache.get(a.id);
|
|
||||||
if (!b64) {
|
|
||||||
const r = await attachmentRead(a.id);
|
|
||||||
b64 = r.data_b64;
|
|
||||||
if ((a.size ?? 0) <= 2 * 1024 * 1024) fullCache.set(a.id, b64); // 大原图不驻留内存
|
|
||||||
}
|
|
||||||
openOrDownload(a.file_name, a.mime ?? undefined, b64);
|
|
||||||
} catch (e) {
|
|
||||||
toast('error', (e as Error).message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 图片直出:逐张读取为 data URL(渐进渲染)
|
// 图片直出:逐张读取为 data URL(渐进渲染)
|
||||||
const [imgMap, setImgMap] = useState<Record<number, string>>({});
|
const [imgMap, setImgMap] = useState<Record<number, string>>({});
|
||||||
@@ -217,8 +205,8 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
|
|||||||
<button
|
<button
|
||||||
key={a.id}
|
key={a.id}
|
||||||
className="group relative aspect-square overflow-hidden rounded-md border border-line bg-surface-2"
|
className="group relative aspect-square overflow-hidden rounded-md border border-line bg-surface-2"
|
||||||
onClick={() => openAtt(a)}
|
onClick={() => setPreviewAtt(a)}
|
||||||
title={'点击查看原图:' + a.file_name}
|
title={'点击预览:' + a.file_name}
|
||||||
>
|
>
|
||||||
{imgMap[a.id] ? (
|
{imgMap[a.id] ? (
|
||||||
<img
|
<img
|
||||||
@@ -243,7 +231,7 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
|
|||||||
<button
|
<button
|
||||||
key={a.id}
|
key={a.id}
|
||||||
className="flex items-center gap-2 rounded-md border border-line bg-surface px-2.5 py-2 text-left text-[12.5px] transition-colors hover:border-line-strong"
|
className="flex items-center gap-2 rounded-md border border-line bg-surface px-2.5 py-2 text-left text-[12.5px] transition-colors hover:border-line-strong"
|
||||||
onClick={() => openAtt(a)}
|
onClick={() => setPreviewAtt(a)}
|
||||||
>
|
>
|
||||||
<FileText size={14} className="shrink-0 text-ink-3" />
|
<FileText size={14} className="shrink-0 text-ink-3" />
|
||||||
<span className="truncate text-ink-1">{a.file_name}</span>
|
<span className="truncate text-ink-1">{a.file_name}</span>
|
||||||
@@ -253,7 +241,7 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-[11px] text-ink-3">点击图片查看原图(原始比例)</div>
|
<div className="text-[11px] text-ink-3">点击图片 / 文件可在线预览(图片 · PDF · 文本 · Word · Excel · 音视频)</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-[12px] text-ink-3">暂无佐证附件</p>
|
<p className="text-[12px] text-ink-3">暂无佐证附件</p>
|
||||||
@@ -262,6 +250,8 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<AttachmentPreview att={previewAtt} onClose={() => setPreviewAtt(null)} />
|
||||||
|
|
||||||
<Confirm
|
<Confirm
|
||||||
open={confirmOpen}
|
open={confirmOpen}
|
||||||
title="删除记录"
|
title="删除记录"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
attachmentAdd, attachmentDelete, attachmentList, attachmentRead,
|
attachmentAdd, attachmentDelete, attachmentList,
|
||||||
footprintGet, footprintList, footprintUpsert, listDimensions, listTaxonomy, listTypes
|
footprintGet, footprintList, footprintUpsert, listDimensions, listTaxonomy, listTypes
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
|
import { AttachmentPreview } from '@/components/common/AttachmentPreview';
|
||||||
import type { Attachment } from '@/lib/types';
|
import type { Attachment } from '@/lib/types';
|
||||||
import type { FootprintInput } from '@/lib/types';
|
import type { FootprintInput } from '@/lib/types';
|
||||||
import { Button, Dialog, Field, Input, Select, Textarea, useToast } from '@/components/ui';
|
import { Button, Dialog, Field, Input, Select, Textarea, useToast } from '@/components/ui';
|
||||||
@@ -49,7 +50,6 @@ const STATUS_OPTS = [
|
|||||||
{ value: 'finished', label: '已完结' }
|
{ value: 'finished', label: '已完结' }
|
||||||
];
|
];
|
||||||
|
|
||||||
import { openOrDownload } from '@/lib/file';
|
|
||||||
|
|
||||||
export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
|
export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -62,6 +62,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
|
|||||||
const [form, setForm] = useState<FormState>(BLANK);
|
const [form, setForm] = useState<FormState>(BLANK);
|
||||||
const [pendingAtts, setPendingAtts] = useState<PendingAtt[]>([]);
|
const [pendingAtts, setPendingAtts] = useState<PendingAtt[]>([]);
|
||||||
const [uploadingAtt, setUploadingAtt] = useState(false);
|
const [uploadingAtt, setUploadingAtt] = useState(false);
|
||||||
|
const [previewAtt, setPreviewAtt] = useState<Attachment | null>(null);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
const typesQ = useQuery({ queryKey: ['types'], queryFn: listTypes, enabled: open });
|
const typesQ = useQuery({ queryKey: ['types'], queryFn: listTypes, enabled: open });
|
||||||
@@ -362,17 +363,10 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
|
|||||||
}}
|
}}
|
||||||
onDeleteSaved={id => delAtt.mutate(id)}
|
onDeleteSaved={id => delAtt.mutate(id)}
|
||||||
onDeletePending={() => {}}
|
onDeletePending={() => {}}
|
||||||
onOpenSaved={async (a: Attachment) => {
|
onOpenSaved={a => setPreviewAtt(a)}
|
||||||
try {
|
|
||||||
const r = await attachmentRead(a.id);
|
|
||||||
openOrDownload(r.file_name, r.mime ?? undefined, r.data_b64);
|
|
||||||
} catch (e) {
|
|
||||||
toast('error', (e as Error).message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className="mt-1.5 text-[11px] text-ink-3">
|
<div className="mt-1.5 text-[11px] text-ink-3">
|
||||||
点击文件名可预览(支持图片 / PDF,其余下载)· 单个文件不超过 80MB
|
点击文件名可在线预览(图片 · PDF · 文本 · Word · Excel · 音视频)· 单个文件不超过 80MB
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -400,6 +394,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<AttachmentPreview att={previewAtt} onClose={() => setPreviewAtt(null)} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// 附件内容会话级缓存:同一附件一次会话只从服务器读一次(>25MB 不驻留内存)
|
||||||
|
import { attachmentRead } from './api';
|
||||||
|
|
||||||
|
const fullCache = new Map<number, { data_b64: string; mime?: string; file_name: string }>();
|
||||||
|
const MAX_CACHE_BYTES = 25 * 1024 * 1024;
|
||||||
|
|
||||||
|
export async function readFull(a: { id: number; size?: number }): Promise<{ data_b64: string; mime?: string; file_name: string }> {
|
||||||
|
const hit = fullCache.get(a.id);
|
||||||
|
if (hit) return hit;
|
||||||
|
const r = await attachmentRead(a.id);
|
||||||
|
if ((a.size ?? 0) <= MAX_CACHE_BYTES) fullCache.set(a.id, r);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function b64ToObjectUrl(dataB64: string, mime?: string): string {
|
||||||
|
const bytes = Uint8Array.from(atob(dataB64), c => c.charCodeAt(0)) as Uint8Array<ArrayBuffer>;
|
||||||
|
return URL.createObjectURL(new Blob([bytes], { type: mime || 'application/octet-stream' }));
|
||||||
|
}
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
declare module 'mammoth/mammoth.browser' {
|
||||||
|
export function convertToHtml(input: { arrayBuffer: ArrayBuffer }): Promise<{ value: string; messages: unknown[] }>;
|
||||||
|
const _default: { convertToHtml: typeof convertToHtml };
|
||||||
|
export default _default;
|
||||||
|
}
|
||||||
+3
-3
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{r as g,e as v,j as t,S as j,s as S}from"./index-DrwMmj76.js";import{C as c,a as d,b as x}from"./card-DvRCiB1a.js";import{u as N,i as u,a as m}from"./useChart-BCLvza0E.js";import{u as k,i as w,a as C,b as L,c as R,d as $,e as z,f as T}from"./echarts-BIWefrIc.js";k([w,C,L,R,$,z,T]);function q(){const[l,n]=g.useState(""),i=N(),e=v({queryKey:["stats",l||void 0],queryFn:()=>S(l?Number(l):void 0)}).data,p=u("2"),r=u("3"),h=m(()=>({grid:{left:34,right:12,top:14,bottom:24},tooltip:{trigger:"axis"},xAxis:{type:"category",data:(e==null?void 0:e.monthly.map(a=>a.ym.slice(2)))??[],axisLine:{lineStyle:{color:r}},axisTick:{show:!1},axisLabel:{color:r,fontSize:10}},yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},series:[{type:"bar",data:(e==null?void 0:e.monthly.map(a=>a.count))??[],barMaxWidth:18,itemStyle:{color:"#3056d3",borderRadius:[3,3,0,0]}}]}),[e,i]),b=m(()=>{const a=((e==null?void 0:e.by_type)??[]).filter(s=>s.count>0);return{tooltip:{trigger:"item"},legend:{orient:"vertical",right:4,top:"middle",textStyle:{color:p,fontSize:11},itemWidth:10,itemHeight:10},series:[{type:"pie",radius:["48%","72%"],center:["34%","50%"],label:{show:!1},data:a.map(s=>({name:s.label,value:s.count,itemStyle:{color:s.color||"#64748b"}}))}]}},[e,i]),y=m(()=>{const a=((e==null?void 0:e.by_level)??[]).filter(s=>s.count>0).slice().reverse();return{grid:{left:64,right:24,top:8,bottom:22},tooltip:{trigger:"axis"},xAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},yAxis:{type:"category",data:a.map(s=>s.label),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:p,fontSize:11}},series:[{type:"bar",data:a.map(s=>({value:s.count,itemStyle:{color:s.color||"#3056d3",borderRadius:[0,3,3,0]}})),barMaxWidth:14}]}},[e,i]),f=((e==null?void 0:e.metrics)??[]).slice(0,3);return t.jsxs("div",{className:"mx-auto max-w-[1080px] px-6 py-5",children:[t.jsxs("div",{className:"mb-4 flex flex-wrap items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex flex-wrap gap-3",children:[t.jsx(o,{label:"总记录",value:(e==null?void 0:e.total)??0}),t.jsx(o,{label:l?l+" 年新增":"本年新增",value:(e==null?void 0:e.total_this_year)??0}),t.jsx(o,{label:"附件总数",value:(e==null?void 0:e.attachment_total)??0}),f.map(a=>t.jsx(o,{label:a.label,value:a.value,unit:a.unit??""},a.key))]}),t.jsx(j,{className:"w-[120px]",value:l,onChange:a=>n(a.target.value),options:[{value:"",label:"全部年份"},...((e==null?void 0:e.years)??[]).map(a=>({value:String(a),label:a+" 年"}))]})]}),!(e!=null&&e.metrics.length)&&t.jsx("div",{className:"mb-4 rounded-md border border-dashed border-line px-4 py-2 text-[12px] text-ink-3",children:"提示:在「统计」配置中注册指标(如志愿时长、读书本数),带单位的数值字段即可在此量化展示。"}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[t.jsxs(c,{children:[t.jsx(d,{title:"近 12 个月活跃度"}),t.jsx(x,{children:t.jsx("div",{ref:h,className:"h-[220px]"})})]}),t.jsxs(c,{children:[t.jsx(d,{title:"类型分布"}),t.jsx(x,{children:t.jsx("div",{ref:b,className:"h-[220px]"})})]}),t.jsxs(c,{className:"col-span-2",children:[t.jsx(d,{title:"级别分布"}),t.jsx(x,{children:t.jsx("div",{ref:y,className:"h-[200px]"})})]})]})]})}function o({label:l,value:n,unit:i}){return t.jsxs("div",{className:"min-w-[120px] rounded-lg border border-line bg-surface px-4 py-3 shadow-card",children:[t.jsx("div",{className:"text-[12px] text-ink-3",children:l}),t.jsxs("div",{className:"num mt-0.5 text-[22px] font-semibold leading-tight text-ink-1",children:[n,i&&t.jsx("span",{className:"ml-1 text-[12px] font-normal text-ink-3",children:i})]})]})}export{q as DashboardPage};
|
import{r as g,e as v,j as t,S as j,s as S}from"./index-C2sli-Mk.js";import{C as c,a as d,b as x}from"./card-DL3ec4KP.js";import{u as N,i as u,a as m}from"./useChart-f1LAeREn.js";import{u as k,i as w,a as C,b as L,c as R,d as $,e as z,f as T}from"./echarts-BIWefrIc.js";k([w,C,L,R,$,z,T]);function q(){const[l,n]=g.useState(""),i=N(),e=v({queryKey:["stats",l||void 0],queryFn:()=>S(l?Number(l):void 0)}).data,p=u("2"),r=u("3"),h=m(()=>({grid:{left:34,right:12,top:14,bottom:24},tooltip:{trigger:"axis"},xAxis:{type:"category",data:(e==null?void 0:e.monthly.map(a=>a.ym.slice(2)))??[],axisLine:{lineStyle:{color:r}},axisTick:{show:!1},axisLabel:{color:r,fontSize:10}},yAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},series:[{type:"bar",data:(e==null?void 0:e.monthly.map(a=>a.count))??[],barMaxWidth:18,itemStyle:{color:"#3056d3",borderRadius:[3,3,0,0]}}]}),[e,i]),b=m(()=>{const a=((e==null?void 0:e.by_type)??[]).filter(s=>s.count>0);return{tooltip:{trigger:"item"},legend:{orient:"vertical",right:4,top:"middle",textStyle:{color:p,fontSize:11},itemWidth:10,itemHeight:10},series:[{type:"pie",radius:["48%","72%"],center:["34%","50%"],label:{show:!1},data:a.map(s=>({name:s.label,value:s.count,itemStyle:{color:s.color||"#64748b"}}))}]}},[e,i]),y=m(()=>{const a=((e==null?void 0:e.by_level)??[]).filter(s=>s.count>0).slice().reverse();return{grid:{left:64,right:24,top:8,bottom:22},tooltip:{trigger:"axis"},xAxis:{type:"value",splitLine:{lineStyle:{color:"rgba(128,138,155,0.15)"}},axisLabel:{color:r,fontSize:10}},yAxis:{type:"category",data:a.map(s=>s.label),axisLine:{show:!1},axisTick:{show:!1},axisLabel:{color:p,fontSize:11}},series:[{type:"bar",data:a.map(s=>({value:s.count,itemStyle:{color:s.color||"#3056d3",borderRadius:[0,3,3,0]}})),barMaxWidth:14}]}},[e,i]),f=((e==null?void 0:e.metrics)??[]).slice(0,3);return t.jsxs("div",{className:"mx-auto max-w-[1080px] px-6 py-5",children:[t.jsxs("div",{className:"mb-4 flex flex-wrap items-center justify-between gap-3",children:[t.jsxs("div",{className:"flex flex-wrap gap-3",children:[t.jsx(o,{label:"总记录",value:(e==null?void 0:e.total)??0}),t.jsx(o,{label:l?l+" 年新增":"本年新增",value:(e==null?void 0:e.total_this_year)??0}),t.jsx(o,{label:"附件总数",value:(e==null?void 0:e.attachment_total)??0}),f.map(a=>t.jsx(o,{label:a.label,value:a.value,unit:a.unit??""},a.key))]}),t.jsx(j,{className:"w-[120px]",value:l,onChange:a=>n(a.target.value),options:[{value:"",label:"全部年份"},...((e==null?void 0:e.years)??[]).map(a=>({value:String(a),label:a+" 年"}))]})]}),!(e!=null&&e.metrics.length)&&t.jsx("div",{className:"mb-4 rounded-md border border-dashed border-line px-4 py-2 text-[12px] text-ink-3",children:"提示:在「统计」配置中注册指标(如志愿时长、读书本数),带单位的数值字段即可在此量化展示。"}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4",children:[t.jsxs(c,{children:[t.jsx(d,{title:"近 12 个月活跃度"}),t.jsx(x,{children:t.jsx("div",{ref:h,className:"h-[220px]"})})]}),t.jsxs(c,{children:[t.jsx(d,{title:"类型分布"}),t.jsx(x,{children:t.jsx("div",{ref:b,className:"h-[220px]"})})]}),t.jsxs(c,{className:"col-span-2",children:[t.jsx(d,{title:"级别分布"}),t.jsx(x,{children:t.jsx("div",{ref:y,className:"h-[200px]"})})]})]})]})}function o({label:l,value:n,unit:i}){return t.jsxs("div",{className:"min-w-[120px] rounded-lg border border-line bg-surface px-4 py-3 shadow-card",children:[t.jsx("div",{className:"text-[12px] text-ink-3",children:l}),t.jsxs("div",{className:"num mt-0.5 text-[22px] font-semibold leading-tight text-ink-1",children:[n,i&&t.jsx("span",{className:"ml-1 text-[12px] font-normal text-ink-3",children:i})]})]})}export{q as DashboardPage};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as K,e as I,j as e,E as O,t as $,u as k,b as S,r as u,f as y,v as H,w as W,B as g,P as q,C as Q,x as J,y as T,z as U,A as N,k as G,T as E,I as j,D as Y,F as C,H as B,J as V}from"./index-DrwMmj76.js";import{C as f,a as z,b as _}from"./card-DvRCiB1a.js";import{u as X,i as M,a as L}from"./useChart-BCLvza0E.js";import{u as Z,i as ee,b as se,a as te,c as ae,d as re,e as ne,f as le}from"./echarts-BIWefrIc.js";/**
|
import{c as K,e as I,j as e,E as O,t as $,u as k,b as S,r as u,f as y,v as H,w as W,B as g,P as q,C as Q,x as J,y as T,z as U,A as N,k as G,T as E,I as j,D as Y,F as C,H as B,J as V}from"./index-C2sli-Mk.js";import{C as f,a as z,b as _}from"./card-DL3ec4KP.js";import{u as X,i as M,a as L}from"./useChart-f1LAeREn.js";import{u as Z,i as ee,b as se,a as te,c as ae,d as re,e as ne,f as le}from"./echarts-BIWefrIc.js";/**
|
||||||
* @license lucide-react v0.475.0 - ISC
|
* @license lucide-react v0.475.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as re,j as e,a as u,r as t,u as oe,b as ce,d as de,e as C,f as ue,I as xe,S as m,B as b,P as Y,g as pe,L as me,G as ve,R as he,h as je,i as ge,k as fe,T as ye,E as be,l as Ne,m as ke,C as Se,n as we,o as Ce,p as Ee,s as qe,q as _e}from"./index-DrwMmj76.js";/**
|
import{c as re,j as e,a as u,r as t,u as oe,b as ce,d as de,e as C,f as ue,I as xe,S as m,B as b,P as Y,g as pe,L as me,G as ve,R as he,h as je,i as ge,k as fe,T as ye,E as be,l as Ne,m as ke,C as Se,n as we,o as Ce,p as Ee,s as qe,q as _e}from"./index-C2sli-Mk.js";/**
|
||||||
* @license lucide-react v0.475.0 - ISC
|
* @license lucide-react v0.475.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as P,u as Q,b as z,e as B,r as i,f as T,j as e,B as p,P as F,K as q,A as E,k as D,T as I,C as K,M as L,D as G,F as r,I as j,N as H,S as R,O as W,o as J}from"./index-DrwMmj76.js";import{C as U}from"./card-DvRCiB1a.js";/**
|
import{c as P,u as Q,b as z,e as B,r as i,f as T,j as e,B as p,P as F,K as q,A as E,k as D,T as I,C as K,M as L,D as G,F as r,I as j,N as H,S as R,O as W,o as J}from"./index-C2sli-Mk.js";import{C as U}from"./card-DL3ec4KP.js";/**
|
||||||
* @license lucide-react v0.475.0 - ISC
|
* @license lucide-react v0.475.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
@@ -1 +1 @@
|
|||||||
import{j as s,a}from"./index-DrwMmj76.js";function i({className:r,...e}){return s.jsx("div",{className:a("rounded-lg border border-line bg-surface shadow-card",r),...e})}function t({title:r,extra:e,className:n}){return s.jsxs("div",{className:a("flex min-h-11 flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-1.5",n),children:[s.jsx("h3",{className:"text-[13px] font-semibold text-ink-1",children:r}),e&&s.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e})]})}function c({className:r,...e}){return s.jsx("div",{className:a("p-4",r),...e})}export{i as C,t as a,c as b};
|
import{j as s,a}from"./index-C2sli-Mk.js";function i({className:r,...e}){return s.jsx("div",{className:a("rounded-lg border border-line bg-surface shadow-card",r),...e})}function t({title:r,extra:e,className:n}){return s.jsxs("div",{className:a("flex min-h-11 flex-wrap items-center justify-between gap-2 border-b border-line px-4 py-1.5",n),children:[s.jsx("h3",{className:"text-[13px] font-semibold text-ink-1",children:r}),e&&s.jsx("div",{className:"flex flex-wrap items-center gap-2",children:e})]})}function c({className:r,...e}){return s.jsx("div",{className:a("p-4",r),...e})}export{i as C,t as a,c as b};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -1 +1 @@
|
|||||||
import{r}from"./index-DrwMmj76.js";import{g as f}from"./echarts-BIWefrIc.js";function p(){const[n,o]=r.useState(0);return r.useEffect(()=>{const s=()=>o(t=>t+1);return window.addEventListener("fp-theme-change",s),()=>window.removeEventListener("fp-theme-change",s)},[]),n}function d(n){return getComputedStyle(document.documentElement).getPropertyValue("--text-"+n).trim()||"#454e5c"}function h(n,o){const s=r.useRef(null),[t,i]=r.useState(null),u=r.useRef(n);return u.current=n,r.useEffect(()=>{if(!t)return;const e=f(t);s.current=e,e.setOption(u.current(),{notMerge:!0});const c=new ResizeObserver(()=>e.resize());return c.observe(t),()=>{c.disconnect(),e.dispose(),s.current=null}},[t]),r.useEffect(()=>{var e;(e=s.current)==null||e.setOption(u.current(),{notMerge:!0})},[t,...o]),e=>i(e)}export{h as a,d as i,p as u};
|
import{r}from"./index-C2sli-Mk.js";import{g as f}from"./echarts-BIWefrIc.js";function p(){const[n,o]=r.useState(0);return r.useEffect(()=>{const s=()=>o(t=>t+1);return window.addEventListener("fp-theme-change",s),()=>window.removeEventListener("fp-theme-change",s)},[]),n}function d(n){return getComputedStyle(document.documentElement).getPropertyValue("--text-"+n).trim()||"#454e5c"}function h(n,o){const s=r.useRef(null),[t,i]=r.useState(null),u=r.useRef(n);return u.current=n,r.useEffect(()=>{if(!t)return;const e=f(t);s.current=e,e.setOption(u.current(),{notMerge:!0});const c=new ResizeObserver(()=>e.resize());return c.observe(t),()=>{c.disconnect(),e.dispose(),s.current=null}},[t]),r.useEffect(()=>{var e;(e=s.current)==null||e.setOption(u.current(),{notMerge:!0})},[t,...o]),e=>i(e)}export{h as a,d as i,p as u};
|
||||||
File diff suppressed because one or more lines are too long
@@ -8,8 +8,8 @@
|
|||||||
<link rel="icon" href="/icons/pwa-192.png" />
|
<link rel="icon" href="/icons/pwa-192.png" />
|
||||||
<link rel="apple-touch-icon" href="/icons/pwa-192.png" />
|
<link rel="apple-touch-icon" href="/icons/pwa-192.png" />
|
||||||
<title>足迹 Footprints</title>
|
<title>足迹 Footprints</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DrwMmj76.js"></script>
|
<script type="module" crossorigin src="/assets/index-C2sli-Mk.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-zuv9BCiF.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BgM7aYS9.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user