deploy: sync server CloudSearch 0.5.6

This commit is contained in:
2026-06-25 02:08:39 +08:00
parent 75f5d26964
commit ce3f95b8b9
53 changed files with 2740 additions and 116 deletions
+39 -4
View File
@@ -3,16 +3,51 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="description" content="随便下 | 资源小站,提供网盘资源搜索、热门榜单和免责声明页面。" />
<meta name="robots" content="index,follow" />
<meta property="og:type" content="website" />
<meta property="og:title" content="CloudSearch - 网盘资源搜索" />
<meta property="og:description" content="随便下 | 资源小站,提供网盘资源搜索、热门榜单和免责声明页面。" />
<meta property="og:url" content="https://zy.hk.timxx.cn/" />
<meta property="og:image" content="https://zy.hk.timxx.cn/api/uploads/logo/site_logo.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="CloudSearch - 网盘资源搜索" />
<meta name="twitter:description" content="随便下 | 资源小站,提供网盘资源搜索、热门榜单和免责声明页面。" />
<meta name="twitter:image" content="https://zy.hk.timxx.cn/api/uploads/logo/site_logo.png" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link rel="canonical" href="https://zy.hk.timxx.cn/" />
<title>CloudSearch - 网盘资源搜索</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<script>
(function() {
fetch('/api/site-config').then(function(r){return r.json()}).then(function(cfg){
if(cfg.site_name) document.title = cfg.site_name + ' - 网盘资源搜索';
}).catch(function(){});
function setMeta(selector, attr, value) {
var el = document.querySelector(selector);
if (!el) return;
el.setAttribute(attr, value);
}
function sync(cfg) {
var origin = window.location.origin;
var siteName = cfg.site_name || 'CloudSearch';
var title = siteName + ' - 网盘资源搜索';
var description = cfg.site_disclaimer || '随便下 | 资源小站,提供网盘资源搜索、热门榜单和免责声明页面。';
var image = cfg.site_logo ? new URL(cfg.site_logo, origin).href : origin + '/api/uploads/logo/site_logo.png';
var canonical = origin + '/';
document.title = title;
setMeta('meta[name="description"]', 'content', description);
setMeta('meta[name="robots"]', 'content', 'index,follow');
setMeta('meta[property="og:title"]', 'content', title);
setMeta('meta[property="og:description"]', 'content', description);
setMeta('meta[property="og:url"]', 'content', canonical);
setMeta('meta[property="og:image"]', 'content', image);
setMeta('meta[name="twitter:title"]', 'content', title);
setMeta('meta[name="twitter:description"]', 'content', description);
setMeta('meta[name="twitter:image"]', 'content', image);
setMeta('link[rel="canonical"]', 'href', canonical);
}
sync({});
fetch('/api/site-config').then(function(r){return r.json()}).then(function(cfg){ sync(cfg || {}); }).catch(function(){});
})();
</script>
</head>
@@ -20,4 +55,4 @@
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
</html>
+387
View File
@@ -23,6 +23,7 @@
"@vitejs/plugin-vue": "^5.1.0",
"typescript": "^5.6.0",
"vite": "^5.4.0",
"vitest": "^2.1.9",
"vue-tsc": "^2.1.0"
}
},
@@ -869,6 +870,121 @@
"vue": "^3.2.25"
}
},
"node_modules/@vitest/expect": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
"integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==",
"dev": true,
"dependencies": {
"@vitest/spy": "2.1.9",
"@vitest/utils": "2.1.9",
"chai": "^5.1.2",
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz",
"integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==",
"dev": true,
"dependencies": {
"@vitest/spy": "2.1.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.12"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^5.0.0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"node_modules/@vitest/mocker/node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/@vitest/pretty-format": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz",
"integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==",
"dev": true,
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz",
"integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==",
"dev": true,
"dependencies": {
"@vitest/utils": "2.1.9",
"pathe": "^1.1.2"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz",
"integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==",
"dev": true,
"dependencies": {
"@vitest/pretty-format": "2.1.9",
"magic-string": "^0.30.12",
"pathe": "^1.1.2"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz",
"integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==",
"dev": true,
"dependencies": {
"tinyspy": "^3.0.2"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz",
"integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==",
"dev": true,
"dependencies": {
"@vitest/pretty-format": "2.1.9",
"loupe": "^3.1.2",
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@volar/language-core": {
"version": "2.4.15",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz",
@@ -1086,6 +1202,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
@@ -1121,6 +1246,15 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -1141,6 +1275,31 @@
"node": ">=6"
}
},
"node_modules/chai": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
"dev": true,
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
"deep-eql": "^5.0.1",
"loupe": "^3.1.0",
"pathval": "^2.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/check-error": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
"integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
"dev": true,
"engines": {
"node": ">= 16"
}
},
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@@ -1194,6 +1353,23 @@
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
@@ -1202,6 +1378,15 @@
"node": ">=0.10.0"
}
},
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1294,6 +1479,12 @@
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -1362,6 +1553,15 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
"dev": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -1568,6 +1768,12 @@
"lodash-es": "*"
}
},
"node_modules/loupe": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
"dev": true
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1623,6 +1829,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
@@ -1698,6 +1910,21 @@
"node": ">=8"
}
},
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
"dev": true
},
"node_modules/pathval": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
"dev": true,
"engines": {
"node": ">= 14.16"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1845,6 +2072,12 @@
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
},
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -1853,6 +2086,18 @@
"node": ">=0.10.0"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true
},
"node_modules/std-env": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
"dev": true
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -1877,6 +2122,45 @@
"node": ">=8"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
"dev": true
},
"node_modules/tinyexec": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"dev": true
},
"node_modules/tinypool": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
"integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
"dev": true,
"engines": {
"node": "^18.0.0 || >=20.0.0"
}
},
"node_modules/tinyrainbow": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz",
"integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==",
"dev": true,
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tinyspy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
"dev": true,
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
@@ -1960,6 +2244,93 @@
}
}
},
"node_modules/vite-node": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz",
"integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.3.7",
"es-module-lexer": "^1.5.4",
"pathe": "^1.1.2",
"vite": "^5.0.0"
},
"bin": {
"vite-node": "vite-node.mjs"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vitest": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz",
"integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==",
"dev": true,
"dependencies": {
"@vitest/expect": "2.1.9",
"@vitest/mocker": "2.1.9",
"@vitest/pretty-format": "^2.1.9",
"@vitest/runner": "2.1.9",
"@vitest/snapshot": "2.1.9",
"@vitest/spy": "2.1.9",
"@vitest/utils": "2.1.9",
"chai": "^5.1.2",
"debug": "^4.3.7",
"expect-type": "^1.1.0",
"magic-string": "^0.30.12",
"pathe": "^1.1.2",
"std-env": "^3.8.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.1",
"tinypool": "^1.0.1",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
"vite-node": "2.1.9",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "2.1.9",
"@vitest/ui": "2.1.9",
"happy-dom": "*",
"jsdom": "*"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
}
}
},
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
@@ -2060,6 +2431,22 @@
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dev": true,
"dependencies": {
"siginfo": "^2.0.0",
"stackback": "0.0.2"
},
"bin": {
"why-is-node-running": "cli.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+3 -1
View File
@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest run",
"preview": "vite preview"
},
"dependencies": {
@@ -24,6 +25,7 @@
"@vitejs/plugin-vue": "^5.1.0",
"typescript": "^5.6.0",
"vite": "^5.4.0",
"vitest": "^2.1.9",
"vue-tsc": "^2.1.0"
}
}
}
@@ -0,0 +1,3 @@
User-agent: *
Allow: /
Sitemap: https://zy.hk.timxx.cn/sitemap.xml
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://zy.hk.timxx.cn/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://zy.hk.timxx.cn/search</loc>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://zy.hk.timxx.cn/disclaimer/</loc>
<changefreq>monthly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://zy.hk.timxx.cn/admin/login</loc>
<changefreq>monthly</changefreq>
<priority>0.2</priority>
</url>
<url>
<loc>https://zy.hk.timxx.cn/user/login</loc>
<changefreq>monthly</changefreq>
<priority>0.2</priority>
</url>
</urlset>
+43
View File
@@ -3,6 +3,49 @@
</template>
<script setup lang="ts">
import { onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { applySeo, resolveSeo } from './seo'
interface SiteConfig {
site_name?: string
site_logo?: string
site_disclaimer?: string
}
const route = useRoute()
let currentSiteName = 'CloudSearch'
let currentSiteLogo = ''
let currentSiteDisclaimer = ''
function refreshSeo() {
const origin = typeof window !== 'undefined' ? window.location.origin : ''
applySeo(resolveSeo(route.path, route.query as Record<string, unknown>, currentSiteName, origin, currentSiteLogo, currentSiteDisclaimer))
}
async function loadSiteConfig() {
try {
const resp = await fetch('/api/site-config')
if (!resp.ok) return
const cfg = (await resp.json()) as SiteConfig
if (cfg.site_name) currentSiteName = cfg.site_name
if (cfg.site_logo) currentSiteLogo = cfg.site_logo
if (cfg.site_disclaimer) currentSiteDisclaimer = cfg.site_disclaimer
} catch {
// keep defaults
}
}
onMounted(async () => {
await loadSiteConfig()
refreshSeo()
})
watch(
() => route.fullPath,
() => refreshSeo(),
{ immediate: true },
)
</script>
<style>
@@ -0,0 +1,42 @@
<template>
<main class="disclaimer-page">
<section class="disclaimer-card">
<h1>免责声明</h1>
<p>CloudSearch 仅聚合和展示公开网络资源线索不存储不上传不分发任何网盘文件内容</p>
<p>搜索结果来自第三方公开页面或接口资源有效性合法性与安全性需由用户自行判断</p>
<p>请勿使用本站搜索保存或传播侵权违法违规内容如权利人认为相关信息侵犯权益请联系站点管理员处理</p>
<router-link class="back-link" to="/">返回首页</router-link>
</section>
</main>
</template>
<style scoped>
.disclaimer-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 32px 16px;
background: #f8fafc;
}
.disclaimer-card {
max-width: 760px;
padding: 32px;
border-radius: 18px;
background: #fff;
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08);
color: #334155;
line-height: 1.8;
}
.disclaimer-card h1 {
margin: 0 0 20px;
color: #0f172a;
}
.back-link {
display: inline-flex;
margin-top: 20px;
color: #2563eb;
text-decoration: none;
font-weight: 600;
}
</style>
@@ -1,12 +1,13 @@
<template>
<div class="home-page">
<div class="hero-section">
<h1 class="sr-only">网盘资源搜索</h1>
<template v-if="configLoaded">
<img v-if="siteLogo" :src="siteLogo" :alt="siteName || 'CloudSearch'" class="logo-img" @error="(e: any) => { (e.target as HTMLElement).style.display='none'; siteLogo='' }" />
<div v-else class="logo-text">{{ siteName || 'CloudSearch' }}</div>
</template>
<div class="search-box">
<el-input v-model="query" placeholder="搜索网盘资源..." size="large" clearable @keyup.enter="handleSearch">
<el-input v-model="query" aria-label="搜索网盘资源" name="q" placeholder="搜索网盘资源..." size="large" clearable @keyup.enter="handleSearch">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<el-button type="primary" size="large" @click="handleSearch" class="search-btn">搜索</el-button>
@@ -27,6 +28,7 @@
<div class="footer-inner">{{ siteDisclaimer }}</div>
<el-button class="footer-btn" size="small" @click="openDisclaimer">免责声明</el-button>
</div>
</div>
</template>
@@ -35,6 +37,7 @@ import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Search } from '@element-plus/icons-vue'
import { getCategorizedRankings, getSiteConfig } from '../api'
import { createDisclaimerTarget, createSearchPath } from '../utils/home-helpers'
const router = useRouter()
const query = ref('')
@@ -50,9 +53,9 @@ const QS = ['学而时习之,不亦说乎。', '温故而知新,可以为师
const CS = ['#e74c3c', '#e67e22', '#f39c12', '#27ae60', '#2980b9', '#8e44ad', '#2c3e50', '#16a085', '#d35400', '#c0392b', '#1abc9c', '#3498db', '#9b59b6', '#34495e', '#e91e63', '#009688', '#ff5722', '#795548', '#607d8b', '#673ab7']
const SS = [48,42,38,34,30,28,26,24,22,20,18,17,16,15,14,13,12]
function handleSearch() { const q = query.value.trim(); if(q) router.push('/search?q='+encodeURIComponent(q)) }
function searchTag(t:string) { router.push('/search?q='+encodeURIComponent(t)) }
function openDisclaimer() { window.open('/disclaimer/', '_blank') }
function handleSearch() { const path = createSearchPath(query.value); if(path) router.push(path) }
function searchTag(t:string) { const path = createSearchPath(t); if(path) router.push(path) }
function openDisclaimer() { window.location.assign(createDisclaimerTarget()) }
onMounted(async () => {
currentQuote.value = QS[Math.floor(Math.random()*QS.length)]
@@ -81,6 +84,7 @@ onMounted(async () => {
<style scoped>
.home-page{min-height:100vh;background:#f5f7fa}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.hero-section{text-align:center;padding:56px 24px 36px;background:#f5f7fa}
.logo-text{font-size:48px;font-weight:700;color:#1d2129;margin-bottom:24px}
.logo-img{display:block;max-width:400px;max-height:100px;margin:0 auto 24px}
@@ -0,0 +1,27 @@
<template>
<main class="not-found-page">
<section class="not-found-card">
<p class="eyebrow">404</p>
<h1>页面不存在</h1>
<p class="description">你访问的页面不存在或已被移动</p>
<el-button type="primary" @click="goHome">返回首页</el-button>
</section>
</main>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
function goHome() {
router.push('/')
}
</script>
<style scoped>
.not-found-page{min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f5f7fa;padding:24px}
.not-found-card{width:min(460px,100%);padding:40px 32px;text-align:center;background:#fff;border-radius:18px;box-shadow:0 12px 32px rgba(31,45,61,.08)}
.eyebrow{margin:0 0 8px;color:#409eff;font-size:14px;font-weight:700;letter-spacing:4px}
h1{margin:0 0 12px;font-size:28px;color:#1d2129}
.description{margin:0 0 24px;color:#606266;line-height:1.7}
</style>
+14 -3
View File
@@ -1,6 +1,6 @@
import { createRouter, createWebHistory } from 'vue-router'
import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router'
const routes = [
export const routes = [
{
path: '/',
name: 'home',
@@ -16,6 +16,12 @@ const routes = [
name: 'result-detail',
component: () => import('./pages/ResultDetail.vue'),
},
{
path: '/disclaimer',
alias: '/disclaimer/',
name: 'disclaimer',
component: () => import('./pages/Disclaimer.vue'),
},
{
path: '/admin/login',
name: 'admin-login',
@@ -71,10 +77,15 @@ const routes = [
path: '/user',
redirect: '/user/login',
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('./pages/NotFound.vue'),
},
]
const router = createRouter({
history: createWebHistory(),
history: typeof window === 'undefined' ? createMemoryHistory() : createWebHistory(),
routes,
})
+25
View File
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { resolveSeo } from './seo'
describe('resolveSeo', () => {
it('builds home page seo for the site root', () => {
const seo = resolveSeo('/', {}, '随便下 | 资源小站', 'https://zy.hk.timxx.cn')
expect(seo.title).toBe('随便下 | 资源小站 - 网盘资源搜索')
expect(seo.description).toContain('网盘资源搜索')
expect(seo.canonical).toBe('https://zy.hk.timxx.cn/')
expect(seo.robots).toBe('index,follow')
})
it('builds search page seo from the q parameter', () => {
const seo = resolveSeo('/search', { q: '阿凡达' }, '随便下 | 资源小站', 'https://zy.hk.timxx.cn')
expect(seo.title).toBe('阿凡达 - 搜索结果 | 随便下 | 资源小站')
expect(seo.description).toContain('阿凡达')
expect(seo.canonical).toBe('https://zy.hk.timxx.cn/search?q=%E9%98%BF%E5%87%A1%E8%BE%BE')
})
it('marks admin pages as noindex', () => {
const seo = resolveSeo('/admin/login', {}, '随便下 | 资源小站', 'https://zy.hk.timxx.cn')
expect(seo.robots).toBe('noindex,nofollow')
expect(seo.title).toContain('管理后台')
})
})
+125
View File
@@ -0,0 +1,125 @@
export interface SeoState {
title: string;
description: string;
canonical: string;
image: string;
robots: string;
}
function getSearchQuery(query: Record<string, unknown>): string {
const q = query.q;
if (typeof q === 'string') return q.trim();
const input = query.input;
if (typeof input === 'string') return input.trim();
return '';
}
function normalizePath(pathname: string): string {
if (!pathname) return '/';
if (!pathname.startsWith('/')) return `/${pathname}`;
return pathname;
}
function buildCanonical(origin: string, pathname: string, query: Record<string, unknown>): string {
const base = origin ? origin.replace(/\/+$/, '') : '';
const normalizedPath = normalizePath(pathname);
const params = new URLSearchParams();
if (normalizedPath.startsWith('/search')) {
const q = getSearchQuery(query);
if (q) params.set('q', q);
}
const suffix = params.toString() ? `?${params.toString()}` : '';
return `${base}${normalizedPath}${suffix}` || normalizedPath;
}
export function resolveSeo(
pathname: string,
query: Record<string, unknown> = {},
siteName = 'CloudSearch',
origin = '',
siteLogo = '',
siteDisclaimer = '',
): SeoState {
const normalizedPath = normalizePath(pathname);
const q = getSearchQuery(query);
const titleSuffix = `${siteName} - 网盘资源搜索`;
let title = titleSuffix;
let description = `${siteName} 提供网盘资源搜索、热门榜单和内容聚合。`;
let robots = 'index,follow';
if (normalizedPath.startsWith('/search')) {
if (q) {
title = `${q} - 搜索结果 | ${siteName}`;
description = `搜索 ${q} 的网盘资源结果,查看链接、分类和保存记录。`;
} else {
title = `搜索结果 | ${siteName}`;
description = `${siteName} 的网盘资源搜索结果页。`;
}
} else if (normalizedPath.startsWith('/result/')) {
title = `资源详情 | ${siteName}`;
description = `查看资源详情、链接信息和关联内容。`;
} else if (normalizedPath.startsWith('/disclaimer')) {
title = `免责声明 | ${siteName}`;
description = siteDisclaimer || `查看 ${siteName} 的免责声明和使用说明。`;
} else if (normalizedPath.startsWith('/admin')) {
title = `管理后台 | ${siteName}`;
description = `访问 ${siteName} 的管理后台。`;
robots = 'noindex,nofollow';
} else if (normalizedPath.startsWith('/user')) {
title = `用户中心 | ${siteName}`;
description = `访问 ${siteName} 的用户中心。`;
robots = 'noindex,nofollow';
}
const canonical = buildCanonical(origin, normalizedPath, query);
const image = siteLogo
? new URL(siteLogo, origin || 'https://zy.hk.timxx.cn').href
: `${origin ? origin.replace(/\/+$/, '') : ''}/api/uploads/logo/site_logo.png` || '/api/uploads/logo/site_logo.png';
return {
title,
description,
canonical,
image,
robots,
};
}
function setMeta(selector: string, attr: string, value: string): void {
if (typeof document === 'undefined') return;
let el = document.querySelector(selector) as HTMLMetaElement | HTMLLinkElement | null;
if (!el) {
el = document.createElement(selector.startsWith('link') ? 'link' : 'meta');
if (selector.startsWith('meta[name="')) {
const name = selector.match(/meta\[name="([^"]+)"\]/)?.[1];
if (name) (el as HTMLMetaElement).setAttribute('name', name);
}
if (selector.startsWith('meta[property="')) {
const property = selector.match(/meta\[property="([^"]+)"\]/)?.[1];
if (property) (el as HTMLMetaElement).setAttribute('property', property);
}
if (selector.startsWith('link[')) {
const rel = selector.match(/link\[rel="([^"]+)"\]/)?.[1];
if (rel) (el as HTMLLinkElement).setAttribute('rel', rel);
}
document.head.appendChild(el);
}
el.setAttribute(attr, value);
}
export function applySeo(seo: SeoState): void {
if (typeof document === 'undefined') return;
document.title = seo.title;
setMeta('meta[name="description"]', 'content', seo.description);
setMeta('meta[name="robots"]', 'content', seo.robots);
setMeta('meta[property="og:title"]', 'content', seo.title);
setMeta('meta[property="og:description"]', 'content', seo.description);
setMeta('meta[property="og:type"]', 'content', 'website');
setMeta('meta[property="og:url"]', 'content', seo.canonical);
setMeta('meta[property="og:image"]', 'content', seo.image);
setMeta('meta[name="twitter:card"]', 'content', 'summary_large_image');
setMeta('meta[name="twitter:title"]', 'content', seo.title);
setMeta('meta[name="twitter:description"]', 'content', seo.description);
setMeta('meta[name="twitter:image"]', 'content', seo.image);
setMeta('link[rel="canonical"]', 'href', seo.canonical);
}
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { createDisclaimerTarget, createSearchPath, getHealthDisplay } from './home-helpers';
describe('home helpers', () => {
it('builds an encoded search path from trimmed user input', () => {
expect(createSearchPath(' 周杰伦 演唱会 ')).toBe('/search?q=%E5%91%A8%E6%9D%B0%E4%BC%A6%20%E6%BC%94%E5%94%B1%E4%BC%9A');
expect(createSearchPath(' ')).toBeNull();
});
it('uses same-tab navigation for the full disclaimer page', () => {
expect(createDisclaimerTarget()).toBe('/disclaimer');
});
it('marks optional video parser gaps as a test-environment note, not a core outage', () => {
const display = getHealthDisplay('ok', {
videoParser: 'unreachable',
videoParserRequired: false,
});
expect(display.isCoreHealthy).toBe(true);
expect(display.label).toBe('核心服务正常');
expect(display.note).toContain('视频解析');
expect(display.note).toContain('测试环境');
});
});
@@ -0,0 +1,35 @@
export interface HealthDisplayComponents {
videoParser?: string;
videoParserRequired?: boolean;
}
export interface HealthDisplay {
label: string;
isCoreHealthy: boolean;
note: string;
}
export function createSearchPath(query: string): string | null {
const trimmed = query.trim();
if (!trimmed) return null;
return `/search?q=${encodeURIComponent(trimmed)}`;
}
export function createDisclaimerTarget(path = '/disclaimer'): string {
return path;
}
export function getHealthDisplay(status: string, components: HealthDisplayComponents = {}): HealthDisplay {
const isCoreHealthy = status === 'ok' || (!components.videoParserRequired && status === 'degraded');
const label = isCoreHealthy ? '核心服务正常' : status === 'degraded' ? '部分服务降级' : '服务异常';
const videoParserOptionalGap = !components.videoParserRequired
&& components.videoParser
&& components.videoParser !== 'ok';
return {
label,
isCoreHealthy,
note: videoParserOptionalGap ? '视频解析仍在开发中,当前测试环境未启用,不影响核心搜索服务。' : '',
};
}
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { createRouter, createMemoryHistory } from 'vue-router';
import { routes } from '../router';
describe('application routes', () => {
function makeRouter() {
return createRouter({ history: createMemoryHistory(), routes });
}
it('resolves public and admin routes before the catch-all not-found route', () => {
const router = makeRouter();
expect(router.resolve('/').name).toBe('home');
expect(router.resolve('/search?q=test').name).toBe('search');
expect(router.resolve('/result/abc123').name).toBe('result-detail');
expect(router.resolve('/admin/login').name).toBe('admin-login');
expect(router.resolve('/admin/dashboard').matched.some(route => route.name === 'admin-dashboard')).toBe(true);
});
it('has an explicit disclaimer route instead of relying on the not-found fallback', () => {
const router = makeRouter();
expect(router.resolve('/disclaimer').name).toBe('disclaimer');
expect(router.resolve('/disclaimer/').name).toBe('disclaimer');
});
it('resolves unknown frontend routes to the not-found page', () => {
const router = makeRouter();
expect(router.resolve('/nonexistent-ui-audit').name).toBe('not-found');
expect(router.resolve('/unknown/path').name).toBe('not-found');
});
});
@@ -0,0 +1,14 @@
const KNOWN_PUBLIC_ROUTE_PATTERNS = [
/^\/$/,
/^\/search\/?$/,
/^\/result\/[^/]+\/?$/,
/^\/admin\/login\/?$/,
/^\/user\/login\/?$/,
/^\/user\/dashboard\/?$/,
/^\/user\/?$/,
];
export function isKnownPublicRoute(path: string): boolean {
const cleanPath = path.split('?')[0].split('#')[0] || '/';
return KNOWN_PUBLIC_ROUTE_PATTERNS.some(pattern => pattern.test(cleanPath));
}