geqintan commited on
Commit
8d7d15e
·
1 Parent(s): 60d5c9f
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. **文档更新**: 更新了 `README.md`,反映了项目的新功能、技术栈和正确的本地运行指南。
26
- 5. **Memory Bank 更新**: 更新所有核心 Memory Bank 文件以反映项目最新状态
27
- 6. **Supabase 解决方案文件更新**: 更新了 `../solutions/supabase_solution.md`。
28
- 7. **`.env` 文件生成**: 生成包含 Supabase 凭证占位符的 `.env` 文件
 
 
 
 
 
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 申请、获取用户电子邮件、修改密码忘记密码功能。认证逻辑已分离到独立的登录页面。项目现在可以在本地通过 `conda` 和 `uvicorn` 命令运行,并准备好与 Supabase 数据库进行实际的数据交互。
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
- <nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
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: true, // 控制图片上传模块的显示
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
- id: null,
33
- email: '',
34
- password: '',
35
- email_verified: false,
36
- is_admin: false
37
- },
38
- isSavingUser: false,
39
- editUserModal: null // Bootstrap Modal instance
 
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
- if (!this.selectedFile) {
51
- this.uploadMessage = '请选择一个文件。';
52
- return;
53
- }
54
 
55
- this.isUploading = true;
56
- this.uploadMessage = '';
57
- this.uploadedImageUrl = '';
58
 
59
- const formData = new FormData();
60
- formData.append('file', this.selectedFile);
61
 
62
- try {
63
- const token = localStorage.getItem('access_token');
64
- const response = await fetch('/api/admin/upload-image', {
65
- method: 'POST',
66
- headers: {
67
- 'Authorization': `Bearer ${token}`
68
- },
69
- body: formData
70
- });
71
- const data = await response.json();
72
 
73
- if (response.ok) {
74
- this.uploadMessage = data.message || '图片上传成功!';
75
- this.uploadedImageUrl = data.path;
76
- // 清空文件输入框
77
- document.getElementById('imageUpload').value = '';
78
- this.selectedFile = null;
79
- } else {
80
- this.uploadMessage = data.detail || '图片上传失败。';
81
- }
82
- } catch (error) {
83
- this.uploadMessage = '网络错误或服务器无响应。';
84
- console.error('图片上传错误:', error);
85
- } finally {
86
- this.isUploading = false;
87
- }
88
- },
89
- async fetchUsers() {
90
- this.userMessage = '加载用户中...';
91
- try {
92
- const token = localStorage.getItem('access_token');
93
- let url = `/api/admin/users?page=${this.currentPage}&page_size=${this.pageSize}`;
94
- if (this.userSearchQuery) {
95
- url += `&search=${encodeURIComponent(this.userSearchQuery)}`;
96
- }
97
- const response = await fetch(url, {
98
- headers: {
99
- 'Authorization': `Bearer ${token}`
100
- }
101
- });
102
- const responseData = await response.json();
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  if (response.ok) {
105
- this.users = responseData.users;
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
- this.userMessage = responseData.detail || '获取用户失败。';
112
  }
113
  } catch (error) {
114
- this.userMessage = '网络错误或服务器无响应。';
115
- console.error('获取用户错误:', error);
116
  }
117
  },
118
- changePage(pageNumber) {
119
- if (pageNumber > 0 && pageNumber <= this.totalPages) {
120
- this.currentPage = pageNumber;
121
- this.fetchUsers();
122
- }
123
  },
124
- editUser(user) {
125
- this.editingUser = { ...user, password: '' }; // Clear password field for security
126
- this.editUserModal.show();
127
  },
128
- async saveUserChanges() {
129
- this.isSavingUser = true;
130
- this.userMessage = '';
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
- // proxyMounted(this); // Removed proxyMounted call
196
- appMounted(this); // Corrected mounted call
197
  console.log('Admin app mounted!');
198
- console.log('Initial totalPages:', this.totalPages); // Add log
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
- showUserManagement(newValue) {
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.'); // Add this log
 
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
+ }