feat: refactor api

This commit is contained in:
Ou 2024-10-03 13:16:14 +08:00
parent 9859049a22
commit f6a66f4eba
20 changed files with 629 additions and 56 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@ node_modules/
dist/
.vercel
.output
.vinxi
.vinxi
.cache

View File

@ -22,11 +22,17 @@
"@tanstack/react-router": "^1.58.9",
"@tanstack/router-devtools": "^1.58.9",
"@unocss/reset": "^0.62.4",
"cheerio": "^1.0.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"fast-xml-parser": "^4.5.0",
"favicons-scraper": "^1.3.2",
"flat-cache": "^6.1.0",
"h3": "^1.12.0",
"iconv-lite": "^0.6.3",
"jotai": "^2.10.0",
"node-fetch": "^3.3.2",
"ofetch": "^1.4.0",
"overlayscrollbars": "^2.10.0",
"overlayscrollbars-react": "^0.5.6",
"react": "^18.3.1",

256
pnpm-lock.yaml generated
View File

@ -32,21 +32,39 @@ importers:
'@unocss/reset':
specifier: ^0.62.4
version: 0.62.4
cheerio:
specifier: ^1.0.0
version: 1.0.0
clsx:
specifier: ^2.1.1
version: 2.1.1
dayjs:
specifier: ^1.11.13
version: 1.11.13
fast-xml-parser:
specifier: ^4.5.0
version: 4.5.0
favicons-scraper:
specifier: ^1.3.2
version: 1.3.2
flat-cache:
specifier: ^6.1.0
version: 6.1.0
h3:
specifier: ^1.12.0
version: 1.12.0
iconv-lite:
specifier: ^0.6.3
version: 0.6.3
jotai:
specifier: ^2.10.0
version: 2.10.0(@types/react@18.3.9)(react@18.3.1)
node-fetch:
specifier: ^3.3.2
version: 3.3.2
ofetch:
specifier: ^1.4.0
version: 1.4.0
overlayscrollbars:
specifier: ^2.10.0
version: 2.10.0
@ -104,7 +122,7 @@ importers:
version: 0.4.12(eslint@9.11.1(jiti@1.21.6))
nitropack:
specifier: ^2.9.7
version: 2.9.7(magicast@0.3.5)
version: 2.9.7(encoding@0.1.13)(magicast@0.3.5)
tsx:
specifier: ^4.19.1
version: 4.19.1
@ -122,7 +140,7 @@ importers:
version: 5.4.8(@types/node@22.6.1)(terser@5.33.0)
vite-plugin-with-nitro:
specifier: 0.0.0-beta.4
version: 0.0.0-beta.4(magicast@0.3.5)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0))
version: 0.0.0-beta.4(encoding@0.1.13)(magicast@0.3.5)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0))
vite-tsconfig-paths:
specifier: ^5.0.1
version: 5.0.1(typescript@5.6.2)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0))
@ -959,6 +977,9 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@keyv/serialize@1.0.1':
resolution: {integrity: sha512-kKXeynfORDGPUEEl2PvTExM2zs+IldC6ZD8jPcfvI351MDNtfMlw9V9s4XZXuJNDK2qR5gbEKxRyoYx3quHUVQ==}
'@mapbox/node-pre-gyp@1.0.11':
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true
@ -1887,6 +1908,9 @@ packages:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
cacheable@1.8.0:
resolution: {integrity: sha512-ewQhhLQ7aUO95MALxgQ/04M8Eh97uCfinTK1pfktsCq+mULf4gNFAg1jSkLi5SDIw/2Md6K6Np3yWjWtYEzvqw==}
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@ -1923,6 +1947,13 @@ packages:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'}
cheerio-select@2.1.0:
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
cheerio@1.0.0:
resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==}
engines: {node: '>=18.17'}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
@ -2048,6 +2079,9 @@ packages:
css-in-js-utils@3.1.0:
resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
css-tree@1.1.3:
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
engines: {node: '>=8.0.0'}
@ -2056,6 +2090,10 @@ packages:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@ -2068,6 +2106,9 @@ packages:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
db0@0.1.4:
resolution: {integrity: sha512-Ft6eCwONYxlwLjBXSJxw0t0RYtA5gW9mq8JfBXn9TtC0nDPlqePAhpv9v4g9aONBi6JI1OXHTKKkUYGd+BOrCA==}
peerDependencies:
@ -2156,6 +2197,19 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
dot-prop@8.0.2:
resolution: {integrity: sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==}
engines: {node: '>=16'}
@ -2190,6 +2244,12 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
encoding-sniffer@0.2.0:
resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==}
encoding@0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
enhanced-resolve@5.17.1:
resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==}
engines: {node: '>=10.13.0'}
@ -2549,6 +2609,10 @@ packages:
fast-shallow-equal@1.0.0:
resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==}
fast-xml-parser@4.5.0:
resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==}
hasBin: true
fastest-stable-stringify@2.0.2:
resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==}
@ -2597,6 +2661,9 @@ packages:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
flat-cache@6.1.0:
resolution: {integrity: sha512-txr9o9zCbXvMOWZ1wZAID4U+CnYxdkSFqxxvd8Bs/a0ZidSs8V8diyv0CviKkPYEWHtbFJloF/QQO2WnkarnlA==}
flatted@3.3.1:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
@ -2748,9 +2815,16 @@ packages:
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
hookified@1.2.0:
resolution: {integrity: sha512-32q0JrtZRAdCGeEa8IcBz4iABsrP4UiDrbFj/WADL1VyNHRvHdGs8MrYvaIZWMzbff1xdS3Tw8kBt6YvcSL7jQ==}
engines: {node: '>=20'}
hosted-git-info@2.8.9:
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
htmlparser2@9.1.0:
resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==}
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@ -2773,6 +2847,10 @@ packages:
hyphenate-style-name@1.1.0:
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
@ -2997,6 +3075,9 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
keyv@5.0.3:
resolution: {integrity: sha512-WmefGWaWkWiWDkIasfHxpWmM1lych/LPtRmNj8jnIQVGLsAgFw73Vg9utZ7ss97/JwRlERABb/fSejTPY4hlZQ==}
klona@2.0.6:
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
engines: {node: '>= 8'}
@ -3351,6 +3432,15 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'}
parse5-htmlparser2-tree-adapter@7.0.0:
resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
parse5-parser-stream@7.1.2:
resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==}
parse5@7.1.2:
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@ -3602,6 +3692,9 @@ packages:
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
@ -3800,6 +3893,9 @@ packages:
strip-literal@2.1.0:
resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==}
strnum@1.0.5:
resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
stylis@4.3.4:
resolution: {integrity: sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==}
@ -3999,6 +4095,10 @@ packages:
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
engines: {node: '>=14.0'}
undici@6.19.8:
resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==}
engines: {node: '>=18.17'}
unenv@1.10.0:
resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==}
@ -4205,6 +4305,14 @@ packages:
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@ -4963,12 +5071,16 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@mapbox/node-pre-gyp@1.0.11':
'@keyv/serialize@1.0.1':
dependencies:
buffer: 6.0.3
'@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)':
dependencies:
detect-libc: 2.0.3
https-proxy-agent: 5.0.1
make-dir: 3.1.0
node-fetch: 2.7.0
node-fetch: 2.7.0(encoding@0.1.13)
nopt: 5.0.0
npmlog: 5.0.1
rimraf: 3.0.2
@ -5686,9 +5798,9 @@ snapshots:
- rollup
- supports-color
'@vercel/nft@0.26.5':
'@vercel/nft@0.26.5(encoding@0.1.13)':
dependencies:
'@mapbox/node-pre-gyp': 1.0.11
'@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13)
'@rollup/pluginutils': 4.2.1
acorn: 8.12.1
acorn-import-attributes: 1.9.5(acorn@8.12.1)
@ -5967,6 +6079,11 @@ snapshots:
cac@6.7.14: {}
cacheable@1.8.0:
dependencies:
hookified: 1.2.0
keyv: 5.0.3
callsites@3.1.0: {}
caniuse-lite@1.0.30001663: {}
@ -6000,6 +6117,29 @@ snapshots:
check-error@2.1.1: {}
cheerio-select@2.1.0:
dependencies:
boolbase: 1.0.0
css-select: 5.1.0
css-what: 6.1.0
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
cheerio@1.0.0:
dependencies:
cheerio-select: 2.1.0
dom-serializer: 2.0.0
domhandler: 5.0.3
domutils: 3.1.0
encoding-sniffer: 0.2.0
htmlparser2: 9.1.0
parse5: 7.1.2
parse5-htmlparser2-tree-adapter: 7.0.0
parse5-parser-stream: 7.1.2
undici: 6.19.8
whatwg-mimetype: 4.0.0
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
@ -6113,6 +6253,14 @@ snapshots:
dependencies:
hyphenate-style-name: 1.1.0
css-select@5.1.0:
dependencies:
boolbase: 1.0.0
css-what: 6.1.0
domhandler: 5.0.3
domutils: 3.1.0
nth-check: 2.1.1
css-tree@1.1.3:
dependencies:
mdn-data: 2.0.14
@ -6123,12 +6271,16 @@ snapshots:
mdn-data: 2.0.30
source-map-js: 1.2.1
css-what@6.1.0: {}
cssesc@3.0.0: {}
csstype@3.1.3: {}
data-uri-to-buffer@4.0.1: {}
dayjs@1.11.13: {}
db0@0.1.4: {}
debug@2.6.9:
@ -6171,6 +6323,24 @@ snapshots:
dependencies:
esutils: 2.0.3
dom-serializer@2.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
domelementtype@2.3.0: {}
domhandler@5.0.3:
dependencies:
domelementtype: 2.3.0
domutils@3.1.0:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dot-prop@8.0.2:
dependencies:
type-fest: 3.13.1
@ -6193,6 +6363,16 @@ snapshots:
encodeurl@2.0.0: {}
encoding-sniffer@0.2.0:
dependencies:
iconv-lite: 0.6.3
whatwg-encoding: 3.1.1
encoding@0.1.13:
dependencies:
iconv-lite: 0.6.3
optional: true
enhanced-resolve@5.17.1:
dependencies:
graceful-fs: 4.2.11
@ -6780,6 +6960,10 @@ snapshots:
fast-shallow-equal@1.0.0: {}
fast-xml-parser@4.5.0:
dependencies:
strnum: 1.0.5
fastest-stable-stringify@2.0.2: {}
fastq@1.17.1:
@ -6824,6 +7008,12 @@ snapshots:
flatted: 3.3.1
keyv: 4.5.4
flat-cache@6.1.0:
dependencies:
cacheable: 1.8.0
flatted: 3.3.1
hookified: 1.2.0
flatted@3.3.1: {}
foreground-child@3.3.0:
@ -6993,8 +7183,17 @@ snapshots:
hookable@5.5.3: {}
hookified@1.2.0: {}
hosted-git-info@2.8.9: {}
htmlparser2@9.1.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
entities: 4.5.0
http-errors@2.0.0:
dependencies:
depd: 2.0.0
@ -7018,6 +7217,10 @@ snapshots:
hyphenate-style-name@1.1.0: {}
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.2: {}
@ -7216,6 +7419,10 @@ snapshots:
dependencies:
json-buffer: 3.0.1
keyv@5.0.3:
dependencies:
'@keyv/serialize': 1.0.1
klona@2.0.6: {}
knitwork@1.1.0: {}
@ -7415,7 +7622,7 @@ snapshots:
natural-compare@1.4.0: {}
nitropack@2.9.7(magicast@0.3.5):
nitropack@2.9.7(encoding@0.1.13)(magicast@0.3.5):
dependencies:
'@cloudflare/kv-asset-handler': 0.3.4
'@netlify/functions': 2.8.1
@ -7428,7 +7635,7 @@ snapshots:
'@rollup/plugin-terser': 0.4.4(rollup@4.22.4)
'@rollup/pluginutils': 5.1.2(rollup@4.22.4)
'@types/http-proxy': 1.17.15
'@vercel/nft': 0.26.5
'@vercel/nft': 0.26.5(encoding@0.1.13)
archiver: 7.0.1
c12: 1.11.2(magicast@0.3.5)
chalk: 5.3.0
@ -7511,9 +7718,11 @@ snapshots:
node-fetch-native@1.6.4: {}
node-fetch@2.7.0:
node-fetch@2.7.0(encoding@0.1.13):
dependencies:
whatwg-url: 5.0.0
optionalDependencies:
encoding: 0.1.13
node-fetch@3.3.2:
dependencies:
@ -7666,6 +7875,19 @@ snapshots:
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
parse5-htmlparser2-tree-adapter@7.0.0:
dependencies:
domhandler: 5.0.3
parse5: 7.1.2
parse5-parser-stream@7.1.2:
dependencies:
parse5: 7.1.2
parse5@7.1.2:
dependencies:
entities: 4.5.0
parseurl@1.3.3: {}
path-exists@4.0.0: {}
@ -7918,6 +8140,8 @@ snapshots:
safe-buffer@5.2.1: {}
safer-buffer@2.1.2: {}
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
@ -8118,6 +8342,8 @@ snapshots:
dependencies:
js-tokens: 9.0.0
strnum@1.0.5: {}
stylis@4.3.4: {}
supports-color@5.5.0:
@ -8290,6 +8516,8 @@ snapshots:
dependencies:
'@fastify/busboy': 2.1.1
undici@6.19.8: {}
unenv@1.10.0:
dependencies:
consola: 3.2.3
@ -8432,13 +8660,13 @@ snapshots:
- supports-color
- terser
vite-plugin-with-nitro@0.0.0-beta.4(magicast@0.3.5)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)):
vite-plugin-with-nitro@0.0.0-beta.4(encoding@0.1.13)(magicast@0.3.5)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)):
dependencies:
esbuild: 0.24.0
fast-glob: 3.3.2
front-matter: 4.0.2
h3: 1.12.0
nitropack: 2.9.7(magicast@0.3.5)
nitropack: 2.9.7(encoding@0.1.13)(magicast@0.3.5)
vite: 5.4.8(@types/node@22.6.1)(terser@5.33.0)
xmlbuilder2: 3.1.1
transitivePeerDependencies:
@ -8538,6 +8766,12 @@ snapshots:
webpack-virtual-modules@0.6.2: {}
whatwg-encoding@3.1.1:
dependencies:
iconv-lite: 0.6.3
whatwg-mimetype@4.0.0: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3

