重新提交
This commit is contained in:
52
.env
52
.env
@@ -1,52 +0,0 @@
|
|||||||
# 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
101
.gitignore
vendored
@@ -1,101 +0,0 @@
|
|||||||
# 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
BIN
data/db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,246 +0,0 @@
|
|||||||
# 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`)。
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
# 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`)。
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
# 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)。
|
|
||||||
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
# 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": "代码生成失败: <错误信息>"}
|
|
||||||
```
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
# 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`,包含格式化的请求数据与成功判断等辅助字段。
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
# 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": "未授权访问"}
|
|
||||||
```
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
# 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": ["邮箱格式不正确"]}}
|
|
||||||
```
|
|
||||||
@@ -1,391 +0,0 @@
|
|||||||
# 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` 状态记录。
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# 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": "<错误信息>"}
|
|
||||||
```
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
68
docs/使用手册.md
68
docs/使用手册.md
@@ -1,68 +0,0 @@
|
|||||||
# 使用手册
|
|
||||||
|
|
||||||
## 一、**环境要求**
|
|
||||||
|
|
||||||
- `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
158
docs/开发规范.md
@@ -1,158 +0,0 @@
|
|||||||
# 新功能开发规范
|
|
||||||
|
|
||||||
## 一、命名规范
|
|
||||||
- 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
53
docs/项目简介.md
@@ -1,53 +0,0 @@
|
|||||||
# 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`。
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
@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
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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}")
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/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
11
hertz.txt
@@ -1,11 +0,0 @@
|
|||||||
# ================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
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
# 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) - 开发规范和最佳实践
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
💡 **提示**: 此模块主要用于功能演示和学习参考,生产环境请根据实际需求进行适当调整和优化。
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class DemoConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'hertz_demo'
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
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))
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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()),
|
|
||||||
]
|
|
||||||
@@ -1,499 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,520 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,556 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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'),
|
|
||||||
]
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
"""
|
|
||||||
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),
|
|
||||||
})
|
|
||||||
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
"""
|
|
||||||
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',
|
|
||||||
'hertz_studio_django_codegen', # 自动注册的应用
|
|
||||||
|
|
||||||
# 必备注册的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',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Hertz Captcha settings (used by hertz_studio_django_captcha.captcha_generator.HertzCaptchaGenerator)
|
|
||||||
HERTZ_CAPTCHA_LENGTH = config('HERTZ_CAPTCHA_LENGTH', default=4, cast=int)
|
|
||||||
HERTZ_CAPTCHA_WIDTH = config('HERTZ_CAPTCHA_WIDTH', default=160, cast=int)
|
|
||||||
HERTZ_CAPTCHA_HEIGHT = config('HERTZ_CAPTCHA_HEIGHT', default=60, cast=int)
|
|
||||||
HERTZ_CAPTCHA_FONT_SIZE = config('HERTZ_CAPTCHA_FONT_SIZE', default=40, cast=int)
|
|
||||||
HERTZ_CAPTCHA_TIMEOUT = config('HERTZ_CAPTCHA_TIMEOUT', default=300, cast=int)
|
|
||||||
HERTZ_CAPTCHA_BACKGROUND_COLOR = config('HERTZ_CAPTCHA_BACKGROUND_COLOR', default='#ffffff')
|
|
||||||
HERTZ_CAPTCHA_FOREGROUND_COLOR = config('HERTZ_CAPTCHA_FOREGROUND_COLOR', default='#000000')
|
|
||||||
HERTZ_CAPTCHA_NOISE_LEVEL = config('HERTZ_CAPTCHA_NOISE_LEVEL', default=0.3, cast=float)
|
|
||||||
HERTZ_CAPTCHA_REDIS_KEY_PREFIX = config('HERTZ_CAPTCHA_REDIS_KEY_PREFIX', default='hertz_captcha:')
|
|
||||||
HERTZ_CAPTCHA_FONT_PATH = config('HERTZ_CAPTCHA_FONT_PATH', default=str(MEDIA_ROOT / 'arial.ttf'))
|
|
||||||
|
|
||||||
# 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',
|
|
||||||
]
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
"""
|
|
||||||
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])
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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')
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
"""
|
|
||||||
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()
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
VITE_API_BASE_URL=http://localhost:8000
|
|
||||||
VITE_TEMPLATE_SETUP_MODE=true
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
VITE_API_BASE_URL=http://localhost:8000
|
|
||||||
VITE_TEMPLATE_SETUP_MODE=true
|
|
||||||
24
hertz_server_django_ui/.gitignore
vendored
24
hertz_server_django_ui/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# 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?
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
<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
80
hertz_server_django_ui/components.d.ts
vendored
@@ -1,80 +0,0 @@
|
|||||||
/* 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']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
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',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!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
5673
hertz_server_django_ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"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 +0,0 @@
|
|||||||
[]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,392 +0,0 @@
|
|||||||
#!/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)
|
|
||||||
})
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
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),
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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/')
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
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/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// 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'
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
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/`)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
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 } })
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
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 }),
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
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 })
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
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/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
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/'),
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
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/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,643 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
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',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
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: '系统配置错误,请联系管理员',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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