Compare commits

..

1 Commits

Author SHA1 Message Date
wangdage12
b0a7b98091 Add MIT License to the project 2025-12-28 17:07:00 +08:00
27 changed files with 47 additions and 4438 deletions

View File

@@ -1 +0,0 @@
VITE_API_BASE_URL = http://localhost:5222/

View File

@@ -1 +0,0 @@
VITE_API_BASE_URL = https://htserver.wdg.cloudns.ch/api/

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 wangdage12
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

500
package-lock.json generated
View File

@@ -8,9 +8,6 @@
"name": "ht-web", "name": "ht-web",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@primer/css": "^22.1.0",
"@tsparticles/slim": "^3.9.1",
"@tsparticles/vue3": "^3.0.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"element-plus": "^2.13.0", "element-plus": "^2.13.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
@@ -1083,25 +1080,6 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@primer/css": {
"version": "22.1.0",
"resolved": "https://registry.npmjs.org/@primer/css/-/css-22.1.0.tgz",
"integrity": "sha512-Nwg9QaRiBeu0BU6h+Su0X07daihX1obiuqGRG8y+SexOnvWhN2J5n4OFAvGfQsit07Y7Q6gGoK+yVU5tb8CtDA==",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"@primer/primitives": "10.x || 11.x"
}
},
"node_modules/@primer/primitives": {
"version": "11.4.0",
"resolved": "https://registry.npmjs.org/@primer/primitives/-/primitives-11.4.0.tgz",
"integrity": "sha512-JIt98Fs0c8vhOw3uNf+sxqmvCdo0VoCZPBRg4frNK/xNpDMZsQh7V0Rp7wiGbr3f1w+4oqv40sfgaftMQTnwXQ==",
"license": "MIT",
"peer": true
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.53", "version": "1.0.0-beta.53",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
@@ -1417,484 +1395,6 @@
"win32" "win32"
] ]
}, },
"node_modules/@tsparticles/basic": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/basic/-/basic-3.9.1.tgz",
"integrity": "sha512-ijr2dHMx0IQHqhKW3qA8tfwrR2XYbbWYdaJMQuBo2CkwBVIhZ76U+H20Y492j/NXpd1FUnt2aC0l4CEVGVGdeQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1",
"@tsparticles/move-base": "3.9.1",
"@tsparticles/plugin-hex-color": "3.9.1",
"@tsparticles/plugin-hsl-color": "3.9.1",
"@tsparticles/plugin-rgb-color": "3.9.1",
"@tsparticles/shape-circle": "3.9.1",
"@tsparticles/updater-color": "3.9.1",
"@tsparticles/updater-opacity": "3.9.1",
"@tsparticles/updater-out-modes": "3.9.1",
"@tsparticles/updater-size": "3.9.1"
}
},
"node_modules/@tsparticles/engine": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/engine/-/engine-3.9.1.tgz",
"integrity": "sha512-DpdgAhWMZ3Eh2gyxik8FXS6BKZ8vyea+Eu5BC4epsahqTGY9V3JGGJcXC6lRJx6cPMAx1A0FaQAojPF3v6rkmQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"hasInstallScript": true,
"license": "MIT"
},
"node_modules/@tsparticles/interaction-external-attract": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-attract/-/interaction-external-attract-3.9.1.tgz",
"integrity": "sha512-5AJGmhzM9o4AVFV24WH5vSqMBzOXEOzIdGLIr+QJf4fRh9ZK62snsusv/ozKgs2KteRYQx+L7c5V3TqcDy2upg==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-external-bounce": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-bounce/-/interaction-external-bounce-3.9.1.tgz",
"integrity": "sha512-bv05+h70UIHOTWeTsTI1AeAmX6R3s8nnY74Ea6p6AbQjERzPYIa0XY19nq/hA7+Nrg+EissP5zgoYYeSphr85A==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-external-bubble": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-bubble/-/interaction-external-bubble-3.9.1.tgz",
"integrity": "sha512-tbd8ox/1GPl+zr+KyHQVV1bW88GE7OM6i4zql801YIlCDrl9wgTDdDFGIy9X7/cwTvTrCePhrfvdkUamXIribQ==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-external-connect": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-connect/-/interaction-external-connect-3.9.1.tgz",
"integrity": "sha512-sq8YfUNsIORjXHzzW7/AJQtfi/qDqLnYG2qOSE1WOsog39MD30RzmiOloejOkfNeUdcGUcfsDgpUuL3UhzFUOA==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-external-grab": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-grab/-/interaction-external-grab-3.9.1.tgz",
"integrity": "sha512-QwXza+sMMWDaMiFxd8y2tJwUK6c+nNw554+/9+tEZeTTk2fCbB0IJ7p/TH6ZGWDL0vo2muK54Njv2fEey191ow==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-external-pause": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-pause/-/interaction-external-pause-3.9.1.tgz",
"integrity": "sha512-Gzv4/FeNir0U/tVM9zQCqV1k+IAgaFjDU3T30M1AeAsNGh/rCITV2wnT7TOGFkbcla27m4Yxa+Fuab8+8pzm+g==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-external-push": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-push/-/interaction-external-push-3.9.1.tgz",
"integrity": "sha512-GvnWF9Qy4YkZdx+WJL2iy9IcgLvzOIu3K7aLYJFsQPaxT8d9TF8WlpoMlWKnJID6H5q4JqQuMRKRyWH8aAKyQw==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-external-remove": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-remove/-/interaction-external-remove-3.9.1.tgz",
"integrity": "sha512-yPThm4UDWejDOWW5Qc8KnnS2EfSo5VFcJUQDWc1+Wcj17xe7vdSoiwwOORM0PmNBzdDpSKQrte/gUnoqaUMwOA==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-external-repulse": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-repulse/-/interaction-external-repulse-3.9.1.tgz",
"integrity": "sha512-/LBppXkrMdvLHlEKWC7IykFhzrz+9nebT2fwSSFXK4plEBxDlIwnkDxd3FbVOAbnBvx4+L8+fbrEx+RvC8diAw==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-external-slow": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-external-slow/-/interaction-external-slow-3.9.1.tgz",
"integrity": "sha512-1ZYIR/udBwA9MdSCfgADsbDXKSFS0FMWuPWz7bm79g3sUxcYkihn+/hDhc6GXvNNR46V1ocJjrj0u6pAynS1KQ==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-particles-attract": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-attract/-/interaction-particles-attract-3.9.1.tgz",
"integrity": "sha512-CYYYowJuGwRLUixQcSU/48PTKM8fCUYThe0hXwQ+yRMLAn053VHzL7NNZzKqEIeEyt5oJoy9KcvubjKWbzMBLQ==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-particles-collisions": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-collisions/-/interaction-particles-collisions-3.9.1.tgz",
"integrity": "sha512-ggGyjW/3v1yxvYW1IF1EMT15M6w31y5zfNNUPkqd/IXRNPYvm0Z0ayhp+FKmz70M5p0UxxPIQHTvAv9Jqnuj8w==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/interaction-particles-links": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/interaction-particles-links/-/interaction-particles-links-3.9.1.tgz",
"integrity": "sha512-MsLbMjy1vY5M5/hu/oa5OSRZAUz49H3+9EBMTIOThiX+a+vpl3sxc9AqNd9gMsPbM4WJlub8T6VBZdyvzez1Vg==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/move-base": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/move-base/-/move-base-3.9.1.tgz",
"integrity": "sha512-X4huBS27d8srpxwOxliWPUt+NtCwY+8q/cx1DvQxyqmTA8VFCGpcHNwtqiN+9JicgzOvSuaORVqUgwlsc7h4pQ==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/move-parallax": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/move-parallax/-/move-parallax-3.9.1.tgz",
"integrity": "sha512-whlOR0bVeyh6J/hvxf/QM3DqvNnITMiAQ0kro6saqSDItAVqg4pYxBfEsSOKq7EhjxNvfhhqR+pFMhp06zoCVA==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/plugin-easing-quad": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/plugin-easing-quad/-/plugin-easing-quad-3.9.1.tgz",
"integrity": "sha512-C2UJOca5MTDXKUTBXj30Kiqr5UyID+xrY/LxicVWWZPczQW2bBxbIbfq9ULvzGDwBTxE2rdvIB8YFKmDYO45qw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/plugin-hex-color": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/plugin-hex-color/-/plugin-hex-color-3.9.1.tgz",
"integrity": "sha512-vZgZ12AjUicJvk7AX4K2eAmKEQX/D1VEjEPFhyjbgI7A65eX72M465vVKIgNA6QArLZ1DLs7Z787LOE6GOBWsg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/plugin-hsl-color": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/plugin-hsl-color/-/plugin-hsl-color-3.9.1.tgz",
"integrity": "sha512-jJd1iGgRwX6eeNjc1zUXiJivaqC5UE+SC2A3/NtHwwoQrkfxGWmRHOsVyLnOBRcCPgBp/FpdDe6DIDjCMO715w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/plugin-rgb-color": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/plugin-rgb-color/-/plugin-rgb-color-3.9.1.tgz",
"integrity": "sha512-SBxk7f1KBfXeTnnklbE2Hx4jBgh6I6HOtxb+Os1gTp0oaghZOkWcCD2dP4QbUu7fVNCMOcApPoMNC8RTFcy9wQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/shape-circle": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/shape-circle/-/shape-circle-3.9.1.tgz",
"integrity": "sha512-DqZFLjbuhVn99WJ+A9ajz9YON72RtCcvubzq6qfjFmtwAK7frvQeb6iDTp6Ze9FUipluxVZWVRG4vWTxi2B+/g==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/shape-emoji": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/shape-emoji/-/shape-emoji-3.9.1.tgz",
"integrity": "sha512-ifvY63usuT+hipgVHb8gelBHSeF6ryPnMxAAEC1RGHhhXfpSRWMtE6ybr+pSsYU52M3G9+TF84v91pSwNrb9ZQ==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/shape-image": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/shape-image/-/shape-image-3.9.1.tgz",
"integrity": "sha512-fCA5eme8VF3oX8yNVUA0l2SLDKuiZObkijb0z3Ky0qj1HUEVlAuEMhhNDNB9E2iELTrWEix9z7BFMePp2CC7AA==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/shape-line": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/shape-line/-/shape-line-3.9.1.tgz",
"integrity": "sha512-wT8NSp0N9HURyV05f371cHKcNTNqr0/cwUu6WhBzbshkYGy1KZUP9CpRIh5FCrBpTev34mEQfOXDycgfG0KiLQ==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/shape-polygon": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/shape-polygon/-/shape-polygon-3.9.1.tgz",
"integrity": "sha512-dA77PgZdoLwxnliH6XQM/zF0r4jhT01pw5y7XTeTqws++hg4rTLV9255k6R6eUqKq0FPSW1/WBsBIl7q/MmrqQ==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/shape-square": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/shape-square/-/shape-square-3.9.1.tgz",
"integrity": "sha512-DKGkDnRyZrAm7T2ipqNezJahSWs6xd9O5LQLe5vjrYm1qGwrFxJiQaAdlb00UNrexz1/SA7bEoIg4XKaFa7qhQ==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/shape-star": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/shape-star/-/shape-star-3.9.1.tgz",
"integrity": "sha512-kdMJpi8cdeb6vGrZVSxTG0JIjCwIenggqk0EYeKAwtOGZFBgL7eHhF2F6uu1oq8cJAbXPujEoabnLsz6mW8XaA==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/slim": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/slim/-/slim-3.9.1.tgz",
"integrity": "sha512-CL5cDmADU7sDjRli0So+hY61VMbdroqbArmR9Av+c1Fisa5ytr6QD7Jv62iwU2S6rvgicEe9OyRmSy5GIefwZw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"license": "MIT",
"dependencies": {
"@tsparticles/basic": "3.9.1",
"@tsparticles/engine": "3.9.1",
"@tsparticles/interaction-external-attract": "3.9.1",
"@tsparticles/interaction-external-bounce": "3.9.1",
"@tsparticles/interaction-external-bubble": "3.9.1",
"@tsparticles/interaction-external-connect": "3.9.1",
"@tsparticles/interaction-external-grab": "3.9.1",
"@tsparticles/interaction-external-pause": "3.9.1",
"@tsparticles/interaction-external-push": "3.9.1",
"@tsparticles/interaction-external-remove": "3.9.1",
"@tsparticles/interaction-external-repulse": "3.9.1",
"@tsparticles/interaction-external-slow": "3.9.1",
"@tsparticles/interaction-particles-attract": "3.9.1",
"@tsparticles/interaction-particles-collisions": "3.9.1",
"@tsparticles/interaction-particles-links": "3.9.1",
"@tsparticles/move-parallax": "3.9.1",
"@tsparticles/plugin-easing-quad": "3.9.1",
"@tsparticles/shape-emoji": "3.9.1",
"@tsparticles/shape-image": "3.9.1",
"@tsparticles/shape-line": "3.9.1",
"@tsparticles/shape-polygon": "3.9.1",
"@tsparticles/shape-square": "3.9.1",
"@tsparticles/shape-star": "3.9.1",
"@tsparticles/updater-life": "3.9.1",
"@tsparticles/updater-rotate": "3.9.1",
"@tsparticles/updater-stroke-color": "3.9.1"
}
},
"node_modules/@tsparticles/updater-color": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/updater-color/-/updater-color-3.9.1.tgz",
"integrity": "sha512-XGWdscrgEMA8L5E7exsE0f8/2zHKIqnTrZymcyuFBw2DCB6BIV+5z6qaNStpxrhq3DbIxxhqqcybqeOo7+Alpg==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/updater-life": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/updater-life/-/updater-life-3.9.1.tgz",
"integrity": "sha512-Oi8aF2RIwMMsjssUkCB6t3PRpENHjdZf6cX92WNfAuqXtQphr3OMAkYFJFWkvyPFK22AVy3p/cFt6KE5zXxwAA==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/updater-opacity": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/updater-opacity/-/updater-opacity-3.9.1.tgz",
"integrity": "sha512-w778LQuRZJ+IoWzeRdrGykPYSSaTeWfBvLZ2XwYEkh/Ss961InOxZKIpcS6i5Kp/Zfw0fS1ZAuqeHwuj///Osw==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/updater-out-modes": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/updater-out-modes/-/updater-out-modes-3.9.1.tgz",
"integrity": "sha512-cKQEkAwbru+hhKF+GTsfbOvuBbx2DSB25CxOdhtW2wRvDBoCnngNdLw91rs+0Cex4tgEeibkebrIKFDDE6kELg==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/updater-rotate": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/updater-rotate/-/updater-rotate-3.9.1.tgz",
"integrity": "sha512-9BfKaGfp28JN82MF2qs6Ae/lJr9EColMfMTHqSKljblwbpVDHte4umuwKl3VjbRt87WD9MGtla66NTUYl+WxuQ==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/updater-size": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/updater-size/-/updater-size-3.9.1.tgz",
"integrity": "sha512-3NSVs0O2ApNKZXfd+y/zNhTXSFeG1Pw4peI8e6z/q5+XLbmue9oiEwoPy/tQLaark3oNj3JU7Q903ZijPyXSzw==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/updater-stroke-color": {
"version": "3.9.1",
"resolved": "https://registry.npmjs.org/@tsparticles/updater-stroke-color/-/updater-stroke-color-3.9.1.tgz",
"integrity": "sha512-3x14+C2is9pZYTg9T2TiA/aM1YMq4wLdYaZDcHm3qO30DZu5oeQq0rm/6w+QOGKYY1Z3Htg9rlSUZkhTHn7eDA==",
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "3.9.1"
}
},
"node_modules/@tsparticles/vue3": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@tsparticles/vue3/-/vue3-3.0.1.tgz",
"integrity": "sha512-BxaSZ0wtxq33SDsrqLkLWoV88Jd5BnBoYjyVhKSNzOLOesCiG8Z5WQC1QZGTez79l/gBe0xaCDF0ng1e2iKJvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/matteobruni"
},
{
"type": "github",
"url": "https://github.com/sponsors/tsparticles"
},
{
"type": "buymeacoffee",
"url": "https://www.buymeacoffee.com/matteobruni"
}
],
"license": "MIT",
"dependencies": {
"@tsparticles/engine": "^3.0.3",
"vue": "^3.3.13"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",

View File

@@ -9,9 +9,6 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@primer/css": "^22.1.0",
"@tsparticles/slim": "^3.9.1",
"@tsparticles/vue3": "^3.0.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"element-plus": "^2.13.0", "element-plus": "^2.13.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1008 KiB

View File

@@ -1 +0,0 @@
Copyright © 2025 DGP Studio, All Rights Reserved.

View File

@@ -1,89 +0,0 @@
import request from '@/utils/request'
/** 公告数据类型 */
export interface Announcement {
Content: string
Id: number
LastUpdateTime: number
Link: string | null
Locale: string | null
MaxPresentVersion: string | null
Severity: number
Title: string
Distribution: string | null
}
/** 公告列表响应数据类型 */
export interface AnnouncementListResponse {
code: number
data: Announcement[]
message: string
}
/**
* 获取公告列表 API
* POST /Announcement/List
*/
export function getAnnouncementListApi(): Promise<Announcement[]> {
return request({
url: '/Announcement/List',
method: 'post',
})
}
/** 创建公告请求参数类型 */
export interface CreateAnnouncementRequest {
Content: string
Title: string
Link?: string | null
Locale?: string | null
MaxPresentVersion?: string | null
Severity?: number | null
Distribution?: string | null
}
/** 创建公告响应数据类型 */
export interface CreateAnnouncementResponse {
code: number
data: {
Id: number
}
message: string
}
/**
* 创建公告 API
* POST /web-api/announcement
* 注意由于request.ts拦截器处理实际返回的是data部分即 { Id: number }
*/
export function createAnnouncementApi(params: CreateAnnouncementRequest): Promise<{ Id: number }> {
return request({
url: '/web-api/announcement',
method: 'post',
data: params,
})
}
/**
* 编辑公告 API
* PUT /web-api/announcement/{announcement_id}
*/
export function updateAnnouncementApi(id: number, params: CreateAnnouncementRequest): Promise<null> {
return request({
url: `/web-api/announcement/${id}`,
method: 'put',
data: params,
})
}
/**
* 删除公告 API
* DELETE /web-api/announcement/{announcement_id}
*/
export function deleteAnnouncementApi(id: number): Promise<null> {
return request({
url: `/web-api/announcement/${id}`,
method: 'delete',
})
}

View File

@@ -1,25 +0,0 @@
import request from '@/utils/request'
/** 登录请求参数 */
export interface WebLoginParams {
email: string
password: string
}
/** 登录返回数据 */
export interface WebLoginResult {
access_token: string
expires_in: number
}
/**
* WEB 登录 API
* POST /web-api/login
*/
export function webLoginApi(data: WebLoginParams) {
return request<WebLoginResult>({
url: '/web-api/login',
method: 'post',
data,
})
}

View File

@@ -1,135 +0,0 @@
import request from '@/utils/request'
/** 下载资源信息 */
export interface DownloadResource {
id?: string
created_at: string
created_by: string
download_url: string
features: string | null
file_hash: string | null
file_size: string | null
is_active: boolean | null
is_test?: boolean
package_type: string
version: string
}
/**
* 获取所有发布的资源
* GET /download-resources
*/
export function getDownloadResourcesApi(): Promise<DownloadResource[]> {
return request({
url: '/download-resources',
method: 'get',
})
}
/**
* 获取最新版本
* GET /download-resources/latest
*/
export function getLatestVersionApi(): Promise<DownloadResource> {
return request({
url: '/download-resources/latest',
method: 'get',
})
}
/**
* 获取测试版本
* GET /download-resources?is_test=true
*/
export function getTestVersionApi(): Promise<DownloadResource[]> {
return request({
url: '/download-resources',
method: 'get',
params: {
is_test: true
}
})
}
/**
* 获取资源列表(包含未激活的)
* GET /web-api/download-resources
* @param package_type 筛选包类型msi或者msix
* @param is_active 筛选是否激活
*/
export function getDownloadResourceListApi(params?: {
package_type?: string
is_active?: string
}): Promise<DownloadResource[]> {
return request({
url: '/web-api/download-resources',
method: 'get',
params,
})
}
/**
* 获取单个资源详情
* GET /web-api/download-resources/{resource_id}
* @param resource_id 资源id
*/
export function getDownloadResourceDetailApi(resource_id: string): Promise<DownloadResource> {
return request({
url: `/web-api/download-resources/${resource_id}`,
method: 'get',
})
}
/**
* 删除下载资源
* DELETE /web-api/download-resources/{resource_id}
* @param resource_id 资源id
*/
export function deleteDownloadResourceApi(resource_id: string): Promise<null> {
return request({
url: `/web-api/download-resources/${resource_id}`,
method: 'delete',
})
}
/** 创建资源请求参数类型 */
export interface CreateResourceRequest {
version: string
package_type: string
download_url: string
features?: string | null
file_size?: string | null
file_hash?: string | null
is_active?: boolean | null
is_test?: boolean
}
/** 创建资源响应数据类型 */
export interface CreateResourceResponse {
id: string
}
/**
* 创建新版本资源信息
* POST /web-api/download-resources
*/
export function createDownloadResourceApi(params: CreateResourceRequest): Promise<CreateResourceResponse> {
return request({
url: '/web-api/download-resources',
method: 'post',
data: params,
})
}
/**
* 更新下载资源
* PUT /web-api/download-resources/{resource_id}
* @param resource_id 资源id
*/
export function updateDownloadResourceApi(resource_id: string, params: Partial<CreateResourceRequest>): Promise<null> {
return request({
url: `/web-api/download-resources/${resource_id}`,
method: 'put',
data: params,
})
}

View File

@@ -1,68 +0,0 @@
import request from '@/utils/request'
/** 用户信息数据结构 */
export interface UserInfo {
CdnExpireAt: string
GachaLogExpireAt: string
IsLicensedDeveloper: boolean
IsMaintainer: boolean
NormalizedUserName: string
UserName: string
}
/** 用户列表中的用户数据结构 */
export interface UserListItem {
CdnExpireAt: string
CreatedAt: string
GachaLogExpireAt: string
IsLicensedDeveloper: boolean
IsMaintainer: boolean
NormalizedUserName: string
UserName: string
_id: string
email: string
}
/** API响应数据结构 */
export interface ApiResponse<T> {
code: number
data: T
message: string
}
/**
* 获取用户信息
* GET /Passport/v2/UserInfo
*/
export function getUserInfoApi(): Promise<UserInfo> {
return request({
url: '/Passport/v2/UserInfo',
method: 'get',
})
}
/**
* 获取用户列表
* GET /web-api/users
* @param params 搜索和筛选参数
* @param params.q 搜索关键词可搜索用户名、邮箱、_id
* @param params.role 按角色筛选maintainer, developer, user
* @param params.email 按邮箱筛选
* @param params.username 按用户名筛选
* @param params.id 按用户ID筛选
* @param params.is 按状态筛选licensed, not-licensed
*/
export function getUserListApi(params?: {
q?: string
role?: string
email?: string
username?: string
id?: string
is?: string
}): Promise<UserListItem[]> {
return request({
url: '/web-api/users',
method: 'get',
params,
})
}

View File

@@ -1,637 +0,0 @@
<template>
<div class="github-search-container gh-search-root" :class="{ 'dark-mode': isDarkMode }">
<div class="search-input-wrapper">
<!-- 语法高亮显示层 -->
<div class="syntax-highlight-layer" v-if="searchValue" v-html="highlightedText"></div>
<!-- 实际输入框 -->
<input
ref="searchInput"
v-model="searchValue"
type="text"
class="form-control"
:class="{ 'has-content': searchValue }"
:placeholder="placeholder"
@input="handleInput"
@keydown="handleKeydown"
@focus="showSuggestions = true"
@blur="handleBlur"
/>
<div class="search-icon">
<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-search">
<path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"></path>
</svg>
</div>
</div>
<!-- 自动补全建议下拉菜单 -->
<div v-if="showSuggestions && filteredSuggestions.length > 0" class="search-suggestions">
<div
v-for="(suggestion, index) in filteredSuggestions"
:key="index"
class="suggestion-item"
:class="{ active: selectedIndex === index }"
@mousedown.prevent="selectSuggestion(suggestion)"
>
<span class="suggestion-text">{{ suggestion.text }}</span>
<span class="suggestion-desc">{{ suggestion.description }}</span>
</div>
</div>
<!-- 搜索限定符帮助提示 -->
<div v-if="showSuggestions && currentQualifier && currentQualifierHelp" class="qualifier-help">
<div class="help-title">{{ currentQualifierHelp.title }}</div>
<div class="help-desc">{{ currentQualifierHelp.description }}</div>
<div class="help-examples">
<div v-for="(example, idx) in currentQualifierHelp.examples" :key="idx" class="example-item">
<code>{{ example }}</code>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
interface SearchQualifier {
key: string
title: string
description: string
examples: string[]
autocomplete?: string[]
}
interface SearchSuggestion {
text: string
description: string
type: 'qualifier' | 'value'
qualifier?: string
}
const props = defineProps<{
modelValue: string
placeholder?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
'search': [query: string, filters: Record<string, string>]
}>()
const searchValue = ref(props.modelValue)
const searchInput = ref<HTMLInputElement>()
const showSuggestions = ref(false)
const selectedIndex = ref(0)
const isDarkMode = ref(false)
// 检测暗色模式
const checkDarkMode = () => {
isDarkMode.value = document.documentElement.classList.contains('dark')
}
onMounted(() => {
checkDarkMode()
// 监听主题变化
const observer = new MutationObserver(checkDarkMode)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
})
onUnmounted(() => {
observer.disconnect()
})
})
// 搜索限定符定义
const qualifiers: SearchQualifier[] = [
{
key: 'role',
title: '角色',
description: '按用户角色筛选',
examples: ['role:maintainer', 'role:developer', 'role:user']
},
{
key: 'email',
title: '邮箱',
description: '按邮箱地址筛选',
examples: ['email:test@example.com', 'email:outlook.com']
},
{
key: 'username',
title: '用户名',
description: '按用户名筛选',
examples: ['username:testuser', 'username:admin']
},
{
key: 'id',
title: '用户ID',
description: '按用户ID筛选',
examples: ['id:123456789', 'id:507f1f77bcf86cd799439011']
}
// 这个应该没必要这个实际上就是筛选开发者权限和role:developer重复了
// {
// key: 'is',
// title: '状态',
// description: '按权限状态筛选',
// examples: ['is:licensed', 'is:not-licensed'],
// autocomplete: ['licensed', 'not-licensed']
// }
]
// 检查是否为已知的限定符
function isQualifier(text: string): boolean {
return qualifiers.some(q => q.key === text)
}
// 生成高亮的 HTML
const highlightedText = computed(() => {
if (!searchValue.value) return ''
let result = ''
const text = searchValue.value
// 按空格分割成段
const segments = text.split(' ')
segments.forEach((segment, index) => {
if (segment === '') {
// 空段表示有空格
result += '<span class="hl-space"> </span>'
} else {
// 检查是否包含冒号
const colonIndex = segment.indexOf(':')
if (colonIndex !== -1) {
// 有冒号,可能是限定符:值格式
const qualifier = segment.slice(0, colonIndex)
const value = segment.slice(colonIndex + 1)
if (isQualifier(qualifier)) {
// 已知限定符
result += `<span class="hl-qualifier">${escapeHtml(qualifier)}</span><span class="hl-colon">:</span><span class="hl-value">${escapeHtml(value)}</span>`
} else {
// 未知限定符,作为普通文本处理
result += `<span class="hl-unknown">${escapeHtml(qualifier)}</span><span class="hl-colon">:</span><span class="hl-value">${escapeHtml(value)}</span>`
}
} else {
// 没有冒号,作为普通文本
result += `<span class="hl-text">${escapeHtml(segment)}</span>`
}
}
// 添加段之间的空格(除了最后一段)
if (index < segments.length - 1) {
result += '<span class="hl-space"> </span>'
}
})
return result
})
// HTML 转义
function escapeHtml(text: string): string {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
// 当前输入的限定符(基于当前段)
const currentQualifier = computed(() => {
const lastSpaceIndex = searchValue.value.lastIndexOf(' ')
const afterLastSpace = searchValue.value.slice(lastSpaceIndex + 1)
const match = afterLastSpace.match(/^(\w+):/)
return match ? match[1] : null
})
// 当前限定符的帮助信息
const currentQualifierHelp = computed(() => {
if (!currentQualifier.value) return null
return qualifiers.find(q => q.key === currentQualifier.value) || null
})
// 建议列表
const suggestions = computed<SearchSuggestion[]>(() => {
const result: SearchSuggestion[] = []
// 检查是否正在输入新的限定符(最后一个空格后)
const lastSpaceIndex = searchValue.value.lastIndexOf(' ')
const afterLastSpace = searchValue.value.slice(lastSpaceIndex + 1)
const hasColonInCurrentSegment = afterLastSpace.includes(':')
// 如果当前段没有冒号,则显示限定符建议
if (!hasColonInCurrentSegment) {
const input = afterLastSpace.toLowerCase()
// 如果输入为空,显示所有限定符;否则显示匹配的限定符
const shouldShowAll = input === ''
qualifiers.forEach(q => {
if (shouldShowAll || q.key.startsWith(input)) {
result.push({
text: `${q.key}:`,
description: q.title,
type: 'qualifier'
})
}
})
}
// 如果当前段有冒号,则显示该限定符的值建议
if (hasColonInCurrentSegment && currentQualifier.value && currentQualifierHelp.value) {
const colonIndex = afterLastSpace.indexOf(':')
const value = afterLastSpace.slice(colonIndex + 1)
const input = value.toLowerCase()
const qualifier = currentQualifierHelp.value
if (qualifier.autocomplete) {
qualifier.autocomplete.forEach(val => {
if (val.startsWith(input)) {
result.push({
text: `${qualifier.key}:${val}`,
description: '',
type: 'value',
qualifier: qualifier.key
})
}
})
}
// 为特定限定符添加常用值建议
if (qualifier.key === 'role') {
const roles = ['maintainer', 'developer', 'user']
roles.forEach(role => {
if (role.startsWith(input)) {
result.push({
text: `role:${role}`,
description: role === 'maintainer' ? '运维人员' : role === 'developer' ? '开发者' : '普通用户',
type: 'value',
qualifier: 'role'
})
}
})
}
}
return result
})
// 过滤后的建议
const filteredSuggestions = computed(() => {
return suggestions.value
})
// 处理输入
function handleInput() {
emit('update:modelValue', searchValue.value)
selectedIndex.value = 0
// 如果有建议内容,自动显示建议框
if (suggestions.value.length > 0) {
showSuggestions.value = true
}
}
// 处理键盘事件
function handleKeydown(event: KeyboardEvent) {
if (!showSuggestions.value || filteredSuggestions.value.length === 0) return
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
selectedIndex.value = Math.min(selectedIndex.value + 1, filteredSuggestions.value.length - 1)
break
case 'ArrowUp':
event.preventDefault()
selectedIndex.value = Math.max(selectedIndex.value - 1, 0)
break
case 'Enter':
const selectedSuggestion = filteredSuggestions.value[selectedIndex.value]
if (selectedSuggestion) {
selectSuggestion(selectedSuggestion)
} else {
handleSearch()
}
break
case 'Escape':
// ESC键关闭建议框
showSuggestions.value = false
break
}
}
// 选择建议
function selectSuggestion(suggestion: SearchSuggestion) {
if (suggestion.type === 'qualifier') {
// 如果是限定符,替换当前输入的限定符部分
const lastSpaceIndex = searchValue.value.lastIndexOf(' ')
const beforeLastSpace = searchValue.value.slice(0, lastSpaceIndex + 1)
searchValue.value = beforeLastSpace + suggestion.text
// 选择限定符后不关闭建议框,让用户继续输入值
} else {
// 如果是值,替换整个限定符+值,并添加空格以便输入下一个限定符
const lastSpaceIndex = searchValue.value.lastIndexOf(' ')
const beforeLastSpace = searchValue.value.slice(0, lastSpaceIndex + 1)
searchValue.value = beforeLastSpace + suggestion.text + ' '
// 选择值后重新打开建议框,方便继续输入下一个限定符
// 使用setTimeout确保DOM更新后再打开
setTimeout(() => {
showSuggestions.value = true
}, 0)
}
emit('update:modelValue', searchValue.value)
nextTick(() => {
searchInput.value?.focus()
})
}
// 处理失去焦点
function handleBlur() {
// 延迟隐藏,以便能够点击建议项
setTimeout(() => {
showSuggestions.value = false
}, 200)
}
// 解析搜索查询
function parseSearchQuery(query: string): { keyword: string; filters: Record<string, string> } {
const filters: Record<string, string> = {}
const parts = query.split(/\s+/)
const keywords: string[] = []
parts.forEach(part => {
if (part.includes(':')) {
const [key, value] = part.split(':')
if (key && value) {
filters[key] = value
}
} else if (part.trim()) {
keywords.push(part)
}
})
return {
keyword: keywords.join(' '),
filters
}
}
// 执行搜索
function handleSearch() {
const { filters } = parseSearchQuery(searchValue.value)
emit('search', searchValue.value, filters)
showSuggestions.value = false
}
// 监听modelValue变化
watch(() => props.modelValue, (newValue) => {
searchValue.value = newValue
})
// 暴露搜索方法
defineExpose({
handleSearch
})
</script>
<style>
/* 定义全局CSS变量 - 亮色模式 */
.gh-search-root {
--color-fg-default: #24292f;
--color-fg-muted: #57606a;
--color-accent-fg: #0969da;
--color-accent-muted: rgba(9, 105, 218, 0.1);
--color-accent-subtle: #ddf4ff;
--color-bg-subtle: #f6f8fa;
--color-bg-canvas: #ffffff;
--color-canvas-overlay: #ffffff;
--color-border-default: #d0d7de;
--color-shadow-medium: 0 3px 6px rgba(140, 149, 159, 0.15);
/* 高亮颜色 - 亮色模式 */
--hl-qualifier: #cf222e;
--hl-colon: #cf222e;
--hl-value: #0550ae;
--hl-text: #24292f;
--hl-unknown: #6e7781;
}
/* 定义全局CSS变量 - 暗色模式 */
.gh-search-root.dark-mode {
--color-fg-default: #c9d1d9;
--color-fg-muted: #8b949e;
--color-accent-fg: #58a6ff;
--color-accent-muted: rgba(88, 166, 255, 0.15);
--color-accent-subtle: rgba(88, 166, 255, 0.15);
--color-bg-subtle: #161b22;
--color-bg-canvas: #0d1117;
--color-canvas-overlay: #161b22;
--color-border-default: #30363d;
--color-shadow-medium: 0 3px 6px rgba(0, 0, 0, 0.5);
/* 高亮颜色 - 暗色模式 */
--hl-qualifier: #ff7b72;
--hl-colon: #ff7b72;
--hl-value: #79c0ff;
--hl-text: #c9d1d9;
--hl-unknown: #8b949e;
}
/* 语法高亮样式 - 非scoped用于v-html生成的HTML */
.gh-search-root .hl-qualifier {
color: var(--hl-qualifier);
font-weight: 500;
}
.gh-search-root .hl-colon {
color: var(--hl-colon);
font-weight: 500;
}
.gh-search-root .hl-value {
color: var(--hl-value);
}
.gh-search-root .hl-text {
color: var(--hl-text);
}
.gh-search-root .hl-unknown {
color: var(--hl-unknown);
}
.gh-search-root .hl-space {
color: var(--color-fg-muted);
}
</style>
<style scoped>
.github-search-container {
position: relative;
width: 100%;
}
.search-input-wrapper {
position: relative;
width: 100%;
background-color: var(--color-bg-subtle);
border-radius: 6px;
box-sizing: border-box;
}
/* 语法高亮显示层 */
.syntax-highlight-layer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 8px 36px 8px 12px;
font-size: 14px;
line-height: 20px;
font-family: inherit;
font-weight: normal;
white-space: pre;
overflow: hidden;
pointer-events: none;
z-index: 1;
color: var(--color-fg-default);
background: transparent;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.form-control {
width: 100%;
padding: 8px 36px 8px 12px;
font-size: 14px;
line-height: 20px;
font-family: inherit;
font-weight: normal;
color: var(--color-fg-default);
caret-color: var(--color-fg-default);
background: transparent;
border: 1px solid var(--color-border-default);
border-radius: 6px;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
position: relative;
z-index: 2;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.form-control.has-content {
color: transparent;
-webkit-text-fill-color: transparent;
}
.form-control:focus {
outline: none;
}
.form-control:focus + .search-icon {
color: var(--color-accent-fg);
}
.search-input-wrapper:focus-within {
border-color: var(--color-accent-fg);
box-shadow: 0 0 0 3px var(--color-accent-muted);
background-color: var(--color-bg-canvas);
}
.form-control::placeholder {
color: var(--color-fg-muted);
}
.search-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--color-fg-muted);
pointer-events: none;
}
.search-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
margin-top: 4px;
background-color: var(--color-canvas-overlay);
border: 1px solid var(--color-border-default);
border-radius: 6px;
box-shadow: var(--color-shadow-medium);
max-height: 300px;
overflow-y: auto;
}
.suggestion-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
transition: background-color 0.15s ease-in-out;
}
.suggestion-item:hover,
.suggestion-item.active {
background-color: var(--color-accent-subtle);
}
.suggestion-text {
font-size: 14px;
font-weight: 600;
color: var(--color-accent-fg);
}
.suggestion-desc {
font-size: 12px;
color: var(--color-fg-muted);
}
.qualifier-help {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 999;
margin-top: 4px;
padding: 12px;
background-color: var(--color-canvas-overlay);
border: 1px solid var(--color-border-default);
border-radius: 6px;
box-shadow: var(--color-shadow-medium);
}
.help-title {
font-size: 14px;
font-weight: 600;
color: var(--color-fg-default);
margin-bottom: 4px;
}
.help-desc {
font-size: 12px;
color: var(--color-fg-muted);
margin-bottom: 8px;
}
.help-examples {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.example-item {
code {
display: inline-block;
padding: 2px 6px;
font-size: 12px;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
color: var(--color-accent-fg);
background-color: var(--color-accent-subtle);
border-radius: 3px;
}
}
</style>

View File

@@ -1,95 +0,0 @@
<template>
<div class="app-header">
<div class="header-left">
<img :src="appIcon" alt="App Icon" class="app-icon" />
<span class="app-name">{{ appName }}</span>
</div>
<div class="header-right">
<template v-for="action in headerActions" :key="action.id">
<component
:is="action.component || 'el-button'"
v-bind="action.props"
@click="action.onClick"
class="header-action-btn"
>
<template v-if="action.icon">
<el-icon>
<component :is="action.icon" />
</el-icon>
</template>
<span>{{ action.label }}</span>
</component>
</template>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface HeaderAction {
id: string
label: string
icon?: any
component?: any
props?: Record<string, any>
onClick: () => void
}
interface Props {
appIcon?: string
appName?: string
actions?: HeaderAction[]
}
const props = withDefaults(defineProps<Props>(), {
appIcon: '/vite.svg',
appName: 'Snap Hutao Web',
actions: () => []
})
const headerActions = computed(() => props.actions)
</script>
<style scoped>
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 32px;
background: var(--header-bg);
border-bottom: 1px solid var(--border-color);
min-height: 70px;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.app-icon {
width: 40px;
height: 40px;
border-radius: 8px;
object-fit: contain;
}
.app-name {
font-size: 20px;
font-weight: 600;
color: var(--text-color);
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.header-action-btn {
display: flex;
align-items: center;
gap: 6px;
}
</style>

View File

@@ -1,7 +0,0 @@
<template>
<router-view></router-view>
</template>
<script setup lang="ts">
// 这是一个占位组件,用于嵌套路由
</script>

View File

@@ -3,7 +3,7 @@
<!-- 有子路由 --> <!-- 有子路由 -->
<el-sub-menu <el-sub-menu
v-if="hasChildren(item)" v-if="hasChildren(item)"
:index="getFullPath(item)" :index="item.path"
> >
<template #title> <template #title>
<el-icon v-if="item.meta?.icon"> <el-icon v-if="item.meta?.icon">
@@ -12,13 +12,13 @@
<span>{{ item.meta?.title }}</span> <span>{{ item.meta?.title }}</span>
</template> </template>
<SidebarItem :routes="item.children!" :parent-path="getFullPath(item)" /> <SidebarItem :routes="item.children!" />
</el-sub-menu> </el-sub-menu>
<!-- 普通菜单 --> <!-- 普通菜单 -->
<el-menu-item <el-menu-item
v-else v-else
:index="getFullPath(item)" :index="item.path"
> >
<el-icon v-if="item.meta?.icon"> <el-icon v-if="item.meta?.icon">
<component :is="icons[item.meta.icon as keyof typeof icons]" /> <component :is="icons[item.meta.icon as keyof typeof icons]" />
@@ -32,25 +32,10 @@
import type { RouteRecordRaw } from 'vue-router' import type { RouteRecordRaw } from 'vue-router'
import * as icons from '@element-plus/icons-vue' import * as icons from '@element-plus/icons-vue'
interface Props { defineProps<{
routes: RouteRecordRaw[] routes: RouteRecordRaw[]
parentPath?: string }>()
}
const props = withDefaults(defineProps<Props>(), {
parentPath: '/dashboard'
})
const hasChildren = (route: RouteRecordRaw) => const hasChildren = (route: RouteRecordRaw) =>
route.children && route.children.length > 0 route.children && route.children.length > 0
const getFullPath = (route: RouteRecordRaw) => {
// 如果是绝对路径,直接返回
if (route.path.startsWith('/')) {
return route.path
}
// 否则拼接父级路径
const basePath = props.parentPath || ''
return basePath ? `${basePath}/${route.path}` : `/${route.path}`
}
</script> </script>

View File

@@ -2,8 +2,8 @@
<el-container class="layout"> <el-container class="layout">
<el-aside :width="isCollapse ? '64px' : '200px'" class="aside"> <el-aside :width="isCollapse ? '64px' : '200px'" class="aside">
<div class="logo" :class="{ collapsed: isCollapse }"> <div class="logo" :class="{ collapsed: isCollapse }">
<span v-if="!isCollapse">Snap Hutao Web</span> <span v-if="!isCollapse">Vite Admin</span>
<span v-else></span> <span v-else>VA</span>
</div> </div>
<el-menu <el-menu
@@ -34,20 +34,6 @@
@change="themeStore.toggleTheme" @change="themeStore.toggleTheme"
/> />
</div> </div>
<el-dropdown class="user-dropdown" @command="handleCommand">
<span class="user-info">
<el-avatar :size="32" :icon="UserFilled" />
<span class="username">{{ userStore.userInfo?.UserName || '用户' }}</span>
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
<el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div> </div>
</el-header> </el-header>
@@ -61,31 +47,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { Fold, Expand, Moon, Sunny, UserFilled, ArrowDown } from '@element-plus/icons-vue' import { Fold, Expand, Moon, Sunny } from '@element-plus/icons-vue'
import SidebarItem from '../components/SidebarItem.vue' import SidebarItem from '../components/SidebarItem.vue'
import { useThemeStore } from '../stores/theme' import { useThemeStore } from '../stores/theme'
import { useUserStore } from '../stores/user'
const isCollapse = ref(false) const isCollapse = ref(false)
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const themeStore = useThemeStore() const themeStore = useThemeStore()
const userStore = useUserStore()
// 处理下拉菜单命令
const handleCommand = (command: string) => {
switch (command) {
case 'profile':
// 跳转到个人中心
router.push('/profile')
break
case 'logout':
// 退出登录
userStore.logout()
router.push('/login')
break
}
}
const toggle = () => (isCollapse.value = !isCollapse.value) const toggle = () => (isCollapse.value = !isCollapse.value)
@@ -95,9 +64,9 @@ const activeMenu = computed(() => route.path)
* 获取菜单路由,直接使用配置的路由结构 * 获取菜单路由,直接使用配置的路由结构
*/ */
const menuRoutes = computed(() => { const menuRoutes = computed(() => {
// 查找 /dashboard 路由的子路由 // 直接返回路由配置中的子路由
const dashboardRoute = router.options.routes.find(r => r.path === '/dashboard') const mainRoute = router.options.routes.find(r => r.path === '/')
return dashboardRoute?.children || [] return mainRoute?.children || []
}) })
</script> </script>
@@ -202,19 +171,4 @@ const menuRoutes = computed(() => {
background-color: var(--main-bg); background-color: var(--main-bg);
padding: 20px; padding: 20px;
} }
.user-dropdown {
margin-left: 20px;
}
.user-info {
display: flex;
align-items: center;
cursor: pointer;
color: var(--text-color);
}
.username {
margin: 0 8px;
}
</style> </style>

View File

@@ -8,10 +8,6 @@ import '@/styles/index.scss'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import { useThemeStore } from './stores/theme' import { useThemeStore } from './stores/theme'
import Particles from '@tsparticles/vue3'
import { loadSlim } from '@tsparticles/slim'
import type { Engine } from '@tsparticles/engine'
const app = createApp(App) const app = createApp(App)
const pinia = createPinia() const pinia = createPinia()
@@ -19,12 +15,6 @@ const pinia = createPinia()
app.use(pinia) app.use(pinia)
app.use(router) app.use(router)
app.use(ElementPlus) app.use(ElementPlus)
app.use(Particles, {
init: async (engine: Engine) => {
await loadSlim(engine)
}
})
import '@/router/permission'
// 初始化主题 // 初始化主题
const themeStore = useThemeStore(pinia) const themeStore = useThemeStore(pinia)

View File

@@ -9,32 +9,21 @@ const routes = [
}, },
{ {
path: '/', path: '/',
component: () => import('@/views/home/index.vue'),
meta: { hidden: true }
},
{
path: '/download',
component: () => import('@/views/download/index.vue'),
meta: { hidden: true }
},
{
path: '/dashboard',
component: DefaultLayout, component: DefaultLayout,
redirect: '/dashboard/home', redirect: '/dashboard',
children: [ children: [
{ {
path: 'home', path: 'dashboard',
component: () => import('@/views/dashboard/index.vue'), component: () => import('@/views/dashboard/index.vue'),
meta: { title: '首页', icon: 'House' }, meta: { title: '首页', icon: 'House' },
}, },
{ {
path: 'user', path: 'user',
component: () => import('@/views/user/index.vue'), component: () => import('@/views/dashboard/index.vue'),
meta: { title: '用户管理', icon: 'User' }, meta: { title: '用户管理', icon: 'User' },
}, },
{ {
path: 'system', path: 'system',
component: () => import('@/components/RouterViewPlaceholder.vue'),
meta: { title: '系统管理', icon: 'Setting' }, meta: { title: '系统管理', icon: 'Setting' },
children: [ children: [
{ {
@@ -47,16 +36,6 @@ const routes = [
component: () => import('@/views/dashboard/index.vue'), component: () => import('@/views/dashboard/index.vue'),
meta: { title: '角色管理', icon: 'UserFilled' }, meta: { title: '角色管理', icon: 'UserFilled' },
}, },
{
path: 'announcement',
component: () => import('@/views/announcement/index.vue'),
meta: { title: '公告管理', icon: 'Bell' },
},
{
path: 'download-manager',
component: () => import('@/views/download-manager/index.vue'),
meta: { title: '下载资源管理', icon: 'Download' },
},
], ],
}, },
], ],

View File

@@ -1,38 +0,0 @@
import router from './index'
import { useUserStore } from '@/stores/user'
router.beforeEach(async (to, _ , next) => {
const userStore = useUserStore()
// 未登录
if (!userStore.token) {
// 主页(/)、登录页和下载页允许未登录访问
if (to.path === '/' || to.path === '/login' || to.path === '/download') {
next()
} else {
next('/login')
}
return
}
// 已登录还去 login
if (to.path === '/login') {
next('/')
return
}
// 如果没有用户信息,尝试获取
if (!userStore.userInfo) {
try {
await userStore.fetchUserInfo()
} catch (error) {
// 获取用户信息失败可能token已过期跳转到登录页
console.error('获取用户信息失败:', error)
userStore.logout()
next('/login')
return
}
}
next()
})

View File

@@ -1,31 +0,0 @@
import { defineStore } from 'pinia'
import { getUserInfoApi } from '@/api/user'
import type { UserInfo } from '@/api/user'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: null as UserInfo | null,
}),
actions: {
setToken(token: string) {
this.token = token
// 持久化存储token
localStorage.setItem('token', token)
},
async fetchUserInfo() {
const userData = await getUserInfoApi()
this.userInfo = userData
return userData
},
logout() {
this.token = ''
this.userInfo = null
// 清除本地存储的token如果有
localStorage.removeItem('token')
},
},
})