9
server/cache.ts Normal file
View File

@ -0,0 +1,9 @@
import { FlatCache } from "flat-cache"
// init
export const cache = new FlatCache({
ttl: 60 * 60 * 1000, // 1 hour
lruSize: 10000, // 10,000 items
expirationInterval: 5 * 1000 * 60, // 5 minutes
persistInterval: 5 * 1000 * 60, // 5 minutes
})

View File

@ -1,10 +1,24 @@
import { defineEventHandler, getQuery, getRouterParam, sendProxy } from "h3"
import { defineEventHandler, getRouterParam } from "h3"
import { fallback, sources } from "#/sources"
// import { cache } from "#/cache"
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id")
const { latest } = getQuery(event)
// https://api-hot.efefee.cn/weibo?cache=false
// https://smzdk.top/api/${id}/new
if (latest !== undefined) return await sendProxy(event, `https://api-hot.efefee.cn/${id}?cache=false`)
return await sendProxy(event, `https://api-hot.efefee.cn/${id}?cache=true`)
const id = getRouterParam(event, "id") as keyof typeof sources
// const { latest } = getQuery(event)
// console.log(id, latest)
if (!id) throw new Error("Invalid source id")
// if (!latest) {
// const _ = cache.get(id)
// if (_) return _
// }
if (!sources[id]) {
const _ = await fallback(id)
// cache.set(id, _)
return _
} else {
const _ = await sources[id]()
// cache.set(id, _)
return _
}
})

