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:
root
2026-10-16 23:23:08 +08:00
parent c25c39a0aa
commit 3ba44aa2de
22 changed files with 902 additions and 358 deletions
+5 -3
View File
@@ -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"
}
}
+233
View File
@@ -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:
@@ -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 { Badge, Button, Confirm, Dialog, useToast } from '@/components/ui';
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 { fmtRange, fmtDateTime, fmtSize } from '@/lib/format';
// 缩略图/原图会话级缓存:同一附件一次会话只读一次
// 缩略图会话级缓存:同一附件一次会话只读一次(原图缓存统一在 lib/att-cache)
const thumbCache = new Map<number, string>();
const fullCache = new Map<number, string>();
export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
open: boolean;
@@ -36,6 +35,7 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
.filter(Boolean) as string[];
const [confirmOpen, setConfirmOpen] = useState(false);
const [previewAtt, setPreviewAtt] = useState<Attachment | null>(null);
const delMut = useMutation({
mutationFn: () => import('@/lib/api').then(m => m.footprintDelete(footprintId!)),
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(渐进渲染)
const [imgMap, setImgMap] = useState<Record<number, string>>({});
@@ -217,8 +205,8 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
<button
key={a.id}
className="group relative aspect-square overflow-hidden rounded-md border border-line bg-surface-2"
onClick={() => openAtt(a)}
title={'点击查看原图:' + a.file_name}
onClick={() => setPreviewAtt(a)}
title={'点击预览:' + a.file_name}
>
{imgMap[a.id] ? (
<img
@@ -243,7 +231,7 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
<button
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"
onClick={() => openAtt(a)}
onClick={() => setPreviewAtt(a)}
>
<FileText size={14} className="shrink-0 text-ink-3" />
<span className="truncate text-ink-1">{a.file_name}</span>
@@ -253,7 +241,7 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
))}
</div>
)}
<div className="text-[11px] text-ink-3">点击图片查看原图(原始比例)</div>
<div className="text-[11px] text-ink-3">点击图片 / 文件可在线预览(图片 · PDF · 文本 · Word · Excel · 音视频)</div>
</div>
) : (
<p className="text-[12px] text-ink-3">暂无佐证附件</p>
@@ -262,6 +250,8 @@ export function RecordDetailDialog({ open, onClose, footprintId, onEdit }: {
</div>
)}
<AttachmentPreview att={previewAtt} onClose={() => setPreviewAtt(null)} />
<Confirm
open={confirmOpen}
title="删除记录"
@@ -1,9 +1,10 @@
import { useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
attachmentAdd, attachmentDelete, attachmentList, attachmentRead,
attachmentAdd, attachmentDelete, attachmentList,
footprintGet, footprintList, footprintUpsert, listDimensions, listTaxonomy, listTypes
} from '@/lib/api';
import { AttachmentPreview } from '@/components/common/AttachmentPreview';
import type { Attachment } from '@/lib/types';
import type { FootprintInput } from '@/lib/types';
import { Button, Dialog, Field, Input, Select, Textarea, useToast } from '@/components/ui';
@@ -49,7 +50,6 @@ const STATUS_OPTS = [
{ value: 'finished', label: '已完结' }
];
import { openOrDownload } from '@/lib/file';
export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
open: boolean;
@@ -62,6 +62,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
const [form, setForm] = useState<FormState>(BLANK);
const [pendingAtts, setPendingAtts] = useState<PendingAtt[]>([]);
const [uploadingAtt, setUploadingAtt] = useState(false);
const [previewAtt, setPreviewAtt] = useState<Attachment | null>(null);
const [error, setError] = useState('');
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)}
onDeletePending={() => {}}
onOpenSaved={async (a: Attachment) => {
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);
}
}}
onOpenSaved={a => setPreviewAtt(a)}
/>
<div className="mt-1.5 text-[11px] text-ink-3">
点击文件名可预览(支持图片 / PDF,其余下载)· 单个文件不超过 80MB
点击文件名可在线预览(图片 · PDF · 文本 · Word · Excel · 音视频)· 单个文件不超过 80MB
</div>
</div>
) : (
@@ -400,6 +394,7 @@ export function RecordEditorDialog({ open, onClose, footprintId, onSaved }: {
</div>
)}
</div>
<AttachmentPreview att={previewAtt} onClose={() => setPreviewAtt(null)} />
</Dialog>
);
}
+18
View File
@@ -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' }));
}
+5
View File
@@ -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;
}
File diff suppressed because one or more lines are too long
@@ -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,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
*
* This source code is licensed under the ISC license.
@@ -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
*
* This source code is licensed under the ISC license.
File diff suppressed because one or more lines are too long
@@ -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
*
* 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 @@
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
+2 -2
View File
@@ -8,8 +8,8 @@
<link rel="icon" href="/icons/pwa-192.png" />
<link rel="apple-touch-icon" href="/icons/pwa-192.png" />
<title>足迹 Footprints</title>
<script type="module" crossorigin src="/assets/index-DrwMmj76.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-zuv9BCiF.css">
<script type="module" crossorigin src="/assets/index-C2sli-Mk.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BgM7aYS9.css">
</head>
<body>
<div id="root"></div>