View File

@@ -1,66 +1,18 @@
import axios from 'axios' import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
const axiosInstance = axios.create({ const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, baseURL: '/api',
timeout: 10000, timeout: 10000,
}) })
/** 请求拦截:自动加 Token */ request.interceptors.request.use((config) => {
axiosInstance.interceptors.request.use((config: any) => { // 可加 token
const userStore = useUserStore()
if (userStore.token) {
config.headers = config.headers || {}
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config return config
}) })
/** 响应拦截:兼容 code / retcode */ request.interceptors.response.use(
axiosInstance.interceptors.response.use( (res) => res.data,
(response: any) => { (err) => Promise.reject(err)
const res = response.data
// 登录接口code
if ('code' in res) {
if (res.code !== 0) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(res.message)
}
return res.data
}
// 用户信息接口retcode
if ('retcode' in res) {
if (res.retcode !== 0) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(res.message)
}
return res.data
}
// 兜底
return res
},
(error: any) => {
// 处理401未授权错误
if (error.response?.status === 401) {
const userStore = useUserStore()
userStore.logout()
// 如果不在登录页,则跳转到登录页
if (window.location.pathname !== '/login') {
window.location.href = '/login'
}
ElMessage.error('登录已过期,请重新登录')
return Promise.reject(new Error('登录已过期'))
}
ElMessage.error(error.message || '网络错误')
return Promise.reject(error)
}
) )
export default axiosInstance export default request