View File

@ -0,0 +1,41 @@
import type { OResponse } from "@shared/types"
import { $fetch } from "ofetch"
export interface Res {
code: number
message: string
name: string
title: string
subtitle: string
total: number
updateTime: string
data: {
title: string
desc: string
time?: string
url: string
mobileUrl: string
}[]
}
export async function fallback(id: string): Promise<OResponse> {
const res: Res = await $fetch(`https://smzdk.top/api/${id}/new`)
if (res.code !== 200 || !res.data) throw new Error(res.message)
return {
status: "success",
data: {
name: res.title,
type: res.subtitle,
updateTime: res.updateTime,
items: res.data.map(item => ({
extra: {
date: item.time,
},
id: item.url,
title: item.title,
url: item.url,
mobileUrl: item.mobileUrl,
})),
},
}
}

View File

@ -0,0 +1,11 @@
import { peopledaily } from "./peopledaily"
import { weibo } from "./weibo"
import { zaobao } from "./zaobao"
export { fallback } from "./fallback"
export const sources = {
peopledaily,
weibo,
zaobao: () => zaobao("中国聚焦"),
}

View File

@ -0,0 +1,20 @@
import type { OResponse, RSS2JSON } from "@shared/types"
import { rss2json } from "#/utils/rss2json"
export async function peopledaily(): Promise<OResponse> {
const source = await rss2json("https://feedx.net/rss/people.xml")
if (!source?.items.length) throw new Error("Cannot fetch data")
return {
status: "success",
data: {
name: "人民日报",
type: "报纸",
updateTime: Date.now(),
items: source.items.slice(0, 30).map((item: RSS2JSON) => ({
title: item.title,
url: item.link,
id: item.link,
})),
},
}
}

