mirror of
https://github.com/wangdage12/Snap.Server.Web.git
synced 2026-02-18 02:42:14 +08:00
Compare commits
1 Commits
dev
...
b0a7b98091
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0a7b98091 |
@@ -1 +0,0 @@
|
|||||||
VITE_API_BASE_URL = http://localhost:5222/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
VITE_API_BASE_URL = https://htserver.wdg.cloudns.ch/api/
|
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal 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
500
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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 |
@@ -1 +0,0 @@
|
|||||||
Copyright © 2025 DGP Studio, All Rights Reserved.
|
|
||||||
@@ -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',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<template>
|
|
||||||
<router-view></router-view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
// 这是一个占位组件,用于嵌套路由
|
|
||||||
</script>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
10
src/main.ts
10
src/main.ts
@@ -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)
|
||||||
|
|||||||
@@ -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' },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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()
|
|
||||||
})
|
|
||||||
@@ -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')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
Reference in New Issue
Block a user