update
Browse files- app.py +15 -0
- memory-bank/activeContext.md +7 -7
- memory-bank/progress.md +11 -5
- routes/admin.py +34 -0
- static/admin.html +1 -192
- static/admin/pictures.html +74 -0
- static/admin/users.html +131 -0
- static/components/admin_navbar.html +32 -0
- static/js/admin.js +99 -165
- static/js/admin/pictures.js +153 -0
- static/js/admin/users.js +179 -0
- static/style.css +29 -0
app.py
CHANGED
|
@@ -65,3 +65,18 @@ async def read_signup():
|
|
| 65 |
async def read_admin():
|
| 66 |
with open("static/admin.html", "r") as f:
|
| 67 |
return f.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
async def read_admin():
|
| 66 |
with open("static/admin.html", "r") as f:
|
| 67 |
return f.read()
|
| 68 |
+
|
| 69 |
+
@app.get("/admin/users", response_class=HTMLResponse)
|
| 70 |
+
async def read_admin_users():
|
| 71 |
+
with open("static/admin/users.html", "r") as f:
|
| 72 |
+
return f.read()
|
| 73 |
+
|
| 74 |
+
@app.get("/static/components/admin_navbar.html", response_class=HTMLResponse)
|
| 75 |
+
async def get_admin_navbar():
|
| 76 |
+
with open("static/components/admin_navbar.html", "r") as f:
|
| 77 |
+
return f.read()
|
| 78 |
+
|
| 79 |
+
@app.get("/admin/pictures", response_class=HTMLResponse)
|
| 80 |
+
async def read_admin_pictures():
|
| 81 |
+
with open("static/admin/pictures.html", "r") as f:
|
| 82 |
+
return f.read()
|
memory-bank/activeContext.md
CHANGED
|
@@ -34,6 +34,10 @@
|
|
| 34 |
* 在 `core/models.py` 中定义了 `ResetPasswordWithCodeRequest` 模型。
|
| 35 |
* 修复了 `routes/auth.py` 中重复的 `signup` 路由定义。
|
| 36 |
* 修改了 `core/utils.py` 中的 `store_verification_code` 和 `verify_stored_code` 函数,使其支持 `prefix` 参数,解决了后端报错。
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
## 下一步计划
|
| 39 |
1. 在 Supabase 控制台中创建 `airsltd` 项目,并设计 `sp_proxies` 表和 `sp_user_api_keys` 表(表名前缀为 `sp_`)。
|
|
@@ -41,22 +45,18 @@
|
|
| 41 |
3. 在 Supabase 控制台中启用邮件认证,并根据需要配置其他认证提供商。
|
| 42 |
4. 测试用户注册、登录、API Key 生成、代理数据获取、密码修改和忘记密码功能。
|
| 43 |
5. 根据实际需求,扩展前端界面和后端 API,实现更多功能。
|
| 44 |
-
|
| 45 |
-
## 下一步计划
|
| 46 |
-
1. 在 Supabase 控制台中创建 `airsltd` 项目,并设计 `sp_proxies` 表和 `sp_user_api_keys` 表(表名前缀为 `sp_`)。
|
| 47 |
-
2. 确保 `.env` 文件中的 Supabase 凭证已正确配置。
|
| 48 |
-
3. 在 Supabase 控制台中启用邮件认证,并根据需要配置其他认证提供商。
|
| 49 |
-
4. 测试用户注册、登录、API Key 生成、代理数据获取、密码修改和忘记密码功能。
|
| 50 |
-
5. 根据实际需求,扩展前端界面和后端 API,实现更多功能。
|
| 51 |
|
| 52 |
## 活跃的决策和考虑事项
|
| 53 |
* **前后端一体**: 项目被确认为前后端一体的 Docker 化应用。
|
| 54 |
* **部署环境**: Hugging Face Spaces 通过 `Dockerfile` 自动生成镜像,无需手动 `docker build`。
|
| 55 |
* **本地运行**: 使用 `conda activate airs && uvicorn app:app --host 0.0.0.0 --port 7860 --reload` 命令在本地运行。
|
| 56 |
* **Supabase 凭证**: 需通过环境变量安全配置。
|
|
|
|
| 57 |
|
| 58 |
## 学习和项目洞察
|
| 59 |
* 理解了 Hugging Face Spaces 的 Docker 部署机制。
|
| 60 |
* 确认了 `spwebsite` 项目的集成方式和运行环境。
|
| 61 |
* 调试了 Supabase 邮件验证问题,并确认了后端 API 的功能。
|
| 62 |
* 成功将认证逻辑分离到独立的登录页面。
|
|
|
|
|
|
| 34 |
* 在 `core/models.py` 中定义了 `ResetPasswordWithCodeRequest` 模型。
|
| 35 |
* 修复了 `routes/auth.py` 中重复的 `signup` 路由定义。
|
| 36 |
* 修改了 `core/utils.py` 中的 `store_verification_code` 和 `verify_stored_code` 函数,使其支持 `prefix` 参数,解决了后端报错。
|
| 37 |
+
13. **管理员功能**:
|
| 38 |
+
* 在 `routes/admin.py` 中实现了图片上传、用户列表获取、用户详情获取、用户更新(包括 `is_admin` 字段)和用户删除功能。
|
| 39 |
+
* 在 `static/index.html` 中调整了导航栏,将“后台管理”链接(由 `v-if="isAdmin"` 控制)移动到“设置”和“关于”之间,并删除了重复的“设置”链接和语言选项。
|
| 40 |
+
* 在 `core/dependencies.py` 中确保 `get_current_user_from_token` 和 `get_current_user_from_api_key` 正确从 Supabase 获取 `is_admin` 字段。
|
| 41 |
|
| 42 |
## 下一步计划
|
| 43 |
1. 在 Supabase 控制台中创建 `airsltd` 项目,并设计 `sp_proxies` 表和 `sp_user_api_keys` 表(表名前缀为 `sp_`)。
|
|
|
|
| 45 |
3. 在 Supabase 控制台中启用邮件认证,并根据需要配置其他认证提供商。
|
| 46 |
4. 测试用户注册、登录、API Key 生成、代理数据获取、密码修改和忘记密码功能。
|
| 47 |
5. 根据实际需求,扩展前端界面和后端 API,实现更多功能。
|
| 48 |
+
6. 确保管理员用户在 Supabase 数据库中的 `is_admin` 字段设置为 `True`,以便前端正确显示“后台管理”链接。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
## 活跃的决策和考虑事项
|
| 51 |
* **前后端一体**: 项目被确认为前后端一体的 Docker 化应用。
|
| 52 |
* **部署环境**: Hugging Face Spaces 通过 `Dockerfile` 自动生成镜像,无需手动 `docker build`。
|
| 53 |
* **本地运行**: 使用 `conda activate airs && uvicorn app:app --host 0.0.0.0 --port 7860 --reload` 命令在本地运行。
|
| 54 |
* **Supabase 凭证**: 需通过环境变量安全配置。
|
| 55 |
+
* **管理员链接显示**: 前端 `isAdmin` 状态依赖于后端 `/api/user/me` 接口返回的 `is_admin` 字段,该字段从 Supabase 数据库获取。
|
| 56 |
|
| 57 |
## 学习和项目洞察
|
| 58 |
* 理解了 Hugging Face Spaces 的 Docker 部署机制。
|
| 59 |
* 确认了 `spwebsite` 项目的集成方式和运行环境。
|
| 60 |
* 调试了 Supabase 邮件验证问题,并确认了后端 API 的功能。
|
| 61 |
* 成功将认证逻辑分离到独立的登录页面。
|
| 62 |
+
* 实现了管理员的用户和图片管理功能,并解决了前端菜单显示问题。
|
memory-bank/progress.md
CHANGED
|
@@ -22,10 +22,15 @@
|
|
| 22 |
* 在 `core/models.py` 中定义了 `ResetPasswordWithCodeRequest` 模型。
|
| 23 |
* 修复了 `routes/auth.py` 中重复的 `signup` 路由定义。
|
| 24 |
* 修改了 `core/utils.py` 中的 `store_verification_code` 和 `verify_stored_code` 函数,使其支持 `prefix` 参数,解决了后端报错。
|
| 25 |
-
4. **
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
## 剩余的工作
|
| 31 |
1. **Supabase 数据库设置**: 在 Supabase 控制台中创建 `airsltd` 项目,并设计 `sp_proxies` 表和 `sp_user_api_keys` 表(表名前缀为 `sp_`)。
|
|
@@ -33,9 +38,10 @@
|
|
| 33 |
3. **功能扩展**: 根据实际需求,进一步开发前端界面和后端 API,实现更多功能。
|
| 34 |
4. **Supabase 认证配置**: 在 Supabase 控制台中启用邮件认证,并根据需要配置其他认证提供商。
|
| 35 |
5. **测试**: 测试用户注册、登录、API Key 生成、代理数据获取、密码修改和忘记密码功能。
|
|
|
|
| 36 |
|
| 37 |
## 当前状态
|
| 38 |
-
项目已完成前后端一体框架的搭建,前端和后端的基本集成已完成,并实现了用户注册、登录、API Key 申请、获取用户电子邮件、修改密码
|
| 39 |
|
| 40 |
## 已知问题
|
| 41 |
* `static/app.js` 中的 `fetchProxies` 方法目前是注释掉的,需要手动启用并根据 Supabase 实际数据结构进行调整。
|
|
|
|
| 22 |
* 在 `core/models.py` 中定义了 `ResetPasswordWithCodeRequest` 模型。
|
| 23 |
* 修复了 `routes/auth.py` 中重复的 `signup` 路由定义。
|
| 24 |
* 修改了 `core/utils.py` 中的 `store_verification_code` 和 `verify_stored_code` 函数,使其支持 `prefix` 参数,解决了后端报错。
|
| 25 |
+
4. **管理员功能**:
|
| 26 |
+
* 实现了管理员图片上传功能(`/api/admin/upload-image`)。
|
| 27 |
+
* 实现了管理员用户列表的查看、搜索、更新和删除功能(`/api/admin/users`)。
|
| 28 |
+
* 前端 `static/index.html` 中添加了 `v-if="isAdmin"` 控制的“后台管理”链接,并将其移动到“设置”和“关于”之间。
|
| 29 |
+
* 删除了 `static/index.html` 中重复的“设置”链接和语言选项。
|
| 30 |
+
5. **文档更新**: 更新了 `README.md`,反映了项目的新功能、技术栈和正确的本地运行指南。
|
| 31 |
+
6. **Memory Bank 更新**: 更新了所有核心 Memory Bank 文件以反映项目最新状态。
|
| 32 |
+
7. **Supabase 解决方案文件更新**: 更新了 `../solutions/supabase_solution.md`。
|
| 33 |
+
8. **`.env` 文件生成**: 生成了包含 Supabase 凭证占位符的 `.env` 文件。
|
| 34 |
|
| 35 |
## 剩余的工作
|
| 36 |
1. **Supabase 数据库设置**: 在 Supabase 控制台中创建 `airsltd` 项目,并设计 `sp_proxies` 表和 `sp_user_api_keys` 表(表名前缀为 `sp_`)。
|
|
|
|
| 38 |
3. **功能扩展**: 根据实际需求,进一步开发前端界面和后端 API,实现更多功能。
|
| 39 |
4. **Supabase 认证配置**: 在 Supabase 控制台中启用邮件认证,并根据需要配置其他认证提供商。
|
| 40 |
5. **测试**: 测试用户注册、登录、API Key 生成、代理数据获取、密码修改和忘记密码功能。
|
| 41 |
+
6. **管理员用户设置**: 确保管理员用户在 Supabase 数据库中的 `is_admin` 字段设置为 `True`,以便前端正确显示“后台管理”链接。
|
| 42 |
|
| 43 |
## 当前状态
|
| 44 |
+
项目已完成前后端一体框架的搭建,前端和后端的基本集成已完成,并实现了用户注册、登录、API Key 申请、获取用户电子邮件、修改密码、忘记密码以及管理员的用户和图片管理功能。认证逻辑已分离到独立的登录页面。项目现在可以在本地通过 `conda` 和 `uvicorn` 命令运行,并准备好与 Supabase 数据库进行实际的数据交互。
|
| 45 |
|
| 46 |
## 已知问题
|
| 47 |
* `static/app.js` 中的 `fetchProxies` 方法目前是注释掉的,需要手动启用并根据 Supabase 实际数据结构进行调整。
|
routes/admin.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
from fastapi import APIRouter, File, UploadFile, HTTPException, status, Depends, Query
|
|
|
|
| 2 |
from typing import List, Optional
|
| 3 |
import os
|
|
|
|
| 4 |
from supabase import create_client, Client
|
| 5 |
from gotrue.errors import AuthApiError
|
| 6 |
from pydantic import BaseModel # Import BaseModel for UserListResponse
|
|
@@ -30,6 +32,38 @@ async def upload_image(file: UploadFile = File(...), current_user: User = Depend
|
|
| 30 |
except Exception as e:
|
| 31 |
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"图片上传失败: {e}")
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
class UserListResponse(BaseModel):
|
| 34 |
users: List[AdminUser]
|
| 35 |
total_count: int
|
|
|
|
| 1 |
from fastapi import APIRouter, File, UploadFile, HTTPException, status, Depends, Query
|
| 2 |
+
from fastapi.responses import FileResponse # Import FileResponse
|
| 3 |
from typing import List, Optional
|
| 4 |
import os
|
| 5 |
+
from pathlib import Path # Import Path
|
| 6 |
from supabase import create_client, Client
|
| 7 |
from gotrue.errors import AuthApiError
|
| 8 |
from pydantic import BaseModel # Import BaseModel for UserListResponse
|
|
|
|
| 32 |
except Exception as e:
|
| 33 |
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"图片上传失败: {e}")
|
| 34 |
|
| 35 |
+
@router.get("/images")
|
| 36 |
+
async def list_images(current_user: User = Depends(get_current_admin_user)):
|
| 37 |
+
"""
|
| 38 |
+
列出 /static/images 目录下的所有图片。
|
| 39 |
+
"""
|
| 40 |
+
if not os.path.exists(UPLOAD_DIRECTORY):
|
| 41 |
+
return []
|
| 42 |
+
|
| 43 |
+
image_files = []
|
| 44 |
+
for filename in os.listdir(UPLOAD_DIRECTORY):
|
| 45 |
+
if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.svg')):
|
| 46 |
+
image_files.append({
|
| 47 |
+
"filename": filename,
|
| 48 |
+
"path": f"/{UPLOAD_DIRECTORY}/{filename}"
|
| 49 |
+
})
|
| 50 |
+
return image_files
|
| 51 |
+
|
| 52 |
+
@router.delete("/images/{filename}", status_code=status.HTTP_204_NO_CONTENT)
|
| 53 |
+
async def delete_image(filename: str, current_user: User = Depends(get_current_admin_user)):
|
| 54 |
+
"""
|
| 55 |
+
删除 /static/images 目录下的指定图片。
|
| 56 |
+
"""
|
| 57 |
+
file_path = Path(UPLOAD_DIRECTORY) / filename
|
| 58 |
+
if not file_path.is_file():
|
| 59 |
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="图片未找到")
|
| 60 |
+
|
| 61 |
+
try:
|
| 62 |
+
os.remove(file_path)
|
| 63 |
+
return # No content for 204
|
| 64 |
+
except Exception as e:
|
| 65 |
+
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"图片删除失败: {e}")
|
| 66 |
+
|
| 67 |
class UserListResponse(BaseModel):
|
| 68 |
users: List[AdminUser]
|
| 69 |
total_count: int
|
static/admin.html
CHANGED
|
@@ -13,202 +13,11 @@
|
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
<div id="app">
|
| 16 |
-
<
|
| 17 |
-
<div class="container-fluid">
|
| 18 |
-
<a class="navbar-brand d-flex align-items-center" href="/">
|
| 19 |
-
<img src="/static/images/ShareAPI.png" alt="API Router Logo" width="30" height="30" class="d-inline-block align-text-top me-2">
|
| 20 |
-
<span class="fw-bold fs-5">API Router</span>
|
| 21 |
-
</a>
|
| 22 |
-
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
| 23 |
-
<span class="navbar-toggler-icon"></span>
|
| 24 |
-
</button>
|
| 25 |
-
<div class="collapse navbar-collapse" id="navbarNav">
|
| 26 |
-
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
| 27 |
-
<li class="nav-item">
|
| 28 |
-
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 29 |
-
<i class="fas fa-comment-dots me-1"></i> 聊天
|
| 30 |
-
</a>
|
| 31 |
-
</li>
|
| 32 |
-
<li class="nav-item">
|
| 33 |
-
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 34 |
-
<i class="fas fa-key me-1"></i> 令牌
|
| 35 |
-
</a>
|
| 36 |
-
</li>
|
| 37 |
-
<li class="nav-item">
|
| 38 |
-
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 39 |
-
<i class="fas fa-shopping-cart me-1"></i> 充值
|
| 40 |
-
</a>
|
| 41 |
-
</li>
|
| 42 |
-
<li class="nav-item">
|
| 43 |
-
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 44 |
-
<i class="fas fa-chart-bar me-1"></i> 总览
|
| 45 |
-
</a>
|
| 46 |
-
</li>
|
| 47 |
-
<li class="nav-item">
|
| 48 |
-
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 49 |
-
<i class="fas fa-file-alt me-1"></i> 日志
|
| 50 |
-
</a>
|
| 51 |
-
</li>
|
| 52 |
-
<li class="nav-item">
|
| 53 |
-
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 54 |
-
<i class="fas fa-cog me-1"></i> 设置
|
| 55 |
-
</a>
|
| 56 |
-
</li>
|
| 57 |
-
<li class="nav-item">
|
| 58 |
-
<a class="nav-link d-flex align-items-center me-3" href="#" @click="showUserManagement = true; showImageUpload = false;">
|
| 59 |
-
<i class="fas fa-users-cog me-1"></i> 用户管理
|
| 60 |
-
</a>
|
| 61 |
-
</li>
|
| 62 |
-
<li class="nav-item">
|
| 63 |
-
<a class="nav-link d-flex align-items-center me-3" href="#">
|
| 64 |
-
<i class="fas fa-info-circle me-1"></i> 关于
|
| 65 |
-
</a>
|
| 66 |
-
</li>
|
| 67 |
-
</ul>
|
| 68 |
-
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
| 69 |
-
<li class="nav-item dropdown">
|
| 70 |
-
<a class="nav-link dropdown-toggle d-flex align-items-center me-3" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
| 71 |
-
<i class="fas fa-font me-1"></i> A|文
|
| 72 |
-
</a>
|
| 73 |
-
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
|
| 74 |
-
<li><a class="dropdown-item" href="#">中文</a></li>
|
| 75 |
-
<li><a class="dropdown-item" href="#">English</a></li>
|
| 76 |
-
</ul>
|
| 77 |
-
</li>
|
| 78 |
-
<li class="nav-item">
|
| 79 |
-
<a class="nav-link d-flex align-items-center" href="/login">
|
| 80 |
-
<i class="fas fa-user me-1"></i> 登录
|
| 81 |
-
</a>
|
| 82 |
-
</li>
|
| 83 |
-
</ul>
|
| 84 |
-
</div>
|
| 85 |
-
</div>
|
| 86 |
-
</nav>
|
| 87 |
|
| 88 |
<div class="container mt-5 pt-5">
|
| 89 |
<h2 class="mb-4">后台管理</h2>
|
| 90 |
|
| 91 |
-
<div class="card shadow-sm p-4 mb-4" v-if="showImageUpload">
|
| 92 |
-
<h4 class="mb-3">图片上传</h4>
|
| 93 |
-
<form @submit.prevent="uploadImage" enctype="multipart/form-data">
|
| 94 |
-
<div class="mb-3">
|
| 95 |
-
<label for="imageUpload" class="form-label">选择图片文件</label>
|
| 96 |
-
<input class="form-control" type="file" id="imageUpload" @change="handleFileUpload" required>
|
| 97 |
-
</div>
|
| 98 |
-
<button type="submit" class="btn btn-primary" :disabled="isUploading">
|
| 99 |
-
<span v-if="isUploading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
| 100 |
-
{{ isUploading ? '上传中...' : '上传图片' }}
|
| 101 |
-
</button>
|
| 102 |
-
</form>
|
| 103 |
-
<p v-if="uploadMessage" class="mt-3 text-info">{{ uploadMessage }}</p>
|
| 104 |
-
<div v-if="uploadedImageUrl" class="mt-3">
|
| 105 |
-
<h5>上传的图片:</h5>
|
| 106 |
-
<img :src="uploadedImageUrl" alt="Uploaded Image" class="img-fluid" style="max-width: 300px;">
|
| 107 |
-
<p class="mt-2">URL: <a :href="uploadedImageUrl" target="_blank">{{ uploadedImageUrl }}</a></p>
|
| 108 |
-
</div>
|
| 109 |
-
</div>
|
| 110 |
-
|
| 111 |
-
<!-- 用户管理模块 -->
|
| 112 |
-
<div class="card shadow-sm p-4 mb-4" v-if="showUserManagement">
|
| 113 |
-
<h4 class="mb-3">用户管理</h4>
|
| 114 |
-
<div class="input-group mb-3">
|
| 115 |
-
<input type="text" class="form-control" placeholder="按邮箱搜索用户" v-model="userSearchQuery" @keyup.enter="fetchUsers">
|
| 116 |
-
<button class="btn btn-outline-secondary" type="button" @click="fetchUsers">搜索</button>
|
| 117 |
-
</div>
|
| 118 |
-
<div class="table-responsive">
|
| 119 |
-
<table class="table table-striped table-hover">
|
| 120 |
-
<thead>
|
| 121 |
-
<tr>
|
| 122 |
-
<th>ID</th>
|
| 123 |
-
<th>邮箱</th>
|
| 124 |
-
<th>已验证</th>
|
| 125 |
-
<th>管理员</th>
|
| 126 |
-
<th>禁用</th> <!-- Add new column header -->
|
| 127 |
-
<th>创建时间</th>
|
| 128 |
-
<th>操作</th>
|
| 129 |
-
</tr>
|
| 130 |
-
</thead>
|
| 131 |
-
<tbody>
|
| 132 |
-
<tr v-for="user in users" :key="user.id">
|
| 133 |
-
<td>{{ user.id }}</td>
|
| 134 |
-
<td>{{ user.email }}</td>
|
| 135 |
-
<td>
|
| 136 |
-
<span v-if="user.email_verified" class="badge bg-success">是</span>
|
| 137 |
-
<span v-else class="badge bg-danger">否</span>
|
| 138 |
-
</td>
|
| 139 |
-
<td>
|
| 140 |
-
<span v-if="user.is_admin" class="badge bg-primary">是</span>
|
| 141 |
-
<span v-else class="badge bg-secondary">否</span>
|
| 142 |
-
</td>
|
| 143 |
-
<td>
|
| 144 |
-
<span v-if="user.disabled" class="badge bg-warning">是</span> <!-- Display disabled status -->
|
| 145 |
-
<span v-else class="badge bg-success">否</span>
|
| 146 |
-
</td>
|
| 147 |
-
<td>{{ new Date(user.created_at).toLocaleString() }}</td>
|
| 148 |
-
<td>
|
| 149 |
-
<button class="btn btn-sm btn-info me-2" @click="editUser(user)">编辑</button>
|
| 150 |
-
<button class="btn btn-sm btn-danger" @click="deleteUser(user.id)">删除</button>
|
| 151 |
-
</td>
|
| 152 |
-
</tr>
|
| 153 |
-
</tbody>
|
| 154 |
-
</table>
|
| 155 |
-
</div>
|
| 156 |
-
<nav aria-label="User pagination" v-if="totalPages > 1">
|
| 157 |
-
<ul class="pagination justify-content-center">
|
| 158 |
-
<li class="page-item" :class="{ disabled: currentPage === 1 }">
|
| 159 |
-
<a class="page-link" href="#" @click.prevent="changePage(currentPage - 1)">上一页</a>
|
| 160 |
-
</li>
|
| 161 |
-
<li class="page-item" v-for="pageNumber in totalPages" :key="pageNumber" :class="{ active: pageNumber === currentPage }">
|
| 162 |
-
<a class="page-link" href="#" @click.prevent="changePage(pageNumber)">{{ pageNumber }}</a>
|
| 163 |
-
</li>
|
| 164 |
-
<li class="page-item" :class="{ disabled: currentPage === totalPages }">
|
| 165 |
-
<a class="page-link" href="#" @click.prevent="changePage(currentPage + 1)">下一页</a>
|
| 166 |
-
</li>
|
| 167 |
-
</ul>
|
| 168 |
-
</nav>
|
| 169 |
-
<p v-if="userMessage" class="mt-3 text-info">{{ userMessage }}</p>
|
| 170 |
-
</div>
|
| 171 |
-
|
| 172 |
-
<!-- 编辑用户模态框 -->
|
| 173 |
-
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
|
| 174 |
-
<div class="modal-dialog">
|
| 175 |
-
<div class="modal-content">
|
| 176 |
-
<div class="modal-header">
|
| 177 |
-
<h5 class="modal-title" id="editUserModalLabel">编辑用户</h5>
|
| 178 |
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 179 |
-
</div>
|
| 180 |
-
<div class="modal-body">
|
| 181 |
-
<form @submit.prevent="saveUserChanges">
|
| 182 |
-
<div class="mb-3">
|
| 183 |
-
<label for="editUserEmail" class="form-label">邮箱</label>
|
| 184 |
-
<input type="email" class="form-control" id="editUserEmail" v-model="editingUser.email" required>
|
| 185 |
-
</div>
|
| 186 |
-
<div class="mb-3">
|
| 187 |
-
<label for="editUserPassword" class="form-label">新密码 (留空则不修改)</label>
|
| 188 |
-
<input type="password" class="form-control" id="editUserPassword" v-model="editingUser.password">
|
| 189 |
-
</div>
|
| 190 |
-
<div class="mb-3 form-check">
|
| 191 |
-
<input type="checkbox" class="form-check-input" id="editEmailVerified" v-model="editingUser.email_verified">
|
| 192 |
-
<label class="form-check-label" for="editEmailVerified">邮箱已验证</label>
|
| 193 |
-
</div>
|
| 194 |
-
<div class="mb-3 form-check">
|
| 195 |
-
<input type="checkbox" class="form-check-input" id="editIsAdmin" v-model="editingUser.is_admin">
|
| 196 |
-
<label class="form-check-label" for="editIsAdmin">管理员</label>
|
| 197 |
-
</div>
|
| 198 |
-
<div class="mb-3 form-check">
|
| 199 |
-
<input type="checkbox" class="form-check-input" id="editIsDisabled" v-model="editingUser.disabled">
|
| 200 |
-
<label class="form-check-label" for="editIsDisabled">禁用</label>
|
| 201 |
-
</div>
|
| 202 |
-
<button type="submit" class="btn btn-primary" :disabled="isSavingUser">
|
| 203 |
-
<span v-if="isSavingUser" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
| 204 |
-
{{ isSavingUser ? '保存中...' : '保存更改' }}
|
| 205 |
-
</button>
|
| 206 |
-
</form>
|
| 207 |
-
</div>
|
| 208 |
-
</div>
|
| 209 |
-
</div>
|
| 210 |
-
</div>
|
| 211 |
-
|
| 212 |
</div>
|
| 213 |
|
| 214 |
<footer class="footer text-center py-3 mt-auto">
|
|
|
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
<div id="app">
|
| 16 |
+
<div v-html="adminNavbar"></div> <!-- 引入共享导航栏 -->
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
<div class="container mt-5 pt-5">
|
| 19 |
<h2 class="mb-4">后台管理</h2>
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
</div>
|
| 22 |
|
| 23 |
<footer class="footer text-center py-3 mt-auto">
|
static/admin/pictures.html
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>SP Website - 图片管理</title>
|
| 7 |
+
<!-- Bootstrap 5.3 CSS CDN -->
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<!-- Font Awesome CDN for icons -->
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
| 11 |
+
<!-- Custom CSS -->
|
| 12 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<div id="app">
|
| 16 |
+
<div v-html="adminNavbar"></div> <!-- 引入共享导航栏 -->
|
| 17 |
+
|
| 18 |
+
<div class="container mt-5 pt-5">
|
| 19 |
+
<h2 class="mb-4">图片管理</h2>
|
| 20 |
+
|
| 21 |
+
<div class="card shadow-sm p-4 mb-4">
|
| 22 |
+
<h4 class="mb-3">图片上传</h4>
|
| 23 |
+
<form @submit.prevent="uploadImage" enctype="multipart/form-data">
|
| 24 |
+
<div class="mb-3">
|
| 25 |
+
<label for="imageUpload" class="form-label">选择图片文件</label>
|
| 26 |
+
<input class="form-control" type="file" id="imageUpload" @change="handleFileUpload" required>
|
| 27 |
+
</div>
|
| 28 |
+
<button type="submit" class="btn btn-primary" :disabled="isUploading">
|
| 29 |
+
<span v-if="isUploading" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
| 30 |
+
{{ isUploading ? '上传中...' : '上传图片' }}
|
| 31 |
+
</button>
|
| 32 |
+
</form>
|
| 33 |
+
<p v-if="uploadMessage" class="mt-3 text-info">{{ uploadMessage }}</p>
|
| 34 |
+
<div v-if="uploadedImageUrl" class="mt-3">
|
| 35 |
+
<h5>上传的图片:</h5>
|
| 36 |
+
<img :src="uploadedImageUrl" alt="Uploaded Image" class="img-fluid" style="max-width: 300px;">
|
| 37 |
+
<p class="mt-2">URL: <a :href="uploadedImageUrl" target="_blank">{{ uploadedImageUrl }}</a></p>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<!-- 图片管理模块 -->
|
| 42 |
+
<div class="card shadow-sm p-4 mb-4">
|
| 43 |
+
<h4 class="mb-3">图片管理</h4>
|
| 44 |
+
<div class="row">
|
| 45 |
+
<div class="col-md-3 col-sm-4 col-6 mb-4" v-for="image in images" :key="image.filename">
|
| 46 |
+
<div class="card h-100 image-card">
|
| 47 |
+
<div class="img-container">
|
| 48 |
+
<img :src="image.path" class="img-fluid img-thumbnail-square" :alt="image.filename">
|
| 49 |
+
</div>
|
| 50 |
+
<div class="card-body d-flex flex-column">
|
| 51 |
+
<h6 class="card-title text-truncate">{{ image.filename }}</h6>
|
| 52 |
+
<button class="btn btn-danger btn-sm mt-auto" @click="deleteImage(image.filename)">删除</button>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
<p v-if="imageMessage" class="mt-3 text-info">{{ imageMessage }}</p>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<footer class="footer text-center py-3 mt-auto">
|
| 63 |
+
<p class="mb-0 text-muted">API Router v0.6.11-preview.6 由 JustSong 构建,源代码遵循 <a href="#" class="text-decoration-none">MIT 协议</a></p>
|
| 64 |
+
</footer>
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<!-- Vue 3 CDN -->
|
| 68 |
+
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
| 69 |
+
<!-- Bootstrap 5.3 JS CDN -->
|
| 70 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 71 |
+
<!-- Custom Vue.js App Logic -->
|
| 72 |
+
<script type="module" src="/static/js/admin/pictures.js"></script>
|
| 73 |
+
</body>
|
| 74 |
+
</html>
|
static/admin/users.html
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>SP Website - 用户管理</title>
|
| 7 |
+
<!-- Bootstrap 5.3 CSS CDN -->
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<!-- Font Awesome CDN for icons -->
|
| 10 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
| 11 |
+
<!-- Custom CSS -->
|
| 12 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 13 |
+
</head>
|
| 14 |
+
<body>
|
| 15 |
+
<div id="app">
|
| 16 |
+
<div v-html="adminNavbar"></div> <!-- 引入共享导航栏 -->
|
| 17 |
+
|
| 18 |
+
<div class="container mt-5 pt-5">
|
| 19 |
+
<h2 class="mb-4">用户管理</h2>
|
| 20 |
+
|
| 21 |
+
<!-- 用户管理模块 -->
|
| 22 |
+
<div class="card shadow-sm p-4 mb-4">
|
| 23 |
+
<h4 class="mb-3">用户管理</h4>
|
| 24 |
+
<div class="input-group mb-3">
|
| 25 |
+
<input type="text" class="form-control" placeholder="按邮箱搜索用户" v-model="userSearchQuery" @keyup.enter="fetchUsers">
|
| 26 |
+
<button class="btn btn-outline-secondary" type="button" @click="fetchUsers">搜索</button>
|
| 27 |
+
</div>
|
| 28 |
+
<div class="table-responsive">
|
| 29 |
+
<table class="table table-striped table-hover">
|
| 30 |
+
<thead>
|
| 31 |
+
<tr>
|
| 32 |
+
<th>ID</th>
|
| 33 |
+
<th>邮箱</th>
|
| 34 |
+
<th>已验证</th>
|
| 35 |
+
<th>管理员</th>
|
| 36 |
+
<th>禁用</th> <!-- Add new column header -->
|
| 37 |
+
<th>创建时间</th>
|
| 38 |
+
<th>操作</th>
|
| 39 |
+
</tr>
|
| 40 |
+
</thead>
|
| 41 |
+
<tbody>
|
| 42 |
+
<tr v-for="user in users" :key="user.id">
|
| 43 |
+
<td>{{ user.id }}</td>
|
| 44 |
+
<td>{{ user.email }}</td>
|
| 45 |
+
<td>
|
| 46 |
+
<span v-if="user.email_verified" class="badge bg-success">是</span>
|
| 47 |
+
<span v-else class="badge bg-danger">否</span>
|
| 48 |
+
</td>
|
| 49 |
+
<td>
|
| 50 |
+
<span v-if="user.is_admin" class="badge bg-primary">是</span>
|
| 51 |
+
<span v-else class="badge bg-secondary">否</span>
|
| 52 |
+
</td>
|
| 53 |
+
<td>
|
| 54 |
+
<span v-if="user.disabled" class="badge bg-warning">是</span> <!-- Display disabled status -->
|
| 55 |
+
<span v-else class="badge bg-success">否</span>
|
| 56 |
+
</td>
|
| 57 |
+
<td>{{ new Date(user.created_at).toLocaleString() }}</td>
|
| 58 |
+
<td>
|
| 59 |
+
<button class="btn btn-sm btn-info me-2" @click="editUser(user)">编辑</button>
|
| 60 |
+
<button class="btn btn-sm btn-danger" @click="deleteUser(user.id)">删除</button>
|
| 61 |
+
</td>
|
| 62 |
+
</tr>
|
| 63 |
+
</tbody>
|
| 64 |
+
</table>
|
| 65 |
+
</div>
|
| 66 |
+
<nav aria-label="User pagination" v-if="totalPages > 1">
|
| 67 |
+
<ul class="pagination justify-content-center">
|
| 68 |
+
<li class="page-item" :class="{ disabled: currentPage === 1 }">
|
| 69 |
+
<a class="page-link" href="#" @click.prevent="changePage(currentPage - 1)">上一页</a>
|
| 70 |
+
</li>
|
| 71 |
+
<li class="page-item" v-for="pageNumber in totalPages" :key="pageNumber" :class="{ active: pageNumber === currentPage }">
|
| 72 |
+
<a class="page-link" href="#" @click.prevent="changePage(pageNumber)">{{ pageNumber }}</a>
|
| 73 |
+
</li>
|
| 74 |
+
<li class="page-item" :class="{ disabled: currentPage === totalPages }">
|
| 75 |
+
<a class="page-link" href="#" @click.prevent="changePage(currentPage + 1)">下一页</a>
|
| 76 |
+
</li>
|
| 77 |
+
</ul>
|
| 78 |
+
</nav>
|
| 79 |
+
<p v-if="userMessage" class="mt-3 text-info">{{ userMessage }}</p>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<!-- 编辑用户模态框 -->
|
| 83 |
+
<div class="modal fade" id="editUserModal" tabindex="-1" aria-labelledby="editUserModalLabel" aria-hidden="true">
|
| 84 |
+
<div class="modal-dialog">
|
| 85 |
+
<div class="modal-content">
|
| 86 |
+
<div class="modal-header">
|
| 87 |
+
<h5 class="modal-title" id="editUserModalLabel">编辑用户</h5>
|
| 88 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="modal-body">
|
| 91 |
+
<form @submit.prevent="saveUserChanges">
|
| 92 |
+
<div class="mb-3">
|
| 93 |
+
<label for="editUserEmail" class="form-label">邮箱</label>
|
| 94 |
+
<input type="email" class="form-control" id="editUserEmail" v-model="editingUser.email" required>
|
| 95 |
+
</div>
|
| 96 |
+
<div class="mb-3">
|
| 97 |
+
<label for="editUserPassword" class="form-label">新密码 (留空则不修改)</label>
|
| 98 |
+
<input type="password" class="form-control" id="editUserPassword" v-model="editingUser.password">
|
| 99 |
+
</div>
|
| 100 |
+
<div class="mb-3 form-check">
|
| 101 |
+
<input type="checkbox" class="form-check-input" id="editEmailVerified" v-model="editingUser.email_verified">
|
| 102 |
+
<label class="form-check-label" for="editEmailVerified">邮箱已验证</label>
|
| 103 |
+
</div>
|
| 104 |
+
<div class="mb-3 form-check">
|
| 105 |
+
<input type="checkbox" class="form-check-input" id="editIsAdmin" v-model="editingUser.is_admin">
|
| 106 |
+
<label class="form-check-label" for="editIsAdmin">管理员</label>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="mb-3 form-check">
|
| 109 |
+
<input type="checkbox" class="form-check-input" id="editIsDisabled" v-model="editingUser.disabled">
|
| 110 |
+
<label class="form-check-label" for="editIsDisabled">禁用</label>
|
| 111 |
+
</div>
|
| 112 |
+
<button type="submit" class="btn btn-primary" :disabled="isSavingUser">
|
| 113 |
+
<span v-if="isSavingUser" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
| 114 |
+
{{ isSavingUser ? '保存中...' : '保存更改' }}
|
| 115 |
+
</button>
|
| 116 |
+
</form>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
<!-- Vue 3 CDN -->
|
| 125 |
+
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
| 126 |
+
<!-- Bootstrap 5.3 JS CDN -->
|
| 127 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
| 128 |
+
<!-- Custom Vue.js App Logic -->
|
| 129 |
+
<script type="module" src="/static/js/admin/users.js"></script>
|
| 130 |
+
</body>
|
| 131 |
+
</html>
|
static/components/admin_navbar.html
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
| 2 |
+
<div class="container-fluid">
|
| 3 |
+
<a class="navbar-brand d-flex align-items-center" href="/">
|
| 4 |
+
<img src="/static/images/ShareAPI.png" alt="API Router Logo" width="30" height="30" class="d-inline-block align-text-top me-2">
|
| 5 |
+
<span class="fw-bold fs-5">API Router</span>
|
| 6 |
+
</a>
|
| 7 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
| 8 |
+
<span class="navbar-toggler-icon"></span>
|
| 9 |
+
</button>
|
| 10 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 11 |
+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
| 12 |
+
<li class="nav-item">
|
| 13 |
+
<a class="nav-link d-flex align-items-center me-3" href="/admin/users">
|
| 14 |
+
<i class="fas fa-users-cog me-1"></i> 用户管理
|
| 15 |
+
</a>
|
| 16 |
+
</li>
|
| 17 |
+
<li class="nav-item">
|
| 18 |
+
<a class="nav-link d-flex align-items-center me-3" href="/admin/pictures">
|
| 19 |
+
<i class="fas fa-images me-1"></i> 图片管理
|
| 20 |
+
</a>
|
| 21 |
+
</li>
|
| 22 |
+
</ul>
|
| 23 |
+
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
| 24 |
+
<li class="nav-item">
|
| 25 |
+
<a class="nav-link d-flex align-items-center" href="/login">
|
| 26 |
+
<i class="fas fa-user me-1"></i> 登录
|
| 27 |
+
</a>
|
| 28 |
+
</li>
|
| 29 |
+
</ul>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
</nav>
|
static/js/admin.js
CHANGED
|
@@ -17,26 +17,31 @@ const app = createApp({
|
|
| 17 |
isUploading: false,
|
| 18 |
uploadMessage: '',
|
| 19 |
uploadedImageUrl: '',
|
| 20 |
-
showImageUpload:
|
| 21 |
-
showUserManagement: false, // 控制用户管理模块的显示
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
// 用户管理相关数据
|
| 24 |
-
users: [],
|
| 25 |
-
userSearchQuery: '',
|
| 26 |
-
currentPage: 1,
|
| 27 |
-
pageSize: 10,
|
| 28 |
-
totalUsers: 0,
|
| 29 |
-
totalPages: 0,
|
| 30 |
-
userMessage: '',
|
| 31 |
-
editingUser: {
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
| 40 |
};
|
| 41 |
},
|
| 42 |
methods: {
|
|
@@ -46,173 +51,102 @@ const app = createApp({
|
|
| 46 |
handleFileUpload(event) {
|
| 47 |
this.selectedFile = event.target.files[0];
|
| 48 |
},
|
| 49 |
-
async uploadImage() {
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
headers: {
|
| 99 |
-
'Authorization': `Bearer ${token}`
|
| 100 |
-
}
|
| 101 |
-
});
|
| 102 |
-
const responseData = await response.json();
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
if (response.ok) {
|
| 105 |
-
this.
|
| 106 |
-
this.totalUsers = responseData.total_count;
|
| 107 |
-
this.totalPages = Math.ceil(this.totalUsers / this.pageSize);
|
| 108 |
-
this.userMessage = '';
|
| 109 |
-
console.log('Fetched users successfully. totalUsers:', this.totalUsers, 'totalPages:', this.totalPages); // Add log
|
| 110 |
} else {
|
| 111 |
-
|
| 112 |
}
|
| 113 |
} catch (error) {
|
| 114 |
-
|
| 115 |
-
console.error('获取用户错误:', error);
|
| 116 |
}
|
| 117 |
},
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
this.fetchUsers();
|
| 122 |
-
}
|
| 123 |
},
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
this.editUserModal.show();
|
| 127 |
},
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
try {
|
| 132 |
-
const token = localStorage.getItem('access_token');
|
| 133 |
-
const updatePayload = {
|
| 134 |
-
email: this.editingUser.email,
|
| 135 |
-
email_verified: this.editingUser.email_verified,
|
| 136 |
-
is_admin: this.editingUser.is_admin
|
| 137 |
-
};
|
| 138 |
-
if (this.editingUser.password) {
|
| 139 |
-
updatePayload.password = this.editingUser.password;
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
const response = await fetch(`/api/admin/users/${this.editingUser.id}`, {
|
| 143 |
-
method: 'PUT',
|
| 144 |
-
headers: {
|
| 145 |
-
'Content-Type': 'application/json',
|
| 146 |
-
'Authorization': `Bearer ${token}`
|
| 147 |
-
},
|
| 148 |
-
body: JSON.stringify(updatePayload)
|
| 149 |
-
});
|
| 150 |
-
const data = await response.json();
|
| 151 |
-
|
| 152 |
-
if (response.ok) {
|
| 153 |
-
this.userMessage = '用户更新成功!';
|
| 154 |
-
this.fetchUsers(); // Refresh user list
|
| 155 |
-
this.editUserModal.hide();
|
| 156 |
-
} else {
|
| 157 |
-
this.userMessage = data.detail || '用户更新失败。';
|
| 158 |
-
}
|
| 159 |
-
} catch (error) {
|
| 160 |
-
this.userMessage = '网络错误或服务器无响应。';
|
| 161 |
-
console.error('更新用户错误:', error);
|
| 162 |
-
} finally {
|
| 163 |
-
this.isSavingUser = false;
|
| 164 |
-
}
|
| 165 |
-
},
|
| 166 |
-
async deleteUser(userId) {
|
| 167 |
-
if (!confirm('确定要删除此用户吗?此操作不可逆!')) {
|
| 168 |
-
return;
|
| 169 |
-
}
|
| 170 |
-
this.userMessage = '删除用户中...';
|
| 171 |
-
try {
|
| 172 |
-
const token = localStorage.getItem('access_token');
|
| 173 |
-
const response = await fetch(`/api/admin/users/${userId}`, {
|
| 174 |
-
method: 'DELETE',
|
| 175 |
-
headers: {
|
| 176 |
-
'Authorization': `Bearer ${token}`
|
| 177 |
-
}
|
| 178 |
-
});
|
| 179 |
-
|
| 180 |
-
if (response.ok) {
|
| 181 |
-
this.userMessage = '用户删除成功!';
|
| 182 |
-
this.fetchUsers(); // Refresh user list
|
| 183 |
-
} else {
|
| 184 |
-
const data = await response.json();
|
| 185 |
-
this.userMessage = data.detail || '用户删除失败。';
|
| 186 |
-
}
|
| 187 |
-
} catch (error) {
|
| 188 |
-
this.userMessage = '网络错误或服务器无响应。';
|
| 189 |
-
console.error('删除用户错误:', error);
|
| 190 |
-
}
|
| 191 |
}
|
| 192 |
},
|
| 193 |
mounted() {
|
| 194 |
authMounted(this);
|
| 195 |
-
|
| 196 |
-
|
| 197 |
console.log('Admin app mounted!');
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
// Initialize Bootstrap Modal
|
| 201 |
-
this.editUserModal = new bootstrap.Modal(document.getElementById('editUserModal'));
|
| 202 |
-
|
| 203 |
-
// Fetch users when the user management tab is active
|
| 204 |
-
if (this.showUserManagement) {
|
| 205 |
-
this.fetchUsers();
|
| 206 |
-
}
|
| 207 |
},
|
| 208 |
watch: {
|
| 209 |
-
|
| 210 |
-
if (newValue) {
|
| 211 |
-
this.fetchUsers();
|
| 212 |
-
}
|
| 213 |
-
}
|
| 214 |
}
|
| 215 |
});
|
| 216 |
|
| 217 |
app.mount('#app');
|
| 218 |
-
console.log('Vue app successfully mounted to #app element.');
|
|
|
|
| 17 |
isUploading: false,
|
| 18 |
uploadMessage: '',
|
| 19 |
uploadedImageUrl: '',
|
| 20 |
+
showImageUpload: false, // 控制图片上传模块的显示 (默认不显示)
|
| 21 |
+
showUserManagement: false, // 控制用户管理模块的显示 (已移除)
|
| 22 |
+
showImageManagement: false, // 控制图片管理模块的显示 (已移除)
|
| 23 |
+
images: [], // 存储图片列表 (已移除)
|
| 24 |
+
imageMessage: '', // 图片管理模块的消息 (已移除)
|
| 25 |
+
adminNavbar: '', // 存储 admin_navbar.html 的内容
|
| 26 |
|
| 27 |
+
// 用户管理相关数据 (已移除,现在在 users.js 中处理)
|
| 28 |
+
// users: [],
|
| 29 |
+
// userSearchQuery: '',
|
| 30 |
+
// currentPage: 1,
|
| 31 |
+
// pageSize: 10,
|
| 32 |
+
// totalUsers: 0,
|
| 33 |
+
// totalPages: 0,
|
| 34 |
+
// userMessage: '',
|
| 35 |
+
// editingUser: {
|
| 36 |
+
// id: null,
|
| 37 |
+
// email: '',
|
| 38 |
+
// password: '',
|
| 39 |
+
// email_verified: false,
|
| 40 |
+
// is_admin: false,
|
| 41 |
+
// disabled: false
|
| 42 |
+
// },
|
| 43 |
+
// isSavingUser: false,
|
| 44 |
+
// editUserModal: null // Bootstrap Modal instance
|
| 45 |
};
|
| 46 |
},
|
| 47 |
methods: {
|
|
|
|
| 51 |
handleFileUpload(event) {
|
| 52 |
this.selectedFile = event.target.files[0];
|
| 53 |
},
|
| 54 |
+
// async uploadImage() { // Moved to pictures.js
|
| 55 |
+
// if (!this.selectedFile) {
|
| 56 |
+
// this.uploadMessage = '请选择一个文件。';
|
| 57 |
+
// return;
|
| 58 |
+
// }
|
| 59 |
|
| 60 |
+
// this.isUploading = true;
|
| 61 |
+
// this.uploadMessage = '';
|
| 62 |
+
// this.uploadedImageUrl = '';
|
| 63 |
|
| 64 |
+
// const formData = new FormData();
|
| 65 |
+
// formData.append('file', this.selectedFile);
|
| 66 |
|
| 67 |
+
// try {
|
| 68 |
+
// const token = localStorage.getItem('access_token');
|
| 69 |
+
// const response = await fetch('/api/admin/upload-image', {
|
| 70 |
+
// method: 'POST',
|
| 71 |
+
// headers: {
|
| 72 |
+
// 'Authorization': `Bearer ${token}`
|
| 73 |
+
// },
|
| 74 |
+
// body: formData
|
| 75 |
+
// });
|
| 76 |
+
// const data = await response.json();
|
| 77 |
|
| 78 |
+
// if (response.ok) {
|
| 79 |
+
// this.uploadMessage = data.message || '图片上传成功!';
|
| 80 |
+
// this.uploadedImageUrl = data.path;
|
| 81 |
+
// document.getElementById('imageUpload').value = '';
|
| 82 |
+
// this.selectedFile = null;
|
| 83 |
+
// } else {
|
| 84 |
+
// this.uploadMessage = data.detail || '图片上传失败。';
|
| 85 |
+
// }
|
| 86 |
+
// } catch (error) {
|
| 87 |
+
// this.uploadMessage = '网络错误或服务器无响应。';
|
| 88 |
+
// console.error('图片上传错误:', error);
|
| 89 |
+
// } finally {
|
| 90 |
+
// this.isUploading = false;
|
| 91 |
+
// }
|
| 92 |
+
// },
|
| 93 |
+
// async fetchImages() { // Moved to pictures.js
|
| 94 |
+
// this.imageMessage = '加载图片中...';
|
| 95 |
+
// try {
|
| 96 |
+
// const token = localStorage.getItem('access_token');
|
| 97 |
+
// const response = await fetch('/api/admin/images', {
|
| 98 |
+
// headers: {
|
| 99 |
+
// 'Authorization': `Bearer ${token}`
|
| 100 |
+
// }
|
| 101 |
+
// });
|
| 102 |
+
// const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
+
// if (response.ok) {
|
| 105 |
+
// this.images = data;
|
| 106 |
+
// this.imageMessage = '';
|
| 107 |
+
// } else {
|
| 108 |
+
// this.imageMessage = data.detail || '获取图片失败。';
|
| 109 |
+
// }
|
| 110 |
+
// } catch (error) {
|
| 111 |
+
// this.imageMessage = '网络错误或服务器无响应。';
|
| 112 |
+
// console.error('删除图片错误:', error);
|
| 113 |
+
// }
|
| 114 |
+
// },
|
| 115 |
+
async loadAdminNavbar() {
|
| 116 |
+
try {
|
| 117 |
+
const response = await fetch('/static/components/admin_navbar.html');
|
| 118 |
if (response.ok) {
|
| 119 |
+
this.adminNavbar = await response.text();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
} else {
|
| 121 |
+
console.error('Failed to load admin navbar:', response.statusText);
|
| 122 |
}
|
| 123 |
} catch (error) {
|
| 124 |
+
console.error('Error loading admin navbar:', error);
|
|
|
|
| 125 |
}
|
| 126 |
},
|
| 127 |
+
// Navigation methods for shared navbar
|
| 128 |
+
navigateToUserManagement() {
|
| 129 |
+
window.location.href = '/admin/users';
|
|
|
|
|
|
|
| 130 |
},
|
| 131 |
+
navigateToImageManagement() {
|
| 132 |
+
window.location.href = '/admin/pictures'; // Navigate to pictures page
|
|
|
|
| 133 |
},
|
| 134 |
+
navigateToImageUpload() {
|
| 135 |
+
// This method is no longer directly used as image upload is part of pictures.html
|
| 136 |
+
window.location.href = '/admin/pictures';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
}
|
| 138 |
},
|
| 139 |
mounted() {
|
| 140 |
authMounted(this);
|
| 141 |
+
appMounted(this);
|
| 142 |
+
this.loadAdminNavbar(); // Load the shared navbar
|
| 143 |
console.log('Admin app mounted!');
|
| 144 |
+
// No specific logic for admin.js as it only acts as a container now
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
},
|
| 146 |
watch: {
|
| 147 |
+
// No specific watchers needed for this page
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
}
|
| 149 |
});
|
| 150 |
|
| 151 |
app.mount('#app');
|
| 152 |
+
console.log('Vue app successfully mounted to #app element.');
|
static/js/admin/pictures.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/admin/pictures.js
|
| 2 |
+
const { createApp } = Vue;
|
| 3 |
+
|
| 4 |
+
import { authData, authMethods, authMounted } from '../auth.js';
|
| 5 |
+
import { appData, appMethods, appMounted } from '../store.js';
|
| 6 |
+
|
| 7 |
+
const app = createApp({
|
| 8 |
+
data() {
|
| 9 |
+
return {
|
| 10 |
+
...authData(),
|
| 11 |
+
...appData(),
|
| 12 |
+
adminNavbar: '', // 存储 admin_navbar.html 的内容
|
| 13 |
+
|
| 14 |
+
selectedFile: null,
|
| 15 |
+
isUploading: false,
|
| 16 |
+
uploadMessage: '',
|
| 17 |
+
uploadedImageUrl: '',
|
| 18 |
+
images: [], // 存储图片列表
|
| 19 |
+
imageMessage: '', // 图片管理模块的消息
|
| 20 |
+
};
|
| 21 |
+
},
|
| 22 |
+
methods: {
|
| 23 |
+
...authMethods(this),
|
| 24 |
+
...appMethods(this),
|
| 25 |
+
async loadAdminNavbar() {
|
| 26 |
+
try {
|
| 27 |
+
const response = await fetch('/static/components/admin_navbar.html');
|
| 28 |
+
if (response.ok) {
|
| 29 |
+
this.adminNavbar = await response.text();
|
| 30 |
+
} else {
|
| 31 |
+
console.error('Failed to load admin navbar:', response.statusText);
|
| 32 |
+
}
|
| 33 |
+
} catch (error) {
|
| 34 |
+
console.error('Error loading admin navbar:', error);
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
handleFileUpload(event) {
|
| 38 |
+
this.selectedFile = event.target.files[0];
|
| 39 |
+
},
|
| 40 |
+
async uploadImage() {
|
| 41 |
+
if (!this.selectedFile) {
|
| 42 |
+
this.uploadMessage = '请选择一个文件。';
|
| 43 |
+
return;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
this.isUploading = true;
|
| 47 |
+
this.uploadMessage = '';
|
| 48 |
+
this.uploadedImageUrl = '';
|
| 49 |
+
|
| 50 |
+
const formData = new FormData();
|
| 51 |
+
formData.append('file', this.selectedFile);
|
| 52 |
+
|
| 53 |
+
try {
|
| 54 |
+
const token = localStorage.getItem('access_token');
|
| 55 |
+
const response = await fetch('/api/admin/upload-image', {
|
| 56 |
+
method: 'POST',
|
| 57 |
+
headers: {
|
| 58 |
+
'Authorization': `Bearer ${token}`
|
| 59 |
+
},
|
| 60 |
+
body: formData
|
| 61 |
+
});
|
| 62 |
+
const data = await response.json();
|
| 63 |
+
|
| 64 |
+
if (response.ok) {
|
| 65 |
+
this.uploadMessage = data.message || '图片上传成功!';
|
| 66 |
+
this.uploadedImageUrl = data.path;
|
| 67 |
+
// 清空文件输入框
|
| 68 |
+
document.getElementById('imageUpload').value = '';
|
| 69 |
+
this.selectedFile = null;
|
| 70 |
+
this.fetchImages(); // Refresh image list after upload
|
| 71 |
+
} else {
|
| 72 |
+
this.uploadMessage = data.detail || '图片上传失败。';
|
| 73 |
+
}
|
| 74 |
+
} catch (error) {
|
| 75 |
+
this.uploadMessage = '网络错误或服务器无响应。';
|
| 76 |
+
console.error('图片上传错误:', error);
|
| 77 |
+
} finally {
|
| 78 |
+
this.isUploading = false;
|
| 79 |
+
}
|
| 80 |
+
},
|
| 81 |
+
async fetchImages() {
|
| 82 |
+
this.imageMessage = '加载图片中...';
|
| 83 |
+
try {
|
| 84 |
+
const token = localStorage.getItem('access_token');
|
| 85 |
+
const response = await fetch('/api/admin/images', {
|
| 86 |
+
headers: {
|
| 87 |
+
'Authorization': `Bearer ${token}`
|
| 88 |
+
}
|
| 89 |
+
});
|
| 90 |
+
const data = await response.json();
|
| 91 |
+
|
| 92 |
+
if (response.ok) {
|
| 93 |
+
this.images = data;
|
| 94 |
+
this.imageMessage = '';
|
| 95 |
+
} else {
|
| 96 |
+
this.imageMessage = data.detail || '获取图片失败。';
|
| 97 |
+
}
|
| 98 |
+
} catch (error) {
|
| 99 |
+
this.imageMessage = '网络错误或服务器无响应。';
|
| 100 |
+
console.error('获取图片错误:', error);
|
| 101 |
+
}
|
| 102 |
+
},
|
| 103 |
+
async deleteImage(filename) {
|
| 104 |
+
if (!confirm(`确定要删除图片 ${filename} 吗?此操作不可逆!`)) {
|
| 105 |
+
return;
|
| 106 |
+
}
|
| 107 |
+
this.imageMessage = '删除图片中...';
|
| 108 |
+
try {
|
| 109 |
+
const token = localStorage.getItem('access_token');
|
| 110 |
+
const response = await fetch(`/api/admin/images/${filename}`, {
|
| 111 |
+
method: 'DELETE',
|
| 112 |
+
headers: {
|
| 113 |
+
'Authorization': `Bearer ${token}`
|
| 114 |
+
}
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
if (response.ok) {
|
| 118 |
+
this.imageMessage = '图片删除成功!';
|
| 119 |
+
this.fetchImages(); // Refresh image list
|
| 120 |
+
} else {
|
| 121 |
+
const data = await response.json();
|
| 122 |
+
this.imageMessage = data.detail || '图片删除失败。';
|
| 123 |
+
}
|
| 124 |
+
} catch (error) {
|
| 125 |
+
this.imageMessage = '网络错误或服务器无响应。';
|
| 126 |
+
console.error('删除图片错误:', error);
|
| 127 |
+
}
|
| 128 |
+
},
|
| 129 |
+
// Navigation methods for shared navbar
|
| 130 |
+
navigateToUserManagement() {
|
| 131 |
+
window.location.href = '/admin/users';
|
| 132 |
+
},
|
| 133 |
+
navigateToImageManagement() {
|
| 134 |
+
// Already on image management page, no need to navigate
|
| 135 |
+
},
|
| 136 |
+
navigateToImageUpload() {
|
| 137 |
+
// Already on image management page, upload is part of this page
|
| 138 |
+
}
|
| 139 |
+
},
|
| 140 |
+
mounted() {
|
| 141 |
+
authMounted(this);
|
| 142 |
+
appMounted(this);
|
| 143 |
+
this.loadAdminNavbar(); // Load the shared navbar
|
| 144 |
+
this.fetchImages(); // Fetch images on page load
|
| 145 |
+
console.log('Image management app mounted!');
|
| 146 |
+
},
|
| 147 |
+
watch: {
|
| 148 |
+
// No specific watchers needed for this standalone page yet
|
| 149 |
+
}
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
app.mount('#app');
|
| 153 |
+
console.log('Image management Vue app successfully mounted to #app element.');
|
static/js/admin/users.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/admin/users.js
|
| 2 |
+
const { createApp } = Vue;
|
| 3 |
+
|
| 4 |
+
import { authData, authMethods, authMounted } from '../auth.js';
|
| 5 |
+
import { appData, appMethods, appMounted } from '../store.js';
|
| 6 |
+
|
| 7 |
+
const app = createApp({
|
| 8 |
+
data() {
|
| 9 |
+
return {
|
| 10 |
+
...authData(),
|
| 11 |
+
...appData(),
|
| 12 |
+
adminNavbar: '', // 存储 admin_navbar.html 的内容
|
| 13 |
+
|
| 14 |
+
// 用户管理相关数据
|
| 15 |
+
users: [],
|
| 16 |
+
userSearchQuery: '',
|
| 17 |
+
currentPage: 1,
|
| 18 |
+
pageSize: 10,
|
| 19 |
+
totalUsers: 0,
|
| 20 |
+
totalPages: 0,
|
| 21 |
+
userMessage: '',
|
| 22 |
+
editingUser: {
|
| 23 |
+
id: null,
|
| 24 |
+
email: '',
|
| 25 |
+
password: '',
|
| 26 |
+
email_verified: false,
|
| 27 |
+
is_admin: false,
|
| 28 |
+
disabled: false
|
| 29 |
+
},
|
| 30 |
+
isSavingUser: false,
|
| 31 |
+
editUserModal: null // Bootstrap Modal instance
|
| 32 |
+
};
|
| 33 |
+
},
|
| 34 |
+
methods: {
|
| 35 |
+
...authMethods(this),
|
| 36 |
+
...appMethods(this),
|
| 37 |
+
async fetchUsers() {
|
| 38 |
+
this.userMessage = '加载用户中...';
|
| 39 |
+
try {
|
| 40 |
+
const token = localStorage.getItem('access_token');
|
| 41 |
+
let url = `/api/admin/users?page=${this.currentPage}&page_size=${this.pageSize}`;
|
| 42 |
+
if (this.userSearchQuery) {
|
| 43 |
+
url += `&search=${encodeURIComponent(this.userSearchQuery)}`;
|
| 44 |
+
}
|
| 45 |
+
const response = await fetch(url, {
|
| 46 |
+
headers: {
|
| 47 |
+
'Authorization': `Bearer ${token}`
|
| 48 |
+
}
|
| 49 |
+
});
|
| 50 |
+
const responseData = await response.json();
|
| 51 |
+
|
| 52 |
+
if (response.ok) {
|
| 53 |
+
this.users = responseData.users;
|
| 54 |
+
this.totalUsers = responseData.total_count;
|
| 55 |
+
this.totalPages = Math.ceil(this.totalUsers / this.pageSize);
|
| 56 |
+
this.userMessage = '';
|
| 57 |
+
console.log('Fetched users successfully. totalUsers:', this.totalUsers, 'totalPages:', this.totalPages);
|
| 58 |
+
} else {
|
| 59 |
+
this.userMessage = responseData.detail || '获取用户失败。';
|
| 60 |
+
}
|
| 61 |
+
} catch (error) {
|
| 62 |
+
this.userMessage = '网络错误或服务器无响应。';
|
| 63 |
+
console.error('获取用户错误:', error);
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
changePage(pageNumber) {
|
| 67 |
+
if (pageNumber > 0 && pageNumber <= this.totalPages) {
|
| 68 |
+
this.currentPage = pageNumber;
|
| 69 |
+
this.fetchUsers();
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
editUser(user) {
|
| 73 |
+
this.editingUser = { ...user, password: '' }; // Clear password field for security
|
| 74 |
+
this.editUserModal.show();
|
| 75 |
+
},
|
| 76 |
+
async saveUserChanges() {
|
| 77 |
+
this.isSavingUser = true;
|
| 78 |
+
this.userMessage = '';
|
| 79 |
+
try {
|
| 80 |
+
const token = localStorage.getItem('access_token');
|
| 81 |
+
const updatePayload = {
|
| 82 |
+
email: this.editingUser.email,
|
| 83 |
+
email_verified: this.editingUser.email_verified,
|
| 84 |
+
is_admin: this.editingUser.is_admin,
|
| 85 |
+
disabled: this.editingUser.disabled
|
| 86 |
+
};
|
| 87 |
+
if (this.editingUser.password) {
|
| 88 |
+
updatePayload.password = this.editingUser.password;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
const response = await fetch(`/api/admin/users/${this.editingUser.id}`, {
|
| 92 |
+
method: 'PUT',
|
| 93 |
+
headers: {
|
| 94 |
+
'Content-Type': 'application/json',
|
| 95 |
+
'Authorization': `Bearer ${token}`
|
| 96 |
+
},
|
| 97 |
+
body: JSON.stringify(updatePayload)
|
| 98 |
+
});
|
| 99 |
+
const data = await response.json();
|
| 100 |
+
|
| 101 |
+
if (response.ok) {
|
| 102 |
+
this.userMessage = '用户更新成功!';
|
| 103 |
+
this.fetchUsers(); // Refresh user list
|
| 104 |
+
this.editUserModal.hide();
|
| 105 |
+
} else {
|
| 106 |
+
this.userMessage = data.detail || '用户更新失败。';
|
| 107 |
+
}
|
| 108 |
+
} catch (error) {
|
| 109 |
+
this.userMessage = '网络错误或服务器无响应。';
|
| 110 |
+
console.error('更新用户错误:', error);
|
| 111 |
+
} finally {
|
| 112 |
+
this.isSavingUser = false;
|
| 113 |
+
}
|
| 114 |
+
},
|
| 115 |
+
async deleteUser(userId) {
|
| 116 |
+
if (!confirm('确定要删除此用户吗?此操作不可逆!')) {
|
| 117 |
+
return;
|
| 118 |
+
}
|
| 119 |
+
this.userMessage = '删除用户中...';
|
| 120 |
+
try {
|
| 121 |
+
const token = localStorage.getItem('access_token');
|
| 122 |
+
const response = await fetch(`/api/admin/users/${userId}`, {
|
| 123 |
+
method: 'DELETE',
|
| 124 |
+
headers: {
|
| 125 |
+
'Authorization': `Bearer ${token}`
|
| 126 |
+
}
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
if (response.ok) {
|
| 130 |
+
this.userMessage = '用户删除成功!';
|
| 131 |
+
this.fetchUsers(); // Refresh user list
|
| 132 |
+
} else {
|
| 133 |
+
const data = await response.json();
|
| 134 |
+
this.userMessage = data.detail || '用户删除失败。';
|
| 135 |
+
}
|
| 136 |
+
} catch (error) {
|
| 137 |
+
this.userMessage = '网络错误或服务器无响应。';
|
| 138 |
+
console.error('删除用户错误:', error);
|
| 139 |
+
}
|
| 140 |
+
},
|
| 141 |
+
async loadAdminNavbar() {
|
| 142 |
+
try {
|
| 143 |
+
const response = await fetch('/static/components/admin_navbar.html');
|
| 144 |
+
if (response.ok) {
|
| 145 |
+
this.adminNavbar = await response.text();
|
| 146 |
+
} else {
|
| 147 |
+
console.error('Failed to load admin navbar:', response.statusText);
|
| 148 |
+
}
|
| 149 |
+
} catch (error) {
|
| 150 |
+
console.error('Error loading admin navbar:', error);
|
| 151 |
+
}
|
| 152 |
+
},
|
| 153 |
+
// Navigation methods for shared navbar
|
| 154 |
+
navigateToUserManagement() {
|
| 155 |
+
// Already on user management page, no need to navigate
|
| 156 |
+
},
|
| 157 |
+
navigateToImageManagement() {
|
| 158 |
+
window.location.href = '/admin/pictures'; // Navigate to pictures page
|
| 159 |
+
},
|
| 160 |
+
navigateToImageUpload() {
|
| 161 |
+
window.location.href = '/admin/pictures'; // Navigate to pictures page
|
| 162 |
+
}
|
| 163 |
+
},
|
| 164 |
+
mounted() {
|
| 165 |
+
authMounted(this);
|
| 166 |
+
appMounted(this);
|
| 167 |
+
this.loadAdminNavbar(); // Load the shared navbar
|
| 168 |
+
console.log('User management app mounted!');
|
| 169 |
+
|
| 170 |
+
this.editUserModal = new bootstrap.Modal(document.getElementById('editUserModal'));
|
| 171 |
+
this.fetchUsers(); // Fetch users on page load
|
| 172 |
+
},
|
| 173 |
+
watch: {
|
| 174 |
+
// No specific watchers needed for this standalone page yet
|
| 175 |
+
}
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
app.mount('#app');
|
| 179 |
+
console.log('User management Vue app successfully mounted to #app element.');
|
static/style.css
CHANGED
|
@@ -461,3 +461,32 @@ body::-webkit-scrollbar {
|
|
| 461 |
display: none; /* Hide divider in mobile view */
|
| 462 |
}
|
| 463 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
display: none; /* Hide divider in mobile view */
|
| 462 |
}
|
| 463 |
}
|
| 464 |
+
|
| 465 |
+
/* Image management specific styles */
|
| 466 |
+
.image-card .img-container {
|
| 467 |
+
position: relative;
|
| 468 |
+
width: 100%;
|
| 469 |
+
padding-top: 100%; /* 1:1 Aspect Ratio */
|
| 470 |
+
overflow: hidden;
|
| 471 |
+
background-color: #f8f9fa; /* Light background for padding */
|
| 472 |
+
display: flex;
|
| 473 |
+
justify-content: center;
|
| 474 |
+
align-items: center;
|
| 475 |
+
border-bottom: 1px solid #e9ecef; /* Separator from card body */
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.image-card .img-thumbnail-square {
|
| 479 |
+
position: absolute;
|
| 480 |
+
top: 50%;
|
| 481 |
+
left: 50%;
|
| 482 |
+
transform: translate(-50%, -50%);
|
| 483 |
+
max-width: calc(100% - 20px); /* 10px padding on each side */
|
| 484 |
+
max-height: calc(100% - 20px); /* 10px padding on each side */
|
| 485 |
+
object-fit: contain; /* Ensure image fits within the square, with padding */
|
| 486 |
+
border: none; /* Remove default thumbnail border */
|
| 487 |
+
padding: 0; /* Remove default thumbnail padding */
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.image-card .card-body {
|
| 491 |
+
padding: 1rem; /* Standard Bootstrap card body padding */
|
| 492 |
+
}
|