View File

@ -0,0 +1,54 @@
import type { OResponse } from "@shared/types"
import { $fetch } from "ofetch"
interface Res {
ok: number // 1 is ok
data: {
realtime:
{
num: number // 看上去是个 id
emoticon: string
icon?: string // 热,新 icon url
icon_width: number
icon_height: number
note: string
small_icon_desc: string
icon_desc?: string // 如果是 荐 ,就是广告
topic_flag: number
icon_desc_color: string
flag: number
word_scheme: string
small_icon_desc_color: string
realpos: number
label_name: string
word: string // 热搜词
rank: number
}[]
}
}
export async function weibo(): Promise<OResponse> {
const url = "https://weibo.com/ajax/side/hotSearch"
const res: Res = await $fetch(url)
if (!res.ok || res.data.realtime.length === 0) throw new Error("Cannot fetch data")
return {
status: "success",
data: {
name: "微博热搜",
updateTime: Date.now(),
type: "热搜",
items: res.data.realtime.filter(k => !k.icon_desc || k.icon_desc !== "荐").map((k) => {
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
return {
id: k.num,
title: k.word,
extra: {
icon: k.icon,
},
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(keyword)}`,
mobileUrl: `https://m.weibo.cn/search?containerid=231522type%3D1%26q%3D${encodeURIComponent(keyword)}&_T_WM=16922097837&v_p=42`,
}
}),
},
}
}

