Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
116ee34b6f | ||
|
|
836acf1aa8 |
@@ -0,0 +1,764 @@
|
||||
# ParamHub API 能力文档
|
||||
|
||||
> 本文档记录系统所有 API 能力,变更时请及时更新
|
||||
>
|
||||
> 最后更新:2026-07-11
|
||||
|
||||
---
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础地址**: `http://localhost:16041`
|
||||
- **认证方式**: Session Cookie(登录后获取)
|
||||
- **认证要求**: 所有 POST/PUT/DELETE 请求需先登录
|
||||
|
||||
---
|
||||
|
||||
## 1. 认证 API
|
||||
|
||||
### 1.1 登录
|
||||
```
|
||||
POST /login
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"password": "admin123"
|
||||
}
|
||||
|
||||
响应:
|
||||
{
|
||||
"success": true,
|
||||
"redirect": "/admin"
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 登出
|
||||
```
|
||||
GET /logout
|
||||
```
|
||||
|
||||
### 1.3 检查登录状态
|
||||
```
|
||||
GET /api/config (未登录返回 401)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 分类管理 API
|
||||
|
||||
### 2.1 获取分类列表
|
||||
```
|
||||
GET /api/categories
|
||||
GET /api/categories?all=1 (包含隐藏分类)
|
||||
```
|
||||
|
||||
### 2.2 获取分类详情
|
||||
```
|
||||
GET /api/categories/{category_id}
|
||||
```
|
||||
|
||||
### 2.3 创建分类
|
||||
```
|
||||
POST /api/categories
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"name": "分类名称",
|
||||
"icon": "ri-icon-name",
|
||||
"order": 0,
|
||||
"visible": true,
|
||||
"fields": ["field1", "field2"] // 动态字段配置
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 更新分类
|
||||
```
|
||||
PUT /api/categories/{category_id}
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:同创建
|
||||
```
|
||||
|
||||
### 2.5 删除分类
|
||||
```
|
||||
DELETE /api/categories/{category_id}
|
||||
```
|
||||
|
||||
### 2.6 切换分类显示/隐藏
|
||||
```
|
||||
POST /api/categories/{category_id}/visible
|
||||
```
|
||||
|
||||
### 2.7 导出分类
|
||||
```
|
||||
GET /api/categories/export // 导出所有分类
|
||||
GET /api/categories/export/{category_id} // 导出单个分类
|
||||
```
|
||||
|
||||
### 2.8 导入分类
|
||||
```
|
||||
POST /api/categories/import?mode=merge|replace
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"categories": [...]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. AI模型 API
|
||||
|
||||
### 3.1 获取模型列表
|
||||
```
|
||||
GET /api/models
|
||||
GET /api/models?all=1 // 包含隐藏模型
|
||||
GET /api/models?q=关键词 // 搜索
|
||||
GET /api/models?sort=name&order=desc // 排序
|
||||
```
|
||||
|
||||
### 3.2 获取模型详情
|
||||
```
|
||||
GET /api/models/{model_id}
|
||||
```
|
||||
|
||||
### 3.3 创建模型
|
||||
```
|
||||
POST /api/models
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"name": "模型名称",
|
||||
"organization": "组织",
|
||||
"parameters": "70B",
|
||||
"context_length": 4096,
|
||||
"mmlu": 85.5,
|
||||
"publish_date": "2024-01-01",
|
||||
"visible": true,
|
||||
"is_pinned": false
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 更新模型
|
||||
```
|
||||
PUT /api/models/{model_id}
|
||||
```
|
||||
|
||||
### 3.5 删除模型
|
||||
```
|
||||
DELETE /api/models/{model_id}
|
||||
```
|
||||
|
||||
### 3.6 切换显示/隐藏
|
||||
```
|
||||
POST /api/models/{model_id}/visible
|
||||
```
|
||||
|
||||
### 3.7 置顶/取消置顶
|
||||
```
|
||||
POST /api/models/{model_id}/pin
|
||||
```
|
||||
|
||||
### 3.8 增加阅读数
|
||||
```
|
||||
POST /api/models/{model_id}/view
|
||||
```
|
||||
|
||||
### 3.9 导出模型
|
||||
```
|
||||
GET /api/models/export
|
||||
```
|
||||
|
||||
### 3.10 导入模型
|
||||
```
|
||||
POST /api/models/import?mode=merge|replace
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"type": "models",
|
||||
"items": [...]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. GPU API
|
||||
|
||||
### 4.1 获取GPU列表
|
||||
```
|
||||
GET /api/gpus
|
||||
GET /api/gpus?all=1
|
||||
GET /api/gpus?q=关键词
|
||||
GET /api/gpus?sort=memory_gb&order=desc
|
||||
```
|
||||
|
||||
### 4.2 获取GPU详情
|
||||
```
|
||||
GET /api/gpus/{gpu_id}
|
||||
```
|
||||
|
||||
### 4.3 创建GPU
|
||||
```
|
||||
POST /api/gpus
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"name": "GPU名称",
|
||||
"manufacturer": "NVIDIA",
|
||||
"memory_gb": 80,
|
||||
"cuda_cores": 10752,
|
||||
"tensor_cores": 336,
|
||||
"price_usd": 30000,
|
||||
"release_year": 2024,
|
||||
"visible": true,
|
||||
"is_pinned": false
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 更新GPU
|
||||
```
|
||||
PUT /api/gpus/{gpu_id}
|
||||
```
|
||||
|
||||
### 4.5 删除GPU
|
||||
```
|
||||
DELETE /api/gpus/{gpu_id}
|
||||
```
|
||||
|
||||
### 4.6 切换显示/隐藏
|
||||
```
|
||||
POST /api/gpus/{gpu_id}/visible
|
||||
```
|
||||
|
||||
### 4.7 置顶/取消置顶
|
||||
```
|
||||
POST /api/gpus/{gpu_id}/pin
|
||||
```
|
||||
|
||||
### 4.8 增加阅读数
|
||||
```
|
||||
POST /api/gpus/{gpu_id}/view
|
||||
```
|
||||
|
||||
### 4.9 导出GPU
|
||||
```
|
||||
GET /api/gpus/export
|
||||
```
|
||||
|
||||
### 4.10 导入GPU
|
||||
```
|
||||
POST /api/gpus/import?mode=merge|replace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. CPU API
|
||||
|
||||
### 5.1 获取CPU列表
|
||||
```
|
||||
GET /api/cpus
|
||||
GET /api/cpus?all=1
|
||||
GET /api/cpus?q=关键词
|
||||
GET /api/cpus?sort=cores&order=desc
|
||||
```
|
||||
|
||||
### 5.2 获取CPU详情
|
||||
```
|
||||
GET /api/cpus/{cpu_id}
|
||||
```
|
||||
|
||||
### 5.3 创建CPU
|
||||
```
|
||||
POST /api/cpus
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"name": "CPU名称",
|
||||
"manufacturer": "AMD",
|
||||
"cores": 64,
|
||||
"threads": 128,
|
||||
"base_clock": 2.4,
|
||||
"boost_clock": 3.7,
|
||||
"price_usd": 8000,
|
||||
"visible": true,
|
||||
"is_pinned": false
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 更新CPU
|
||||
```
|
||||
PUT /api/cpus/{cpu_id}
|
||||
```
|
||||
|
||||
### 5.5 删除CPU
|
||||
```
|
||||
DELETE /api/cpus/{cpu_id}
|
||||
```
|
||||
|
||||
### 5.6 切换显示/隐藏
|
||||
```
|
||||
POST /api/cpus/{cpu_id}/visible
|
||||
```
|
||||
|
||||
### 5.7 置顶/取消置顶
|
||||
```
|
||||
POST /api/cpus/{cpu_id}/pin
|
||||
```
|
||||
|
||||
### 5.8 增加阅读数
|
||||
```
|
||||
POST /api/cpus/{cpu_id}/view
|
||||
```
|
||||
|
||||
### 5.9 导出CPU
|
||||
```
|
||||
GET /api/cpus/export
|
||||
```
|
||||
|
||||
### 5.10 导入CPU
|
||||
```
|
||||
POST /api/cpus/import?mode=merge|replace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 动态分类数据 API
|
||||
|
||||
### 6.1 获取分类数据列表
|
||||
```
|
||||
GET /api/items/{category_id}
|
||||
GET /api/items/{category_id}?all=1
|
||||
GET /api/items/{category_id}?sort=price&order=desc
|
||||
```
|
||||
|
||||
### 6.2 获取数据详情
|
||||
```
|
||||
GET /api/items/{category_id}/{item_id}
|
||||
```
|
||||
|
||||
### 6.3 创建数据
|
||||
```
|
||||
POST /api/items/{category_id}
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"name": "名称",
|
||||
"visible": true,
|
||||
"is_pinned": false,
|
||||
// ... 其他动态字段
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 更新数据
|
||||
```
|
||||
PUT /api/items/{category_id}/{item_id}
|
||||
```
|
||||
|
||||
### 6.5 删除数据
|
||||
```
|
||||
DELETE /api/items/{category_id}/{item_id}
|
||||
```
|
||||
|
||||
### 6.6 切换显示/隐藏
|
||||
```
|
||||
POST /api/items/{category_id}/{item_id}/visible
|
||||
```
|
||||
|
||||
### 6.7 置顶/取消置顶
|
||||
```
|
||||
POST /api/items/{category_id}/{item_id}/pin
|
||||
```
|
||||
|
||||
### 6.8 增加阅读数
|
||||
```
|
||||
POST /api/items/{category_id}/{item_id}/view
|
||||
```
|
||||
|
||||
### 6.9 导出分类数据
|
||||
```
|
||||
GET /api/items/{category_id}/export
|
||||
```
|
||||
|
||||
### 6.10 导入分类数据
|
||||
```
|
||||
POST /api/items/{category_id}/import?mode=merge|replace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 知识库 API
|
||||
|
||||
### 7.1 获取知识库列表
|
||||
```
|
||||
GET /api/knowledge
|
||||
GET /api/knowledge?all=1
|
||||
GET /api/knowledge?q=关键词
|
||||
GET /api/knowledge?category=分类名
|
||||
```
|
||||
|
||||
### 7.2 获取知识详情
|
||||
```
|
||||
GET /api/knowledge/{knowledge_id}
|
||||
```
|
||||
|
||||
### 7.3 创建知识
|
||||
```
|
||||
POST /api/knowledge
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"title": "标题",
|
||||
"content": "内容",
|
||||
"category": "分类",
|
||||
"order": 0,
|
||||
"visible": true
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 更新知识
|
||||
```
|
||||
PUT /api/knowledge/{knowledge_id}
|
||||
```
|
||||
|
||||
### 7.5 删除知识
|
||||
```
|
||||
DELETE /api/knowledge/{knowledge_id}
|
||||
```
|
||||
|
||||
### 7.6 切换显示/隐藏
|
||||
```
|
||||
POST /api/knowledge/{knowledge_id}/visible
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 审核系统 API
|
||||
|
||||
### 8.1 获取待审核列表
|
||||
```
|
||||
GET /api/reviews
|
||||
GET /api/reviews?status=pending // 待审核
|
||||
GET /api/reviews?status=approved // 已通过
|
||||
GET /api/reviews?status=rejected // 已拒绝
|
||||
GET /api/reviews?status=all // 全部
|
||||
```
|
||||
|
||||
### 8.2 获取待审核数量
|
||||
```
|
||||
GET /api/reviews/count
|
||||
```
|
||||
|
||||
### 8.3 获取审核详情
|
||||
```
|
||||
GET /api/reviews/{review_id}
|
||||
```
|
||||
|
||||
### 8.4 通过审核
|
||||
```
|
||||
POST /api/reviews/{review_id}/approve
|
||||
```
|
||||
|
||||
### 8.5 拒绝审核
|
||||
```
|
||||
POST /api/reviews/{review_id}/reject
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"reason": "拒绝原因"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 通知系统 API
|
||||
|
||||
### 9.1 获取通知列表
|
||||
```
|
||||
GET /api/notifications
|
||||
GET /api/notifications?unread=1 // 仅未读
|
||||
GET /api/notifications?limit=20
|
||||
```
|
||||
|
||||
### 9.2 获取未读数量
|
||||
```
|
||||
GET /api/notifications/unread-count
|
||||
```
|
||||
|
||||
### 9.3 标记已读
|
||||
```
|
||||
POST /api/notifications/{notification_id}/read
|
||||
```
|
||||
|
||||
### 9.4 全部标记已读
|
||||
```
|
||||
POST /api/notifications/read-all
|
||||
```
|
||||
|
||||
### 9.5 删除通知
|
||||
```
|
||||
DELETE /api/notifications/{notification_id}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 智能解析 API
|
||||
|
||||
### 10.1 获取解析提示词模板
|
||||
```
|
||||
POST /api/parse-prompt
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"category_type": "model|gpu|cpu|dynamic",
|
||||
"category_id": "分类ID",
|
||||
"subcategory_id": "子分类ID"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 图片解析
|
||||
```
|
||||
POST /api/parse-images
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"text": "附加文本描述",
|
||||
"images": ["图片URL或Base64"],
|
||||
"category_type": "model|gpu|cpu|dynamic",
|
||||
"subcategory_id": "子分类ID",
|
||||
"custom_prompt": "自定义提示词"
|
||||
}
|
||||
|
||||
响应:
|
||||
{
|
||||
"success": true,
|
||||
"count": 1,
|
||||
"products": [解析结果]
|
||||
}
|
||||
```
|
||||
|
||||
### 10.3 智能添加 - AI模型
|
||||
```
|
||||
POST /api/models/smart-add
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"text": "文本描述",
|
||||
"images": ["图片URL"],
|
||||
"subcategory_id": "子分类ID",
|
||||
"custom_prompt": "自定义提示词"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 智能添加 - GPU
|
||||
```
|
||||
POST /api/gpus/smart-add
|
||||
```
|
||||
|
||||
### 10.5 智能添加 - CPU
|
||||
```
|
||||
POST /api/cpus/smart-add
|
||||
```
|
||||
|
||||
### 10.6 智能添加 - 动态分类
|
||||
```
|
||||
POST /api/items/{category_id}/smart-add
|
||||
```
|
||||
|
||||
### 10.7 智能补充 - AI模型
|
||||
```
|
||||
POST /api/models/{model_id}/smart-update
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"text": "补充信息文本",
|
||||
"images": ["图片URL"]
|
||||
}
|
||||
|
||||
响应:
|
||||
{
|
||||
"success": true,
|
||||
"updated_fields": ["field1", "field2"],
|
||||
"model": 更新后的模型数据
|
||||
}
|
||||
```
|
||||
|
||||
### 10.8 智能补充 - GPU
|
||||
```
|
||||
POST /api/gpus/{gpu_id}/smart-update
|
||||
```
|
||||
|
||||
### 10.9 智能补充 - CPU
|
||||
```
|
||||
POST /api/cpus/{cpu_id}/smart-update
|
||||
```
|
||||
|
||||
### 10.10 智能补充 - 动态分类
|
||||
```
|
||||
POST /api/items/{category_id}/{item_id}/smart-update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 搜索与统计 API
|
||||
|
||||
### 11.1 全局搜索
|
||||
```
|
||||
GET /api/search?q=关键词
|
||||
|
||||
响应:
|
||||
{
|
||||
"models": [...],
|
||||
"gpus": [...],
|
||||
"cpus": [...]
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 显存计算
|
||||
```
|
||||
GET /api/calculate/vram?params=7&precision=fp16
|
||||
|
||||
参数:
|
||||
- params: 模型参数量(单位:B)
|
||||
- precision: 精度 (fp32|fp16|int8|int4)
|
||||
|
||||
响应:
|
||||
{
|
||||
"model_vram": 14.0, // 模型显存需求(GB)
|
||||
"total_vram": 18.2, // 总显存需求(含30%余量)
|
||||
"suitable_gpus": [...] // 适用GPU列表
|
||||
}
|
||||
```
|
||||
|
||||
### 11.3 统计数据
|
||||
```
|
||||
GET /api/stats
|
||||
|
||||
响应:
|
||||
{
|
||||
"models_count": 10,
|
||||
"gpus_count": 5,
|
||||
"cpus_count": 8,
|
||||
"categories_count": 3,
|
||||
"knowledge_count": 20,
|
||||
"latest_models": [...]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 网站配置 API
|
||||
|
||||
### 12.1 获取配置
|
||||
```
|
||||
GET /api/config
|
||||
|
||||
响应:
|
||||
{
|
||||
"site_name": "ParamHub",
|
||||
"admin_password": "admin123",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### 12.2 更新配置
|
||||
```
|
||||
PUT /api/config
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"site_name": "新名称",
|
||||
"admin_password": "新密码"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. 图片上传 API
|
||||
|
||||
### 13.1 上传图片(文件)
|
||||
```
|
||||
POST /api/upload/image
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
请求体:
|
||||
- file: 图片文件
|
||||
|
||||
响应:
|
||||
{
|
||||
"success": true,
|
||||
"filename": "abc123_1234567890.png",
|
||||
"url": "/static/uploads/abc123_1234567890.png"
|
||||
}
|
||||
```
|
||||
|
||||
### 13.2 上传图片(Base64)
|
||||
```
|
||||
POST /api/upload/image/base64
|
||||
Content-Type: application/json
|
||||
|
||||
请求体:
|
||||
{
|
||||
"image": "data:image/png;base64,xxxxx...",
|
||||
"ext": "png"
|
||||
}
|
||||
```
|
||||
|
||||
### 13.3 删除图片
|
||||
```
|
||||
DELETE /api/upload/image/delete/{filename}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误响应
|
||||
|
||||
所有 API 错误响应格式:
|
||||
```json
|
||||
{
|
||||
"error": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
常见错误码:
|
||||
- `400` - 请求参数错误
|
||||
- `401` - 未登录
|
||||
- `404` - 资源不存在
|
||||
- `500` - 服务器错误
|
||||
|
||||
---
|
||||
|
||||
## 审核模式说明
|
||||
|
||||
当系统开启审核模式(`REQUIRE_REVIEW = True`)时:
|
||||
|
||||
1. 创建产品(POST /api/models, /api/gpus, /api/cpus, /api/items/{category_id})时:
|
||||
- 数据不会直接入库
|
||||
- 而是提交到审核队列
|
||||
- 返回 `{ "success": true, "message": "已提交审核", "review_id": "xxx" }`
|
||||
|
||||
2. 管理员需通过审核 API 确认:
|
||||
- `POST /api/reviews/{review_id}/approve` - 通过
|
||||
- `POST /api/reviews/{review_id}/reject` - 拒绝
|
||||
|
||||
---
|
||||
|
||||
## 变更日志
|
||||
|
||||
| 日期 | 变更内容 |
|
||||
|------|----------|
|
||||
| 2026-07-11 | 初始版本,记录所有 API 能力 |
|
||||
@@ -54,18 +54,27 @@ param-hub-python/
|
||||
|
||||
## API接口
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/api/models` | GET | 获取模型列表 |
|
||||
| `/api/models/<id>` | GET | 获取模型详情 |
|
||||
| `/api/gpus` | GET | 获取GPU列表 |
|
||||
| `/api/gpus/<id>` | GET | 获取GPU详情 |
|
||||
| `/api/cpus` | GET | 获取CPU列表 |
|
||||
| `/api/cpus/<id>` | GET | 获取CPU详情 |
|
||||
| `/api/search` | GET | 全局搜索 |
|
||||
| `/api/calculate/vram` | GET | 显存计算 |
|
||||
| `/api/stats` | GET | 统计数据 |
|
||||
完整 API 能力文档请查看 [API.md](API.md)
|
||||
|
||||
### 快速参考
|
||||
|
||||
| 模块 | 主要接口 |
|
||||
|------|----------|
|
||||
| 认证 | `/login`, `/logout` |
|
||||
| 分类 | `/api/categories` CRUD + 导入导出 |
|
||||
| AI模型 | `/api/models` CRUD + 置顶 + 热度 + 导入导出 |
|
||||
| GPU | `/api/gpus` CRUD + 置顶 + 热度 + 导入导出 |
|
||||
| CPU | `/api/cpus` CRUD + 置顶 + 热度 + 导入导出 |
|
||||
| 动态分类 | `/api/items/{category_id}` CRUD |
|
||||
| 知识库 | `/api/knowledge` CRUD |
|
||||
| 审核 | `/api/reviews` 审核通过/拒绝 |
|
||||
| 通知 | `/api/notifications` 通知管理 |
|
||||
| 智能解析 | `/api/parse-images`, `/api/*/smart-add`, `/api/*/smart-update` |
|
||||
| 搜索统计 | `/api/search`, `/api/calculate/vram`, `/api/stats` |
|
||||
| 配置 | `/api/config` |
|
||||
| 图片上传 | `/api/upload/image` |
|
||||
|
||||
## 版本
|
||||
|
||||
- v2.0.0 - 产品审核发布 + 后台通知系统 + 完整API能力
|
||||
- v0.1.0 - 初始版本
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
ParamHub - 参数百科
|
||||
AI大模型与硬件参数速查平台
|
||||
v1.8.0 - 模块化重构 + 后台登录认证
|
||||
v2.0.0 - 产品审核发布 + 后台通知系统
|
||||
"""
|
||||
from flask import Flask, request, jsonify, session, redirect, url_for
|
||||
from flask_cors import CORS
|
||||
@@ -10,6 +10,9 @@ from datetime import timedelta
|
||||
from config import SECRET_KEY
|
||||
from utils import load_config
|
||||
|
||||
# 审核开关:设为 True 则所有产品需要审核
|
||||
REQUIRE_REVIEW = True
|
||||
|
||||
# ─── Flask 应用创建 ────────────────────────────────────────────
|
||||
|
||||
app = Flask(__name__, static_folder='static', static_url_path='/static')
|
||||
@@ -79,17 +82,24 @@ app.register_blueprint(upload_bp)
|
||||
from modules.routes.api_pin import pin_bp
|
||||
app.register_blueprint(pin_bp)
|
||||
|
||||
from modules.routes.api_notifications import notifications_bp
|
||||
app.register_blueprint(notifications_bp)
|
||||
|
||||
from modules.routes.api_reviews import reviews_bp
|
||||
app.register_blueprint(reviews_bp)
|
||||
|
||||
|
||||
# ─── 启动 ──────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 50)
|
||||
print("ParamHub - 参数百科 v1.8.0")
|
||||
print("ParamHub - 参数百科 v2.0.0")
|
||||
print("=" * 50)
|
||||
print("模块化重构 + 后台登录认证")
|
||||
print("产品审核发布 + 后台通知系统")
|
||||
print(f"访问地址: http://localhost:16041")
|
||||
print(f"后台管理: http://localhost:16041/admin")
|
||||
print(f"默认密码: admin123 (可在 config.json 中修改)")
|
||||
print(f"审核模式: {'开启' if REQUIRE_REVIEW else '关闭'}")
|
||||
print("=" * 50)
|
||||
|
||||
app.run(host='0.0.0.0', port=16041, debug=False)
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
+11
-15
@@ -1,15 +1,11 @@
|
||||
==================================================
|
||||
ParamHub - 参数百科 v1.8.0
|
||||
==================================================
|
||||
模块化重构 + 后台登录认证
|
||||
访问地址: http://localhost:16041
|
||||
后台管理: http://localhost:16041/admin
|
||||
默认密码: admin123 (可在 config.json 中修改)
|
||||
==================================================
|
||||
* Serving Flask app 'app'
|
||||
* Debug mode: off
|
||||
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||
* Running on all addresses (0.0.0.0)
|
||||
* Running on http://127.0.0.1:16041
|
||||
* Running on http://192.168.0.101:16041
|
||||
Press CTRL+C to quit
|
||||
Traceback (most recent call last):
|
||||
File "/home/openclaw/.openclaw/workspace-hz4th_coder/works/param-hub-python/app.py", line 52, in <module>
|
||||
from modules.routes.api_models import models_bp
|
||||
File "/home/openclaw/.openclaw/workspace-hz4th_coder/works/param-hub-python/modules/routes/api_models.py", line 18, in <module>
|
||||
@models_bp.route('/api/models')
|
||||
File "/home/openclaw/.local/lib/python3.12/site-packages/flask/sansio/scaffold.py", line 43, in wrapper_func
|
||||
self._check_setup_finished(f_name)
|
||||
File "/home/openclaw/.local/lib/python3.12/site-packages/flask/sansio/blueprints.py", line 215, in _check_setup_finished
|
||||
raise AssertionError(
|
||||
AssertionError: The setup method 'route' can no longer be called on the blueprint 'api_models'. It has already been registered at least once, any changes will not be applied consistently.
|
||||
Make sure all imports, decorators, functions, etc. needed to set up the blueprint are done before registering it.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,6 +11,14 @@ from utils import load_data, save_data, parse_date_to_timestamp
|
||||
cpus_bp = Blueprint('api_cpus', __name__)
|
||||
|
||||
|
||||
def is_review_required():
|
||||
try:
|
||||
import app as app_module
|
||||
return getattr(app_module, 'REQUIRE_REVIEW', False)
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def _safe_sort_key(x, key):
|
||||
val = x.get(key)
|
||||
if val is None:
|
||||
@@ -54,6 +62,23 @@ def api_cpu_detail(cpu_id):
|
||||
@cpus_bp.route('/api/cpus', methods=['POST'])
|
||||
def api_create_cpu():
|
||||
data = request.get_json()
|
||||
|
||||
# 审核模式
|
||||
if is_review_required():
|
||||
from modules.routes.api_reviews import submit_for_review
|
||||
from modules.routes.api_notifications import create_notification
|
||||
|
||||
review = submit_for_review('cpus', data, source='web')
|
||||
product_name = data.get('name', '未知')
|
||||
create_notification(
|
||||
title='新产品待审核',
|
||||
message=f'有新的CPU "{product_name}"待审核',
|
||||
level='warning',
|
||||
category='review',
|
||||
data={'review_id': review['id'], 'category': 'cpus'}
|
||||
)
|
||||
return jsonify({'success': True, 'message': '已提交审核,请等待管理员确认', 'review_id': review['id']})
|
||||
|
||||
cpus = load_data(CPUS_FILE)
|
||||
data['id'] = uuid.uuid4().hex[:12]
|
||||
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
@@ -11,6 +11,14 @@ from utils import load_data, save_data, parse_date_to_timestamp
|
||||
gpus_bp = Blueprint('api_gpus', __name__)
|
||||
|
||||
|
||||
def is_review_required():
|
||||
try:
|
||||
import app as app_module
|
||||
return getattr(app_module, 'REQUIRE_REVIEW', False)
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def _safe_sort_key(x, key):
|
||||
val = x.get(key)
|
||||
if val is None:
|
||||
@@ -54,6 +62,23 @@ def api_gpu_detail(gpu_id):
|
||||
@gpus_bp.route('/api/gpus', methods=['POST'])
|
||||
def api_create_gpu():
|
||||
data = request.get_json()
|
||||
|
||||
# 审核模式
|
||||
if is_review_required():
|
||||
from modules.routes.api_reviews import submit_for_review
|
||||
from modules.routes.api_notifications import create_notification
|
||||
|
||||
review = submit_for_review('gpus', data, source='web')
|
||||
product_name = data.get('name', '未知')
|
||||
create_notification(
|
||||
title='新产品待审核',
|
||||
message=f'有新的GPU "{product_name}"待审核',
|
||||
level='warning',
|
||||
category='review',
|
||||
data={'review_id': review['id'], 'category': 'gpus'}
|
||||
)
|
||||
return jsonify({'success': True, 'message': '已提交审核,请等待管理员确认', 'review_id': review['id']})
|
||||
|
||||
gpus = load_data(GPUS_FILE)
|
||||
data['id'] = uuid.uuid4().hex[:12]
|
||||
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
@@ -11,6 +11,14 @@ from utils import load_data, save_data, parse_date_to_timestamp
|
||||
items_bp = Blueprint('api_items', __name__)
|
||||
|
||||
|
||||
def is_review_required():
|
||||
try:
|
||||
import app as app_module
|
||||
return getattr(app_module, 'REQUIRE_REVIEW', False)
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
@items_bp.route('/api/items/<category_id>')
|
||||
def api_items(category_id):
|
||||
items_file = DATA_DIR / f'items_{category_id}.json'
|
||||
@@ -49,6 +57,23 @@ def api_item_detail(category_id, item_id):
|
||||
@items_bp.route('/api/items/<category_id>', methods=['POST'])
|
||||
def api_create_item(category_id):
|
||||
data = request.get_json()
|
||||
|
||||
# 审核模式
|
||||
if is_review_required():
|
||||
from modules.routes.api_reviews import submit_for_review
|
||||
from modules.routes.api_notifications import create_notification
|
||||
|
||||
review = submit_for_review(category_id, data, source='web')
|
||||
product_name = data.get('name', '未知')
|
||||
create_notification(
|
||||
title='新产品待审核',
|
||||
message=f'有新的产品"{product_name}"待审核',
|
||||
level='warning',
|
||||
category='review',
|
||||
data={'review_id': review['id'], 'category': category_id}
|
||||
)
|
||||
return jsonify({'success': True, 'message': '已提交审核,请等待管理员确认', 'review_id': review['id']})
|
||||
|
||||
items_file = DATA_DIR / f'items_{category_id}.json'
|
||||
items = load_data(items_file)
|
||||
data['id'] = uuid.uuid4().hex[:12]
|
||||
|
||||
@@ -11,6 +11,15 @@ from utils import load_data, save_data, parse_date_to_timestamp, safe_sort_key
|
||||
models_bp = Blueprint('api_models', __name__)
|
||||
|
||||
|
||||
def is_review_required():
|
||||
"""检查是否需要审核"""
|
||||
try:
|
||||
import app as app_module
|
||||
return getattr(app_module, 'REQUIRE_REVIEW', False)
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
@models_bp.route('/api/models')
|
||||
def api_models():
|
||||
models = load_data(MODELS_FILE)
|
||||
@@ -47,6 +56,24 @@ def api_model_detail(model_id):
|
||||
@models_bp.route('/api/models', methods=['POST'])
|
||||
def api_create_model():
|
||||
data = request.get_json()
|
||||
|
||||
# 审核模式:提交到审核队列
|
||||
if is_review_required():
|
||||
from modules.routes.api_reviews import submit_for_review
|
||||
from modules.routes.api_notifications import create_notification
|
||||
|
||||
review = submit_for_review('ai-models', data, source='web')
|
||||
product_name = data.get('name', '未知')
|
||||
create_notification(
|
||||
title='新产品待审核',
|
||||
message=f'有新的AI模型"{product_name}"待审核',
|
||||
level='warning',
|
||||
category='review',
|
||||
data={'review_id': review['id'], 'category': 'ai-models'}
|
||||
)
|
||||
return jsonify({'success': True, 'message': '已提交审核,请等待管理员确认', 'review_id': review['id']})
|
||||
|
||||
# 直接创建模式
|
||||
models = load_data(MODELS_FILE)
|
||||
data['id'] = uuid.uuid4().hex[:12]
|
||||
data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
通知管理 API
|
||||
"""
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify
|
||||
from config import DATA_DIR
|
||||
from utils import load_data, save_data
|
||||
|
||||
notifications_bp = Blueprint('api_notifications', __name__)
|
||||
|
||||
NOTIFICATIONS_FILE = DATA_DIR / 'notifications.json'
|
||||
|
||||
|
||||
@notifications_bp.route('/api/notifications')
|
||||
def api_notifications():
|
||||
"""获取通知列表"""
|
||||
notifications = load_data(NOTIFICATIONS_FILE)
|
||||
|
||||
# 筛选参数
|
||||
unread_only = request.args.get('unread', '0') == '1'
|
||||
limit = int(request.args.get('limit', 50))
|
||||
|
||||
if unread_only:
|
||||
notifications = [n for n in notifications if not n.get('read', False)]
|
||||
|
||||
# 按时间倒序
|
||||
notifications = sorted(notifications, key=lambda x: x.get('created_at', ''), reverse=True)
|
||||
|
||||
return jsonify(notifications[:limit])
|
||||
|
||||
|
||||
@notifications_bp.route('/api/notifications/unread-count')
|
||||
def api_unread_count():
|
||||
"""获取未读通知数量"""
|
||||
notifications = load_data(NOTIFICATIONS_FILE)
|
||||
count = len([n for n in notifications if not n.get('read', False)])
|
||||
return jsonify({'count': count})
|
||||
|
||||
|
||||
@notifications_bp.route('/api/notifications/<notification_id>/read', methods=['POST'])
|
||||
def api_mark_read(notification_id):
|
||||
"""标记通知为已读"""
|
||||
notifications = load_data(NOTIFICATIONS_FILE)
|
||||
notification = next((n for n in notifications if n['id'] == notification_id), None)
|
||||
if not notification:
|
||||
return jsonify({'error': 'Notification not found'}), 404
|
||||
notification['read'] = True
|
||||
notification['read_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
save_data(NOTIFICATIONS_FILE, notifications)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@notifications_bp.route('/api/notifications/read-all', methods=['POST'])
|
||||
def api_mark_all_read():
|
||||
"""标记所有通知为已读"""
|
||||
notifications = load_data(NOTIFICATIONS_FILE)
|
||||
for n in notifications:
|
||||
if not n.get('read', False):
|
||||
n['read'] = True
|
||||
n['read_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
save_data(NOTIFICATIONS_FILE, notifications)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@notifications_bp.route('/api/notifications/<notification_id>', methods=['DELETE'])
|
||||
def api_delete_notification(notification_id):
|
||||
"""删除通知"""
|
||||
notifications = load_data(NOTIFICATIONS_FILE)
|
||||
notifications = [n for n in notifications if n['id'] != notification_id]
|
||||
save_data(NOTIFICATIONS_FILE, notifications)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# ─── 内部函数:创建通知 ───────────────────────────────────────────────────────
|
||||
|
||||
def create_notification(title, message, level='info', category='system', data=None):
|
||||
"""
|
||||
创建一条通知
|
||||
|
||||
参数:
|
||||
title: 通知标题
|
||||
message: 通知内容
|
||||
level: 级别 info/warning/error/success
|
||||
category: 分类 system/api/review/statistics
|
||||
data: 附加数据(dict)
|
||||
"""
|
||||
notifications = load_data(NOTIFICATIONS_FILE)
|
||||
|
||||
notification = {
|
||||
'id': uuid.uuid4().hex[:12],
|
||||
'title': title,
|
||||
'message': message,
|
||||
'level': level, # info, warning, error, success
|
||||
'category': category, # system, api, review, statistics
|
||||
'data': data or {},
|
||||
'read': False,
|
||||
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
notifications.append(notification)
|
||||
|
||||
# 只保留最近500条通知
|
||||
if len(notifications) > 500:
|
||||
notifications = sorted(notifications, key=lambda x: x.get('created_at', ''), reverse=True)[:500]
|
||||
|
||||
save_data(NOTIFICATIONS_FILE, notifications)
|
||||
return notification
|
||||
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
产品审核 API
|
||||
"""
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify, session
|
||||
from config import DATA_DIR, MODELS_FILE, GPUS_FILE, CPUS_FILE
|
||||
from utils import load_data, save_data
|
||||
|
||||
reviews_bp = Blueprint('api_reviews', __name__)
|
||||
|
||||
PENDING_FILE = DATA_DIR / 'pending_reviews.json'
|
||||
|
||||
|
||||
@reviews_bp.route('/api/reviews')
|
||||
def api_reviews():
|
||||
"""获取待审核列表"""
|
||||
reviews = load_data(PENDING_FILE)
|
||||
|
||||
# 筛选参数
|
||||
status = request.args.get('status', 'pending') # pending, approved, rejected, all
|
||||
limit = int(request.args.get('limit', 100))
|
||||
|
||||
if status != 'all':
|
||||
reviews = [r for r in reviews if r.get('status', 'pending') == status]
|
||||
|
||||
# 按时间倒序
|
||||
reviews = sorted(reviews, key=lambda x: x.get('created_at', ''), reverse=True)
|
||||
|
||||
return jsonify(reviews[:limit])
|
||||
|
||||
|
||||
@reviews_bp.route('/api/reviews/count')
|
||||
def api_reviews_count():
|
||||
"""获取待审核数量"""
|
||||
reviews = load_data(PENDING_FILE)
|
||||
pending_count = len([r for r in reviews if r.get('status', 'pending') == 'pending'])
|
||||
return jsonify({'count': pending_count})
|
||||
|
||||
|
||||
@reviews_bp.route('/api/reviews/<review_id>')
|
||||
def api_review_detail(review_id):
|
||||
"""获取审核详情"""
|
||||
reviews = load_data(PENDING_FILE)
|
||||
review = next((r for r in reviews if r['id'] == review_id), None)
|
||||
if not review:
|
||||
return jsonify({'error': 'Review not found'}), 404
|
||||
return jsonify(review)
|
||||
|
||||
|
||||
@reviews_bp.route('/api/reviews/<review_id>/approve', methods=['POST'])
|
||||
def api_approve_review(review_id):
|
||||
"""通过审核"""
|
||||
reviews = load_data(PENDING_FILE)
|
||||
review = next((r for r in reviews if r['id'] == review_id), None)
|
||||
if not review:
|
||||
return jsonify({'error': 'Review not found'}), 404
|
||||
|
||||
if review.get('status') != 'pending':
|
||||
return jsonify({'error': '该申请已处理'}), 400
|
||||
|
||||
# 获取数据文件
|
||||
category_id = review.get('category_id')
|
||||
data_file = get_data_file(category_id)
|
||||
if not data_file:
|
||||
return jsonify({'error': 'Unknown category'}), 400
|
||||
|
||||
# 添加数据到正式文件
|
||||
items = load_data(data_file)
|
||||
item_data = review.get('data', {})
|
||||
|
||||
# 确保有ID和时间戳
|
||||
if 'id' not in item_data or not item_data['id']:
|
||||
item_data['id'] = uuid.uuid4().hex[:12]
|
||||
item_data['created_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
item_data['visible'] = True
|
||||
item_data['approved'] = True
|
||||
item_data['approved_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
items.append(item_data)
|
||||
save_data(data_file, items)
|
||||
|
||||
# 更新审核状态
|
||||
review['status'] = 'approved'
|
||||
review['approved_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
review['approved_by'] = session.get('username', 'admin')
|
||||
save_data(PENDING_FILE, reviews)
|
||||
|
||||
return jsonify({'success': True, 'item': item_data})
|
||||
|
||||
|
||||
@reviews_bp.route('/api/reviews/<review_id>/reject', methods=['POST'])
|
||||
def api_reject_review(review_id):
|
||||
"""拒绝审核"""
|
||||
reviews = load_data(PENDING_FILE)
|
||||
review = next((r for r in reviews if r['id'] == review_id), None)
|
||||
if not review:
|
||||
return jsonify({'error': 'Review not found'}), 404
|
||||
|
||||
if review.get('status') != 'pending':
|
||||
return jsonify({'error': '该申请已处理'}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
reason = data.get('reason', '')
|
||||
|
||||
# 更新审核状态
|
||||
review['status'] = 'rejected'
|
||||
review['rejected_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
review['rejected_by'] = session.get('username', 'admin')
|
||||
review['reject_reason'] = reason
|
||||
save_data(PENDING_FILE, reviews)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
def get_data_file(category_id):
|
||||
"""获取类别对应的数据文件"""
|
||||
builtin_map = {
|
||||
'ai-models': MODELS_FILE,
|
||||
'gpus': GPUS_FILE,
|
||||
'cpus': CPUS_FILE
|
||||
}
|
||||
|
||||
if category_id in builtin_map:
|
||||
return builtin_map[category_id]
|
||||
|
||||
# 动态分类
|
||||
return DATA_DIR / f'items_{category_id}.json'
|
||||
|
||||
|
||||
# ─── 内部函数:提交审核 ───────────────────────────────────────────────────────
|
||||
|
||||
def submit_for_review(category_id, data, source='web', submitter=None):
|
||||
"""
|
||||
提交产品到审核队列
|
||||
|
||||
参数:
|
||||
category_id: 分类ID
|
||||
data: 产品数据(dict)
|
||||
source: 来源 web/api
|
||||
submitter: 提交者
|
||||
"""
|
||||
reviews = load_data(PENDING_FILE)
|
||||
|
||||
review = {
|
||||
'id': uuid.uuid4().hex[:12],
|
||||
'category_id': category_id,
|
||||
'data': data,
|
||||
'source': source,
|
||||
'submitter': submitter or 'anonymous',
|
||||
'status': 'pending', # pending, approved, rejected
|
||||
'created_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
reviews.append(review)
|
||||
save_data(PENDING_FILE, reviews)
|
||||
|
||||
return review
|
||||
@@ -41,6 +41,21 @@
|
||||
<!-- 概览 -->
|
||||
<section id="section-overview">
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">管理概览</h1>
|
||||
|
||||
<!-- 待办提醒 -->
|
||||
<div id="todoAlert" class="mb-6 hidden">
|
||||
<div class="bg-orange-50 border border-orange-200 rounded-lg p-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<i class="ri-notification-badge-line text-2xl text-orange-600"></i>
|
||||
<div>
|
||||
<div class="font-medium text-orange-800">有 <span id="todoCount">0</span> 条待处理事项</div>
|
||||
<div class="text-sm text-orange-600" id="todoDetail"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="showSection('reviews')" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700">立即处理</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-5 gap-4 mb-8" id="statsCards"><div class="text-center text-gray-400 py-4">加载中...</div></div>
|
||||
<div class="bg-white rounded-xl p-6 shadow-sm mb-8">
|
||||
<h2 class="text-lg font-semibold text-gray-800 mb-4">快捷操作</h2>
|
||||
@@ -275,6 +290,49 @@
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 通知中心 -->
|
||||
<section id="section-notifications" class="hidden">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">通知中心</h1>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="markAllRead()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300"><i class="ri-check-double-line mr-1"></i>全部已读</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="notificationsList" class="space-y-3">
|
||||
<div class="text-center text-gray-400 py-8">加载中...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 审核管理 -->
|
||||
<section id="section-reviews" class="hidden">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">审核管理</h1>
|
||||
<div class="flex gap-2">
|
||||
<select id="reviewStatusFilter" onchange="loadReviews()" class="px-4 py-2 border rounded-lg">
|
||||
<option value="pending">待审核</option>
|
||||
<option value="approved">已通过</option>
|
||||
<option value="rejected">已拒绝</option>
|
||||
<option value="all">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 border-b">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">产品名称</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">分类</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">来源</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">提交时间</th>
|
||||
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">状态</th>
|
||||
<th class="px-4 py-3 text-center text-sm font-medium text-gray-600">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reviewsTable"><tr><td colspan="6" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
@@ -766,6 +824,24 @@
|
||||
</a>
|
||||
`;
|
||||
|
||||
// 通知中心(显示未读数)
|
||||
html += `
|
||||
<a href="#notifications" onclick="showSection('notifications')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300" data-section="notifications">
|
||||
<i class="ri-notification-line"></i>
|
||||
<span>通知中心</span>
|
||||
<span id="navNotificationBadge" class="ml-auto px-1.5 py-0.5 bg-red-500 text-white text-xs rounded-full hidden">0</span>
|
||||
</a>
|
||||
`;
|
||||
|
||||
// 审核管理(显示待审核数)
|
||||
html += `
|
||||
<a href="#reviews" onclick="showSection('reviews')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300" data-section="reviews">
|
||||
<i class="ri-checkbox-circle-line"></i>
|
||||
<span>审核管理</span>
|
||||
<span id="navReviewBadge" class="ml-auto px-1.5 py-0.5 bg-orange-500 text-white text-xs rounded-full hidden">0</span>
|
||||
</a>
|
||||
`;
|
||||
|
||||
document.getElementById('sidebarNav').innerHTML = html;
|
||||
}
|
||||
|
||||
@@ -882,6 +958,8 @@
|
||||
if (section === 'gpus') loadAdminGpus();
|
||||
if (section === 'cpus') loadAdminCpus();
|
||||
if (section === 'knowledge') loadAdminKnowledge();
|
||||
if (section === 'notifications') loadNotifications();
|
||||
if (section === 'reviews') loadReviews();
|
||||
}
|
||||
|
||||
// 加载网站配置
|
||||
@@ -990,6 +1068,9 @@
|
||||
document.getElementById('recent-models').innerHTML = models.length > 0
|
||||
? models.map(m => `<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"><div><span class="font-medium text-gray-800">${m.name}</span><span class="text-sm text-gray-500 ml-2">${m.organization}</span></div><div class="text-sm text-gray-400">${m.is_open_source ? '开源' : '商业'}</div></div>`).join('')
|
||||
: '<div class="text-gray-400">暂无数据</div>';
|
||||
|
||||
// 加载通知计数
|
||||
loadNotificationCounts();
|
||||
}
|
||||
|
||||
// 内置分类列表
|
||||
@@ -2993,6 +3074,232 @@
|
||||
} catch (e) { alert('导入失败: ' + e.message); }
|
||||
}
|
||||
|
||||
// ─── 通知和审核功能 ─────────────────────────────────────────────────
|
||||
|
||||
// 加载未读通知和待审核数
|
||||
async function loadNotificationCounts() {
|
||||
try {
|
||||
const [notifRes, reviewRes] = await Promise.all([
|
||||
fetch('/api/notifications/unread-count'),
|
||||
fetch('/api/reviews/count')
|
||||
]);
|
||||
const notifData = await notifRes.json();
|
||||
const reviewData = await reviewRes.json();
|
||||
|
||||
// 更新导航栏徽章
|
||||
const notifBadge = document.getElementById('navNotificationBadge');
|
||||
const reviewBadge = document.getElementById('navReviewBadge');
|
||||
|
||||
if (notifData.count > 0) {
|
||||
notifBadge.textContent = notifData.count > 99 ? '99+' : notifData.count;
|
||||
notifBadge.classList.remove('hidden');
|
||||
} else {
|
||||
notifBadge.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (reviewData.count > 0) {
|
||||
reviewBadge.textContent = reviewData.count > 99 ? '99+' : reviewData.count;
|
||||
reviewBadge.classList.remove('hidden');
|
||||
} else {
|
||||
reviewBadge.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 更新概览页的待办提醒
|
||||
const todoAlert = document.getElementById('todoAlert');
|
||||
const totalCount = notifData.count + reviewData.count;
|
||||
if (totalCount > 0) {
|
||||
todoAlert.classList.remove('hidden');
|
||||
document.getElementById('todoCount').textContent = totalCount;
|
||||
document.getElementById('todoDetail').textContent =
|
||||
`${notifData.count} 条未读通知,${reviewData.count} 条待审核产品`;
|
||||
} else {
|
||||
todoAlert.classList.add('hidden');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载通知数失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载通知列表
|
||||
async function loadNotifications() {
|
||||
try {
|
||||
const res = await fetch('/api/notifications?limit=50');
|
||||
const notifications = await res.json();
|
||||
|
||||
if (notifications.length === 0) {
|
||||
document.getElementById('notificationsList').innerHTML = `
|
||||
<div class="bg-white rounded-xl p-8 text-center text-gray-400">
|
||||
<i class="ri-notification-off-line text-4xl mb-2"></i>
|
||||
<div>暂无通知</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const levelColors = {
|
||||
info: 'bg-blue-50 border-blue-200',
|
||||
warning: 'bg-orange-50 border-orange-200',
|
||||
error: 'bg-red-50 border-red-200',
|
||||
success: 'bg-green-50 border-green-200'
|
||||
};
|
||||
const levelIcons = {
|
||||
info: 'ri-information-line text-blue-600',
|
||||
warning: 'ri-alert-line text-orange-600',
|
||||
error: 'ri-error-warning-line text-red-600',
|
||||
success: 'ri-checkbox-circle-line text-green-600'
|
||||
};
|
||||
|
||||
const html = notifications.map(n => `
|
||||
<div class="bg-white rounded-xl p-4 border ${n.read ? 'opacity-60' : ''} ${levelColors[n.level] || 'bg-gray-50 border-gray-200'}" id="notif-${n.id}">
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="${levelIcons[n.level] || 'ri-notification-line text-gray-600'} text-xl mt-0.5"></i>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-gray-800">${n.title}</div>
|
||||
<div class="text-sm text-gray-600 mt-1">${n.message}</div>
|
||||
<div class="text-xs text-gray-400 mt-2">${n.created_at}</div>
|
||||
</div>
|
||||
${!n.read ? `<button onclick="markRead('${n.id}')" class="text-xs text-indigo-600 hover:text-indigo-800">标为已读</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
document.getElementById('notificationsList').innerHTML = html;
|
||||
} catch (e) {
|
||||
document.getElementById('notificationsList').innerHTML = `
|
||||
<div class="bg-white rounded-xl p-8 text-center text-red-500">加载失败: ${e.message}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 标记单个通知已读
|
||||
async function markRead(notifId) {
|
||||
await fetch(`/api/notifications/${notifId}/read`, {method: 'POST'});
|
||||
loadNotifications();
|
||||
loadNotificationCounts();
|
||||
}
|
||||
|
||||
// 标记全部已读
|
||||
async function markAllRead() {
|
||||
await fetch('/api/notifications/read-all', {method: 'POST'});
|
||||
loadNotifications();
|
||||
loadNotificationCounts();
|
||||
}
|
||||
|
||||
// 加载审核列表
|
||||
async function loadReviews() {
|
||||
const status = document.getElementById('reviewStatusFilter').value;
|
||||
try {
|
||||
const res = await fetch(`/api/reviews?status=${status}`);
|
||||
const reviews = await res.json();
|
||||
|
||||
if (reviews.length === 0) {
|
||||
document.getElementById('reviewsTable').innerHTML = `
|
||||
<tr><td colspan="6" class="text-center text-gray-400 py-8">暂无数据</td></tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const statusLabels = {
|
||||
pending: '<span class="px-2 py-1 bg-orange-100 text-orange-700 rounded text-xs">待审核</span>',
|
||||
approved: '<span class="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">已通过</span>',
|
||||
rejected: '<span class="px-2 py-1 bg-red-100 text-red-700 rounded text-xs">已拒绝</span>'
|
||||
};
|
||||
|
||||
const html = reviews.map(r => {
|
||||
const catName = categories.find(c => c.id === r.category_id)?.name || r.category_id;
|
||||
return `
|
||||
<tr class="border-b hover:bg-gray-50">
|
||||
<td class="px-4 py-3 font-medium text-gray-800">${r.data?.name || '-'}</td>
|
||||
<td class="px-4 py-3 text-gray-600">${catName}</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
${r.source === 'api' ? '<span class="text-indigo-600">API</span>' : '<span class="text-gray-500">网页</span>'}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-gray-500">${r.created_at}</td>
|
||||
<td class="px-4 py-3">${statusLabels[r.status] || r.status}</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
${r.status === 'pending' ? `
|
||||
<button onclick="approveReview('${r.id}')" class="text-green-600 hover:text-green-800 mr-2" title="通过"><i class="ri-check-line"></i></button>
|
||||
<button onclick="rejectReview('${r.id}')" class="text-red-600 hover:text-red-800" title="拒绝"><i class="ri-close-line"></i></button>
|
||||
<button onclick="viewReviewDetail('${r.id}')" class="text-blue-600 hover:text-blue-800 ml-2" title="查看详情"><i class="ri-eye-line"></i></button>
|
||||
` : `
|
||||
<button onclick="viewReviewDetail('${r.id}')" class="text-blue-600 hover:text-blue-800" title="查看详情"><i class="ri-eye-line"></i></button>
|
||||
`}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
document.getElementById('reviewsTable').innerHTML = html;
|
||||
} catch (e) {
|
||||
document.getElementById('reviewsTable').innerHTML = `
|
||||
<tr><td colspan="6" class="text-center text-red-500 py-8">加载失败: ${e.message}</td></tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 通过审核
|
||||
async function approveReview(reviewId) {
|
||||
if (!confirm('确认通过该产品审核?')) return;
|
||||
try {
|
||||
const res = await fetch(`/api/reviews/${reviewId}/approve`, {method: 'POST'});
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
alert('操作失败: ' + data.error);
|
||||
} else {
|
||||
alert('审核通过,产品已发布!');
|
||||
loadReviews();
|
||||
loadNotificationCounts();
|
||||
loadOverview();
|
||||
}
|
||||
} catch (e) {
|
||||
alert('操作失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 拒绝审核
|
||||
async function rejectReview(reviewId) {
|
||||
const reason = prompt('请输入拒绝原因(可选):');
|
||||
if (reason === null) return; // 用户取消
|
||||
try {
|
||||
const res = await fetch(`/api/reviews/${reviewId}/reject`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({reason})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
alert('操作失败: ' + data.error);
|
||||
} else {
|
||||
alert('已拒绝该产品!');
|
||||
loadReviews();
|
||||
loadNotificationCounts();
|
||||
}
|
||||
} catch (e) {
|
||||
alert('操作失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 查看审核详情
|
||||
async function viewReviewDetail(reviewId) {
|
||||
try {
|
||||
const res = await fetch(`/api/reviews/${reviewId}`);
|
||||
const review = await res.json();
|
||||
|
||||
let html = '<div class="space-y-4">';
|
||||
html += `<div><span class="text-gray-500">状态:</span> ${review.status}</div>`;
|
||||
html += `<div><span class="text-gray-500">分类:</span> ${review.category_id}</div>`;
|
||||
html += `<div><span class="text-gray-500">来源:</span> ${review.source}</div>`;
|
||||
html += `<div><span class="text-gray-500">提交时间:</span> ${review.created_at}</div>`;
|
||||
html += '<div class="border-t pt-4"><h3 class="font-medium mb-2">产品数据:</h3>';
|
||||
html += '<pre class="bg-gray-50 p-3 rounded text-sm overflow-auto max-h-60">' + JSON.stringify(review.data, null, 2) + '</pre>';
|
||||
html += '</div></div>';
|
||||
|
||||
alert(html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim());
|
||||
} catch (e) {
|
||||
alert('加载失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user