View File

@@ -1,603 +0,0 @@
<template>
<el-card>
<template #header>
<div class="card-header">
<span>公告管理</span>
<div>
<el-button type="success" @click="handleCreate">创建公告</el-button>
<el-button type="primary" @click="handleRefresh">刷新</el-button>
</div>
</div>
</template>
<el-table
v-loading="loading"
:data="announcementList"
style="width: 100%"
>
<el-table-column prop="Id" label="ID" width="80" />
<el-table-column prop="Title" label="标题" width="200" />
<el-table-column prop="Content" label="内容" show-overflow-tooltip />
<el-table-column prop="Severity" label="严重等级" width="100">
<template #default="{ row }">
<el-tag
:type="getSeverityType(row.Severity)"
size="small"
>
{{ getSeverityText(row.Severity) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="Distribution" label="发行版" width="120">
<template #default="{ row }">
<el-tag v-if="row.Distribution" size="small" type="info">
{{ row.Distribution }}
</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="Link" label="链接" width="200">
<template #default="{ row }">
<el-link
v-if="row.Link"
:href="row.Link"
target="_blank"
type="primary"
>
查看详情
</el-link>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="LastUpdateTime" label="更新时间" width="180">
<template #default="{ row }">
{{ formatTime(row.LastUpdateTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button
size="small"
type="primary"
link
@click="handleView(row)"
>
查看
</el-button>
<el-button
size="small"
type="warning"
link
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
size="small"
type="danger"
link
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 公告详情弹窗 -->
<el-dialog
v-model="dialogVisible"
title="公告详情"
width="50%"
>
<div
v-if="currentAnnouncement"
:class="['announcement-box', getSeverityClass(currentAnnouncement.Severity)]"
>
<!-- 标题 -->
<div class="announcement-title">
{{ currentAnnouncement.Title }}
</div>
<!-- 内容 -->
<div class="announcement-content">
<pre>{{ currentAnnouncement.Content }}</pre>
</div>
<!-- 底部信息 -->
<div class="announcement-footer">
<span class="announcement-time">
{{ formatTime(currentAnnouncement.LastUpdateTime) }}
</span>
<el-link
v-if="currentAnnouncement.Link"
:href="currentAnnouncement.Link"
target="_blank"
type="primary"
>
查看详情
</el-link>
</div>
</div>
</el-dialog>
<!-- 创建公告弹窗 -->
<el-dialog
v-model="createDialogVisible"
title="创建公告"
width="60%"
>
<el-form
ref="createFormRef"
:model="createForm"
:rules="createRules"
label-width="100px"
>
<el-form-item label="标题" prop="Title">
<el-input
v-model="createForm.Title"
placeholder="请输入公告标题"
/>
</el-form-item>
<el-form-item label="内容" prop="Content">
<el-input
v-model="createForm.Content"
type="textarea"
:rows="6"
placeholder="请输入公告内容"
/>
</el-form-item>
<el-form-item label="链接">
<el-input
v-model="createForm.Link"
placeholder="可选,详细信息链接"
/>
</el-form-item>
<el-form-item label="严重等级">
<el-select
v-model="createForm.Severity"
placeholder="请选择严重等级"
clearable
>
<el-option label="信息" :value="0" />
<el-option label="低" :value="1" />
<el-option label="中" :value="2" />
<el-option label="高" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="语言代码">
<el-input
v-model="createForm.Locale"
placeholder="可选,如 zh-CN, en-US"
/>
</el-form-item>
<el-form-item label="最大显示版本">
<el-input
v-model="createForm.MaxPresentVersion"
placeholder="可选,最大显示版本号"
/>
</el-form-item>
<el-form-item label="发行版名称">
<el-input
v-model="createForm.Distribution"
placeholder="可选,用于区分不同发行版,默认为空表示所有发行版"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="createLoading"
@click="handleCreateSubmit"
>
创建
</el-button>
</div>
</template>
</el-dialog>
<!-- 编辑公告弹窗 -->
<el-dialog
v-model="editDialogVisible"
title="编辑公告"
width="60%"
>
<el-form
ref="editFormRef"
:model="editForm"
:rules="createRules"
label-width="100px"
>
<el-form-item label="标题" prop="Title">
<el-input
v-model="editForm.Title"
placeholder="请输入公告标题"
/>
</el-form-item>
<el-form-item label="内容" prop="Content">
<el-input
v-model="editForm.Content"
type="textarea"
:rows="6"
placeholder="请输入公告内容"
/>
</el-form-item>
<el-form-item label="链接">
<el-input
v-model="editForm.Link"
placeholder="可选,详细信息链接"
/>
</el-form-item>
<el-form-item label="严重等级">
<el-select
v-model="editForm.Severity"
placeholder="请选择严重等级"
clearable
>
<el-option label="信息" :value="0" />
<el-option label="低" :value="1" />
<el-option label="中" :value="2" />
<el-option label="高" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="语言代码">
<el-input
v-model="editForm.Locale"
placeholder="可选,如 zh-CN, en-US"
/>
</el-form-item>
<el-form-item label="最大显示版本">
<el-input
v-model="editForm.MaxPresentVersion"
placeholder="可选,最大显示版本号"
/>
</el-form-item>
<el-form-item label="发行版名称">
<el-input
v-model="editForm.Distribution"
placeholder="可选,用于区分不同发行版,默认为空表示所有发行版"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="editLoading"
@click="handleEditSubmit"
>
保存
</el-button>
</div>
</template>
</el-dialog>
</el-card>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { getAnnouncementListApi, createAnnouncementApi, updateAnnouncementApi, deleteAnnouncementApi, type Announcement, type CreateAnnouncementRequest } from '@/api/announcement'
const loading = ref(false)
const announcementList = ref<Announcement[]>([])
const dialogVisible = ref(false)
const currentAnnouncement = ref<Announcement | null>(null)
// 创建公告相关
const createDialogVisible = ref(false)
const createLoading = ref(false)
const createFormRef = ref<FormInstance>()
// 编辑公告相关
const editDialogVisible = ref(false)
const editLoading = ref(false)
const editFormRef = ref<FormInstance>()
const currentEditId = ref<number | null>(null)
const createForm = reactive<CreateAnnouncementRequest>({
Title: '',
Content: '',
Link: '',
Locale: '',
MaxPresentVersion: '',
Severity: 0,
Distribution: '',
})
const editForm = reactive<CreateAnnouncementRequest>({
Title: '',
Content: '',
Link: '',
Locale: '',
MaxPresentVersion: '',
Severity: 0,
Distribution: '',
})
const createRules: FormRules = {
Title: [
{ required: true, message: '请输入公告标题', trigger: 'blur' },
{ min: 1, max: 200, message: '标题长度应为 1 到 200 个字符', trigger: 'blur' },
],
Content: [
{ required: true, message: '请输入公告内容', trigger: 'blur' },
{ min: 1, max: 2000, message: '内容长度应为 1 到 2000 个字符', trigger: 'blur' },
],
}
const getAnnouncementList = async () => {
loading.value = true
try {
const data = await getAnnouncementListApi()
announcementList.value = data || []
} catch (error) {
ElMessage.error('获取公告列表失败')
} finally {
loading.value = false
}
}
const handleRefresh = () => {
getAnnouncementList()
}
const handleView = (announcement: Announcement) => {
currentAnnouncement.value = announcement
dialogVisible.value = true
}
const getSeverityType = (severity: number) => {
if (severity === 0) return 'info'
if (severity === 1) return 'success'
if (severity === 2) return 'warning'
return 'danger'
}
const getSeverityText = (severity: number) => {
if (severity === 0) return '信息'
if (severity === 1) return '低'
if (severity === 2) return '中'
return '高'
}
const formatTime = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString()
}
const getSeverityClass = (severity: number) => {
if (severity === 0) return 'announcement-box-info'
if (severity === 1) return 'announcement-box-success'
if (severity === 2) return 'announcement-box-warning'
return 'announcement-box-danger'
}
const handleCreate = () => {
// 重置表单
Object.assign(createForm, {
Title: '',
Content: '',
Link: '',
Locale: '',
MaxPresentVersion: '',
Severity: 0,
Distribution: '',
})
createDialogVisible.value = true
}
const handleCreateSubmit = async () => {
if (!createFormRef.value) return
try {
await createFormRef.value.validate()
createLoading.value = true
// 由于request.ts拦截器已经处理了code字段这里直接返回data
const result = await createAnnouncementApi(createForm)
// result 直接就是 { Id: number }
if (result && result.Id) {
ElMessage.success('公告创建成功')
createDialogVisible.value = false
// 刷新列表
await getAnnouncementList()
} else {
ElMessage.error('创建公告失败')
}
} catch (error) {
ElMessage.error('创建公告失败')
} finally {
createLoading.value = false
}
}
const handleEdit = (announcement: Announcement) => {
currentEditId.value = announcement.Id
// 填充表单数据
Object.assign(editForm, {
Title: announcement.Title,
Content: announcement.Content,
Link: announcement.Link || '',
Locale: announcement.Locale || '',
MaxPresentVersion: announcement.MaxPresentVersion || '',
Severity: announcement.Severity,
Distribution: announcement.Distribution || '',
})
editDialogVisible.value = true
}
const handleEditSubmit = async () => {
if (!editFormRef.value || !currentEditId.value) return
try {
await editFormRef.value.validate()
editLoading.value = true
await updateAnnouncementApi(currentEditId.value, editForm)
ElMessage.success('公告更新成功')
editDialogVisible.value = false
// 刷新列表
await getAnnouncementList()
} catch (error) {
ElMessage.error('更新公告失败')
} finally {
editLoading.value = false
}
}
const handleDelete = async (announcement: Announcement) => {
try {
await ElMessageBox.confirm(
`确定要删除公告"${announcement.Title}"吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
await deleteAnnouncementApi(announcement.Id)
ElMessage.success('公告删除成功')
// 刷新列表
await getAnnouncementList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除公告失败')
}
}
}
onMounted(() => {
getAnnouncementList()
})
</script>
<style>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 公告正文容器 */
.announcement-box {
padding: 16px;
border-radius: 6px;
background-color: var(--announcement-bg);
color: var(--announcement-text);
transition: background-color 0.2s, color 0.2s;
}
/* 不同严重等级的公告背景色和文字色 */
.announcement-box-info {
background-color: var(--announcement-bg-info);
color: var(--announcement-text-info);
}
.announcement-box-success {
background-color: var(--announcement-bg-success);
color: var(--announcement-text-success);
}
.announcement-box-warning {
background-color: var(--announcement-bg-warning);
color: var(--announcement-text-warning);
}
.announcement-box-danger {
background-color: var(--announcement-bg-danger);
color: var(--announcement-text-danger);
}
/* 标题 */
.announcement-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
}
/* 内容 */
.announcement-content {
margin-bottom: 16px;
}
.announcement-content pre {
white-space: pre-wrap;
word-break: break-word;
margin: 0;
font-family: inherit;
line-height: 1.6;
}
/* 底部 */
.announcement-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
}
/* 时间 */
.announcement-time {
color: var(--announcement-time);
}
/* ===== 明亮模式变量 ===== */
:root {
--announcement-bg: #eaf3ff;
--announcement-text: #303133;
--announcement-time: #909399;
--announcement-bg-info: #e1e1e1;
--announcement-text-info: #000000;
--announcement-bg-success: #f0f9eb;
--announcement-text-success: #3a7a3a;
--announcement-bg-warning: #fdf6ec;
--announcement-text-warning: #b26a00;
--announcement-bg-danger: #fef0f0;
--announcement-text-danger: #a94442;
}
/* ===== 暗色模式Element Plus ===== */
html.dark {
--announcement-bg: #1f1f1f;
--announcement-text: #e5eaf3;
--announcement-time: #a3a6ad;
--announcement-bg-info: #575e64;
--announcement-text-info: #ffffff;
--announcement-bg-success: #1e2b22;
--announcement-text-success: #a5d6a7;
--announcement-bg-warning: #2c211b;
--announcement-text-warning: #ffd54f;
--announcement-bg-danger: #2a1a1a;
--announcement-text-danger: #ef9a9a;
}
</style>

View File

@@ -1,661 +0,0 @@
<template>
<div class="resource-management">
<!-- 搜索栏和统计 -->
<div class="search-statistics-row">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="包类型">
<el-select
v-model="searchForm.package_type"
placeholder="全部"
clearable
@change="handleSearch"
>
<el-option label="MSI" value="msi" />
<el-option label="MSIX" value="msix" />
</el-select>
</el-form-item>
<el-form-item label="激活状态">
<el-select
v-model="searchForm.is_active"
placeholder="全部"
clearable
@change="handleSearch"
>
<el-option label="已激活" value="true" />
<el-option label="未激活" value="false" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :loading="loading">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 统计信息 -->
<div class="statistics" v-if="!loading && resourceList.length > 0">
<el-statistic title="资源总数" :value="resourceList.length" />
<el-statistic title="MSI包" :value="msiCount" />
<el-statistic title="MSIX包" :value="msixCount" />
<el-statistic title="已激活" :value="activeCount" />
</div>
</div>
<!-- 操作按钮 -->
<div class="toolbar">
<el-button type="success" @click="handleCreate">创建资源</el-button>
<el-button type="primary" @click="handleRefresh" :loading="loading">刷新</el-button>
</div>
<!-- 资源表格 -->
<el-table
:data="resourceList"
style="width: 100%"
border
v-loading="loading"
element-loading-text="正在加载资源数据..."
>
<el-table-column prop="version" label="版本号" width="120" />
<el-table-column label="包类型" width="100">
<template #default="scope">
<el-tag :type="scope.row.package_type === 'msix' ? 'success' : 'primary'" size="small">
{{ scope.row.package_type.toUpperCase() }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="激活状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.is_active ? 'success' : 'info'" size="small">
{{ scope.row.is_active ? '已激活' : '未激活' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="file_size" label="文件大小" width="120" />
<el-table-column label="下载链接" min-width="200">
<template #default="scope">
<el-link
:href="scope.row.download_url"
target="_blank"
type="primary"
:underline="false"
>
{{ scope.row.download_url }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="features" label="新功能描述" min-width="250" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="scope">
{{ formatTime(scope.row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button
size="small"
type="primary"
link
@click="handleView(scope.row)"
>
详情
</el-button>
<el-button
size="small"
type="warning"
link
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
size="small"
type="danger"
link
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 空状态 -->
<el-empty v-if="!loading && resourceList.length === 0" description="暂无资源数据" />
<!-- 资源详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
title="资源详情"
width="60%"
>
<div v-if="currentResource" class="resource-detail">
<el-descriptions :column="2" border>
<el-descriptions-item label="版本号">
<el-tag type="primary">{{ currentResource.version }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="包类型">
<el-tag :type="currentResource.package_type === 'msix' ? 'success' : 'primary'">
{{ currentResource.package_type.toUpperCase() }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="激活状态">
<el-tag :type="currentResource.is_active ? 'success' : 'info'">
{{ currentResource.is_active ? '已激活' : '未激活' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="文件大小">
{{ currentResource.file_size || '-' }}
</el-descriptions-item>
<el-descriptions-item label="文件哈希" :span="2">
{{ currentResource.file_hash || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建者ID">
{{ currentResource.created_by }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatTime(currentResource.created_at) }}
</el-descriptions-item>
<el-descriptions-item label="下载链接" :span="2">
<el-link
:href="currentResource.download_url"
target="_blank"
type="primary"
>
{{ currentResource.download_url }}
</el-link>
</el-descriptions-item>
<el-descriptions-item label="新功能描述" :span="2">
<div class="features-content">
{{ currentResource.features || '无' }}
</div>
</el-descriptions-item>
</el-descriptions>
</div>
</el-dialog>
<!-- 创建资源弹窗 -->
<el-dialog
v-model="createDialogVisible"
title="创建资源"
width="60%"
>
<el-form
ref="createFormRef"
:model="createForm"
:rules="createRules"
label-width="120px"
>
<el-form-item label="版本号" prop="version">
<el-input
v-model="createForm.version"
placeholder="请输入版本号1.0.0"
/>
</el-form-item>
<el-form-item label="包类型" prop="package_type">
<el-select
v-model="createForm.package_type"
placeholder="请选择包类型"
style="width: 100%"
>
<el-option label="MSI" value="msi" />
<el-option label="MSIX" value="msix" />
</el-select>
</el-form-item>
<el-form-item label="下载链接" prop="download_url">
<el-input
v-model="createForm.download_url"
placeholder="请输入下载URL"
/>
</el-form-item>
<el-form-item label="文件大小">
<el-input
v-model="createForm.file_size"
placeholder="可选114514KB"
/>
</el-form-item>
<el-form-item label="文件哈希">
<el-input
v-model="createForm.file_hash"
placeholder="可选,文件哈希值"
/>
</el-form-item>
<el-form-item label="激活状态">
<el-switch
v-model="createForm.is_active"
active-text="已激活"
inactive-text="未激活"
/>
</el-form-item>
<el-form-item label="测试版">
<el-switch
v-model="createForm.is_test"
active-text=""
inactive-text=""
/>
</el-form-item>
<el-form-item label="新功能描述">
<el-input
v-model="createForm.features"
type="textarea"
:rows="4"
placeholder="可选,描述新功能内容"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="createLoading"
@click="handleCreateSubmit"
>
创建
</el-button>
</div>
</template>
</el-dialog>
<!-- 编辑资源弹窗 -->
<el-dialog
v-model="editDialogVisible"
title="编辑资源"
width="60%"
>
<el-form
ref="editFormRef"
:model="editForm"
:rules="editRules"
label-width="120px"
>
<el-form-item label="版本号" prop="version">
<el-input
v-model="editForm.version"
placeholder="请输入版本号1.0.0"
/>
</el-form-item>
<el-form-item label="包类型" prop="package_type">
<el-select
v-model="editForm.package_type"
placeholder="请选择包类型"
style="width: 100%"
>
<el-option label="MSI" value="msi" />
<el-option label="MSIX" value="msix" />
</el-select>
</el-form-item>
<el-form-item label="下载链接" prop="download_url">
<el-input
v-model="editForm.download_url"
placeholder="请输入下载URL"
/>
</el-form-item>
<el-form-item label="文件大小">
<el-input
v-model="editForm.file_size"
placeholder="可选114514KB"
/>
</el-form-item>
<el-form-item label="文件哈希">
<el-input
v-model="editForm.file_hash"
placeholder="可选,文件哈希值"
/>
</el-form-item>
<el-form-item label="激活状态">
<el-switch
v-model="editForm.is_active"
active-text="已激活"
inactive-text="未激活"
/>
</el-form-item>
<el-form-item label="测试版">
<el-switch
v-model="editForm.is_test"
active-text=""
inactive-text=""
/>
</el-form-item>
<el-form-item label="新功能描述">
<el-input
v-model="editForm.features"
type="textarea"
:rows="4"
placeholder="可选,描述新功能内容"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="editLoading"
@click="handleEditSubmit"
>
保存
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import {
getDownloadResourceListApi,
deleteDownloadResourceApi,
createDownloadResourceApi,
updateDownloadResourceApi,
type DownloadResource,
type CreateResourceRequest,
} from '@/api/download'
interface SearchForm {
package_type: string
is_active: string
}
const searchForm = reactive<SearchForm>({
package_type: '',
is_active: '',
})
const resourceList = ref<DownloadResource[]>([])
const loading = ref(false)
const detailDialogVisible = ref(false)
const currentResource = ref<DownloadResource | null>(null)
// 创建资源相关
const createDialogVisible = ref(false)
const createLoading = ref(false)
const createFormRef = ref<FormInstance>()
const createForm = reactive<CreateResourceRequest>({
version: '',
package_type: 'msix',
download_url: '',
features: '',
file_size: '',
file_hash: '',
is_active: true,
is_test: false,
})
const createRules: FormRules = {
version: [
{ required: true, message: '请输入版本号', trigger: 'blur' },
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式应为 x.y.z如 1.0.0', trigger: 'blur' },
],
package_type: [
{ required: true, message: '请选择包类型', trigger: 'change' },
],
download_url: [
{ required: true, message: '请输入下载链接', trigger: 'blur' },
{ type: 'url', message: '请输入有效的URL地址', trigger: 'blur' },
],
}
// 编辑资源相关
const editDialogVisible = ref(false)
const editLoading = ref(false)
const editFormRef = ref<FormInstance>()
const currentEditId = ref<string | null>(null)
const editForm = reactive<CreateResourceRequest>({
version: '',
package_type: 'msix',
download_url: '',
features: '',
file_size: '',
file_hash: '',
is_active: true,
is_test: false,
})
const editRules: FormRules = {
version: [
{ required: true, message: '请输入版本号', trigger: 'blur' },
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式应为 x.y.z如 1.0.0', trigger: 'blur' },
],
package_type: [
{ required: true, message: '请选择包类型', trigger: 'change' },
],
download_url: [
{ required: true, message: '请输入下载链接', trigger: 'blur' },
{ type: 'url', message: '请输入有效的URL地址', trigger: 'blur' },
],
}
// 统计数据
const msiCount = computed(() =>
resourceList.value.filter(item => item.package_type === 'msi').length
)
const msixCount = computed(() =>
resourceList.value.filter(item => item.package_type === 'msix').length
)
const activeCount = computed(() =>
resourceList.value.filter(item => item.is_active === true).length
)
// 格式化时间
function formatTime(dateStr: string) {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
// 获取资源列表
async function fetchResourceList() {
loading.value = true
try {
const params: Record<string, any> = {}
if (searchForm.package_type) {
params.package_type = searchForm.package_type
}
if (searchForm.is_active) {
params.is_active = searchForm.is_active
}
const data = await getDownloadResourceListApi(Object.keys(params).length > 0 ? params : undefined)
resourceList.value = data || []
} catch (error) {
console.error('获取资源列表失败:', error)
ElMessage.error('获取资源列表失败')
resourceList.value = []
} finally {
loading.value = false
}
}
function handleSearch() {
fetchResourceList()
}
function handleReset() {
searchForm.package_type = ''
searchForm.is_active = ''
fetchResourceList()
}
function handleRefresh() {
fetchResourceList()
}
function handleCreate() {
// 重置表单
Object.assign(createForm, {
version: '',
package_type: 'msix',
download_url: '',
features: '',
file_size: '',
file_hash: '',
is_active: true,
is_test: false,
})
createDialogVisible.value = true
}
async function handleCreateSubmit() {
if (!createFormRef.value) return
try {
await createFormRef.value.validate()
createLoading.value = true
const result = await createDownloadResourceApi(createForm)
if (result && result.id) {
ElMessage.success('资源创建成功')
createDialogVisible.value = false
// 刷新列表
await fetchResourceList()
} else {
ElMessage.error('创建资源失败')
}
} catch (error) {
ElMessage.error('创建资源失败')
} finally {
createLoading.value = false
}
}
function handleEdit(resource: DownloadResource) {
if (!resource.id) {
ElMessage.error('资源ID不存在无法编辑')
return
}
currentEditId.value = resource.id
// 填充表单数据
Object.assign(editForm, {
version: resource.version,
package_type: resource.package_type,
download_url: resource.download_url,
features: resource.features || '',
file_size: resource.file_size || '',
file_hash: resource.file_hash || '',
is_active: resource.is_active ?? true,
is_test: resource.is_test ?? false,
})
editDialogVisible.value = true
}
async function handleEditSubmit() {
if (!editFormRef.value || !currentEditId.value) return
try {
await editFormRef.value.validate()
editLoading.value = true
await updateDownloadResourceApi(currentEditId.value, editForm)
ElMessage.success('资源更新成功')
editDialogVisible.value = false
// 刷新列表
await fetchResourceList()
} catch (error) {
ElMessage.error('更新资源失败')
} finally {
editLoading.value = false
}
}
function handleView(resource: DownloadResource) {
currentResource.value = resource
detailDialogVisible.value = true
}
async function handleDelete(resource: DownloadResource) {
try {
await ElMessageBox.confirm(
`确定要删除版本 ${resource.version}${resource.package_type.toUpperCase()} 包吗?`,
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
if (!resource.id) {
ElMessage.error('资源ID不存在无法删除')
return
}
await deleteDownloadResourceApi(resource.id)
ElMessage.success('资源删除成功')
await fetchResourceList()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除资源失败')
}
}
}
onMounted(() => {
fetchResourceList()
})
</script>
<style scoped>
.resource-management {
padding: 24px;
}
.search-statistics-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16px;
}
.search-form {
margin-bottom: 0;
flex: 1;
}
.statistics {
margin-left: 32px;
display: flex;
gap: 24px;
margin-top: 0;
}
.toolbar {
margin-bottom: 16px;
}
.features-content {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.6;
}
</style>

View File

@@ -1,730 +0,0 @@
<template>
<div class="download-page">
<Header
:app-icon="appIcon"
:app-name="appName"
:actions="headerActions"
/>
<div class="download-content">
<div class="download-section">
<h1 class="section-title">
<el-icon><Download /></el-icon>
下载中心
</h1>
<p class="section-description">选择适合您的安装包立即开始使用 Snap Hutao<br>系统要求新版本Windows10及Windows11</p>
<!-- 版本类型切换 -->
<div class="version-tabs">
<el-tabs v-model="versionType" @tab-change="switchVersionType">
<el-tab-pane label="正式版" name="stable">
<template #label>
<span class="tab-label">
<el-icon><Document /></el-icon>
正式版
</span>
</template>
</el-tab-pane>
<el-tab-pane label="测试版" name="test">
<template #label>
<span class="tab-label">
<el-icon><Box /></el-icon>
测试版
</span>
</template>
</el-tab-pane>
</el-tabs>
<!-- 测试版警告提示 -->
<el-alert
v-if="versionType === 'test'"
title="测试版说明"
type="warning"
:closable="false"
show-icon
>
<p>测试版可能存在未知的 Bug 和稳定性问题仅供测试和体验新功能使用如需稳定使用请下载正式版</p>
</el-alert>
</div>
<!-- 最新版本下载区域 -->
<div v-if="latestVersion" class="latest-version-card">
<div class="latest-header">
<el-tag
:type="versionType === 'test' ? 'warning' : 'success'"
size="large"
effect="dark"
>
{{ versionType === 'test' ? '最新测试版本' : '最新版本' }}
</el-tag>
<h2 class="version-title">{{ latestVersion.version }}</h2>
</div>
<div v-if="latestVersion.features" class="features-section">
<h3 class="features-title">更新内容</h3>
<p class="features-text">{{ latestVersion.features }}</p>
</div>
<div class="download-buttons">
<el-tooltip
v-if="latestVersion.packages.msi"
effect="dark"
placement="top"
:content="getTooltipText(latestVersion, 'msi')"
>
<el-button
type="primary"
size="large"
@click="downloadFile(latestVersion, 'msi')"
:loading="downloading"
>
<el-icon><Document /></el-icon>
<span>下载 MSI 安装包</span>
</el-button>
</el-tooltip>
<el-tooltip
v-if="latestVersion.packages.msix"
effect="dark"
placement="top"
:content="getTooltipText(latestVersion, 'msix')"
>
<el-button
type="success"
size="large"
@click="downloadFile(latestVersion, 'msix')"
:loading="downloading"
>
<el-icon><Box /></el-icon>
<span>下载 MSIX 安装包</span>
</el-button>
</el-tooltip>
</div>
<div class="version-info">
<span class="info-item">
<el-icon><Clock /></el-icon>
发布时间{{ formatDate(latestVersion.created_at) }}
</span>
</div>
</div>
<!-- 历史版本列表仅正式版显示 -->
<el-divider v-if="versionType === 'stable'">历史版本</el-divider>
<div v-loading="loading" class="history-list">
<!-- 正式版显示历史版本空状态 -->
<div v-if="versionType === 'stable' && historyVersions.length === 0 && !loading" class="empty-state">
<el-icon><FolderOpened /></el-icon>
<p>暂无历史版本</p>
</div>
<!-- 正式版显示历史版本列表 -->
<div v-else-if="versionType === 'stable' && historyVersions.length > 0" class="version-table">
<div
v-for="(item, index) in historyVersions"
:key="index"
class="version-item"
>
<div class="version-main">
<div class="version-number">{{ item.version }}</div>
<div class="version-meta">
<span class="meta-item">
<el-icon><Clock /></el-icon>
{{ formatDate(item.created_at) }}
</span>
</div>
<div v-if="item.features" class="version-features">
{{ item.features }}
</div>
</div>
<div class="version-actions">
<el-tooltip
v-if="item.packages.msi"
effect="dark"
placement="top"
:content="getTooltipText(item, 'msi')"
>
<el-button
type="primary"
size="small"
@click="downloadFile(item, 'msi')"
>
MSI
</el-button>
</el-tooltip>
<el-tooltip
v-if="item.packages.msix"
effect="dark"
placement="top"
:content="getTooltipText(item, 'msix')"
>
<el-button
type="success"
size="small"
@click="downloadFile(item, 'msix')"
>
MSIX
</el-button>
</el-tooltip>
</div>
</div>
</div>
<!-- 测试版显示空状态 -->
<div v-if="versionType === 'test' && !latestVersion && !loading" class="empty-state">
<el-icon><FolderOpened /></el-icon>
<p>暂无测试版本</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
Download,
Document,
Box,
Clock,
FolderOpened,
} from '@element-plus/icons-vue'
import Header from '@/components/Header.vue'
import { getDownloadResourcesApi, getTestVersionApi } from '@/api/download'
const router = useRouter()
// 配置项
const appIcon = ref('/HT_logo.png')
const appName = ref('Snap Hutao')
// 页头右侧按钮配置
const headerActions = ref([
{
id: 'home',
label: '返回首页',
icon: undefined,
component: 'el-button',
props: {
type: 'default',
link: true
},
onClick: () => {
router.push('/')
}
}
])
// 版本信息接口(合并不同包类型)
interface VersionInfo {
version: string
created_at: string
created_by: string
features: string | null
file_hash: string | null
file_size: string | null
is_active: boolean | null
packages: {
msi: string | null
msix: string | null
msi_size: string | null
msix_size: string | null
}
}
// 包类型描述
const packageDescriptions = {
msi: '传统安装包方便部署但是可能有BUG。',
msix: '现代化安装包稳定性更好原生体验推荐使用安装稍繁琐解压以后右键Add-AppDevPackage.ps1选择“以PowerShell运行”进行安装。',
}
// 数据状态
const latestVersion = ref<VersionInfo | null>(null)
const historyVersions = ref<VersionInfo[]>([])
const loading = ref(false)
const downloading = ref(false)
const versionType = ref<'stable' | 'test'>('stable')
/**
* 格式化日期
*/
function formatDate(dateStr: string) {
const date = new Date(dateStr)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
}
/**
* 下载文件
*/
function downloadFile(item: VersionInfo, packageType: 'msi' | 'msix') {
const downloadUrl = item.packages[packageType]
if (!downloadUrl) {
ElMessage.warning(`${item.version} 版本暂无 ${packageType.toUpperCase()} 安装包`)
return
}
downloading.value = true
const a = document.createElement('a')
a.href = downloadUrl
// 如果是msix的话文件是用zip格式压缩的
if (packageType === 'msix') {
a.download = `Snap.Hutao.${item.version}.zip`
} else {
a.download = `Snap.Hutao.${item.version}.msi`
}
a.target = '_blank'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
ElMessage.success(`开始下载 ${item.version} 版本的 ${packageType.toUpperCase()} 安装包`)
setTimeout(() => {
downloading.value = false
}, 1000)
}
/**
* 获取下载按钮的提示内容
*/
function getTooltipText(item: VersionInfo, packageType: 'msi' | 'msix') {
const size = item.packages[`${packageType}_size` as keyof typeof item.packages] as string | null
const desc = packageDescriptions[packageType]
if (size) {
return `${desc}\n文件大小${size}`
}
return desc
}
/**
* 加载正式版本
*/
async function loadAllVersions() {
try {
loading.value = true
const data = await getDownloadResourcesApi()
if (!data || data.length === 0) {
return
}
// 按版本号分组
const versionMap = new Map<string, VersionInfo>()
data.forEach((item) => {
if (!versionMap.has(item.version)) {
// 首次遇到该版本,创建新记录
versionMap.set(item.version, {
version: item.version,
created_at: item.created_at,
created_by: item.created_by,
features: item.features,
file_hash: item.file_hash,
file_size: item.file_size,
is_active: item.is_active,
packages: {
msi: null,
msix: null,
msi_size: null,
msix_size: null,
},
})
}
// 添加包类型的下载链接和大小
const versionInfo = versionMap.get(item.version)
if (versionInfo) {
if (item.package_type === 'msi') {
versionInfo.packages.msi = item.download_url
versionInfo.packages.msi_size = item.file_size
} else if (item.package_type === 'msix') {
versionInfo.packages.msix = item.download_url
versionInfo.packages.msix_size = item.file_size
}
}
})
// 转换为数组并按创建时间倒序排序
const versions = Array.from(versionMap.values()).sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
)
if (versions.length > 0) {
latestVersion.value = versions[0] ?? null
historyVersions.value = versions.slice(1)
}
} catch (error) {
console.error('加载版本列表失败:', error)
ElMessage.error('加载版本列表失败')
} finally {
loading.value = false
}
}
/**
* 加载测试版本
*/
async function loadTestVersion() {
try {
loading.value = true
const data = await getTestVersionApi()
if (!data || data.length === 0) {
latestVersion.value = null
historyVersions.value = []
return
}
// 按版本号分组
const versionMap = new Map<string, VersionInfo>()
data.forEach((item) => {
if (!versionMap.has(item.version)) {
// 首次遇到该版本,创建新记录
versionMap.set(item.version, {
version: item.version,
created_at: item.created_at,
created_by: item.created_by,
features: item.features,
file_hash: item.file_hash,
file_size: item.file_size,
is_active: item.is_active,
packages: {
msi: null,
msix: null,
msi_size: null,
msix_size: null,
},
})
}
// 添加包类型的下载链接和大小
const versionInfo = versionMap.get(item.version)
if (versionInfo) {
if (item.package_type === 'msi') {
versionInfo.packages.msi = item.download_url
versionInfo.packages.msi_size = item.file_size
} else if (item.package_type === 'msix') {
versionInfo.packages.msix = item.download_url
versionInfo.packages.msix_size = item.file_size
}
}
})
// 转换为数组并按创建时间倒序排序
const versions = Array.from(versionMap.values()).sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
)
if (versions.length > 0) {
latestVersion.value = versions[0] ?? null
// 测试版本只显示最新版本,不显示历史版本
historyVersions.value = []
} else {
latestVersion.value = null
historyVersions.value = []
}
} catch (error) {
console.error('加载测试版本失败:', error)
ElMessage.error('加载测试版本失败')
} finally {
loading.value = false
}
}
/**
* 切换版本类型
*/
function switchVersionType(type: 'stable' | 'test') {
versionType.value = type
latestVersion.value = null
historyVersions.value = []
if (type === 'stable') {
loadAllVersions()
} else {
loadTestVersion()
}
}
onMounted(() => {
loadAllVersions()
})
</script>
<style scoped>
.download-page {
min-height: 100vh;
background: var(--main-bg);
display: flex;
flex-direction: column;
}
.download-content {
flex: 1;
padding: 40px 20px;
}
.download-section {
max-width: 1000px;
margin: 0 auto;
}
.section-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 36px;
font-weight: 700;
margin-bottom: 12px;
color: var(--text-color);
background: linear-gradient(135deg, var(--aside-active), #67c23a);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.section-description {
font-size: 16px;
color: var(--text-color);
opacity: 0.8;
margin-bottom: 20px;
}
/* 版本类型切换 */
.version-tabs {
margin-bottom: 32px;
}
.version-tabs :deep(.el-tabs__header) {
margin: 0;
}
.version-tabs :deep(.el-tabs__nav-wrap::after) {
display: none;
}
.version-tabs :deep(.el-tabs__item) {
font-size: 16px;
padding: 0 24px;
}
.version-tabs .tab-label {
display: flex;
align-items: center;
gap: 6px;
}
.version-tabs :deep(.el-alert) {
margin-top: 16px;
padding: 12px 16px;
}
.version-tabs :deep(.el-alert__description) {
margin-top: 8px;
font-size: 14px;
line-height: 1.5;
}
/* 最新版本卡片 */
.latest-version-card {
background: var(--card-bg);
border-radius: 16px;
padding: 32px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
margin-bottom: 40px;
}
.latest-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
}
.version-title {
font-size: 32px;
font-weight: 600;
color: var(--text-color);
margin: 0;
}
.features-section {
margin-bottom: 24px;
}
.features-title {
font-size: 16px;
font-weight: 600;
color: var(--text-color);
margin-bottom: 12px;
}
.features-text {
font-size: 14px;
color: var(--text-color);
opacity: 0.8;
line-height: 1.6;
white-space: pre-wrap;
background: var(--main-bg);
padding: 16px;
border-radius: 8px;
}
.download-buttons {
display: flex;
gap: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.download-buttons .el-button {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
font-size: 16px;
border-radius: 8px;
min-width: 200px;
justify-content: center;
}
.version-info {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.info-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: var(--text-color);
opacity: 0.7;
}
/* 历史版本列表 */
.history-list {
min-height: 200px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-color);
opacity: 0.5;
}
.empty-state .el-icon {
font-size: 64px;
margin-bottom: 16px;
}
.version-table {
display: flex;
flex-direction: column;
gap: 16px;
}
.version-item {
background: var(--card-bg);
border-radius: 12px;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
transition: all 0.3s ease;
}
.version-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
.version-main {
flex: 1;
}
.version-number {
font-size: 20px;
font-weight: 600;
color: var(--text-color);
margin-bottom: 8px;
}
.version-meta {
display: flex;
gap: 16px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.meta-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
color: var(--text-color);
opacity: 0.7;
}
.version-features {
font-size: 14px;
color: var(--text-color);
opacity: 0.8;
line-height: 1.6;
white-space: pre-wrap;
background: var(--main-bg);
padding: 12px;
border-radius: 6px;
max-height: 80px;
overflow-y: auto;
}
.version-actions {
display: flex;
gap: 12px;
flex-shrink: 0;
}
/* 响应式 */
@media (max-width: 768px) {
.section-title {
font-size: 28px;
}
.latest-version-card {
padding: 24px;
}
.download-buttons {
flex-direction: column;
}
.download-buttons .el-button {
width: 100%;
}
.version-item {
flex-direction: column;
align-items: flex-start;
}
.version-actions {
width: 100%;
justify-content: flex-end;
}
}
</style>

View File

@@ -1,229 +0,0 @@
<template>
<div class="home-page">
<Header
:app-icon="appIcon"
:app-name="appName"
:actions="headerActions"
/>
<div class="home-content">
<div class="hero-section">
<img :src="appIcon" alt="App Icon" class="hero-icon" />
<h1 class="hero-title">{{ appName }}</h1>
<p class="hero-description">
{{ appDescription }}
</p>
<div class="hero-buttons">
<template v-for="button in heroButtons" :key="button.id">
<el-button
:type="button.type || 'primary'"
:size="button.size || 'large'"
@click="button.onClick"
>
<el-icon v-if="button.icon">
<component :is="button.icon" />
</el-icon>
<span>{{ button.label }}</span>
</el-button>
</template>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { Setting, Document, ChatDotRound } from '@element-plus/icons-vue'
import Header from '@/components/Header.vue'
import { ElMessage } from 'element-plus'
const router = useRouter()
// 配置项
const appIcon = ref('/HT_logo.png')
const appName = ref('Snap Hutao')
const appDescription = ref('胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。')
// 页头右侧按钮配置
const headerActions = ref([
{
id: 'github',
label: 'GitHub 仓库',
icon: Document,
component: 'el-button',
props: {
type: 'default',
link: true
},
onClick: () => {
window.open('https://github.com/wangdage12/Snap.Hutao', '_blank')
}
},
{
id: 'discord',
label: '加入 Discord 服务器',
icon: ChatDotRound,
component: 'el-button',
props: {
type: 'default',
link: true
},
onClick: () => {
window.open('https://discord.gg/ucH3mgeWpQ', '_blank')
}
},
{
id: 'console',
label: '控制台',
icon: Setting,
component: 'el-button',
props: {
type: 'primary'
},
onClick: () => {
router.push('/dashboard')
}
}
])
// 主页中心按钮配置
const heroButtons = ref([
// {
// id: 'btn1',
// label: '快速开始',
// type: 'primary',
// size: 'large',
// icon: undefined,
// onClick: () => {
// window.open('https://github.com/wangdage12/Snap.Hutao', '_blank')
// }
// },
{
id: 'btn2',
label: '立即下载',
type: 'primary',
size: 'large',
icon: undefined,
onClick: () => {
router.push('/download')
}
},
{
id: 'btn3',
label: '查看文档',
type: 'default',
size: 'large',
icon: undefined,
onClick: () => {
// window.open('https://github.com/wangdage12/Snap.Server.Web', '_blank')
ElMessage.info('文档正在编写中,敬请期待!')
}
}
])
</script>
<style scoped>
.home-page {
min-height: 100vh;
background: var(--main-bg);
display: flex;
flex-direction: column;
}
.home-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
}
.hero-section {
text-align: center;
max-width: 800px;
}
.hero-icon {
width: 120px;
height: 120px;
margin-bottom: 10px;
border-radius: 20px;
object-fit: contain;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.hero-title {
font-size: 48px;
font-weight: 700;
margin-bottom: 16px;
color: var(--text-color);
background: linear-gradient(135deg, var(--aside-active), #67c23a);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-description {
font-size: 18px;
color: var(--text-color);
line-height: 1.6;
margin-bottom: 40px;
opacity: 0.8;
}
.hero-buttons {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
.hero-buttons .el-button {
display: flex;
align-items: center;
gap: 6px;
padding: 16px 32px;
font-size: 16px;
border-radius: 8px;
transition: all 0.3s ease;
}
.hero-buttons .el-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.app-header {
padding: 12px 16px;
}
.hero-title {
font-size: 36px;
}
.hero-description {
font-size: 16px;
}
.hero-buttons {
flex-direction: column;
align-items: stretch;
}
.hero-buttons .el-button {
width: 100%;
}
}
</style>

View File

@@ -1,212 +1,7 @@
<template> <template>
<div class="login-container"> <div class="login-container">
<!-- 粒子动画背景 --> <h1>Login Page</h1>
<vue-particles <!-- Login form goes here -->
id="tsparticles"
:options="particleOptions"
/>
<el-card class="login-card">
<h2 class="title">Snap Hutao服务器管理-登录</h2>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="rules"
label-width="80px"
>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="loginForm.email"
placeholder="请输入邮箱"
clearable
/>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
show-password
clearable
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="loading"
style="width: 100%"
@click="handleLogin"
>
登录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div> </div>
</template> </template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { webLoginApi } from '@/api/auth'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
// 表单实例
const loginFormRef = ref<FormInstance>()
// loading 状态
const loading = ref(false)
// 表单数据
const loginForm = reactive({
email: '',
password: '',
})
const router = useRouter()
// 校验规则
const rules: FormRules = {
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少 6 个字符', trigger: 'blur' },
],
}
// 登录方法
const handleLogin = async () => {
if (!loginFormRef.value) return
await loginFormRef.value.validate(async (valid) => {
if (!valid) return
loading.value = true
try {
const response = await webLoginApi(loginForm)
const userStore = useUserStore()
// 处理响应数据结构
const tokenData = response as any
userStore.setToken(tokenData.access_token)
// 获取用户信息
await userStore.fetchUserInfo()
ElMessage.success('登录成功')
router.push({ path: '/dashboard' })
} catch (error: any) {
ElMessage.error(error.message || '登录失败,请重试')
} finally {
loading.value = false
}
})
}
// 粒子配置选项
const particleOptions = ref({
background: {
color: "#0d0d0d"
},
fpsLimit: 60,
particles: {
number: {
value: 200,
density: {
enable: true,
area: 800
}
},
color: {
value: "#00bcd4"
},
shape: {
type: "circle"
},
opacity: {
value: 0.6
},
size: {
value: 2
},
move: {
enable: true,
speed: 0.6,
direction: "none",
outModes: "out"
},
links: {
enable: true,
distance: 120,
color: "#00bcd4",
opacity: 0.4,
width: 1
}
},
interactivity: {
events: {
onHover: {
enable: true,
mode: "grab"
},
onClick: {
enable: true,
mode: "push"
}
},
modes: {
grab: {
distance: 180,
links: {
opacity: 0.6
}
},
push: {
quantity: 4
}
}
},
detectRetina: true
})
</script>
<style scoped>
.login-container {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #409eff, #66b1ff);
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
width: 400px;
padding: 20px;
position: relative;
z-index: 10;
}
.title {
text-align: center;
margin-bottom: 20px;
}
</style>

View File

@@ -1,213 +0,0 @@
<template>
<div class="user-management">
<!-- 搜索栏和用户统计并排 -->
<div class="search-statistics-row">
<!-- GitHub样式搜索框 -->
<div class="search-container">
<GitHubSearchInput
ref="githubSearch"
v-model="searchQuery"
placeholder="搜索用户... 例如: role:developer username:test"
@search="handleGitHubSearch"
/>
<div class="search-actions">
<el-button type="primary" @click="executeSearch" :loading="loading">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
</div>
<!-- 用户统计 -->
<div class="statistics" v-if="!loading && displayUserList.length > 0">
<el-statistic title="当前显示用户数" :value="displayUserList.length" />
<el-statistic title="运维人员" :value="maintainerCount" />
<el-statistic title="开发者" :value="developerCount" />
<el-statistic v-if="isSearchMode" title="搜索模式" value="进行中" />
</div>
</div>
<!-- 操作按钮 -->
<div class="toolbar">
<el-button type="primary" @click="handleRefresh" :loading="loading">刷新</el-button>
</div>
<!-- 用户表格 -->
<el-table
:data="displayUserList"
style="width: 100%"
border
v-loading="loading"
element-loading-text="正在加载用户数据..."
>
<el-table-column prop="_id" label="ID" width="120" />
<el-table-column prop="UserName" label="用户名" min-width="150" />
<el-table-column prop="email" label="邮箱" min-width="200" />
<el-table-column label="角色" width="150">
<template #default="scope">
<el-tag v-if="scope.row.IsMaintainer" type="danger">运维</el-tag>
<el-tag v-if="scope.row.IsLicensedDeveloper" type="success">开发者</el-tag>
<el-tag v-if="!scope.row.IsMaintainer && !scope.row.IsLicensedDeveloper" type="info">普通用户</el-tag>
</template>
</el-table-column>
<el-table-column prop="CreatedAt" label="注册时间" width="180" />
<el-table-column label="权限状态" width="120">
<template #default="scope">
<el-tag :type="scope.row.IsMaintainer || scope.row.IsLicensedDeveloper ? 'success' : 'info'">
{{ scope.row.IsMaintainer || scope.row.IsLicensedDeveloper ? '高权限' : '普通' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<!-- 空状态 -->
<el-empty v-if="!loading && displayUserList.length === 0" description="暂无用户数据" />
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import GitHubSearchInput from '@/components/GitHubSearchInput.vue'
import { getUserListApi, type UserListItem } from '@/api/user'
const githubSearch = ref<InstanceType<typeof GitHubSearchInput>>()
const searchQuery = ref('')
const currentFilters = ref<Record<string, string>>({})
const userList = ref<UserListItem[]>([])
const loading = ref(false)
const isSearchMode = ref(false)
// 显示的用户列表(直接使用后端返回的数据)
const displayUserList = computed(() => {
return userList.value
})
// 统计数据(基于当前显示的列表)
const maintainerCount = computed(() =>
displayUserList.value.filter(user => user.IsMaintainer).length
)
const developerCount = computed(() =>
displayUserList.value.filter(user => user.IsLicensedDeveloper).length
)
// 获取用户列表
async function fetchUserList(filters?: Record<string, string>) {
loading.value = true
try {
const data = await getUserListApi(filters)
userList.value = data
if (filters && Object.keys(filters).length > 0) {
ElMessage.success(`找到 ${data.length} 个匹配的用户`)
isSearchMode.value = true
} else {
ElMessage.success('用户列表加载成功')
isSearchMode.value = false
}
} catch (error) {
console.error('获取用户列表失败:', error)
ElMessage.error('获取用户列表失败,请检查权限设置')
userList.value = []
} finally {
loading.value = false
}
}
// 处理GitHub搜索
function handleGitHubSearch(_query: string, filters: Record<string, string>) {
currentFilters.value = filters
fetchUserList(filters)
}
// 执行搜索(通过搜索框)
function executeSearch() {
if (githubSearch.value) {
githubSearch.value.handleSearch()
} else {
handleGitHubSearch(searchQuery.value, currentFilters.value)
}
}
// 重置搜索
function handleReset() {
searchQuery.value = ''
currentFilters.value = {}
fetchUserList()
}
// 刷新列表
function handleRefresh() {
if (isSearchMode.value && Object.keys(currentFilters.value).length > 0) {
fetchUserList(currentFilters.value)
} else {
fetchUserList()
}
}
// 页面加载时获取用户列表
onMounted(() => {
fetchUserList()
})
</script>
<style scoped>
.user-management {
padding: 24px;
}
.search-statistics-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16px;
gap: 24px;
}
.search-container {
width: 600px;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 12px;
}
.search-container :deep(.github-search-container) {
flex: 1;
min-width: 0;
}
.search-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.statistics {
display: flex;
gap: 24px;
flex-shrink: 0;
}
.toolbar {
margin-bottom: 16px;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.search-statistics-row {
flex-direction: column;
}
.search-container {
width: 100%;
}
.statistics {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
}
}
</style>