54
server/sources/zaobao.ts Normal file
View File

@ -0,0 +1,54 @@
import { Buffer } from "node:buffer"
import { $fetch } from "ofetch"
import * as cheerio from "cheerio"
import iconv from "iconv-lite"
import type { NewsItem, OResponse } from "@shared/types"
import { tranformToUTC } from "#/utils/date"
const columns = [
"人物记事",
"观点评论",
"中国聚焦",
"香港澳门",
"台湾新闻",
"国际时政",
"国际军事",
"国际视野",
] as const
// type: "中国聚焦" | "人物记事" | "观点评论"
export async function zaobao(type: typeof columns[number] = "中国聚焦"): Promise<OResponse> {
const response = await $fetch("https://www.kzaobao.com/top.html", {
responseType: "arrayBuffer",
})
const base = "https://www.kzaobao.com"
const utf8String = iconv.decode(Buffer.from(response), "gb2312")
const $ = cheerio.load(utf8String)
const $main = $(`#cd0${columns.indexOf(type) + 1}`)
const news: NewsItem[] = []
$main.find("tr").each((_, el) => {
const a = $(el).find("h3>a")
// https://www.kzaobao.com/shiju/20241002/170659.html
const url = a.attr("href")
const title = a.text()
if (url && title) {
const date = $(el).find("td:nth-child(3)").text()
news.push({
url: base + url,
title,
id: url,
extra: {
date: date && tranformToUTC(date),
},
})
}
})
return {
status: "success",
data: {
name: `联合早报`,
type,
updateTime: Date.now(),
items: news,
},
}
}

14
server/utils/date.ts Normal file
View File

@ -0,0 +1,14 @@
import dayjs from "dayjs"
import utcPlugin from "dayjs/plugin/utc"
import timezonePlugin from "dayjs/plugin/timezone"
dayjs.extend(utcPlugin)
dayjs.extend(timezonePlugin)
/**
* UTC
*/
export function tranformToUTC(date: string, format?: string, timezone: string = "Asia/Shanghai"): number {
if (!format) return dayjs.tz(date, timezone).valueOf()
return dayjs.tz(date, format, timezone).valueOf()
}

1
server/utils/index.ts Normal file
View File

@ -0,0 +1 @@
import type { SourceInfo } from "@shared/types"

