1
This commit is contained in:
52
.env
Normal file
52
.env
Normal file
@@ -0,0 +1,52 @@
|
||||
# Django Configuration
|
||||
SECRET_KEY=django-insecure-0a1bx*8!97l^4z#ml#ufn_*9ut*)zlso$*k-g^h&(2=p@^51md
|
||||
DEBUG=True
|
||||
#ALLOWED_HOSTS=localhost,127.0.0.1,django-host,192.168.1.22
|
||||
ALLOWED_HOSTS=*
|
||||
# 切换数据源,支持sqlite/mysql
|
||||
DB_ENGINE=sqlite
|
||||
USE_REDIS_AS_DB=True
|
||||
|
||||
# MySQL Configuration (when USE_REDIS_AS_DB=False)
|
||||
DB_NAME=hertz_server
|
||||
DB_USER=root
|
||||
DB_PASSWORD=123456
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_URL=redis://127.0.0.1:6379/0
|
||||
|
||||
# CORS Configuration
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||
CORS_ALLOW_ALL_ORIGINS=True
|
||||
|
||||
|
||||
# Email Configuration
|
||||
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||||
EMAIL_HOST=smtp.qq.com
|
||||
EMAIL_PORT=465
|
||||
EMAIL_USE_SSL=True
|
||||
EMAIL_USE_TLS=False
|
||||
EMAIL_HOST_USER=your_email@qq.com
|
||||
EMAIL_HOST_PASSWORD=your_email_password
|
||||
DEFAULT_FROM_EMAIL=your_email@qq.com
|
||||
|
||||
# 注册邮箱验证码开关(0=关闭,1=开启)
|
||||
REGISTER_EMAIL_VERIFICATION=0
|
||||
|
||||
# Hertz Captcha Configuration
|
||||
HERTZ_CAPTCHA_LENGTH=4
|
||||
HERTZ_CAPTCHA_WIDTH=120
|
||||
HERTZ_CAPTCHA_HEIGHT=50
|
||||
HERTZ_CAPTCHA_FONT_SIZE=30
|
||||
HERTZ_CAPTCHA_TIMEOUT=300
|
||||
HERTZ_CAPTCHA_BACKGROUND_COLOR=#ffffff
|
||||
HERTZ_CAPTCHA_FOREGROUND_COLOR=#000000
|
||||
HERTZ_CAPTCHA_NOISE_LEVEL=0.3
|
||||
HERTZ_CAPTCHA_REDIS_KEY_PREFIX=hertz_captcha:
|
||||
|
||||
# Auth Middleware Configuration - 不需要登录验证的URL模式(支持正则表达式)
|
||||
# 格式:使用逗号分隔的正则表达式模式
|
||||
# 示例:/api/demo 表示demo接口,/api/.* 表示/api路径下的所有
|
||||
NO_AUTH_PATTERNS=^/api/auth/login/?$,^/api/auth/register/?$,^/api/auth/email/code/?$,^/api/auth/send-email-code/?$,^/api/auth/password/reset/?$,^/api/captcha/.*$,^/api/docs/.*$,^/api/redoc/.*$,^/api/schema/.*$,^/admin/.*$,^/static/.*$,^/media/.*$,^/demo/.*$,^/websocket/.*$,^/api/system/.*$,^/yolo/.*$,
|
||||
101
.gitignore
vendored
Normal file
101
.gitignore
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
# Python bytecode
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# python envs
|
||||
venv/
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Django
|
||||
*.log
|
||||
local_settings.py
|
||||
media/
|
||||
staticfiles/
|
||||
static_root/
|
||||
|
||||
|
||||
# IDE / editors
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
|
||||
# Type checking
|
||||
.mypy_cache/
|
||||
.pytype/
|
||||
.pyre/
|
||||
|
||||
# Celery
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# Sphinx docs
|
||||
docs/_build/
|
||||
|
||||
# PyInstaller
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Project-specific data/models (uploads & intermediates)
|
||||
media/models/
|
||||
media/uploads/
|
||||
media/yolo/temp/
|
||||
media/yolo/models/
|
||||
media/sklearn/models/
|
||||
media/detection/temp/
|
||||
media/detection/result/
|
||||
media/detection/original/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Yolo models
|
||||
hertz_studio_django_utils/yolo/Train/runs/
|
||||
|
||||
# 技术支持文档
|
||||
shared/
|
||||
BIN
data/db.sqlite3
Normal file
BIN
data/db.sqlite3
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_AlertLevelManagement.7z
Normal file
BIN
data/frontend_7z/admin_AlertLevelManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_AlertProcessingCenter.7z
Normal file
BIN
data/frontend_7z/admin_AlertProcessingCenter.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_ArticleManagement.7z
Normal file
BIN
data/frontend_7z/admin_ArticleManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_Dashboard.7z
Normal file
BIN
data/frontend_7z/admin_Dashboard.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_DatasetManagement.7z
Normal file
BIN
data/frontend_7z/admin_DatasetManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_DepartmentManagement.7z
Normal file
BIN
data/frontend_7z/admin_DepartmentManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_DetectionHistoryManagement.7z
Normal file
BIN
data/frontend_7z/admin_DetectionHistoryManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_LogManagement.7z
Normal file
BIN
data/frontend_7z/admin_LogManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_MenuManagement.7z
Normal file
BIN
data/frontend_7z/admin_MenuManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_ModelManagement.7z
Normal file
BIN
data/frontend_7z/admin_ModelManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_NotificationManagement.7z
Normal file
BIN
data/frontend_7z/admin_NotificationManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_Role.7z
Normal file
BIN
data/frontend_7z/admin_Role.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_UserManagement.7z
Normal file
BIN
data/frontend_7z/admin_UserManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/admin_YoloTrainManagement.7z
Normal file
BIN
data/frontend_7z/admin_YoloTrainManagement.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/user_AIChat.7z
Normal file
BIN
data/frontend_7z/user_AIChat.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/user_ArticleCenter.7z
Normal file
BIN
data/frontend_7z/user_ArticleCenter.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/user_KnowledgeBase.7z
Normal file
BIN
data/frontend_7z/user_KnowledgeBase.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/user_LiveDetection.7z
Normal file
BIN
data/frontend_7z/user_LiveDetection.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/user_Messages.7z
Normal file
BIN
data/frontend_7z/user_Messages.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/user_Profile.7z
Normal file
BIN
data/frontend_7z/user_Profile.7z
Normal file
Binary file not shown.
BIN
data/frontend_7z/user_YoloDetection.7z
Normal file
BIN
data/frontend_7z/user_YoloDetection.7z
Normal file
Binary file not shown.
246
docs/API接口文档/AI聊天模块接口文档.md
Normal file
246
docs/API接口文档/AI聊天模块接口文档.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# Hertz Studio Django AI聊天模块接口文档
|
||||
|
||||
- 基础路径: `/api/ai/`
|
||||
- 统一响应: 使用 `HertzResponse`,结构如下
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
- 路由挂载: 项目主路由通过 `path('api/ai/', include('hertz_studio_django_ai.urls'))` 挂载(`hertz_server_django/urls.py:23`)。
|
||||
- 认证说明: 所有接口需在请求头携带 `Authorization: Bearer <access_token>`(`hertz_studio_django_ai/views.py:34` 使用 `login_required`)。
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 获取聊天列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/ai/chats/`
|
||||
- 查询参数:
|
||||
- `query` 可选,按标题模糊搜索
|
||||
- `page` 可选,默认 `1`
|
||||
- `page_size` 可选,默认 `10`
|
||||
- 实现: `AIChatListView.get`(`hertz_studio_django_ai/views.py:36`)
|
||||
- 请求示例:
|
||||
```http
|
||||
GET /api/ai/chats/?query=Python&page=1&page_size=10
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"total": 25,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"chats": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Python编程问题",
|
||||
"created_at": "2024-01-15 10:30:00",
|
||||
"updated_at": "2024-01-15 10:35:00",
|
||||
"latest_message": "如何使用Django创建API接口?..."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建聊天
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/ai/chats/create/`
|
||||
- 请求体: `application/json`
|
||||
- 字段: `title` 可选,默认 `"新对话"`
|
||||
- 实现: `AIChatCreateView.post`(`hertz_studio_django_ai/views.py:100`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/ai/chats/create/
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "AI编程助手"
|
||||
}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "创建成功",
|
||||
"data": {
|
||||
"chat_id": 3,
|
||||
"title": "AI编程助手"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 聊天详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/ai/chats/{chat_id}/`
|
||||
- 路径参数: `chat_id` 聊天ID(整数)
|
||||
- 实现: `AIChatDetailView.get`(`hertz_studio_django_ai/views.py:137`)
|
||||
- 请求示例:
|
||||
```http
|
||||
GET /api/ai/chats/1/
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"title": "Python编程问题",
|
||||
"created_at": "2024-01-15 10:30:00",
|
||||
"updated_at": "2024-01-15 10:35:00",
|
||||
"messages": [
|
||||
{
|
||||
"id": 1,
|
||||
"role": "user",
|
||||
"content": "如何使用Django创建API接口?",
|
||||
"created_at": "2024-01-15 10:30:00"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"role": "assistant",
|
||||
"content": "使用Django REST Framework可以快速构建API...",
|
||||
"created_at": "2024-01-15 10:30:30"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 更新聊天
|
||||
- 方法: `PUT`
|
||||
- 路径: `/api/ai/chats/{chat_id}/update/`
|
||||
- 路径参数: `chat_id` 聊天ID(整数)
|
||||
- 请求体: `application/json`
|
||||
- 字段: `title` 新标题
|
||||
- 实现: `AIChatUpdateView.put`(`hertz_studio_django_ai/views.py:192`)
|
||||
- 请求示例:
|
||||
```http
|
||||
PUT /api/ai/chats/1/update/
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "更新后的标题"
|
||||
}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "更新成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 删除聊天(批量)
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/ai/chats/delete/`
|
||||
- 请求体: `application/json`
|
||||
- 字段: `chat_ids` 要删除的聊天ID数组(整数)
|
||||
- 实现: `AIChatDeleteView.post`(`hertz_studio_django_ai/views.py:231`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/ai/chats/delete/
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"chat_ids": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "成功删除3个聊天",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 发送消息
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/ai/chats/{chat_id}/send/`
|
||||
- 路径参数: `chat_id` 聊天ID(整数)
|
||||
- 请求体: `application/json`
|
||||
- 字段: `content` 必填,消息内容,不能为空
|
||||
- 实现: `AIChatSendMessageView.post`(`hertz_studio_django_ai/views.py:280`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/ai/chats/1/send/
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"content": "你好,请介绍一下Python的特点"
|
||||
}
|
||||
```
|
||||
- 成功返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"user_message": {
|
||||
"id": 5,
|
||||
"role": "user",
|
||||
"content": "你好,请介绍一下Python的特点",
|
||||
"created_at": "2024-01-15 11:30:00"
|
||||
},
|
||||
"ai_message": {
|
||||
"id": 6,
|
||||
"role": "assistant",
|
||||
"content": "Python是一种高级编程语言,语法简洁,生态丰富...",
|
||||
"created_at": "2024-01-15 11:30:05"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- 失败返回示例(参数验证失败):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"code": 422,
|
||||
"message": "参数验证失败",
|
||||
"data": {
|
||||
"content": ["消息内容不能为空"]
|
||||
}
|
||||
}
|
||||
```
|
||||
- 失败返回示例(聊天不存在):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"code": 404,
|
||||
"message": "聊天不存在或无权访问",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
- 失败返回示例(AI生成失败):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"code": 500,
|
||||
"message": "AI回复生成失败:服务不可用",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
## 附注
|
||||
- 列表与详情时间字段为字符串格式 `YYYY-MM-DD HH:mm:ss`(`hertz_studio_django_ai/views.py:65`、`hertz_studio_django_ai/views.py:151`)。
|
||||
- AI模型名称由配置项 `settings.AI_MODEL_NAME` 控制,默认 `deepseek-r1:1.5b`(`hertz_studio_django_ai/views.py:263`)。
|
||||
367
docs/API接口文档/Wiki模块接口文档.md
Normal file
367
docs/API接口文档/Wiki模块接口文档.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# Hertz Studio Django Wiki 接口文档
|
||||
|
||||
- 基础路径: `/api/wiki/`
|
||||
- 统一响应: 使用 `HertzResponse`,结构如下(参考 `hertz_studio_django_utils/responses/HertzResponse.py`)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
- 路由挂载: 项目主路由中已通过 `path('api/wiki/', include('hertz_studio_django_wiki.urls'))` 挂载(`hertz_server_django/urls.py:29`)。
|
||||
- 认证说明: 标注“需要登录”的接口需在请求头携带 `Authorization: Bearer <token>`(`hertz_studio_django_auth/utils/decorators/auth_decorators.py:1`)。
|
||||
|
||||
|
||||
## 一、知识分类管理
|
||||
|
||||
### (1)获取分类列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/wiki/categories/`
|
||||
- 认证: 不需要
|
||||
- 查询参数:
|
||||
- `page`: 页码,默认 `1`
|
||||
- `page_size`: 每页数量,默认 `10`
|
||||
- `name`: 分类名称关键字
|
||||
- `parent_id`: 父分类ID(`0` 表示顶级)
|
||||
- `is_active`: `true/false`
|
||||
- 实现: `wiki_category_list`(`hertz_studio_django_wiki/views.py:41`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/wiki/categories/?page=1&page_size=10&name=技术"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "技术文档",
|
||||
"description": "技术相关文档",
|
||||
"parent": null,
|
||||
"parent_name": null,
|
||||
"sort_order": 1,
|
||||
"is_active": true,
|
||||
"created_at": "2024-01-01T10:00:00Z",
|
||||
"updated_at": "2024-01-01T10:00:00Z",
|
||||
"children_count": 3,
|
||||
"articles_count": 15,
|
||||
"full_path": "技术文档"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"page_size": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (2)获取树形分类
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/wiki/categories/tree/`
|
||||
- 认证: 不需要
|
||||
- 实现: `wiki_category_tree`(`hertz_studio_django_wiki/views.py:101`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/wiki/categories/tree/"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "技术文档",
|
||||
"description": "技术相关文档",
|
||||
"sort_order": 1,
|
||||
"is_active": true,
|
||||
"articles_count": 15,
|
||||
"children": [
|
||||
{"id": 2, "name": "后端", "children": []}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### (3)创建分类
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/wiki/categories/create/`
|
||||
- 认证: 不需要
|
||||
- 请求体: `application/json`
|
||||
- 字段: `name`, `description`, `parent`, `sort_order`, `is_active`
|
||||
- 实现: `wiki_category_create`(`hertz_studio_django_wiki/views.py:136`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/wiki/categories/create/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "新分类",
|
||||
"description": "分类描述",
|
||||
"parent": null,
|
||||
"sort_order": 10,
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "知识分类创建成功",
|
||||
"data": {
|
||||
"id": 5,
|
||||
"name": "新分类",
|
||||
"description": "分类描述",
|
||||
"parent": null,
|
||||
"parent_name": null,
|
||||
"sort_order": 10,
|
||||
"is_active": true,
|
||||
"created_at": "2025-11-17T10:00:00Z",
|
||||
"updated_at": "2025-11-17T10:00:00Z",
|
||||
"children_count": 0,
|
||||
"articles_count": 0,
|
||||
"full_path": "新分类"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (4)分类详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/wiki/categories/{category_id}/`
|
||||
- 认证: 不需要
|
||||
- 实现: `wiki_category_detail`(`hertz_studio_django_wiki/views.py:178`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/wiki/categories/1/"
|
||||
```
|
||||
- 返回示例: 同“获取分类列表”中的单项结构。
|
||||
|
||||
### (5)更新分类
|
||||
- 方法: `PUT`(支持部分更新)
|
||||
- 路径: `/api/wiki/categories/{category_id}/update/`
|
||||
- 认证: 不需要
|
||||
- 请求体: `application/json`
|
||||
- 可更新字段: `name`, `description`, `parent`, `sort_order`, `is_active`
|
||||
- 实现: `wiki_category_update`(`hertz_studio_django_wiki/views.py:220`)
|
||||
- 请求示例:
|
||||
```http
|
||||
PUT /api/wiki/categories/1/update/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "更新后的分类名",
|
||||
"description": "更新后的描述",
|
||||
"sort_order": 20
|
||||
}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "知识分类更新成功", "data": {"id": 1, "name": "更新后的分类名"}}
|
||||
```
|
||||
|
||||
### (6)删除分类
|
||||
- 方法: `DELETE`
|
||||
- 路径: `/api/wiki/categories/{category_id}/delete/`
|
||||
- 认证: 不需要
|
||||
- 行为: 软删除(将 `is_active=false`);若存在子分类或文章将返回错误
|
||||
- 实现: `wiki_category_delete`(`hertz_studio_django_wiki/views.py:270`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8000/api/wiki/categories/1/delete/"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "知识分类删除成功"}
|
||||
```
|
||||
|
||||
|
||||
## 二、知识文章管理
|
||||
|
||||
### (1)获取文章列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/wiki/articles/`
|
||||
- 认证: 不需要
|
||||
- 查询参数:
|
||||
- `page`, `page_size`
|
||||
- `title`: 标题关键字
|
||||
- `category_id`: 分类ID
|
||||
- `author_id`: 作者ID
|
||||
- `status`: `draft|published|archived`
|
||||
- 实现: `wiki_article_list`(`hertz_studio_django_wiki/views.py:318`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/wiki/articles/?page=1&page_size=10&category_id=1&status=published"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 101,
|
||||
"title": "如何部署Django",
|
||||
"summary": "部署流程概览",
|
||||
"image": null,
|
||||
"category_name": "技术文档",
|
||||
"author_name": "alice",
|
||||
"status": "published",
|
||||
"status_display": "已发布",
|
||||
"view_count": 42,
|
||||
"created_at": "2025-11-01T09:00:00Z",
|
||||
"updated_at": "2025-11-10T09:00:00Z",
|
||||
"published_at": "2025-11-10T09:00:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"page_size": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (2)创建文章(需要登录)
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/wiki/articles/create/`
|
||||
- 认证: 需要登录(`Authorization: Bearer <token>`)
|
||||
- 请求体: `application/json`
|
||||
- 字段: `title`, `content`, `summary`, `image`, `category`, `status`, `tags`, `sort_order`
|
||||
- 实现: `wiki_article_create`(`hertz_studio_django_wiki/views.py:384`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/wiki/articles/create/
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "新文章",
|
||||
"content": "文章内容...",
|
||||
"summary": "文章摘要...",
|
||||
"image": null,
|
||||
"category": 1,
|
||||
"status": "draft",
|
||||
"tags": "django,部署",
|
||||
"sort_order": 10
|
||||
}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "知识文章创建成功",
|
||||
"data": {
|
||||
"id": 102,
|
||||
"title": "新文章",
|
||||
"content": "文章内容...",
|
||||
"summary": "文章摘要...",
|
||||
"image": null,
|
||||
"category": 1,
|
||||
"category_name": "技术文档",
|
||||
"author": 2,
|
||||
"author_name": "alice",
|
||||
"status": "draft",
|
||||
"status_display": "草稿",
|
||||
"tags": "django,部署",
|
||||
"tags_list": ["django", "部署"],
|
||||
"view_count": 0,
|
||||
"sort_order": 10,
|
||||
"created_at": "2025-11-17T11:00:00Z",
|
||||
"updated_at": "2025-11-17T11:00:00Z",
|
||||
"published_at": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (3)文章详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/wiki/articles/{article_id}/`
|
||||
- 认证: 不需要
|
||||
- 行为: 获取详情并增加浏览量
|
||||
- 实现: `wiki_article_detail`(`hertz_studio_django_wiki/views.py:426`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/wiki/articles/102/"
|
||||
```
|
||||
- 返回示例: 同“创建文章”中的完整字段结构,`view_count` 将递增。
|
||||
|
||||
### (4)更新文章
|
||||
- 方法: `PUT`(支持部分更新)
|
||||
- 路径: `/api/wiki/articles/{article_id}/update/`
|
||||
- 认证: 不需要
|
||||
- 请求体: `application/json`
|
||||
- 可更新字段: `title`, `content`, `summary`, `image`, `category`, `status`, `tags`, `sort_order`
|
||||
- 实现: `wiki_article_update`(`hertz_studio_django_wiki/views.py:472`)
|
||||
- 请求示例:
|
||||
```http
|
||||
PUT /api/wiki/articles/102/update/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "published",
|
||||
"summary": "更新后的摘要"
|
||||
}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "知识文章更新成功", "data": {"id": 102, "status": "published"}}
|
||||
```
|
||||
|
||||
### (5)删除文章
|
||||
- 方法: `DELETE`
|
||||
- 路径: `/api/wiki/articles/{article_id}/delete/`
|
||||
- 认证: 不需要
|
||||
- 实现: `wiki_article_delete`(`hertz_studio_django_wiki/views.py:518`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8000/api/wiki/articles/102/delete/"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "知识文章删除成功"}
|
||||
```
|
||||
|
||||
### (6)发布文章
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/wiki/articles/{article_id}/publish/`
|
||||
- 认证: 不需要
|
||||
- 实现: `wiki_article_publish`(`hertz_studio_django_wiki/views.py:561`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/wiki/articles/102/publish/"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "知识文章发布成功"}
|
||||
```
|
||||
- 失败示例(已发布再次发布):
|
||||
```json
|
||||
{"success": false, "code": 500, "message": "系统错误", "error": "文章已经是发布状态"}
|
||||
```
|
||||
|
||||
### (7)归档文章
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/wiki/articles/{article_id}/archive/`
|
||||
- 认证: 不需要
|
||||
- 实现: `wiki_article_archive`(`hertz_studio_django_wiki/views.py:609`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/wiki/articles/102/archive/"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "知识文章归档成功"}
|
||||
```
|
||||
|
||||
|
||||
## 三、备注
|
||||
- 文章状态枚举: `draft|published|archived`(`hertz_studio_django_wiki/models.py:35`)。
|
||||
- 分类软删除通过 `is_active=false` 实现;删除校验会阻止删除存在子分类或文章的分类(`views.py:270`)。
|
||||
- 文章详情接口会递增 `view_count`(`hertz_studio_django_wiki/models.py:70` 和 `views.py:431`)。
|
||||
461
docs/API接口文档/YOLO模块接口文档.md
Normal file
461
docs/API接口文档/YOLO模块接口文档.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# Hertz Studio Django YOLO 接口文档
|
||||
|
||||
- 基础路径: `/api/yolo/`
|
||||
- 统一响应: 使用 `HertzResponse` 封装,结构为:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
参考 `hertz_studio_django_utils/responses/HertzResponse.py`
|
||||
- 认证说明: 标注“需要登录”的接口需在请求头携带 `Authorization: Bearer <token>`,验证逻辑参考 `hertz_studio_django_auth/utils/decorators/auth_decorators.py`。
|
||||
|
||||
|
||||
## 一、模型上传与转换
|
||||
|
||||
### (1)上传模型(压缩包或文件夹)
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/yolo/upload/`
|
||||
- 认证: 不需要
|
||||
- 请求类型: `multipart/form-data`
|
||||
- 参数:
|
||||
- `zip_file`: ZIP 压缩包文件,与 `folder_files` 互斥
|
||||
- `folder_files[...]`: 文件夹内的多个文件(键名形如 `folder_files[path/to/file]`),与 `zip_file` 互斥
|
||||
- `name`: 模型名称(必填)
|
||||
- `version`: 模型版本(默认 `1.0`)
|
||||
- `description`: 模型描述(可选)
|
||||
- 参考实现: `views.py` 中 `model_upload`,`_handle_zip_upload`,`_handle_folder_upload`,`_validate_and_create_model`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:1018,1058,1129,1182)
|
||||
- 示例请求(ZIP 上传):
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/yolo/upload/" \
|
||||
-F "zip_file=@/path/to/yolo_model.zip" \
|
||||
-F "name=MyYolo" \
|
||||
-F "version=1.0" \
|
||||
-F "description=Demo model"
|
||||
```
|
||||
- 示例响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "模型上传成功",
|
||||
"data": {
|
||||
"id": 12,
|
||||
"name": "MyYolo",
|
||||
"version": "1.0",
|
||||
"folder_path": "/absolute/path/to/media/models/MyYolo_xxxxxxxx",
|
||||
"weights_path": "/absolute/path/to/media/models/MyYolo_xxxxxxxx/weights",
|
||||
"model_path": "/absolute/path/to/media/models/MyYolo_xxxxxxxx/weights/best.pt",
|
||||
"best_model_path": "/absolute/path/to/media/models/.../weights/best.pt",
|
||||
"last_model_path": "/absolute/path/to/media/models/.../weights/last.pt",
|
||||
"categories": {"0":"person","1":"bicycle"},
|
||||
"created_at": "2025-10-22T03:20:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (2)上传 .pt 并转换为 ONNX
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/yolo/onnx/upload/`
|
||||
- 认证: 不需要
|
||||
- 请求类型: `multipart/form-data`
|
||||
- 参数:
|
||||
- `file`: `.pt` 模型文件(必填)
|
||||
- `imgsz`: 导出图像尺寸(如 `640` 或 `640,640`,默认 `640`)
|
||||
- `opset`: ONNX opset 版本(默认 `12`)
|
||||
- `simplify`: 是否简化 ONNX(`true/false`,默认 `false`)
|
||||
- 参考实现: `views.py` 中 `upload_pt_convert_onnx`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:877)
|
||||
- 示例请求:
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/yolo/onnx/upload/" \
|
||||
-F "file=@/path/to/best.pt" \
|
||||
-F "imgsz=640" \
|
||||
-F "opset=12" \
|
||||
-F "simplify=true"
|
||||
```
|
||||
- 示例响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "ONNX 导出成功",
|
||||
"data": {
|
||||
"onnx_relative_path": "yolo/ONNX/best_xxxxxxxx.onnx",
|
||||
"download_url": "http://localhost:8000/media/yolo/ONNX/best_xxxxxxxx.onnx",
|
||||
"labels_relative_path": "yolo/ONNX/best_xxxxxxxx.labels.json",
|
||||
"labels_download_url": "http://localhost:8000/media/yolo/ONNX/best_xxxxxxxx.labels.json"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 二、模型管理
|
||||
|
||||
### (1)获取模型列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/models/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `model_list`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:101)
|
||||
- 示例响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "获取模型列表成功",
|
||||
"data": [
|
||||
{"id": 1, "name": "ModelA", "version": "1.0", "is_enabled": true, "created_at": "2025-10-22T03:20:00Z"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### (2)获取模型详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/models/{pk}/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `model_detail`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:121)
|
||||
- 示例响应(节选):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "获取模型详情成功",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "ModelA",
|
||||
"version": "1.0",
|
||||
"model_file": "/media/yolo/models/modela.pt",
|
||||
"model_folder_path": "/abs/path/models/modela_...",
|
||||
"model_path": "/abs/path/models/modela_/weights/best.pt",
|
||||
"weights_folder_path": "/abs/path/models/modela_/weights",
|
||||
"categories": {"0": "person"},
|
||||
"is_enabled": true,
|
||||
"description": "...",
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (2)更新模型
|
||||
- 方法: `PUT` 或 `PATCH`
|
||||
- 路径: `/api/yolo/models/{pk}/update/`
|
||||
- 认证: 不需要
|
||||
- 请求类型: `application/json` 或 `multipart/form-data`
|
||||
- 可更新字段: `description`, `is_enabled`,(如上传 `model_file` 必须为 `.pt`)
|
||||
- 参考实现: `model_update`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:134)
|
||||
- 示例请求(PATCH):
|
||||
```http
|
||||
PATCH /api/yolo/models/1/update/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"description": "更新描述",
|
||||
"is_enabled": true
|
||||
}
|
||||
```
|
||||
- 示例响应:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "模型更新成功", "data": {"id": 1, "name": "ModelA", "is_enabled": true}}
|
||||
```
|
||||
|
||||
### (3)删除模型
|
||||
- 方法: `DELETE`
|
||||
- 路径: `/api/yolo/models/{pk}/delete/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `model_delete`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:152)
|
||||
- 示例响应:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "模型删除成功"}
|
||||
```
|
||||
|
||||
### (4)启用指定模型
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/yolo/models/{pk}/enable/`
|
||||
- 认证: 不需要
|
||||
- 行为: 先禁用其他模型,再启用当前模型
|
||||
- 参考实现: `model_enable`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:165)
|
||||
- 示例响应:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "模型 ModelA 已启用", "data": {"id": 1, "is_enabled": true}}
|
||||
```
|
||||
|
||||
### (5)获取当前启用的模型
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/models/enabled/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `model_enabled`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:186)
|
||||
- 示例响应:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "获取启用模型成功", "data": {"id": 1, "name": "ModelA"}}
|
||||
```
|
||||
|
||||
### (6)创建模型(占位)
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/yolo/models/create/`
|
||||
- 说明: 返回 405,提示使用 `/api/yolo/upload/`
|
||||
- 参考实现: `model_create`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:114)
|
||||
|
||||
|
||||
## 三、模型类别管理
|
||||
|
||||
> 提示:类别通常随模型上传自动导入。手动创建/删除不推荐,仅保留接口。
|
||||
|
||||
### (1)获取类别列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/categories/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `category_list`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:1301)
|
||||
|
||||
### (2)获取类别详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/categories/{pk}/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `category_detail`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:1329)
|
||||
|
||||
### (3)更新类别
|
||||
- 方法: `PUT` 或 `PATCH`
|
||||
- 路径: `/api/yolo/categories/{pk}/update/`
|
||||
- 认证: 不需要
|
||||
- 请求类型: `application/json`
|
||||
- 可更新字段: `alias`, `alert_level`(`high|medium|low|none`), `is_active`
|
||||
- 参考实现: `category_update`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:1342)
|
||||
- 示例请求(PATCH):
|
||||
```http
|
||||
PATCH /api/yolo/categories/10/update/
|
||||
Content-Type: application/json
|
||||
|
||||
{"alias": "行人", "alert_level": "high", "is_active": true}
|
||||
```
|
||||
- 示例响应:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "更新类别成功", "data": {"id": 10, "alias": "行人", "alert_level": "high"}}
|
||||
```
|
||||
|
||||
### (4)切换类别启用状态
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/yolo/categories/{pk}/toggle-status/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `category_toggle_status`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:1385)
|
||||
- 示例响应:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "类别 'person' 启用成功", "data": {"is_active": true}}
|
||||
```
|
||||
|
||||
### (5)获取启用的类别列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/categories/active/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `category_active_list`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:1405)
|
||||
|
||||
### (6)创建类别(不推荐)
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/yolo/categories/create/`
|
||||
- 认证: 不需要
|
||||
- 请求类型: `application/json`
|
||||
- 参考实现: `category_create`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:1312)
|
||||
|
||||
### (7)删除类别(不推荐)
|
||||
- 方法: `DELETE`
|
||||
- 路径: `/api/yolo/categories/{pk}/delete/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `category_delete`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:1362)
|
||||
|
||||
|
||||
## 四、目标检测
|
||||
|
||||
### 执行检测
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/yolo/detect/`
|
||||
- 认证: 需要登录(`Authorization: Bearer <token>`)
|
||||
- 请求类型: `multipart/form-data`
|
||||
- 参数:
|
||||
- `file`: 要检测的图片或视频文件(支持图片:`.jpg,.jpeg,.png,.bmp,.tiff,.webp`;视频:`.mp4,.avi,.mov,.mkv,.wmv,.flv`)
|
||||
- `model_id`: 指定模型ID(可选,未提供则使用当前启用模型)
|
||||
- `confidence_threshold`: 置信度阈值(默认 `0.5`,范围 `0.1-1.0`)
|
||||
- 参考实现: `yolo_detection`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:446)
|
||||
- 示例请求:
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/yolo/detect/" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-F "file=@/path/to/image.jpg" \
|
||||
-F "model_id=1" \
|
||||
-F "confidence_threshold=0.5"
|
||||
```
|
||||
- 示例响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "检测完成",
|
||||
"data": {
|
||||
"detection_id": 1001,
|
||||
"result_file_url": "/media/detection/result/result_xxx.jpg",
|
||||
"original_file_url": "/media/detection/original/uuid_image.jpg",
|
||||
"object_count": 3,
|
||||
"detected_categories": ["person"],
|
||||
"confidence_scores": [0.91, 0.87, 0.79],
|
||||
"avg_confidence": 0.8567,
|
||||
"processing_time": 0.43,
|
||||
"model_used": "ModelA 1.0",
|
||||
"confidence_threshold": 0.5,
|
||||
"user_id": 2,
|
||||
"user_name": "alice",
|
||||
"alert_level": "medium"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 五、检测记录
|
||||
|
||||
### (1)获取检测记录列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/detections/`
|
||||
- 认证: 不需要
|
||||
- 查询参数:
|
||||
- `type`: `image` 或 `video`
|
||||
- `model_id`: 模型ID
|
||||
- `user_id`: 用户ID
|
||||
- 参考实现: `detection_list`(d:\All_template\yolo\hertz_studio_django_yolo\views.py:204)
|
||||
- 示例响应(节选):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "获取检测记录列表成功",
|
||||
"data": [
|
||||
{
|
||||
"id": 1001,
|
||||
"original_file": "/media/detection/original/uuid_image.jpg",
|
||||
"result_file": "/media/detection/result/result_xxx.jpg",
|
||||
"original_filename": "uuid_image.jpg",
|
||||
"result_filename": "result_xxx.jpg",
|
||||
"detection_type": "image",
|
||||
"model_name": "ModelA 1.0",
|
||||
"model_info": {"id":1, "name":"ModelA", "version":"1.0"},
|
||||
"object_count": 3,
|
||||
"detected_categories": ["person"],
|
||||
"confidence_threshold": 0.5,
|
||||
"confidence_scores": [0.91, 0.87, 0.79],
|
||||
"avg_confidence": 0.8567,
|
||||
"processing_time": 0.43,
|
||||
"created_at": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### (2)获取指定用户的检测记录
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/detections/{user_id}/user/`
|
||||
- 认证: 不需要
|
||||
- 查询参数同上
|
||||
- 参考实现: `user_detection_records`(d:\AllTemplate\yolo\hertz_studio_django_yolo\views.py:231)
|
||||
|
||||
### (3)获取检测记录详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/detections/{pk}/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `detection_detail`(d:\AllTemplate\yolo\hertz_studio_django_yolo\views.py:253)
|
||||
|
||||
### (4)删除检测记录
|
||||
- 方法: `DELETE`
|
||||
- 路径: `/api/yolo/detections/{pk}/delete/`
|
||||
- 认证: 不需要
|
||||
- 行为: 同时删除其关联的原始文件、结果文件及关联的告警
|
||||
- 参考实现: `detection_delete`(d:\AllTemplate\yolo\hertz_studio_django_yolo\views.py:265)
|
||||
- 示例响应:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "检测记录删除成功"}
|
||||
```
|
||||
|
||||
### (5)批量删除检测记录
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/yolo/detections/batch-delete/`
|
||||
- 认证: 不需要
|
||||
- 请求类型: `application/json`
|
||||
- 请求体:
|
||||
```json
|
||||
{"ids": [1001, 1002, 1003]}
|
||||
```
|
||||
- 参考实现: `detection_batch_delete`(d:\AllTemplate\yolo\hertz_studio_django_yolo\views.py:299)
|
||||
- 示例响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "成功删除 3 条检测记录",
|
||||
"data": {
|
||||
"deleted_count": 3,
|
||||
"found_ids": ["1001","1002","1003"],
|
||||
"not_found_ids": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (6)检测统计
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/stats/`
|
||||
- 认证: 不需要
|
||||
- 参考实现: `detection_stats`(d:\AllTemplate\yolo\hertz_studio_django_yolo\views.py:840)
|
||||
|
||||
|
||||
## 六、告警记录
|
||||
|
||||
### (1)获取告警记录列表(管理员)
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/alerts/`
|
||||
- 认证: 不需要
|
||||
- 查询参数:
|
||||
- `status`: 默认 `pending`;传 `all` 表示不过滤
|
||||
- `level`: 告警等级(`high|medium|low|none`)
|
||||
- `user_id`: 用户ID
|
||||
- `alter_category`: 告警类别关键字(注意字段名为 `alter_category`)
|
||||
- 参考实现: `alert_list`(d:\AllTemplate\yolo\hertz_studio_django_yolo\views.py:358)
|
||||
|
||||
### (2)获取用户的告警记录
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/yolo/users/{user_id}/alerts/`
|
||||
- 认证: 需要登录(仅本人或管理员可查)
|
||||
- 查询参数:
|
||||
- `status`: `pending|is_confirm|false_positive|all`
|
||||
- `level`: `high|medium|low|none`
|
||||
- `category`: 类别关键字
|
||||
- 参考实现: `user_alert_records`(d:\AllTemplate\yolo\hertz_studio_django_yolo\views.py:391)
|
||||
|
||||
### (3)更新告警状态
|
||||
- 方法: `PUT` 或 `PATCH`
|
||||
- 路径: `/api/yolo/alerts/{pk}/update-status/`
|
||||
- 认证: 不需要
|
||||
- 请求类型: `application/json`
|
||||
- 请求体:
|
||||
```json
|
||||
{"status": "is_confirm"}
|
||||
```
|
||||
- 可选值: `pending`, `is_confirm`, `false_positive`
|
||||
- 参考实现: `alert_update_status`(d:\AllTemplate\yolo\hertz_studio_django_yolo\views.py:426)
|
||||
- 示例响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "更新告警状态成功",
|
||||
"data": {
|
||||
"id": 555,
|
||||
"status": "is_confirm",
|
||||
"alert_level": "medium",
|
||||
"alert_category": "person",
|
||||
"alert_level_display": "中"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 七、备注
|
||||
- 所有文件型字段响应通常包含可直接访问的媒体 URL,媒体服务由 `MEDIA_URL=/media/` 提供。
|
||||
- 分类的告警等级枚举参考 `ModelCategory.ALERT_LEVELS` 与 `Alert.ALERT_LEVELS`(d:\AllTemplate\yolo\hertz_studio_django_yolo\models.py:118,153)。
|
||||
- 检测请求的文件大小限制:图片 ≤ 50MB,视频 ≤ 500MB(d:\AllTemplate\yolo\hertz_studio_django_yolo\serializers.py:99)。
|
||||
|
||||
123
docs/API接口文档/代码生成模块接口文档.md
Normal file
123
docs/API接口文档/代码生成模块接口文档.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Hertz Studio Django 代码生成模块接口文档
|
||||
|
||||
- 基础路径: `/api/codegen/`
|
||||
- 统一响应: 使用 `HertzResponse`,结构如下(参考 `hertz_studio_django_utils/responses/HertzResponse.py`)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
- 路由挂载: 项目主路由中已通过 `path('api/codegen/', include(('hertz_studio_django_codegen.urls', 'hertz_studio_django_codegen'), namespace='codegen'))` 挂载(`hertz_server_django/urls.py:51`)。
|
||||
- 认证说明: 接口需要登录,需在请求头携带 `Authorization: Bearer <token>`(`hertz_studio_django_codegen/views/simple_generator_views.py:136`)。
|
||||
|
||||
|
||||
## 一、简化代码生成
|
||||
|
||||
### 生成应用代码与菜单配置
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/codegen/simple/generate/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `simple_code_generate`(`hertz_studio_django_codegen/views/simple_generator_views.py:136`)
|
||||
- 请求体: `application/json`
|
||||
- 字段:
|
||||
- `module_name` 字符串,模块中文名,例如 `产品管理`
|
||||
- `model_name` 字符串,模型英文名,例如 `Product`
|
||||
- `app_name` 字符串,Django 应用名称,例如 `hertz_studio_django_product`
|
||||
- `fields` 数组,字段定义列表,每项包含:`name`、`type`、`verbose_name`、`help_text`、`required`、`max_length`、`choices`
|
||||
- `operations` 数组,支持的操作,默认 `['list','create','update','delete']`
|
||||
- `menu_config` 对象,菜单配置:`enabled`、`parent_code`、`prefix`、`sort_order`、`icon`
|
||||
- `generate_app` 布尔,是否生成应用代码,默认 `true`
|
||||
- `generate_menu` 布尔,是否生成菜单配置,默认 `true`
|
||||
- 请求示例(同时生成应用与菜单):
|
||||
```http
|
||||
POST /api/codegen/simple/generate/
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"module_name": "产品管理",
|
||||
"model_name": "Product",
|
||||
"app_name": "hertz_studio_django_product",
|
||||
"fields": [
|
||||
{"name": "name", "type": "CharField", "verbose_name": "产品名称", "required": true, "max_length": 100},
|
||||
{"name": "price", "type": "FloatField", "verbose_name": "价格", "required": true},
|
||||
{"name": "status", "type": "IntegerField", "verbose_name": "状态", "choices": [[0,"下线"],[1,"上线"]]}
|
||||
],
|
||||
"operations": ["list","create","update","delete"],
|
||||
"menu_config": {"enabled": true, "parent_code": "system", "prefix": "product", "sort_order": 10, "icon": "box"},
|
||||
"generate_app": true,
|
||||
"generate_menu": true
|
||||
}
|
||||
```
|
||||
- 返回示例(成功):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "成功生成产品管理模块代码和菜单配置",
|
||||
"data": {
|
||||
"generated_files": {
|
||||
"hertz_studio_django_product/models.py": "...",
|
||||
"hertz_studio_django_product/serializers.py": "...",
|
||||
"hertz_studio_django_product/views.py": "...",
|
||||
"hertz_studio_django_product/urls.py": "...",
|
||||
"hertz_studio_django_product/apps.py": "..."
|
||||
},
|
||||
"menu_configs": [
|
||||
{"code": "product", "name": "产品管理", "type": "menu", "sort_order": 10},
|
||||
{"code": "product:list", "name": "产品列表", "type": "permission", "sort_order": 11}
|
||||
],
|
||||
"app_path": "hertz_studio_django_product",
|
||||
"menu_file": "d:/All_template/yolo/pending_menus_product.py"
|
||||
}
|
||||
}
|
||||
```
|
||||
- 请求示例(仅生成菜单配置):
|
||||
```http
|
||||
POST /api/codegen/simple/generate/
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"module_name": "库存管理",
|
||||
"model_name": "Inventory",
|
||||
"app_name": "hertz_studio_django_inventory",
|
||||
"fields": [],
|
||||
"operations": ["list"],
|
||||
"menu_config": {"enabled": true, "parent_code": "system", "prefix": "inventory"},
|
||||
"generate_app": false,
|
||||
"generate_menu": true
|
||||
}
|
||||
```
|
||||
- 返回示例(仅菜单):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "成功生成库存管理模块代码和菜单配置",
|
||||
"data": {
|
||||
"generated_files": {},
|
||||
"menu_configs": [{"code": "inventory", "name": "库存管理", "type": "menu"}],
|
||||
"app_path": "",
|
||||
"menu_file": "d:/All_template/yolo/pending_menus_inventory.py"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 二、错误响应示例
|
||||
- 缺少必填参数:
|
||||
```json
|
||||
{"success": false, "code": 422, "message": "缺少必填参数: fields"}
|
||||
```
|
||||
- 请求体格式错误(非JSON):
|
||||
```json
|
||||
{"success": false, "code": 422, "message": "请求参数格式错误,请使用JSON格式"}
|
||||
```
|
||||
- 生成失败(异常信息):
|
||||
```json
|
||||
{"success": false, "code": 500, "message": "代码生成失败: <错误信息>"}
|
||||
```
|
||||
121
docs/API接口文档/日志模块接口文档.md
Normal file
121
docs/API接口文档/日志模块接口文档.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Hertz Studio Django 日志模块接口文档
|
||||
|
||||
- 基础路径: `/api/log/`
|
||||
- 统一响应: 使用 `HertzResponse`,结构如下(参考 `hertz_studio_django_utils/responses/HertzResponse.py`)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
- 路由挂载: 项目主路由中已通过 `path('api/log/', include('hertz_studio_django_log.urls'))` 挂载(`hertz_server_django/urls.py:46`)。
|
||||
- 认证与权限: 接口需要登录并具备相应权限,其中列表接口需 `system:log:list`,详情接口需 `system:log:query`(`hertz_studio_django_log/views/log_views.py:18`, `225`)。
|
||||
|
||||
|
||||
## 一、操作日志列表
|
||||
|
||||
### 获取操作日志列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/log/list/`
|
||||
- 权限: 仅管理员(`system:log:list`)
|
||||
- 查询参数:
|
||||
- `user_id`: 用户ID
|
||||
- `username`: 用户名(模糊匹配)
|
||||
- `action_type`: 操作类型(`create|update|delete|view|list|login|logout|export|import|other`)
|
||||
- `module`: 操作模块(模糊匹配)
|
||||
- `status`: 操作状态(`0|1`)
|
||||
- `ip_address`: IP地址
|
||||
- `start_date`: 开始时间(ISO日期时间)
|
||||
- `end_date`: 结束时间(ISO日期时间)
|
||||
- `page`: 页码,默认 `1`
|
||||
- `page_size`: 每页数量,默认 `20`
|
||||
- 实现: `operation_log_list`(`hertz_studio_django_log/views/log_views.py:117`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/log/list/?username=admin&action_type=login&page=1&page_size=10" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例(节选):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "获取操作日志列表成功",
|
||||
"data": {
|
||||
"logs": [
|
||||
{
|
||||
"log_id": 1001,
|
||||
"username": "admin",
|
||||
"action_type": "login",
|
||||
"action_type_display": "登录",
|
||||
"module": "认证",
|
||||
"description": "用户登录",
|
||||
"ip_address": "127.0.0.1",
|
||||
"response_status": 200,
|
||||
"status": 1,
|
||||
"status_display": "成功",
|
||||
"is_success": true,
|
||||
"created_at": "2025-11-17T08:30:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total_count": 1,
|
||||
"total_pages": 1,
|
||||
"has_next": false,
|
||||
"has_previous": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 二、操作日志详情
|
||||
|
||||
### 获取操作日志详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/log/detail/{log_id}/`
|
||||
- 权限: 仅管理员(`system:log:query`)
|
||||
- 路径参数: `log_id` 日志ID
|
||||
- 实现: `operation_log_detail`(`hertz_studio_django_log/views/log_views.py:259`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/log/detail/1001/" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例(节选):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "获取操作日志详情成功",
|
||||
"data": {
|
||||
"log_id": 1001,
|
||||
"user": 1,
|
||||
"username": "admin",
|
||||
"action_type": "login",
|
||||
"action_type_display": "登录",
|
||||
"module": "认证",
|
||||
"description": "用户登录",
|
||||
"target_model": null,
|
||||
"target_id": null,
|
||||
"ip_address": "127.0.0.1",
|
||||
"user_agent": "Mozilla/5.0",
|
||||
"request_data": {"username": "admin"},
|
||||
"formatted_request_data": "{\n \"username\": \"admin\"\n}",
|
||||
"response_status": 200,
|
||||
"status": 1,
|
||||
"status_display": "成功",
|
||||
"is_success": true,
|
||||
"created_at": "2025-11-17T08:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 三、备注
|
||||
- 列表返回采用 `OperationLogListSerializer` 字段子集以简化展示。
|
||||
- 详情返回使用 `OperationLogSerializer`,包含格式化的请求数据与成功判断等辅助字段。
|
||||
302
docs/API接口文档/系统监控模块接口文档.md
Normal file
302
docs/API接口文档/系统监控模块接口文档.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# Hertz Studio Django 系统监控模块接口文档
|
||||
|
||||
- 基础路径: `/api/system/`
|
||||
- 统一响应: 使用 `HertzResponse`,结构如下(参考 `hertz_studio_django_utils/responses/HertzResponse.py`)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
- 路由挂载: 项目主路由中已通过 `path('api/system/', include('hertz_studio_django_system_monitor.urls'))` 挂载(`hertz_server_django/urls.py:23`)。
|
||||
- 认证说明: 所有接口均需要登录,需在请求头携带 `Authorization: Bearer <token>`(`hertz_studio_django_auth/utils/decorators/auth_decorators.py:1`)。
|
||||
|
||||
|
||||
## 一、系统信息
|
||||
|
||||
### 获取系统信息
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/system/system/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `SystemInfoView.get`(`hertz_studio_django_system_monitor/views.py:63`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/system/system/" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"hostname": "DESKTOP-ABC123",
|
||||
"platform": "Windows-10-10.0.19041-SP0",
|
||||
"architecture": "AMD64",
|
||||
"boot_time": "2025-11-16T08:30:00Z",
|
||||
"uptime": "2 days, 14:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 二、CPU 信息
|
||||
|
||||
### 获取CPU信息
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/system/cpu/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `CPUInfoView.get`(`hertz_studio_django_system_monitor/views.py:63`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/system/cpu/" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"cpu_count": 8,
|
||||
"cpu_percent": 25.6,
|
||||
"cpu_freq": {"current": 2400.0, "min": 800.0, "max": 3600.0},
|
||||
"load_avg": [1.2, 1.5, 1.8]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 三、内存信息
|
||||
|
||||
### 获取内存信息
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/system/memory/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `MemoryInfoView.get`(`hertz_studio_django_system_monitor/views.py:94`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/system/memory/" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"total": 17179869184,
|
||||
"available": 8589934592,
|
||||
"used": 8589934592,
|
||||
"percent": 50.0,
|
||||
"free": 8589934592
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 四、磁盘信息
|
||||
|
||||
### 获取磁盘信息
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/system/disks/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `DiskInfoView.get`(`hertz_studio_django_system_monitor/views.py:127`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/system/disks/" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": [
|
||||
{
|
||||
"device": "C:\\",
|
||||
"mountpoint": "C:\\",
|
||||
"fstype": "NTFS",
|
||||
"total": 1073741824000,
|
||||
"used": 536870912000,
|
||||
"free": 536870912000,
|
||||
"percent": 50.0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 五、网络信息
|
||||
|
||||
### 获取网络信息
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/system/network/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `NetworkInfoView.get`(`hertz_studio_django_system_monitor/views.py:170`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/system/network/" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": [
|
||||
{
|
||||
"interface": "以太网",
|
||||
"bytes_sent": 1048576000,
|
||||
"bytes_recv": 2097152000,
|
||||
"packets_sent": 1000000,
|
||||
"packets_recv": 1500000
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 六、进程信息
|
||||
|
||||
### 获取进程信息
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/system/processes/`
|
||||
- 认证: 需要登录
|
||||
- 查询参数:
|
||||
- `limit`: 返回条数,默认 `20`
|
||||
- `sort_by`: 排序字段,默认 `cpu_percent`(可选 `cpu_percent|memory_percent|create_time`)
|
||||
- 实现: `ProcessInfoView.get`(`hertz_studio_django_system_monitor/views.py:204`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/system/processes/?limit=10&sort_by=cpu_percent" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": [
|
||||
{
|
||||
"pid": 1234,
|
||||
"name": "python.exe",
|
||||
"status": "running",
|
||||
"cpu_percent": 15.6,
|
||||
"memory_percent": 8.2,
|
||||
"memory_info": {"rss": 134217728, "vms": 268435456},
|
||||
"create_time": "2025-11-16T10:30:00Z",
|
||||
"cmdline": ["python", "manage.py", "runserver"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 七、GPU 信息
|
||||
|
||||
### 获取GPU信息
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/system/gpu/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `GPUInfoView.get`(`hertz_studio_django_system_monitor/views.py:259`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/system/gpu/" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例(有GPU设备):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"gpu_available": true,
|
||||
"gpu_info": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "NVIDIA GeForce RTX 3080",
|
||||
"load": 45.6,
|
||||
"memory_total": 10240,
|
||||
"memory_used": 4096,
|
||||
"memory_util": 40.0,
|
||||
"temperature": 65
|
||||
}
|
||||
],
|
||||
"timestamp": "2025-11-16 18:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
- 返回示例(无GPU设备):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"gpu_available": false,
|
||||
"message": "未检测到GPU设备",
|
||||
"timestamp": "2025-11-16 18:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
- 返回示例(GPU库不可用):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"gpu_available": false,
|
||||
"message": "GPU监控不可用,请安装GPUtil库",
|
||||
"timestamp": "2025-11-16 18:30:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 八、综合监控
|
||||
|
||||
### 获取系统监测综合信息
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/system/monitor/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `SystemMonitorView.get`(`hertz_studio_django_system_monitor/views.py:325`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/system/monitor/" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例(节选):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"system": {"hostname": "DESKTOP-ABC123", "platform": "Windows-10-10.0.19041-SP0", "architecture": "AMD64", "boot_time": "2025-11-16T08:30:00Z", "uptime": "2 days, 14:30:00"},
|
||||
"cpu": {"cpu_count": 8, "cpu_percent": 25.6, "cpu_freq": {"current": 2400.0, "min": 800.0, "max": 3600.0}, "load_avg": [1.2, 1.5, 1.8]},
|
||||
"memory": {"total": 17179869184, "available": 8589934592, "used": 8589934592, "percent": 50.0, "free": 8589934592},
|
||||
"disks": [{"device": "C:\\", "mountpoint": "C:\\", "fstype": "NTFS", "total": 1073741824000, "used": 536870912000, "free": 536870912000, "percent": 50.0}],
|
||||
"network": [{"interface": "以太网", "bytes_sent": 1048576000, "bytes_recv": 2097152000, "packets_sent": 1000000, "packets_recv": 1500000}],
|
||||
"processes": [{"pid": 1234, "name": "python.exe", "status": "running", "cpu_percent": 15.6, "memory_percent": 8.2, "memory_info": {"rss": 134217728, "vms": 268435456}, "create_time": "2025-11-16T10:30:00Z", "cmdline": ["python", "manage.py", "runserver"]}],
|
||||
"gpus": [{"id": 0, "name": "NVIDIA GeForce RTX 3080", "load": 45.6, "memory_total": 10240, "memory_used": 4096, "memory_util": 40.0, "temperature": 65}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 九、错误响应示例
|
||||
- 通用错误格式:
|
||||
```json
|
||||
{"success": false, "code": 401, "message": "未授权访问"}
|
||||
```
|
||||
452
docs/API接口文档/认证模块接口文档.md
Normal file
452
docs/API接口文档/认证模块接口文档.md
Normal file
@@ -0,0 +1,452 @@
|
||||
# Hertz Studio Django 认证模块接口文档
|
||||
|
||||
- 基础路径: `/api/`
|
||||
- 统一响应: 使用 `HertzResponse`,结构如下(参考 `hertz_studio_django_utils/responses/HertzResponse.py`)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
- 路由挂载: 项目主路由中已通过 `path('api/', include('hertz_studio_django_auth.urls'))` 挂载(`hertz_server_django/urls.py:31`)。
|
||||
- 认证说明:
|
||||
- 登录、注册、发送邮箱验证码、重置密码:不需要登录
|
||||
- 其他接口需要在请求头携带 `Authorization: Bearer <access_token>`(`hertz_studio_django_auth/utils/decorators/auth_decorators.py:24`)。
|
||||
- 路由前缀说明:
|
||||
- 认证相关接口前缀为 `/api/auth/`
|
||||
- 管理接口(用户/角色/菜单/部门)前缀为 `/api/`
|
||||
|
||||
|
||||
## 一、用户认证
|
||||
|
||||
### (1)用户登录
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/auth/login/`
|
||||
- 认证: 不需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `username`, `password`, `captcha_code`, `captcha_key`
|
||||
- 实现: `user_login`(`hertz_studio_django_auth/views/auth_views.py:132`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/auth/login/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "Passw0rd!",
|
||||
"captcha_code": "A1B2",
|
||||
"captcha_key": "c1a2b3c4-d5e6-7890-abcd-ef1234567890"
|
||||
}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"access_token": "eyJhbGci...",
|
||||
"refresh_token": "eyJhbGci...",
|
||||
"user_info": {
|
||||
"user_id": 1,
|
||||
"username": "admin",
|
||||
"email": "admin@example.com",
|
||||
"phone": "13800000000",
|
||||
"real_name": "管理员",
|
||||
"avatar": null,
|
||||
"roles": [{"role_id": 1, "role_name": "管理员", "role_code": "admin"}],
|
||||
"permissions": ["system:user:list", "system:role:list"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (2)用户注册
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/auth/register/`
|
||||
- 认证: 不需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `username`, `password`, `confirm_password`, `email`, `phone`, `real_name`, `email_code?`
|
||||
- 实现: `user_register`(`hertz_studio_django_auth/views/auth_views.py:131`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/auth/register/
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "newuser",
|
||||
"password": "Passw0rd!",
|
||||
"confirm_password": "Passw0rd!",
|
||||
"email": "new@example.com",
|
||||
"phone": "13800000001",
|
||||
"real_name": "新用户",
|
||||
"email_code": "123456"
|
||||
}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "注册成功", "data": {"user_id": 12, "username": "newuser"}}
|
||||
```
|
||||
|
||||
### (3)用户登出
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/auth/logout/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `user_logout`(`hertz_studio_django_auth/views/auth_views.py:184`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/auth/logout/" -H "Authorization: Bearer <access_token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "登出成功"}
|
||||
```
|
||||
|
||||
### (4)修改密码
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/auth/password/change/`
|
||||
- 认证: 需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `old_password`, `new_password`, `confirm_password`
|
||||
- 实现: `change_password`(`hertz_studio_django_auth/views/auth_views.py:214`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/auth/password/change/
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"old_password": "Passw0rd!", "new_password": "N3wPass!", "confirm_password": "N3wPass!"}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "密码修改成功"}
|
||||
```
|
||||
|
||||
### (5)重置密码
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/auth/password/reset/`
|
||||
- 认证: 不需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `email`, `email_code`, `new_password`, `confirm_password`
|
||||
- 实现: `reset_password`(`hertz_studio_django_auth/views/auth_views.py:259`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/auth/password/reset/
|
||||
Content-Type: application/json
|
||||
|
||||
{"email": "user@example.com", "email_code": "654321", "new_password": "N3wPass!", "confirm_password": "N3wPass!"}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "密码重置成功"}
|
||||
```
|
||||
|
||||
### (6)获取当前用户信息
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/auth/user/info/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `get_user_info`(`hertz_studio_django_auth/views/auth_views.py:310`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/auth/user/info/" -H "Authorization: Bearer <access_token>"
|
||||
```
|
||||
- 返回示例(节选):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"user_id": 1,
|
||||
"username": "admin",
|
||||
"email": "admin@example.com",
|
||||
"phone": "13800000000",
|
||||
"real_name": "管理员",
|
||||
"department_id": 2,
|
||||
"department_name": "技术部",
|
||||
"roles": [{"role_id": 1, "role_name": "管理员", "role_code": "admin"}],
|
||||
"last_login_time": "2025-11-17T09:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (7)更新当前用户信息
|
||||
- 方法: `PUT`
|
||||
- 路径: `/api/auth/user/info/update/`
|
||||
- 认证: 需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `email`, `phone`, `real_name`, `avatar`, `gender`, `birthday`
|
||||
- 实现: `update_user_info`(`hertz_studio_django_auth/views/auth_views.py:339`)
|
||||
- 请求示例:
|
||||
```http
|
||||
PUT /api/auth/user/info/update/
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"real_name": "张三", "phone": "13800000002"}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "用户信息更新成功", "data": {"real_name": "张三", "phone": "13800000002"}}
|
||||
```
|
||||
|
||||
### (8)获取用户菜单
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/auth/user/menus/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `get_user_menus`(`hertz_studio_django_auth/views/auth_views.py:368`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/auth/user/menus/" -H "Authorization: Bearer <access_token>"
|
||||
```
|
||||
- 返回示例(树形结构):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": [
|
||||
{"menu_id": 1, "menu_name": "系统管理", "menu_code": "system", "menu_type": 1, "path": "/system", "children": [
|
||||
{"menu_id": 2, "menu_name": "用户管理", "menu_code": "system:user", "menu_type": 2, "path": "/system/users", "children": []}
|
||||
]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### (9)发送邮箱验证码
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/auth/email/code/`
|
||||
- 认证: 不需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `email`, `code_type`(如 `register|reset_password`)
|
||||
- 实现: `send_email_code`(`hertz_studio_django_auth/views/auth_views.py:441`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/auth/email/code/
|
||||
Content-Type: application/json
|
||||
|
||||
{"email": "user@example.com", "code_type": "register"}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "验证码发送成功,请查收邮件", "data": {"email": "user@example.com", "code_type": "register", "expires_in": 300}}
|
||||
```
|
||||
|
||||
### (10)刷新访问令牌
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/auth/token/refresh/`
|
||||
- 认证: 需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `refresh_token`
|
||||
- 实现: `refresh_token`(`hertz_studio_django_auth/views/auth_views.py:544`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/auth/token/refresh/
|
||||
Authorization: Bearer <access_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"refresh_token": "eyJhbGci..."}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "token刷新成功", "data": {"access_token": "eyJhbGci..."}}
|
||||
```
|
||||
|
||||
|
||||
## 二、用户管理
|
||||
|
||||
- 前缀: `/api/`
|
||||
|
||||
### (1)获取用户列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/users/`
|
||||
- 权限: `system:user:list`
|
||||
- 查询参数: `page`, `page_size`, `username`, `email`, `real_name`, `department_id`, `status`
|
||||
- 实现: `user_list`(`hertz_studio_django_auth/views/management_views.py:38`)
|
||||
|
||||
### (2)创建用户
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/users/create/`
|
||||
- 权限: `system:user:add`
|
||||
- 请求体: `UserManagementSerializer` 字段
|
||||
- 实现: `user_create`(`hertz_studio_django_auth/views/management_views.py:106`)
|
||||
|
||||
### (3)获取用户详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/users/{user_id}/`
|
||||
- 权限: `system:user:query`
|
||||
- 实现: `user_detail`(`hertz_studio_django_auth/views/management_views.py:149`)
|
||||
|
||||
### (4)更新用户
|
||||
- 方法: `PUT`
|
||||
- 路径: `/api/users/{user_id}/update/`
|
||||
- 权限: `system:user:edit`
|
||||
- 请求体: `UserManagementSerializer`
|
||||
- 实现: `user_update`(`hertz_studio_django_auth/views/management_views.py:190`)
|
||||
|
||||
### (5)删除用户
|
||||
- 方法: `DELETE`
|
||||
- 路径: `/api/users/{user_id}/delete/`
|
||||
- 权限: `system:user:remove`
|
||||
- 实现: `user_delete`(`hertz_studio_django_auth/views/management_views.py:236`)
|
||||
|
||||
### (6)分配用户角色
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/users/assign-roles/`
|
||||
- 权限: `system:user:role`
|
||||
- 请求体: `user_id`, `role_ids: number[]`
|
||||
- 实现: `user_assign_roles`(`hertz_studio_django_auth/views/management_views.py:279`)
|
||||
|
||||
|
||||
## 三、角色管理
|
||||
|
||||
- 前缀: `/api/`
|
||||
|
||||
### (1)获取角色列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/roles/`
|
||||
- 权限: `system:role:list`
|
||||
- 查询参数: `page`, `page_size`, `role_name`, `role_code`, `status`
|
||||
- 实现: `role_list`(`hertz_studio_django_auth/views/management_views.py:328`)
|
||||
|
||||
### (2)创建角色
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/roles/create/`
|
||||
- 权限: `system:role:add`
|
||||
- 请求体: `RoleManagementSerializer`
|
||||
- 实现: `role_create`(`hertz_studio_django_auth/views/management_views.py:393`)
|
||||
|
||||
### (3)获取角色详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/roles/{role_id}/`
|
||||
- 权限: `system:role:query`
|
||||
- 实现: `role_detail`(`hertz_studio_django_auth/views/management_views.py:437`)
|
||||
|
||||
### (4)更新角色
|
||||
- 方法: `PUT`
|
||||
- 路径: `/api/roles/{role_id}/update/`
|
||||
- 权限: `system:role:edit`
|
||||
- 请求体: `RoleManagementSerializer`
|
||||
- 实现: `role_update`(`hertz_studio_django_auth/views/management_views.py:477`)
|
||||
|
||||
### (5)删除角色
|
||||
- 方法: `DELETE`
|
||||
- 路径: `/api/roles/{role_id}/delete/`
|
||||
- 权限: `system:role:remove`
|
||||
- 实现: `role_delete`(`hertz_studio_django_auth/views/management_views.py:527`)
|
||||
|
||||
### (6)分配角色菜单
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/roles/assign-menus/`
|
||||
- 权限: `system:role:menu`
|
||||
- 请求体: `role_id`, `menu_ids: number[]`
|
||||
- 实现: `role_assign_menus`(`hertz_studio_django_auth/views/management_views.py:579`)
|
||||
|
||||
### (7)获取角色菜单
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/roles/{role_id}/menus/`
|
||||
- 权限: `system:role:menu`
|
||||
- 实现: `role_menus`(`hertz_studio_django_auth/views/management_views.py:631`)
|
||||
|
||||
|
||||
## 四、菜单管理
|
||||
|
||||
- 前缀: `/api/`
|
||||
|
||||
### (1)获取菜单列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/menus/`
|
||||
- 权限: `system:menu:list`
|
||||
- 实现: `menu_list`(`hertz_studio_django_auth/views/management_views.py:665`)
|
||||
|
||||
### (2)获取菜单树
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/menus/tree/`
|
||||
- 权限: `system:menu:list`
|
||||
- 实现: `menu_tree`(`hertz_studio_django_auth/views/management_views.py:710`)
|
||||
|
||||
### (3)创建菜单
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/menus/create/`
|
||||
- 权限: `system:menu:add`
|
||||
- 请求体: `MenuManagementSerializer`
|
||||
- 实现: `menu_create`(`hertz_studio_django_auth/views/management_views.py:744`)
|
||||
|
||||
### (4)获取菜单详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/menus/{menu_id}/`
|
||||
- 权限: `system:menu:query`
|
||||
- 实现: `menu_detail`(`hertz_studio_django_auth/views/management_views.py:787`)
|
||||
|
||||
### (5)更新菜单
|
||||
- 方法: `PUT`
|
||||
- 路径: `/api/menus/{menu_id}/update/`
|
||||
- 权限: `system:menu:edit`
|
||||
- 请求体: `MenuManagementSerializer`
|
||||
- 实现: `menu_update`(`hertz_studio_django_auth/views/management_views.py:828`)
|
||||
|
||||
### (6)删除菜单
|
||||
- 方法: `DELETE`
|
||||
- 路径: `/api/menus/{menu_id}/delete/`
|
||||
- 权限: `system:menu:remove`
|
||||
- 实现: `menu_delete`(`hertz_studio_django_auth/views/management_views.py:878`)
|
||||
|
||||
|
||||
## 五、部门管理
|
||||
|
||||
- 前缀: `/api/`
|
||||
|
||||
### (1)获取部门列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/departments/`
|
||||
- 权限: `system:dept:list`
|
||||
- 实现: `department_list`(`hertz_studio_django_auth/views/management_views.py:921`)
|
||||
|
||||
### (2)获取部门树
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/departments/tree/`
|
||||
- 权限: `system:dept:list`
|
||||
- 实现: `department_tree`(`hertz_studio_django_auth/views/management_views.py:963`)
|
||||
|
||||
### (3)创建部门
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/departments/create/`
|
||||
- 权限: `system:dept:add`
|
||||
- 请求体: `DepartmentManagementSerializer`
|
||||
- 实现: `department_create`(`hertz_studio_django_auth/views/management_views.py:997`)
|
||||
|
||||
### (4)获取部门详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/departments/{dept_id}/`
|
||||
- 权限: `system:dept:query`
|
||||
- 实现: `department_detail`(`hertz_studio_django_auth/views/management_views.py:1040`)
|
||||
|
||||
### (5)更新部门
|
||||
- 方法: `PUT`
|
||||
- 路径: `/api/departments/{dept_id}/update/`
|
||||
- 权限: `system:dept:edit`
|
||||
- 请求体: `DepartmentManagementSerializer`
|
||||
- 实现: `department_update`(`hertz_studio_django_auth/views/management_views.py:1081`)
|
||||
|
||||
### (6)删除部门
|
||||
- 方法: `DELETE`
|
||||
- 路径: `/api/departments/{dept_id}/delete/`
|
||||
- 权限: `system:dept:remove`
|
||||
- 实现: `department_delete`(`hertz_studio_django_auth/views/management_views.py:1131`)
|
||||
|
||||
|
||||
## 六、错误响应示例
|
||||
- 未授权访问:
|
||||
```json
|
||||
{"success": false, "code": 401, "message": "未提供认证令牌"}
|
||||
```
|
||||
- 权限不足:
|
||||
```json
|
||||
{"success": false, "code": 403, "message": "缺少权限:system:user:list"}
|
||||
```
|
||||
- 参数验证失败:
|
||||
```json
|
||||
{"success": false, "code": 422, "message": "参数验证失败", "errors": {"email": ["邮箱格式不正确"]}}
|
||||
```
|
||||
391
docs/API接口文档/通知模块接口文档.md
Normal file
391
docs/API接口文档/通知模块接口文档.md
Normal file
@@ -0,0 +1,391 @@
|
||||
# Hertz Studio Django 通知模块接口文档
|
||||
|
||||
- 基础路径: `/api/notice/`
|
||||
- 统一响应: 使用 `HertzResponse`,结构如下(参考 `hertz_studio_django_utils/responses/HertzResponse.py`)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
- 路由挂载: 项目主路由中已通过 `path('api/notice/', include('hertz_studio_django_notice.urls'))` 挂载(`hertz_server_django/urls.py:19`)。
|
||||
- 认证说明: 所有接口均需要登录,需在请求头携带 `Authorization: Bearer <token>`(`hertz_studio_django_auth/utils/decorators/auth_decorators.py:1`)。
|
||||
|
||||
|
||||
## 一、管理员通知接口
|
||||
|
||||
### (1)创建通知
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/notice/admin/create/`
|
||||
- 认证: 需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `title`, `content`, `notice_type`, `priority`, `is_top`, `publish_time`, `expire_time`, `attachment_url`
|
||||
- 实现: `admin_create_notice`(`hertz_studio_django_notice/views/admin_views.py:39`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/notice/admin/create/
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"title": "系统维护通知",
|
||||
"content": "将于周六晚间进行系统维护。",
|
||||
"notice_type": 1,
|
||||
"priority": 3,
|
||||
"is_top": true,
|
||||
"publish_time": "2025-11-18 09:00:00",
|
||||
"expire_time": "2025-11-20 23:59:59",
|
||||
"attachment_url": null
|
||||
}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "通知创建成功", "data": {"notice_id": 101}}
|
||||
```
|
||||
|
||||
### (2)更新通知
|
||||
- 方法: `PUT`
|
||||
- 路径: `/api/notice/admin/update/{notice_id}/`
|
||||
- 认证: 需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `title`, `content`, `notice_type`, `priority`, `is_top`, `publish_time`, `expire_time`, `attachment_url`, `status`
|
||||
- 实现: `admin_update_notice`(`hertz_studio_django_notice/views/admin_views.py:94`)
|
||||
- 请求示例:
|
||||
```http
|
||||
PUT /api/notice/admin/update/101/
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"priority": 4, "is_top": false}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "通知更新成功"}
|
||||
```
|
||||
|
||||
### (3)删除通知
|
||||
- 方法: `DELETE`
|
||||
- 路径: `/api/notice/admin/delete/{notice_id}/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `admin_delete_notice`(`hertz_studio_django_notice/views/admin_views.py:146`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8000/api/notice/admin/delete/101/" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "通知删除成功"}
|
||||
```
|
||||
|
||||
### (4)获取通知列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/notice/admin/list/`
|
||||
- 认证: 需要登录
|
||||
- 查询参数:
|
||||
- `page`, `page_size`
|
||||
- `notice_type`, `status`, `priority`, `is_top`, `keyword`
|
||||
- `start_date`, `end_date`(按发布时间范围筛选)
|
||||
- 实现: `admin_get_notice_list`(`hertz_studio_django_notice/views/admin_views.py:184`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/notice/admin/list/?page=1&page_size=10&status=1&is_top=true" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例(节选):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "获取通知列表成功",
|
||||
"data": {
|
||||
"notices": [
|
||||
{
|
||||
"notice_id": 101,
|
||||
"title": "系统维护通知",
|
||||
"notice_type": 1,
|
||||
"notice_type_display": "系统通知",
|
||||
"priority": 3,
|
||||
"priority_display": "高",
|
||||
"status": 1,
|
||||
"status_display": "已发布",
|
||||
"is_top": true,
|
||||
"publish_time": "2025-11-18T09:00:00Z",
|
||||
"expire_time": "2025-11-20T23:59:59Z",
|
||||
"publisher_name": "管理员",
|
||||
"view_count": 12,
|
||||
"is_expired": false,
|
||||
"read_count": 30,
|
||||
"unread_count": 5,
|
||||
"created_at": "2025-11-17T10:00:00Z",
|
||||
"updated_at": "2025-11-17T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {"current_page": 1, "page_size": 10, "total_pages": 1, "total_count": 1, "has_next": false, "has_previous": false}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (5)获取通知详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/notice/admin/detail/{notice_id}/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `admin_get_notice_detail`(`hertz_studio_django_notice/views/admin_views.py:273`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/notice/admin/detail/101/" -H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例(节选):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "获取通知详情成功",
|
||||
"data": {
|
||||
"notice_id": 101,
|
||||
"title": "系统维护通知",
|
||||
"content": "将于周六晚间进行系统维护。",
|
||||
"notice_type": 1,
|
||||
"notice_type_display": "系统通知",
|
||||
"priority": 3,
|
||||
"priority_display": "高",
|
||||
"status": 1,
|
||||
"status_display": "已发布",
|
||||
"is_top": true,
|
||||
"publish_time": "2025-11-18T09:00:00Z",
|
||||
"expire_time": "2025-11-20T23:59:59Z",
|
||||
"attachment_url": null,
|
||||
"publisher_name": "管理员",
|
||||
"publisher_username": "admin",
|
||||
"view_count": 12,
|
||||
"is_expired": false,
|
||||
"read_count": 30,
|
||||
"unread_count": 5,
|
||||
"created_at": "2025-11-17T10:00:00Z",
|
||||
"updated_at": "2025-11-17T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (6)发布通知
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/notice/admin/publish/{notice_id}/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `admin_publish_notice`(`hertz_studio_django_notice/views/admin_views.py:317`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/notice/admin/publish/101/" -H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "通知发布成功"}
|
||||
```
|
||||
|
||||
### (7)撤回通知
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/notice/admin/withdraw/{notice_id}/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `admin_withdraw_notice`(`hertz_studio_django_notice/views/admin_views.py:374`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/notice/admin/withdraw/101/" -H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "通知撤回成功"}
|
||||
```
|
||||
|
||||
|
||||
## 二、用户通知接口
|
||||
|
||||
### (1)获取通知列表
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/notice/user/list/`
|
||||
- 认证: 需要登录
|
||||
- 查询参数:
|
||||
- `page`, `page_size`
|
||||
- `notice_type`, `is_read`, `is_starred`, `priority`, `keyword`
|
||||
- `show_expired`: `true/false`,默认不显示过期通知
|
||||
- 实现: `user_get_notice_list`(`hertz_studio_django_notice/views/user_views.py:28`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/notice/user/list/?page=1&page_size=10&is_read=false&is_starred=true" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例(节选):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "获取通知列表成功",
|
||||
"data": {
|
||||
"notices": [
|
||||
{
|
||||
"title": "系统维护通知",
|
||||
"notice_type_display": "系统通知",
|
||||
"priority_display": "高",
|
||||
"is_top": true,
|
||||
"publish_time": "2025-11-18T09:00:00Z",
|
||||
"is_read": false,
|
||||
"read_time": null,
|
||||
"is_starred": true,
|
||||
"starred_time": "2025-11-17T12:00:00Z",
|
||||
"is_expired": false,
|
||||
"created_at": "2025-11-17T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"page_size": 10,
|
||||
"total_pages": 1,
|
||||
"total_count": 1,
|
||||
"has_next": false,
|
||||
"has_previous": false
|
||||
},
|
||||
"statistics": {"total_count": 10, "unread_count": 2, "starred_count": 3}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (2)获取通知详情
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/notice/user/detail/{notice_id}/`
|
||||
- 认证: 需要登录
|
||||
- 行为: 自动标记为已读并增加查看次数
|
||||
- 实现: `user_get_notice_detail`(`hertz_studio_django_notice/views/user_views.py:147`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/notice/user/detail/101/" -H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例(节选):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "获取通知详情成功",
|
||||
"data": {
|
||||
"title": "系统维护通知",
|
||||
"content": "将于周六晚间进行系统维护。",
|
||||
"notice_type_display": "系统通知",
|
||||
"priority_display": "高",
|
||||
"attachment_url": null,
|
||||
"publish_time": "2025-11-18T09:00:00Z",
|
||||
"expire_time": "2025-11-20T23:59:59Z",
|
||||
"is_top": true,
|
||||
"is_expired": false,
|
||||
"publisher_name": "管理员",
|
||||
"is_read": true,
|
||||
"read_time": "2025-11-17T12:05:00Z",
|
||||
"is_starred": true,
|
||||
"starred_time": "2025-11-17T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### (3)标记通知已读
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/notice/user/mark-read/`
|
||||
- 认证: 需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `notice_id`
|
||||
- 实现: `user_mark_notice_read`(`hertz_studio_django_notice/views/user_views.py:214`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/notice/user/mark-read/
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"notice_id": 101}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "标记已读成功"}
|
||||
```
|
||||
|
||||
### (4)批量标记已读
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/notice/user/batch-mark-read/`
|
||||
- 认证: 需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `notice_ids: number[]`
|
||||
- 实现: `user_batch_mark_read`(`hertz_studio_django_notice/views/user_views.py:260`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/notice/user/batch-mark-read/
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"notice_ids": [101, 102, 103]}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "批量标记已读成功"}
|
||||
```
|
||||
|
||||
### (5)标记全部已读
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/notice/user/mark-all-read/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `user_mark_all_read`(`hertz_studio_django_notice/views/user_views.py:317`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/notice/user/mark-all-read/" -H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "标记全部已读成功,共标记3条通知", "data": {"updated_count": 3}}
|
||||
```
|
||||
|
||||
### (6)切换收藏状态
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/notice/user/toggle-star/`
|
||||
- 认证: 需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `notice_id`, `is_starred`
|
||||
- 实现: `user_toggle_notice_star`(`hertz_studio_django_notice/views/user_views.py:401`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/notice/user/toggle-star/
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{"notice_id": 101, "is_starred": true}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{"success": true, "code": 200, "message": "收藏成功"}
|
||||
```
|
||||
|
||||
### (7)获取通知统计信息
|
||||
- 方法: `GET`
|
||||
- 路径: `/api/notice/user/statistics/`
|
||||
- 认证: 需要登录
|
||||
- 实现: `user_get_notice_statistics`(`hertz_studio_django_notice/views/user_views.py:417`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl "http://localhost:8000/api/notice/user/statistics/" -H "Authorization: Bearer <token>"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "获取统计信息成功",
|
||||
"data": {
|
||||
"total_count": 10,
|
||||
"unread_count": 2,
|
||||
"read_count": 8,
|
||||
"starred_count": 3,
|
||||
"type_statistics": {"系统通知": 6, "公告通知": 3, "活动通知": 1, "维护通知": 0},
|
||||
"priority_statistics": {"低": 2, "中": 4, "高": 3, "紧急": 1}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 三、备注
|
||||
- 列表与详情序列包含显示用枚举字段(如 `notice_type_display`, `priority_display`, `status_display`)。
|
||||
- 用户视图中 `user_get_notice_detail` 会自动将未读标记为已读并累加 `view_count`。
|
||||
- 管理员发布接口会为所有启用用户创建 `HertzUserNotice` 状态记录。
|
||||
83
docs/API接口文档/验证码模块接口文档.md
Normal file
83
docs/API接口文档/验证码模块接口文档.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# Hertz Studio Django 验证码模块接口文档
|
||||
|
||||
- 基础路径: `/api/captcha/`
|
||||
- 统一响应: 使用 `HertzResponse`,结构如下(参考 `hertz_studio_django_utils/responses/HertzResponse.py`)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
- 路由挂载: 项目主路由中已通过 `path('api/captcha/', include('hertz_studio_django_captcha.urls'))` 挂载(`hertz_server_django/urls.py:28`)。
|
||||
- 认证说明: 接口允许匿名访问(`AllowAny`),不需要登录(`hertz_studio_django_captcha/api_views.py:18`, `hertz_studio_django_captcha/api_views.py:60`)。
|
||||
- 验证接口说明: 独立的验证码验证接口已删除,验证逻辑集成到具体业务接口(`hertz_studio_django_captcha/api_views.py:54`)。
|
||||
|
||||
|
||||
## 一、生成验证码
|
||||
|
||||
### 生成新的验证码
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/captcha/generate/`
|
||||
- 认证: 不需要登录
|
||||
- 实现: `CaptchaGenerateAPIView.post`(`hertz_studio_django_captcha/api_views.py:38`)
|
||||
- 请求示例:
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/api/captcha/generate/"
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "验证码生成成功",
|
||||
"data": {
|
||||
"captcha_id": "c1a2b3c4-d5e6-7890-abcd-ef1234567890",
|
||||
"image_data": "data:image/png;base64,iVBORw0KGgo...",
|
||||
"expires_in": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 二、刷新验证码
|
||||
|
||||
### 刷新已有验证码,生成新图像
|
||||
- 方法: `POST`
|
||||
- 路径: `/api/captcha/refresh/`
|
||||
- 认证: 不需要登录
|
||||
- 请求体: `application/json`
|
||||
- 字段: `captcha_id` 旧验证码ID
|
||||
- 实现: `CaptchaRefreshAPIView.post`(`hertz_studio_django_captcha/api_views.py:84`)
|
||||
- 请求示例:
|
||||
```http
|
||||
POST /api/captcha/refresh/
|
||||
Content-Type: application/json
|
||||
|
||||
{"captcha_id": "c1a2b3c4-d5e6-7890-abcd-ef1234567890"}
|
||||
```
|
||||
- 返回示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"code": 200,
|
||||
"message": "验证码刷新成功",
|
||||
"data": {
|
||||
"captcha_id": "f2b3c4d5-e6f7-8901-bcde-0123456789ab",
|
||||
"image_data": "data:image/png;base64,iVBORw0KGgo...",
|
||||
"expires_in": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 三、错误响应示例
|
||||
- 参数不完整(刷新接口):
|
||||
```json
|
||||
{"success": false, "code": 400, "message": "参数不完整"}
|
||||
```
|
||||
- 生成/刷新失败(异常信息):
|
||||
```json
|
||||
{"success": false, "code": 500, "message": "验证码生成失败", "error": "<错误信息>"}
|
||||
```
|
||||
BIN
docs/img/ac87c1f6-a28b-4959-ae98-cab5e150c56b.png
Normal file
BIN
docs/img/ac87c1f6-a28b-4959-ae98-cab5e150c56b.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/img/img_1.png
Normal file
BIN
docs/img/img_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
BIN
docs/img/img_2.png
Normal file
BIN
docs/img/img_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
BIN
docs/img/img_3.png
Normal file
BIN
docs/img/img_3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
68
docs/使用手册.md
Normal file
68
docs/使用手册.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 使用手册
|
||||
|
||||
## 一、**环境要求**
|
||||
|
||||
- `Python 3.10+`(建议 3.11/3.12)
|
||||
- 操作系统:Windows(PowerShell)
|
||||
- 可选:本地 `Redis` 服务(默认地址 `redis://127.0.0.1:6379`)
|
||||
|
||||
## 二、**依赖安装**
|
||||
|
||||
- 一次性使用镜像安装:
|
||||
|
||||
在项目根目录终端执行:
|
||||
|
||||
`pip install -r requirements.txt -i https://hertz:hertz@hzpypi.hzsystems.cn/simple/`
|
||||
|
||||
- 注:若提示依赖安装失败,请将机器码保存并联系管理员开通机器码注册
|
||||
|
||||
## **三、启动服务**
|
||||
|
||||
- 通过脚本启动(支持端口参数):
|
||||
- `python start_server.py --port 8000`
|
||||
- 访问地址:
|
||||
- `http://127.0.0.1:8000/`
|
||||
- WebSocket:`ws://127.0.0.1:8000/ws/`
|
||||
- 首次启动将自动执行:
|
||||
- 扫描并注册新应用到 `INSTALLED_APPS` 与 `urls.py`(`start_server.py:173`、`start_server.py:98`)。
|
||||
- 执行 `makemigrations` 与 `migrate`(`start_server.py:1109`)。
|
||||
- 初始化超级管理员/部门/菜单/角色等(`start_server.py:877`)。
|
||||
- 创建菜单生成器工具 `generate_menu.py`(`start_server.py:780`)。
|
||||
- 启动 `daphne` 并开启热重启监听(`start_server.py:1016`、`start_server.py:1063`)。
|
||||
|
||||
## 四、默认账号
|
||||
|
||||
- 超级管理员:
|
||||
- 用户名:`hertz`
|
||||
- 密码:`hertz`
|
||||
- 普通用户
|
||||
- 用户名:`demo`
|
||||
- 密码:`123456`
|
||||
|
||||
## 五、**常见配置说明**
|
||||
|
||||
- CORS:通过 `.env` 配置 `CORS_ALLOWED_ORIGINS` 与 `CORS_ALLOW_ALL_ORIGINS`
|
||||
- 静态与媒体:
|
||||
- 静态:`STATICFILES_DIRS = [BASE_DIR / 'static']`
|
||||
- 媒体:`MEDIA_ROOT = BASE_DIR / 'media'`(`hertz_server_django/settings.py:209-216`)。
|
||||
- WebSocket:使用 `channels` 与 `channels-redis`,层配置读取 `REDIS_URL`(`hertz_server_django/settings.py:302-309`)。
|
||||
|
||||
## **六、问题排查**
|
||||
|
||||
- `daphne` 或 `watchdog` 未安装:
|
||||
- 运行:`pip install daphne watchdog -i https://pypi.tuna.tsinghua.edu.cn/simple`(`start_server.py:1235-1248` 有依赖检查)。
|
||||
- Redis 未运行:
|
||||
- 安装并启动 Redis,或调整 `REDIS_URL` 指向可用实例。
|
||||
|
||||
## **七、项目结构**
|
||||
|
||||
- 核心配置:`hertz_server_django/settings.py`、`hertz_server_django/urls.py`
|
||||
- 启动脚本:`start_server.py`
|
||||
- 依赖清单:`requirements.txt`
|
||||
- 静态资源:`static/`,媒体资源:`media/`
|
||||
- 业务模块:`hertz_studio_django_*` 系列包(鉴权、日志、通知、监控、Wiki、AI、YOLO、代码生成等)。
|
||||
|
||||
## 八、**快速启动**
|
||||
|
||||
- 安装依赖:`pip install -r requirements.txt -i https://hertz:hertz@hzpypi.hzsystems.cn/simple/`
|
||||
- 启动服务:`python start_server.py --port 8000`
|
||||
158
docs/开发规范.md
Normal file
158
docs/开发规范.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# 新功能开发规范
|
||||
|
||||
## 一、命名规范
|
||||
- APP 名称:`hertz_studio_django_xxx`,全小写,用下划线分隔
|
||||
- Python 包与模块:全小写,短名称,避免缩写不清晰
|
||||
- URL 命名空间:`app_name = 'hertz_studio_django_xxx'`
|
||||
- 数据库表名:`Meta.db_table = 'hertz_xxx_model'`,避免与其他库冲突
|
||||
- 迁移文件命名:描述性动词+对象,如 `0003_add_field_to_model`
|
||||
|
||||
## 二、项目结构与约定
|
||||
- 标准结构:`apps.py`、`models.py`、`serializers.py`、`views.py`、`urls.py`、`admin.py`
|
||||
- 静态资源:`media/<app_name>/...` 放置同路径资源以覆盖库静态文件
|
||||
- 配置集中:在 `settings.py` 维护,使用前缀化大写变量(如 `YOLO_MODEL`)
|
||||
|
||||
## 三、接口返回规范
|
||||
- 统一使用 `HertzResponse`(路径:`hertz_studio_django_utils/responses/HertzResponse.py`)
|
||||
- 成功:
|
||||
```python
|
||||
from hertz_studio_django_utils.responses.HertzResponse import HertzResponse
|
||||
return HertzResponse.success(data={'id': 1}, message='操作成功')
|
||||
```
|
||||
- 失败:
|
||||
```python
|
||||
return HertzResponse.fail(message='业务失败')
|
||||
```
|
||||
- 错误:
|
||||
```python
|
||||
return HertzResponse.error(message='系统错误', error=str(e))
|
||||
```
|
||||
- 验证失败:
|
||||
```python
|
||||
return HertzResponse.validation_error(message='参数验证失败', errors=serializer.errors)
|
||||
```
|
||||
- 统一键:`success | code | message | data`,禁止返回非标准顶层结构
|
||||
|
||||
## 四、API 设计规范
|
||||
- 路径语义化:`/models/`、`/detections/`、`/alerts/`
|
||||
- 方法约定:`GET` 查询、`POST` 创建/动作、`PUT/PATCH` 更新、`DELETE` 删除
|
||||
- 分页:请求参数 `page, page_size`;响应 `total, items`
|
||||
- 过滤与排序:查询参数 `q, order_by, filters`;谨慎开放可排序字段
|
||||
|
||||
## 五、认证与授权
|
||||
- 强制认证:业务敏感接口使用登录态(装饰器或 DRF 权限类)
|
||||
- 权限控制:按用户/组/角色配置;避免在视图中硬编码权限
|
||||
- 速率限制:对登录、验证码、检测等接口进行限流
|
||||
|
||||
## 六、日志与审计
|
||||
- 请求审计:记录请求方法、路径、用户、响应码、耗时
|
||||
- 业务事件:模型启用/删除、检测结果、告警变更记录
|
||||
- 脱敏:对密码、令牌、隐私字段进行统一脱敏
|
||||
|
||||
## 七、配置约定
|
||||
- 所有库配置集中在 `settings.py`,使用前缀化变量:
|
||||
- AI:`AI_MODEL_PROVIDER`、`AI_DEFAULT_MODEL`、`AI_TIMEOUT`
|
||||
- Auth:`AUTH_LOGIN_REDIRECT_URL`、`AUTH_ENABLE_OAUTH`
|
||||
- Captcha:`CAPTCHA_TYPE`、`CAPTCHA_EXPIRE_SECONDS`
|
||||
- Log:`LOG_LEVEL`、`LOG_SINKS`、`LOG_REDACT_FIELDS`
|
||||
- Notice:`NOTICE_CHANNELS`、`NOTICE_RETRY`
|
||||
- Monitor:`MONITOR_PROBES`、`MONITOR_ALERTS`
|
||||
- Wiki:`WIKI_MARKDOWN`、`WIKI_SEARCH_BACKEND`
|
||||
- YOLO:`YOLO_MODEL`、`YOLO_DEVICE`、`YOLO_CONF_THRESHOLD`
|
||||
- Codegen:`CODEGEN_TEMPLATES_DIR`、`CODEGEN_OUTPUT_DIR`
|
||||
|
||||
## 八、可扩展性(不改库源码)
|
||||
- 视图子类化 + 路由覆盖:在项目中继承库视图并替换路由匹配
|
||||
```python
|
||||
# urls.py
|
||||
from django.urls import path, include
|
||||
from your_app.views import MyDetectView
|
||||
urlpatterns = [
|
||||
path('yolo/detect/', MyDetectView.as_view(), name='detect'),
|
||||
path('yolo/', include('hertz_studio_django_yolo.urls', namespace='hertz_studio_django_yolo')),
|
||||
]
|
||||
```
|
||||
- 猴子补丁:在 `AppConfig.ready()` 将库函数替换为自定义函数
|
||||
```python
|
||||
# apps.py
|
||||
from django.apps import AppConfig
|
||||
class YourAppConfig(AppConfig):
|
||||
name = 'your_app'
|
||||
def ready(self):
|
||||
from hertz_studio_django_yolo import views as yviews
|
||||
from your_app.views import my_yolo_detection
|
||||
yviews.yolo_detection = my_yolo_detection
|
||||
```
|
||||
- Admin 重注册:`unregister` 后 `register` 自定义 `ModelAdmin`
|
||||
- 信号连接:在 `ready()` 中连接库暴露的信号以扩展行为
|
||||
|
||||
## 九、示例:覆写 YOLO 检测返回值
|
||||
- 目标位置:`hertz_studio_django_yolo/views.py:586-603`
|
||||
- 最小替换示例(路由覆盖):
|
||||
```python
|
||||
from rest_framework.decorators import api_view, parser_classes
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from hertz_studio_django_utils.responses.HertzResponse import HertzResponse
|
||||
from hertz_studio_django_yolo.views import _perform_detection
|
||||
from hertz_studio_django_yolo.models import YoloModel, DetectionRecord
|
||||
import uuid, os, time, shutil
|
||||
|
||||
@api_view(['POST'])
|
||||
@parser_classes([MultiPartParser, FormParser])
|
||||
@login_required
|
||||
def my_yolo_detection(request):
|
||||
# 复用库的流程,省略若干步骤,仅演示返回体差异化
|
||||
serializer = hertz_studio_django_yolo.serializers.DetectionRequestSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return HertzResponse.validation_error(message='参数验证失败', errors=serializer.errors)
|
||||
uploaded_file = serializer.validated_data['file']
|
||||
yolo_model = YoloModel.get_enabled_model()
|
||||
original_path = '...' # 省略:存储原始文件
|
||||
result_path, object_count, detected_categories, confidence_scores, avg_confidence = _perform_detection('...', yolo_model.model_path, 0.5, 'image', yolo_model)
|
||||
processing_time = time.time() - time.time()
|
||||
detection_record = DetectionRecord.objects.create(
|
||||
original_file=original_path,
|
||||
result_file='...',
|
||||
detection_type='image',
|
||||
model_name=f"{yolo_model.name} {yolo_model.version}",
|
||||
model=yolo_model,
|
||||
user=request.user,
|
||||
user_name=request.user.username,
|
||||
object_count=object_count,
|
||||
detected_categories=detected_categories,
|
||||
confidence_threshold=0.5,
|
||||
confidence_scores=confidence_scores,
|
||||
avg_confidence=avg_confidence,
|
||||
processing_time=processing_time
|
||||
)
|
||||
return HertzResponse.success(
|
||||
data={
|
||||
'id': detection_record.id,
|
||||
'file': {
|
||||
'result_url': detection_record.result_file.url,
|
||||
'original_url': detection_record.original_file.url
|
||||
},
|
||||
'stats': {
|
||||
'count': object_count,
|
||||
'categories': detected_categories,
|
||||
'scores': confidence_scores,
|
||||
'avg_score': round(avg_confidence, 4) if avg_confidence is not None else None,
|
||||
'time': round(processing_time, 2)
|
||||
},
|
||||
'model': {
|
||||
'name': yolo_model.name,
|
||||
'version': yolo_model.version,
|
||||
'threshold': 0.5
|
||||
},
|
||||
'user': {
|
||||
'id': getattr(request.user, 'user_id', None),
|
||||
'name': request.user.username
|
||||
}
|
||||
},
|
||||
message='检测完成'
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
53
docs/项目简介.md
Normal file
53
docs/项目简介.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Hertz Studio 后端
|
||||
|
||||
## **一、系统简介**
|
||||
|
||||
- 统一后端服务,提供 `REST API` 与 `WebSocket`,面向 AI 工作室与通用后台场景。
|
||||
- 模块化设计,覆盖认证与权限、通知公告、日志、知识库、系统监控、AI 对话、代码生成、Sklearn 推理、YOLO 目标检测等。
|
||||
- 基于 `ASGI` 架构,使用 `Daphne` 运行;默认使用 `SQLite`,可切换 `MySQL`;缓存与消息通道使用 `Redis`。
|
||||
- 自动化启动脚本 `start_server.py` 支持数据库迁移与初始数据(菜单、角色、超级管理员)初始化,以及热重启文件监听。
|
||||
|
||||
## 二、**体验账户**
|
||||
|
||||
- 管理员
|
||||
|
||||
账号:hertz 密码:hertz
|
||||
|
||||
- 普通用户
|
||||
|
||||
账号:demo 密码:123456
|
||||
|
||||
## 三、**技术栈**
|
||||
|
||||
- 后端框架:`Django 5`、`Django REST Framework`、`Channels` + `Daphne`。
|
||||
- 数据与缓存:`SQLite`(默认)/ `MySQL`(可选)、`Redis`(缓存、会话、Channel Layer)。
|
||||
- API 文档:`drf-spectacular` 自动生成,提供 Swagger 与 Redoc 页面。
|
||||
- 认证与安全:自定义 `AuthMiddleware` + `JWT`(`pyjwt`),`CORS` 支持。
|
||||
- AI / ML:`Ultralytics YOLO`、`OpenCV`、`NumPy`、`Scikit-learn`、`Joblib`,以及本地 `Ollama` 对话集成。
|
||||
- 工具与其他:`Mako` 模板(代码生成)、`Pillow`、`watchdog`、`psutil`、`GPUtil`。
|
||||
|
||||
## **四、功能**
|
||||
|
||||
- 认证与权限(`hertz_studio_django_auth`)
|
||||
- 用户注册/登录/登出、密码管理、用户信息维护。
|
||||
- `JWT` 发放与刷新,角色/菜单权限体系,接口权限由 `AuthMiddleware` 统一控制。
|
||||
- 图形验证码(`hertz_studio_django_captcha`)
|
||||
- 可配置验证码生成与校验、尺寸/颜色/噪声等参数支持。
|
||||
- 通知公告(`hertz_studio_django_notice`)
|
||||
- 公告 CRUD、状态管理,面向工作室信息发布。
|
||||
- 日志管理(`hertz_studio_django_log`)
|
||||
- 操作日志采集与查询,支持接口级日志记录装饰器。
|
||||
- 知识库(`hertz_studio_django_wiki`)
|
||||
- 文章与分类管理,面向知识内容沉淀与检索。
|
||||
- 系统监控(`hertz_studio_django_system_monitor`)
|
||||
- CPU/内存/磁盘/GPU 指标采集与展示,基于 `psutil`/`GPUtil`。
|
||||
- AI 对话(`hertz_studio_django_ai`)
|
||||
- 对接本地 `Ollama`,提供对话接口与 `WebSocket` 推送能力。
|
||||
- 代码生成(`hertz_studio_django_codegen`)
|
||||
- 基于 `Mako` 的 Django 代码与菜单生成器,支持 CLI 生成并同步权限。
|
||||
- Sklearn/PyTorch 推理(`hertz_studio_django_sklearn`)
|
||||
- 模型上传、元数据解析(特征/输入输出模式)、统一预测接口,支持 `predict_proba`。
|
||||
- YOLO 目标检测(`hertz_studio_django_yolo`)
|
||||
- 模型管理与启用切换、检测接口、结果中文别名标注与图像绘制;默认模型位于 `static/models/yolov12/weights/best.pt`。
|
||||
- Demo 与首页(`hertz_demo`)
|
||||
- 示例页面(验证码、邮件、WebSocket)与首页模板 `templates/index.html`。
|
||||
84
generate_menu.py
Normal file
84
generate_menu.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
菜单生成器命令行工具
|
||||
用于快速生成菜单配置和权限同步
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import django
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目路径
|
||||
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# 设置Django环境
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
|
||||
django.setup()
|
||||
|
||||
from hertz_studio_django_utils.code_generator.menu_generator import MenuGenerator
|
||||
|
||||
|
||||
def generate_crud_menu(args):
|
||||
"""生成CRUD菜单"""
|
||||
generator = MenuGenerator()
|
||||
|
||||
operations = args.operations.split(',') if args.operations else ['list', 'create', 'update', 'delete']
|
||||
|
||||
menus = generator.generate_menu_config(
|
||||
module_name=args.module_name,
|
||||
model_name=args.model_name,
|
||||
operations=operations,
|
||||
parent_code=args.parent_code,
|
||||
menu_prefix=args.prefix,
|
||||
sort_order=args.sort_order,
|
||||
icon=args.icon
|
||||
)
|
||||
|
||||
# 保存到待同步文件
|
||||
pending_file = os.path.join(project_root, 'pending_menus.py')
|
||||
with open(pending_file, 'w', encoding='utf-8') as f:
|
||||
f.write('# 待同步的菜单配置\n')
|
||||
f.write('pending_menus = [\n')
|
||||
for menu in menus:
|
||||
f.write(' {\n')
|
||||
for key, value in menu.items():
|
||||
if isinstance(value, str):
|
||||
f.write(f" '{key}': '{value}',\n")
|
||||
elif value is None:
|
||||
f.write(f" '{key}': None,\n")
|
||||
else:
|
||||
f.write(f" '{key}': {value},\n")
|
||||
f.write(' },\n')
|
||||
f.write(']\n')
|
||||
|
||||
print(f"已生成 {len(menus)} 个菜单配置,保存到 pending_menus.py")
|
||||
print("请重启服务器以同步菜单到数据库")
|
||||
|
||||
|
||||
def menu_generator_main():
|
||||
parser = argparse.ArgumentParser(description='菜单生成器')
|
||||
subparsers = parser.add_subparsers(dest='command', help='可用命令')
|
||||
|
||||
# CRUD菜单生成命令
|
||||
crud_parser = subparsers.add_parser('crud', help='生成CRUD菜单')
|
||||
crud_parser.add_argument('module_name', help='模块名称(中文)')
|
||||
crud_parser.add_argument('model_name', help='模型名称(英文)')
|
||||
crud_parser.add_argument('--parent-code', default='system', help='父级菜单代码')
|
||||
crud_parser.add_argument('--prefix', default='system', help='菜单前缀')
|
||||
crud_parser.add_argument('--operations', help='操作列表(逗号分隔)')
|
||||
crud_parser.add_argument('--sort-order', type=int, default=1, help='排序')
|
||||
crud_parser.add_argument('--icon', help='图标')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'crud':
|
||||
generate_crud_menu(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
menu_generator_main()
|
||||
13
get_machine_code.bat
Normal file
13
get_machine_code.bat
Normal file
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ================================
|
||||
echo Hertz Django Get Machine Code
|
||||
echo ================================
|
||||
|
||||
python get_machine_code.py
|
||||
|
||||
echo.
|
||||
echo ================================
|
||||
echo Please send the machine code to the after-sales personnel
|
||||
echo ================================
|
||||
pause >nul
|
||||
18
get_machine_code.py
Normal file
18
get_machine_code.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import platform
|
||||
import uuid
|
||||
import hashlib
|
||||
|
||||
|
||||
def get_machine_id() -> str:
|
||||
"""生成机器码。
|
||||
|
||||
根据当前系统信息(平台、架构、MAC地址)生成唯一机器码,
|
||||
使用 SHA256 取前16位并转大写,前缀为 HERTZ_STUDIO_。
|
||||
"""
|
||||
system_info = f"{platform.platform()}-{platform.machine()}-{uuid.getnode()}"
|
||||
return 'HERTZ_STUDIO_' + hashlib.sha256(system_info.encode()).hexdigest()[:16].upper()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
machine_code = get_machine_id()
|
||||
print(f"您的机器码是: {machine_code}")
|
||||
63
get_tokens.py
Normal file
63
get_tokens.py
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
获取用户JWT token的脚本
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
|
||||
# 设置Django环境
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
|
||||
django.setup()
|
||||
|
||||
from hertz_studio_django_auth.models import HertzUser
|
||||
from hertz_studio_django_auth.utils.auth.token_utils import TokenUtils
|
||||
|
||||
def get_user_tokens():
|
||||
"""获取用户token"""
|
||||
try:
|
||||
# 获取普通用户hertz
|
||||
user = HertzUser.objects.get(username='demo')
|
||||
user_roles = user.roles.all()
|
||||
print(f'找到用户: {user.username}, 角色: {[role.role_code for role in user_roles]}')
|
||||
|
||||
# 生成token
|
||||
user_data = {
|
||||
'user_id': str(user.user_id),
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'roles': [role.role_code for role in user_roles],
|
||||
'permissions': []
|
||||
}
|
||||
token_data = TokenUtils.generate_token(user_data)
|
||||
print(f'普通用户token: {token_data}')
|
||||
print()
|
||||
|
||||
# 获取管理员用户hertz
|
||||
admin_user = HertzUser.objects.get(username='hertz')
|
||||
admin_roles = admin_user.roles.all()
|
||||
print(f'找到管理员: {admin_user.username}, 角色: {[role.role_code for role in admin_roles]}')
|
||||
|
||||
# 生成管理员token
|
||||
admin_data = {
|
||||
'user_id': str(admin_user.user_id),
|
||||
'username': admin_user.username,
|
||||
'email': admin_user.email,
|
||||
'roles': [role.role_code for role in admin_roles],
|
||||
'permissions': []
|
||||
}
|
||||
admin_token_data = TokenUtils.generate_token(admin_data)
|
||||
print(f'管理员token: {admin_token_data}')
|
||||
|
||||
return {
|
||||
'user_token': token_data,
|
||||
'admin_token': admin_token_data
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f'错误: {e}')
|
||||
return None
|
||||
|
||||
if __name__ == '__main__':
|
||||
get_user_tokens()
|
||||
11
hertz.txt
Normal file
11
hertz.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
# ================hertz官方库================
|
||||
hertz-studio-django-ai
|
||||
hertz-studio-django-auth
|
||||
hertz-studio-django-captcha
|
||||
hertz-studio-django-kb
|
||||
hertz-studio-django-log
|
||||
hertz-studio-django-notice
|
||||
hertz-studio-django-system-monitor
|
||||
hertz-studio-django-wiki
|
||||
hertz-studio-django-yolo
|
||||
hertz-studio-django-yolo-train
|
||||
274
hertz_demo/README.md
Normal file
274
hertz_demo/README.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# Hertz Demo 演示模块
|
||||
|
||||
## 📋 模块概述
|
||||
|
||||
Hertz Demo 模块是一个功能演示和测试模块,提供了完整的示例代码和交互式演示页面,帮助开发者快速了解和使用 Hertz Server Django 框架的各项功能特性。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- **验证码演示**: 展示多种验证码类型的生成、刷新和验证功能
|
||||
- **邮件系统演示**: 提供邮件模板预览和发送测试功能
|
||||
- **WebSocket演示**: 实时通信功能演示和测试
|
||||
- **交互式界面**: 美观的Web界面,支持实时操作和反馈
|
||||
- **完整示例代码**: 提供可直接参考的实现代码
|
||||
|
||||
## 📁 模块结构
|
||||
|
||||
```
|
||||
hertz_demo/
|
||||
├── __init__.py # 模块初始化
|
||||
├── apps.py # Django应用配置
|
||||
├── models.py # 数据模型(预留)
|
||||
├── views.py # 视图函数和业务逻辑
|
||||
├── urls.py # URL路由配置
|
||||
├── tests.py # 单元测试
|
||||
├── consumers.py # WebSocket消费者
|
||||
├── routing.py # WebSocket路由
|
||||
└── templates/ # 模板文件
|
||||
├── captcha_demo.html # 验证码演示页面
|
||||
├── email_demo.html # 邮件系统演示页面
|
||||
└── websocket_demo.html # WebSocket演示页面
|
||||
```
|
||||
|
||||
## 🎯 核心功能详解
|
||||
|
||||
### 1. 验证码演示功能
|
||||
|
||||
验证码演示页面提供三种验证码类型:
|
||||
- **随机字符验证码**: 随机生成的字母数字组合
|
||||
- **数学运算验证码**: 简单的数学计算验证
|
||||
- **单词验证码**: 英文单词验证
|
||||
|
||||
**主要功能**:
|
||||
- 验证码实时生成和刷新
|
||||
- 前端Ajax验证
|
||||
- 后端表单验证
|
||||
- 验证码类型切换
|
||||
|
||||
### 2. 邮件系统演示功能
|
||||
|
||||
邮件演示页面提供多种邮件模板:
|
||||
- **欢迎邮件**: 用户注册欢迎邮件模板
|
||||
- **系统通知**: 系统消息通知模板
|
||||
- **邮箱验证**: 邮箱验证邮件模板
|
||||
- **自定义邮件**: 支持自定义主题和内容
|
||||
|
||||
**主要功能**:
|
||||
- 邮件模板实时预览
|
||||
- 邮件发送测试
|
||||
- 收件人邮箱验证
|
||||
- 发送状态反馈
|
||||
|
||||
### 3. WebSocket演示功能
|
||||
|
||||
WebSocket演示页面提供实时通信功能:
|
||||
- **连接状态管理**: 显示WebSocket连接状态
|
||||
- **消息发送接收**: 实时消息通信
|
||||
- **广播功能**: 消息广播演示
|
||||
- **错误处理**: 连接异常处理
|
||||
|
||||
## 🚀 API接口
|
||||
|
||||
### 演示页面路由
|
||||
|
||||
| 路由 | 方法 | 描述 |
|
||||
|------|------|------|
|
||||
| `/demo/captcha/` | GET | 验证码演示页面 |
|
||||
| `/demo/email/` | GET | 邮件系统演示页面 |
|
||||
| `/demo/websocket/` | GET | WebSocket演示页面 |
|
||||
| `/websocket/test/` | GET | WebSocket测试页面 |
|
||||
|
||||
### Ajax接口
|
||||
|
||||
**验证码相关**:
|
||||
- `POST /demo/captcha/` (Ajax): 验证码刷新和验证
|
||||
- 请求体: `{"action": "refresh/verify", "captcha_id": "...", "user_input": "..."}`
|
||||
|
||||
**邮件发送**:
|
||||
- `POST /demo/email/` (Ajax): 发送演示邮件
|
||||
- 请求体: 邮件类型、收件人邮箱、自定义内容等
|
||||
|
||||
## ⚙️ 配置参数
|
||||
|
||||
### 邮件配置(settings.py)
|
||||
```python
|
||||
# 邮件服务器配置
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = 'smtp.gmail.com'
|
||||
EMAIL_PORT = 587
|
||||
EMAIL_USE_TLS = True
|
||||
EMAIL_HOST_USER = 'your-email@gmail.com'
|
||||
EMAIL_HOST_PASSWORD = 'your-app-password'
|
||||
DEFAULT_FROM_EMAIL = 'noreply@yourdomain.com'
|
||||
```
|
||||
|
||||
### WebSocket配置
|
||||
```python
|
||||
# ASGI配置
|
||||
ASGI_APPLICATION = 'hertz_server_django.asgi.application'
|
||||
|
||||
# Channel layers配置
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
'hosts': [('127.0.0.1', 6379)],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 🛠️ 快速开始
|
||||
|
||||
### 1. 访问演示页面
|
||||
|
||||
启动开发服务器后,访问以下URL:
|
||||
|
||||
```bash
|
||||
# 验证码演示
|
||||
http://localhost:8000/demo/captcha/
|
||||
|
||||
# 邮件系统演示
|
||||
http://localhost:8000/demo/email/
|
||||
|
||||
# WebSocket演示
|
||||
http://localhost:8000/demo/websocket/
|
||||
```
|
||||
|
||||
### 2. 测试验证码功能
|
||||
|
||||
1. 打开验证码演示页面
|
||||
2. 选择验证码类型
|
||||
3. 点击验证码图片可刷新
|
||||
4. 输入验证码进行验证
|
||||
5. 观察验证结果反馈
|
||||
|
||||
### 3. 测试邮件功能
|
||||
|
||||
1. 打开邮件演示页面
|
||||
2. 选择邮件模板类型
|
||||
3. 输入收件人邮箱
|
||||
4. 点击发送测试邮件
|
||||
5. 查看发送状态
|
||||
|
||||
### 4. 测试WebSocket功能
|
||||
|
||||
1. 打开WebSocket演示页面
|
||||
2. 点击"连接"按钮建立连接
|
||||
3. 在输入框中发送消息
|
||||
4. 观察消息接收和广播
|
||||
5. 测试断开重连功能
|
||||
|
||||
## 🔧 高级用法
|
||||
|
||||
### 自定义邮件模板
|
||||
|
||||
在 `views.py` 中的 `generate_email_content` 函数中添加新的邮件模板:
|
||||
|
||||
```python
|
||||
def generate_email_content(email_type, recipient_name, custom_subject='', custom_message=''):
|
||||
email_templates = {
|
||||
'your_template': {
|
||||
'subject': '您的邮件主题',
|
||||
'html_template': '''
|
||||
<html>
|
||||
<!-- 您的HTML模板内容 -->
|
||||
</html>
|
||||
'''
|
||||
}
|
||||
}
|
||||
# ...
|
||||
```
|
||||
|
||||
### 扩展验证码类型
|
||||
|
||||
在验证码演示中扩展新的验证码类型:
|
||||
|
||||
```python
|
||||
# 在 captcha_demo 函数中添加新的验证码类型
|
||||
captcha_types = {
|
||||
'random_char': '随机字符验证码',
|
||||
'math': '数学运算验证码',
|
||||
'word': '单词验证码',
|
||||
'new_type': '您的新验证码类型' # 新增类型
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket消息处理
|
||||
|
||||
在 `consumers.py` 中扩展WebSocket消息处理逻辑:
|
||||
|
||||
```python
|
||||
class DemoConsumer(WebsocketConsumer):
|
||||
async def receive(self, text_data):
|
||||
data = json.loads(text_data)
|
||||
message_type = data.get('type')
|
||||
|
||||
if message_type == 'custom_message':
|
||||
# 处理自定义消息类型
|
||||
await self.handle_custom_message(data)
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
### 运行单元测试
|
||||
|
||||
```bash
|
||||
python manage.py test hertz_demo
|
||||
```
|
||||
|
||||
### 测试覆盖范围
|
||||
|
||||
- 验证码功能测试
|
||||
- 邮件发送测试
|
||||
- WebSocket连接测试
|
||||
- 页面渲染测试
|
||||
|
||||
## 🔒 安全考虑
|
||||
|
||||
### 验证码安全
|
||||
- 验证码有效期限制
|
||||
- 验证次数限制
|
||||
- 防止暴力破解
|
||||
|
||||
### 邮件安全
|
||||
- 收件人邮箱验证
|
||||
- 发送频率限制
|
||||
- 防止邮件滥用
|
||||
|
||||
### WebSocket安全
|
||||
- 连接认证
|
||||
- 消息内容过滤
|
||||
- 防止DDoS攻击
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q: 邮件发送失败怎么办?
|
||||
A: 检查邮件服务器配置,确保SMTP设置正确,邮箱密码为应用专用密码。
|
||||
|
||||
### Q: WebSocket连接失败怎么办?
|
||||
A: 检查Redis服务是否运行,确保CHANNEL_LAYERS配置正确。
|
||||
|
||||
### Q: 验证码验证总是失败?
|
||||
A: 检查验证码存储后端(Redis)是否正常运行。
|
||||
|
||||
### Q: 如何添加新的演示功能?
|
||||
A: 在views.py中添加新的视图函数,在urls.py中配置路由,在templates中添加模板文件。
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-01)
|
||||
- 初始版本发布
|
||||
- 包含验证码、邮件、WebSocket演示功能
|
||||
- 提供完整的示例代码和文档
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [🏠 返回主项目](../README.md) - Hertz Server Django 主项目文档
|
||||
- [🔐 认证授权模块](../hertz_studio_django_auth/README.md) - 用户管理和权限控制
|
||||
- [🛠️ 工具类模块](../hertz_studio_django_utils/README.md) - 加密、邮件和验证工具
|
||||
- [📋 代码风格指南](../docs/CODING_STYLE.md) - 开发规范和最佳实践
|
||||
|
||||
---
|
||||
|
||||
💡 **提示**: 此模块主要用于功能演示和学习参考,生产环境请根据实际需求进行适当调整和优化。
|
||||
0
hertz_demo/__init__.py
Normal file
0
hertz_demo/__init__.py
Normal file
6
hertz_demo/apps.py
Normal file
6
hertz_demo/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DemoConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'hertz_demo'
|
||||
120
hertz_demo/consumers.py
Normal file
120
hertz_demo/consumers.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from channels.db import database_sync_to_async
|
||||
|
||||
|
||||
class ChatConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
self.room_name = self.scope['url_route']['kwargs']['room_name']
|
||||
self.room_group_name = f'chat_{self.room_name}'
|
||||
|
||||
# Join room group
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
# Leave room group
|
||||
await self.channel_layer.group_discard(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Receive message from WebSocket
|
||||
async def receive(self, text_data):
|
||||
text_data_json = json.loads(text_data)
|
||||
message_type = text_data_json.get('type', 'chat_message')
|
||||
message = text_data_json.get('message', '')
|
||||
username = text_data_json.get('username', 'Anonymous')
|
||||
|
||||
# Send message to room group
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': message_type,
|
||||
'message': message,
|
||||
'username': username,
|
||||
'timestamp': datetime.now().strftime('%H:%M:%S')
|
||||
}
|
||||
)
|
||||
|
||||
# Receive message from room group
|
||||
async def chat_message(self, event):
|
||||
message = event['message']
|
||||
username = event['username']
|
||||
timestamp = event['timestamp']
|
||||
|
||||
# Send message to WebSocket
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'chat_message',
|
||||
'message': message,
|
||||
'username': username,
|
||||
'timestamp': timestamp
|
||||
}))
|
||||
|
||||
async def user_join(self, event):
|
||||
username = event['username']
|
||||
timestamp = event['timestamp']
|
||||
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'user_notification',
|
||||
'message': f'{username} 加入了聊天室',
|
||||
'username': username,
|
||||
'timestamp': timestamp
|
||||
}))
|
||||
|
||||
async def user_leave(self, event):
|
||||
username = event['username']
|
||||
timestamp = event['timestamp']
|
||||
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'user_notification',
|
||||
'message': f'{username} 离开了聊天室',
|
||||
'username': username,
|
||||
'timestamp': timestamp
|
||||
}))
|
||||
|
||||
|
||||
class EchoConsumer(AsyncWebsocketConsumer):
|
||||
async def connect(self):
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
pass
|
||||
|
||||
async def receive(self, text_data):
|
||||
try:
|
||||
# 解析接收到的JSON数据
|
||||
data = json.loads(text_data)
|
||||
message = data.get('message', '').strip()
|
||||
|
||||
if message:
|
||||
# 返回回声消息
|
||||
response = {
|
||||
'type': 'echo_message',
|
||||
'original_message': message,
|
||||
'echo_message': f'回声: {message}',
|
||||
'timestamp': datetime.now().strftime('%H:%M:%S')
|
||||
}
|
||||
else:
|
||||
# 如果消息为空
|
||||
response = {
|
||||
'type': 'error',
|
||||
'message': '消息不能为空',
|
||||
'timestamp': datetime.now().strftime('%H:%M:%S')
|
||||
}
|
||||
|
||||
await self.send(text_data=json.dumps(response, ensure_ascii=False))
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# JSON解析错误
|
||||
error_response = {
|
||||
'type': 'error',
|
||||
'message': '无效的JSON格式',
|
||||
'timestamp': datetime.now().strftime('%H:%M:%S')
|
||||
}
|
||||
await self.send(text_data=json.dumps(error_response, ensure_ascii=False))
|
||||
0
hertz_demo/migrations/__init__.py
Normal file
0
hertz_demo/migrations/__init__.py
Normal file
3
hertz_demo/models.py
Normal file
3
hertz_demo/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
7
hertz_demo/routing.py
Normal file
7
hertz_demo/routing.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
|
||||
re_path(r'ws/echo/$', consumers.EchoConsumer.as_asgi()),
|
||||
]
|
||||
499
hertz_demo/templates/captcha_demo.html
Normal file
499
hertz_demo/templates/captcha_demo.html
Normal file
@@ -0,0 +1,499 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hertz验证码演示 - Hertz Server Django</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.captcha-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.captcha-image {
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.captcha-image:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.8rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.back-link a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.back-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #667eea;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.info-box h3 {
|
||||
color: #667eea;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.info-box ul {
|
||||
margin-left: 1.5rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-box li {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.info-box code {
|
||||
background: #e9ecef;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.captcha-types {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #28a745;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.type-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
border: 2px solid #dee2e6;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
background: #dee2e6;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.type-btn.active {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.demo-section h4 {
|
||||
color: #856404;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-section p {
|
||||
color: #856404;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.success-message {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔐 Hertz验证码演示</h1>
|
||||
<p>{{ demo_description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<!-- 验证码功能介绍 -->
|
||||
<div class="demo-card">
|
||||
<div class="demo-header">
|
||||
<h3>🎯 Hertz验证码功能特性</h3>
|
||||
<p>自定义验证码系统,支持Redis缓存</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<div class="info-box">
|
||||
<ul>
|
||||
<li>🔤 随机字符验证码 - 生成随机字母数字组合</li>
|
||||
<li>🎨 自定义样式配置 - 支持颜色、字体、噪声等设置</li>
|
||||
<li>⚡ Ajax刷新功能 - 无需刷新页面</li>
|
||||
<li>💾 Redis缓存 - 高性能数据存储</li>
|
||||
<li>⏰ 超时自动失效 - 可配置过期时间</li>
|
||||
<li>🔧 灵活配置 - 通过settings.py进行配置</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="captcha-types">
|
||||
<h3 style="margin-bottom: 1rem; color: #667eea;">配置信息</h3>
|
||||
<p style="color: #666; font-size: 0.9rem; margin-bottom: 0.5rem;">• 验证码长度: 可通过HERTZ_CAPTCHA_LENGTH配置</p>
|
||||
<p style="color: #666; font-size: 0.9rem; margin-bottom: 0.5rem;">• 图片尺寸: 可通过HERTZ_CAPTCHA_WIDTH/HEIGHT配置</p>
|
||||
<p style="color: #666; font-size: 0.9rem; margin-bottom: 0.5rem;">• 过期时间: 可通过HERTZ_CAPTCHA_TIMEOUT配置</p>
|
||||
<p style="color: #666; font-size: 0.9rem;">• Redis前缀: 可通过HERTZ_CAPTCHA_REDIS_KEY_PREFIX配置</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证码测试 -->
|
||||
<div class="demo-card">
|
||||
<div class="demo-header">
|
||||
<h3>🔒 Hertz验证码测试</h3>
|
||||
<p>输入验证码进行功能测试</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="success-message" style="{% if message.tags == 'error' %}background: #f8d7da; color: #721c24; border-color: #f5c6cb;{% endif %}">
|
||||
{% if message.tags == 'success' %}✅{% elif message.tags == 'error' %}❌{% endif %} {{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="demo-section">
|
||||
<h4>📋 Hertz验证码说明</h4>
|
||||
<p>• 随机字符验证码:生成随机字母和数字组合</p>
|
||||
<p>• 特点:自定义样式,支持噪声干扰,Redis缓存存储</p>
|
||||
<p>• 功能:支持Ajax刷新,自动过期失效</p>
|
||||
</div>
|
||||
|
||||
<form method="post" id="captcha-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="captcha_input">验证码:</label>
|
||||
<div class="captcha-container">
|
||||
<img id="captcha-image" src="{{ initial_captcha.image_data }}"
|
||||
alt="验证码" class="captcha-image" onclick="refreshCaptcha()"
|
||||
style="cursor: pointer; border: 2px solid #e1e5e9; border-radius: 8px;">
|
||||
<button type="button" class="refresh-btn" onclick="refreshCaptcha()">🔄 刷新</button>
|
||||
</div>
|
||||
<input type="text" id="captcha_input" name="captcha_input"
|
||||
placeholder="请输入验证码" required
|
||||
style="width: 100%; padding: 0.8rem; border: 2px solid #e1e5e9; border-radius: 8px; font-size: 1rem; margin-top: 0.5rem;">
|
||||
<input type="hidden" id="captcha_id" name="captcha_id" value="{{ initial_captcha.captcha_id }}">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-btn">验证提交</button>
|
||||
</form>
|
||||
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #007bff;">
|
||||
<h4 style="color: #007bff; margin-bottom: 0.5rem;">💡 使用提示</h4>
|
||||
<p style="color: #004085; margin-bottom: 0.3rem;">• 点击验证码图片可以刷新</p>
|
||||
<p style="color: #004085; margin-bottom: 0.3rem;">• 验证码不区分大小写</p>
|
||||
<p style="color: #004085;">• 验证码有效期为5分钟</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="/" style="color: white; text-decoration: none; font-weight: 500; font-size: 1.1rem;">← 返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentCaptchaId = '{{ initial_captcha.captcha_id }}';
|
||||
|
||||
function refreshCaptcha() {
|
||||
fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'refresh',
|
||||
captcha_id: currentCaptchaId
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('captcha-image').src = data.data.image_data;
|
||||
document.getElementById('captcha_id').value = data.data.captcha_id;
|
||||
document.getElementById('captcha_input').value = '';
|
||||
currentCaptchaId = data.data.captcha_id;
|
||||
} else {
|
||||
console.error('刷新验证码失败:', data.error);
|
||||
alert('刷新验证码失败,请重试');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('刷新验证码失败:', error);
|
||||
alert('刷新验证码失败,请重试');
|
||||
});
|
||||
}
|
||||
|
||||
// 表单提交处理
|
||||
const captchaForm = document.getElementById('captcha-form');
|
||||
if (captchaForm) {
|
||||
captchaForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const captchaId = document.getElementById('captcha_id').value;
|
||||
const userInput = document.getElementById('captcha_input').value;
|
||||
|
||||
if (!userInput.trim()) {
|
||||
alert('请输入验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'verify',
|
||||
captcha_id: captchaId,
|
||||
user_input: userInput
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (data.valid) {
|
||||
alert('✅ ' + data.message);
|
||||
document.getElementById('captcha_input').value = '';
|
||||
refreshCaptcha();
|
||||
} else {
|
||||
alert('❌ ' + data.message);
|
||||
document.getElementById('captcha_input').value = '';
|
||||
refreshCaptcha();
|
||||
}
|
||||
} else {
|
||||
alert('❌ 验证失败: ' + (data.error || '未知错误'));
|
||||
refreshCaptcha();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('提交失败:', error);
|
||||
alert('❌ 提交失败,请重试');
|
||||
refreshCaptcha();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 回车键提交
|
||||
document.getElementById('captcha_input').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
captchaForm.dispatchEvent(new Event('submit'));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
520
hertz_demo/templates/email_demo.html
Normal file
520
hertz_demo/templates/email_demo.html
Normal file
@@ -0,0 +1,520 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>邮件系统演示 - Hertz Server Django</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.back-link a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.back-link a:hover {
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.demo-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.demo-card h3 {
|
||||
color: #667eea;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.demo-card p {
|
||||
color: #666;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #218838;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
.email-types {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 2px solid #667eea;
|
||||
background: white;
|
||||
color: #667eea;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.type-btn.active,
|
||||
.type-btn:hover {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.email-preview {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 5px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.email-preview h4 {
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.email-preview .preview-content {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 3px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.email-types {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="back-link">
|
||||
<a href="/">← 返回首页</a>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1>📧 邮件系统演示</h1>
|
||||
<p>体验Django邮件发送功能,支持多种邮件类型和模板</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<div class="demo-card">
|
||||
<h3>📮 邮件发送功能</h3>
|
||||
<p>本演示展示了Django邮件系统的核心功能:</p>
|
||||
<ul style="color: #666; margin-left: 1.5rem; margin-bottom: 1rem;">
|
||||
<li>支持HTML和纯文本邮件</li>
|
||||
<li>多种邮件模板类型</li>
|
||||
<li>SMTP配置和发送状态</li>
|
||||
<li>邮件预览和验证</li>
|
||||
</ul>
|
||||
|
||||
<div class="email-types">
|
||||
<button class="type-btn active" data-type="welcome">欢迎邮件</button>
|
||||
<button class="type-btn" data-type="notification">通知邮件</button>
|
||||
<button class="type-btn" data-type="verification">验证邮件</button>
|
||||
<button class="type-btn" data-type="custom">自定义邮件</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h3>✉️ 发送邮件测试</h3>
|
||||
|
||||
<div id="message-area"></div>
|
||||
|
||||
<form id="email-form" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="email-type" name="email_type" value="welcome">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="recipient_email">收件人邮箱 *</label>
|
||||
<input type="email" id="recipient_email" name="recipient_email" required
|
||||
placeholder="请输入收件人邮箱地址">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="subject-group">
|
||||
<label for="subject">邮件主题</label>
|
||||
<input type="text" id="subject" name="subject"
|
||||
placeholder="邮件主题将根据类型自动生成">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="message-group" style="display: none;">
|
||||
<label for="message">邮件内容</label>
|
||||
<textarea id="message" name="message"
|
||||
placeholder="请输入自定义邮件内容"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="recipient_name">收件人姓名</label>
|
||||
<input type="text" id="recipient_name" name="recipient_name"
|
||||
placeholder="收件人姓名(可选)">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="send-btn">
|
||||
📤 发送邮件
|
||||
</button>
|
||||
<button type="button" class="btn btn-success" id="preview-btn">
|
||||
👁️ 预览邮件
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在发送邮件,请稍候...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card" id="preview-card" style="display: none;">
|
||||
<h3>📋 邮件预览</h3>
|
||||
<div class="email-preview">
|
||||
<h4>邮件内容预览:</h4>
|
||||
<div class="preview-content" id="preview-content">
|
||||
<!-- 预览内容将在这里显示 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 邮件类型切换
|
||||
document.querySelectorAll('.type-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
// 移除所有active类
|
||||
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
|
||||
// 添加active类到当前按钮
|
||||
this.classList.add('active');
|
||||
|
||||
const type = this.dataset.type;
|
||||
document.getElementById('email-type').value = type;
|
||||
|
||||
// 根据类型显示/隐藏字段
|
||||
const subjectGroup = document.getElementById('subject-group');
|
||||
const messageGroup = document.getElementById('message-group');
|
||||
const subjectInput = document.getElementById('subject');
|
||||
|
||||
if (type === 'custom') {
|
||||
messageGroup.style.display = 'block';
|
||||
subjectInput.placeholder = '请输入邮件主题';
|
||||
subjectInput.required = true;
|
||||
} else {
|
||||
messageGroup.style.display = 'none';
|
||||
subjectInput.placeholder = '邮件主题将根据类型自动生成';
|
||||
subjectInput.required = false;
|
||||
}
|
||||
|
||||
// 更新预览
|
||||
updatePreview();
|
||||
});
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
document.getElementById('email-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const sendBtn = document.getElementById('send-btn');
|
||||
const loading = document.getElementById('loading');
|
||||
const messageArea = document.getElementById('message-area');
|
||||
|
||||
// 显示加载状态
|
||||
sendBtn.disabled = true;
|
||||
loading.classList.add('show');
|
||||
messageArea.innerHTML = '';
|
||||
|
||||
fetch('/demo/email/', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
loading.classList.remove('show');
|
||||
sendBtn.disabled = false;
|
||||
|
||||
if (data.success) {
|
||||
messageArea.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
✅ ${data.message}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
messageArea.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
❌ ${data.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
loading.classList.remove('show');
|
||||
sendBtn.disabled = false;
|
||||
messageArea.innerHTML = `
|
||||
<div class="alert alert-error">
|
||||
❌ 发送失败:网络错误
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
});
|
||||
|
||||
// 预览功能
|
||||
document.getElementById('preview-btn').addEventListener('click', function() {
|
||||
updatePreview();
|
||||
const previewCard = document.getElementById('preview-card');
|
||||
previewCard.style.display = previewCard.style.display === 'none' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
function updatePreview() {
|
||||
const type = document.getElementById('email-type').value;
|
||||
const recipientName = document.getElementById('recipient_name').value || '用户';
|
||||
const subject = document.getElementById('subject').value;
|
||||
const message = document.getElementById('message').value;
|
||||
|
||||
let previewContent = '';
|
||||
|
||||
switch(type) {
|
||||
case 'welcome':
|
||||
previewContent = `
|
||||
<h4>🎉 欢迎加入我们!</h4>
|
||||
<p>亲爱的 ${recipientName},</p>
|
||||
<p>欢迎您注册成为我们的用户!我们很高兴您能加入我们的大家庭。</p>
|
||||
<p>在这里,您可以享受到优质的服务和丰富的功能。如果您有任何问题,请随时联系我们。</p>
|
||||
<p>祝您使用愉快!</p>
|
||||
<p>此致<br>Hertz Server Django 团队</p>
|
||||
`;
|
||||
break;
|
||||
case 'notification':
|
||||
previewContent = `
|
||||
<h4>🔔 系统通知</h4>
|
||||
<p>亲爱的 ${recipientName},</p>
|
||||
<p>您有一条新的系统通知:</p>
|
||||
<div style="background: #f8f9fa; padding: 1rem; border-left: 4px solid #007bff; margin: 1rem 0;">
|
||||
<p>您的账户设置已更新,如果这不是您的操作,请立即联系我们。</p>
|
||||
</div>
|
||||
<p>如有疑问,请联系客服。</p>
|
||||
<p>此致<br>Hertz Server Django 团队</p>
|
||||
`;
|
||||
break;
|
||||
case 'verification':
|
||||
previewContent = `
|
||||
<h4>🔐 邮箱验证</h4>
|
||||
<p>亲爱的 ${recipientName},</p>
|
||||
<p>请点击下面的链接验证您的邮箱地址:</p>
|
||||
<div style="text-align: center; margin: 2rem 0;">
|
||||
<a href="#" style="background: #667eea; color: white; padding: 1rem 2rem; text-decoration: none; border-radius: 5px;">验证邮箱</a>
|
||||
</div>
|
||||
<p>如果您没有注册账户,请忽略此邮件。</p>
|
||||
<p>此致<br>Hertz Server Django 团队</p>
|
||||
`;
|
||||
break;
|
||||
case 'custom':
|
||||
previewContent = `
|
||||
<h4>${subject || '自定义邮件'}</h4>
|
||||
<p>亲爱的 ${recipientName},</p>
|
||||
<div style="margin: 1rem 0;">
|
||||
${message ? message.replace(/\n/g, '<br>') : '请输入邮件内容'}
|
||||
</div>
|
||||
<p>此致<br>Hertz Server Django 团队</p>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
document.getElementById('preview-content').innerHTML = previewContent;
|
||||
}
|
||||
|
||||
// 初始化预览
|
||||
updatePreview();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
556
hertz_demo/templates/websocket_demo.html
Normal file
556
hertz_demo/templates/websocket_demo.html
Normal file
@@ -0,0 +1,556 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebSocket演示 - Hertz Server Django</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-header {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.demo-header h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.demo-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: inline-block;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-connecting {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message-area {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #f9f9f9;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.3rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.message-system {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.message-user {
|
||||
background: #e8f5e8;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.message-echo {
|
||||
background: #fff3e0;
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
.message-error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.message-notification {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.8rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a6fd8;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #da190b;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chat-controls {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chat-controls input {
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.back-link a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.back-link a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🌐 WebSocket演示</h1>
|
||||
<p>实时通信功能展示 - 支持聊天室和回声测试</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<!-- 回声测试 -->
|
||||
<div class="demo-card">
|
||||
<div class="demo-header">
|
||||
<h3>🔄 回声测试</h3>
|
||||
<p>发送消息,服务器会回声返回</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<div class="connection-status status-disconnected" id="echo-status">未连接</div>
|
||||
<div class="message-area" id="echo-messages"></div>
|
||||
<div class="input-group">
|
||||
<input type="text" class="input-field" id="echo-input" placeholder="输入消息..." disabled>
|
||||
<button class="btn btn-primary" id="echo-send" disabled>发送</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-success" id="echo-connect">连接</button>
|
||||
<button class="btn btn-danger" id="echo-disconnect" disabled>断开</button>
|
||||
<button class="btn btn-primary" id="echo-clear">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聊天室 -->
|
||||
<div class="demo-card">
|
||||
<div class="demo-header">
|
||||
<h3>💬 聊天室</h3>
|
||||
<p>多用户实时聊天功能</p>
|
||||
</div>
|
||||
<div class="demo-content">
|
||||
<div class="connection-status status-disconnected" id="chat-status">未连接</div>
|
||||
<div class="chat-controls">
|
||||
<input type="text" class="input-field" id="username" placeholder="用户名" value="用户" style="width: 120px;">
|
||||
<input type="text" class="input-field" id="room-name" placeholder="房间名" value="general" style="width: 120px;">
|
||||
<button class="btn btn-success" id="chat-connect">加入聊天室</button>
|
||||
<button class="btn btn-danger" id="chat-disconnect" disabled>离开聊天室</button>
|
||||
</div>
|
||||
<div class="message-area" id="chat-messages"></div>
|
||||
<div class="input-group">
|
||||
<input type="text" class="input-field" id="chat-input" placeholder="输入消息..." disabled>
|
||||
<button class="btn btn-primary" id="chat-send" disabled>发送</button>
|
||||
<button class="btn btn-primary" id="chat-clear">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="/">← 返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// WebSocket连接管理
|
||||
let echoSocket = null;
|
||||
let chatSocket = null;
|
||||
|
||||
// DOM元素
|
||||
const echoStatus = document.getElementById('echo-status');
|
||||
const echoMessages = document.getElementById('echo-messages');
|
||||
const echoInput = document.getElementById('echo-input');
|
||||
const echoSendBtn = document.getElementById('echo-send');
|
||||
const echoConnectBtn = document.getElementById('echo-connect');
|
||||
const echoDisconnectBtn = document.getElementById('echo-disconnect');
|
||||
const echoClearBtn = document.getElementById('echo-clear');
|
||||
|
||||
const chatStatus = document.getElementById('chat-status');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatSendBtn = document.getElementById('chat-send');
|
||||
const chatConnectBtn = document.getElementById('chat-connect');
|
||||
const chatDisconnectBtn = document.getElementById('chat-disconnect');
|
||||
const chatClearBtn = document.getElementById('chat-clear');
|
||||
const usernameInput = document.getElementById('username');
|
||||
const roomNameInput = document.getElementById('room-name');
|
||||
|
||||
// 工具函数
|
||||
function addMessage(container, message, type = 'system') {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message message-${type}`;
|
||||
messageDiv.innerHTML = `<strong>[${message.timestamp}]</strong> ${message.message}`;
|
||||
container.appendChild(messageDiv);
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(statusElement, status, text) {
|
||||
statusElement.className = `connection-status status-${status}`;
|
||||
statusElement.textContent = text;
|
||||
}
|
||||
|
||||
// 回声测试WebSocket
|
||||
function connectEcho() {
|
||||
updateStatus(echoStatus, 'connecting', '连接中...');
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/echo/`;
|
||||
|
||||
echoSocket = new WebSocket(wsUrl);
|
||||
|
||||
echoSocket.onopen = function(e) {
|
||||
updateStatus(echoStatus, 'connected', '已连接');
|
||||
echoInput.disabled = false;
|
||||
echoSendBtn.disabled = false;
|
||||
echoConnectBtn.disabled = true;
|
||||
echoDisconnectBtn.disabled = false;
|
||||
};
|
||||
|
||||
echoSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log('收到WebSocket消息:', data); // 调试信息
|
||||
let messageType = 'system';
|
||||
let displayMessage = data;
|
||||
|
||||
if (data.type === 'echo_message') {
|
||||
messageType = 'echo';
|
||||
// 解码Unicode字符并创建显示用的消息对象
|
||||
let echoText = data.echo_message;
|
||||
if (typeof echoText === 'string') {
|
||||
// 处理可能的Unicode编码
|
||||
try {
|
||||
echoText = decodeURIComponent(escape(echoText));
|
||||
} catch (e) {
|
||||
// 如果解码失败,使用原始文本
|
||||
console.log('Unicode解码失败,使用原始文本');
|
||||
}
|
||||
}
|
||||
displayMessage = {
|
||||
message: echoText || data.echo_message || '回声消息',
|
||||
timestamp: data.timestamp || new Date().toLocaleTimeString()
|
||||
};
|
||||
console.log('处理回声消息:', displayMessage); // 调试信息
|
||||
} else if (data.type === 'error') {
|
||||
messageType = 'error';
|
||||
displayMessage = {
|
||||
message: data.message || '发生错误',
|
||||
timestamp: data.timestamp || new Date().toLocaleTimeString()
|
||||
};
|
||||
} else {
|
||||
// 处理其他类型的消息
|
||||
displayMessage = {
|
||||
message: data.message || JSON.stringify(data),
|
||||
timestamp: data.timestamp || new Date().toLocaleTimeString()
|
||||
};
|
||||
}
|
||||
|
||||
addMessage(echoMessages, displayMessage, messageType);
|
||||
};
|
||||
|
||||
echoSocket.onclose = function(e) {
|
||||
updateStatus(echoStatus, 'disconnected', '已断开');
|
||||
echoInput.disabled = true;
|
||||
echoSendBtn.disabled = true;
|
||||
echoConnectBtn.disabled = false;
|
||||
echoDisconnectBtn.disabled = true;
|
||||
};
|
||||
|
||||
echoSocket.onerror = function(e) {
|
||||
updateStatus(echoStatus, 'disconnected', '连接错误');
|
||||
addMessage(echoMessages, {
|
||||
message: 'WebSocket连接发生错误',
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
}, 'error');
|
||||
};
|
||||
}
|
||||
|
||||
function disconnectEcho() {
|
||||
if (echoSocket) {
|
||||
echoSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
function sendEchoMessage() {
|
||||
const message = echoInput.value.trim();
|
||||
if (message && echoSocket && echoSocket.readyState === WebSocket.OPEN) {
|
||||
echoSocket.send(JSON.stringify({
|
||||
'message': message
|
||||
}));
|
||||
echoInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天室WebSocket
|
||||
function connectChat() {
|
||||
const username = usernameInput.value.trim() || '匿名用户';
|
||||
const roomName = roomNameInput.value.trim() || 'general';
|
||||
|
||||
updateStatus(chatStatus, 'connecting', '连接中...');
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/chat/${roomName}/`;
|
||||
|
||||
chatSocket = new WebSocket(wsUrl);
|
||||
|
||||
chatSocket.onopen = function(e) {
|
||||
updateStatus(chatStatus, 'connected', `已连接到房间: ${roomName}`);
|
||||
chatInput.disabled = false;
|
||||
chatSendBtn.disabled = false;
|
||||
chatConnectBtn.disabled = true;
|
||||
chatDisconnectBtn.disabled = false;
|
||||
usernameInput.disabled = true;
|
||||
roomNameInput.disabled = true;
|
||||
|
||||
// 发送加入通知
|
||||
chatSocket.send(JSON.stringify({
|
||||
'type': 'user_join',
|
||||
'username': username
|
||||
}));
|
||||
};
|
||||
|
||||
chatSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
let messageType = 'system';
|
||||
|
||||
if (data.type === 'chat_message') {
|
||||
messageType = 'user';
|
||||
data.message = `${data.username}: ${data.message}`;
|
||||
} else if (data.type === 'user_notification') {
|
||||
messageType = 'notification';
|
||||
} else if (data.type === 'error') {
|
||||
messageType = 'error';
|
||||
}
|
||||
|
||||
addMessage(chatMessages, data, messageType);
|
||||
};
|
||||
|
||||
chatSocket.onclose = function(e) {
|
||||
updateStatus(chatStatus, 'disconnected', '已断开');
|
||||
chatInput.disabled = true;
|
||||
chatSendBtn.disabled = true;
|
||||
chatConnectBtn.disabled = false;
|
||||
chatDisconnectBtn.disabled = true;
|
||||
usernameInput.disabled = false;
|
||||
roomNameInput.disabled = false;
|
||||
};
|
||||
|
||||
chatSocket.onerror = function(e) {
|
||||
updateStatus(chatStatus, 'disconnected', '连接错误');
|
||||
addMessage(chatMessages, {
|
||||
message: 'WebSocket连接发生错误',
|
||||
timestamp: new Date().toLocaleTimeString()
|
||||
}, 'error');
|
||||
};
|
||||
}
|
||||
|
||||
function disconnectChat() {
|
||||
if (chatSocket) {
|
||||
const username = usernameInput.value.trim() || '匿名用户';
|
||||
|
||||
// 发送离开通知
|
||||
chatSocket.send(JSON.stringify({
|
||||
'type': 'user_leave',
|
||||
'username': username
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
chatSocket.close();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function sendChatMessage() {
|
||||
const message = chatInput.value.trim();
|
||||
const username = usernameInput.value.trim() || '匿名用户';
|
||||
|
||||
if (message && chatSocket && chatSocket.readyState === WebSocket.OPEN) {
|
||||
chatSocket.send(JSON.stringify({
|
||||
'type': 'chat_message',
|
||||
'message': message,
|
||||
'username': username
|
||||
}));
|
||||
chatInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 事件监听器
|
||||
echoConnectBtn.addEventListener('click', connectEcho);
|
||||
echoDisconnectBtn.addEventListener('click', disconnectEcho);
|
||||
echoSendBtn.addEventListener('click', sendEchoMessage);
|
||||
echoClearBtn.addEventListener('click', () => {
|
||||
echoMessages.innerHTML = '';
|
||||
});
|
||||
|
||||
echoInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendEchoMessage();
|
||||
}
|
||||
});
|
||||
|
||||
chatConnectBtn.addEventListener('click', connectChat);
|
||||
chatDisconnectBtn.addEventListener('click', disconnectChat);
|
||||
chatSendBtn.addEventListener('click', sendChatMessage);
|
||||
chatClearBtn.addEventListener('click', () => {
|
||||
chatMessages.innerHTML = '';
|
||||
});
|
||||
|
||||
chatInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendChatMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面卸载时断开连接
|
||||
window.addEventListener('beforeunload', function() {
|
||||
if (echoSocket) {
|
||||
echoSocket.close();
|
||||
}
|
||||
if (chatSocket) {
|
||||
disconnectChat();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
3
hertz_demo/tests.py
Normal file
3
hertz_demo/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
11
hertz_demo/urls.py
Normal file
11
hertz_demo/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'hertz_demo'
|
||||
|
||||
urlpatterns = [
|
||||
path('demo/captcha/', views.captcha_demo, name='captcha_demo'),
|
||||
path('demo/websocket/', views.websocket_demo, name='websocket_demo'),
|
||||
path('websocket/test/', views.websocket_test, name='websocket_test'),
|
||||
path('demo/email/', views.email_demo, name='email_demo'),
|
||||
]
|
||||
331
hertz_demo/views.py
Normal file
331
hertz_demo/views.py
Normal file
@@ -0,0 +1,331 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
from django.core.mail import send_mail, EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.contrib import messages
|
||||
from django import forms
|
||||
from hertz_studio_django_captcha.captcha_generator import HertzCaptchaGenerator
|
||||
import json
|
||||
from django.conf import settings
|
||||
import random
|
||||
import string
|
||||
|
||||
class HertzCaptchaForm(forms.Form):
|
||||
"""Hertz验证码表单"""
|
||||
captcha_input = forms.CharField(
|
||||
max_length=10,
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': '请输入验证码',
|
||||
'class': 'form-control',
|
||||
'autocomplete': 'off'
|
||||
}),
|
||||
label='验证码'
|
||||
)
|
||||
captcha_id = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
|
||||
def captcha_demo(request):
|
||||
"""
|
||||
验证码演示页面
|
||||
展示多种验证码功能的使用方法
|
||||
"""
|
||||
# 获取请求的验证码类型
|
||||
captcha_type = request.GET.get('type', 'random_char')
|
||||
|
||||
# 初始化验证码生成器
|
||||
captcha_generator = HertzCaptchaGenerator()
|
||||
|
||||
if request.method == 'POST':
|
||||
# 检查是否是Ajax请求
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
action = data.get('action')
|
||||
|
||||
if action == 'refresh':
|
||||
# 刷新验证码
|
||||
captcha_data = captcha_generator.generate_captcha()
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'data': captcha_data
|
||||
})
|
||||
elif action == 'verify':
|
||||
# 验证验证码
|
||||
captcha_id = data.get('captcha_id', '')
|
||||
user_input = data.get('user_input', '')
|
||||
|
||||
is_valid = captcha_generator.verify_captcha(captcha_id, user_input)
|
||||
|
||||
if is_valid:
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'valid': True,
|
||||
'message': f'验证成功!验证码类型: {captcha_type}'
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'valid': False,
|
||||
'message': '验证码错误,请重新输入'
|
||||
})
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': '请求数据格式错误'
|
||||
})
|
||||
else:
|
||||
# 普通表单提交处理
|
||||
form = HertzCaptchaForm(request.POST)
|
||||
username = request.POST.get('username', '')
|
||||
captcha_id = request.POST.get('captcha_id', '')
|
||||
captcha_input = request.POST.get('captcha_input', '')
|
||||
|
||||
# 验证验证码
|
||||
is_valid = captcha_generator.verify_captcha(captcha_id, captcha_input)
|
||||
|
||||
if is_valid and username:
|
||||
# 生成新的验证码用于显示
|
||||
initial_captcha = captcha_generator.generate_captcha()
|
||||
return render(request, 'captcha_demo.html', {
|
||||
'form': HertzCaptchaForm(),
|
||||
'success_message': f'验证成功!用户名: {username},验证码类型: {captcha_type}',
|
||||
'captcha_unavailable': False,
|
||||
'current_type': captcha_type,
|
||||
'initial_captcha': initial_captcha,
|
||||
'captcha_types': {
|
||||
'random_char': '随机字符验证码',
|
||||
'math': '数学运算验证码',
|
||||
'word': '单词验证码'
|
||||
}
|
||||
})
|
||||
|
||||
# GET请求或表单验证失败时,生成初始验证码
|
||||
form = HertzCaptchaForm()
|
||||
initial_captcha = captcha_generator.generate_captcha()
|
||||
|
||||
return render(request, 'captcha_demo.html', {
|
||||
'form': form,
|
||||
'captcha_unavailable': False,
|
||||
'current_type': captcha_type,
|
||||
'initial_captcha': initial_captcha,
|
||||
'captcha_types': {
|
||||
'random_char': '随机字符验证码',
|
||||
'math': '数学运算验证码',
|
||||
'word': '单词验证码'
|
||||
}
|
||||
})
|
||||
|
||||
def websocket_demo(request):
|
||||
"""WebSocket演示页面"""
|
||||
return render(request, 'websocket_demo.html')
|
||||
|
||||
def websocket_test(request):
|
||||
"""
|
||||
WebSocket简单测试页面
|
||||
"""
|
||||
return render(request, 'websocket_test.html')
|
||||
|
||||
# 测试热重启功能 - 添加注释触发文件变化
|
||||
|
||||
def email_demo(request):
|
||||
"""邮件系统演示页面"""
|
||||
if request.method == 'GET':
|
||||
return render(request, 'email_demo.html')
|
||||
|
||||
elif request.method == 'POST':
|
||||
try:
|
||||
# 获取表单数据
|
||||
email_type = request.POST.get('email_type', 'welcome')
|
||||
recipient_email = request.POST.get('recipient_email')
|
||||
recipient_name = request.POST.get('recipient_name', '用户')
|
||||
custom_subject = request.POST.get('subject', '')
|
||||
custom_message = request.POST.get('message', '')
|
||||
|
||||
if not recipient_email:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': '请输入收件人邮箱地址'
|
||||
})
|
||||
|
||||
# 根据邮件类型生成内容
|
||||
email_content = generate_email_content(email_type, recipient_name, custom_subject, custom_message)
|
||||
|
||||
# 发送邮件
|
||||
success = send_demo_email(
|
||||
recipient_email=recipient_email,
|
||||
subject=email_content['subject'],
|
||||
html_content=email_content['html_content'],
|
||||
text_content=email_content['text_content']
|
||||
)
|
||||
|
||||
if success:
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'邮件已成功发送到 {recipient_email}'
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': '邮件发送失败,请检查邮件配置'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': f'发送失败:{str(e)}'
|
||||
})
|
||||
|
||||
def generate_email_content(email_type, recipient_name, custom_subject='', custom_message=''):
|
||||
"""根据邮件类型生成邮件内容"""
|
||||
|
||||
email_templates = {
|
||||
'welcome': {
|
||||
'subject': '🎉 欢迎加入 Hertz Server Django!',
|
||||
'html_template': f'''
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 28px;">🎉 欢迎加入我们!</h1>
|
||||
</div>
|
||||
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong>,</p>
|
||||
<p>欢迎您注册成为我们的用户!我们很高兴您能加入我们的大家庭。</p>
|
||||
<p>在这里,您可以享受到:</p>
|
||||
<ul style="color: #666;">
|
||||
<li>🔐 安全的验证码系统</li>
|
||||
<li>🌐 实时WebSocket通信</li>
|
||||
<li>📧 完善的邮件服务</li>
|
||||
<li>📚 详细的API文档</li>
|
||||
</ul>
|
||||
<p>如果您有任何问题,请随时联系我们。</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="http://127.0.0.1:8000/" style="background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">开始使用</a>
|
||||
</div>
|
||||
<p>祝您使用愉快!</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
},
|
||||
'notification': {
|
||||
'subject': '🔔 系统通知 - Hertz Server Django',
|
||||
'html_template': f'''
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: #007bff; color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 24px;">🔔 系统通知</h1>
|
||||
</div>
|
||||
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong>,</p>
|
||||
<p>您有一条新的系统通知:</p>
|
||||
<div style="background: #f8f9fa; padding: 20px; border-left: 4px solid #007bff; margin: 20px 0;">
|
||||
<p style="margin: 0; font-weight: 500;">您的账户设置已更新,如果这不是您的操作,请立即联系我们。</p>
|
||||
</div>
|
||||
<p>系统会持续为您提供安全保障,如有疑问请联系客服。</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="http://127.0.0.1:8000/" style="background: #007bff; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">查看详情</a>
|
||||
</div>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
},
|
||||
'verification': {
|
||||
'subject': '🔐 邮箱验证 - Hertz Server Django',
|
||||
'html_template': f'''
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: #28a745; color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 24px;">🔐 邮箱验证</h1>
|
||||
</div>
|
||||
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong>,</p>
|
||||
<p>感谢您注册 Hertz Server Django!请点击下面的按钮验证您的邮箱地址:</p>
|
||||
<div style="text-align: center; margin: 40px 0;">
|
||||
<a href="http://127.0.0.1:8000/verify?token=demo_token" style="background: #28a745; color: white; padding: 15px 40px; text-decoration: none; border-radius: 5px; display: inline-block; font-size: 16px; font-weight: 500;">验证邮箱地址</a>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 14px;">如果按钮无法点击,请复制以下链接到浏览器:<br>
|
||||
<code style="background: #f8f9fa; padding: 5px; border-radius: 3px;">http://127.0.0.1:8000/verify?token=demo_token</code></p>
|
||||
<p style="color: #666;">如果您没有注册账户,请忽略此邮件。此验证链接将在24小时后失效。</p>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
},
|
||||
'custom': {
|
||||
'subject': custom_subject or '自定义邮件 - Hertz Server Django',
|
||||
'html_template': f'''
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
|
||||
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
||||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
|
||||
<h1 style="margin: 0; font-size: 24px;">{custom_subject or '自定义邮件'}</h1>
|
||||
</div>
|
||||
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
|
||||
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong>,</p>
|
||||
<div style="margin: 20px 0; font-size: 16px;">
|
||||
{custom_message.replace(chr(10), '<br>') if custom_message else '这是一封自定义邮件。'}
|
||||
</div>
|
||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
template = email_templates.get(email_type, email_templates['welcome'])
|
||||
html_content = template['html_template']
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
return {
|
||||
'subject': template['subject'],
|
||||
'html_content': html_content,
|
||||
'text_content': text_content
|
||||
}
|
||||
|
||||
def send_demo_email(recipient_email, subject, html_content, text_content):
|
||||
"""发送演示邮件"""
|
||||
try:
|
||||
# 检查邮件配置
|
||||
if not settings.EMAIL_HOST_USER or not settings.EMAIL_HOST_PASSWORD:
|
||||
print("邮件配置不完整,使用控制台输出模式")
|
||||
print(f"收件人: {recipient_email}")
|
||||
print(f"主题: {subject}")
|
||||
print(f"内容: {text_content[:200]}...")
|
||||
return True
|
||||
|
||||
# 创建邮件
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[recipient_email]
|
||||
)
|
||||
|
||||
# 添加HTML内容
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
|
||||
# 发送邮件
|
||||
email.send()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"邮件发送失败: {str(e)}")
|
||||
return False
|
||||
0
hertz_server_django/__init__.py
Normal file
0
hertz_server_django/__init__.py
Normal file
56
hertz_server_django/asgi.py
Normal file
56
hertz_server_django/asgi.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
ASGI config for hertz_server_django project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
|
||||
|
||||
# Import Django first to ensure proper initialization
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
# Initialize Django ASGI application early to ensure the AppRegistry
|
||||
# is populated before importing code that may import ORM models.
|
||||
django_asgi_app = get_asgi_application()
|
||||
|
||||
# Import other modules AFTER Django setup
|
||||
from django.conf import settings
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
|
||||
# Import websocket routing AFTER Django setup to avoid AppRegistryNotReady
|
||||
from hertz_demo import routing as demo_routing
|
||||
|
||||
if 'hertz_studio_django_yolo' in settings.INSTALLED_APPS:
|
||||
from hertz_studio_django_yolo import routing as yolo_routing
|
||||
websocket_urlpatterns = (
|
||||
demo_routing.websocket_urlpatterns +
|
||||
yolo_routing.websocket_urlpatterns
|
||||
)
|
||||
else:
|
||||
websocket_urlpatterns = demo_routing.websocket_urlpatterns
|
||||
|
||||
# 在开发环境下放宽Origin校验,便于第三方客户端(如 Apifox、wscat)调试
|
||||
websocket_app = AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
websocket_urlpatterns
|
||||
)
|
||||
)
|
||||
|
||||
if getattr(settings, 'DEBUG', False):
|
||||
application = ProtocolTypeRouter({
|
||||
"http": django_asgi_app,
|
||||
"websocket": websocket_app,
|
||||
})
|
||||
else:
|
||||
application = ProtocolTypeRouter({
|
||||
"http": django_asgi_app,
|
||||
"websocket": AllowedHostsOriginValidator(websocket_app),
|
||||
})
|
||||
|
||||
351
hertz_server_django/settings.py
Normal file
351
hertz_server_django/settings.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
Django settings for hertz_server_django project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.2.6.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from decouple import config
|
||||
|
||||
# 修复DRF的ip_address_validators函数
|
||||
def fix_drf_ip_validators():
|
||||
"""
|
||||
修复DRF的ip_address_validators函数返回值问题
|
||||
"""
|
||||
try:
|
||||
from rest_framework import fields
|
||||
|
||||
# 保存原始函数
|
||||
original_ip_address_validators = fields.ip_address_validators
|
||||
|
||||
def fixed_ip_address_validators(protocol, unpack_ipv4):
|
||||
"""
|
||||
修复后的ip_address_validators函数,确保返回两个值
|
||||
"""
|
||||
validators = original_ip_address_validators(protocol, unpack_ipv4)
|
||||
# 如果只返回了validators,添加默认的error_message
|
||||
if isinstance(validators, list):
|
||||
return validators, 'Enter a valid IP address.'
|
||||
else:
|
||||
# 如果已经返回了两个值,直接返回
|
||||
return validators
|
||||
|
||||
# 应用猴子补丁
|
||||
fields.ip_address_validators = fixed_ip_address_validators
|
||||
|
||||
except ImportError:
|
||||
# 如果DRF未安装,忽略错误
|
||||
pass
|
||||
|
||||
# 应用修复
|
||||
fix_drf_ip_validators()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = config('SECRET_KEY', default='django-insecure-0a1bx*8!97l^4z#ml#ufn_*9ut*)zlso$*k-g^h&(2=p@^51md')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = config('DEBUG', default=True, cast=bool)
|
||||
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||
|
||||
# Database engine configuration (sqlite/mysql) with backward compatibility
|
||||
# Prefer `DB_ENGINE` env var; fallback to legacy `USE_REDIS_AS_DB`
|
||||
DB_ENGINE = config('DB_ENGINE', default=None)
|
||||
USE_REDIS_AS_DB = config('USE_REDIS_AS_DB', default=True, cast=bool)
|
||||
if DB_ENGINE is None:
|
||||
DB_ENGINE = 'sqlite' if USE_REDIS_AS_DB else 'mysql'
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# Third party apps
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
'channels',
|
||||
'drf_spectacular',
|
||||
|
||||
# 必备注册的app,不要删
|
||||
'hertz_demo', # 初始化演示模块
|
||||
'hertz_studio_django_captcha', # 验证码模块
|
||||
'hertz_studio_django_auth', # 权限模块
|
||||
'hertz_studio_django_system_monitor', # 系统监测模块
|
||||
'hertz_studio_django_log', # 日志管理模块
|
||||
'hertz_studio_django_notice', # 通知模块
|
||||
|
||||
# ======在下面导入你需要的app======
|
||||
'hertz_studio_django_ai', #ai聊天模块
|
||||
'hertz_studio_django_kb', # 知识库 ai和kb库是相互绑定的
|
||||
'hertz_studio_django_wiki', # 文章模块
|
||||
'hertz_studio_django_yolo', # YOLO目标检测模块
|
||||
'hertz_studio_django_yolo_train', # Yolo训练模块
|
||||
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'hertz_studio_django_auth.utils.middleware.AuthMiddleware', # 权限认证中间件
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'hertz_server_django.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [BASE_DIR / 'templates']
|
||||
,
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'hertz_server_django.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
if DB_ENGINE == 'sqlite':
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'data/db.sqlite3',
|
||||
}
|
||||
}
|
||||
# Use Redis-backed sessions when on SQLite (optional, keeps prior behavior)
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
||||
SESSION_CACHE_ALIAS = 'default'
|
||||
elif DB_ENGINE == 'mysql':
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': config('DB_NAME', default='hertz_server'),
|
||||
'USER': config('DB_USER', default='root'),
|
||||
'PASSWORD': config('DB_PASSWORD', default='root'),
|
||||
'HOST': config('DB_HOST', default='localhost'),
|
||||
'PORT': config('DB_PORT', default='3306'),
|
||||
'OPTIONS': {
|
||||
'charset': 'utf8mb4',
|
||||
},
|
||||
}
|
||||
}
|
||||
else:
|
||||
# Fallback to SQLite for unexpected values
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'data/db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
# Redis
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django_redis.cache.RedisCache',
|
||||
'LOCATION': config('REDIS_URL', default='redis://127.0.0.1:6379/0'),
|
||||
'OPTIONS': {
|
||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / 'static',
|
||||
]
|
||||
|
||||
# Media files (User uploaded files)
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Django REST Framework configuration
|
||||
# 使用自定义AuthMiddleware进行认证,不使用DRF的认证和权限系统
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [], # 不使用DRF认证类
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.AllowAny', # 所有接口默认允许访问,由AuthMiddleware控制权限
|
||||
],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||
'PAGE_SIZE': 20,
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||
],
|
||||
}
|
||||
|
||||
# Spectacular (OpenAPI 3.0) configuration
|
||||
SPECTACULAR_SETTINGS = {
|
||||
'TITLE': 'Hertz Server API',
|
||||
'DESCRIPTION': 'API documentation for Hertz Server Django project',
|
||||
'VERSION': '1.0.0',
|
||||
'SERVE_INCLUDE_SCHEMA': False,
|
||||
'COMPONENT_SPLIT_REQUEST': True,
|
||||
'SCHEMA_PATH_PREFIX': '/api/',
|
||||
}
|
||||
|
||||
# CORS configuration
|
||||
CORS_ALLOWED_ORIGINS = config(
|
||||
'CORS_ALLOWED_ORIGINS',
|
||||
default='http://localhost:3000,http://127.0.0.1:3000',
|
||||
cast=lambda v: [s.strip() for s in v.split(',')]
|
||||
)
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = config('CORS_ALLOW_ALL_ORIGINS', default=False, cast=bool)
|
||||
|
||||
# Captcha settings
|
||||
CAPTCHA_IMAGE_SIZE = (
|
||||
config('CAPTCHA_IMAGE_SIZE_WIDTH', default=120, cast=int),
|
||||
config('CAPTCHA_IMAGE_SIZE_HEIGHT', default=50, cast=int)
|
||||
)
|
||||
CAPTCHA_LENGTH = config('CAPTCHA_LENGTH', default=4, cast=int)
|
||||
CAPTCHA_TIMEOUT = config('CAPTCHA_TIMEOUT', default=5, cast=int) # minutes
|
||||
CAPTCHA_FONT_SIZE = config('CAPTCHA_FONT_SIZE', default=40, cast=int)
|
||||
CAPTCHA_BACKGROUND_COLOR = config('CAPTCHA_BACKGROUND_COLOR', default='#ffffff')
|
||||
CAPTCHA_FOREGROUND_COLOR = config('CAPTCHA_FOREGROUND_COLOR', default='#000000')
|
||||
# 验证码词典文件路径
|
||||
CAPTCHA_WORDS_DICTIONARY = str(BASE_DIR / 'captcha_words.txt')
|
||||
# 验证码挑战函数配置
|
||||
CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.random_char_challenge' # 默认使用随机字符
|
||||
# 数学验证码配置
|
||||
CAPTCHA_MATH_CHALLENGE_OPERATOR = '+-*'
|
||||
# 验证码噪声和过滤器
|
||||
CAPTCHA_NOISE_FUNCTIONS = (
|
||||
'captcha.helpers.noise_arcs',
|
||||
'captcha.helpers.noise_dots',
|
||||
)
|
||||
CAPTCHA_FILTER_FUNCTIONS = (
|
||||
'captcha.helpers.post_smooth',
|
||||
)
|
||||
|
||||
# Email configuration
|
||||
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
|
||||
EMAIL_HOST = config('EMAIL_HOST', default='smtp.qq.com')
|
||||
EMAIL_PORT = config('EMAIL_PORT', default=465, cast=int)
|
||||
EMAIL_USE_SSL = config('EMAIL_USE_SSL', default=True, cast=bool)
|
||||
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
|
||||
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='563161210@qq.com')
|
||||
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
|
||||
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='563161210@qq.com')
|
||||
|
||||
# 注册邮箱验证码开关(0=关闭,1=开启)
|
||||
REGISTER_EMAIL_VERIFICATION = config('REGISTER_EMAIL_VERIFICATION', default=0, cast=int)
|
||||
|
||||
# Channels configuration for WebSocket support
|
||||
ASGI_APPLICATION = 'hertz_server_django.asgi.application'
|
||||
|
||||
# Channel layers configuration
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
"hosts": [config('REDIS_URL', default='redis://127.0.0.1:6379/2')],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# 自定义用户模型
|
||||
AUTH_USER_MODEL = 'hertz_studio_django_auth.HertzUser'
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET_KEY = config('JWT_SECRET_KEY', default=SECRET_KEY)
|
||||
JWT_ALGORITHM = 'HS256'
|
||||
JWT_ACCESS_TOKEN_LIFETIME = config('JWT_ACCESS_TOKEN_LIFETIME', default=60 * 60 * 24, cast=int) # 24小时
|
||||
JWT_REFRESH_TOKEN_LIFETIME = config('JWT_REFRESH_TOKEN_LIFETIME', default=60 * 60 * 24 * 7, cast=int) # 7天
|
||||
|
||||
# 权限系统配置
|
||||
HERTZ_AUTH_SETTINGS = {
|
||||
'SUPER_ADMIN_PERMISSIONS': ['*'], # 超级管理员拥有所有权限
|
||||
'DEFAULT_PERMISSIONS': [], # 默认权限
|
||||
}
|
||||
|
||||
# AuthMiddleware配置 - 不需要登录验证的URL模式(支持正则表达式)
|
||||
NO_AUTH_PATTERNS = config(
|
||||
'NO_AUTH_PATTERNS',
|
||||
default=r'^/api/auth/login/?$,^/api/auth/register/?$,^/api/auth/email/code/?$,^/api/auth/send-email-code/?$,^/api/auth/password/reset/?$,^/api/captcha/.*$,^/api/docs/.*$,^/api/redoc/.*$,^/api/schema/.*$,^/admin/.*$,^/static/.*$,^/media/.*$,^/demo/.*$,^/websocket/.*$,^/api/system/.*$',
|
||||
cast=lambda v: [s.strip() for s in v.split(',')]
|
||||
)
|
||||
|
||||
# 密码加密配置
|
||||
PASSWORD_HASHERS = [
|
||||
'hertz_studio_django_utils.crypto.MD5PasswordHasher', # 使用MD5加密
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||
]
|
||||
73
hertz_server_django/urls.py
Normal file
73
hertz_server_django/urls.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
URL configuration for hertz_server_django project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# API documentation routes
|
||||
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
|
||||
# 首页路由
|
||||
path('', views.index, name='index'),
|
||||
|
||||
# Hertz Captcha routes
|
||||
path('api/captcha/', include('hertz_studio_django_captcha.urls')),
|
||||
|
||||
# Hertz Auth routes
|
||||
path('api/', include('hertz_studio_django_auth.urls')),
|
||||
|
||||
# Demo app routes
|
||||
path('', include('hertz_demo.urls')),
|
||||
|
||||
# Hertz System Monitor routes
|
||||
path('api/system/', include('hertz_studio_django_system_monitor.urls')),
|
||||
|
||||
# Hertz Log routes
|
||||
path('api/log/', include('hertz_studio_django_log.urls')),
|
||||
|
||||
# Hertz Notice routes
|
||||
path('api/notice/', include('hertz_studio_django_notice.urls')),
|
||||
|
||||
# ===========在下面添加你需要的路由===========
|
||||
# Hertz AI routes
|
||||
path('api/ai/', include('hertz_studio_django_ai.urls')),
|
||||
|
||||
# Hertz Knowledge Base routes
|
||||
path('api/kb/', include('hertz_studio_django_kb.urls')),
|
||||
|
||||
# Hertz Wiki routes
|
||||
path('api/wiki/', include('hertz_studio_django_wiki.urls')),
|
||||
|
||||
# Hertz YOLO routes
|
||||
path('api/yolo/', include('hertz_studio_django_yolo.urls')),
|
||||
|
||||
# YOLO 训练管理
|
||||
path('api/yolo/train/', include('hertz_studio_django_yolo_train.urls')),
|
||||
|
||||
|
||||
]
|
||||
|
||||
# 在开发环境下提供媒体文件服务
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS[0])
|
||||
13
hertz_server_django/views.py
Normal file
13
hertz_server_django/views.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.shortcuts import render
|
||||
|
||||
from hertz_studio_django_auth.utils.decorators import no_login_required
|
||||
|
||||
|
||||
@no_login_required
|
||||
def index(request):
|
||||
"""
|
||||
系统首页视图
|
||||
展示系统的基础介绍和功能特性
|
||||
"""
|
||||
return render(request, 'index.html')
|
||||
16
hertz_server_django/wsgi.py
Normal file
16
hertz_server_django/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for hertz_server_django project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
25
hertz_server_django_ui/.editorconfig
Normal file
25
hertz_server_django_ui/.editorconfig
Normal file
@@ -0,0 +1,25 @@
|
||||
# EditorConfig配置文件
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.{js,ts,vue}]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
[*.{css,scss,sass}]
|
||||
indent_size = 2
|
||||
10
hertz_server_django_ui/.env
Normal file
10
hertz_server_django_ui/.env
Normal file
@@ -0,0 +1,10 @@
|
||||
# API 基础地址
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
|
||||
# 应用配置
|
||||
VITE_APP_TITLE=Hertz Admin
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# 开发服务器配置
|
||||
VITE_DEV_SERVER_HOST=localhost
|
||||
VITE_DEV_SERVER_PORT=3000
|
||||
2
hertz_server_django_ui/.env.development
Normal file
2
hertz_server_django_ui/.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
VITE_TEMPLATE_SETUP_MODE=true
|
||||
2
hertz_server_django_ui/.env.production
Normal file
2
hertz_server_django_ui/.env.production
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
VITE_TEMPLATE_SETUP_MODE=true
|
||||
24
hertz_server_django_ui/.gitignore
vendored
Normal file
24
hertz_server_django_ui/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
327
hertz_server_django_ui/README.md
Normal file
327
hertz_server_django_ui/README.md
Normal file
@@ -0,0 +1,327 @@
|
||||
<div align="center">
|
||||
|
||||
<h1>通用大模型模板 · Hertz Admin + AI</h1>
|
||||
|
||||
现代化的管理后台前端模板,面向二次开发的前端工程师。内置账号体系、权限路由、主题美化、知识库、YOLO 模型全流程(管理 / 类别 / 告警 / 历史)等典型模块。
|
||||
|
||||
<p>
|
||||
基于 Vite + Vue 3 + TypeScript + Ant Design Vue + Pinia + Vue Router 构建
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ✨ 特性(面向前端)
|
||||
|
||||
- **工程化完善**:TS 强类型、模块化 API、统一请求封装、权限化菜单/路由
|
||||
- **设计统一**:全局“超现代风格”主题,卡片 / 弹窗 / 按钮 / 输入 / 分页风格一致
|
||||
- **业务可复用**:
|
||||
- 文章管理:分类树 + 列表搜索 + 编辑/发布
|
||||
- YOLO 模型:模型管理、模型类别管理、告警处理中心、检测历史管理
|
||||
- AI 助手:多会话列表 + 消息记录 + 多布局对话界面(含错误调试信息)
|
||||
- 认证体系:登录/注册、验证码
|
||||
- **可扩展**:清晰的目录划分和命名规范,方便直接加模块或替换现有实现
|
||||
|
||||
## 🧩 技术栈
|
||||
|
||||
- 构建:Vite
|
||||
- 语言:TypeScript
|
||||
- 框架:Vue 3(Composition API)
|
||||
- UI:Ant Design Vue
|
||||
- 状态:Pinia
|
||||
- 路由:Vue Router
|
||||
|
||||
## 📦 项目结构与职责
|
||||
|
||||
> 根目录:`通用大模型模板/`
|
||||
|
||||
```bash
|
||||
通用大模型模板/
|
||||
└─ hertz_server_diango_ui_2/ # 前端工程(Vite)
|
||||
├─ public/ # 公共静态资源(不走打包器)
|
||||
├─ src/
|
||||
│ ├─ api/ # 接口定义(auth / yolo / knowledge / captcha / ai ...)
|
||||
│ │ └─ yolo.ts # YOLO 模型 & 检测 & 类别相关 API
|
||||
│ ├─ locales/ # 国际化文案
|
||||
│ ├─ router/ # 路由与菜单配置
|
||||
│ │ ├─ admin_menu.ts # 管理端菜单 + 路由映射(权限 key)
|
||||
│ │ ├─ user_menu_ai.ts # 用户端菜单 + 路由映射(含 AI 助手)
|
||||
│ │ └─ index.ts # Vue Router 实例 + 全局路由守卫
|
||||
│ ├─ stores/ # Pinia Store
|
||||
│ │ ├─ hertz_app.ts # 全局应用设置(语言、布局、菜单折叠等)
|
||||
│ │ ├─ hertz_user.ts # 用户 / 鉴权状态
|
||||
│ │ └─ hertz_theme.ts # 主题配置与 CSS 变量
|
||||
│ ├─ styles/ # 全局样式与变量
|
||||
│ │ ├─ index.scss # 全局组件风格覆盖(Button / Table / Modal ...)
|
||||
│ │ └─ variables.scss # 主题色、阴影、圆角等变量
|
||||
│ ├─ utils/ # 工具方法 & 基础设施
|
||||
│ │ ├─ hertz_request.ts # Axios 封装(baseURL、拦截器、错误提示)
|
||||
│ │ ├─ hertz_url.ts # 统一 URL 构造(API / 媒体 / WebSocket)
|
||||
│ │ ├─ hertz_env.ts # 读取 & 校验 env 变量
|
||||
│ │ └─ hertz_router_utils.ts # 路由相关工具 & 调试
|
||||
│ ├─ views/ # 所有页面
|
||||
│ │ ├─ admin_page/ # 管理端页面
|
||||
│ │ │ ├─ ModelManagement.vue # YOLO 模型管理
|
||||
│ │ │ ├─ AlertLevelManagement.vue # 模型类别管理
|
||||
│ │ │ ├─ DetectionHistoryManagement.vue # 检测历史管理
|
||||
│ │ │ └─ ... # 其他管理端模块
|
||||
│ │ ├─ user_pages/ # 用户端页面(检测端 + AI 助手)
|
||||
│ │ │ ├─ index.vue # 用户端主布局 + 顶部导航
|
||||
│ │ │ ├─ AiChat.vue # AI 助手对话页面
|
||||
│ │ │ ├─ YoloDetection.vue # 离线检测页面
|
||||
│ │ │ ├─ LiveDetection.vue # 实时检测页面(WebSocket)
|
||||
│ │ │ └─ ... # 告警中心 / 通知中心 / 知识库等
|
||||
│ │ ├─ Login.vue # 登录页
|
||||
│ │ └─ register.vue # 注册页
|
||||
│ ├─ App.vue # 应用根组件
|
||||
│ └─ main.ts # 入口文件(挂载 Vue / 路由 / Pinia)
|
||||
├─ .env.development # 开发环境变量(前端专用)
|
||||
├─ .env.production # 生产构建环境变量
|
||||
├─ vite.config.ts # Vite 配置(代理、构建、别名等)
|
||||
└─ package.json
|
||||
```
|
||||
|
||||
## 📁 文件与命名规范(建议)
|
||||
|
||||
- **组件 / 页面**
|
||||
- 页面:`src/views/admin_page/FooBarManagement.vue`,以业务 + Management 命名
|
||||
- 纯组件:放到 `src/components/`,使用大驼峰命名,如 `UserSelector.vue`
|
||||
- **接口文件**
|
||||
- 同一业务一个文件:`src/api/yolo.ts`、`src/api/auth.ts`
|
||||
- 内部导出 `xxxApi` 对象 + TS 类型:`type AlertLevel`, `type YoloModel` 等
|
||||
- **样式**
|
||||
- 全局或主题相关:放 `src/styles/`(注意不要在这里写页面私有样式)
|
||||
- 单页面样式:使用 `<style scoped lang="scss">` 写在对应 `.vue` 内
|
||||
- **工具函数**
|
||||
- 通用工具:`src/utils/` 下按领域拆分,如 `hertz_url.ts`、`hertz_env.ts`
|
||||
|
||||
## 🌐 后端 IP / 域名配置指引(前端视角最重要)
|
||||
|
||||
当前工程已经统一了后端地址配置,只需要 **改 2 个地方**:
|
||||
|
||||
1. **环境变量文件**(推荐只改这个)
|
||||
|
||||
- `hertz_server_diango_ui_2/.env.development`
|
||||
- `hertz_server_diango_ui_2/.env.production`
|
||||
|
||||
两个文件里都有一行:
|
||||
|
||||
```bash
|
||||
# 示例:开发环境
|
||||
VITE_API_BASE_URL=http://localhost:8000
|
||||
```
|
||||
|
||||
约定:
|
||||
|
||||
- **只写协议 + 域名/IP + 端口**,不要包含 `/api`
|
||||
- ✅ `http://localhost:8000`
|
||||
- ❌ `http://localhost:8000/api`
|
||||
- 开发与生产可指向不同后端,只要保证同样的接口路径即可。
|
||||
|
||||
2. **Vite 代理 & URL 工具**(已接好,通常不用改)
|
||||
|
||||
- `vite.config.ts`
|
||||
- 利用 `loadEnv` 读取 `VITE_API_BASE_URL`,自动去掉末尾 `/`:
|
||||
|
||||
```ts
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
const apiBaseUrl = env.VITE_API_BASE_URL || 'http://localhost:3000'
|
||||
const backendOrigin = apiBaseUrl.replace(/\/+$/, '')
|
||||
```
|
||||
|
||||
- 开发环境通过代理转发:
|
||||
|
||||
```ts
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': { target: backendOrigin, changeOrigin: true },
|
||||
'/media': { target: backendOrigin, changeOrigin: true }
|
||||
}
|
||||
}
|
||||
|
||||
define: {
|
||||
__VITE_API_BASE_URL__: JSON.stringify(`${backendOrigin}/api`)
|
||||
}
|
||||
```
|
||||
|
||||
- `src/utils/hertz_url.ts`
|
||||
|
||||
- 统一获取后端基础地址:
|
||||
|
||||
```ts
|
||||
export function getBackendBaseUrl(): string {
|
||||
return import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'
|
||||
}
|
||||
```
|
||||
|
||||
- 构造 HTTP / WebSocket / 媒体地址:
|
||||
|
||||
```ts
|
||||
export function getApiBaseUrl() {
|
||||
return import.meta.env.DEV ? '' : getBackendBaseUrl()
|
||||
}
|
||||
|
||||
export function getMediaBaseUrl() {
|
||||
if (import.meta.env.DEV) return ''
|
||||
return getBackendBaseUrl().replace('/api', '')
|
||||
}
|
||||
|
||||
export function getFullFileUrl(relativePath: string) {
|
||||
const baseURL = getBackendBaseUrl()
|
||||
return `${baseURL}${relativePath}`
|
||||
}
|
||||
```
|
||||
|
||||
- `src/utils/hertz_request.ts`
|
||||
|
||||
- Axios 实例的 `baseURL` 在开发环境为空字符串(走 Vite 代理);生产环境使用 `VITE_API_BASE_URL`:
|
||||
|
||||
```ts
|
||||
const isDev = import.meta.env.DEV
|
||||
const baseURL = isDev ? '' : (import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000')
|
||||
```
|
||||
|
||||
👉 **结论:前端同事只需要改 `.env.development` 和 `.env.production` 里的 `VITE_API_BASE_URL`,其余 URL 都通过工具/代理自动生效,无需到处搜 `localhost`。**
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
```bash
|
||||
# 进入工程目录
|
||||
cd hertz_server_diango_ui_2
|
||||
|
||||
# 安装依赖
|
||||
npm i
|
||||
|
||||
# 开发启动(默认 http://localhost:3001)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 🔧 关键模块速览
|
||||
|
||||
- **主题与 Design System**
|
||||
- 入口:`src/styles/index.scss`、`src/styles/variables.scss`
|
||||
- 内容:按钮 / 表格 / 弹窗 / 输入框 等统一风格,含毛玻璃、hover、active、focus 细节
|
||||
|
||||
- **菜单与路由**
|
||||
- `src/router/admin_menu.ts`:单文件维护管理端菜单树 + 路由映射 + 权限标识
|
||||
- 面包屑逻辑已整理:不再重复展示“首页/”,只保留当前层级链路
|
||||
|
||||
- **YOLO 模块**
|
||||
- `ModelManagement.vue`:模型上传 / 列表 / 启用、拖拽上传区
|
||||
- `AlertLevelManagement.vue`:模型类别管理,支持单条 & 批量修改告警等级
|
||||
- `DetectionHistoryManagement.vue`:检测历史列表、图片/视频预览
|
||||
|
||||
- **认证模块**
|
||||
- API:`src/api/auth.ts`
|
||||
- 页面:`src/views/Login.vue`、`src/views/register.vue`
|
||||
- 注册表单字段已与后端约定一致:
|
||||
`username, password, confirm_password, email, phone, real_name, captcha, captcha_id`
|
||||
|
||||
## 🧪 常见问题(FAQ)
|
||||
|
||||
- **需要改哪些地方才能连上新的后端 IP?**
|
||||
- 只改:`.env.development` 和 `.env.production` 的 `VITE_API_BASE_URL`
|
||||
- 不需要:修改页面内的 `http://localhost:xxxx`,已统一收敛到工具函数
|
||||
|
||||
- **接口不走 / 返回字段对不上?**
|
||||
- 对比:`src/api/*.ts` 里定义的请求路径与 payload
|
||||
- 打开浏览器 Network 看真实请求 URL、body 与响应
|
||||
|
||||
- **页面样式和设计稿不一致?**
|
||||
- 先看 `src/styles/index.scss` 是否有全局覆盖
|
||||
- 再查对应 `.vue` 文件中的 scoped 样式是否有特殊处理
|
||||
|
||||
## 🛠️ 二次开发建议
|
||||
|
||||
- **新增管理模块**
|
||||
- 在 `src/views/admin_page/` 下新增页面,如 `FooBarManagement.vue`
|
||||
- 在 `src/router/admin_menu.ts` 中增加菜单配置(path + component + permission)
|
||||
|
||||
- **扩展接口**
|
||||
- 在 `src/api/` 新增 `xxx.ts`,导出 `xxxApi` 对象
|
||||
- 使用统一的 `request` 封装(`hertz_request.ts`),保持错误处理一致
|
||||
|
||||
- **改造主题 / 品牌色**
|
||||
- 修改 `src/styles/variables.scss` 中的主色、背景色、圆角、阴影
|
||||
- 如需大改导航栏、卡片风格,优先在全局样式里做统一,而不是每页重新写
|
||||
|
||||
## 🧩 模块选择与模板模式
|
||||
|
||||
- **模块配置文件**
|
||||
- 路径:`src/config/hertz_modules.ts`
|
||||
- 内容:
|
||||
- 使用 `HERTZ_MODULES` 统一管理“管理端 / 用户端”各功能模块
|
||||
- 每个模块包含:`key`(模块标识)、`label`(展示名称)、`group`(admin/user)、`defaultEnabled`(是否默认启用)
|
||||
- 运行时通过 `isModuleEnabled` / `getEnabledModuleKeys` 控制路由和菜单是否展示对应模块。
|
||||
|
||||
- **模块选择页面(功能 DIY)**
|
||||
- 页面:`src/views/ModuleSetup.vue`
|
||||
- 路由:`/template/modules`
|
||||
- 说明:
|
||||
1. 勾选需要启用的模块,未勾选的模块在菜单和路由中隐藏(仅运行时屏蔽,不改动源码)。
|
||||
2. 点击“保存配置并刷新”可多次预览效果;点击“保存并跳转登录”会在保存后跳转到登录页。
|
||||
3. 选择结果会以 `hertz_enabled_modules` 的形式保存在浏览器 Local Storage 中。
|
||||
|
||||
- **模板模式开关**
|
||||
- 通过环境变量控制:`VITE_TEMPLATE_SETUP_MODE`
|
||||
- 建议在开发环境 (`.env.development`) 中开启:
|
||||
|
||||
```bash
|
||||
VITE_TEMPLATE_SETUP_MODE=true
|
||||
```
|
||||
|
||||
- 当模板模式开启且浏览器中 **没有** `hertz_enabled_modules` 记录时,路由守卫会在首次进入时自动重定向到 `/template/modules`,强制先完成模块选择。
|
||||
- 如果已经配置过模块,下次 `npm run dev` 将直接进入系统。如需重新进入模块选择页:
|
||||
1. 打开浏览器开发者工具 → Application → Local Storage
|
||||
2. 选择当前站点,删除键 `hertz_enabled_modules`
|
||||
3. 刷新页面即可再次进入模块选择流程。
|
||||
|
||||
## ✂️ 一键裁剪(npm run prune)
|
||||
|
||||
> 适用于已经确定“哪些功能模块不再需要”的场景,用于真正瘦身前端代码体积。建议在执行前先提交一次 Git。
|
||||
|
||||
- **脚本位置与命令**
|
||||
- 脚本:`scripts/prune-modules.mjs`
|
||||
- 命令:
|
||||
|
||||
```bash
|
||||
npm run prune
|
||||
```
|
||||
|
||||
- **推荐使用流程**
|
||||
1. 启动开发环境:`npm run dev`。
|
||||
2. 打开 `/template/modules`,通过勾选确认“需要保留的模块”,用“保存配置并刷新”反复调试菜单/路由效果。
|
||||
3. 确认无误后,关闭开发服务器。
|
||||
4. 在终端执行 `npm run prune`,按照 CLI 提示:
|
||||
- 选择要“裁剪掉”的模块(通常是你在模块选择页面中未勾选的模块)。
|
||||
- 选择裁剪模式:
|
||||
- **模式 1:仅屏蔽**
|
||||
- 修改 `admin_menu.ts` / `user_menu_ai.ts` 中对应模块的 `moduleKey`,加上 `__pruned__` 前缀
|
||||
- 注释组件映射行,使这些模块在菜单和路由中完全隐藏
|
||||
- **不删除任何 `.vue` 文件**,方便后续恢复
|
||||
- **模式 2:删除**
|
||||
- 在模式 1 的基础上,额外删除对应模块的视图文件,如 `src/views/admin_page/UserManagement.vue` 等
|
||||
- 这是不可逆操作,建议先在模式 1 下验证,再使用模式 2 做最终瘦身
|
||||
|
||||
- **影响范围(前端)**
|
||||
- 管理端:
|
||||
- `src/router/admin_menu.ts` 中对应模块的菜单配置和组件映射
|
||||
- `src/views/admin_page/*.vue` 中不需要的页面(仅在删除模式下移除)
|
||||
- 用户端:
|
||||
- `src/router/user_menu_ai.ts` 中对应模块配置
|
||||
- `src/views/user_pages/*.vue` 中不需要的页面(仅在删除模式下移除)
|
||||
|
||||
## 📜 NPM 脚本
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"prune": "node scripts/prune-modules.mjs"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
80
hertz_server_django_ui/components.d.ts
vendored
Normal file
80
hertz_server_django_ui/components.d.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AAlert: typeof import('ant-design-vue/es')['Alert']
|
||||
AAvatar: typeof import('ant-design-vue/es')['Avatar']
|
||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
|
||||
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ACard: typeof import('ant-design-vue/es')['Card']
|
||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
|
||||
ACol: typeof import('ant-design-vue/es')['Col']
|
||||
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
|
||||
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
|
||||
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
|
||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
||||
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
||||
AEmpty: typeof import('ant-design-vue/es')['Empty']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AImage: typeof import('ant-design-vue/es')['Image']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputGroup: typeof import('ant-design-vue/es')['InputGroup']
|
||||
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
|
||||
ALayout: typeof import('ant-design-vue/es')['Layout']
|
||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||
ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
|
||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
||||
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
|
||||
AList: typeof import('ant-design-vue/es')['List']
|
||||
AListItem: typeof import('ant-design-vue/es')['ListItem']
|
||||
AListItemMeta: typeof import('ant-design-vue/es')['ListItemMeta']
|
||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
AModal: typeof import('ant-design-vue/es')['Modal']
|
||||
APagination: typeof import('ant-design-vue/es')['Pagination']
|
||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
|
||||
AResult: typeof import('ant-design-vue/es')['Result']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||
ASlider: typeof import('ant-design-vue/es')['Slider']
|
||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||
AStatistic: typeof import('ant-design-vue/es')['Statistic']
|
||||
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
|
||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
||||
ATable: typeof import('ant-design-vue/es')['Table']
|
||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||
ATextarea: typeof import('ant-design-vue/es')['Textarea']
|
||||
ATimeline: typeof import('ant-design-vue/es')['Timeline']
|
||||
ATimelineItem: typeof import('ant-design-vue/es')['TimelineItem']
|
||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||
ATree: typeof import('ant-design-vue/es')['Tree']
|
||||
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
|
||||
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
81
hertz_server_django_ui/eslint.config.js
Normal file
81
hertz_server_django_ui/eslint.config.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import js from '@eslint/js'
|
||||
import vue from 'eslint-plugin-vue'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default [
|
||||
// JavaScript 推荐规则
|
||||
js.configs.recommended,
|
||||
|
||||
// TypeScript 推荐规则
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
// Vue 推荐规则
|
||||
...vue.configs['flat/recommended'],
|
||||
|
||||
// 项目特定配置
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,ts,vue}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
// 浏览器环境
|
||||
console: 'readonly',
|
||||
window: 'readonly',
|
||||
document: 'readonly',
|
||||
navigator: 'readonly',
|
||||
fetch: 'readonly',
|
||||
// Node.js环境
|
||||
process: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
module: 'readonly',
|
||||
require: 'readonly',
|
||||
global: 'readonly',
|
||||
// Vite环境
|
||||
import: 'readonly',
|
||||
// Vue环境
|
||||
Vue: 'readonly',
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
// 禁用所有可能导致WebStorm警告的规则
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-debugger': 'off',
|
||||
'no-alert': 'off',
|
||||
'no-prototype-builtins': 'off',
|
||||
|
||||
// Vue相关规则
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-unused-vars': 'off',
|
||||
'vue/no-unused-components': 'off',
|
||||
'vue/no-unused-properties': 'off',
|
||||
'vue/require-v-for-key': 'off',
|
||||
'vue/no-use-v-if-with-v-for': 'off',
|
||||
|
||||
// TypeScript规则
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/prefer-const': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
}
|
||||
},
|
||||
|
||||
// 忽略文件
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'dist/**',
|
||||
'.git/**',
|
||||
'coverage/**',
|
||||
'*.config.js',
|
||||
'*.config.ts',
|
||||
]
|
||||
}
|
||||
]
|
||||
13
hertz_server_django_ui/index.html
Normal file
13
hertz_server_django_ui/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理系统模板</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
5673
hertz_server_django_ui/package-lock.json
generated
Normal file
5673
hertz_server_django_ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
hertz_server_django_ui/package.json
Normal file
40
hertz_server_django_ui/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "hertz_server_django_ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"prune": "node scripts/prune-modules.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^24.5.2",
|
||||
"ant-design-vue": "^3.2.20",
|
||||
"axios": "^1.12.2",
|
||||
"echarts": "^6.0.0",
|
||||
"jszip": "^3.10.1",
|
||||
"onnxruntime-web": "^1.23.2",
|
||||
"pinia": "^3.0.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vue": "^3.5.21",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"postcss": "^8.5.6",
|
||||
"sass-embedded": "^1.93.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.44.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.1.6",
|
||||
"vue-tsc": "^3.0.7"
|
||||
}
|
||||
}
|
||||
1
hertz_server_django_ui/public/models/manifest.json
Normal file
1
hertz_server_django_ui/public/models/manifest.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
1
hertz_server_django_ui/public/vite.svg
Normal file
1
hertz_server_django_ui/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
392
hertz_server_django_ui/scripts/prune-modules.mjs
Normal file
392
hertz_server_django_ui/scripts/prune-modules.mjs
Normal file
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// 一键裁剪脚本:根据功能模块删除或屏蔽对应的菜单配置和页面文件
|
||||
// 设计原则:
|
||||
// - 先通过运行时模块开关/页面确认要保留哪些模块
|
||||
// - 然后运行本脚本,选择要“裁剪掉”的模块,以及裁剪模式:
|
||||
// 1) 仅屏蔽(修改 moduleKey,使其永远不会被启用,保留页面文件)
|
||||
// 2) 删除(在 1 的基础上,再删除对应 .vue 页面文件)
|
||||
// - 脚本只操作前端代码,不影响后端
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import readline from 'readline'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const projectRoot = path.resolve(__dirname, '..')
|
||||
|
||||
/** 模块定义(与 src/config/hertz_modules.ts 保持一致) */
|
||||
const MODULES = [
|
||||
{ key: 'admin.user-management', label: '管理端 · 用户管理', group: 'admin' },
|
||||
{ key: 'admin.department-management', label: '管理端 · 部门管理', group: 'admin' },
|
||||
{ key: 'admin.menu-management', label: '管理端 · 菜单管理', group: 'admin' },
|
||||
{ key: 'admin.role-management', label: '管理端 · 角色管理', group: 'admin' },
|
||||
{ key: 'admin.notification-management', label: '管理端 · 通知管理', group: 'admin' },
|
||||
{ key: 'admin.log-management', label: '管理端 · 日志管理', group: 'admin' },
|
||||
{ key: 'admin.knowledge-base', label: '管理端 · 文章管理', group: 'admin' },
|
||||
{ key: 'admin.yolo-model', label: '管理端 · YOLO 模型相关', group: 'admin' },
|
||||
|
||||
{ key: 'user.system-monitor', label: '用户端 · 系统监控', group: 'user' },
|
||||
{ key: 'user.ai-chat', label: '用户端 · AI 助手', group: 'user' },
|
||||
{ key: 'user.yolo-detection', label: '用户端 · YOLO 检测', group: 'user' },
|
||||
{ key: 'user.live-detection', label: '用户端 · 实时检测', group: 'user' },
|
||||
{ key: 'user.detection-history', label: '用户端 · 检测历史', group: 'user' },
|
||||
{ key: 'user.alert-center', label: '用户端 · 告警中心', group: 'user' },
|
||||
{ key: 'user.notice-center', label: '用户端 · 通知中心', group: 'user' },
|
||||
{ key: 'user.knowledge-center', label: '用户端 · 知识库中心', group: 'user' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 每个模块对应的裁剪配置:
|
||||
* - adminModuleKey / userModuleKey: 在路由配置文件中的 moduleKey 值
|
||||
* - adminComponentNames / userComponentNames: 在组件映射对象中的组件名(*.vue)
|
||||
* - viewFiles: 可以安全删除的页面文件(相对项目根路径)
|
||||
*/
|
||||
const PRUNE_CONFIG = {
|
||||
'admin.user-management': {
|
||||
adminModuleKey: 'admin.user-management',
|
||||
adminComponentNames: ['UserManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/UserManagement.vue'],
|
||||
},
|
||||
'admin.department-management': {
|
||||
adminModuleKey: 'admin.department-management',
|
||||
adminComponentNames: ['DepartmentManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/DepartmentManagement.vue'],
|
||||
},
|
||||
'admin.menu-management': {
|
||||
adminModuleKey: 'admin.menu-management',
|
||||
adminComponentNames: ['MenuManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/MenuManagement.vue'],
|
||||
},
|
||||
'admin.role-management': {
|
||||
adminModuleKey: 'admin.role-management',
|
||||
adminComponentNames: ['Role.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/Role.vue'],
|
||||
},
|
||||
'admin.notification-management': {
|
||||
adminModuleKey: 'admin.notification-management',
|
||||
adminComponentNames: ['NotificationManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/NotificationManagement.vue'],
|
||||
},
|
||||
'admin.log-management': {
|
||||
adminModuleKey: 'admin.log-management',
|
||||
adminComponentNames: ['LogManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/LogManagement.vue'],
|
||||
},
|
||||
'admin.knowledge-base': {
|
||||
adminModuleKey: 'admin.knowledge-base',
|
||||
adminComponentNames: ['KnowledgeBaseManagement.vue'],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: ['src/views/admin_page/KnowledgeBaseManagement.vue'],
|
||||
},
|
||||
'admin.yolo-model': {
|
||||
adminModuleKey: 'admin.yolo-model',
|
||||
adminComponentNames: [
|
||||
'ModelManagement.vue',
|
||||
'AlertLevelManagement.vue',
|
||||
'AlertProcessingCenter.vue',
|
||||
'DetectionHistoryManagement.vue',
|
||||
],
|
||||
userModuleKey: null,
|
||||
userComponentNames: [],
|
||||
viewFiles: [
|
||||
'src/views/admin_page/ModelManagement.vue',
|
||||
'src/views/admin_page/AlertLevelManagement.vue',
|
||||
'src/views/admin_page/AlertProcessingCenter.vue',
|
||||
'src/views/admin_page/DetectionHistoryManagement.vue',
|
||||
],
|
||||
},
|
||||
'user.system-monitor': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.system-monitor',
|
||||
userComponentNames: ['SystemMonitor.vue'],
|
||||
viewFiles: ['src/views/user_pages/SystemMonitor.vue'],
|
||||
},
|
||||
'user.ai-chat': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.ai-chat',
|
||||
userComponentNames: ['AiChat.vue'],
|
||||
viewFiles: ['src/views/user_pages/AiChat.vue'],
|
||||
},
|
||||
'user.yolo-detection': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.yolo-detection',
|
||||
userComponentNames: ['YoloDetection.vue'],
|
||||
viewFiles: ['src/views/user_pages/YoloDetection.vue'],
|
||||
},
|
||||
'user.live-detection': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.live-detection',
|
||||
userComponentNames: ['LiveDetection.vue'],
|
||||
viewFiles: ['src/views/user_pages/LiveDetection.vue'],
|
||||
},
|
||||
'user.detection-history': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.detection-history',
|
||||
userComponentNames: ['DetectionHistory.vue'],
|
||||
viewFiles: ['src/views/user_pages/DetectionHistory.vue'],
|
||||
},
|
||||
'user.alert-center': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.alert-center',
|
||||
userComponentNames: ['AlertCenter.vue'],
|
||||
viewFiles: ['src/views/user_pages/AlertCenter.vue'],
|
||||
},
|
||||
'user.notice-center': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.notice-center',
|
||||
userComponentNames: ['NoticeCenter.vue'],
|
||||
viewFiles: ['src/views/user_pages/NoticeCenter.vue'],
|
||||
},
|
||||
'user.knowledge-center': {
|
||||
adminModuleKey: null,
|
||||
adminComponentNames: [],
|
||||
userModuleKey: 'user.knowledge-center',
|
||||
userComponentNames: ['KnowledgeCenter.vue'],
|
||||
// 注意:这里只删除 KnowledgeCenter.vue,保留 KnowledgeDetail.vue,避免复杂路由修改
|
||||
viewFiles: ['src/views/user_pages/KnowledgeCenter.vue'],
|
||||
},
|
||||
}
|
||||
|
||||
const ADMIN_MENU_FILE = 'src/router/admin_menu.ts'
|
||||
const USER_MENU_FILE = 'src/router/user_menu_ai.ts'
|
||||
|
||||
/**
|
||||
* 简单的 CLI 交互封装
|
||||
*/
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
||||
|
||||
function ask(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer.trim())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function resolvePath(relativePath) {
|
||||
return path.resolve(projectRoot, relativePath)
|
||||
}
|
||||
|
||||
function readTextFile(relativePath) {
|
||||
const full = resolvePath(relativePath)
|
||||
if (!fs.existsSync(full)) {
|
||||
return null
|
||||
}
|
||||
return fs.readFileSync(full, 'utf8')
|
||||
}
|
||||
|
||||
function writeTextFile(relativePath, content) {
|
||||
const full = resolvePath(relativePath)
|
||||
fs.writeFileSync(full, content, 'utf8')
|
||||
}
|
||||
|
||||
function commentComponentLines(content, componentNames) {
|
||||
if (!componentNames || componentNames.length === 0) return content
|
||||
const lines = content.split('\n')
|
||||
const nameSet = new Set(componentNames)
|
||||
|
||||
const updated = lines.map((line) => {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith('//')) return line
|
||||
|
||||
for (const name of nameSet) {
|
||||
if (line.includes(`'${name}'`)) {
|
||||
return `// ${line}`
|
||||
}
|
||||
}
|
||||
return line
|
||||
})
|
||||
|
||||
return updated.join('\n')
|
||||
}
|
||||
|
||||
function updateModuleKey(content, originalKey) {
|
||||
if (!originalKey) return content
|
||||
const patterns = [
|
||||
`moduleKey: '${originalKey}'`,
|
||||
`moduleKey: "${originalKey}"`,
|
||||
]
|
||||
const pruned = `moduleKey: '__pruned__${originalKey}'`
|
||||
|
||||
if (content.includes(pruned)) {
|
||||
return content
|
||||
}
|
||||
|
||||
let updated = content
|
||||
let found = false
|
||||
|
||||
for (const p of patterns) {
|
||||
if (updated.includes(p)) {
|
||||
updated = updated.replace(p, pruned)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
console.warn(`⚠️ 未在文件中找到 moduleKey: ${originalKey}`)
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
function collectViewFilesForModules(selectedKeys) {
|
||||
const files = new Set()
|
||||
for (const key of selectedKeys) {
|
||||
const cfg = PRUNE_CONFIG[key]
|
||||
if (!cfg) continue
|
||||
for (const f of cfg.viewFiles) {
|
||||
files.add(f)
|
||||
}
|
||||
}
|
||||
return Array.from(files)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('===== Hertz 模板 · 一键裁剪脚本 =====')
|
||||
console.log('说明:')
|
||||
console.log('1. 建议先在浏览器里通过“模板模式 + 模块选择页”确认要保留的模块')
|
||||
console.log('2. 然后关闭 dev 服务器,运行本脚本选择要裁剪掉的模块')
|
||||
console.log('3. 先可选择“仅屏蔽”,确认无误后,再选择“删除”彻底缩减代码体积')
|
||||
console.log('')
|
||||
|
||||
console.log('当前可裁剪模块:')
|
||||
MODULES.forEach((m, index) => {
|
||||
console.log(`${index + 1}. [${m.group}] ${m.label} (${m.key})`)
|
||||
})
|
||||
console.log('')
|
||||
|
||||
const indexAnswer = await ask('请输入要“裁剪掉”的模块序号(多个用逗号分隔,例如 2,4,7),或直接回车取消:')
|
||||
if (!indexAnswer) {
|
||||
console.log('未选择任何模块,退出。')
|
||||
rl.close()
|
||||
return
|
||||
}
|
||||
|
||||
const indexes = indexAnswer
|
||||
.split(',')
|
||||
.map((s) => parseInt(s.trim(), 10))
|
||||
.filter((n) => !Number.isNaN(n) && n >= 1 && n <= MODULES.length)
|
||||
|
||||
if (indexes.length === 0) {
|
||||
console.log('未解析出有效的序号,退出。')
|
||||
rl.close()
|
||||
return
|
||||
}
|
||||
|
||||
const selectedModules = Array.from(new Set(indexes.map((i) => MODULES[i - 1])))
|
||||
console.log('\n将要裁剪的模块:')
|
||||
selectedModules.forEach((m) => {
|
||||
console.log(`- [${m.group}] ${m.label} (${m.key})`)
|
||||
})
|
||||
|
||||
console.log('\n裁剪模式:')
|
||||
console.log('1) 仅屏蔽模块:')
|
||||
console.log(' - 修改 router 配置中的 moduleKey 为 __pruned__...')
|
||||
console.log(' - 生成的菜单和路由中将完全隐藏这些模块')
|
||||
console.log(' - 不删除任何 .vue 页面文件(可随时恢复)')
|
||||
console.log('2) 删除模块:')
|
||||
console.log(' - 在 1 的基础上,额外删除对应的 .vue 页面文件')
|
||||
console.log(' - 删除操作不可逆,请确保已经提交或备份代码\n')
|
||||
|
||||
const modeAnswer = await ask('请选择裁剪模式(1 = 仅屏蔽,2 = 删除):')
|
||||
const mode = modeAnswer === '2' ? 'delete' : 'comment'
|
||||
|
||||
const viewFiles = collectViewFilesForModules(selectedModules.map((m) => m.key))
|
||||
|
||||
console.log('\n即将进行如下修改:')
|
||||
console.log('- 修改文件: src/router/admin_menu.ts(按需)')
|
||||
console.log('- 修改文件: src/router/user_menu_ai.ts(按需)')
|
||||
if (mode === 'delete') {
|
||||
console.log('- 删除页面文件:')
|
||||
viewFiles.forEach((f) => console.log(` · ${f}`))
|
||||
} else {
|
||||
console.log('- 不删除任何页面文件,仅屏蔽模块')
|
||||
}
|
||||
|
||||
const confirm = await ask('\n确认执行这些修改吗?(y/N): ')
|
||||
if (confirm.toLowerCase() !== 'y') {
|
||||
console.log('已取消操作。')
|
||||
rl.close()
|
||||
return
|
||||
}
|
||||
|
||||
// 1) 修改 admin_menu.ts
|
||||
let adminMenuContent = readTextFile(ADMIN_MENU_FILE)
|
||||
if (adminMenuContent) {
|
||||
const adminKeys = selectedModules.map((m) => m.key).filter((k) => PRUNE_CONFIG[k]?.adminModuleKey)
|
||||
if (adminKeys.length > 0) {
|
||||
for (const key of adminKeys) {
|
||||
const cfg = PRUNE_CONFIG[key]
|
||||
adminMenuContent = updateModuleKey(adminMenuContent, cfg.adminModuleKey)
|
||||
adminMenuContent = commentComponentLines(adminMenuContent, cfg.adminComponentNames)
|
||||
}
|
||||
writeTextFile(ADMIN_MENU_FILE, adminMenuContent)
|
||||
console.log('✅ 已更新 src/router/admin_menu.ts')
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 修改 user_menu_ai.ts
|
||||
let userMenuContent = readTextFile(USER_MENU_FILE)
|
||||
if (userMenuContent) {
|
||||
const userKeys = selectedModules.map((m) => m.key).filter((k) => PRUNE_CONFIG[k]?.userModuleKey)
|
||||
if (userKeys.length > 0) {
|
||||
for (const key of userKeys) {
|
||||
const cfg = PRUNE_CONFIG[key]
|
||||
userMenuContent = updateModuleKey(userMenuContent, cfg.userModuleKey)
|
||||
userMenuContent = commentComponentLines(userMenuContent, cfg.userComponentNames)
|
||||
}
|
||||
writeTextFile(USER_MENU_FILE, userMenuContent)
|
||||
console.log('✅ 已更新 src/router/user_menu_ai.ts')
|
||||
}
|
||||
}
|
||||
|
||||
// 3) 删除 .vue 页面文件(仅在 delete 模式下)
|
||||
if (mode === 'delete') {
|
||||
console.log('\n开始删除页面文件...')
|
||||
for (const relative of viewFiles) {
|
||||
const full = resolvePath(relative)
|
||||
if (fs.existsSync(full)) {
|
||||
fs.rmSync(full)
|
||||
console.log(`🗑️ 已删除: ${relative}`)
|
||||
} else {
|
||||
console.log(`⚠️ 文件不存在,跳过: ${relative}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n🎉 裁剪完成。建议执行以下操作检查:')
|
||||
console.log('- 重新运行: npm run dev')
|
||||
console.log('- 在浏览器中确认菜单和路由是否符合预期')
|
||||
console.log('- 如需恢复,请使用 Git 回退或重新拷贝模板')
|
||||
|
||||
rl.close()
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('执行过程中发生错误:', err)
|
||||
rl.close()
|
||||
process.exit(1)
|
||||
})
|
||||
85
hertz_server_django_ui/src/App.vue
Normal file
85
hertz_server_django_ui/src/App.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useUserStore } from './stores/hertz_user'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { ConfigProvider } from 'ant-design-vue'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import enUS from 'ant-design-vue/es/locale/en_US'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 主题配置 - 简约现代风格
|
||||
const theme = ref({
|
||||
algorithm: 'default' as 'default' | 'dark' | 'compact',
|
||||
token: {
|
||||
colorPrimary: '#2563eb',
|
||||
colorSuccess: '#10b981',
|
||||
colorWarning: '#f59e0b',
|
||||
colorError: '#ef4444',
|
||||
borderRadius: 8,
|
||||
fontSize: 14,
|
||||
},
|
||||
})
|
||||
|
||||
// 语言配置
|
||||
const locale = ref(zhCN)
|
||||
|
||||
// 主题切换
|
||||
const toggleTheme = () => {
|
||||
const currentTheme = localStorage.getItem('theme') || 'light'
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light'
|
||||
|
||||
localStorage.setItem('theme', newTheme)
|
||||
|
||||
if (newTheme === 'dark') {
|
||||
theme.value.algorithm = 'dark'
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
theme.value.algorithm = 'default'
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化主题
|
||||
onMounted(() => {
|
||||
const savedTheme = localStorage.getItem('theme') || 'light'
|
||||
if (savedTheme === 'dark') {
|
||||
theme.value.algorithm = 'dark'
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
} else {
|
||||
theme.value.algorithm = 'default'
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const showLayout = computed(() => {
|
||||
return userStore.isLoggedIn
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app">
|
||||
<ConfigProvider :theme="theme" :locale="locale">
|
||||
<RouterView />
|
||||
</ConfigProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
96
hertz_server_django_ui/src/api/ai.ts
Normal file
96
hertz_server_django_ui/src/api/ai.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 会话与消息类型
|
||||
export interface AIChatItem {
|
||||
id: number
|
||||
title: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
latest_message?: string
|
||||
}
|
||||
|
||||
export interface AIChatDetail {
|
||||
id: number
|
||||
title: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AIChatMessage {
|
||||
id: number
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ChatListData {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
chats: AIChatItem[]
|
||||
}
|
||||
|
||||
export interface ChatDetailData {
|
||||
chat: AIChatDetail
|
||||
messages: AIChatMessage[]
|
||||
}
|
||||
|
||||
export interface SendMessageData {
|
||||
user_message: AIChatMessage
|
||||
ai_message: AIChatMessage
|
||||
}
|
||||
|
||||
// 将后端可能返回的 chat_id 统一规范为 id
|
||||
const normalizeChatItem = (raw: any): AIChatItem => ({
|
||||
id: typeof raw?.id === 'number' ? raw.id : Number(raw?.chat_id),
|
||||
title: raw?.title,
|
||||
created_at: raw?.created_at,
|
||||
updated_at: raw?.updated_at,
|
||||
latest_message: raw?.latest_message,
|
||||
})
|
||||
|
||||
const normalizeChatDetail = (raw: any): AIChatDetail => ({
|
||||
id: typeof raw?.id === 'number' ? raw.id : Number(raw?.chat_id),
|
||||
title: raw?.title,
|
||||
created_at: raw?.created_at,
|
||||
updated_at: raw?.updated_at,
|
||||
})
|
||||
|
||||
export const aiApi = {
|
||||
listChats: (params?: { query?: string; page?: number; page_size?: number }): Promise<ApiResponse<ChatListData>> =>
|
||||
request.get('/api/ai/chats/', { params, showError: false }).then((resp: any) => {
|
||||
if (resp?.data?.chats && Array.isArray(resp.data.chats)) {
|
||||
resp.data.chats = resp.data.chats.map((c: any) => normalizeChatItem(c))
|
||||
}
|
||||
return resp as ApiResponse<ChatListData>
|
||||
}),
|
||||
|
||||
createChat: (body?: { title?: string }): Promise<ApiResponse<AIChatDetail>> =>
|
||||
request.post('/api/ai/chats/create/', body || { title: '新对话' }).then((resp: any) => {
|
||||
if (resp?.data) resp.data = normalizeChatDetail(resp.data)
|
||||
return resp as ApiResponse<AIChatDetail>
|
||||
}),
|
||||
|
||||
getChatDetail: (chatId: number): Promise<ApiResponse<ChatDetailData>> =>
|
||||
request.get(`/api/ai/chats/${chatId}/`).then((resp: any) => {
|
||||
if (resp?.data?.chat) resp.data.chat = normalizeChatDetail(resp.data.chat)
|
||||
return resp as ApiResponse<ChatDetailData>
|
||||
}),
|
||||
|
||||
updateChat: (chatId: number, body: { title: string }): Promise<ApiResponse<null>> =>
|
||||
request.put(`/api/ai/chats/${chatId}/update/`, body),
|
||||
|
||||
deleteChats: (chatIds: number[]): Promise<ApiResponse<null>> =>
|
||||
request.post('/api/ai/chats/delete/', { chat_ids: chatIds }),
|
||||
|
||||
sendMessage: (chatId: number, body: { content: string }): Promise<ApiResponse<SendMessageData>> =>
|
||||
request.post(`/api/ai/chats/${chatId}/send/`, body),
|
||||
}
|
||||
47
hertz_server_django_ui/src/api/auth.ts
Normal file
47
hertz_server_django_ui/src/api/auth.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 注册接口数据类型
|
||||
export interface RegisterData {
|
||||
username: string
|
||||
password: string
|
||||
confirm_password: string
|
||||
email: string
|
||||
phone: string
|
||||
real_name: string
|
||||
captcha: string
|
||||
captcha_id: string
|
||||
}
|
||||
|
||||
// 发送邮箱验证码数据类型
|
||||
export interface SendEmailCodeData {
|
||||
email: string
|
||||
code_type: string
|
||||
}
|
||||
|
||||
// 登录接口数据类型
|
||||
export interface LoginData {
|
||||
username: string
|
||||
password: string
|
||||
captcha_code: string
|
||||
captcha_key: string
|
||||
}
|
||||
|
||||
// 注册API
|
||||
export const registerUser = (data: RegisterData) => {
|
||||
return request.post('/api/auth/register/', data)
|
||||
}
|
||||
|
||||
// 登录API
|
||||
export const loginUser = (data: LoginData) => {
|
||||
return request.post('/api/auth/login/', data)
|
||||
}
|
||||
|
||||
// 发送邮箱验证码API
|
||||
export const sendEmailCode = (data: SendEmailCodeData) => {
|
||||
return request.post('/api/auth/email/code/', data)
|
||||
}
|
||||
|
||||
// 登出API
|
||||
export const logoutUser = () => {
|
||||
return request.post('/api/auth/logout/')
|
||||
}
|
||||
89
hertz_server_django_ui/src/api/captcha.ts
Normal file
89
hertz_server_django_ui/src/api/captcha.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 验证码相关接口类型定义
|
||||
export interface CaptchaResponse {
|
||||
captcha_id: string
|
||||
image_data: string // base64编码的图片
|
||||
expires_in: number // 过期时间(秒)
|
||||
}
|
||||
|
||||
export interface CaptchaRefreshResponse {
|
||||
captcha_id: string
|
||||
image_data: string // base64编码的图片
|
||||
expires_in: number // 过期时间(秒)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
export const generateCaptcha = async (): Promise<CaptchaResponse> => {
|
||||
console.log('🚀 开始发送验证码生成请求...')
|
||||
console.log('📍 请求URL:', `${import.meta.env.VITE_API_BASE_URL}/api/captcha/generate/`)
|
||||
console.log('🌐 环境变量 VITE_API_BASE_URL:', import.meta.env.VITE_API_BASE_URL)
|
||||
|
||||
try {
|
||||
const response = await request.post<{
|
||||
code: number
|
||||
message: string
|
||||
data: CaptchaResponse
|
||||
}>('/api/captcha/generate/')
|
||||
|
||||
console.log('✅ 验证码生成请求成功:', response)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('❌ 验证码生成请求失败 - 完整错误信息:')
|
||||
console.error('错误对象:', error)
|
||||
console.error('错误类型:', typeof error)
|
||||
console.error('错误消息:', error?.message)
|
||||
console.error('错误代码:', error?.code)
|
||||
console.error('错误状态:', error?.status)
|
||||
console.error('错误响应:', error?.response)
|
||||
console.error('错误请求:', error?.request)
|
||||
console.error('错误配置:', error?.config)
|
||||
|
||||
// 检查是否是网络错误
|
||||
if (error?.code === 'NETWORK_ERROR' || error?.message?.includes('Network Error')) {
|
||||
console.error('🌐 网络连接错误 - 可能的原因:')
|
||||
console.error('1. 后端服务器未启动')
|
||||
console.error('2. API地址不正确')
|
||||
console.error('3. CORS配置问题')
|
||||
console.error('4. 防火墙阻止连接')
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新验证码
|
||||
*/
|
||||
export const refreshCaptcha = async (captcha_id: string): Promise<CaptchaRefreshResponse> => {
|
||||
console.log('🔄 开始发送验证码刷新请求...')
|
||||
console.log('📍 请求URL:', `${import.meta.env.VITE_API_BASE_URL}/api/captcha/refresh/`)
|
||||
console.log('📦 请求数据:', { captcha_id })
|
||||
|
||||
try {
|
||||
const response = await request.post<{
|
||||
code: number
|
||||
message: string
|
||||
data: CaptchaRefreshResponse
|
||||
}>('/api/captcha/refresh/', {
|
||||
captcha_id
|
||||
})
|
||||
|
||||
console.log('✅ 验证码刷新请求成功:', response)
|
||||
return response.data
|
||||
} catch (error: any) {
|
||||
console.error('❌ 验证码刷新请求失败 - 完整错误信息:')
|
||||
console.error('错误对象:', error)
|
||||
console.error('错误类型:', typeof error)
|
||||
console.error('错误消息:', error?.message)
|
||||
console.error('错误代码:', error?.code)
|
||||
console.error('错误状态:', error?.status)
|
||||
console.error('错误响应:', error?.response)
|
||||
console.error('错误请求:', error?.request)
|
||||
console.error('错误配置:', error?.config)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
393
hertz_server_django_ui/src/api/dashboard.ts
Normal file
393
hertz_server_django_ui/src/api/dashboard.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
import { logApi, type OperationLogListItem } from './log'
|
||||
import { systemMonitorApi, type SystemInfo, type CpuInfo, type MemoryInfo, type DiskInfo } from './system_monitor'
|
||||
import { noticeUserApi } from './notice_user'
|
||||
import { knowledgeApi } from './knowledge'
|
||||
|
||||
// 仪表盘统计数据类型定义
|
||||
export interface DashboardStats {
|
||||
totalUsers: number
|
||||
totalNotifications: number
|
||||
totalLogs: number
|
||||
totalKnowledge: number
|
||||
userGrowthRate: number
|
||||
notificationGrowthRate: number
|
||||
logGrowthRate: number
|
||||
knowledgeGrowthRate: number
|
||||
}
|
||||
|
||||
// 最近活动数据类型
|
||||
export interface RecentActivity {
|
||||
id: number
|
||||
action: string
|
||||
time: string
|
||||
user: string
|
||||
type: 'login' | 'create' | 'update' | 'system' | 'register'
|
||||
}
|
||||
|
||||
// 系统状态数据类型
|
||||
export interface SystemStatus {
|
||||
cpuUsage: number
|
||||
memoryUsage: number
|
||||
diskUsage: number
|
||||
networkStatus: 'normal' | 'warning' | 'error'
|
||||
}
|
||||
|
||||
// 访问趋势数据类型
|
||||
export interface VisitTrend {
|
||||
date: string
|
||||
visits: number
|
||||
users: number
|
||||
}
|
||||
|
||||
// 仪表盘数据汇总类型
|
||||
export interface DashboardData {
|
||||
stats: DashboardStats
|
||||
recentActivities: RecentActivity[]
|
||||
systemStatus: SystemStatus
|
||||
visitTrends: VisitTrend[]
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 仪表盘API接口
|
||||
export const dashboardApi = {
|
||||
// 获取仪表盘统计数据
|
||||
getStats: (): Promise<ApiResponse<DashboardStats>> => {
|
||||
return request.get('/api/dashboard/stats/')
|
||||
},
|
||||
|
||||
// 获取真实统计数据
|
||||
getRealStats: async (): Promise<ApiResponse<DashboardStats>> => {
|
||||
try {
|
||||
// 并行获取各种统计数据
|
||||
const [notificationStats, logStats, knowledgeStats] = await Promise.all([
|
||||
noticeUserApi.statistics().catch(() => ({ success: false, data: { total_count: 0, unread_count: 0 } })),
|
||||
logApi.getList({ page: 1, page_size: 1 }).catch(() => ({ success: false, data: { count: 0 } })),
|
||||
knowledgeApi.getArticles({ page: 1, page_size: 1 }).catch(() => ({ success: false, data: { total: 0 } }))
|
||||
])
|
||||
|
||||
// 计算统计数据
|
||||
const totalNotifications = notificationStats.success ? (notificationStats.data.total_count || 0) : 0
|
||||
|
||||
// 处理日志数据 - 兼容多种返回结构
|
||||
let totalLogs = 0
|
||||
if (logStats.success && logStats.data) {
|
||||
const logData = logStats.data as any
|
||||
console.log('日志API响应数据:', logData)
|
||||
// 兼容DRF标准结构:{ count, next, previous, results }
|
||||
if ('count' in logData) {
|
||||
totalLogs = Number(logData.count) || 0
|
||||
} else if ('total' in logData) {
|
||||
totalLogs = Number(logData.total) || 0
|
||||
} else if ('total_count' in logData) {
|
||||
totalLogs = Number(logData.total_count) || 0
|
||||
} else if (logData.pagination && logData.pagination.total_count) {
|
||||
totalLogs = Number(logData.pagination.total_count) || 0
|
||||
}
|
||||
console.log('解析出的日志总数:', totalLogs)
|
||||
} else {
|
||||
console.log('日志API调用失败:', logStats)
|
||||
}
|
||||
|
||||
const totalKnowledge = knowledgeStats.success ? (knowledgeStats.data.total || 0) : 0
|
||||
|
||||
console.log('统计数据汇总:', { totalNotifications, totalLogs, totalKnowledge })
|
||||
|
||||
// 模拟增长率(实际项目中应该从后端获取)
|
||||
const stats: DashboardStats = {
|
||||
totalUsers: 0, // 暂时设为0,需要用户管理API
|
||||
totalNotifications,
|
||||
totalLogs,
|
||||
totalKnowledge,
|
||||
userGrowthRate: 0,
|
||||
notificationGrowthRate: Math.floor(Math.random() * 20) - 10, // 模拟 -10% 到 +10%
|
||||
logGrowthRate: Math.floor(Math.random() * 30) - 15, // 模拟 -15% 到 +15%
|
||||
knowledgeGrowthRate: Math.floor(Math.random() * 25) - 12 // 模拟 -12% 到 +13%
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: stats
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取真实统计数据失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '获取统计数据失败',
|
||||
data: {
|
||||
totalUsers: 0,
|
||||
totalNotifications: 0,
|
||||
totalLogs: 0,
|
||||
totalKnowledge: 0,
|
||||
userGrowthRate: 0,
|
||||
notificationGrowthRate: 0,
|
||||
logGrowthRate: 0,
|
||||
knowledgeGrowthRate: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取最近活动(从日志接口)
|
||||
getRecentActivities: async (limit: number = 10): Promise<ApiResponse<RecentActivity[]>> => {
|
||||
try {
|
||||
const response = await logApi.getList({ page: 1, page_size: limit })
|
||||
if (response.success && response.data) {
|
||||
// 根据实际API响应结构,数据可能在data.logs或data.results中
|
||||
const logs = (response.data as any).logs || (response.data as any).results || []
|
||||
const activities: RecentActivity[] = logs.map((log: any) => ({
|
||||
id: log.log_id || log.id,
|
||||
action: log.description || log.operation_description || `${log.action_type_display || log.operation_type} - ${log.module || log.operation_module}`,
|
||||
time: formatTimeAgo(log.created_at),
|
||||
user: log.username || log.user?.username || '未知用户',
|
||||
type: mapLogTypeToActivityType(log.action_type || log.operation_type)
|
||||
}))
|
||||
return {
|
||||
success: true,
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: activities
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '获取活动数据失败',
|
||||
data: []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取最近活动失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '获取活动数据失败',
|
||||
data: []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取系统状态(从系统监控接口)
|
||||
getSystemStatus: async (): Promise<ApiResponse<SystemStatus>> => {
|
||||
try {
|
||||
const [cpuResponse, memoryResponse, disksResponse] = await Promise.all([
|
||||
systemMonitorApi.getCpu(),
|
||||
systemMonitorApi.getMemory(),
|
||||
systemMonitorApi.getDisks()
|
||||
])
|
||||
|
||||
if (cpuResponse.success && memoryResponse.success && disksResponse.success) {
|
||||
// 根据实际API响应结构映射数据
|
||||
const systemStatus: SystemStatus = {
|
||||
// CPU使用率:从 cpu_percent 字段获取
|
||||
cpuUsage: Math.round(cpuResponse.data.cpu_percent || 0),
|
||||
// 内存使用率:从 percent 字段获取
|
||||
memoryUsage: Math.round(memoryResponse.data.percent || 0),
|
||||
// 磁盘使用率:从磁盘数组的第一个磁盘的 percent 字段获取
|
||||
diskUsage: disksResponse.data.length > 0 ? Math.round(disksResponse.data[0].percent || 0) : 0,
|
||||
networkStatus: 'normal' as const
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: systemStatus
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '获取系统状态失败',
|
||||
data: {
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
diskUsage: 0,
|
||||
networkStatus: 'error' as const
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统状态失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '获取系统状态失败',
|
||||
data: {
|
||||
cpuUsage: 0,
|
||||
memoryUsage: 0,
|
||||
diskUsage: 0,
|
||||
networkStatus: 'error' as const
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取访问趋势
|
||||
getVisitTrends: (period: 'week' | 'month' | 'year' = 'week'): Promise<ApiResponse<VisitTrend[]>> => {
|
||||
return request.get('/api/dashboard/visit-trends/', { params: { period } })
|
||||
},
|
||||
|
||||
// 获取完整仪表盘数据
|
||||
getDashboardData: (): Promise<ApiResponse<DashboardData>> => {
|
||||
return request.get('/api/dashboard/overview/')
|
||||
},
|
||||
|
||||
// 模拟数据方法(用于开发阶段)
|
||||
getMockStats: (): Promise<DashboardStats> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
totalUsers: 1128,
|
||||
todayVisits: 893,
|
||||
totalOrders: 234,
|
||||
totalRevenue: 12560.50,
|
||||
userGrowthRate: 12,
|
||||
visitGrowthRate: 8,
|
||||
orderGrowthRate: -3,
|
||||
revenueGrowthRate: 15
|
||||
})
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
|
||||
getMockActivities: (): Promise<RecentActivity[]> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve([
|
||||
{
|
||||
id: 1,
|
||||
action: '用户 张三 登录了系统',
|
||||
time: '2分钟前',
|
||||
user: '张三',
|
||||
type: 'login'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
action: '管理员 李四 创建了新部门',
|
||||
time: '5分钟前',
|
||||
user: '李四',
|
||||
type: 'create'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
action: '用户 王五 修改了个人信息',
|
||||
time: '10分钟前',
|
||||
user: '王五',
|
||||
type: 'update'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
action: '系统自动备份完成',
|
||||
time: '1小时前',
|
||||
user: '系统',
|
||||
type: 'system'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
action: '新用户 赵六 注册成功',
|
||||
time: '2小时前',
|
||||
user: '赵六',
|
||||
type: 'register'
|
||||
}
|
||||
])
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
|
||||
getMockSystemStatus: (): Promise<SystemStatus> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
cpuUsage: 45,
|
||||
memoryUsage: 67,
|
||||
diskUsage: 32,
|
||||
networkStatus: 'normal'
|
||||
})
|
||||
}, 200)
|
||||
})
|
||||
},
|
||||
|
||||
getMockVisitTrends: (period: 'week' | 'month' | 'year' = 'week'): Promise<VisitTrend[]> => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const data = {
|
||||
week: [
|
||||
{ date: '周一', visits: 120, users: 80 },
|
||||
{ date: '周二', visits: 150, users: 95 },
|
||||
{ date: '周三', visits: 180, users: 110 },
|
||||
{ date: '周四', visits: 200, users: 130 },
|
||||
{ date: '周五', visits: 250, users: 160 },
|
||||
{ date: '周六', visits: 180, users: 120 },
|
||||
{ date: '周日', visits: 160, users: 100 }
|
||||
],
|
||||
month: [
|
||||
{ date: '第1周', visits: 800, users: 500 },
|
||||
{ date: '第2周', visits: 950, users: 600 },
|
||||
{ date: '第3周', visits: 1100, users: 700 },
|
||||
{ date: '第4周', visits: 1200, users: 750 }
|
||||
],
|
||||
year: [
|
||||
{ date: '1月', visits: 3200, users: 2000 },
|
||||
{ date: '2月', visits: 3800, users: 2400 },
|
||||
{ date: '3月', visits: 4200, users: 2600 },
|
||||
{ date: '4月', visits: 3900, users: 2300 },
|
||||
{ date: '5月', visits: 4500, users: 2800 },
|
||||
{ date: '6月', visits: 5000, users: 3100 }
|
||||
]
|
||||
}
|
||||
resolve(data[period])
|
||||
}, 400)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:格式化时间为相对时间
|
||||
function formatTimeAgo(dateString: string): string {
|
||||
const now = new Date()
|
||||
const date = new Date(dateString)
|
||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
|
||||
|
||||
if (diffInSeconds < 60) {
|
||||
return `${diffInSeconds}秒前`
|
||||
} else if (diffInSeconds < 3600) {
|
||||
const minutes = Math.floor(diffInSeconds / 60)
|
||||
return `${minutes}分钟前`
|
||||
} else if (diffInSeconds < 86400) {
|
||||
const hours = Math.floor(diffInSeconds / 3600)
|
||||
return `${hours}小时前`
|
||||
} else {
|
||||
const days = Math.floor(diffInSeconds / 86400)
|
||||
return `${days}天前`
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:将日志操作类型映射为活动类型
|
||||
function mapLogTypeToActivityType(operationType: string): RecentActivity['type'] {
|
||||
if (!operationType) return 'system'
|
||||
|
||||
const lowerType = operationType.toLowerCase()
|
||||
|
||||
if (lowerType.includes('login') || lowerType.includes('登录')) {
|
||||
return 'login'
|
||||
} else if (lowerType.includes('create') || lowerType.includes('创建') || lowerType.includes('add') || lowerType.includes('新增')) {
|
||||
return 'create'
|
||||
} else if (lowerType.includes('update') || lowerType.includes('修改') || lowerType.includes('edit') || lowerType.includes('更新')) {
|
||||
return 'update'
|
||||
} else if (lowerType.includes('register') || lowerType.includes('注册')) {
|
||||
return 'register'
|
||||
} else if (lowerType.includes('view') || lowerType.includes('查看') || lowerType.includes('get') || lowerType.includes('获取')) {
|
||||
return 'system'
|
||||
} else {
|
||||
return 'system'
|
||||
}
|
||||
}
|
||||
93
hertz_server_django_ui/src/api/department.ts
Normal file
93
hertz_server_django_ui/src/api/department.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 部门数据类型定义
|
||||
export interface Department {
|
||||
dept_id: number
|
||||
parent_id: number | null
|
||||
dept_name: string
|
||||
dept_code: string
|
||||
leader: string
|
||||
phone: string | null
|
||||
email: string | null
|
||||
status: number
|
||||
sort_order: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
children?: Department[]
|
||||
user_count?: number
|
||||
}
|
||||
|
||||
// API响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 部门列表数据类型
|
||||
export interface DepartmentListData {
|
||||
list: Department[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export type DepartmentListResponse = ApiResponse<DepartmentListData>
|
||||
|
||||
// 部门列表查询参数
|
||||
export interface DepartmentListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
status?: number
|
||||
parent_id?: number
|
||||
}
|
||||
|
||||
// 创建部门参数
|
||||
export interface CreateDepartmentParams {
|
||||
parent_id: null
|
||||
dept_name: string
|
||||
dept_code: string
|
||||
leader: string
|
||||
phone: string
|
||||
email: string
|
||||
status: number
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
// 更新部门参数
|
||||
export type UpdateDepartmentParams = Partial<CreateDepartmentParams>
|
||||
|
||||
// 部门API接口
|
||||
export const departmentApi = {
|
||||
// 获取部门列表
|
||||
getDepartmentList: (params?: DepartmentListParams): Promise<ApiResponse<Department[]>> => {
|
||||
return request.get('/api/departments/', { params })
|
||||
},
|
||||
|
||||
// 获取部门详情
|
||||
getDepartment: (id: number): Promise<ApiResponse<Department>> => {
|
||||
return request.get(`/api/departments/${id}/`)
|
||||
},
|
||||
|
||||
// 创建部门
|
||||
createDepartment: (data: CreateDepartmentParams): Promise<ApiResponse<Department>> => {
|
||||
return request.post('/api/departments/create/', data)
|
||||
},
|
||||
|
||||
// 更新部门
|
||||
updateDepartment: (id: number, data: UpdateDepartmentParams): Promise<ApiResponse<Department>> => {
|
||||
return request.put(`/api/departments/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 删除部门
|
||||
deleteDepartment: (id: number): Promise<ApiResponse<any>> => {
|
||||
return request.delete(`/api/departments/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 获取部门树
|
||||
getDepartmentTree: (): Promise<ApiResponse<Department[]>> => {
|
||||
return request.get('/api/departments/tree/')
|
||||
}
|
||||
}
|
||||
17
hertz_server_django_ui/src/api/index.ts
Normal file
17
hertz_server_django_ui/src/api/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// API 统一出口文件
|
||||
export * from './captcha'
|
||||
export * from './auth'
|
||||
export * from './user'
|
||||
export * from './department'
|
||||
export * from './menu'
|
||||
export * from './role'
|
||||
export * from './password'
|
||||
export * from './system_monitor'
|
||||
export * from './dashboard'
|
||||
|
||||
export * from './ai'
|
||||
// 这里可以继续添加其它 API 模块的导出,例如:
|
||||
// export * from './admin'
|
||||
export * from './log'
|
||||
export * from './knowledge'
|
||||
export * from './kb'
|
||||
131
hertz_server_django_ui/src/api/kb.ts
Normal file
131
hertz_server_django_ui/src/api/kb.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用响应结构(与后端 HertzResponse 对齐)
|
||||
export interface KbApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 知识库条目
|
||||
export interface KbItem {
|
||||
id: number
|
||||
title: string
|
||||
modality: 'text' | 'code' | 'image' | 'audio' | 'video' | string
|
||||
source_type: 'text' | 'file' | 'url' | string
|
||||
chunk_count?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
created_chunk_count?: number
|
||||
// 允许后端扩展字段
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface KbItemListParams {
|
||||
query?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
}
|
||||
|
||||
export interface KbItemListData {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
list: KbItem[]
|
||||
}
|
||||
|
||||
// 语义搜索
|
||||
export interface KbSearchParams {
|
||||
q: string
|
||||
k?: number
|
||||
}
|
||||
|
||||
// 问答(RAG)
|
||||
export interface KbQaPayload {
|
||||
question: string
|
||||
k?: number
|
||||
}
|
||||
|
||||
export interface KbQaData {
|
||||
answer: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
// 图谱查询参数(实体 / 关系)
|
||||
export interface KbGraphListParams {
|
||||
query?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
// 关系检索可选参数
|
||||
source?: number
|
||||
target?: number
|
||||
relation_type?: string
|
||||
}
|
||||
|
||||
export const kbApi = {
|
||||
// 知识库条目:列表
|
||||
listItems(params?: KbItemListParams): Promise<KbApiResponse<KbItemListData>> {
|
||||
return request.get('/api/kb/items/list/', { params })
|
||||
},
|
||||
|
||||
// 语义搜索
|
||||
search(params: KbSearchParams): Promise<KbApiResponse<any>> {
|
||||
return request.get('/api/kb/search/', { params })
|
||||
},
|
||||
|
||||
// 问答(RAG)
|
||||
qa(payload: KbQaPayload): Promise<KbApiResponse<KbQaData>> {
|
||||
return request.post('/api/kb/qa/', payload)
|
||||
},
|
||||
|
||||
// 图谱:实体列表
|
||||
listEntities(params?: KbGraphListParams): Promise<KbApiResponse<any>> {
|
||||
return request.get('/api/kb/graph/entities/', { params })
|
||||
},
|
||||
|
||||
// 图谱:关系列表
|
||||
listRelations(params?: KbGraphListParams): Promise<KbApiResponse<any>> {
|
||||
return request.get('/api/kb/graph/relations/', { params })
|
||||
},
|
||||
|
||||
// 知识库条目:创建(JSON 文本)
|
||||
createItemJson(payload: { title: string; modality?: string; source_type?: string; content?: string; metadata?: any }): Promise<KbApiResponse<KbItem>> {
|
||||
return request.post('/api/kb/items/create/', payload)
|
||||
},
|
||||
|
||||
// 知识库条目:创建(文件上传)
|
||||
createItemFile(formData: FormData): Promise<KbApiResponse<KbItem>> {
|
||||
return request.post('/api/kb/items/create/', formData)
|
||||
},
|
||||
|
||||
// 图谱:创建实体
|
||||
createEntity(payload: { name: string; type: string; properties?: any }): Promise<KbApiResponse<any>> {
|
||||
return request.post('/api/kb/graph/entities/', payload)
|
||||
},
|
||||
|
||||
// 图谱:更新实体
|
||||
updateEntity(id: number, payload: { name?: string; type?: string; properties?: any }): Promise<KbApiResponse<any>> {
|
||||
return request.put(`/api/kb/graph/entities/${id}/`, payload)
|
||||
},
|
||||
|
||||
// 图谱:删除实体
|
||||
deleteEntity(id: number): Promise<KbApiResponse<null>> {
|
||||
return request.delete(`/api/kb/graph/entities/${id}/`)
|
||||
},
|
||||
|
||||
// 图谱:创建关系
|
||||
createRelation(payload: { source: number; target: number; relation_type: string; properties?: any; source_chunk?: number }): Promise<KbApiResponse<any>> {
|
||||
return request.post('/api/kb/graph/relations/', payload)
|
||||
},
|
||||
|
||||
// 图谱:删除关系
|
||||
deleteRelation(id: number): Promise<KbApiResponse<null>> {
|
||||
return request.delete(`/api/kb/graph/relations/${id}/`)
|
||||
},
|
||||
|
||||
// 图谱:自动抽取实体与关系
|
||||
extractGraph(payload: { text?: string; item_id?: number }): Promise<KbApiResponse<{ entities: number; relations: number }>> {
|
||||
return request.post('/api/kb/graph/extract/', payload)
|
||||
},
|
||||
}
|
||||
173
hertz_server_django_ui/src/api/knowledge.ts
Normal file
173
hertz_server_django_ui/src/api/knowledge.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用响应结构
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 分类类型
|
||||
export interface KnowledgeCategory {
|
||||
id: number
|
||||
name: string
|
||||
description?: string
|
||||
parent?: number | null
|
||||
parent_name?: string | null
|
||||
sort_order?: number
|
||||
is_active?: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
children_count?: number
|
||||
articles_count?: number
|
||||
full_path?: string
|
||||
children?: KnowledgeCategory[]
|
||||
}
|
||||
|
||||
export interface CategoryListData {
|
||||
list: KnowledgeCategory[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface CategoryListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
name?: string
|
||||
parent_id?: number
|
||||
is_active?: boolean
|
||||
}
|
||||
|
||||
// 文章类型
|
||||
export interface KnowledgeArticleListItem {
|
||||
id: number
|
||||
title: string
|
||||
summary?: string | null
|
||||
image?: string | null
|
||||
category_name: string
|
||||
author_name: string
|
||||
status: 'draft' | 'published' | 'archived'
|
||||
status_display: string
|
||||
view_count?: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
published_at?: string | null
|
||||
}
|
||||
|
||||
export interface KnowledgeArticleDetail extends KnowledgeArticleListItem {
|
||||
content: string
|
||||
category: number
|
||||
author: number
|
||||
tags?: string
|
||||
tags_list?: string[]
|
||||
sort_order?: number
|
||||
}
|
||||
|
||||
export interface ArticleListData {
|
||||
list: KnowledgeArticleListItem[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
export interface ArticleListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
title?: string
|
||||
category_id?: number
|
||||
author_id?: number
|
||||
status?: 'draft' | 'published' | 'archived'
|
||||
tags?: string
|
||||
}
|
||||
|
||||
export interface CreateArticlePayload {
|
||||
title: string
|
||||
content: string
|
||||
summary?: string
|
||||
image?: string
|
||||
category: number
|
||||
status?: 'draft' | 'published'
|
||||
tags?: string
|
||||
sort_order?: number
|
||||
}
|
||||
|
||||
export interface UpdateArticlePayload {
|
||||
title?: string
|
||||
content?: string
|
||||
summary?: string
|
||||
image?: string
|
||||
category?: number
|
||||
status?: 'draft' | 'published' | 'archived'
|
||||
tags?: string
|
||||
sort_order?: number
|
||||
}
|
||||
|
||||
// 知识库 API
|
||||
export const knowledgeApi = {
|
||||
// 分类:列表
|
||||
getCategories: (params?: CategoryListParams): Promise<ApiResponse<CategoryListData>> => {
|
||||
return request.get('/api/wiki/categories/', { params })
|
||||
},
|
||||
|
||||
// 分类:树形
|
||||
getCategoryTree: (): Promise<ApiResponse<KnowledgeCategory[]>> => {
|
||||
return request.get('/api/wiki/categories/tree/')
|
||||
},
|
||||
|
||||
// 分类:详情
|
||||
getCategory: (id: number): Promise<ApiResponse<KnowledgeCategory>> => {
|
||||
return request.get(`/api/wiki/categories/${id}/`)
|
||||
},
|
||||
|
||||
// 分类:创建
|
||||
createCategory: (data: Partial<KnowledgeCategory>): Promise<ApiResponse<KnowledgeCategory>> => {
|
||||
return request.post('/api/wiki/categories/create/', data)
|
||||
},
|
||||
|
||||
// 分类:更新
|
||||
updateCategory: (id: number, data: Partial<KnowledgeCategory>): Promise<ApiResponse<KnowledgeCategory>> => {
|
||||
return request.put(`/api/wiki/categories/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 分类:删除
|
||||
deleteCategory: (id: number): Promise<ApiResponse<null>> => {
|
||||
return request.delete(`/api/wiki/categories/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 文章:列表
|
||||
getArticles: (params?: ArticleListParams): Promise<ApiResponse<ArticleListData>> => {
|
||||
return request.get('/api/wiki/articles/', { params })
|
||||
},
|
||||
|
||||
// 文章:详情
|
||||
getArticle: (id: number): Promise<ApiResponse<KnowledgeArticleDetail>> => {
|
||||
return request.get(`/api/wiki/articles/${id}/`)
|
||||
},
|
||||
|
||||
// 文章:创建
|
||||
createArticle: (data: CreateArticlePayload): Promise<ApiResponse<KnowledgeArticleDetail>> => {
|
||||
return request.post('/api/wiki/articles/create/', data)
|
||||
},
|
||||
|
||||
// 文章:更新
|
||||
updateArticle: (id: number, data: UpdateArticlePayload): Promise<ApiResponse<KnowledgeArticleDetail>> => {
|
||||
return request.put(`/api/wiki/articles/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 文章:删除
|
||||
deleteArticle: (id: number): Promise<ApiResponse<null>> => {
|
||||
return request.delete(`/api/wiki/articles/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 文章:发布
|
||||
publishArticle: (id: number): Promise<ApiResponse<null>> => {
|
||||
return request.post(`/api/wiki/articles/${id}/publish/`)
|
||||
},
|
||||
|
||||
// 文章:归档
|
||||
archiveArticle: (id: number): Promise<ApiResponse<null>> => {
|
||||
return request.post(`/api/wiki/articles/${id}/archive/`)
|
||||
},
|
||||
}
|
||||
110
hertz_server_django_ui/src/api/log.ts
Normal file
110
hertz_server_django_ui/src/api/log.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用 API 响应结构
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 列表查询参数
|
||||
export interface LogListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
user_id?: number
|
||||
operation_type?: string
|
||||
operation_module?: string
|
||||
start_date?: string // YYYY-MM-DD
|
||||
end_date?: string // YYYY-MM-DD
|
||||
ip_address?: string
|
||||
status?: number
|
||||
// 新增:按请求方法与路径、关键字筛选(与后端保持可选兼容)
|
||||
request_method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | string
|
||||
request_path?: string
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
// 列表项(精简字段)
|
||||
export interface OperationLogItem {
|
||||
id: number
|
||||
user?: {
|
||||
id: number
|
||||
username: string
|
||||
email?: string
|
||||
} | null
|
||||
operation_type: string
|
||||
// 展示字段
|
||||
action_type_display?: string
|
||||
operation_module: string
|
||||
operation_description?: string
|
||||
target_model?: string
|
||||
target_object_id?: string
|
||||
ip_address?: string
|
||||
request_method: string
|
||||
request_path: string
|
||||
response_status: number
|
||||
// 结果与状态展示
|
||||
status_display?: string
|
||||
is_success?: boolean
|
||||
execution_time?: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 列表响应 data 结构
|
||||
export interface LogListData {
|
||||
count: number
|
||||
next: string | null
|
||||
previous: string | null
|
||||
results: OperationLogItem[]
|
||||
}
|
||||
|
||||
export type LogListResponse = ApiResponse<LogListData>
|
||||
|
||||
// 详情数据(完整字段)
|
||||
export interface OperationLogDetail {
|
||||
id: number
|
||||
user?: {
|
||||
id: number
|
||||
username: string
|
||||
email?: string
|
||||
} | null
|
||||
operation_type: string
|
||||
action_type_display?: string
|
||||
operation_module: string
|
||||
operation_description: string
|
||||
target_model?: string
|
||||
target_object_id?: string
|
||||
ip_address?: string
|
||||
user_agent?: string
|
||||
request_method: string
|
||||
request_path: string
|
||||
request_data?: Record<string, any>
|
||||
response_status: number
|
||||
status_display?: string
|
||||
is_success?: boolean
|
||||
response_data?: Record<string, any>
|
||||
execution_time?: number
|
||||
created_at: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
export type LogDetailResponse = ApiResponse<OperationLogDetail>
|
||||
|
||||
export const logApi = {
|
||||
// 获取操作日志列表
|
||||
getList: (params: LogListParams, options?: { signal?: AbortSignal }): Promise<LogListResponse> => {
|
||||
// 关闭统一错误弹窗,由页面自行处理
|
||||
return request.get('/api/log/list/', { params, showError: false, signal: options?.signal })
|
||||
},
|
||||
|
||||
// 获取操作日志详情
|
||||
getDetail: (logId: number): Promise<LogDetailResponse> => {
|
||||
return request.get(`/api/log/detail/${logId}/`)
|
||||
},
|
||||
|
||||
// 兼容查询参数方式的详情(部分后端实现为 /api/log/detail/?id=xx 或 ?log_id=xx)
|
||||
getDetailByQuery: (logId: number): Promise<LogDetailResponse> => {
|
||||
return request.get('/api/log/detail/', { params: { id: logId, log_id: logId } })
|
||||
},
|
||||
}
|
||||
361
hertz_server_django_ui/src/api/menu.ts
Normal file
361
hertz_server_django_ui/src/api/menu.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 后端返回的原始菜单数据格式
|
||||
export interface RawMenu {
|
||||
menu_id: number
|
||||
menu_name: string
|
||||
menu_code: string
|
||||
menu_type: number // 后端返回数字:1=菜单, 2=按钮, 3=接口
|
||||
parent_id?: number | null
|
||||
path?: string
|
||||
component?: string | null
|
||||
icon?: string
|
||||
permission?: string
|
||||
sort_order?: number
|
||||
description?: string
|
||||
status?: number
|
||||
is_external?: boolean
|
||||
is_cache?: boolean
|
||||
is_visible?: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
children?: RawMenu[]
|
||||
}
|
||||
|
||||
// 前端使用的菜单接口类型定义
|
||||
export interface Menu {
|
||||
menu_id: number
|
||||
menu_name: string
|
||||
menu_code: string
|
||||
menu_type: number // 1=菜单, 2=按钮, 3=接口
|
||||
parent_id?: number
|
||||
path?: string
|
||||
component?: string
|
||||
icon?: string
|
||||
permission?: string
|
||||
sort_order?: number
|
||||
status?: number
|
||||
is_external?: boolean
|
||||
is_cache?: boolean
|
||||
is_visible?: boolean
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
children?: Menu[]
|
||||
}
|
||||
|
||||
// API响应基础结构
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 菜单列表数据结构
|
||||
export interface MenuListData {
|
||||
list: Menu[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// 菜单列表响应类型
|
||||
export type MenuListResponse = ApiResponse<MenuListData>
|
||||
|
||||
// 菜单列表查询参数
|
||||
export interface MenuListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
status?: number
|
||||
menu_type?: string
|
||||
parent_id?: number
|
||||
}
|
||||
|
||||
// 创建菜单参数
|
||||
export interface CreateMenuParams {
|
||||
menu_name: string
|
||||
menu_code: string
|
||||
menu_type: number // 1=菜单, 2=按钮, 3=接口
|
||||
parent_id?: number
|
||||
path?: string
|
||||
component?: string
|
||||
icon?: string
|
||||
permission?: string
|
||||
sort_order?: number
|
||||
status?: number
|
||||
is_external?: boolean
|
||||
is_cache?: boolean
|
||||
is_visible?: boolean
|
||||
}
|
||||
|
||||
// 更新菜单参数
|
||||
export type UpdateMenuParams = Partial<CreateMenuParams>
|
||||
|
||||
// 菜单树响应类型
|
||||
export type MenuTreeResponse = ApiResponse<Menu[]>
|
||||
|
||||
// 数据转换工具函数
|
||||
const convertMenuType = (type: number): 'menu' | 'button' | 'api' => {
|
||||
switch (type) {
|
||||
case 1: return 'menu'
|
||||
case 2: return 'button'
|
||||
case 3: return 'api'
|
||||
default: return 'menu'
|
||||
}
|
||||
}
|
||||
|
||||
// 解码Unicode字符串
|
||||
const decodeUnicode = (str: string): string => {
|
||||
try {
|
||||
return str.replace(/\\u[\dA-F]{4}/gi, (match) => {
|
||||
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16))
|
||||
})
|
||||
} catch (error) {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
// 转换原始菜单数据为前端格式
|
||||
const transformRawMenu = (rawMenu: RawMenu): Menu => {
|
||||
// 确保status字段被正确转换
|
||||
let statusValue: number
|
||||
if (rawMenu.status === undefined || rawMenu.status === null) {
|
||||
// 如果status缺失,默认为启用(1)
|
||||
statusValue = 1
|
||||
} else {
|
||||
// 如果有值,转换为数字
|
||||
if (typeof rawMenu.status === 'string') {
|
||||
const parsed = parseInt(rawMenu.status, 10)
|
||||
statusValue = isNaN(parsed) ? 1 : parsed
|
||||
} else {
|
||||
statusValue = Number(rawMenu.status)
|
||||
// 如果转换失败,默认为启用
|
||||
if (isNaN(statusValue)) {
|
||||
statusValue = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
menu_id: rawMenu.menu_id,
|
||||
menu_name: decodeUnicode(rawMenu.menu_name),
|
||||
menu_code: rawMenu.menu_code,
|
||||
menu_type: rawMenu.menu_type,
|
||||
parent_id: rawMenu.parent_id || undefined,
|
||||
path: rawMenu.path,
|
||||
component: rawMenu.component,
|
||||
icon: rawMenu.icon,
|
||||
permission: rawMenu.permission,
|
||||
sort_order: rawMenu.sort_order,
|
||||
status: statusValue, // 使用转换后的值
|
||||
is_external: rawMenu.is_external,
|
||||
is_cache: rawMenu.is_cache,
|
||||
is_visible: rawMenu.is_visible,
|
||||
created_at: rawMenu.created_at,
|
||||
updated_at: rawMenu.updated_at,
|
||||
children: rawMenu.children ? rawMenu.children.map(transformRawMenu) : []
|
||||
}
|
||||
}
|
||||
|
||||
// 将菜单数据数组转换为列表格式
|
||||
const transformToMenuList = (rawMenus: RawMenu[]): MenuListData => {
|
||||
const transformedMenus = rawMenus.map(transformRawMenu)
|
||||
|
||||
// 递归收集所有菜单项
|
||||
const collectAllMenus = (menu: Menu): Menu[] => {
|
||||
const result = [menu]
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
menu.children.forEach(child => {
|
||||
result.push(...collectAllMenus(child))
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 收集所有菜单项
|
||||
const allMenus: Menu[] = []
|
||||
transformedMenus.forEach(menu => {
|
||||
allMenus.push(...collectAllMenus(menu))
|
||||
})
|
||||
|
||||
return {
|
||||
list: allMenus,
|
||||
total: allMenus.length,
|
||||
page: 1,
|
||||
page_size: allMenus.length
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单树结构
|
||||
const buildMenuTree = (rawMenus: RawMenu[]): Menu[] => {
|
||||
const transformedMenus = rawMenus.map(transformRawMenu)
|
||||
|
||||
// 创建菜单映射
|
||||
const menuMap = new Map<number, Menu>()
|
||||
transformedMenus.forEach(menu => {
|
||||
menuMap.set(menu.menu_id, { ...menu, children: [] })
|
||||
})
|
||||
|
||||
// 构建树结构
|
||||
const rootMenus: Menu[] = []
|
||||
transformedMenus.forEach(menu => {
|
||||
const menuItem = menuMap.get(menu.menu_id)!
|
||||
|
||||
if (menu.parent_id && menuMap.has(menu.parent_id)) {
|
||||
const parent = menuMap.get(menu.parent_id)!
|
||||
if (!parent.children) parent.children = []
|
||||
parent.children.push(menuItem)
|
||||
} else {
|
||||
rootMenus.push(menuItem)
|
||||
}
|
||||
})
|
||||
|
||||
return rootMenus
|
||||
}
|
||||
|
||||
// 菜单API
|
||||
export const menuApi = {
|
||||
// 获取菜单列表
|
||||
getMenuList: async (params?: MenuListParams): Promise<MenuListResponse> => {
|
||||
try {
|
||||
const response = await request.get<ApiResponse<RawMenu[]>>('/api/menus/', { params })
|
||||
|
||||
if (response.success && response.data && Array.isArray(response.data)) {
|
||||
const menuListData = transformToMenuList(response.data)
|
||||
return {
|
||||
success: true,
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
data: menuListData
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
code: response.code || 500,
|
||||
message: response.message || '获取菜单数据失败',
|
||||
data: {
|
||||
list: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单列表失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '网络请求失败',
|
||||
data: {
|
||||
list: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取菜单树
|
||||
getMenuTree: async (): Promise<MenuTreeResponse> => {
|
||||
try {
|
||||
const response = await request.get<ApiResponse<RawMenu[]>>('/api/menus/tree/')
|
||||
|
||||
if (response.success && response.data && Array.isArray(response.data)) {
|
||||
// 调试:检查原始数据中的status值
|
||||
if (response.data.length > 0) {
|
||||
console.log('🔍 原始菜单数据status检查(前5条):', response.data.slice(0, 5).map((m: RawMenu) => ({
|
||||
menu_name: m.menu_name,
|
||||
menu_id: m.menu_id,
|
||||
status: m.status,
|
||||
statusType: typeof m.status
|
||||
})))
|
||||
}
|
||||
|
||||
// 后端已经返回树形结构,直接转换数据格式即可
|
||||
const transformedData = response.data.map(transformRawMenu)
|
||||
|
||||
// 调试:检查转换后的status值
|
||||
if (transformedData.length > 0) {
|
||||
console.log('🔍 转换后菜单数据status检查(前5条):', transformedData.slice(0, 5).map((m: Menu) => ({
|
||||
menu_name: m.menu_name,
|
||||
menu_id: m.menu_id,
|
||||
status: m.status,
|
||||
statusType: typeof m.status
|
||||
})))
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
data: transformedData
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
code: response.code || 500,
|
||||
message: response.message || '获取菜单树失败',
|
||||
data: []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单树失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '网络请求失败',
|
||||
data: []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 获取单个菜单
|
||||
getMenu: async (id: number): Promise<ApiResponse<Menu>> => {
|
||||
try {
|
||||
const response = await request.get<ApiResponse<RawMenu>>(`/api/menus/${id}/`)
|
||||
|
||||
if (response.success && response.data) {
|
||||
const transformedMenu = transformRawMenu(response.data)
|
||||
return {
|
||||
success: true,
|
||||
code: response.code,
|
||||
message: response.message,
|
||||
data: transformedMenu
|
||||
}
|
||||
}
|
||||
|
||||
return response as ApiResponse<Menu>
|
||||
} catch (error) {
|
||||
console.error('获取菜单详情失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '网络请求失败',
|
||||
data: {} as Menu
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 创建菜单
|
||||
createMenu: (data: CreateMenuParams): Promise<ApiResponse<Menu>> => {
|
||||
return request.post('/api/menus/create/', data)
|
||||
},
|
||||
|
||||
// 更新菜单
|
||||
updateMenu: (id: number, data: UpdateMenuParams): Promise<ApiResponse<Menu>> => {
|
||||
return request.put(`/api/menus/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 删除菜单
|
||||
deleteMenu: (id: number): Promise<ApiResponse<any>> => {
|
||||
return request.delete(`/api/menus/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 批量删除菜单
|
||||
batchDeleteMenus: (ids: number[]): Promise<ApiResponse<any>> => {
|
||||
return request.post('/api/menus/batch-delete/', { menu_ids: ids })
|
||||
}
|
||||
}
|
||||
87
hertz_server_django_ui/src/api/notice_user.ts
Normal file
87
hertz_server_django_ui/src/api/notice_user.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 用户端通知模块 API 类型定义
|
||||
export interface UserNoticeListItem {
|
||||
notice: number
|
||||
title: string
|
||||
notice_type_display: string
|
||||
priority_display: string
|
||||
is_top: boolean
|
||||
publish_time: string
|
||||
is_read: boolean
|
||||
read_time: string | null
|
||||
is_starred: boolean
|
||||
starred_time: string | null
|
||||
is_expired: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface UserNoticeListData {
|
||||
notices: UserNoticeListItem[]
|
||||
pagination: {
|
||||
current_page: number
|
||||
page_size: number
|
||||
total_pages: number
|
||||
total_count: number
|
||||
has_next: boolean
|
||||
has_previous: boolean
|
||||
}
|
||||
statistics: {
|
||||
total_count: number
|
||||
unread_count: number
|
||||
starred_count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface UserNoticeDetailData {
|
||||
notice: number
|
||||
title: string
|
||||
content: string
|
||||
notice_type_display: string
|
||||
priority_display: string
|
||||
attachment_url: string | null
|
||||
publish_time: string
|
||||
expire_time: string
|
||||
is_top: boolean
|
||||
is_expired: boolean
|
||||
publisher_name: string | null
|
||||
is_read: boolean
|
||||
read_time: string
|
||||
is_starred: boolean
|
||||
starred_time: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export const noticeUserApi = {
|
||||
// 查看通知列表
|
||||
list: (params?: { page?: number; page_size?: number }): Promise<ApiResponse<UserNoticeListData>> =>
|
||||
request.get('/api/notice/user/list/', { params }),
|
||||
|
||||
// 查看通知详情
|
||||
detail: (notice_id: number | string): Promise<ApiResponse<UserNoticeDetailData>> =>
|
||||
request.get(`/api/notice/user/detail/${notice_id}/`),
|
||||
|
||||
// 标记通知已读
|
||||
markRead: (notice_id: number | string): Promise<ApiResponse<null>> =>
|
||||
request.post('/api/notice/user/mark-read/', { notice_id }),
|
||||
|
||||
// 批量标记通知已读
|
||||
batchMarkRead: (notice_ids: Array<number | string>): Promise<ApiResponse<{ updated_count: number }>> =>
|
||||
request.post('/api/notice/user/batch-mark-read/', { notice_ids }),
|
||||
|
||||
// 用户获取通知统计
|
||||
statistics: (): Promise<ApiResponse<{ total_count: number; unread_count: number; read_count: number; starred_count: number; type_statistics?: Record<string, number>; priority_statistics?: Record<string, number> }>> =>
|
||||
request.get('/api/notice/user/statistics/'),
|
||||
|
||||
// 收藏/取消收藏通知
|
||||
toggleStar: (notice_id: number | string, is_starred: boolean): Promise<ApiResponse<null>> =>
|
||||
request.post('/api/notice/user/toggle-star/', { notice_id, is_starred }),
|
||||
}
|
||||
31
hertz_server_django_ui/src/api/password.ts
Normal file
31
hertz_server_django_ui/src/api/password.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 修改密码接口参数
|
||||
export interface ChangePasswordParams {
|
||||
old_password: string
|
||||
new_password: string
|
||||
confirm_password: string
|
||||
}
|
||||
|
||||
// 重置密码接口参数
|
||||
export interface ResetPasswordParams {
|
||||
email: string
|
||||
email_code: string
|
||||
new_password: string
|
||||
confirm_password: string
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
export const changePassword = (params: ChangePasswordParams) => {
|
||||
return request.post('/api/auth/password/change/', params)
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
export const resetPassword = (params: ResetPasswordParams) => {
|
||||
return request.post('/api/auth/password/reset/', params)
|
||||
}
|
||||
|
||||
// 发送重置密码邮箱验证码
|
||||
export const sendResetPasswordCode = (email: string) => {
|
||||
return request.post('/api/auth/password/reset/code/', { email })
|
||||
}
|
||||
130
hertz_server_django_ui/src/api/role.ts
Normal file
130
hertz_server_django_ui/src/api/role.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 权限接口类型定义
|
||||
export interface Permission {
|
||||
permission_id: number
|
||||
permission_name: string
|
||||
permission_code: string
|
||||
permission_type: 'menu' | 'button' | 'api'
|
||||
parent_id?: number
|
||||
path?: string
|
||||
icon?: string
|
||||
sort_order?: number
|
||||
description?: string
|
||||
status?: number
|
||||
children?: Permission[]
|
||||
}
|
||||
|
||||
// 角色接口类型定义
|
||||
export interface Role {
|
||||
role_id: number
|
||||
role_name: string
|
||||
role_code: string
|
||||
description?: string
|
||||
status?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
permissions?: Permission[]
|
||||
}
|
||||
|
||||
// API响应基础结构
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 角色列表数据结构
|
||||
export interface RoleListData {
|
||||
list: Role[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// 角色列表响应类型
|
||||
export type RoleListResponse = ApiResponse<RoleListData>
|
||||
|
||||
// 角色列表查询参数
|
||||
export interface RoleListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
// 创建角色参数
|
||||
export interface CreateRoleParams {
|
||||
role_name: string
|
||||
role_code: string
|
||||
description?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
// 更新角色参数
|
||||
export type UpdateRoleParams = Partial<CreateRoleParams>
|
||||
|
||||
// 角色权限分配参数
|
||||
export interface AssignRolePermissionsParams {
|
||||
role_id: number
|
||||
menu_ids: number[]
|
||||
user_type?: number
|
||||
department_id?: number
|
||||
}
|
||||
|
||||
// 权限列表响应类型
|
||||
export type PermissionListResponse = ApiResponse<Permission[]>
|
||||
|
||||
// 角色API
|
||||
export const roleApi = {
|
||||
// 获取角色列表
|
||||
getRoleList: (params?: RoleListParams): Promise<RoleListResponse> => {
|
||||
return request.get('/api/roles/', { params })
|
||||
},
|
||||
|
||||
// 获取单个角色
|
||||
getRole: (id: number): Promise<ApiResponse<Role>> => {
|
||||
return request.get(`/api/roles/${id}/`)
|
||||
},
|
||||
|
||||
// 创建角色
|
||||
createRole: (data: CreateRoleParams): Promise<ApiResponse<Role>> => {
|
||||
return request.post('/api/roles/create/', data)
|
||||
},
|
||||
|
||||
// 更新角色
|
||||
updateRole: (id: number, data: UpdateRoleParams): Promise<ApiResponse<Role>> => {
|
||||
return request.put(`/api/roles/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 删除角色
|
||||
deleteRole: (id: number): Promise<ApiResponse<any>> => {
|
||||
return request.delete(`/api/roles/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 批量删除角色
|
||||
batchDeleteRoles: (ids: number[]): Promise<ApiResponse<any>> => {
|
||||
return request.post('/api/roles/batch-delete/', { role_ids: ids })
|
||||
},
|
||||
|
||||
// 获取角色权限
|
||||
getRolePermissions: (id: number): Promise<ApiResponse<Permission[]>> => {
|
||||
return request.get(`/api/roles/${id}/menus/`)
|
||||
},
|
||||
|
||||
// 分配角色权限
|
||||
assignRolePermissions: (data: AssignRolePermissionsParams): Promise<ApiResponse<any>> => {
|
||||
return request.post(`/api/roles/assign-menus/`, data)
|
||||
},
|
||||
|
||||
// 获取所有权限列表
|
||||
getPermissionList: (): Promise<PermissionListResponse> => {
|
||||
return request.get('/api/menus/')
|
||||
},
|
||||
|
||||
// 获取权限树
|
||||
getPermissionTree: (): Promise<PermissionListResponse> => {
|
||||
return request.get('/api/menus/tree/')
|
||||
}
|
||||
}
|
||||
114
hertz_server_django_ui/src/api/system_monitor.ts
Normal file
114
hertz_server_django_ui/src/api/system_monitor.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 通用响应类型
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 1. 系统信息
|
||||
export interface SystemInfo {
|
||||
hostname: string
|
||||
platform: string
|
||||
architecture: string
|
||||
boot_time: string
|
||||
uptime: string
|
||||
}
|
||||
|
||||
// 2. CPU 信息
|
||||
export interface CpuInfo {
|
||||
cpu_count: number
|
||||
cpu_percent: number
|
||||
cpu_freq: {
|
||||
current: number
|
||||
min: number
|
||||
max: number
|
||||
}
|
||||
load_avg: number[]
|
||||
}
|
||||
|
||||
// 3. 内存信息
|
||||
export interface MemoryInfo {
|
||||
total: number
|
||||
available: number
|
||||
used: number
|
||||
percent: number
|
||||
free: number
|
||||
}
|
||||
|
||||
// 4. 磁盘信息
|
||||
export interface DiskInfo {
|
||||
device: string
|
||||
mountpoint: string
|
||||
fstype: string
|
||||
total: number
|
||||
used: number
|
||||
free: number
|
||||
percent: number
|
||||
}
|
||||
|
||||
// 5. 网络信息
|
||||
export interface NetworkInfo {
|
||||
interface: string
|
||||
bytes_sent: number
|
||||
bytes_recv: number
|
||||
packets_sent: number
|
||||
packets_recv: number
|
||||
}
|
||||
|
||||
// 6. 进程信息
|
||||
export interface ProcessInfo {
|
||||
pid: number
|
||||
name: string
|
||||
status: string
|
||||
cpu_percent: number
|
||||
memory_percent: number
|
||||
memory_info: {
|
||||
rss: number
|
||||
vms: number
|
||||
}
|
||||
create_time: string
|
||||
cmdline: string[]
|
||||
}
|
||||
|
||||
// 7. GPU 信息
|
||||
export interface GpuInfoItem {
|
||||
id: number
|
||||
name: string
|
||||
load: number
|
||||
memory_total: number
|
||||
memory_used: number
|
||||
memory_util: number
|
||||
temperature: number
|
||||
}
|
||||
|
||||
export interface GpuInfoResponse {
|
||||
gpu_available: boolean
|
||||
gpu_info?: GpuInfoItem[]
|
||||
message?: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
// 8. 综合监测信息
|
||||
export interface MonitorData {
|
||||
system: SystemInfo
|
||||
cpu: CpuInfo
|
||||
memory: MemoryInfo
|
||||
disks: DiskInfo[]
|
||||
network: NetworkInfo[]
|
||||
processes: ProcessInfo[]
|
||||
gpus: Array<{ gpu_available: boolean; message?: string; timestamp: string }>
|
||||
}
|
||||
|
||||
export const systemMonitorApi = {
|
||||
getSystem: (): Promise<ApiResponse<SystemInfo>> => request.get('/api/system/system/'),
|
||||
getCpu: (): Promise<ApiResponse<CpuInfo>> => request.get('/api/system/cpu/'),
|
||||
getMemory: (): Promise<ApiResponse<MemoryInfo>> => request.get('/api/system/memory/'),
|
||||
getDisks: (): Promise<ApiResponse<DiskInfo[]>> => request.get('/api/system/disks/'),
|
||||
getNetwork: (): Promise<ApiResponse<NetworkInfo[]>> => request.get('/api/system/network/'),
|
||||
getProcesses: (): Promise<ApiResponse<ProcessInfo[]>> => request.get('/api/system/processes/'),
|
||||
getGpu: (): Promise<ApiResponse<GpuInfoResponse>> => request.get('/api/system/gpu/'),
|
||||
getMonitor: (): Promise<ApiResponse<MonitorData>> => request.get('/api/system/monitor/'),
|
||||
}
|
||||
121
hertz_server_django_ui/src/api/user.ts
Normal file
121
hertz_server_django_ui/src/api/user.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// 角色接口类型定义
|
||||
export interface Role {
|
||||
role_id: number
|
||||
role_name: string
|
||||
role_code: string
|
||||
role_ids?: string
|
||||
}
|
||||
|
||||
// 用户接口类型定义(匹配后端实际数据结构)
|
||||
export interface User {
|
||||
user_id: number
|
||||
username: string
|
||||
email: string
|
||||
phone?: string
|
||||
real_name?: string
|
||||
avatar?: string
|
||||
gender: number
|
||||
birthday?: string
|
||||
department_id?: number
|
||||
status: number
|
||||
last_login_time?: string
|
||||
last_login_ip?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
roles: Role[]
|
||||
}
|
||||
|
||||
// API响应基础结构
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 用户列表数据结构
|
||||
export interface UserListData {
|
||||
list: User[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// 用户列表响应类型
|
||||
export type UserListResponse = ApiResponse<UserListData>
|
||||
|
||||
// 用户列表查询参数
|
||||
export interface UserListParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
status?: number
|
||||
role_ids?: string
|
||||
}
|
||||
|
||||
// 分配角色参数
|
||||
export interface AssignRolesParams {
|
||||
user_id: number
|
||||
role_ids: number[] // 角色ID数组
|
||||
}
|
||||
|
||||
// 用户API
|
||||
export const userApi = {
|
||||
// 获取用户列表
|
||||
getUserList: (params?: UserListParams): Promise<UserListResponse> => {
|
||||
return request.get('/api/users/', { params })
|
||||
},
|
||||
|
||||
// 获取单个用户
|
||||
getUser: (id: number): Promise<ApiResponse<User>> => {
|
||||
return request.get(`/api/users/${id}/`)
|
||||
},
|
||||
|
||||
// 创建用户
|
||||
createUser: (data: Partial<User>): Promise<ApiResponse<User>> => {
|
||||
return request.post('/api/users/create/', data)
|
||||
},
|
||||
|
||||
// 更新用户
|
||||
updateUser: (id: number, data: Partial<User>): Promise<ApiResponse<User>> => {
|
||||
return request.put(`/api/users/${id}/update/`, data)
|
||||
},
|
||||
|
||||
// 删除用户
|
||||
deleteUser: (id: number): Promise<ApiResponse<any>> => {
|
||||
return request.delete(`/api/users/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 批量删除用户
|
||||
batchDeleteUsers: (ids: number[]): Promise<ApiResponse<any>> => {
|
||||
return request.post('/api/admin/users/batch-delete/', { user_ids: ids })
|
||||
},
|
||||
|
||||
// 获取当前用户信息
|
||||
getUserInfo: (): Promise<ApiResponse<User>> => {
|
||||
return request.get('/api/auth/user/info/')
|
||||
},
|
||||
|
||||
// 更新当前用户信息
|
||||
updateUserInfo: (data: Partial<User>): Promise<ApiResponse<User>> => {
|
||||
return request.put('/api/auth/user/info/update/', data)
|
||||
},
|
||||
|
||||
uploadAvatar: (file: File): Promise<ApiResponse<User>> => {
|
||||
const formData = new FormData()
|
||||
formData.append('avatar', file)
|
||||
return request.upload('/api/auth/user/avatar/upload/', formData)
|
||||
},
|
||||
|
||||
// 分配用户角色
|
||||
assignRoles: (data: AssignRolesParams): Promise<ApiResponse<any>> => {
|
||||
return request.post('/api/users/assign-roles/', data)
|
||||
},
|
||||
|
||||
// 获取所有角色列表
|
||||
getRoleList: (): Promise<ApiResponse<Role[]>> => {
|
||||
return request.get('/api/roles/')
|
||||
}
|
||||
}
|
||||
643
hertz_server_django_ui/src/api/yolo.ts
Normal file
643
hertz_server_django_ui/src/api/yolo.ts
Normal file
@@ -0,0 +1,643 @@
|
||||
import { request } from '@/utils/hertz_request'
|
||||
|
||||
// YOLO检测相关接口类型定义
|
||||
export interface YoloDetectionRequest {
|
||||
image: File
|
||||
model_id?: string
|
||||
confidence_threshold?: number
|
||||
nms_threshold?: number
|
||||
}
|
||||
|
||||
export interface DetectionBbox {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface YoloDetection {
|
||||
class_id: number
|
||||
class_name: string
|
||||
confidence: number
|
||||
bbox: DetectionBbox
|
||||
}
|
||||
|
||||
export interface YoloDetectionResponse {
|
||||
message: string
|
||||
data?: {
|
||||
detection_id: number
|
||||
result_file_url: string
|
||||
original_file_url: string
|
||||
object_count: number
|
||||
detected_categories: string[]
|
||||
confidence_scores: number[]
|
||||
avg_confidence: number | null
|
||||
processing_time: number
|
||||
model_used: string
|
||||
confidence_threshold: number
|
||||
user_id: number
|
||||
user_name: string
|
||||
alert_level?: 'low' | 'medium' | 'high'
|
||||
}
|
||||
}
|
||||
|
||||
export interface YoloModel {
|
||||
id: string
|
||||
name: string
|
||||
version: string
|
||||
description: string
|
||||
classes: string[]
|
||||
is_active: boolean
|
||||
is_enabled: boolean
|
||||
model_file: string
|
||||
model_folder_path: string
|
||||
model_path: string
|
||||
weights_folder_path: string
|
||||
categories: { [key: string]: any }
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface YoloModelListResponse {
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: {
|
||||
models: YoloModel[]
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
// 数据集管理相关类型
|
||||
export interface YoloDatasetSummary {
|
||||
id: number
|
||||
name: string
|
||||
version?: string
|
||||
root_folder_path: string
|
||||
data_yaml_path: string
|
||||
nc?: number
|
||||
description?: string
|
||||
created_at?: string
|
||||
}
|
||||
|
||||
export interface YoloDatasetDetail extends YoloDatasetSummary {
|
||||
names?: string[]
|
||||
train_images_count?: number
|
||||
train_labels_count?: number
|
||||
val_images_count?: number
|
||||
val_labels_count?: number
|
||||
test_images_count?: number
|
||||
test_labels_count?: number
|
||||
}
|
||||
|
||||
export interface YoloDatasetSampleItem {
|
||||
image: string
|
||||
image_size?: number
|
||||
label?: string
|
||||
filename: string
|
||||
}
|
||||
|
||||
// YOLO 训练任务相关类型
|
||||
export type YoloTrainStatus =
|
||||
| 'queued'
|
||||
| 'running'
|
||||
| 'canceling'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'canceled'
|
||||
|
||||
export interface YoloTrainDatasetOption {
|
||||
id: number
|
||||
name: string
|
||||
version?: string
|
||||
yaml: string
|
||||
}
|
||||
|
||||
export interface YoloTrainVersionOption {
|
||||
family: 'v8' | '11' | '12'
|
||||
config_path: string
|
||||
sizes: string[]
|
||||
}
|
||||
|
||||
export interface YoloTrainOptionsResponse {
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: {
|
||||
datasets: YoloTrainDatasetOption[]
|
||||
versions: YoloTrainVersionOption[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface YoloTrainingJob {
|
||||
id: number
|
||||
dataset: number
|
||||
dataset_name: string
|
||||
model_family: 'v8' | '11' | '12'
|
||||
model_size?: 'n' | 's' | 'm' | 'l' | 'x'
|
||||
weight_path?: string
|
||||
config_path?: string
|
||||
status: YoloTrainStatus
|
||||
logs_path?: string
|
||||
runs_path?: string
|
||||
best_model_path?: string
|
||||
last_model_path?: string
|
||||
progress: number
|
||||
epochs: number
|
||||
imgsz: number
|
||||
batch: number
|
||||
device: string
|
||||
optimizer: 'SGD' | 'Adam' | 'AdamW' | 'RMSProp'
|
||||
error_message?: string
|
||||
created_at: string
|
||||
started_at?: string | null
|
||||
finished_at?: string | null
|
||||
}
|
||||
|
||||
export interface StartTrainingPayload {
|
||||
dataset_id: number
|
||||
model_family: 'v8' | '11' | '12'
|
||||
model_size?: 'n' | 's' | 'm' | 'l' | 'x'
|
||||
epochs?: number
|
||||
imgsz?: number
|
||||
batch?: number
|
||||
device?: string
|
||||
optimizer?: 'SGD' | 'Adam' | 'AdamW' | 'RMSProp'
|
||||
}
|
||||
|
||||
export interface YoloTrainLogsResponse {
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: {
|
||||
content: string
|
||||
next_offset: number
|
||||
finished: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// YOLO检测API
|
||||
export const yoloApi = {
|
||||
// 执行YOLO检测
|
||||
async detectImage(detectionRequest: YoloDetectionRequest): Promise<YoloDetectionResponse> {
|
||||
console.log('🔍 构建检测请求:', detectionRequest)
|
||||
console.log('📁 文件对象详情:', {
|
||||
name: detectionRequest.image.name,
|
||||
size: detectionRequest.image.size,
|
||||
type: detectionRequest.image.type,
|
||||
lastModified: detectionRequest.image.lastModified
|
||||
})
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', detectionRequest.image)
|
||||
|
||||
if (detectionRequest.model_id) {
|
||||
formData.append('model_id', detectionRequest.model_id)
|
||||
}
|
||||
if (detectionRequest.confidence_threshold) {
|
||||
formData.append('confidence_threshold', detectionRequest.confidence_threshold.toString())
|
||||
}
|
||||
if (detectionRequest.nms_threshold) {
|
||||
formData.append('nms_threshold', detectionRequest.nms_threshold.toString())
|
||||
}
|
||||
|
||||
// 调试FormData内容
|
||||
console.log('📤 FormData内容:')
|
||||
for (const [key, value] of formData.entries()) {
|
||||
if (value instanceof File) {
|
||||
console.log(` ${key}: File(${value.name}, ${value.size} bytes, ${value.type})`)
|
||||
} else {
|
||||
console.log(` ${key}:`, value)
|
||||
}
|
||||
}
|
||||
|
||||
return request.post('/api/yolo/detect/', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 获取当前启用的YOLO模型信息
|
||||
async getCurrentEnabledModel(): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
// 关闭全局错误提示,由调用方(如 YOLO 检测页面)自行处理“未启用模型”等业务文案
|
||||
return request.get('/api/yolo/models/enabled/', { showError: false })
|
||||
},
|
||||
|
||||
// 获取模型详情
|
||||
async getModelInfo(modelId: string): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
return request.get(`/api/yolo/models/${modelId}`)
|
||||
},
|
||||
|
||||
// 批量检测
|
||||
async detectBatch(images: File[], modelId?: string): Promise<YoloDetectionResponse[]> {
|
||||
const promises = images.map(image =>
|
||||
this.detectImage({
|
||||
image,
|
||||
model_id: modelId,
|
||||
confidence_threshold: 0.5,
|
||||
nms_threshold: 0.4
|
||||
})
|
||||
)
|
||||
|
||||
return Promise.all(promises)
|
||||
},
|
||||
|
||||
// 获取模型列表
|
||||
async getModels(): Promise<{ success: boolean; data?: YoloModel[]; message?: string }> {
|
||||
return request.get('/api/yolo/models/')
|
||||
},
|
||||
|
||||
// 上传模型
|
||||
async uploadModel(formData: FormData): Promise<{ success: boolean; message?: string }> {
|
||||
// 使用专门的upload方法,它会自动处理Content-Type
|
||||
return request.upload('/api/yolo/upload/', formData)
|
||||
},
|
||||
|
||||
// 更新模型信息
|
||||
async updateModel(modelId: string, data: { name: string; version: string }): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
return request.put(`/api/yolo/models/${modelId}/update/`, data)
|
||||
},
|
||||
|
||||
// 删除模型
|
||||
async deleteModel(modelId: string): Promise<{ success: boolean; message?: string }> {
|
||||
return request.delete(`/api/yolo/models/${modelId}/delete/`)
|
||||
},
|
||||
|
||||
// 启用模型
|
||||
async enableModel(modelId: string): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
return request.post(`/api/yolo/models/${modelId}/enable/`)
|
||||
},
|
||||
|
||||
// 获取模型详情
|
||||
async getModelDetail(modelId: string): Promise<{ success: boolean; data?: YoloModel; message?: string }> {
|
||||
return request.get(`/api/yolo/models/${modelId}/`)
|
||||
},
|
||||
|
||||
// 获取检测历史记录列表
|
||||
async getDetectionHistory(params?: {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
model_id?: string
|
||||
}): Promise<{ success: boolean; data?: DetectionHistoryRecord[]; message?: string }> {
|
||||
return request.get('/api/yolo/detections/', { params })
|
||||
},
|
||||
|
||||
// 获取检测记录详情
|
||||
async getDetectionDetail(recordId: string): Promise<{ success: boolean; data?: DetectionHistoryRecord; message?: string }> {
|
||||
return request.get(`/api/detections/${recordId}/`)
|
||||
},
|
||||
|
||||
// 删除检测记录
|
||||
async deleteDetection(recordId: string): Promise<{ success: boolean; message?: string }> {
|
||||
return request.delete(`/api/yolo/detections/${recordId}/delete/`)
|
||||
},
|
||||
|
||||
// 批量删除检测记录
|
||||
async batchDeleteDetections(ids: number[]): Promise<{ success: boolean; message?: string }> {
|
||||
return request.post('/api/yolo/detections/batch-delete/', { ids })
|
||||
},
|
||||
|
||||
// 获取检测统计
|
||||
async getDetectionStats(): Promise<{ success: boolean; data?: any; message?: string }> {
|
||||
return request.get('/api/yolo/stats/')
|
||||
},
|
||||
|
||||
// 数据集管理相关接口
|
||||
// 上传数据集
|
||||
async uploadDataset(formData: FormData): Promise<{ success: boolean; data?: YoloDatasetDetail; message?: string }> {
|
||||
return request.upload('/api/yolo/datasets/upload/', formData)
|
||||
},
|
||||
|
||||
// 获取数据集列表
|
||||
async getDatasets(): Promise<{ success: boolean; data?: YoloDatasetSummary[]; message?: string }> {
|
||||
return request.get('/api/yolo/datasets/')
|
||||
},
|
||||
|
||||
// 获取数据集详情
|
||||
async getDatasetDetail(datasetId: number): Promise<{ success: boolean; data?: YoloDatasetDetail; message?: string }> {
|
||||
return request.get(`/api/yolo/datasets/${datasetId}/`)
|
||||
},
|
||||
|
||||
// 删除数据集
|
||||
async deleteDataset(datasetId: number): Promise<{ success: boolean; message?: string }> {
|
||||
return request.post(`/api/yolo/datasets/${datasetId}/delete/`)
|
||||
},
|
||||
|
||||
// 获取数据集样本
|
||||
async getDatasetSamples(
|
||||
datasetId: number,
|
||||
params: { split?: 'train' | 'val' | 'test'; limit?: number; offset?: number } = {}
|
||||
): Promise<{
|
||||
success: boolean
|
||||
data?: { items: YoloDatasetSampleItem[]; total: number }
|
||||
message?: string
|
||||
}> {
|
||||
return request.get(`/api/yolo/datasets/${datasetId}/samples/`, { params })
|
||||
},
|
||||
|
||||
// YOLO 训练任务相关接口
|
||||
// 获取训练选项(可用数据集与模型版本)
|
||||
async getTrainOptions(): Promise<YoloTrainOptionsResponse> {
|
||||
return request.get('/api/yolo/train/options/')
|
||||
},
|
||||
|
||||
// 获取训练任务列表
|
||||
async getTrainJobs(): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: YoloTrainingJob[]
|
||||
}> {
|
||||
return request.get('/api/yolo/train/jobs/')
|
||||
},
|
||||
|
||||
// 创建并启动训练任务
|
||||
async startTrainJob(payload: StartTrainingPayload): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: YoloTrainingJob
|
||||
}> {
|
||||
return request.post('/api/yolo/train/jobs/start/', payload)
|
||||
},
|
||||
|
||||
// 获取训练任务详情
|
||||
async getTrainJobDetail(id: number): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: YoloTrainingJob
|
||||
}> {
|
||||
return request.get(`/api/yolo/train/jobs/${id}/`)
|
||||
},
|
||||
|
||||
// 获取训练任务日志(分页读取)
|
||||
async getTrainJobLogs(
|
||||
id: number,
|
||||
params: { offset?: number; max?: number } = {}
|
||||
): Promise<YoloTrainLogsResponse> {
|
||||
return request.get(`/api/yolo/train/jobs/${id}/logs/`, { params })
|
||||
},
|
||||
|
||||
// 取消训练任务
|
||||
async cancelTrainJob(id: number): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: YoloTrainingJob
|
||||
}> {
|
||||
return request.post(`/api/yolo/train/jobs/${id}/cancel/`)
|
||||
},
|
||||
|
||||
// 下载训练结果(ZIP)
|
||||
async downloadTrainJobResult(id: number): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: { url: string; size: number }
|
||||
}> {
|
||||
return request.get(`/api/yolo/train/jobs/${id}/download/`)
|
||||
},
|
||||
|
||||
// 删除训练任务
|
||||
async deleteTrainJob(id: number): Promise<{
|
||||
success: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
}> {
|
||||
return request.post(`/api/yolo/train/jobs/${id}/delete/`)
|
||||
},
|
||||
|
||||
// 警告等级管理相关接口
|
||||
// 获取警告等级列表
|
||||
async getAlertLevels(): Promise<{ success: boolean; data?: AlertLevel[]; message?: string }> {
|
||||
return request.get('/api/yolo/categories/')
|
||||
},
|
||||
|
||||
// 获取警告等级详情
|
||||
async getAlertLevelDetail(levelId: string): Promise<{ success: boolean; data?: AlertLevel; message?: string }> {
|
||||
return request.get(`/api/yolo/categories/${levelId}/`)
|
||||
},
|
||||
|
||||
// 更新警告等级
|
||||
async updateAlertLevel(levelId: string, data: { alert_level?: 'low' | 'medium' | 'high'; alias?: string }): Promise<{ success: boolean; data?: AlertLevel; message?: string }> {
|
||||
return request.put(`/api/yolo/categories/${levelId}/update/`, data)
|
||||
},
|
||||
|
||||
// 切换警告等级状态
|
||||
async toggleAlertLevelStatus(levelId: string): Promise<{ success: boolean; data?: AlertLevel; message?: string }> {
|
||||
return request.post(`/api/yolo/categories/${levelId}/toggle-status/`)
|
||||
},
|
||||
|
||||
// 获取活跃的警告等级列表
|
||||
async getActiveAlertLevels(): Promise<{ success: boolean; data?: AlertLevel[]; message?: string }> {
|
||||
return request.get('/api/yolo/categories/active/')
|
||||
},
|
||||
|
||||
// 上传并转换PT模型为ONNX格式
|
||||
async uploadAndConvertToOnnx(formData: FormData): Promise<{
|
||||
success: boolean
|
||||
message?: string
|
||||
data?: {
|
||||
onnx_path?: string
|
||||
onnx_url?: string
|
||||
download_url?: string
|
||||
onnx_relative_path?: string
|
||||
file_name?: string
|
||||
labels_download_url?: string
|
||||
labels_relative_path?: string
|
||||
classes?: string[]
|
||||
}
|
||||
}> {
|
||||
// 适配后端 @views.py 中的 upload_pt_convert_onnx 实现
|
||||
// 统一走 /api/upload_pt_convert_onnx
|
||||
// 按你的后端接口:/yolo/onnx/upload/
|
||||
// 注意带上结尾斜杠,避免 404
|
||||
return request.upload('/api/yolo/onnx/upload/', formData)
|
||||
}
|
||||
}
|
||||
|
||||
// 警告等级管理相关接口
|
||||
export interface AlertLevel {
|
||||
id: number
|
||||
model: number
|
||||
model_name: string
|
||||
name: string
|
||||
alias: string
|
||||
display_name: string
|
||||
category_id: number
|
||||
alert_level: 'low' | 'medium' | 'high'
|
||||
alert_level_display: string
|
||||
is_active: boolean
|
||||
// 前端编辑状态字段
|
||||
editingAlias?: boolean
|
||||
tempAlias?: string
|
||||
}
|
||||
|
||||
// 用户检测历史相关接口
|
||||
export interface DetectionHistoryRecord {
|
||||
id: number
|
||||
user_id: number
|
||||
original_filename: string
|
||||
result_filename: string
|
||||
original_file: string
|
||||
result_file: string
|
||||
detection_type: 'image' | 'video'
|
||||
object_count: number
|
||||
detected_categories: string[]
|
||||
confidence_scores: number[]
|
||||
avg_confidence: number | null
|
||||
processing_time: number
|
||||
model_name: string
|
||||
model_info: any
|
||||
created_at: string
|
||||
confidence_threshold?: number // 置信度阈值(原始设置值)
|
||||
// 为了兼容前端显示,添加计算字段
|
||||
filename?: string
|
||||
image_url?: string
|
||||
detections?: YoloDetection[]
|
||||
}
|
||||
|
||||
export interface DetectionHistoryParams {
|
||||
page?: number
|
||||
page_size?: number
|
||||
search?: string
|
||||
class_filter?: string
|
||||
start_date?: string
|
||||
end_date?: string
|
||||
model_id?: string
|
||||
}
|
||||
|
||||
export interface DetectionHistoryResponse {
|
||||
success?: boolean
|
||||
message?: string
|
||||
data?: {
|
||||
records: DetectionHistoryRecord[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
} | DetectionHistoryRecord[]
|
||||
// 支持直接返回数组的情况
|
||||
results?: DetectionHistoryRecord[]
|
||||
count?: number
|
||||
// 支持Django REST framework的分页格式
|
||||
next?: string
|
||||
previous?: string
|
||||
}
|
||||
|
||||
// 用户检测历史API
|
||||
export const detectionHistoryApi = {
|
||||
// 获取用户检测历史
|
||||
async getUserDetectionHistory(userId: number, params: DetectionHistoryParams = {}): Promise<DetectionHistoryResponse> {
|
||||
return request.get('/api/yolo/detections/', {
|
||||
params: {
|
||||
user_id: userId,
|
||||
...params
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 获取检测记录详情
|
||||
async getDetectionRecordDetail(recordId: number): Promise<{
|
||||
success?: boolean
|
||||
code?: number
|
||||
message?: string
|
||||
data?: DetectionHistoryRecord
|
||||
}> {
|
||||
return request.get(`/api/yolo/detections/${recordId}/`)
|
||||
},
|
||||
|
||||
// 删除检测记录
|
||||
async deleteDetectionRecord(userId: number, recordId: string): Promise<{ success: boolean; message?: string }> {
|
||||
return request.delete(`/api/yolo/detections/${recordId}/delete/`)
|
||||
},
|
||||
|
||||
// 批量删除检测记录
|
||||
async batchDeleteDetectionRecords(userId: number, recordIds: string[]): Promise<{ success: boolean; message?: string }> {
|
||||
return request.post('/api/yolo/detections/batch-delete/', { ids: recordIds })
|
||||
},
|
||||
|
||||
// 导出检测历史
|
||||
async exportDetectionHistory(userId: number, params: DetectionHistoryParams = {}): Promise<Blob> {
|
||||
const response = await request.get('/api/yolo/detections/export/', {
|
||||
params: {
|
||||
user_id: userId,
|
||||
...params
|
||||
},
|
||||
responseType: 'blob'
|
||||
})
|
||||
return response
|
||||
},
|
||||
|
||||
// 获取检测统计信息
|
||||
async getDetectionStats(userId: number): Promise<{
|
||||
success: boolean
|
||||
data?: {
|
||||
total_detections: number
|
||||
total_images: number
|
||||
class_counts: Record<string, number>
|
||||
recent_activity: Array<{
|
||||
date: string
|
||||
count: number
|
||||
}>
|
||||
}
|
||||
message?: string
|
||||
}> {
|
||||
return request.get('/api/yolo/detections/stats/', {
|
||||
params: { user_id: userId }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 告警相关接口类型定义
|
||||
export interface AlertRecord {
|
||||
id: number
|
||||
detection_record: number
|
||||
detection_info: {
|
||||
id: number
|
||||
detection_type: string
|
||||
original_filename: string
|
||||
result_filename: string
|
||||
object_count: number
|
||||
avg_confidence: number
|
||||
}
|
||||
user: number
|
||||
user_name: string
|
||||
alert_level: string
|
||||
alert_level_display: string
|
||||
alert_category: string
|
||||
category: number
|
||||
category_info: {
|
||||
id: number
|
||||
name: string
|
||||
alert_level: string
|
||||
alert_level_display: string
|
||||
}
|
||||
status: string
|
||||
created_at: string
|
||||
deleted_at: string | null
|
||||
}
|
||||
|
||||
// 告警管理API
|
||||
export const alertApi = {
|
||||
// 获取所有告警记录
|
||||
async getAllAlerts(): Promise<{ success: boolean; data?: AlertRecord[]; message?: string }> {
|
||||
return request.get('/api/yolo/alerts/')
|
||||
},
|
||||
|
||||
// 获取当前用户的告警记录
|
||||
async getUserAlerts(userId: string): Promise<{ success: boolean; data?: AlertRecord[]; message?: string }> {
|
||||
return request.get(`/api/yolo/users/${userId}/alerts/`)
|
||||
},
|
||||
|
||||
// 处理告警(更新状态)
|
||||
async updateAlertStatus(alertId: string, status: string): Promise<{ success: boolean; data?: AlertRecord; message?: string }> {
|
||||
return request.put(`/api/yolo/alerts/${alertId}/update-status/`, { status })
|
||||
}
|
||||
}
|
||||
|
||||
// 默认导出
|
||||
export default yoloApi
|
||||
85
hertz_server_django_ui/src/config/hertz_modules.ts
Normal file
85
hertz_server_django_ui/src/config/hertz_modules.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
export type HertzModuleGroup = 'admin' | 'user'
|
||||
|
||||
export interface HertzModule {
|
||||
key: string
|
||||
label: string
|
||||
group: HertzModuleGroup
|
||||
description?: string
|
||||
defaultEnabled: boolean
|
||||
}
|
||||
|
||||
export const HERTZ_MODULES: HertzModule[] = [
|
||||
{ key: 'admin.user-management', label: '管理端 · 用户管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.department-management', label: '管理端 · 部门管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.menu-management', label: '管理端 · 菜单管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.role-management', label: '管理端 · 角色管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.notification-management', label: '管理端 · 通知管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.log-management', label: '管理端 · 日志管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.knowledge-base', label: '管理端 · 文章管理', group: 'admin', defaultEnabled: true },
|
||||
{ key: 'admin.yolo-model', label: '管理端 · YOLO 模型相关', group: 'admin', defaultEnabled: true },
|
||||
|
||||
{ key: 'user.system-monitor', label: '用户端 · 系统监控', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.ai-chat', label: '用户端 · AI 助手', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.yolo-detection', label: '用户端 · YOLO 检测', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.live-detection', label: '用户端 · 实时检测', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.detection-history', label: '用户端 · 检测历史', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.alert-center', label: '用户端 · 告警中心', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.notice-center', label: '用户端 · 通知中心', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.knowledge-center', label: '用户端 · 文章中心', group: 'user', defaultEnabled: true },
|
||||
{ key: 'user.kb-center', label: '用户端 · 知识库中心', group: 'user', defaultEnabled: true },
|
||||
]
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'hertz_enabled_modules'
|
||||
|
||||
export function getEnabledModuleKeys(): string[] {
|
||||
const fallback = HERTZ_MODULES.filter(m => m.defaultEnabled).map(m => m.key)
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return fallback
|
||||
}
|
||||
|
||||
try {
|
||||
const stored = window.localStorage.getItem(LOCAL_STORAGE_KEY)
|
||||
if (!stored) return fallback
|
||||
const parsed = JSON.parse(stored)
|
||||
if (Array.isArray(parsed)) {
|
||||
const valid = parsed.filter((k): k is string => typeof k === 'string')
|
||||
// 自动合并新增的默认启用模块,避免新模块在已有选择下被永久隐藏
|
||||
const missingDefaults = HERTZ_MODULES
|
||||
.filter(m => m.defaultEnabled && !valid.includes(m.key))
|
||||
.map(m => m.key)
|
||||
return [...valid, ...missingDefaults]
|
||||
}
|
||||
return fallback
|
||||
} catch {
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
export function setEnabledModuleKeys(keys: string[]): void {
|
||||
if (typeof window === 'undefined') return
|
||||
try {
|
||||
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(keys))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function isModuleEnabled(moduleKey?: string, enabledKeys?: string[]): boolean {
|
||||
if (!moduleKey) return true
|
||||
const keys = enabledKeys ?? getEnabledModuleKeys()
|
||||
return keys.indexOf(moduleKey) !== -1
|
||||
}
|
||||
|
||||
export function getModulesByGroup(group: HertzModuleGroup): HertzModule[] {
|
||||
return HERTZ_MODULES.filter(m => m.group === group)
|
||||
}
|
||||
|
||||
export function hasModuleSelection(): boolean {
|
||||
if (typeof window === 'undefined') return false
|
||||
try {
|
||||
return window.localStorage.getItem(LOCAL_STORAGE_KEY) !== null
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
159
hertz_server_django_ui/src/locales/en-US.ts
Normal file
159
hertz_server_django_ui/src/locales/en-US.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
export default {
|
||||
common: {
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
delete: 'Delete',
|
||||
edit: 'Edit',
|
||||
add: 'Add',
|
||||
search: 'Search',
|
||||
reset: 'Reset',
|
||||
loading: 'Loading...',
|
||||
noData: 'No Data',
|
||||
success: 'Success',
|
||||
error: 'Error',
|
||||
warning: 'Warning',
|
||||
info: 'Info',
|
||||
},
|
||||
nav: {
|
||||
home: 'Home',
|
||||
dashboard: 'Dashboard',
|
||||
user: 'User Management',
|
||||
role: 'Role Management',
|
||||
menu: 'Menu Management',
|
||||
settings: 'System Settings',
|
||||
profile: 'Profile',
|
||||
logout: 'Logout',
|
||||
},
|
||||
login: {
|
||||
title: 'Login',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
login: 'Login',
|
||||
forgotPassword: 'Forgot Password?',
|
||||
rememberMe: 'Remember Me',
|
||||
},
|
||||
success: {
|
||||
// General success messages
|
||||
operationSuccess: 'Operation Successful',
|
||||
saveSuccess: 'Save Successful',
|
||||
deleteSuccess: 'Delete Successful',
|
||||
updateSuccess: 'Update Successful',
|
||||
|
||||
// Login and registration related success messages
|
||||
loginSuccess: 'Login Successful',
|
||||
registerSuccess: 'Registration Successful! Please Login',
|
||||
logoutSuccess: 'Logout Successful',
|
||||
emailCodeSent: 'Verification Code Sent to Your Email',
|
||||
|
||||
// User management related success messages
|
||||
userCreated: 'User Created Successfully',
|
||||
userUpdated: 'User Information Updated Successfully',
|
||||
userDeleted: 'User Deleted Successfully',
|
||||
roleAssigned: 'Role Assigned Successfully',
|
||||
|
||||
// Other operation success messages
|
||||
uploadSuccess: 'File Upload Successful',
|
||||
downloadSuccess: 'File Download Successful',
|
||||
copySuccess: 'Copy Successful',
|
||||
},
|
||||
error: {
|
||||
// General errors
|
||||
// 404: 'Page Not Found',
|
||||
403: 'Access Denied, Please Contact Administrator',
|
||||
500: 'Internal Server Error, Please Try Again Later',
|
||||
networkError: 'Network Connection Failed, Please Check Network Settings',
|
||||
timeout: 'Request Timeout, Please Try Again Later',
|
||||
|
||||
// Login related errors
|
||||
loginFailed: 'Login Failed, Please Check Username and Password',
|
||||
usernameRequired: 'Please Enter Username',
|
||||
passwordRequired: 'Please Enter Password',
|
||||
captchaRequired: 'Please Enter Captcha',
|
||||
captchaError: 'Captcha Error, Please Re-enter (Case Sensitive)',
|
||||
captchaExpired: 'Captcha Expired, Please Refresh and Re-enter',
|
||||
accountLocked: 'Account Locked, Please Contact Administrator',
|
||||
accountDisabled: 'Account Disabled, Please Contact Administrator',
|
||||
passwordExpired: 'Password Expired, Please Change Password',
|
||||
loginAttemptsExceeded: 'Too Many Login Attempts, Account Temporarily Locked',
|
||||
|
||||
// Registration related errors
|
||||
registerFailed: 'Registration Failed, Please Check Input Information',
|
||||
usernameExists: 'Username Already Exists, Please Choose Another',
|
||||
emailExists: 'Email Already Registered, Please Use Another Email',
|
||||
phoneExists: 'Phone Number Already Registered, Please Use Another',
|
||||
emailFormatError: 'Invalid Email Format, Please Enter Valid Email',
|
||||
phoneFormatError: 'Invalid Phone Format, Please Enter 11-digit Phone Number',
|
||||
passwordTooWeak: 'Password Too Weak, Please Include Uppercase, Lowercase, Numbers and Special Characters',
|
||||
passwordMismatch: 'Passwords Do Not Match',
|
||||
emailCodeError: 'Email Verification Code Error or Expired',
|
||||
emailCodeRequired: 'Please Enter Email Verification Code',
|
||||
emailCodeLength: 'Verification Code Must Be 6 Digits',
|
||||
emailRequired: 'Please Enter Email',
|
||||
usernameLength: 'Username Length Must Be 3-20 Characters',
|
||||
passwordLength: 'Password Length Must Be 6-20 Characters',
|
||||
confirmPasswordRequired: 'Please Confirm Password',
|
||||
phoneRequired: 'Please Enter Phone Number',
|
||||
realNameRequired: 'Please Enter Real Name',
|
||||
realNameLength: 'Name Length Must Be 2-10 Characters',
|
||||
|
||||
// Permission related errors
|
||||
accessDenied: 'Access Denied, You Do Not Have Permission to Perform This Action',
|
||||
roleNotFound: 'Role Not Found or Deleted',
|
||||
permissionDenied: 'Permission Denied, Cannot Perform This Action',
|
||||
tokenExpired: 'Login Expired, Please Login Again',
|
||||
tokenInvalid: 'Invalid Login Status, Please Login Again',
|
||||
|
||||
// User management related errors
|
||||
userNotFound: 'User Not Found or Deleted',
|
||||
userCreateFailed: 'Failed to Create User, Please Check Input Information',
|
||||
userUpdateFailed: 'Failed to Update User Information',
|
||||
userDeleteFailed: 'Failed to Delete User, User May Be In Use',
|
||||
cannotDeleteSelf: 'Cannot Delete Your Own Account',
|
||||
cannotDeleteAdmin: 'Cannot Delete Administrator Account',
|
||||
|
||||
// Department management related errors
|
||||
departmentNotFound: 'Department Not Found or Deleted',
|
||||
departmentNameExists: 'Department Name Already Exists',
|
||||
departmentHasUsers: 'Department Has Users, Cannot Delete',
|
||||
departmentCreateFailed: 'Failed to Create Department',
|
||||
departmentUpdateFailed: 'Failed to Update Department Information',
|
||||
departmentDeleteFailed: 'Failed to Delete Department',
|
||||
|
||||
// Role management related errors
|
||||
roleNameExists: 'Role Name Already Exists',
|
||||
roleCreateFailed: 'Failed to Create Role',
|
||||
roleUpdateFailed: 'Failed to Update Role Information',
|
||||
roleDeleteFailed: 'Failed to Delete Role',
|
||||
roleInUse: 'Role In Use, Cannot Delete',
|
||||
|
||||
// File upload related errors
|
||||
fileUploadFailed: 'File Upload Failed',
|
||||
fileSizeExceeded: 'File Size Exceeded Limit',
|
||||
fileTypeNotSupported: 'File Type Not Supported',
|
||||
fileRequired: 'Please Select File to Upload',
|
||||
|
||||
// Data validation related errors
|
||||
invalidInput: 'Invalid Input Data Format',
|
||||
requiredFieldMissing: 'Required Field Cannot Be Empty',
|
||||
fieldTooLong: 'Input Content Exceeds Length Limit',
|
||||
fieldTooShort: 'Input Content Length Insufficient',
|
||||
invalidDate: 'Invalid Date Format',
|
||||
invalidNumber: 'Invalid Number Format',
|
||||
|
||||
// Operation related errors
|
||||
operationFailed: 'Operation Failed, Please Try Again Later',
|
||||
saveSuccess: 'Save Successful',
|
||||
saveFailed: 'Save Failed, Please Check Input Information',
|
||||
deleteSuccess: 'Delete Successful',
|
||||
deleteFailed: 'Delete Failed, Please Try Again Later',
|
||||
updateSuccess: 'Update Successful',
|
||||
updateFailed: 'Update Failed, Please Check Input Information',
|
||||
|
||||
// System related errors
|
||||
systemMaintenance: 'System Under Maintenance, Please Visit Later',
|
||||
serviceUnavailable: 'Service Temporarily Unavailable, Please Try Again Later',
|
||||
databaseError: 'Database Connection Error, Please Contact Technical Support',
|
||||
configError: 'System Configuration Error, Please Contact Administrator',
|
||||
},
|
||||
}
|
||||
18
hertz_server_django_ui/src/locales/index.ts
Normal file
18
hertz_server_django_ui/src/locales/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import zhCN from './zh-CN'
|
||||
import enUS from './en-US'
|
||||
|
||||
const messages = {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
locale: 'zh-CN',
|
||||
fallbackLocale: 'en-US',
|
||||
messages,
|
||||
legacy: false,
|
||||
globalInjection: true,
|
||||
})
|
||||
|
||||
export default i18n
|
||||
172
hertz_server_django_ui/src/locales/zh-CN.ts
Normal file
172
hertz_server_django_ui/src/locales/zh-CN.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
export default {
|
||||
common: {
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
add: '添 加',
|
||||
search: '搜索',
|
||||
reset: '重置',
|
||||
loading: '加载中...',
|
||||
noData: '暂无数据',
|
||||
success: '成功',
|
||||
error: '错误',
|
||||
warning: '警告',
|
||||
info: '提示',
|
||||
},
|
||||
nav: {
|
||||
home: '首页',
|
||||
dashboard: '仪表板',
|
||||
user: '用户管理',
|
||||
role: '角色管理',
|
||||
menu: '菜单管理',
|
||||
settings: '系统设置',
|
||||
profile: '个人资料',
|
||||
logout: '退出登录',
|
||||
},
|
||||
login: {
|
||||
title: '登录',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
login: '登录',
|
||||
forgotPassword: '忘记密码?',
|
||||
rememberMe: '记住我',
|
||||
},
|
||||
register: {
|
||||
title: '注册',
|
||||
username: '用户名',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
confirmPassword: '确认密码',
|
||||
register: '注册',
|
||||
agreement: '我已阅读并同意',
|
||||
userAgreement: '用户协议',
|
||||
privacyPolicy: '隐私政策',
|
||||
hasAccount: '已有账号?',
|
||||
goToLogin: '立即登录',
|
||||
},
|
||||
success: {
|
||||
// 通用成功提示
|
||||
operationSuccess: '操作成功',
|
||||
saveSuccess: '保存成功',
|
||||
deleteSuccess: '删除成功',
|
||||
updateSuccess: '更新成功',
|
||||
|
||||
// 登录注册相关成功提示
|
||||
loginSuccess: '登录成功',
|
||||
registerSuccess: '注册成功!请前往登录',
|
||||
logoutSuccess: '退出登录成功',
|
||||
emailCodeSent: '验证码已发送到您的邮箱',
|
||||
|
||||
// 用户管理相关成功提示
|
||||
userCreated: '用户创建成功',
|
||||
userUpdated: '用户信息更新成功',
|
||||
userDeleted: '用户删除成功',
|
||||
roleAssigned: '角色分配成功',
|
||||
|
||||
// 其他操作成功提示
|
||||
uploadSuccess: '文件上传成功',
|
||||
downloadSuccess: '文件下载成功',
|
||||
copySuccess: '复制成功',
|
||||
},
|
||||
error: {
|
||||
// 通用错误
|
||||
// 404: '页面未找到',
|
||||
403: '权限不足,请联系管理员',
|
||||
500: '服务器内部错误,请稍后重试',
|
||||
networkError: '网络连接失败,请检查网络设置',
|
||||
timeout: '请求超时,请稍后重试',
|
||||
|
||||
// 登录相关错误
|
||||
loginFailed: '登录失败,请检查用户名和密码',
|
||||
usernameRequired: '请输入用户名',
|
||||
passwordRequired: '请输入密码',
|
||||
captchaRequired: '请输入验证码',
|
||||
captchaError: '验证码错误,请重新输入(区分大小写)',
|
||||
captchaExpired: '验证码已过期,请刷新后重新输入',
|
||||
accountLocked: '账户已被锁定,请联系管理员',
|
||||
accountDisabled: '账户已被禁用,请联系管理员',
|
||||
passwordExpired: '密码已过期,请修改密码',
|
||||
loginAttemptsExceeded: '登录尝试次数过多,账户已被临时锁定',
|
||||
|
||||
// 注册相关错误
|
||||
registerFailed: '注册失败,请检查输入信息',
|
||||
usernameExists: '用户名已存在,请选择其他用户名',
|
||||
emailExists: '邮箱已被注册,请使用其他邮箱',
|
||||
phoneExists: '手机号已被注册,请使用其他手机号',
|
||||
emailFormatError: '邮箱格式不正确,请输入有效的邮箱地址',
|
||||
phoneFormatError: '手机号格式不正确,请输入11位手机号',
|
||||
passwordTooWeak: '密码强度不足,请包含大小写字母、数字和特殊字符',
|
||||
passwordMismatch: '两次输入的密码不一致',
|
||||
emailCodeError: '邮箱验证码错误或已过期',
|
||||
emailCodeRequired: '请输入邮箱验证码',
|
||||
emailCodeLength: '验证码长度为6位',
|
||||
emailRequired: '请输入邮箱',
|
||||
usernameLength: '用户名长度为3-20个字符',
|
||||
passwordLength: '密码长度为6-20个字符',
|
||||
confirmPasswordRequired: '请确认密码',
|
||||
phoneRequired: '请输入手机号',
|
||||
realNameRequired: '请输入真实姓名',
|
||||
realNameLength: '姓名长度为2-10个字符',
|
||||
|
||||
// 权限相关错误
|
||||
accessDenied: '访问被拒绝,您没有执行此操作的权限',
|
||||
roleNotFound: '角色不存在或已被删除',
|
||||
permissionDenied: '权限不足,无法执行此操作',
|
||||
tokenExpired: '登录已过期,请重新登录',
|
||||
tokenInvalid: '登录状态无效,请重新登录',
|
||||
|
||||
// 用户管理相关错误
|
||||
userNotFound: '用户不存在或已被删除',
|
||||
userCreateFailed: '创建用户失败,请检查输入信息',
|
||||
userUpdateFailed: '更新用户信息失败',
|
||||
userDeleteFailed: '删除用户失败,该用户可能正在使用中',
|
||||
cannotDeleteSelf: '不能删除自己的账户',
|
||||
cannotDeleteAdmin: '不能删除管理员账户',
|
||||
|
||||
// 部门管理相关错误
|
||||
departmentNotFound: '部门不存在或已被删除',
|
||||
departmentNameExists: '部门名称已存在',
|
||||
departmentHasUsers: '部门下还有用户,无法删除',
|
||||
departmentCreateFailed: '创建部门失败',
|
||||
departmentUpdateFailed: '更新部门信息失败',
|
||||
departmentDeleteFailed: '删除部门失败',
|
||||
|
||||
// 角色管理相关错误
|
||||
roleNameExists: '角色名称已存在',
|
||||
roleCreateFailed: '创建角色失败',
|
||||
roleUpdateFailed: '更新角色信息失败',
|
||||
roleDeleteFailed: '删除角色失败',
|
||||
roleInUse: '角色正在使用中,无法删除',
|
||||
|
||||
// 文件上传相关错误
|
||||
fileUploadFailed: '文件上传失败',
|
||||
fileSizeExceeded: '文件大小超出限制',
|
||||
fileTypeNotSupported: '不支持的文件类型',
|
||||
fileRequired: '请选择要上传的文件',
|
||||
|
||||
// 数据验证相关错误
|
||||
invalidInput: '输入数据格式不正确',
|
||||
requiredFieldMissing: '必填字段不能为空',
|
||||
fieldTooLong: '输入内容超出长度限制',
|
||||
fieldTooShort: '输入内容长度不足',
|
||||
invalidDate: '日期格式不正确',
|
||||
invalidNumber: '数字格式不正确',
|
||||
|
||||
// 操作相关错误
|
||||
operationFailed: '操作失败,请稍后重试',
|
||||
saveSuccess: '保存成功',
|
||||
saveFailed: '保存失败,请检查输入信息',
|
||||
deleteSuccess: '删除成功',
|
||||
deleteFailed: '删除失败,请稍后重试',
|
||||
updateSuccess: '更新成功',
|
||||
updateFailed: '更新失败,请检查输入信息',
|
||||
|
||||
// 系统相关错误
|
||||
systemMaintenance: '系统正在维护中,请稍后访问',
|
||||
serviceUnavailable: '服务暂时不可用,请稍后重试',
|
||||
databaseError: '数据库连接错误,请联系技术支持',
|
||||
configError: '系统配置错误,请联系管理员',
|
||||
},
|
||||
}
|
||||
47
hertz_server_django_ui/src/main.ts
Normal file
47
hertz_server_django_ui/src/main.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { i18n } from './locales'
|
||||
import { checkEnvironmentVariables, validateEnvironment } from './utils/hertz_env'
|
||||
import './styles/index.scss'
|
||||
|
||||
// 导入Ant Design Vue
|
||||
import 'ant-design-vue/dist/antd.css'
|
||||
|
||||
// 开发环境检查
|
||||
if (import.meta.env.DEV) {
|
||||
checkEnvironmentVariables()
|
||||
validateEnvironment()
|
||||
}
|
||||
|
||||
// 创建Vue应用实例
|
||||
const app = createApp(App)
|
||||
|
||||
// 使用Pinia状态管理
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
|
||||
// 使用路由
|
||||
app.use(router)
|
||||
|
||||
// 使用国际化
|
||||
app.use(i18n)
|
||||
|
||||
// 初始化应用设置
|
||||
import { useAppStore } from './stores/hertz_app'
|
||||
const appStore = useAppStore()
|
||||
appStore.initAppSettings()
|
||||
|
||||
// 检查用户认证状态
|
||||
import { useUserStore } from './stores/hertz_user'
|
||||
const userStore = useUserStore()
|
||||
userStore.checkAuth()
|
||||
|
||||
// 初始化主题(必须在挂载前加载)
|
||||
import { useThemeStore } from './stores/hertz_theme'
|
||||
const themeStore = useThemeStore()
|
||||
themeStore.loadTheme()
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user