Upload 7 files
Browse files- .gitattributes +1 -0
- package-lock.json +0 -0
- package.json +18 -0
- public/bg.png +3 -0
- public/favicon.png +0 -0
- public/index.html +1758 -0
- server.js +695 -0
- zbpack.json +4 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
public/bg.png filter=lfs diff=lfs merge=lfs -text
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "zeabur-monitor",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Multi-account Zeabur monitoring dashboard",
|
| 5 |
+
"main": "server.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node server.js",
|
| 8 |
+
"dev": "nodemon server.js"
|
| 9 |
+
},
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"cors": "^2.8.5",
|
| 12 |
+
"dotenv": "^16.3.1",
|
| 13 |
+
"express": "^4.18.2"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"nodemon": "^3.0.1"
|
| 17 |
+
}
|
| 18 |
+
}
|
public/bg.png
ADDED
|
Git LFS Details
|
public/favicon.png
ADDED
|
|
public/index.html
ADDED
|
@@ -0,0 +1,1758 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Zeabur 多账号监控面板</title>
|
| 7 |
+
<link rel="icon" type="image/png" href="favicon.png">
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
| 9 |
+
<style>
|
| 10 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 11 |
+
:root {
|
| 12 |
+
--card-opacity: 0.3;
|
| 13 |
+
}
|
| 14 |
+
@keyframes gradient {
|
| 15 |
+
0% { background-position: 0% 50%; }
|
| 16 |
+
50% { background-position: 100% 50%; }
|
| 17 |
+
100% { background-position: 0% 50%; }
|
| 18 |
+
}
|
| 19 |
+
body {
|
| 20 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 21 |
+
background: url('bg.png') center/cover fixed;
|
| 22 |
+
position: relative;
|
| 23 |
+
min-height: 100vh;
|
| 24 |
+
padding: 20px;
|
| 25 |
+
}
|
| 26 |
+
body::before {
|
| 27 |
+
content: '';
|
| 28 |
+
position: fixed;
|
| 29 |
+
top: 0;
|
| 30 |
+
left: 0;
|
| 31 |
+
right: 0;
|
| 32 |
+
bottom: 0;
|
| 33 |
+
background: rgba(255, 255, 255, 0.05);
|
| 34 |
+
z-index: 0;
|
| 35 |
+
}
|
| 36 |
+
.container {
|
| 37 |
+
max-width: 1400px;
|
| 38 |
+
margin: 0 auto;
|
| 39 |
+
position: relative;
|
| 40 |
+
z-index: 1;
|
| 41 |
+
}
|
| 42 |
+
h1 {
|
| 43 |
+
color: white;
|
| 44 |
+
margin-bottom: 30px;
|
| 45 |
+
font-size: 28px;
|
| 46 |
+
font-weight: 700;
|
| 47 |
+
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
| 48 |
+
}
|
| 49 |
+
.refresh-btn {
|
| 50 |
+
background: linear-gradient(135deg, #f696c6 0%, #fbb6d8 100%);
|
| 51 |
+
color: white;
|
| 52 |
+
border: 2px solid rgba(255,255,255,0.3);
|
| 53 |
+
padding: 10px 20px;
|
| 54 |
+
border-radius: 25px;
|
| 55 |
+
cursor: pointer;
|
| 56 |
+
font-size: 14px;
|
| 57 |
+
font-weight: 600;
|
| 58 |
+
transition: all 0.3s;
|
| 59 |
+
box-shadow: 0 4px 15px rgba(246,150,198,0.3);
|
| 60 |
+
}
|
| 61 |
+
.refresh-btn:hover {
|
| 62 |
+
transform: translateY(-2px);
|
| 63 |
+
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
|
| 64 |
+
}
|
| 65 |
+
.refresh-btn:disabled {
|
| 66 |
+
opacity: 0.6;
|
| 67 |
+
cursor: not-allowed;
|
| 68 |
+
transform: none;
|
| 69 |
+
}
|
| 70 |
+
@keyframes fadeInUp {
|
| 71 |
+
from {
|
| 72 |
+
opacity: 0;
|
| 73 |
+
transform: translateY(20px);
|
| 74 |
+
}
|
| 75 |
+
to {
|
| 76 |
+
opacity: 1;
|
| 77 |
+
transform: translateY(0);
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
.account-card {
|
| 81 |
+
background: rgba(255,255,255,var(--card-opacity));
|
| 82 |
+
backdrop-filter: blur(var(--blur-amount)) saturate(var(--saturate-amount));
|
| 83 |
+
-webkit-backdrop-filter: blur(var(--blur-amount)) saturate(var(--saturate-amount));
|
| 84 |
+
border-radius: 20px;
|
| 85 |
+
padding: 24px;
|
| 86 |
+
margin-bottom: 20px;
|
| 87 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, var(--shadow-opacity));
|
| 88 |
+
border: 1px solid rgba(255,255,255,var(--border-opacity));
|
| 89 |
+
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.2s ease, z-index 0s 0.2s;
|
| 90 |
+
animation: fadeInUp 0.6s ease-out;
|
| 91 |
+
will-change: transform;
|
| 92 |
+
transform: translate3d(0, 0, 0);
|
| 93 |
+
position: relative;
|
| 94 |
+
z-index: 1;
|
| 95 |
+
}
|
| 96 |
+
.account-card:hover {
|
| 97 |
+
transform: translate3d(0, -8px, 0) scale(1.02);
|
| 98 |
+
border-color: rgba(255,255,255,0.3);
|
| 99 |
+
z-index: 10;
|
| 100 |
+
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.2s ease, z-index 0s 0s;
|
| 101 |
+
}
|
| 102 |
+
.account-header {
|
| 103 |
+
display: flex;
|
| 104 |
+
justify-content: space-between;
|
| 105 |
+
align-items: center;
|
| 106 |
+
margin-bottom: 15px;
|
| 107 |
+
padding: 10px;
|
| 108 |
+
margin: -10px -10px 15px -10px;
|
| 109 |
+
border-radius: 12px;
|
| 110 |
+
transition: background 0.2s;
|
| 111 |
+
}
|
| 112 |
+
.account-header:hover {
|
| 113 |
+
background: rgba(255,255,255,0.1);
|
| 114 |
+
}
|
| 115 |
+
.account-name {
|
| 116 |
+
font-size: 20px;
|
| 117 |
+
font-weight: bold;
|
| 118 |
+
color: #333;
|
| 119 |
+
}
|
| 120 |
+
.balance {
|
| 121 |
+
font-size: 24px;
|
| 122 |
+
font-weight: bold;
|
| 123 |
+
color: #4CAF50;
|
| 124 |
+
}
|
| 125 |
+
.balance.low { color: #ff9800; }
|
| 126 |
+
.balance.critical { color: #f44336; }
|
| 127 |
+
.projects-grid {
|
| 128 |
+
display: grid;
|
| 129 |
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
| 130 |
+
gap: 16px;
|
| 131 |
+
margin-top: 15px;
|
| 132 |
+
}
|
| 133 |
+
@keyframes shimmer {
|
| 134 |
+
0% { background-position: -1000px 0; }
|
| 135 |
+
100% { background-position: 1000px 0; }
|
| 136 |
+
}
|
| 137 |
+
.project-card {
|
| 138 |
+
background: rgba(255,255,255,var(--card-opacity));
|
| 139 |
+
backdrop-filter: blur(var(--blur-amount-small)) saturate(var(--saturate-amount));
|
| 140 |
+
-webkit-backdrop-filter: blur(var(--blur-amount-small)) saturate(var(--saturate-amount));
|
| 141 |
+
border-radius: 16px;
|
| 142 |
+
padding: 20px;
|
| 143 |
+
border: 1px solid rgba(255,255,255,var(--border-opacity-light));
|
| 144 |
+
position: relative;
|
| 145 |
+
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
| 146 |
+
display: flex;
|
| 147 |
+
flex-direction: column;
|
| 148 |
+
min-height: 280px;
|
| 149 |
+
height: 100%;
|
| 150 |
+
overflow: hidden;
|
| 151 |
+
box-shadow: 0 4px 16px rgba(0, 0, 0, var(--shadow-opacity));
|
| 152 |
+
}
|
| 153 |
+
.project-card::before {
|
| 154 |
+
content: '';
|
| 155 |
+
position: absolute;
|
| 156 |
+
top: 0;
|
| 157 |
+
left: -2px;
|
| 158 |
+
right: -2px;
|
| 159 |
+
bottom: 0;
|
| 160 |
+
background: linear-gradient(135deg, #f696c6, #fbb6d8, #fdd7e8);
|
| 161 |
+
border-radius: 16px;
|
| 162 |
+
z-index: -1;
|
| 163 |
+
opacity: 0;
|
| 164 |
+
transition: opacity 0.4s;
|
| 165 |
+
}
|
| 166 |
+
.project-card:hover {
|
| 167 |
+
transform: translateY(-8px) scale(1.03);
|
| 168 |
+
}
|
| 169 |
+
.project-card:hover::before {
|
| 170 |
+
opacity: 1;
|
| 171 |
+
}
|
| 172 |
+
.project-name {
|
| 173 |
+
font-weight: bold;
|
| 174 |
+
color: #333;
|
| 175 |
+
margin-bottom: 5px;
|
| 176 |
+
}
|
| 177 |
+
.project-name button:hover {
|
| 178 |
+
background: rgba(246, 150, 198, 0.4) !important;
|
| 179 |
+
transform: scale(1.1);
|
| 180 |
+
}
|
| 181 |
+
.project-info {
|
| 182 |
+
font-size: 12px;
|
| 183 |
+
color: #666;
|
| 184 |
+
margin-bottom: 10px;
|
| 185 |
+
}
|
| 186 |
+
.service-item {
|
| 187 |
+
background: rgba(255,255,255,var(--service-opacity));
|
| 188 |
+
backdrop-filter: blur(var(--blur-amount-tiny)) saturate(var(--saturate-amount));
|
| 189 |
+
-webkit-backdrop-filter: blur(var(--blur-amount-tiny)) saturate(var(--saturate-amount));
|
| 190 |
+
padding: 14px 16px;
|
| 191 |
+
margin: 8px 0;
|
| 192 |
+
border-radius: 12px;
|
| 193 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 194 |
+
border: 1px solid rgba(255, 255, 255, var(--border-opacity-strong));
|
| 195 |
+
min-height: 70px;
|
| 196 |
+
display: flex;
|
| 197 |
+
align-items: center;
|
| 198 |
+
gap: 20px;
|
| 199 |
+
position: relative;
|
| 200 |
+
overflow: hidden;
|
| 201 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, var(--shadow-opacity-light));
|
| 202 |
+
}
|
| 203 |
+
.service-item::before {
|
| 204 |
+
content: '';
|
| 205 |
+
position: absolute;
|
| 206 |
+
top: 0;
|
| 207 |
+
left: -100%;
|
| 208 |
+
width: 100%;
|
| 209 |
+
height: 100%;
|
| 210 |
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
|
| 211 |
+
transition: left 0.5s;
|
| 212 |
+
}
|
| 213 |
+
.service-item:hover {
|
| 214 |
+
background: rgba(255,255,255,0.45);
|
| 215 |
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
| 216 |
+
transform: translateX(8px) scale(1.02);
|
| 217 |
+
border-color: rgba(255, 255, 255, 0.6);
|
| 218 |
+
}
|
| 219 |
+
.service-item:hover::before {
|
| 220 |
+
left: 100%;
|
| 221 |
+
}
|
| 222 |
+
.services-container {
|
| 223 |
+
flex: 1;
|
| 224 |
+
display: flex;
|
| 225 |
+
flex-direction: column;
|
| 226 |
+
}
|
| 227 |
+
.modal-overlay {
|
| 228 |
+
position: fixed;
|
| 229 |
+
top: 0;
|
| 230 |
+
left: 0;
|
| 231 |
+
right: 0;
|
| 232 |
+
bottom: 0;
|
| 233 |
+
background: rgba(0,0,0,0.5);
|
| 234 |
+
backdrop-filter: blur(4px);
|
| 235 |
+
display: flex;
|
| 236 |
+
align-items: center;
|
| 237 |
+
justify-content: center;
|
| 238 |
+
z-index: 1000;
|
| 239 |
+
}
|
| 240 |
+
.modal-content {
|
| 241 |
+
background: white;
|
| 242 |
+
border-radius: 16px;
|
| 243 |
+
padding: 30px;
|
| 244 |
+
max-width: 500px;
|
| 245 |
+
width: 90%;
|
| 246 |
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
| 247 |
+
}
|
| 248 |
+
.modal-title {
|
| 249 |
+
font-size: 24px;
|
| 250 |
+
font-weight: 700;
|
| 251 |
+
color: #333;
|
| 252 |
+
margin-bottom: 20px;
|
| 253 |
+
}
|
| 254 |
+
.input-group {
|
| 255 |
+
margin-bottom: 20px;
|
| 256 |
+
}
|
| 257 |
+
.input-label {
|
| 258 |
+
display: block;
|
| 259 |
+
font-size: 14px;
|
| 260 |
+
font-weight: 600;
|
| 261 |
+
color: #666;
|
| 262 |
+
margin-bottom: 8px;
|
| 263 |
+
}
|
| 264 |
+
.input-field {
|
| 265 |
+
width: 100%;
|
| 266 |
+
padding: 12px 16px;
|
| 267 |
+
border: 2px solid #e5e7eb;
|
| 268 |
+
border-radius: 8px;
|
| 269 |
+
font-size: 14px;
|
| 270 |
+
transition: all 0.3s;
|
| 271 |
+
}
|
| 272 |
+
.input-field:focus {
|
| 273 |
+
outline: none;
|
| 274 |
+
border-color: #f696c6;
|
| 275 |
+
box-shadow: 0 0 0 3px rgba(246,150,198,0.1);
|
| 276 |
+
}
|
| 277 |
+
.btn-primary {
|
| 278 |
+
background: linear-gradient(135deg, #f696c6 0%, #fbb6d8 100%);
|
| 279 |
+
color: white;
|
| 280 |
+
border: none;
|
| 281 |
+
padding: 12px 24px;
|
| 282 |
+
border-radius: 8px;
|
| 283 |
+
font-size: 14px;
|
| 284 |
+
font-weight: 600;
|
| 285 |
+
cursor: pointer;
|
| 286 |
+
transition: all 0.3s;
|
| 287 |
+
width: 100%;
|
| 288 |
+
}
|
| 289 |
+
.btn-primary:hover {
|
| 290 |
+
transform: translateY(-2px);
|
| 291 |
+
box-shadow: 0 6px 20px rgba(246,150,198,0.4);
|
| 292 |
+
}
|
| 293 |
+
.btn-secondary {
|
| 294 |
+
background: #f3f4f6;
|
| 295 |
+
color: #666;
|
| 296 |
+
border: none;
|
| 297 |
+
padding: 12px 24px;
|
| 298 |
+
border-radius: 8px;
|
| 299 |
+
font-size: 14px;
|
| 300 |
+
font-weight: 600;
|
| 301 |
+
cursor: pointer;
|
| 302 |
+
transition: all 0.3s;
|
| 303 |
+
width: 100%;
|
| 304 |
+
margin-top: 10px;
|
| 305 |
+
}
|
| 306 |
+
.btn-secondary:hover {
|
| 307 |
+
background: #e5e7eb;
|
| 308 |
+
}
|
| 309 |
+
.add-account-btn {
|
| 310 |
+
background: rgba(255,255,255,0.2);
|
| 311 |
+
color: white;
|
| 312 |
+
border: 2px solid rgba(255,255,255,0.3);
|
| 313 |
+
padding: 10px 20px;
|
| 314 |
+
border-radius: 25px;
|
| 315 |
+
cursor: pointer;
|
| 316 |
+
font-size: 14px;
|
| 317 |
+
font-weight: 600;
|
| 318 |
+
transition: all 0.3s;
|
| 319 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
| 320 |
+
}
|
| 321 |
+
.add-account-btn:hover {
|
| 322 |
+
background: rgba(255,255,255,0.3);
|
| 323 |
+
transform: translateY(-2px);
|
| 324 |
+
}
|
| 325 |
+
input[type="range"] {
|
| 326 |
+
-webkit-appearance: none;
|
| 327 |
+
appearance: none;
|
| 328 |
+
background: rgba(255,255,255,0.3);
|
| 329 |
+
border-radius: 10px;
|
| 330 |
+
height: 6px;
|
| 331 |
+
outline: none;
|
| 332 |
+
}
|
| 333 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 334 |
+
-webkit-appearance: none;
|
| 335 |
+
appearance: none;
|
| 336 |
+
width: 18px;
|
| 337 |
+
height: 18px;
|
| 338 |
+
border-radius: 50%;
|
| 339 |
+
background: white;
|
| 340 |
+
cursor: pointer;
|
| 341 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
| 342 |
+
}
|
| 343 |
+
input[type="range"]::-moz-range-thumb {
|
| 344 |
+
width: 18px;
|
| 345 |
+
height: 18px;
|
| 346 |
+
border-radius: 50%;
|
| 347 |
+
background: white;
|
| 348 |
+
cursor: pointer;
|
| 349 |
+
border: none;
|
| 350 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
| 351 |
+
}
|
| 352 |
+
@keyframes pulse {
|
| 353 |
+
0%, 100% { opacity: 1; }
|
| 354 |
+
50% { opacity: 0.7; }
|
| 355 |
+
}
|
| 356 |
+
.status {
|
| 357 |
+
padding: 6px 14px;
|
| 358 |
+
border-radius: 20px;
|
| 359 |
+
font-size: 11px;
|
| 360 |
+
font-weight: 700;
|
| 361 |
+
text-transform: uppercase;
|
| 362 |
+
letter-spacing: 0.5px;
|
| 363 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
| 364 |
+
position: relative;
|
| 365 |
+
overflow: hidden;
|
| 366 |
+
}
|
| 367 |
+
.status::before {
|
| 368 |
+
content: '';
|
| 369 |
+
position: absolute;
|
| 370 |
+
top: 50%;
|
| 371 |
+
left: 50%;
|
| 372 |
+
width: 100%;
|
| 373 |
+
height: 100%;
|
| 374 |
+
background: rgba(255,255,255,0.3);
|
| 375 |
+
transform: translate(-50%, -50%) scale(0);
|
| 376 |
+
border-radius: 50%;
|
| 377 |
+
transition: transform 0.6s;
|
| 378 |
+
}
|
| 379 |
+
.status:hover::before {
|
| 380 |
+
transform: translate(-50%, -50%) scale(2);
|
| 381 |
+
}
|
| 382 |
+
.status.running {
|
| 383 |
+
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
| 384 |
+
color: white;
|
| 385 |
+
animation: pulse 2s ease-in-out infinite;
|
| 386 |
+
}
|
| 387 |
+
.status.stopped {
|
| 388 |
+
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
|
| 389 |
+
color: white;
|
| 390 |
+
}
|
| 391 |
+
.status.suspended {
|
| 392 |
+
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
|
| 393 |
+
color: white;
|
| 394 |
+
}
|
| 395 |
+
.status.deploying {
|
| 396 |
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
| 397 |
+
color: white;
|
| 398 |
+
}
|
| 399 |
+
.status.paused {
|
| 400 |
+
background: linear-gradient(135deg, #4b6cb7 0%, #182848 100%);
|
| 401 |
+
color: white;
|
| 402 |
+
}
|
| 403 |
+
.error {
|
| 404 |
+
background: #ffebee;
|
| 405 |
+
color: #c62828;
|
| 406 |
+
padding: 15px;
|
| 407 |
+
border-radius: 6px;
|
| 408 |
+
margin-bottom: 15px;
|
| 409 |
+
}
|
| 410 |
+
.loading {
|
| 411 |
+
text-align: center;
|
| 412 |
+
padding: 40px;
|
| 413 |
+
color: white;
|
| 414 |
+
font-size: 16px;
|
| 415 |
+
}
|
| 416 |
+
.stats {
|
| 417 |
+
display: grid;
|
| 418 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 419 |
+
gap: 10px;
|
| 420 |
+
margin-top: 10px;
|
| 421 |
+
}
|
| 422 |
+
.stat-box {
|
| 423 |
+
padding: 10px;
|
| 424 |
+
border-radius: 4px;
|
| 425 |
+
text-align: center;
|
| 426 |
+
}
|
| 427 |
+
.stat-label {
|
| 428 |
+
font-size: 12px;
|
| 429 |
+
color: #666;
|
| 430 |
+
margin-bottom: 5px;
|
| 431 |
+
}
|
| 432 |
+
.stat-value {
|
| 433 |
+
font-size: 18px;
|
| 434 |
+
font-weight: bold;
|
| 435 |
+
color: #333;
|
| 436 |
+
}
|
| 437 |
+
</style>
|
| 438 |
+
</head>
|
| 439 |
+
<body>
|
| 440 |
+
<div id="app">
|
| 441 |
+
<!-- 首次设置密码界面 -->
|
| 442 |
+
<div v-if="showSetPasswordModal" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 10000;">
|
| 443 |
+
<div style="background: white; padding: 40px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.2); max-width: 400px; width: 90%;">
|
| 444 |
+
<h2 style="text-align: center; color: #f696c6; margin-bottom: 10px;">🎉 欢迎使用</h2>
|
| 445 |
+
<p style="text-align: center; color: #666; font-size: 14px; margin-bottom: 30px;">请设置管理员密码</p>
|
| 446 |
+
<div style="margin-bottom: 20px;">
|
| 447 |
+
<input
|
| 448 |
+
v-model="setPassword"
|
| 449 |
+
type="password"
|
| 450 |
+
placeholder="请输入密码(至少6位)"
|
| 451 |
+
@keyup.enter="setAdminPassword"
|
| 452 |
+
style="width: 100%; padding: 12px; border: 2px solid #f696c6; border-radius: 8px; font-size: 14px; outline: none;"
|
| 453 |
+
/>
|
| 454 |
+
</div>
|
| 455 |
+
<div style="margin-bottom: 20px;">
|
| 456 |
+
<input
|
| 457 |
+
v-model="setPasswordConfirm"
|
| 458 |
+
type="password"
|
| 459 |
+
placeholder="请再次输入密码"
|
| 460 |
+
@keyup.enter="setAdminPassword"
|
| 461 |
+
style="width: 100%; padding: 12px; border: 2px solid #f696c6; border-radius: 8px; font-size: 14px; outline: none;"
|
| 462 |
+
/>
|
| 463 |
+
</div>
|
| 464 |
+
<div v-if="setPasswordError" style="color: #c00; font-size: 13px; margin-bottom: 15px; text-align: center;">
|
| 465 |
+
{{ setPasswordError }}
|
| 466 |
+
</div>
|
| 467 |
+
<button
|
| 468 |
+
@click="setAdminPassword"
|
| 469 |
+
style="width: 100%; padding: 12px; background: #f696c6; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer;"
|
| 470 |
+
>
|
| 471 |
+
设置密码
|
| 472 |
+
</button>
|
| 473 |
+
</div>
|
| 474 |
+
</div>
|
| 475 |
+
|
| 476 |
+
<!-- 登录界面 -->
|
| 477 |
+
<div v-if="showLoginModal" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 10000;">
|
| 478 |
+
<div style="background: white; padding: 40px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.2); max-width: 400px; width: 90%;">
|
| 479 |
+
<h2 style="text-align: center; color: #f696c6; margin-bottom: 30px;">🔐 管理员登录</h2>
|
| 480 |
+
<div style="margin-bottom: 20px;">
|
| 481 |
+
<input
|
| 482 |
+
v-model="loginPassword"
|
| 483 |
+
type="password"
|
| 484 |
+
placeholder="请输入管理员密码"
|
| 485 |
+
@keyup.enter="verifyPassword"
|
| 486 |
+
style="width: 100%; padding: 12px; border: 2px solid #f696c6; border-radius: 8px; font-size: 14px; outline: none;"
|
| 487 |
+
/>
|
| 488 |
+
</div>
|
| 489 |
+
<div v-if="loginError" style="color: #c00; font-size: 13px; margin-bottom: 15px; text-align: center;">
|
| 490 |
+
{{ loginError }}
|
| 491 |
+
</div>
|
| 492 |
+
<button
|
| 493 |
+
@click="verifyPassword"
|
| 494 |
+
style="width: 100%; padding: 12px; background: #f696c6; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer;"
|
| 495 |
+
>
|
| 496 |
+
登录
|
| 497 |
+
</button>
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
|
| 501 |
+
<div v-if="isAuthenticated" class="container">
|
| 502 |
+
<h1>🚀 Zeabur 多账号监控面板</h1>
|
| 503 |
+
<div style="background: rgba(255,255,255,0.9); backdrop-filter: blur(10px); padding: 15px 18px; border-radius: 12px; margin-bottom: 15px; font-size: 13px; border: 1px solid rgba(255,255,255,0.3); box-shadow: 0 4px 12px rgba(0,0,0,0.1);">
|
| 504 |
+
<div style="font-weight: 700; margin-bottom: 8px; color: #f696c6;">💡 使用说明</div>
|
| 505 |
+
<div style="line-height: 1.8; color: #555;">
|
| 506 |
+
• 数据每30秒自动刷新
|
| 507 |
+
</div>
|
| 508 |
+
</div>
|
| 509 |
+
|
| 510 |
+
<div style="display: flex; gap: 10px; margin-bottom: 20px; align-items: center; flex-wrap: wrap;">
|
| 511 |
+
<button class="refresh-btn" @click="fetchData" :disabled="loading">
|
| 512 |
+
{{ loading ? '加载中...' : '🔄 刷新数据' }}
|
| 513 |
+
</button>
|
| 514 |
+
<button class="add-account-btn" @click="showManageModal = true">
|
| 515 |
+
⚙️ 管理账号
|
| 516 |
+
</button>
|
| 517 |
+
<button class="add-account-btn" @click="clearCache" style="background: rgba(255,100,100,0.3);">
|
| 518 |
+
🗑️ 清除缓存
|
| 519 |
+
</button>
|
| 520 |
+
|
| 521 |
+
<!-- 透明度控制 -->
|
| 522 |
+
<div style="display: flex; align-items: center; gap: 10px; background: rgba(255,255,255,0.2); backdrop-filter: blur(10px); padding: 8px 16px; border-radius: 25px; border: 1px solid rgba(255,255,255,0.3);">
|
| 523 |
+
<span style="color: white; font-size: 13px; font-weight: 600;">透明度</span>
|
| 524 |
+
<input
|
| 525 |
+
type="range"
|
| 526 |
+
v-model="opacity"
|
| 527 |
+
min="0"
|
| 528 |
+
max="100"
|
| 529 |
+
style="width: 100px; cursor: pointer;"
|
| 530 |
+
/>
|
| 531 |
+
<span style="color: white; font-size: 13px; font-weight: 600; min-width: 35px;">{{ opacity }}%</span>
|
| 532 |
+
</div>
|
| 533 |
+
|
| 534 |
+
<span style="color: white; font-size: 14px; margin-left: auto; text-shadow: 0 2px 4px rgba(0,0,0,0.2);">
|
| 535 |
+
上次更新: {{ lastUpdate }}
|
| 536 |
+
</span>
|
| 537 |
+
</div>
|
| 538 |
+
|
| 539 |
+
<!-- 日志模态框 -->
|
| 540 |
+
<div v-if="showLogsModal" class="modal-overlay" @click.self="showLogsModal = false" style="z-index: 2000;">
|
| 541 |
+
<div style="background: #1e1e1e; border-radius: 16px; width: 90%; max-width: 900px; height: 80vh; display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.5);">
|
| 542 |
+
<div style="padding: 16px 20px; border-bottom: 1px solid #333; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;">
|
| 543 |
+
<h2 style="color: #f696c6; font-size: 16px; margin: 0;">📋 {{ logsModalTitle }}</h2>
|
| 544 |
+
<button @click="showLogsModal = false" style="background: #444; color: #fff; border: none; width: 28px; height: 28px; border-radius: 6px; cursor: pointer; font-size: 16px; line-height: 1;">×</button>
|
| 545 |
+
</div>
|
| 546 |
+
<div style="padding: 10px 20px; background: #264f78; color: #d4d4d4; font-size: 12px; display: flex; gap: 16px; flex-wrap: wrap; flex-shrink: 0;">
|
| 547 |
+
<span>项目: {{ logsModalInfo.project }}</span>
|
| 548 |
+
<span>账号: {{ logsModalInfo.account }}</span>
|
| 549 |
+
<span>日志: {{ logsModalInfo.count }} 条</span>
|
| 550 |
+
<span>时间: {{ logsModalInfo.time }}</span>
|
| 551 |
+
</div>
|
| 552 |
+
<div style="flex: 1; overflow-y: auto; padding: 12px 16px; min-height: 0;">
|
| 553 |
+
<div v-if="logsLoading" style="text-align: center; padding: 40px; color: #888;">⏳ 正在加载日志...</div>
|
| 554 |
+
<pre v-else style="background: #252526; padding: 12px; border-radius: 8px; font-family: Consolas, Monaco, monospace; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; color: #d4d4d4; margin: 0; min-height: 100%;">{{ logsContent || '暂无日志' }}</pre>
|
| 555 |
+
</div>
|
| 556 |
+
<div style="padding: 12px 20px; border-top: 1px solid #333; text-align: right; flex-shrink: 0;">
|
| 557 |
+
<button @click="showLogsModal = false" style="padding: 8px 20px; background: #f696c6; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; font-size: 13px;">关闭</button>
|
| 558 |
+
</div>
|
| 559 |
+
</div>
|
| 560 |
+
</div>
|
| 561 |
+
|
| 562 |
+
<!-- 账号管理模态框 -->
|
| 563 |
+
<div v-if="showManageModal" class="modal-overlay" @mousedown.self="showManageModal = false">
|
| 564 |
+
<div class="modal-content" style="max-width: 700px;">
|
| 565 |
+
<div class="modal-title">⚙️ 账号管理</div>
|
| 566 |
+
|
| 567 |
+
<!-- 账号列表 -->
|
| 568 |
+
<div style="margin-bottom: 20px;">
|
| 569 |
+
<!-- 显示本地管理的账号 -->
|
| 570 |
+
<div v-for="(account, index) in managedAccounts" :key="'local-' + index"
|
| 571 |
+
style="background: #f9fafb; padding: 15px; border-radius: 8px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
|
| 572 |
+
<div style="flex: 1;">
|
| 573 |
+
<div style="font-weight: 600; color: #333; margin-bottom: 4px;">{{ account.name }}</div>
|
| 574 |
+
<div style="font-size: 12px; color: #999;">{{ maskEmail(account.email) || '未知邮箱' }}</div>
|
| 575 |
+
</div>
|
| 576 |
+
<button @click="removeAccount(index)"
|
| 577 |
+
style="background: #fee; color: #c00; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">
|
| 578 |
+
🗑️ 删除
|
| 579 |
+
</button>
|
| 580 |
+
</div>
|
| 581 |
+
|
| 582 |
+
<!-- 显示服务器配置的账号(只读) -->
|
| 583 |
+
<div v-if="managedAccounts.length === 0 && accounts.length > 0">
|
| 584 |
+
<div style="font-size: 12px; color: #666; margin-bottom: 10px; padding: 10px; background: #f0f9ff; border-radius: 6px;">
|
| 585 |
+
💡 以下账号来自服务器配置(.env 文件),无法在此删除
|
| 586 |
+
</div>
|
| 587 |
+
<div v-for="account in accounts" :key="'server-' + account.name"
|
| 588 |
+
style="background: #f9fafb; padding: 15px; border-radius: 8px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
|
| 589 |
+
<div style="flex: 1;">
|
| 590 |
+
<div style="font-weight: 600; color: #333; margin-bottom: 4px;">{{ account.name }}</div>
|
| 591 |
+
<div style="font-size: 12px; color: #999;">{{ account.data?.email || account.data?.username || '服务器配置' }}</div>
|
| 592 |
+
</div>
|
| 593 |
+
<span style="font-size: 12px; color: #999; padding: 6px 12px; background: #e5e7eb; border-radius: 6px;">
|
| 594 |
+
🔒 服务器配置
|
| 595 |
+
</span>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
|
| 599 |
+
<div v-if="managedAccounts.length === 0 && accounts.length === 0" style="text-align: center; color: #999; padding: 30px;">
|
| 600 |
+
暂无账号,点击下方按钮添加
|
| 601 |
+
</div>
|
| 602 |
+
</div>
|
| 603 |
+
|
| 604 |
+
<!-- 添加新账号表单 -->
|
| 605 |
+
<div style="border-top: 2px solid #e5e7eb; padding-top: 20px;">
|
| 606 |
+
<div style="font-weight: 600; margin-bottom: 15px; color: #f696c6;">➕ 添加新账号</div>
|
| 607 |
+
|
| 608 |
+
<div class="input-group">
|
| 609 |
+
<label class="input-label">账号名称</label>
|
| 610 |
+
<input
|
| 611 |
+
v-model="newAccount.name"
|
| 612 |
+
type="text"
|
| 613 |
+
class="input-field"
|
| 614 |
+
placeholder="例如:我的账号"
|
| 615 |
+
/>
|
| 616 |
+
</div>
|
| 617 |
+
|
| 618 |
+
<div class="input-group">
|
| 619 |
+
<label class="input-label">API Token</label>
|
| 620 |
+
<input
|
| 621 |
+
v-model="newAccount.token"
|
| 622 |
+
type="password"
|
| 623 |
+
class="input-field"
|
| 624 |
+
placeholder="sk-xxxxxxxxxxxxxxxx"
|
| 625 |
+
/>
|
| 626 |
+
</div>
|
| 627 |
+
|
| 628 |
+
<div v-if="addAccountError" style="background: #fee; color: #c00; padding: 10px; border-radius: 6px; margin-bottom: 15px; font-size: 13px;">
|
| 629 |
+
{{ addAccountError }}
|
| 630 |
+
</div>
|
| 631 |
+
|
| 632 |
+
<div v-if="addAccountSuccess" style="background: #efe; color: #060; padding: 10px; border-radius: 6px; margin-bottom: 15px; font-size: 13px;">
|
| 633 |
+
{{ addAccountSuccess }}
|
| 634 |
+
</div>
|
| 635 |
+
|
| 636 |
+
<button class="btn-primary" @click="addAccountToList" :disabled="addingAccount">
|
| 637 |
+
{{ addingAccount ? '验证中...' : '➕ 添加到列表' }}
|
| 638 |
+
</button>
|
| 639 |
+
</div>
|
| 640 |
+
|
| 641 |
+
<!-- 批量添加 -->
|
| 642 |
+
<div style="margin-top: 30px; padding-top: 30px; border-top: 2px dashed #e5e7eb;">
|
| 643 |
+
<div style="font-weight: 600; color: #333; margin-bottom: 15px; font-size: 16px;">📦 批量添加账号</div>
|
| 644 |
+
<div style="margin-bottom: 10px; font-size: 13px; color: #666;">
|
| 645 |
+
每行一个账号,支持格式:<code style="background: #f3f4f6; padding: 2px 6px; border-radius: 4px;">名称:Token</code> 或 <code style="background: #f3f4f6; padding: 2px 6px; border-radius: 4px;">名称:Token</code> 或 <code style="background: #f3f4f6; padding: 2px 6px; border-radius: 4px;">名称(Token)</code> 或 <code style="background: #f3f4f6; padding: 2px 6px; border-radius: 4px;">名称(Token)</code>
|
| 646 |
+
</div>
|
| 647 |
+
<div style="position: relative; min-height: 120px;">
|
| 648 |
+
<div style="width: 100%; min-height: 120px; padding: 12px; border: 2px solid transparent; border-radius: 8px; font-size: 13px; font-family: monospace; white-space: pre-wrap; word-break: break-all; pointer-events: none; color: #333; line-height: 1.5;">{{ maskedBatchAccounts }}</div>
|
| 649 |
+
<textarea
|
| 650 |
+
v-model="batchAccounts"
|
| 651 |
+
@input="updateBatchDisplay"
|
| 652 |
+
placeholder="每行一个账号,格式:账号名称:API_Token"
|
| 653 |
+
style="width: 100%; min-height: 120px; padding: 12px; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 13px; font-family: monospace; resize: vertical; outline: none; position: absolute; top: 0; left: 0; color: transparent; caret-color: black; background: transparent; line-height: 1.5;"
|
| 654 |
+
></textarea>
|
| 655 |
+
</div>
|
| 656 |
+
<div v-if="batchAddError" style="color: #c00; font-size: 13px; margin-top: 10px;">
|
| 657 |
+
{{ batchAddError }}
|
| 658 |
+
</div>
|
| 659 |
+
<div v-if="batchAddSuccess" style="color: #0a0; font-size: 13px; margin-top: 10px;">
|
| 660 |
+
{{ batchAddSuccess }}
|
| 661 |
+
</div>
|
| 662 |
+
<button
|
| 663 |
+
class="btn-primary"
|
| 664 |
+
@click="batchAddAccounts"
|
| 665 |
+
:disabled="addingAccount"
|
| 666 |
+
style="margin-top: 15px;"
|
| 667 |
+
>
|
| 668 |
+
{{ addingAccount ? '添加中...' : '📦 批量添加' }}
|
| 669 |
+
</button>
|
| 670 |
+
</div>
|
| 671 |
+
|
| 672 |
+
<button class="btn-secondary" @click="closeManageModal">
|
| 673 |
+
关闭
|
| 674 |
+
</button>
|
| 675 |
+
</div>
|
| 676 |
+
</div>
|
| 677 |
+
|
| 678 |
+
<!-- 旧的添加账号模态框(保留兼容) -->
|
| 679 |
+
<div v-if="showAddModal" class="modal-overlay" @mousedown.self="showAddModal = false">
|
| 680 |
+
<div class="modal-content">
|
| 681 |
+
<div class="modal-title">➕ 添加 Zeabur 账号</div>
|
| 682 |
+
|
| 683 |
+
<div class="input-group">
|
| 684 |
+
<label class="input-label">账号名称</label>
|
| 685 |
+
<input
|
| 686 |
+
v-model="newAccount.name"
|
| 687 |
+
type="text"
|
| 688 |
+
class="input-field"
|
| 689 |
+
placeholder="例如:我的账号"
|
| 690 |
+
/>
|
| 691 |
+
</div>
|
| 692 |
+
|
| 693 |
+
<div class="input-group">
|
| 694 |
+
<label class="input-label">API Token</label>
|
| 695 |
+
<input
|
| 696 |
+
v-model="newAccount.token"
|
| 697 |
+
type="password"
|
| 698 |
+
class="input-field"
|
| 699 |
+
placeholder="sk-xxxxxxxxxxxxxxxx"
|
| 700 |
+
/>
|
| 701 |
+
<div style="font-size: 12px; color: #999; margin-top: 6px;">
|
| 702 |
+
在 Zeabur 控制台的设置中创建 API Token
|
| 703 |
+
</div>
|
| 704 |
+
</div>
|
| 705 |
+
|
| 706 |
+
<div v-if="addAccountError" style="background: #fee; color: #c00; padding: 10px; border-radius: 6px; margin-bottom: 15px; font-size: 13px;">
|
| 707 |
+
{{ addAccountError }}
|
| 708 |
+
</div>
|
| 709 |
+
|
| 710 |
+
<div v-if="addAccountSuccess" style="background: #efe; color: #060; padding: 10px; border-radius: 6px; margin-bottom: 15px; font-size: 13px;">
|
| 711 |
+
{{ addAccountSuccess }}
|
| 712 |
+
</div>
|
| 713 |
+
|
| 714 |
+
<button class="btn-primary" @click="addAccount" :disabled="addingAccount">
|
| 715 |
+
{{ addingAccount ? '验证中...' : '添加账号' }}
|
| 716 |
+
</button>
|
| 717 |
+
|
| 718 |
+
<button class="btn-secondary" @click="closeAddModal">
|
| 719 |
+
取消
|
| 720 |
+
</button>
|
| 721 |
+
</div>
|
| 722 |
+
</div>
|
| 723 |
+
|
| 724 |
+
<div v-if="accounts.length > 0" class="account-card" style="background: rgba(246, 150, 198, 0.3); backdrop-filter: blur(20px) saturate(180%); color: white; border: 1px solid rgba(255,255,255,0.3);">
|
| 725 |
+
<h3 style="margin-bottom: 15px; font-size: 20px; font-weight: 700;">✨ 总览</h3>
|
| 726 |
+
<div class="stats">
|
| 727 |
+
<div class="stat-box" style="background: transparent; border: 1px solid rgba(255,255,255,0.2);">
|
| 728 |
+
<div class="stat-label" style="color: rgba(255,255,255,0.9);">账号数</div>
|
| 729 |
+
<div class="stat-value" style="color: #4ade80;">{{ accounts.length }}</div>
|
| 730 |
+
</div>
|
| 731 |
+
<div class="stat-box" style="background: transparent; border: 1px solid rgba(255,255,255,0.2);">
|
| 732 |
+
<div class="stat-label" style="color: rgba(255,255,255,0.9);">项目总数</div>
|
| 733 |
+
<div class="stat-value" style="color: #4ade80;">{{ totalProjects }}</div>
|
| 734 |
+
</div>
|
| 735 |
+
<div class="stat-box" style="background: transparent; border: 1px solid rgba(255,255,255,0.2);">
|
| 736 |
+
<div class="stat-label" style="color: rgba(255,255,255,0.9);">服务总数</div>
|
| 737 |
+
<div class="stat-value" style="color: #4ade80;">{{ totalServices }}</div>
|
| 738 |
+
</div>
|
| 739 |
+
<div class="stat-box" style="background: transparent; border: 1px solid rgba(255,255,255,0.2);">
|
| 740 |
+
<div class="stat-label" style="color: rgba(255,255,255,0.9);">运行中</div>
|
| 741 |
+
<div class="stat-value" style="color: #4ade80;">{{ runningServices }}</div>
|
| 742 |
+
</div>
|
| 743 |
+
<div class="stat-box" style="background: transparent; border: 1px solid rgba(255,255,255,0.2);">
|
| 744 |
+
<div class="stat-label" style="color: rgba(255,255,255,0.9);">总费用</div>
|
| 745 |
+
<div class="stat-value" style="color: #ef4444;">${{ totalCost.toFixed(2) }}</div>
|
| 746 |
+
</div>
|
| 747 |
+
</div>
|
| 748 |
+
</div>
|
| 749 |
+
|
| 750 |
+
<div v-if="loading && accounts.length === 0" class="loading">
|
| 751 |
+
⚡ 正在加载数据...
|
| 752 |
+
</div>
|
| 753 |
+
|
| 754 |
+
<div v-for="account in accounts" :key="account.name" class="account-card">
|
| 755 |
+
<div class="account-header" @click="toggleAccount(account.name)" style="cursor: pointer;">
|
| 756 |
+
<div style="flex: 1;">
|
| 757 |
+
<div style="display: flex; align-items: center; gap: 10px;">
|
| 758 |
+
<span style="font-size: 20px; transition: transform 0.3s;" :style="{ transform: isAccountExpanded(account.name) ? 'rotate(90deg)' : 'rotate(0deg)' }">
|
| 759 |
+
▶
|
| 760 |
+
</span>
|
| 761 |
+
<div>
|
| 762 |
+
<div class="account-name">{{ account.name }}</div>
|
| 763 |
+
<div v-if="account.data" style="color: #666; font-size: 14px;">
|
| 764 |
+
{{ maskEmail(account.data.email) || account.data.username }}
|
| 765 |
+
</div>
|
| 766 |
+
</div>
|
| 767 |
+
</div>
|
| 768 |
+
</div>
|
| 769 |
+
<div style="display: flex; flex-direction: column; align-items: flex-end; gap: 8px;">
|
| 770 |
+
<div v-if="account.data" class="balance" :class="getBalanceClass(account.data.credit)">
|
| 771 |
+
${{ (account.data.credit / 100).toFixed(2) }}
|
| 772 |
+
</div>
|
| 773 |
+
<div v-if="account.aihub && account.aihub.balance"
|
| 774 |
+
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 8px 16px; border-radius: 12px; font-size: 14px; font-weight: 600; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);">
|
| 775 |
+
🤖 AI Hub: ${{ (account.aihub.balance / 100000).toFixed(2) }}
|
| 776 |
+
</div>
|
| 777 |
+
</div>
|
| 778 |
+
</div>
|
| 779 |
+
|
| 780 |
+
<div v-if="account.error" class="error">
|
| 781 |
+
❌ 错误: {{ account.error }}
|
| 782 |
+
</div>
|
| 783 |
+
|
| 784 |
+
<div v-if="account.projects && isAccountExpanded(account.name)" class="projects-grid">
|
| 785 |
+
<div v-for="project in account.projects" :key="project._id" class="project-card">
|
| 786 |
+
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;">
|
| 787 |
+
<div style="flex: 1;">
|
| 788 |
+
<div class="project-name" style="display: flex; align-items: center; gap: 8px;">
|
| 789 |
+
<span v-if="!project.isEditing">📦 {{ project.name }}</span>
|
| 790 |
+
<input
|
| 791 |
+
v-else
|
| 792 |
+
v-model="project.editingName"
|
| 793 |
+
@keyup.enter="saveProjectName(account, project)"
|
| 794 |
+
@keyup.esc="cancelEditProjectName(project)"
|
| 795 |
+
@blur="saveProjectName(account, project)"
|
| 796 |
+
style="flex: 1; padding: 4px 8px; border: 2px solid #f696c6; border-radius: 6px; font-size: 14px; font-weight: bold; outline: none;"
|
| 797 |
+
ref="projectNameInput"
|
| 798 |
+
/>
|
| 799 |
+
<button
|
| 800 |
+
v-if="!project.isEditing"
|
| 801 |
+
@click="startEditProjectName(project)"
|
| 802 |
+
style="padding: 4px 8px; background: rgba(246, 150, 198, 0.2); border: 1px solid rgba(246, 150, 198, 0.4); border-radius: 6px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center;"
|
| 803 |
+
title="重命名项目"
|
| 804 |
+
>
|
| 805 |
+
✏️
|
| 806 |
+
</button>
|
| 807 |
+
<button
|
| 808 |
+
v-else
|
| 809 |
+
@click="cancelEditProjectName(project)"
|
| 810 |
+
style="padding: 4px 8px; background: rgba(255, 100, 100, 0.2); border: 1px solid rgba(255, 100, 100, 0.4); border-radius: 6px; cursor: pointer; transition: all 0.2s;"
|
| 811 |
+
title="取消"
|
| 812 |
+
>
|
| 813 |
+
✖️
|
| 814 |
+
</button>
|
| 815 |
+
</div>
|
| 816 |
+
<div class="project-info">
|
| 817 |
+
📍 {{ project.region }}
|
| 818 |
+
</div>
|
| 819 |
+
</div>
|
| 820 |
+
<div style="text-align: right;">
|
| 821 |
+
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">本月用量</div>
|
| 822 |
+
<div style="font-size: 20px; font-weight: bold; color: #2196F3;">
|
| 823 |
+
${{ formatCost(project.cost) }}
|
| 824 |
+
<span v-if="!project.hasCostData"
|
| 825 |
+
style="font-size: 12px; color: #ff9800;"
|
| 826 |
+
title="未配置费用数据">
|
| 827 |
+
⚠️
|
| 828 |
+
</span>
|
| 829 |
+
</div>
|
| 830 |
+
</div>
|
| 831 |
+
</div>
|
| 832 |
+
<div style="display: flex; gap: 15px; margin-bottom: 12px; font-size: 13px; color: #666;">
|
| 833 |
+
<span>🔧 {{ project.services.length }} 个服务</span>
|
| 834 |
+
<span>✅ {{ project.services.filter(s => s.status === 'RUNNING').length }} 运行中</span>
|
| 835 |
+
<span>⏸️ {{ project.services.filter(s => s.status === 'SUSPENDED').length }} 已暂停</span>
|
| 836 |
+
</div>
|
| 837 |
+
<div class="services-container">
|
| 838 |
+
<div v-for="service in project.services" :key="service._id" class="service-item" style="flex-direction: column; align-items: stretch; padding: 12px;">
|
| 839 |
+
<!-- 服务名称和资源信息 -->
|
| 840 |
+
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
|
| 841 |
+
<div style="flex: 1; min-width: 0;">
|
| 842 |
+
<div style="font-weight: 600; font-size: 14px; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 4px;" :title="service.name">
|
| 843 |
+
{{ service.name }}
|
| 844 |
+
</div>
|
| 845 |
+
<div v-if="service.resourceLimit" style="font-size: 11px; color: #666; display: flex; gap: 12px;">
|
| 846 |
+
<span style="display: flex; align-items: center; gap: 4px;">
|
| 847 |
+
<span style="font-size: 14px;">💻</span>
|
| 848 |
+
<span>{{ service.resourceLimit.cpu }}m</span>
|
| 849 |
+
</span>
|
| 850 |
+
<span style="display: flex; align-items: center; gap: 4px;">
|
| 851 |
+
<span style="font-size: 14px;">🧠</span>
|
| 852 |
+
<span>{{ service.resourceLimit.memory }}MB</span>
|
| 853 |
+
</span>
|
| 854 |
+
</div>
|
| 855 |
+
</div>
|
| 856 |
+
</div>
|
| 857 |
+
|
| 858 |
+
<!-- 状态和操作按钮 -->
|
| 859 |
+
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
|
| 860 |
+
<span :class="['status', service.status.toLowerCase()]" style="flex-shrink: 0;">
|
| 861 |
+
{{ service.status === 'RUNNING' ? '✅ 运行中' : service.status === 'SUSPENDED' ? '⏸️ 已暂停' : service.status }}
|
| 862 |
+
</span>
|
| 863 |
+
<button v-if="service.status === 'RUNNING'"
|
| 864 |
+
@click="pauseService(account, project, service)"
|
| 865 |
+
style="padding: 6px 12px; background: linear-gradient(135deg, #ff9500 0%, #ff9f0a 100%); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 11px; font-weight: 600; transition: transform 0.15s ease-out;">
|
| 866 |
+
⏸️ 暂停
|
| 867 |
+
</button>
|
| 868 |
+
<button @click="restartService(account, project, service)"
|
| 869 |
+
style="padding: 6px 12px; background: linear-gradient(135deg, #007aff 0%, #5ac8fa 100%); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 11px; font-weight: 600; transition: transform 0.15s ease-out;">
|
| 870 |
+
{{ service.status === 'SUSPENDED' ? '▶️ 启动' : '🔄 重启' }}
|
| 871 |
+
</button>
|
| 872 |
+
<button @click="showServiceLogs(account, project, service)"
|
| 873 |
+
style="padding: 6px 12px; background: linear-gradient(135deg, #af52de 0%, #bf5af2 100%); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 11px; font-weight: 600; transition: transform 0.15s ease-out;">
|
| 874 |
+
📋 日志
|
| 875 |
+
</button>
|
| 876 |
+
</div>
|
| 877 |
+
</div>
|
| 878 |
+
<div v-if="project.services.length === 0" style="color: #999; font-size: 14px; text-align: center; padding: 20px;">
|
| 879 |
+
暂无服务
|
| 880 |
+
</div>
|
| 881 |
+
</div>
|
| 882 |
+
|
| 883 |
+
<!-- 项目域名 -->
|
| 884 |
+
<div v-if="getProjectDomains(project).length > 0" style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.3); display: flex; justify-content: center;">
|
| 885 |
+
<div style="display: flex; flex-direction: column; gap: 6px; max-width: 280px; width: 100%;">
|
| 886 |
+
<a v-for="domainInfo in getProjectDomains(project)"
|
| 887 |
+
:key="domainInfo.domain"
|
| 888 |
+
:href="'https://' + domainInfo.domain"
|
| 889 |
+
target="_blank"
|
| 890 |
+
style="display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; text-decoration: none; border-radius: 16px; font-size: 13px; font-weight: 600; transition: all 0.3s; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);"
|
| 891 |
+
@mouseover="$event.target.style.transform = 'translateY(-2px)'; $event.target.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.5)'"
|
| 892 |
+
@mouseout="$event.target.style.transform = 'translateY(0)'; $event.target.style.boxShadow = '0 2px 8px rgba(102, 126, 234, 0.3)'">
|
| 893 |
+
<div style="display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;">
|
| 894 |
+
<span style="font-size: 16px; flex-shrink: 0;">🌐</span>
|
| 895 |
+
<span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ domainInfo.domain }}</span>
|
| 896 |
+
</div>
|
| 897 |
+
<span v-if="domainInfo.isGenerated" style="background: rgba(255,255,255,0.25); padding: 3px 10px; border-radius: 10px; font-size: 11px; flex-shrink: 0; margin-left: 6px;">自动</span>
|
| 898 |
+
</a>
|
| 899 |
+
</div>
|
| 900 |
+
</div>
|
| 901 |
+
</div>
|
| 902 |
+
</div>
|
| 903 |
+
</div>
|
| 904 |
+
|
| 905 |
+
<div v-if="!loading && accounts.length === 0" class="error">
|
| 906 |
+
未配置账号或无法获取数据。请检查 .env 配置文件。
|
| 907 |
+
</div>
|
| 908 |
+
</div>
|
| 909 |
+
</div>
|
| 910 |
+
|
| 911 |
+
<script>
|
| 912 |
+
const { createApp } = Vue;
|
| 913 |
+
|
| 914 |
+
createApp({
|
| 915 |
+
data() {
|
| 916 |
+
return {
|
| 917 |
+
accounts: [],
|
| 918 |
+
loading: false,
|
| 919 |
+
lastUpdate: '--:--:--',
|
| 920 |
+
showAddModal: false,
|
| 921 |
+
showManageModal: false,
|
| 922 |
+
managedAccounts: [],
|
| 923 |
+
projectCosts: {},
|
| 924 |
+
newAccount: {
|
| 925 |
+
name: '',
|
| 926 |
+
token: '',
|
| 927 |
+
balance: ''
|
| 928 |
+
},
|
| 929 |
+
addingAccount: false,
|
| 930 |
+
addAccountError: '',
|
| 931 |
+
addAccountSuccess: '',
|
| 932 |
+
opacity: 39,
|
| 933 |
+
expandedAccounts: {},
|
| 934 |
+
// 密码验证
|
| 935 |
+
isAuthenticated: false,
|
| 936 |
+
showLoginModal: false,
|
| 937 |
+
showSetPasswordModal: false,
|
| 938 |
+
loginPassword: '',
|
| 939 |
+
loginError: '',
|
| 940 |
+
setPassword: '',
|
| 941 |
+
setPasswordConfirm: '',
|
| 942 |
+
setPasswordError: '',
|
| 943 |
+
// 批量添加
|
| 944 |
+
batchAccounts: '',
|
| 945 |
+
maskedBatchAccounts: '',
|
| 946 |
+
batchAddError: '',
|
| 947 |
+
batchAddSuccess: '',
|
| 948 |
+
// 日志模态框
|
| 949 |
+
showLogsModal: false,
|
| 950 |
+
logsModalTitle: '',
|
| 951 |
+
logsModalInfo: {},
|
| 952 |
+
logsContent: '',
|
| 953 |
+
logsLoading: false
|
| 954 |
+
};
|
| 955 |
+
},
|
| 956 |
+
async mounted() {
|
| 957 |
+
// 检查服务器是否已设置密码
|
| 958 |
+
const hasPasswordResponse = await fetch('/api/check-password');
|
| 959 |
+
const { hasPassword } = await hasPasswordResponse.json();
|
| 960 |
+
|
| 961 |
+
if (!hasPassword) {
|
| 962 |
+
// 首次使用,显示设置密码界面
|
| 963 |
+
this.showSetPasswordModal = true;
|
| 964 |
+
return;
|
| 965 |
+
}
|
| 966 |
+
|
| 967 |
+
// 检查本地是否有保存的密码和时间戳
|
| 968 |
+
const savedPassword = localStorage.getItem('admin_password');
|
| 969 |
+
const savedTime = localStorage.getItem('password_time');
|
| 970 |
+
|
| 971 |
+
if (savedPassword && savedTime) {
|
| 972 |
+
const now = Date.now();
|
| 973 |
+
const elapsed = now - parseInt(savedTime);
|
| 974 |
+
const fourDays = 4 * 24 * 60 * 60 * 1000; // 4天的毫秒数
|
| 975 |
+
|
| 976 |
+
if (elapsed < fourDays) {
|
| 977 |
+
// 4天内,自动登录
|
| 978 |
+
this.loginPassword = savedPassword;
|
| 979 |
+
await this.verifyPassword();
|
| 980 |
+
return;
|
| 981 |
+
}
|
| 982 |
+
}
|
| 983 |
+
|
| 984 |
+
// 需要输入密码
|
| 985 |
+
this.showLoginModal = true;
|
| 986 |
+
},
|
| 987 |
+
watch: {
|
| 988 |
+
opacity(newVal) {
|
| 989 |
+
localStorage.setItem('card_opacity', newVal);
|
| 990 |
+
this.updateOpacity();
|
| 991 |
+
}
|
| 992 |
+
},
|
| 993 |
+
beforeUnmount() {
|
| 994 |
+
// 清理定时器,防止内存泄漏
|
| 995 |
+
if (this.refreshInterval) {
|
| 996 |
+
clearInterval(this.refreshInterval);
|
| 997 |
+
}
|
| 998 |
+
},
|
| 999 |
+
computed: {
|
| 1000 |
+
totalProjects() {
|
| 1001 |
+
let total = 0;
|
| 1002 |
+
for (const acc of this.accounts) {
|
| 1003 |
+
if (acc.projects) total += acc.projects.length;
|
| 1004 |
+
}
|
| 1005 |
+
return total;
|
| 1006 |
+
},
|
| 1007 |
+
totalServices() {
|
| 1008 |
+
let total = 0;
|
| 1009 |
+
for (const acc of this.accounts) {
|
| 1010 |
+
if (acc.projects) {
|
| 1011 |
+
for (const p of acc.projects) {
|
| 1012 |
+
if (p.services) total += p.services.length;
|
| 1013 |
+
}
|
| 1014 |
+
}
|
| 1015 |
+
}
|
| 1016 |
+
return total;
|
| 1017 |
+
},
|
| 1018 |
+
runningServices() {
|
| 1019 |
+
let total = 0;
|
| 1020 |
+
for (const acc of this.accounts) {
|
| 1021 |
+
if (acc.projects) {
|
| 1022 |
+
for (const p of acc.projects) {
|
| 1023 |
+
if (p.services) {
|
| 1024 |
+
for (const s of p.services) {
|
| 1025 |
+
if (s.status === 'RUNNING') total++;
|
| 1026 |
+
}
|
| 1027 |
+
}
|
| 1028 |
+
}
|
| 1029 |
+
}
|
| 1030 |
+
}
|
| 1031 |
+
return total;
|
| 1032 |
+
},
|
| 1033 |
+
totalCost() {
|
| 1034 |
+
let total = 0;
|
| 1035 |
+
for (const acc of this.accounts) {
|
| 1036 |
+
if (acc.projects) {
|
| 1037 |
+
for (const p of acc.projects) {
|
| 1038 |
+
total += p.cost || 0;
|
| 1039 |
+
}
|
| 1040 |
+
}
|
| 1041 |
+
}
|
| 1042 |
+
return total;
|
| 1043 |
+
}
|
| 1044 |
+
},
|
| 1045 |
+
methods: {
|
| 1046 |
+
// 设置密码(首次)
|
| 1047 |
+
async setAdminPassword() {
|
| 1048 |
+
this.setPasswordError = '';
|
| 1049 |
+
|
| 1050 |
+
if (!this.setPassword || this.setPassword.length < 6) {
|
| 1051 |
+
this.setPasswordError = '密码长度至少6位';
|
| 1052 |
+
return;
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
if (this.setPassword !== this.setPasswordConfirm) {
|
| 1056 |
+
this.setPasswordError = '两次输入的密码不一致';
|
| 1057 |
+
return;
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
try {
|
| 1061 |
+
const response = await fetch('/api/set-password', {
|
| 1062 |
+
method: 'POST',
|
| 1063 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1064 |
+
body: JSON.stringify({ password: this.setPassword })
|
| 1065 |
+
});
|
| 1066 |
+
|
| 1067 |
+
const result = await response.json();
|
| 1068 |
+
if (result.success) {
|
| 1069 |
+
// 设置成功,自动登录
|
| 1070 |
+
this.loginPassword = this.setPassword;
|
| 1071 |
+
localStorage.setItem('admin_password', this.setPassword);
|
| 1072 |
+
localStorage.setItem('password_time', Date.now().toString());
|
| 1073 |
+
|
| 1074 |
+
this.showSetPasswordModal = false;
|
| 1075 |
+
this.isAuthenticated = true;
|
| 1076 |
+
|
| 1077 |
+
await this.loadManagedAccounts();
|
| 1078 |
+
this.loadProjectCosts();
|
| 1079 |
+
this.fetchData();
|
| 1080 |
+
|
| 1081 |
+
// 启动自动刷新
|
| 1082 |
+
this.refreshInterval = setInterval(() => this.fetchData(), 30000);
|
| 1083 |
+
|
| 1084 |
+
// 加载透明度设置
|
| 1085 |
+
const savedOpacity = localStorage.getItem('card_opacity');
|
| 1086 |
+
if (savedOpacity) {
|
| 1087 |
+
this.opacity = parseInt(savedOpacity);
|
| 1088 |
+
this.updateOpacity();
|
| 1089 |
+
}
|
| 1090 |
+
} else {
|
| 1091 |
+
this.setPasswordError = result.error || '设置失败';
|
| 1092 |
+
}
|
| 1093 |
+
} catch (error) {
|
| 1094 |
+
this.setPasswordError = '设置失败: ' + error.message;
|
| 1095 |
+
}
|
| 1096 |
+
},
|
| 1097 |
+
// 验证密码
|
| 1098 |
+
async verifyPassword() {
|
| 1099 |
+
this.loginError = '';
|
| 1100 |
+
try {
|
| 1101 |
+
const response = await fetch('/api/verify-password', {
|
| 1102 |
+
method: 'POST',
|
| 1103 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1104 |
+
body: JSON.stringify({ password: this.loginPassword })
|
| 1105 |
+
});
|
| 1106 |
+
|
| 1107 |
+
const result = await response.json();
|
| 1108 |
+
if (result.success) {
|
| 1109 |
+
this.isAuthenticated = true;
|
| 1110 |
+
this.showLoginModal = false;
|
| 1111 |
+
|
| 1112 |
+
// 保存密码和时间戳
|
| 1113 |
+
localStorage.setItem('admin_password', this.loginPassword);
|
| 1114 |
+
localStorage.setItem('password_time', Date.now().toString());
|
| 1115 |
+
|
| 1116 |
+
await this.loadManagedAccounts();
|
| 1117 |
+
this.loadProjectCosts();
|
| 1118 |
+
this.fetchData();
|
| 1119 |
+
|
| 1120 |
+
// 启动自动刷新
|
| 1121 |
+
this.refreshInterval = setInterval(() => this.fetchData(), 30000);
|
| 1122 |
+
|
| 1123 |
+
// 加载透明度设置
|
| 1124 |
+
const savedOpacity = localStorage.getItem('card_opacity');
|
| 1125 |
+
if (savedOpacity) {
|
| 1126 |
+
this.opacity = parseInt(savedOpacity);
|
| 1127 |
+
this.updateOpacity();
|
| 1128 |
+
}
|
| 1129 |
+
} else {
|
| 1130 |
+
this.loginError = '密码错误,请重试';
|
| 1131 |
+
}
|
| 1132 |
+
} catch (error) {
|
| 1133 |
+
this.loginError = '验证失败: ' + error.message;
|
| 1134 |
+
}
|
| 1135 |
+
},
|
| 1136 |
+
// 邮箱打码
|
| 1137 |
+
maskEmail(email) {
|
| 1138 |
+
if (!email || !email.includes('@')) return email;
|
| 1139 |
+
const [local, domain] = email.split('@');
|
| 1140 |
+
if (local.length <= 4) return email;
|
| 1141 |
+
const masked = local.substring(0, 2) + 'x'.repeat(local.length - 4) + local.substring(local.length - 2);
|
| 1142 |
+
return masked + '@' + domain;
|
| 1143 |
+
},
|
| 1144 |
+
// 获取请求头(包含密码)
|
| 1145 |
+
getAuthHeaders() {
|
| 1146 |
+
return {
|
| 1147 |
+
'Content-Type': 'application/json',
|
| 1148 |
+
'x-admin-password': this.loginPassword
|
| 1149 |
+
};
|
| 1150 |
+
},
|
| 1151 |
+
async loadManagedAccounts() {
|
| 1152 |
+
try {
|
| 1153 |
+
// 从服务器加载账号
|
| 1154 |
+
const response = await fetch('/api/server-accounts', {
|
| 1155 |
+
headers: this.getAuthHeaders()
|
| 1156 |
+
});
|
| 1157 |
+
const accounts = await response.json();
|
| 1158 |
+
if (accounts && accounts.length > 0) {
|
| 1159 |
+
this.managedAccounts = accounts;
|
| 1160 |
+
console.log(`📋 从服务器加载 ${accounts.length} 个账号`);
|
| 1161 |
+
}
|
| 1162 |
+
} catch (error) {
|
| 1163 |
+
console.log('⚠️ 从服务器加载账号失败:', error.message);
|
| 1164 |
+
}
|
| 1165 |
+
},
|
| 1166 |
+
async saveManagedAccounts() {
|
| 1167 |
+
try {
|
| 1168 |
+
// 保存到服务器
|
| 1169 |
+
const response = await fetch('/api/server-accounts', {
|
| 1170 |
+
method: 'POST',
|
| 1171 |
+
headers: this.getAuthHeaders(),
|
| 1172 |
+
body: JSON.stringify({ accounts: this.managedAccounts })
|
| 1173 |
+
});
|
| 1174 |
+
const result = await response.json();
|
| 1175 |
+
if (result.success) {
|
| 1176 |
+
console.log('✅ 账号已保存到服务器');
|
| 1177 |
+
}
|
| 1178 |
+
} catch (error) {
|
| 1179 |
+
console.error('❌ 保存账号到服务器失败:', error.message);
|
| 1180 |
+
}
|
| 1181 |
+
},
|
| 1182 |
+
loadProjectCosts() {
|
| 1183 |
+
const saved = localStorage.getItem('zeabur_project_costs');
|
| 1184 |
+
if (saved) {
|
| 1185 |
+
this.projectCosts = JSON.parse(saved);
|
| 1186 |
+
}
|
| 1187 |
+
},
|
| 1188 |
+
|
| 1189 |
+
async fetchData() {
|
| 1190 |
+
this.loading = true;
|
| 1191 |
+
try {
|
| 1192 |
+
// 如果有账号,使用账号
|
| 1193 |
+
if (this.managedAccounts.length > 0) {
|
| 1194 |
+
// 清除账号中的手动余额,让服务器使用 API 真实数据
|
| 1195 |
+
const accountsWithoutManualBalance = this.managedAccounts.map(acc => ({
|
| 1196 |
+
...acc,
|
| 1197 |
+
balance: null // 不发送手动余额
|
| 1198 |
+
}));
|
| 1199 |
+
|
| 1200 |
+
const [accountsRes, projectsRes] = await Promise.all([
|
| 1201 |
+
fetch('/api/temp-accounts', {
|
| 1202 |
+
method: 'POST',
|
| 1203 |
+
headers: this.getAuthHeaders(),
|
| 1204 |
+
body: JSON.stringify({ accounts: accountsWithoutManualBalance })
|
| 1205 |
+
}).then(r => r.json()),
|
| 1206 |
+
fetch('/api/temp-projects', {
|
| 1207 |
+
method: 'POST',
|
| 1208 |
+
headers: this.getAuthHeaders(),
|
| 1209 |
+
body: JSON.stringify({
|
| 1210 |
+
accounts: accountsWithoutManualBalance,
|
| 1211 |
+
projectCosts: {} // 不发送手动费用,让服务器尝试从 API 获取
|
| 1212 |
+
})
|
| 1213 |
+
}).then(r => r.json())
|
| 1214 |
+
]);
|
| 1215 |
+
|
| 1216 |
+
console.log('API 返回的账号数据:', accountsRes);
|
| 1217 |
+
console.log('API 返回的项目数据:', projectsRes);
|
| 1218 |
+
|
| 1219 |
+
this.accounts = accountsRes.map((account, index) => {
|
| 1220 |
+
const projectData = projectsRes[index];
|
| 1221 |
+
console.log(`账号 ${account.name} 余额: ${account.data?.credit} (${account.data?.credit/100} USD)`);
|
| 1222 |
+
return {
|
| 1223 |
+
...account,
|
| 1224 |
+
projects: projectData.projects || []
|
| 1225 |
+
};
|
| 1226 |
+
});
|
| 1227 |
+
} else {
|
| 1228 |
+
// 否则使用服务器配置的账号
|
| 1229 |
+
const [accountsRes, projectsRes] = await Promise.all([
|
| 1230 |
+
fetch('/api/accounts').then(r => r.json()),
|
| 1231 |
+
fetch('/api/projects').then(r => r.json())
|
| 1232 |
+
]);
|
| 1233 |
+
|
| 1234 |
+
this.accounts = accountsRes.map((account, index) => {
|
| 1235 |
+
const projectData = projectsRes[index];
|
| 1236 |
+
return {
|
| 1237 |
+
...account,
|
| 1238 |
+
projects: projectData.projects || []
|
| 1239 |
+
};
|
| 1240 |
+
});
|
| 1241 |
+
}
|
| 1242 |
+
} catch (error) {
|
| 1243 |
+
console.error('获取数据失败:', error);
|
| 1244 |
+
alert('获取数据失败: ' + error.message);
|
| 1245 |
+
} finally {
|
| 1246 |
+
this.loading = false;
|
| 1247 |
+
this.lastUpdate = new Date().toLocaleTimeString('zh-CN');
|
| 1248 |
+
}
|
| 1249 |
+
},
|
| 1250 |
+
getBalanceClass(credit) {
|
| 1251 |
+
const balance = credit / 100;
|
| 1252 |
+
if (balance < 10) return 'critical';
|
| 1253 |
+
if (balance < 50) return 'low';
|
| 1254 |
+
return '';
|
| 1255 |
+
},
|
| 1256 |
+
async batchAddAccounts() {
|
| 1257 |
+
this.batchAddError = '';
|
| 1258 |
+
this.batchAddSuccess = '';
|
| 1259 |
+
|
| 1260 |
+
if (!this.batchAccounts.trim()) {
|
| 1261 |
+
this.batchAddError = '请输入账号信息';
|
| 1262 |
+
return;
|
| 1263 |
+
}
|
| 1264 |
+
|
| 1265 |
+
const lines = this.batchAccounts.trim().split('\n');
|
| 1266 |
+
const accounts = [];
|
| 1267 |
+
|
| 1268 |
+
// 解析每一行
|
| 1269 |
+
for (let i = 0; i < lines.length; i++) {
|
| 1270 |
+
const line = lines[i].trim();
|
| 1271 |
+
if (!line) continue;
|
| 1272 |
+
|
| 1273 |
+
let name = '';
|
| 1274 |
+
let token = '';
|
| 1275 |
+
|
| 1276 |
+
// 尝试匹配括号格式:名称(token) 或 名称(token)
|
| 1277 |
+
const bracketMatch = line.match(/^(.+?)[((](.+?)[))]$/);
|
| 1278 |
+
if (bracketMatch) {
|
| 1279 |
+
name = bracketMatch[1].trim();
|
| 1280 |
+
token = bracketMatch[2].trim();
|
| 1281 |
+
} else if (line.includes(':')) {
|
| 1282 |
+
// 冒号格式:名称:token
|
| 1283 |
+
const parts = line.split(':');
|
| 1284 |
+
name = parts[0].trim();
|
| 1285 |
+
token = parts.slice(1).join(':').trim();
|
| 1286 |
+
} else if (line.includes(':')) {
|
| 1287 |
+
// 中文冒号格式:名称:token
|
| 1288 |
+
const parts = line.split(':');
|
| 1289 |
+
name = parts[0].trim();
|
| 1290 |
+
token = parts.slice(1).join(':').trim();
|
| 1291 |
+
} else {
|
| 1292 |
+
this.batchAddError = `第 ${i + 1} 行格式错误,支持的格式:名称:Token 或 名称:Token 或 名称(Token) 或 名称(Token)`;
|
| 1293 |
+
return;
|
| 1294 |
+
}
|
| 1295 |
+
|
| 1296 |
+
if (!name || !token) {
|
| 1297 |
+
this.batchAddError = `第 ${i + 1} 行:账号名称或 Token 不能为空`;
|
| 1298 |
+
return;
|
| 1299 |
+
}
|
| 1300 |
+
|
| 1301 |
+
accounts.push({ name, token });
|
| 1302 |
+
}
|
| 1303 |
+
|
| 1304 |
+
if (accounts.length === 0) {
|
| 1305 |
+
this.batchAddError = '没有有效的账号信息';
|
| 1306 |
+
return;
|
| 1307 |
+
}
|
| 1308 |
+
|
| 1309 |
+
this.addingAccount = true;
|
| 1310 |
+
let successCount = 0;
|
| 1311 |
+
let failedAccounts = [];
|
| 1312 |
+
|
| 1313 |
+
// 逐个验证并添加
|
| 1314 |
+
for (const account of accounts) {
|
| 1315 |
+
try {
|
| 1316 |
+
const response = await fetch('/api/validate-account', {
|
| 1317 |
+
method: 'POST',
|
| 1318 |
+
headers: this.getAuthHeaders(),
|
| 1319 |
+
body: JSON.stringify({
|
| 1320 |
+
accountName: account.name,
|
| 1321 |
+
apiToken: account.token
|
| 1322 |
+
})
|
| 1323 |
+
});
|
| 1324 |
+
|
| 1325 |
+
const data = await response.json();
|
| 1326 |
+
|
| 1327 |
+
if (response.ok) {
|
| 1328 |
+
// 检查是否已存在
|
| 1329 |
+
const exists = this.managedAccounts.some(acc => acc.name === account.name);
|
| 1330 |
+
if (!exists) {
|
| 1331 |
+
this.managedAccounts.push({
|
| 1332 |
+
name: account.name,
|
| 1333 |
+
token: account.token,
|
| 1334 |
+
email: data.userData.email,
|
| 1335 |
+
username: data.userData.username
|
| 1336 |
+
});
|
| 1337 |
+
successCount++;
|
| 1338 |
+
} else {
|
| 1339 |
+
failedAccounts.push(`${account.name}(已存在)`);
|
| 1340 |
+
}
|
| 1341 |
+
} else {
|
| 1342 |
+
failedAccounts.push(`${account.name}(${data.error || '验证失败'})`);
|
| 1343 |
+
}
|
| 1344 |
+
} catch (error) {
|
| 1345 |
+
failedAccounts.push(`${account.name}(网络错误)`);
|
| 1346 |
+
}
|
| 1347 |
+
}
|
| 1348 |
+
|
| 1349 |
+
this.addingAccount = false;
|
| 1350 |
+
|
| 1351 |
+
if (successCount > 0) {
|
| 1352 |
+
await this.saveManagedAccounts();
|
| 1353 |
+
this.fetchData();
|
| 1354 |
+
}
|
| 1355 |
+
|
| 1356 |
+
// 显示结果
|
| 1357 |
+
if (successCount > 0 && failedAccounts.length === 0) {
|
| 1358 |
+
this.batchAddSuccess = `✅ 成功添加 ${successCount} 个账号`;
|
| 1359 |
+
this.batchAccounts = '';
|
| 1360 |
+
this.maskedBatchAccounts = '';
|
| 1361 |
+
} else if (successCount > 0) {
|
| 1362 |
+
this.batchAddSuccess = `✅ 成功添加 ${successCount} 个账号`;
|
| 1363 |
+
this.batchAddError = `❌ 失败: ${failedAccounts.join(', ')}`;
|
| 1364 |
+
} else {
|
| 1365 |
+
this.batchAddError = `❌ 全部失败: ${failedAccounts.join(', ')}`;
|
| 1366 |
+
}
|
| 1367 |
+
|
| 1368 |
+
// 3秒后清除提示
|
| 1369 |
+
setTimeout(() => {
|
| 1370 |
+
this.batchAddSuccess = '';
|
| 1371 |
+
if (successCount > 0 && failedAccounts.length === 0) {
|
| 1372 |
+
this.batchAddError = '';
|
| 1373 |
+
}
|
| 1374 |
+
}, 3000);
|
| 1375 |
+
},
|
| 1376 |
+
async addAccountToList() {
|
| 1377 |
+
if (!this.newAccount.name || !this.newAccount.token) {
|
| 1378 |
+
this.addAccountError = '请填写账号名称和 API Token';
|
| 1379 |
+
return;
|
| 1380 |
+
}
|
| 1381 |
+
|
| 1382 |
+
this.addingAccount = true;
|
| 1383 |
+
this.addAccountError = '';
|
| 1384 |
+
this.addAccountSuccess = '';
|
| 1385 |
+
|
| 1386 |
+
try {
|
| 1387 |
+
const response = await fetch('/api/validate-account', {
|
| 1388 |
+
method: 'POST',
|
| 1389 |
+
headers: this.getAuthHeaders(),
|
| 1390 |
+
body: JSON.stringify({
|
| 1391 |
+
accountName: this.newAccount.name,
|
| 1392 |
+
apiToken: this.newAccount.token
|
| 1393 |
+
})
|
| 1394 |
+
});
|
| 1395 |
+
|
| 1396 |
+
const data = await response.json();
|
| 1397 |
+
|
| 1398 |
+
if (response.ok) {
|
| 1399 |
+
// 添加到本地列表
|
| 1400 |
+
this.managedAccounts.push({
|
| 1401 |
+
name: this.newAccount.name,
|
| 1402 |
+
token: this.newAccount.token,
|
| 1403 |
+
balance: this.newAccount.balance ? parseFloat(this.newAccount.balance) : null,
|
| 1404 |
+
email: data.userData.email,
|
| 1405 |
+
username: data.userData.username
|
| 1406 |
+
});
|
| 1407 |
+
|
| 1408 |
+
this.saveManagedAccounts();
|
| 1409 |
+
this.addAccountSuccess = `✅ 账号添加成功!用户: ${data.userData.username}`;
|
| 1410 |
+
|
| 1411 |
+
// 清空表单
|
| 1412 |
+
this.newAccount = { name: '', token: '', balance: '' };
|
| 1413 |
+
|
| 1414 |
+
// 刷新数据
|
| 1415 |
+
setTimeout(() => {
|
| 1416 |
+
this.fetchData();
|
| 1417 |
+
this.addAccountSuccess = '';
|
| 1418 |
+
}, 1500);
|
| 1419 |
+
} else {
|
| 1420 |
+
this.addAccountError = data.error || '添加失败';
|
| 1421 |
+
}
|
| 1422 |
+
} catch (error) {
|
| 1423 |
+
this.addAccountError = '网络错误: ' + error.message;
|
| 1424 |
+
} finally {
|
| 1425 |
+
this.addingAccount = false;
|
| 1426 |
+
}
|
| 1427 |
+
},
|
| 1428 |
+
async removeAccount(index) {
|
| 1429 |
+
const accountName = this.managedAccounts[index].name;
|
| 1430 |
+
const password = prompt(`删除账号"${accountName}"需要验证管理员密码:`);
|
| 1431 |
+
|
| 1432 |
+
if (!password) return;
|
| 1433 |
+
|
| 1434 |
+
// 验证密码
|
| 1435 |
+
try {
|
| 1436 |
+
const response = await fetch('/api/verify-password', {
|
| 1437 |
+
method: 'POST',
|
| 1438 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1439 |
+
body: JSON.stringify({ password })
|
| 1440 |
+
});
|
| 1441 |
+
|
| 1442 |
+
const result = await response.json();
|
| 1443 |
+
if (result.success) {
|
| 1444 |
+
this.managedAccounts.splice(index, 1);
|
| 1445 |
+
await this.saveManagedAccounts();
|
| 1446 |
+
this.fetchData();
|
| 1447 |
+
alert('账号已删除');
|
| 1448 |
+
} else {
|
| 1449 |
+
alert('密码错误,删除失败');
|
| 1450 |
+
}
|
| 1451 |
+
} catch (error) {
|
| 1452 |
+
alert('验证失败: ' + error.message);
|
| 1453 |
+
}
|
| 1454 |
+
},
|
| 1455 |
+
closeAddModal() {
|
| 1456 |
+
this.showAddModal = false;
|
| 1457 |
+
this.newAccount = { name: '', token: '', balance: '' };
|
| 1458 |
+
this.addAccountError = '';
|
| 1459 |
+
this.addAccountSuccess = '';
|
| 1460 |
+
},
|
| 1461 |
+
closeManageModal() {
|
| 1462 |
+
this.showManageModal = false;
|
| 1463 |
+
this.newAccount = { name: '', token: '', balance: '' };
|
| 1464 |
+
this.addAccountError = '';
|
| 1465 |
+
this.addAccountSuccess = '';
|
| 1466 |
+
},
|
| 1467 |
+
updateOpacity() {
|
| 1468 |
+
const opacity = this.opacity / 100;
|
| 1469 |
+
const root = document.documentElement;
|
| 1470 |
+
if (!root) return; // 防止 DOM 未加载
|
| 1471 |
+
|
| 1472 |
+
// 设置所有相关的CSS变量
|
| 1473 |
+
root.style.setProperty('--card-opacity', opacity);
|
| 1474 |
+
root.style.setProperty('--service-opacity', Math.min(opacity + 0.05, 1));
|
| 1475 |
+
root.style.setProperty('--blur-amount', `${20 * opacity}px`);
|
| 1476 |
+
root.style.setProperty('--blur-amount-small', `${15 * opacity}px`);
|
| 1477 |
+
root.style.setProperty('--blur-amount-tiny', `${10 * opacity}px`);
|
| 1478 |
+
root.style.setProperty('--saturate-amount', `${100 + 80 * opacity}%`);
|
| 1479 |
+
root.style.setProperty('--shadow-opacity', 0.1 * opacity);
|
| 1480 |
+
root.style.setProperty('--shadow-opacity-light', 0.05 * opacity);
|
| 1481 |
+
root.style.setProperty('--border-opacity', 0.3 * opacity);
|
| 1482 |
+
root.style.setProperty('--border-opacity-light', 0.4 * opacity);
|
| 1483 |
+
root.style.setProperty('--border-opacity-strong', 0.5 * opacity);
|
| 1484 |
+
},
|
| 1485 |
+
clearCache() {
|
| 1486 |
+
if (confirm('确定要清除所有缓存数据吗?这将删除所有本地保存的账号、余额和费用数据。')) {
|
| 1487 |
+
// 清除所有本地数据
|
| 1488 |
+
this.managedAccounts = [];
|
| 1489 |
+
this.projectCosts = {};
|
| 1490 |
+
localStorage.removeItem('zeabur_accounts');
|
| 1491 |
+
localStorage.removeItem('zeabur_project_costs');
|
| 1492 |
+
|
| 1493 |
+
alert('缓存已清除!正在重新获取数据...');
|
| 1494 |
+
this.fetchData();
|
| 1495 |
+
}
|
| 1496 |
+
},
|
| 1497 |
+
toggleAccount(accountName) {
|
| 1498 |
+
this.expandedAccounts[accountName] = !this.expandedAccounts[accountName];
|
| 1499 |
+
},
|
| 1500 |
+
isAccountExpanded(accountName) {
|
| 1501 |
+
return this.expandedAccounts[accountName] !== false;
|
| 1502 |
+
},
|
| 1503 |
+
// 暂停服务
|
| 1504 |
+
async pauseService(account, project, service) {
|
| 1505 |
+
if (!confirm(`确定要暂停服务"${service.name}"吗?`)) return;
|
| 1506 |
+
|
| 1507 |
+
try {
|
| 1508 |
+
// 获取环境 ID
|
| 1509 |
+
const environmentId = project.environments && project.environments[0] ? project.environments[0]._id : null;
|
| 1510 |
+
if (!environmentId) {
|
| 1511 |
+
alert('❌ 无法获取环境 ID,请刷新页面后重试');
|
| 1512 |
+
return;
|
| 1513 |
+
}
|
| 1514 |
+
|
| 1515 |
+
// 获取账号 token
|
| 1516 |
+
const accountData = this.managedAccounts.find(acc => acc.name === account.name);
|
| 1517 |
+
if (!accountData || !accountData.token) {
|
| 1518 |
+
alert('❌ 无法获取账号 token,请重新添加账号');
|
| 1519 |
+
return;
|
| 1520 |
+
}
|
| 1521 |
+
|
| 1522 |
+
const response = await fetch('/api/service/pause', {
|
| 1523 |
+
method: 'POST',
|
| 1524 |
+
headers: this.getAuthHeaders(),
|
| 1525 |
+
body: JSON.stringify({
|
| 1526 |
+
token: accountData.token,
|
| 1527 |
+
serviceId: service._id,
|
| 1528 |
+
environmentId: environmentId
|
| 1529 |
+
})
|
| 1530 |
+
});
|
| 1531 |
+
|
| 1532 |
+
const result = await response.json();
|
| 1533 |
+
if (result.success) {
|
| 1534 |
+
alert('✅ 服务已暂停');
|
| 1535 |
+
this.fetchData();
|
| 1536 |
+
} else {
|
| 1537 |
+
alert('❌ 暂停失败: ' + (result.error || JSON.stringify(result)));
|
| 1538 |
+
}
|
| 1539 |
+
} catch (error) {
|
| 1540 |
+
alert('❌ 操作失败: ' + error.message);
|
| 1541 |
+
}
|
| 1542 |
+
},
|
| 1543 |
+
// 重启服务
|
| 1544 |
+
async restartService(account, project, service) {
|
| 1545 |
+
const action = service.status === 'SUSPENDED' ? '启动' : '重启';
|
| 1546 |
+
if (!confirm(`确定要${action}服务"${service.name}"吗?`)) return;
|
| 1547 |
+
|
| 1548 |
+
try {
|
| 1549 |
+
// 获取环境 ID
|
| 1550 |
+
const environmentId = project.environments && project.environments[0] ? project.environments[0]._id : null;
|
| 1551 |
+
if (!environmentId) {
|
| 1552 |
+
alert('❌ 无法获取环境 ID,请刷新页面后重试');
|
| 1553 |
+
return;
|
| 1554 |
+
}
|
| 1555 |
+
|
| 1556 |
+
// 获取账号 token
|
| 1557 |
+
const accountData = this.managedAccounts.find(acc => acc.name === account.name);
|
| 1558 |
+
if (!accountData || !accountData.token) {
|
| 1559 |
+
alert('❌ 无法获取账号 token,请重新添加账号');
|
| 1560 |
+
return;
|
| 1561 |
+
}
|
| 1562 |
+
|
| 1563 |
+
const response = await fetch('/api/service/restart', {
|
| 1564 |
+
method: 'POST',
|
| 1565 |
+
headers: this.getAuthHeaders(),
|
| 1566 |
+
body: JSON.stringify({
|
| 1567 |
+
token: accountData.token,
|
| 1568 |
+
serviceId: service._id,
|
| 1569 |
+
environmentId: environmentId
|
| 1570 |
+
})
|
| 1571 |
+
});
|
| 1572 |
+
|
| 1573 |
+
const result = await response.json();
|
| 1574 |
+
if (result.success) {
|
| 1575 |
+
alert(`✅ 服务已${action}`);
|
| 1576 |
+
this.fetchData();
|
| 1577 |
+
} else {
|
| 1578 |
+
alert(`❌ ${action}失败: ` + (result.error || JSON.stringify(result)));
|
| 1579 |
+
}
|
| 1580 |
+
} catch (error) {
|
| 1581 |
+
alert('❌ 操作失败: ' + error.message);
|
| 1582 |
+
}
|
| 1583 |
+
},
|
| 1584 |
+
// 查看服务日志
|
| 1585 |
+
async showServiceLogs(account, project, service) {
|
| 1586 |
+
this.logsModalTitle = '服务日志 - ' + service.name;
|
| 1587 |
+
this.logsModalInfo = { project: project.name, account: account.name, count: 0, time: new Date().toLocaleString('zh-CN') };
|
| 1588 |
+
this.logsContent = '';
|
| 1589 |
+
this.logsLoading = true;
|
| 1590 |
+
this.showLogsModal = true;
|
| 1591 |
+
|
| 1592 |
+
try {
|
| 1593 |
+
const environmentId = project.environments && project.environments[0] ? project.environments[0]._id : null;
|
| 1594 |
+
if (!environmentId) { this.logsContent = '❌ 无法获取环境 ID'; this.logsLoading = false; return; }
|
| 1595 |
+
|
| 1596 |
+
const accountData = this.managedAccounts.find(acc => acc.name === account.name);
|
| 1597 |
+
if (!accountData || !accountData.token) { this.logsContent = '❌ 无法获取账号 token'; this.logsLoading = false; return; }
|
| 1598 |
+
|
| 1599 |
+
const response = await fetch('/api/service/logs', {
|
| 1600 |
+
method: 'POST',
|
| 1601 |
+
headers: this.getAuthHeaders(),
|
| 1602 |
+
body: JSON.stringify({ token: accountData.token, serviceId: service._id, environmentId: environmentId, projectId: project._id, limit: 200 })
|
| 1603 |
+
});
|
| 1604 |
+
|
| 1605 |
+
const result = await response.json();
|
| 1606 |
+
if (result.success && result.logs) {
|
| 1607 |
+
this.logsContent = result.logs.map(log => '[' + new Date(log.timestamp).toLocaleString('zh-CN') + '] ' + log.message).join('\n');
|
| 1608 |
+
this.logsModalInfo.count = result.count;
|
| 1609 |
+
} else {
|
| 1610 |
+
this.logsContent = '❌ 获取日志失败: ' + (result.error || '未知错误');
|
| 1611 |
+
}
|
| 1612 |
+
} catch (error) {
|
| 1613 |
+
this.logsContent = '❌ 获取日志失败: ' + error.message;
|
| 1614 |
+
} finally {
|
| 1615 |
+
this.logsLoading = false;
|
| 1616 |
+
}
|
| 1617 |
+
},
|
| 1618 |
+
// 格式化费用显示(小于 $0.01 显示为 $0.01)
|
| 1619 |
+
formatCost(cost) {
|
| 1620 |
+
if (cost > 0 && cost < 0.01) {
|
| 1621 |
+
return '0.01';
|
| 1622 |
+
}
|
| 1623 |
+
return cost.toFixed(2);
|
| 1624 |
+
},
|
| 1625 |
+
// 更新批量添加的打码显示
|
| 1626 |
+
updateBatchDisplay() {
|
| 1627 |
+
if (!this.batchAccounts) {
|
| 1628 |
+
this.maskedBatchAccounts = '';
|
| 1629 |
+
return;
|
| 1630 |
+
}
|
| 1631 |
+
const lines = this.batchAccounts.split('\n');
|
| 1632 |
+
this.maskedBatchAccounts = lines.map(line => {
|
| 1633 |
+
// 尝试匹配括号格式:名称(token) 或 名称(token)
|
| 1634 |
+
const bracketMatch = line.match(/^(.+?)[((](.+?)[))]$/);
|
| 1635 |
+
if (bracketMatch) {
|
| 1636 |
+
const name = bracketMatch[1];
|
| 1637 |
+
const bracket = line.includes('(') ? '(' : '(';
|
| 1638 |
+
const closeBracket = line.includes(')') ? ')' : ')';
|
| 1639 |
+
const maskedToken = bracketMatch[2].replace(/./g, '●');
|
| 1640 |
+
return name + bracket + maskedToken + closeBracket;
|
| 1641 |
+
}
|
| 1642 |
+
|
| 1643 |
+
// 冒号格式
|
| 1644 |
+
let separatorIndex = -1;
|
| 1645 |
+
let separator = '';
|
| 1646 |
+
|
| 1647 |
+
if (line.includes(':')) {
|
| 1648 |
+
separatorIndex = line.indexOf(':');
|
| 1649 |
+
separator = ':';
|
| 1650 |
+
} else if (line.includes(':')) {
|
| 1651 |
+
separatorIndex = line.indexOf(':');
|
| 1652 |
+
separator = ':';
|
| 1653 |
+
}
|
| 1654 |
+
|
| 1655 |
+
if (separatorIndex === -1) return line;
|
| 1656 |
+
|
| 1657 |
+
const name = line.substring(0, separatorIndex);
|
| 1658 |
+
const token = line.substring(separatorIndex + 1);
|
| 1659 |
+
return name + separator + token.replace(/./g, '●');
|
| 1660 |
+
}).join('\n');
|
| 1661 |
+
},
|
| 1662 |
+
// 获取项目的所有域名
|
| 1663 |
+
getProjectDomains(project) {
|
| 1664 |
+
const domains = [];
|
| 1665 |
+
if (project.services) {
|
| 1666 |
+
project.services.forEach(service => {
|
| 1667 |
+
if (service.domains && service.domains.length > 0) {
|
| 1668 |
+
service.domains.forEach(d => {
|
| 1669 |
+
if (d.domain) {
|
| 1670 |
+
domains.push({
|
| 1671 |
+
domain: d.domain,
|
| 1672 |
+
isGenerated: d.isGenerated || false
|
| 1673 |
+
});
|
| 1674 |
+
}
|
| 1675 |
+
});
|
| 1676 |
+
}
|
| 1677 |
+
});
|
| 1678 |
+
}
|
| 1679 |
+
return domains;
|
| 1680 |
+
},
|
| 1681 |
+
// 开始编辑项目名称
|
| 1682 |
+
startEditProjectName(project) {
|
| 1683 |
+
project.isEditing = true;
|
| 1684 |
+
project.editingName = project.name;
|
| 1685 |
+
setTimeout(() => {
|
| 1686 |
+
const inputs = document.querySelectorAll('input[type="text"]');
|
| 1687 |
+
const lastInput = inputs[inputs.length - 1];
|
| 1688 |
+
if (lastInput) lastInput.focus();
|
| 1689 |
+
}, 50);
|
| 1690 |
+
},
|
| 1691 |
+
// 取消编辑项目名称
|
| 1692 |
+
cancelEditProjectName(project) {
|
| 1693 |
+
project.isEditing = false;
|
| 1694 |
+
project.editingName = '';
|
| 1695 |
+
},
|
| 1696 |
+
// 保存项目名称
|
| 1697 |
+
async saveProjectName(account, project) {
|
| 1698 |
+
// 如果不在编辑状态,直接返回(避免 blur 事件重复触发)
|
| 1699 |
+
if (!project.isEditing) {
|
| 1700 |
+
return;
|
| 1701 |
+
}
|
| 1702 |
+
|
| 1703 |
+
if (!project.editingName || project.editingName.trim() === '') {
|
| 1704 |
+
alert('❌ 项目名称不能为空');
|
| 1705 |
+
return;
|
| 1706 |
+
}
|
| 1707 |
+
|
| 1708 |
+
if (project.editingName === project.name) {
|
| 1709 |
+
this.cancelEditProjectName(project);
|
| 1710 |
+
return;
|
| 1711 |
+
}
|
| 1712 |
+
|
| 1713 |
+
try {
|
| 1714 |
+
const accountData = this.managedAccounts.find(acc => acc.name === account.name);
|
| 1715 |
+
if (!accountData || !accountData.token) {
|
| 1716 |
+
alert('❌ 无法获取账号 token,请重新添加账号');
|
| 1717 |
+
return;
|
| 1718 |
+
}
|
| 1719 |
+
|
| 1720 |
+
const response = await fetch('/api/project/rename', {
|
| 1721 |
+
method: 'POST',
|
| 1722 |
+
headers: this.getAuthHeaders(),
|
| 1723 |
+
body: JSON.stringify({
|
| 1724 |
+
token: accountData.token,
|
| 1725 |
+
projectId: project._id,
|
| 1726 |
+
newName: project.editingName.trim()
|
| 1727 |
+
})
|
| 1728 |
+
});
|
| 1729 |
+
|
| 1730 |
+
const result = await response.json();
|
| 1731 |
+
if (result.success) {
|
| 1732 |
+
project.name = project.editingName.trim();
|
| 1733 |
+
this.cancelEditProjectName(project);
|
| 1734 |
+
alert('✅ 项目名称已更新');
|
| 1735 |
+
} else {
|
| 1736 |
+
alert('❌ 更新失败: ' + (result.error || '未知错误'));
|
| 1737 |
+
}
|
| 1738 |
+
} catch (error) {
|
| 1739 |
+
alert('❌ 操作失败: ' + error.message);
|
| 1740 |
+
}
|
| 1741 |
+
}
|
| 1742 |
+
}
|
| 1743 |
+
}).mount('#app');
|
| 1744 |
+
</script>
|
| 1745 |
+
|
| 1746 |
+
|
| 1747 |
+
<!-- 项目来源 -->
|
| 1748 |
+
<div style="text-align: center; padding: 30px 20px;">
|
| 1749 |
+
<div style="display: inline-block; background: rgba(255,255,255,0.95); backdrop-filter: blur(10px); padding: 12px 24px; border-radius: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); border: 1px solid rgba(246,150,198,0.3); font-size: 13px;">
|
| 1750 |
+
<span style="color: #666;">⭐ 项目地址:</span>
|
| 1751 |
+
<a href="https://github.com/jiujiu532/zeabur-monitor" target="_blank" style="color: #f696c6; text-decoration: none; font-weight: 600; margin-left: 5px;">
|
| 1752 |
+
github.com/jiujiu532/zeabur-monitor
|
| 1753 |
+
</a>
|
| 1754 |
+
</div>
|
| 1755 |
+
</div>
|
| 1756 |
+
|
| 1757 |
+
</body>
|
| 1758 |
+
</html>
|
server.js
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require('dotenv').config();
|
| 2 |
+
const express = require('express');
|
| 3 |
+
const cors = require('cors');
|
| 4 |
+
const https = require('https');
|
| 5 |
+
const fs = require('fs');
|
| 6 |
+
const path = require('path');
|
| 7 |
+
|
| 8 |
+
const app = express();
|
| 9 |
+
const PORT = process.env.PORT || 3000;
|
| 10 |
+
|
| 11 |
+
app.use(cors());
|
| 12 |
+
app.use(express.json());
|
| 13 |
+
|
| 14 |
+
// 密码验证中间件
|
| 15 |
+
function requireAuth(req, res, next) {
|
| 16 |
+
const password = req.headers['x-admin-password'];
|
| 17 |
+
const savedPassword = loadAdminPassword();
|
| 18 |
+
|
| 19 |
+
if (!savedPassword) {
|
| 20 |
+
// 如果没有设置密码,允许访问(首次设置)
|
| 21 |
+
next();
|
| 22 |
+
} else if (password === savedPassword) {
|
| 23 |
+
next();
|
| 24 |
+
} else {
|
| 25 |
+
res.status(401).json({ error: '密码错误' });
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
app.use(express.static('public'));
|
| 30 |
+
|
| 31 |
+
// 数据文件路径
|
| 32 |
+
const ACCOUNTS_FILE = path.join(__dirname, 'accounts.json');
|
| 33 |
+
const PASSWORD_FILE = path.join(__dirname, 'password.json');
|
| 34 |
+
|
| 35 |
+
// 读取服务器存储的账号
|
| 36 |
+
function loadServerAccounts() {
|
| 37 |
+
try {
|
| 38 |
+
if (fs.existsSync(ACCOUNTS_FILE)) {
|
| 39 |
+
const data = fs.readFileSync(ACCOUNTS_FILE, 'utf8');
|
| 40 |
+
return JSON.parse(data);
|
| 41 |
+
}
|
| 42 |
+
} catch (e) {
|
| 43 |
+
console.error('❌ 读取账号文件失败:', e.message);
|
| 44 |
+
}
|
| 45 |
+
return [];
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// 保存账号到服务器
|
| 49 |
+
function saveServerAccounts(accounts) {
|
| 50 |
+
try {
|
| 51 |
+
fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2), 'utf8');
|
| 52 |
+
return true;
|
| 53 |
+
} catch (e) {
|
| 54 |
+
console.error('❌ 保存账号文件失败:', e.message);
|
| 55 |
+
return false;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// 读取管理员密码
|
| 60 |
+
function loadAdminPassword() {
|
| 61 |
+
try {
|
| 62 |
+
if (fs.existsSync(PASSWORD_FILE)) {
|
| 63 |
+
const data = fs.readFileSync(PASSWORD_FILE, 'utf8');
|
| 64 |
+
return JSON.parse(data).password;
|
| 65 |
+
}
|
| 66 |
+
} catch (e) {
|
| 67 |
+
console.error('❌ 读取密码文件失败:', e.message);
|
| 68 |
+
}
|
| 69 |
+
return null;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// 保存管理员密码
|
| 73 |
+
function saveAdminPassword(password) {
|
| 74 |
+
try {
|
| 75 |
+
fs.writeFileSync(PASSWORD_FILE, JSON.stringify({ password }, null, 2), 'utf8');
|
| 76 |
+
return true;
|
| 77 |
+
} catch (e) {
|
| 78 |
+
console.error('❌ 保存密码文件失败:', e.message);
|
| 79 |
+
return false;
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Zeabur GraphQL 查询
|
| 84 |
+
async function queryZeabur(token, query) {
|
| 85 |
+
return new Promise((resolve, reject) => {
|
| 86 |
+
const data = JSON.stringify({ query });
|
| 87 |
+
const options = {
|
| 88 |
+
hostname: 'api.zeabur.com',
|
| 89 |
+
path: '/graphql',
|
| 90 |
+
method: 'POST',
|
| 91 |
+
headers: {
|
| 92 |
+
'Authorization': `Bearer ${token}`,
|
| 93 |
+
'Content-Type': 'application/json',
|
| 94 |
+
'Content-Length': data.length
|
| 95 |
+
},
|
| 96 |
+
timeout: 10000
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
const req = https.request(options, (res) => {
|
| 100 |
+
let body = '';
|
| 101 |
+
res.on('data', (chunk) => body += chunk);
|
| 102 |
+
res.on('end', () => {
|
| 103 |
+
try {
|
| 104 |
+
resolve(JSON.parse(body));
|
| 105 |
+
} catch (e) {
|
| 106 |
+
reject(new Error('Invalid JSON response'));
|
| 107 |
+
}
|
| 108 |
+
});
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
req.on('error', reject);
|
| 112 |
+
req.on('timeout', () => {
|
| 113 |
+
req.destroy();
|
| 114 |
+
reject(new Error('Request timeout'));
|
| 115 |
+
});
|
| 116 |
+
req.write(data);
|
| 117 |
+
req.end();
|
| 118 |
+
});
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// 获取用户信息和项目
|
| 122 |
+
async function fetchAccountData(token) {
|
| 123 |
+
// 查询用户信息
|
| 124 |
+
const userQuery = `
|
| 125 |
+
query {
|
| 126 |
+
me {
|
| 127 |
+
_id
|
| 128 |
+
username
|
| 129 |
+
email
|
| 130 |
+
credit
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
`;
|
| 134 |
+
|
| 135 |
+
// 查询项目信息
|
| 136 |
+
const projectsQuery = `
|
| 137 |
+
query {
|
| 138 |
+
projects {
|
| 139 |
+
edges {
|
| 140 |
+
node {
|
| 141 |
+
_id
|
| 142 |
+
name
|
| 143 |
+
region {
|
| 144 |
+
name
|
| 145 |
+
}
|
| 146 |
+
environments {
|
| 147 |
+
_id
|
| 148 |
+
}
|
| 149 |
+
services {
|
| 150 |
+
_id
|
| 151 |
+
name
|
| 152 |
+
status
|
| 153 |
+
template
|
| 154 |
+
resourceLimit {
|
| 155 |
+
cpu
|
| 156 |
+
memory
|
| 157 |
+
}
|
| 158 |
+
domains {
|
| 159 |
+
domain
|
| 160 |
+
isGenerated
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
`;
|
| 168 |
+
|
| 169 |
+
// 查询 AI Hub 余额
|
| 170 |
+
const aihubQuery = `
|
| 171 |
+
query GetAIHubTenant {
|
| 172 |
+
aihubTenant {
|
| 173 |
+
balance
|
| 174 |
+
keys {
|
| 175 |
+
keyID
|
| 176 |
+
alias
|
| 177 |
+
cost
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
`;
|
| 182 |
+
|
| 183 |
+
const [userData, projectsData, aihubData] = await Promise.all([
|
| 184 |
+
queryZeabur(token, userQuery),
|
| 185 |
+
queryZeabur(token, projectsQuery),
|
| 186 |
+
queryZeabur(token, aihubQuery).catch(() => ({ data: { aihubTenant: null } }))
|
| 187 |
+
]);
|
| 188 |
+
|
| 189 |
+
return {
|
| 190 |
+
user: userData.data?.me || {},
|
| 191 |
+
projects: (projectsData.data?.projects?.edges || []).map(edge => edge.node),
|
| 192 |
+
aihub: aihubData.data?.aihubTenant || null
|
| 193 |
+
};
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
// 获取项目用量数据
|
| 197 |
+
async function fetchUsageData(token, userID, projects = []) {
|
| 198 |
+
const now = new Date();
|
| 199 |
+
const year = now.getFullYear();
|
| 200 |
+
const month = now.getMonth() + 1;
|
| 201 |
+
const fromDate = `${year}-${String(month).padStart(2, '0')}-01`;
|
| 202 |
+
// 使用明天的日期确保包含今天的所有数据
|
| 203 |
+
const tomorrow = new Date(now);
|
| 204 |
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
| 205 |
+
const toDate = `${tomorrow.getFullYear()}-${String(tomorrow.getMonth() + 1).padStart(2, '0')}-${String(tomorrow.getDate()).padStart(2, '0')}`;
|
| 206 |
+
|
| 207 |
+
const usageQuery = {
|
| 208 |
+
operationName: 'GetHeaderMonthlyUsage',
|
| 209 |
+
variables: {
|
| 210 |
+
from: fromDate,
|
| 211 |
+
to: toDate,
|
| 212 |
+
groupByEntity: 'PROJECT',
|
| 213 |
+
groupByTime: 'DAY',
|
| 214 |
+
groupByType: 'ALL',
|
| 215 |
+
userID: userID
|
| 216 |
+
},
|
| 217 |
+
query: `query GetHeaderMonthlyUsage($from: String!, $to: String!, $groupByEntity: GroupByEntity, $groupByTime: GroupByTime, $groupByType: GroupByType, $userID: ObjectID!) {
|
| 218 |
+
usages(
|
| 219 |
+
from: $from
|
| 220 |
+
to: $to
|
| 221 |
+
groupByEntity: $groupByEntity
|
| 222 |
+
groupByTime: $groupByTime
|
| 223 |
+
groupByType: $groupByType
|
| 224 |
+
userID: $userID
|
| 225 |
+
) {
|
| 226 |
+
categories
|
| 227 |
+
data {
|
| 228 |
+
id
|
| 229 |
+
name
|
| 230 |
+
groupByEntity
|
| 231 |
+
usageOfEntity
|
| 232 |
+
__typename
|
| 233 |
+
}
|
| 234 |
+
__typename
|
| 235 |
+
}
|
| 236 |
+
}`
|
| 237 |
+
};
|
| 238 |
+
|
| 239 |
+
return new Promise((resolve, reject) => {
|
| 240 |
+
const data = JSON.stringify(usageQuery);
|
| 241 |
+
const options = {
|
| 242 |
+
hostname: 'api.zeabur.com',
|
| 243 |
+
path: '/graphql',
|
| 244 |
+
method: 'POST',
|
| 245 |
+
headers: {
|
| 246 |
+
'Authorization': `Bearer ${token}`,
|
| 247 |
+
'Content-Type': 'application/json',
|
| 248 |
+
'Content-Length': Buffer.byteLength(data)
|
| 249 |
+
},
|
| 250 |
+
timeout: 10000
|
| 251 |
+
};
|
| 252 |
+
|
| 253 |
+
const req = https.request(options, (res) => {
|
| 254 |
+
let body = '';
|
| 255 |
+
res.on('data', (chunk) => body += chunk);
|
| 256 |
+
res.on('end', () => {
|
| 257 |
+
try {
|
| 258 |
+
const result = JSON.parse(body);
|
| 259 |
+
const usages = result.data?.usages?.data || [];
|
| 260 |
+
|
| 261 |
+
// 计算每个项目的总费用
|
| 262 |
+
const projectCosts = {};
|
| 263 |
+
let totalUsage = 0;
|
| 264 |
+
|
| 265 |
+
usages.forEach(project => {
|
| 266 |
+
const projectTotal = project.usageOfEntity.reduce((a, b) => a + b, 0);
|
| 267 |
+
// 单个项目显示:向上取整到 $0.01(与 Zeabur 官方一致)
|
| 268 |
+
const displayCost = projectTotal > 0 ? Math.ceil(projectTotal * 100) / 100 : 0;
|
| 269 |
+
projectCosts[project.id] = displayCost;
|
| 270 |
+
// 总用量计算:使用原始费用(不取整,保证总余额准确)
|
| 271 |
+
totalUsage += projectTotal;
|
| 272 |
+
});
|
| 273 |
+
|
| 274 |
+
resolve({
|
| 275 |
+
projectCosts,
|
| 276 |
+
totalUsage,
|
| 277 |
+
freeQuotaRemaining: 5 - totalUsage, // 免费额度 $5
|
| 278 |
+
freeQuotaLimit: 5
|
| 279 |
+
});
|
| 280 |
+
} catch (e) {
|
| 281 |
+
reject(new Error('Invalid JSON response'));
|
| 282 |
+
}
|
| 283 |
+
});
|
| 284 |
+
});
|
| 285 |
+
|
| 286 |
+
req.on('error', reject);
|
| 287 |
+
req.on('timeout', () => {
|
| 288 |
+
req.destroy();
|
| 289 |
+
reject(new Error('Request timeout'));
|
| 290 |
+
});
|
| 291 |
+
req.write(data);
|
| 292 |
+
req.end();
|
| 293 |
+
});
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
// 临时账号API - 获取账号信息
|
| 297 |
+
app.post('/api/temp-accounts', requireAuth, express.json(), async (req, res) => {
|
| 298 |
+
const { accounts } = req.body;
|
| 299 |
+
|
| 300 |
+
console.log('📥 收到账号请求:', accounts?.length, '个账号');
|
| 301 |
+
|
| 302 |
+
if (!accounts || !Array.isArray(accounts)) {
|
| 303 |
+
return res.status(400).json({ error: '无效的账号列表' });
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
const results = await Promise.all(accounts.map(async (account) => {
|
| 307 |
+
try {
|
| 308 |
+
console.log(`🔍 正在获取账号 [${account.name}] 的数据...`);
|
| 309 |
+
const { user, projects, aihub } = await fetchAccountData(account.token);
|
| 310 |
+
console.log(` API 返回的 credit: ${user.credit}`);
|
| 311 |
+
|
| 312 |
+
// 获取用量数据
|
| 313 |
+
let usageData = { totalUsage: 0, freeQuotaRemaining: 5, freeQuotaLimit: 5 };
|
| 314 |
+
if (user._id) {
|
| 315 |
+
try {
|
| 316 |
+
usageData = await fetchUsageData(account.token, user._id, projects);
|
| 317 |
+
console.log(`💰 [${account.name}] 用量: $${usageData.totalUsage.toFixed(2)}, 剩余: $${usageData.freeQuotaRemaining.toFixed(2)}`);
|
| 318 |
+
} catch (e) {
|
| 319 |
+
console.log(`⚠️ [${account.name}] 获取用量失败:`, e.message);
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// 计算剩余额度并转换为 credit(以分为单位)
|
| 324 |
+
const creditInCents = Math.round(usageData.freeQuotaRemaining * 100);
|
| 325 |
+
|
| 326 |
+
return {
|
| 327 |
+
name: account.name,
|
| 328 |
+
success: true,
|
| 329 |
+
data: {
|
| 330 |
+
...user,
|
| 331 |
+
credit: creditInCents, // 使用计算的剩余额度
|
| 332 |
+
totalUsage: usageData.totalUsage,
|
| 333 |
+
freeQuotaLimit: usageData.freeQuotaLimit
|
| 334 |
+
},
|
| 335 |
+
aihub: aihub
|
| 336 |
+
};
|
| 337 |
+
} catch (error) {
|
| 338 |
+
console.error(`❌ [${account.name}] 错误:`, error.message);
|
| 339 |
+
return {
|
| 340 |
+
name: account.name,
|
| 341 |
+
success: false,
|
| 342 |
+
error: error.message
|
| 343 |
+
};
|
| 344 |
+
}
|
| 345 |
+
}));
|
| 346 |
+
|
| 347 |
+
console.log('📤 返回结果:', results.length, '个账号');
|
| 348 |
+
res.json(results);
|
| 349 |
+
});
|
| 350 |
+
|
| 351 |
+
// 临时账号API - 获取项目信息
|
| 352 |
+
app.post('/api/temp-projects', requireAuth, express.json(), async (req, res) => {
|
| 353 |
+
const { accounts } = req.body;
|
| 354 |
+
|
| 355 |
+
console.log('📥 收到项目请求:', accounts?.length, '个账号');
|
| 356 |
+
|
| 357 |
+
if (!accounts || !Array.isArray(accounts)) {
|
| 358 |
+
return res.status(400).json({ error: '无效的账号列表' });
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
const results = await Promise.all(accounts.map(async (account) => {
|
| 362 |
+
try {
|
| 363 |
+
console.log(`🔍 正在获��账号 [${account.name}] 的项目...`);
|
| 364 |
+
const { user, projects } = await fetchAccountData(account.token);
|
| 365 |
+
|
| 366 |
+
// 获取用量数据
|
| 367 |
+
let projectCosts = {};
|
| 368 |
+
if (user._id) {
|
| 369 |
+
try {
|
| 370 |
+
const usageData = await fetchUsageData(account.token, user._id, projects);
|
| 371 |
+
projectCosts = usageData.projectCosts;
|
| 372 |
+
} catch (e) {
|
| 373 |
+
console.log(`⚠️ [${account.name}] 获取用量失败:`, e.message);
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
console.log(`📦 [${account.name}] 找到 ${projects.length} 个项目`);
|
| 378 |
+
|
| 379 |
+
const projectsWithCost = projects.map(project => {
|
| 380 |
+
const cost = projectCosts[project._id] || 0;
|
| 381 |
+
console.log(` - ${project.name}: $${cost.toFixed(2)}`);
|
| 382 |
+
|
| 383 |
+
return {
|
| 384 |
+
_id: project._id,
|
| 385 |
+
name: project.name,
|
| 386 |
+
region: project.region?.name || 'Unknown',
|
| 387 |
+
environments: project.environments || [],
|
| 388 |
+
services: project.services || [],
|
| 389 |
+
cost: cost,
|
| 390 |
+
hasCostData: cost > 0
|
| 391 |
+
};
|
| 392 |
+
});
|
| 393 |
+
|
| 394 |
+
return {
|
| 395 |
+
name: account.name,
|
| 396 |
+
success: true,
|
| 397 |
+
projects: projectsWithCost
|
| 398 |
+
};
|
| 399 |
+
} catch (error) {
|
| 400 |
+
console.error(`❌ [${account.name}] 错误:`, error.message);
|
| 401 |
+
return {
|
| 402 |
+
name: account.name,
|
| 403 |
+
success: false,
|
| 404 |
+
error: error.message
|
| 405 |
+
};
|
| 406 |
+
}
|
| 407 |
+
}));
|
| 408 |
+
|
| 409 |
+
console.log('📤 返回项目结果');
|
| 410 |
+
res.json(results);
|
| 411 |
+
});
|
| 412 |
+
|
| 413 |
+
// 验证账号
|
| 414 |
+
app.post('/api/validate-account', requireAuth, express.json(), async (req, res) => {
|
| 415 |
+
const { accountName, apiToken } = req.body;
|
| 416 |
+
|
| 417 |
+
if (!accountName || !apiToken) {
|
| 418 |
+
return res.status(400).json({ error: '账号名称和 API Token 不能为空' });
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
try {
|
| 422 |
+
const { user } = await fetchAccountData(apiToken);
|
| 423 |
+
|
| 424 |
+
if (user._id) {
|
| 425 |
+
res.json({
|
| 426 |
+
success: true,
|
| 427 |
+
message: '账号验证成功!',
|
| 428 |
+
userData: user,
|
| 429 |
+
accountName,
|
| 430 |
+
apiToken
|
| 431 |
+
});
|
| 432 |
+
} else {
|
| 433 |
+
res.status(400).json({ error: 'API Token 无效或没有权限' });
|
| 434 |
+
}
|
| 435 |
+
} catch (error) {
|
| 436 |
+
res.status(400).json({ error: 'API Token 验证失败: ' + error.message });
|
| 437 |
+
}
|
| 438 |
+
});
|
| 439 |
+
|
| 440 |
+
// 从环境变量读取预配置的账号
|
| 441 |
+
function getEnvAccounts() {
|
| 442 |
+
const accountsEnv = process.env.ACCOUNTS;
|
| 443 |
+
if (!accountsEnv) return [];
|
| 444 |
+
|
| 445 |
+
try {
|
| 446 |
+
// 格式: "账号1名称:token1,账号2名称:token2"
|
| 447 |
+
return accountsEnv.split(',').map(item => {
|
| 448 |
+
const [name, token] = item.split(':');
|
| 449 |
+
return { name: name.trim(), token: token.trim() };
|
| 450 |
+
}).filter(acc => acc.name && acc.token);
|
| 451 |
+
} catch (e) {
|
| 452 |
+
console.error('❌ 解析环境变量 ACCOUNTS 失败:', e.message);
|
| 453 |
+
return [];
|
| 454 |
+
}
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
// 检查是否已设置密码
|
| 458 |
+
app.get('/api/check-password', (req, res) => {
|
| 459 |
+
const savedPassword = loadAdminPassword();
|
| 460 |
+
res.json({ hasPassword: !!savedPassword });
|
| 461 |
+
});
|
| 462 |
+
|
| 463 |
+
// 设置管理员密码(首次)
|
| 464 |
+
app.post('/api/set-password', (req, res) => {
|
| 465 |
+
const { password } = req.body;
|
| 466 |
+
const savedPassword = loadAdminPassword();
|
| 467 |
+
|
| 468 |
+
if (savedPassword) {
|
| 469 |
+
return res.status(400).json({ error: '密码已设置,无法重复设置' });
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
if (!password || password.length < 6) {
|
| 473 |
+
return res.status(400).json({ error: '密码长度至少6位' });
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
if (saveAdminPassword(password)) {
|
| 477 |
+
console.log('✅ 管理员密码已设置');
|
| 478 |
+
res.json({ success: true });
|
| 479 |
+
} else {
|
| 480 |
+
res.status(500).json({ error: '保存密码失败' });
|
| 481 |
+
}
|
| 482 |
+
});
|
| 483 |
+
|
| 484 |
+
// 验证密码
|
| 485 |
+
app.post('/api/verify-password', (req, res) => {
|
| 486 |
+
const { password } = req.body;
|
| 487 |
+
const savedPassword = loadAdminPassword();
|
| 488 |
+
|
| 489 |
+
if (!savedPassword) {
|
| 490 |
+
return res.status(400).json({ success: false, error: '请先设置密码' });
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
if (password === savedPassword) {
|
| 494 |
+
res.json({ success: true });
|
| 495 |
+
} else {
|
| 496 |
+
res.status(401).json({ success: false, error: '密码错误' });
|
| 497 |
+
}
|
| 498 |
+
});
|
| 499 |
+
|
| 500 |
+
// 获取所有账号(服务器存储 + 环境变量)
|
| 501 |
+
app.get('/api/server-accounts', requireAuth, async (req, res) => {
|
| 502 |
+
const serverAccounts = loadServerAccounts();
|
| 503 |
+
const envAccounts = getEnvAccounts();
|
| 504 |
+
|
| 505 |
+
// 合并账号,环境变量账号优先
|
| 506 |
+
const allAccounts = [...envAccounts, ...serverAccounts];
|
| 507 |
+
console.log(`📋 返回 ${allAccounts.length} 个账号 (环境变量: ${envAccounts.length}, 服务器: ${serverAccounts.length})`);
|
| 508 |
+
res.json(allAccounts);
|
| 509 |
+
});
|
| 510 |
+
|
| 511 |
+
// 保存账号到服务器
|
| 512 |
+
app.post('/api/server-accounts', requireAuth, async (req, res) => {
|
| 513 |
+
const { accounts } = req.body;
|
| 514 |
+
|
| 515 |
+
if (!accounts || !Array.isArray(accounts)) {
|
| 516 |
+
return res.status(400).json({ error: '无效的账号列表' });
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
if (saveServerAccounts(accounts)) {
|
| 520 |
+
console.log(`✅ 保存 ${accounts.length} 个账号到服务器`);
|
| 521 |
+
res.json({ success: true, message: '账号已保存到服务器' });
|
| 522 |
+
} else {
|
| 523 |
+
res.status(500).json({ error: '保存失败' });
|
| 524 |
+
}
|
| 525 |
+
});
|
| 526 |
+
|
| 527 |
+
// 删除服务器账号
|
| 528 |
+
app.delete('/api/server-accounts/:index', requireAuth, async (req, res) => {
|
| 529 |
+
const index = parseInt(req.params.index);
|
| 530 |
+
const accounts = loadServerAccounts();
|
| 531 |
+
|
| 532 |
+
if (index >= 0 && index < accounts.length) {
|
| 533 |
+
const removed = accounts.splice(index, 1);
|
| 534 |
+
if (saveServerAccounts(accounts)) {
|
| 535 |
+
console.log(`🗑️ 删除账号: ${removed[0].name}`);
|
| 536 |
+
res.json({ success: true, message: '账号已删除' });
|
| 537 |
+
} else {
|
| 538 |
+
res.status(500).json({ error: '删除失败' });
|
| 539 |
+
}
|
| 540 |
+
} else {
|
| 541 |
+
res.status(404).json({ error: '账号不存在' });
|
| 542 |
+
}
|
| 543 |
+
});
|
| 544 |
+
|
| 545 |
+
// 服务器配置的账号API(兼容旧版本)
|
| 546 |
+
app.get('/api/accounts', async (req, res) => {
|
| 547 |
+
res.json([]);
|
| 548 |
+
});
|
| 549 |
+
|
| 550 |
+
app.get('/api/projects', async (req, res) => {
|
| 551 |
+
res.json([]);
|
| 552 |
+
});
|
| 553 |
+
|
| 554 |
+
// 暂停服务
|
| 555 |
+
app.post('/api/service/pause', requireAuth, async (req, res) => {
|
| 556 |
+
const { token, serviceId, environmentId } = req.body;
|
| 557 |
+
|
| 558 |
+
if (!token || !serviceId || !environmentId) {
|
| 559 |
+
return res.status(400).json({ error: '缺少必要参数' });
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
try {
|
| 563 |
+
const mutation = `mutation { suspendService(serviceID: "${serviceId}", environmentID: "${environmentId}") }`;
|
| 564 |
+
const result = await queryZeabur(token, mutation);
|
| 565 |
+
|
| 566 |
+
if (result.data?.suspendService) {
|
| 567 |
+
res.json({ success: true, message: '服务已暂停' });
|
| 568 |
+
} else {
|
| 569 |
+
res.status(400).json({ error: '暂停失败', details: result });
|
| 570 |
+
}
|
| 571 |
+
} catch (error) {
|
| 572 |
+
res.status(500).json({ error: '暂停服务失败: ' + error.message });
|
| 573 |
+
}
|
| 574 |
+
});
|
| 575 |
+
|
| 576 |
+
// 重启服务
|
| 577 |
+
app.post('/api/service/restart', requireAuth, async (req, res) => {
|
| 578 |
+
const { token, serviceId, environmentId } = req.body;
|
| 579 |
+
|
| 580 |
+
if (!token || !serviceId || !environmentId) {
|
| 581 |
+
return res.status(400).json({ error: '缺少必要参数' });
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
try {
|
| 585 |
+
const mutation = `mutation { restartService(serviceID: "${serviceId}", environmentID: "${environmentId}") }`;
|
| 586 |
+
const result = await queryZeabur(token, mutation);
|
| 587 |
+
|
| 588 |
+
if (result.data?.restartService) {
|
| 589 |
+
res.json({ success: true, message: '服务已重启' });
|
| 590 |
+
} else {
|
| 591 |
+
res.status(400).json({ error: '重启失败', details: result });
|
| 592 |
+
}
|
| 593 |
+
} catch (error) {
|
| 594 |
+
res.status(500).json({ error: '重启服务失败: ' + error.message });
|
| 595 |
+
}
|
| 596 |
+
});
|
| 597 |
+
|
| 598 |
+
// 获取服务日志
|
| 599 |
+
app.post('/api/service/logs', requireAuth, express.json(), async (req, res) => {
|
| 600 |
+
const { token, serviceId, environmentId, projectId, limit = 200 } = req.body;
|
| 601 |
+
|
| 602 |
+
if (!token || !serviceId || !environmentId || !projectId) {
|
| 603 |
+
return res.status(400).json({ error: '缺少必要参数' });
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
try {
|
| 607 |
+
const query = `
|
| 608 |
+
query {
|
| 609 |
+
runtimeLogs(
|
| 610 |
+
projectID: "${projectId}"
|
| 611 |
+
serviceID: "${serviceId}"
|
| 612 |
+
environmentID: "${environmentId}"
|
| 613 |
+
) {
|
| 614 |
+
message
|
| 615 |
+
timestamp
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
`;
|
| 619 |
+
|
| 620 |
+
const result = await queryZeabur(token, query);
|
| 621 |
+
|
| 622 |
+
if (result.data?.runtimeLogs) {
|
| 623 |
+
// 按时间戳排序,最新的在最后
|
| 624 |
+
const sortedLogs = result.data.runtimeLogs.sort((a, b) => {
|
| 625 |
+
return new Date(a.timestamp) - new Date(b.timestamp);
|
| 626 |
+
});
|
| 627 |
+
|
| 628 |
+
// 获取最后 N 条日志
|
| 629 |
+
const logs = sortedLogs.slice(-limit);
|
| 630 |
+
|
| 631 |
+
res.json({
|
| 632 |
+
success: true,
|
| 633 |
+
logs,
|
| 634 |
+
count: logs.length,
|
| 635 |
+
totalCount: result.data.runtimeLogs.length
|
| 636 |
+
});
|
| 637 |
+
} else {
|
| 638 |
+
res.status(400).json({ error: '获取日志失败', details: result });
|
| 639 |
+
}
|
| 640 |
+
} catch (error) {
|
| 641 |
+
res.status(500).json({ error: '获取日志失败: ' + error.message });
|
| 642 |
+
}
|
| 643 |
+
});
|
| 644 |
+
|
| 645 |
+
// 重命名项目
|
| 646 |
+
app.post('/api/project/rename', requireAuth, async (req, res) => {
|
| 647 |
+
const { token, projectId, newName } = req.body;
|
| 648 |
+
|
| 649 |
+
console.log(`📝 收到重命名请求: projectId=${projectId}, newName=${newName}`);
|
| 650 |
+
|
| 651 |
+
if (!token || !projectId || !newName) {
|
| 652 |
+
return res.status(400).json({ error: '缺少必要参数' });
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
try {
|
| 656 |
+
const mutation = `mutation { renameProject(_id: "${projectId}", name: "${newName}") }`;
|
| 657 |
+
console.log(`🔍 发送 GraphQL mutation:`, mutation);
|
| 658 |
+
|
| 659 |
+
const result = await queryZeabur(token, mutation);
|
| 660 |
+
console.log(`📥 API 响应:`, JSON.stringify(result, null, 2));
|
| 661 |
+
|
| 662 |
+
if (result.data?.renameProject) {
|
| 663 |
+
console.log(`✅ 项目已重命名: ${newName}`);
|
| 664 |
+
res.json({ success: true, message: '项目已重命名' });
|
| 665 |
+
} else {
|
| 666 |
+
console.log(`❌ 重命名失败:`, result);
|
| 667 |
+
res.status(400).json({ error: '重命名失败', details: result });
|
| 668 |
+
}
|
| 669 |
+
} catch (error) {
|
| 670 |
+
console.log(`❌ 异常:`, error);
|
| 671 |
+
res.status(500).json({ error: '重命名项目失败: ' + error.message });
|
| 672 |
+
}
|
| 673 |
+
});
|
| 674 |
+
|
| 675 |
+
app.listen(PORT, () => {
|
| 676 |
+
console.log(`✨ Zeabur Monitor 运行在 http://localhost:${PORT}`);
|
| 677 |
+
|
| 678 |
+
const envAccounts = getEnvAccounts();
|
| 679 |
+
const serverAccounts = loadServerAccounts();
|
| 680 |
+
const totalAccounts = envAccounts.length + serverAccounts.length;
|
| 681 |
+
|
| 682 |
+
if (totalAccounts > 0) {
|
| 683 |
+
console.log(`📋 已加载 ${totalAccounts} 个账号`);
|
| 684 |
+
if (envAccounts.length > 0) {
|
| 685 |
+
console.log(` 环境变量: ${envAccounts.length} 个`);
|
| 686 |
+
envAccounts.forEach(acc => console.log(` - ${acc.name}`));
|
| 687 |
+
}
|
| 688 |
+
if (serverAccounts.length > 0) {
|
| 689 |
+
console.log(` 服务器存储: ${serverAccounts.length} 个`);
|
| 690 |
+
serverAccounts.forEach(acc => console.log(` - ${acc.name}`));
|
| 691 |
+
}
|
| 692 |
+
} else {
|
| 693 |
+
console.log(`📊 准备就绪,等待添加账号...`);
|
| 694 |
+
}
|
| 695 |
+
});
|
zbpack.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"build_command": "npm install",
|
| 3 |
+
"start_command": "npm start"
|
| 4 |
+
}
|