81
server/utils/rss2json.ts Normal file
View File

@ -0,0 +1,81 @@
import { XMLParser } from "fast-xml-parser"
import { $fetch } from "ofetch"
export async function rss2json(url: string) {
if (!/^https?:\/\/[^\s$.?#].\S*/i.test(url)) return null
const data = await $fetch(url)
const xml = new XMLParser({
attributeNamePrefix: "",
textNodeName: "$text",
ignoreAttributes: false,
})
const result = xml.parse(data as string)
let channel = result.rss && result.rss.channel ? result.rss.channel : result.feed
if (Array.isArray(channel)) channel = channel[0]
const rss = {
title: channel.title ?? "",
description: channel.description ?? "",
link: channel.link && channel.link.href ? channel.link.href : channel.link,
image: channel.image ? channel.image.url : channel["itunes:image"] ? channel["itunes:image"].href : "",
category: channel.category || [],
items: [],
}
let items = channel.item || channel.entry || []
if (items && !Array.isArray(items)) items = [items]
for (let i = 0; i < items.length; i++) {
const val = items[i]
const media = {}
const obj = {
id: val.guid && val.guid.$text ? val.guid.$text : val.id,
title: val.title && val.title.$text ? val.title.$text : val.title,
description: val.summary && val.summary.$text ? val.summary.$text : val.description,
link: val.link && val.link.href ? val.link.href : val.link,
author: val.author && val.author.name ? val.author.name : val["dc:creator"],
published: val.created ? Date.parse(val.created) : val.pubDate ? Date.parse(val.pubDate) : Date.now(),
created: val.updated ? Date.parse(val.updated) : val.pubDate ? Date.parse(val.pubDate) : val.created ? Date.parse(val.created) : Date.now(),
category: val.category || [],
content: val.content && val.content.$text ? val.content.$text : val["content:encoded"],
enclosures: val.enclosure ? (Array.isArray(val.enclosure) ? val.enclosure : [val.enclosure]) : [],
};
["content:encoded", "podcast:transcript", "itunes:summary", "itunes:author", "itunes:explicit", "itunes:duration", "itunes:season", "itunes:episode", "itunes:episodeType", "itunes:image"].forEach((s) => {
// @ts-expect-error TODO
if (val[s]) obj[s.replace(":", "_")] = val[s]
})
if (val["media:thumbnail"]) {
Object.assign(media, { thumbnail: val["media:thumbnail"] })
obj.enclosures.push(val["media:thumbnail"])
}
if (val["media:content"]) {
Object.assign(media, { thumbnail: val["media:content"] })
obj.enclosures.push(val["media:content"])
}
if (val["media:group"]) {
if (val["media:group"]["media:title"]) obj.title = val["media:group"]["media:title"]
if (val["media:group"]["media:description"]) obj.description = val["media:group"]["media:description"]
if (val["media:group"]["media:thumbnail"]) obj.enclosures.push(val["media:group"]["media:thumbnail"].url)
if (val["media:group"]["media:content"]) obj.enclosures.push(val["media:group"]["media:content"])
}
Object.assign(obj, { media })
// @ts-expect-error TODO
rss.items.push(obj)
}
return rss
}

View File

@ -10,24 +10,34 @@ export interface Section {
}
export interface NewsItem {
id: string | number // unique
title: string
cover?: string
author?: string
desc?: string
url: string
timestamp?: number
mobileUrl?: string
extra?: Record<string, any>
}
// 路由数据
export interface SourceInfo {
name: string
title: string
type: string
description?: string
params?: Record<string, string | object>
total: number
link?: string
updateTime: string
data: NewsItem[]
updateTime: number | string
items: NewsItem[]
}
export type OResponse = {
status: "success" | "cache"
data: SourceInfo
} | {
status: "error"
message?: string
}
export interface RSS2JSON {
id?: string
title: string
description: string
link: string
published: number
created: number
}

View File

@ -1,4 +1,4 @@
import type { SourceID, SourceInfo } from "@shared/types"
import type { OResponse, SourceID, SourceInfo } from "@shared/types"
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
import type { UseQueryResult } from "@tanstack/react-query"
import { useQuery } from "@tanstack/react-query"
@ -6,7 +6,7 @@ import { relativeTime } from "@shared/utils"
import clsx from "clsx"
import { useInView } from "react-intersection-observer"
import { useAtom } from "jotai"
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react"
import { sources } from "@shared/data"
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
import { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
@ -45,7 +45,7 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
<div
ref={ref}
className={clsx(
"flex flex-col bg-base border rounded-md h-500px",
"flex flex-col bg-base border rounded-md h-450px border-gray-500/40",
isDragged && "op-50",
isOverlay ? "bg-glass" : "",
)}
@ -71,8 +71,12 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
if (Date.now() - _refetchTime < 1000) {
url = `/api/${_id}?latest`
}
const response = await fetch(url)
return await response.json() as SourceInfo
const response = await fetch(url).then(res => res.json())
if (response.status === "error") {
throw new Error(response.message)
} else {
return response.data as SourceInfo
}
},
// refetch 时显示原有的数据
placeholderData: prev => prev,
@ -125,7 +129,7 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
className={clsx("i-ph:arrow-clockwise", query.isFetching && "animate-spin")}
onClick={manualRefetch}
/>
<button type="button" className={clsx(focusSources.includes(id) ? "i-ph:star-fill" : "i-ph:star")} onClick={addFocusList} />
<button type="button" className={clsx(focusSources.includes(id) ? "i-ph:star-fill" : "i-ph:star", "color-primary")} onClick={addFocusList} />
</div>
</div>
</>
@ -154,7 +158,7 @@ function Num({ num }: { num: number }) {
}
function NewsList({ query }: Query) {
const items = query.data?.data
const items = query.data?.items
if (items?.length) {
return (
<>
@ -162,12 +166,12 @@ function NewsList({ query }: Query) {
<div key={item.title} className="flex gap-2 items-center">
<Num num={i + 1} />
<a href={item.url} target="_blank" className="my-1 w-full flex items-center justify-between flex-wrap">
<span className="flex-1 mr-2">
<span className="flex-1 mr-2 hover:(underline underline-offset-4)">
{item.title}
</span>
{item.timestamp && (
{item?.extra?.date && (
<span className="text-xs text-gray-4/80">
{relativeTime(item.timestamp)}
{relativeTime(item.extra.date)}
</span>
)}
</a>

24
test/common.test.ts Normal file
View File

@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest"
import { tranformToUTC } from "#/utils/date"
describe("transform Beijing time to UTC in different timezone", () => {
const a = "2024/10/3 12:26:16"
const b = 1727929576000
it("in UTC", () => {
Object.assign(process.env, { TZ: "UTC" })
const date = tranformToUTC(a)
expect(date).toBe(b)
})
it("in Beijing", () => {
Object.assign(process.env, { TZ: "Asia/Shanghai" })
const date = tranformToUTC(a)
expect(date).toBe(b)
})
it("in New York", () => {
Object.assign(process.env, { TZ: "America/New_York" })
const date = tranformToUTC(a)
expect(date).toBe(b)
})
})

View File

@ -1,11 +0,0 @@
import { metadata } from "@shared/data"
import { describe, expect, it } from "vitest"
// 通过两次 diff 来找出数组的差异
describe("data", () => {
it.for(Object.entries(metadata))(` "%s" source list shoule be unique`, ([, { sourceList }]) => {
if (sourceList) {
expect(new Set(sourceList).size).toBe(sourceList.length)
}
})
})

9
test/rss2json.test.ts Normal file
View File

@ -0,0 +1,9 @@
import { it } from "vitest"
import { zaobao } from "#/sources/zaobao"
it.skip("res", {
timeout: 10000,
}, async () => {
const res = await zaobao()
console.log(res)
})

View File

@ -1,5 +1,5 @@
{
"extends": ["./tsconfig.base.json", "./dist/.nitro/types/tsconfig.json"],
"extends": ["./tsconfig.base.json"],
"compilerOptions": {
"lib": ["ES2020"],
"baseUrl": ".",

View File

@ -22,14 +22,11 @@ export default defineConfig({
srcDir: "server",
alias: {
"@shared": fileURLToPath(new URL("shared", import.meta.url)),
"#": fileURLToPath(new URL("server", import.meta.url)),
},
runtimeConfig: {
// apiPrefix: "",
},
typescript: {
generateTsConfig: true,
},
minify: false,
preset: process.env.VERCEL ? "vercel-edge" : "node-server",
}),
],