前后端第一版提交

This commit is contained in:
2025-11-11 17:21:59 +08:00
commit 96e9a6d396
241 changed files with 197906 additions and 0 deletions

103
.gitignore vendored Normal file
View File

@@ -0,0 +1,103 @@
# Python bytecode
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# 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
db.sqlite3
data/db.sqlite3
data/*.sqlite3
media/
staticfiles/
static_root/
# Environment variables & virtualenvs
.env
.env.*
.envrc
.venv/
venv/
ENV/
env/
# 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

84
generate_menu.py Normal file
View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python
"""
菜单生成器命令行工具
用于快速生成菜单配置和权限同步
"""
import os
import sys
import argparse
import django
from pathlib import Path
# 添加项目路径
project_root = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, project_root)
# 设置Django环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
django.setup()
from hertz_studio_django_utils.code_generator.menu_generator import MenuGenerator
def generate_crud_menu(args):
"""生成CRUD菜单"""
generator = MenuGenerator()
operations = args.operations.split(',') if args.operations else ['list', 'create', 'update', 'delete']
menus = generator.generate_menu_config(
module_name=args.module_name,
model_name=args.model_name,
operations=operations,
parent_code=args.parent_code,
menu_prefix=args.prefix,
sort_order=args.sort_order,
icon=args.icon
)
# 保存到待同步文件
pending_file = os.path.join(project_root, 'pending_menus.py')
with open(pending_file, 'w', encoding='utf-8') as f:
f.write('# 待同步的菜单配置\n')
f.write('pending_menus = [\n')
for menu in menus:
f.write(' {\n')
for key, value in menu.items():
if isinstance(value, str):
f.write(f" '{key}': '{value}',\n")
elif value is None:
f.write(f" '{key}': None,\n")
else:
f.write(f" '{key}': {value},\n")
f.write(' },\n')
f.write(']\n')
print(f"已生成 {len(menus)} 个菜单配置,保存到 pending_menus.py")
print("请重启服务器以同步菜单到数据库")
def 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__":
main()

63
get_tokens.py Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
获取用户JWT token的脚本
"""
import os
import sys
import django
# 设置Django环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hertz_server_django.settings')
django.setup()
from hertz_studio_django_auth.models import HertzUser
from hertz_studio_django_auth.utils.auth.token_utils import TokenUtils
def get_user_tokens():
"""获取用户token"""
try:
# 获取普通用户luobo
user = HertzUser.objects.get(username='luobo')
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()
# 获取管理员用户pppp
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()

274
hertz_demo/README.md Normal file
View File

@@ -0,0 +1,274 @@
# Hertz Demo 演示模块
## 📋 模块概述
Hertz Demo 模块是一个功能演示和测试模块,提供了完整的示例代码和交互式演示页面,帮助开发者快速了解和使用 Hertz Server Django 框架的各项功能特性。
## ✨ 功能特性
- **验证码演示**: 展示多种验证码类型的生成、刷新和验证功能
- **邮件系统演示**: 提供邮件模板预览和发送测试功能
- **WebSocket演示**: 实时通信功能演示和测试
- **交互式界面**: 美观的Web界面支持实时操作和反馈
- **完整示例代码**: 提供可直接参考的实现代码
## 📁 模块结构
```
hertz_demo/
├── __init__.py # 模块初始化
├── apps.py # Django应用配置
├── models.py # 数据模型(预留)
├── views.py # 视图函数和业务逻辑
├── urls.py # URL路由配置
├── tests.py # 单元测试
├── consumers.py # WebSocket消费者
├── routing.py # WebSocket路由
└── templates/ # 模板文件
├── captcha_demo.html # 验证码演示页面
├── email_demo.html # 邮件系统演示页面
└── websocket_demo.html # WebSocket演示页面
```
## 🎯 核心功能详解
### 1. 验证码演示功能
验证码演示页面提供三种验证码类型:
- **随机字符验证码**: 随机生成的字母数字组合
- **数学运算验证码**: 简单的数学计算验证
- **单词验证码**: 英文单词验证
**主要功能**:
- 验证码实时生成和刷新
- 前端Ajax验证
- 后端表单验证
- 验证码类型切换
### 2. 邮件系统演示功能
邮件演示页面提供多种邮件模板:
- **欢迎邮件**: 用户注册欢迎邮件模板
- **系统通知**: 系统消息通知模板
- **邮箱验证**: 邮箱验证邮件模板
- **自定义邮件**: 支持自定义主题和内容
**主要功能**:
- 邮件模板实时预览
- 邮件发送测试
- 收件人邮箱验证
- 发送状态反馈
### 3. WebSocket演示功能
WebSocket演示页面提供实时通信功能
- **连接状态管理**: 显示WebSocket连接状态
- **消息发送接收**: 实时消息通信
- **广播功能**: 消息广播演示
- **错误处理**: 连接异常处理
## 🚀 API接口
### 演示页面路由
| 路由 | 方法 | 描述 |
|------|------|------|
| `/demo/captcha/` | GET | 验证码演示页面 |
| `/demo/email/` | GET | 邮件系统演示页面 |
| `/demo/websocket/` | GET | WebSocket演示页面 |
| `/websocket/test/` | GET | WebSocket测试页面 |
### Ajax接口
**验证码相关**:
- `POST /demo/captcha/` (Ajax): 验证码刷新和验证
- 请求体: `{"action": "refresh/verify", "captcha_id": "...", "user_input": "..."}`
**邮件发送**:
- `POST /demo/email/` (Ajax): 发送演示邮件
- 请求体: 邮件类型、收件人邮箱、自定义内容等
## ⚙️ 配置参数
### 邮件配置settings.py
```python
# 邮件服务器配置
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@gmail.com'
EMAIL_HOST_PASSWORD = 'your-app-password'
DEFAULT_FROM_EMAIL = 'noreply@yourdomain.com'
```
### WebSocket配置
```python
# ASGI配置
ASGI_APPLICATION = 'hertz_server_django.asgi.application'
# Channel layers配置
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('127.0.0.1', 6379)],
},
},
}
```
## 🛠️ 快速开始
### 1. 访问演示页面
启动开发服务器后访问以下URL
```bash
# 验证码演示
http://localhost:8000/demo/captcha/
# 邮件系统演示
http://localhost:8000/demo/email/
# WebSocket演示
http://localhost:8000/demo/websocket/
```
### 2. 测试验证码功能
1. 打开验证码演示页面
2. 选择验证码类型
3. 点击验证码图片可刷新
4. 输入验证码进行验证
5. 观察验证结果反馈
### 3. 测试邮件功能
1. 打开邮件演示页面
2. 选择邮件模板类型
3. 输入收件人邮箱
4. 点击发送测试邮件
5. 查看发送状态
### 4. 测试WebSocket功能
1. 打开WebSocket演示页面
2. 点击"连接"按钮建立连接
3. 在输入框中发送消息
4. 观察消息接收和广播
5. 测试断开重连功能
## 🔧 高级用法
### 自定义邮件模板
`views.py` 中的 `generate_email_content` 函数中添加新的邮件模板:
```python
def generate_email_content(email_type, recipient_name, custom_subject='', custom_message=''):
email_templates = {
'your_template': {
'subject': '您的邮件主题',
'html_template': '''
<html>
<!-- 您的HTML模板内容 -->
</html>
'''
}
}
# ...
```
### 扩展验证码类型
在验证码演示中扩展新的验证码类型:
```python
# 在 captcha_demo 函数中添加新的验证码类型
captcha_types = {
'random_char': '随机字符验证码',
'math': '数学运算验证码',
'word': '单词验证码',
'new_type': '您的新验证码类型' # 新增类型
}
```
### WebSocket消息处理
`consumers.py` 中扩展WebSocket消息处理逻辑
```python
class DemoConsumer(WebsocketConsumer):
async def receive(self, text_data):
data = json.loads(text_data)
message_type = data.get('type')
if message_type == 'custom_message':
# 处理自定义消息类型
await self.handle_custom_message(data)
```
## 🧪 测试
### 运行单元测试
```bash
python manage.py test hertz_demo
```
### 测试覆盖范围
- 验证码功能测试
- 邮件发送测试
- WebSocket连接测试
- 页面渲染测试
## 🔒 安全考虑
### 验证码安全
- 验证码有效期限制
- 验证次数限制
- 防止暴力破解
### 邮件安全
- 收件人邮箱验证
- 发送频率限制
- 防止邮件滥用
### WebSocket安全
- 连接认证
- 消息内容过滤
- 防止DDoS攻击
## ❓ 常见问题
### Q: 邮件发送失败怎么办?
A: 检查邮件服务器配置确保SMTP设置正确邮箱密码为应用专用密码。
### Q: WebSocket连接失败怎么办
A: 检查Redis服务是否运行确保CHANNEL_LAYERS配置正确。
### Q: 验证码验证总是失败?
A: 检查验证码存储后端Redis是否正常运行。
### Q: 如何添加新的演示功能?
A: 在views.py中添加新的视图函数在urls.py中配置路由在templates中添加模板文件。
## 📝 更新日志
### v1.0.0 (2024-01-01)
- 初始版本发布
- 包含验证码、邮件、WebSocket演示功能
- 提供完整的示例代码和文档
## 🔗 相关链接
- [🏠 返回主项目](../README.md) - Hertz Server Django 主项目文档
- [🔐 认证授权模块](../hertz_studio_django_auth/README.md) - 用户管理和权限控制
- [🛠️ 工具类模块](../hertz_studio_django_utils/README.md) - 加密、邮件和验证工具
- [📋 代码风格指南](../docs/CODING_STYLE.md) - 开发规范和最佳实践
---
💡 **提示**: 此模块主要用于功能演示和学习参考,生产环境请根据实际需求进行适当调整和优化。

0
hertz_demo/__init__.py Normal file
View File

6
hertz_demo/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class DemoConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'hertz_demo'

120
hertz_demo/consumers.py Normal file
View File

@@ -0,0 +1,120 @@
import json
from datetime import datetime
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json.get('type', 'chat_message')
message = text_data_json.get('message', '')
username = text_data_json.get('username', 'Anonymous')
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': message_type,
'message': message,
'username': username,
'timestamp': datetime.now().strftime('%H:%M:%S')
}
)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
username = event['username']
timestamp = event['timestamp']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'type': 'chat_message',
'message': message,
'username': username,
'timestamp': timestamp
}))
async def user_join(self, event):
username = event['username']
timestamp = event['timestamp']
await self.send(text_data=json.dumps({
'type': 'user_notification',
'message': f'{username} 加入了聊天室',
'username': username,
'timestamp': timestamp
}))
async def user_leave(self, event):
username = event['username']
timestamp = event['timestamp']
await self.send(text_data=json.dumps({
'type': 'user_notification',
'message': f'{username} 离开了聊天室',
'username': username,
'timestamp': timestamp
}))
class EchoConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
async def disconnect(self, close_code):
pass
async def receive(self, text_data):
try:
# 解析接收到的JSON数据
data = json.loads(text_data)
message = data.get('message', '').strip()
if message:
# 返回回声消息
response = {
'type': 'echo_message',
'original_message': message,
'echo_message': f'回声: {message}',
'timestamp': datetime.now().strftime('%H:%M:%S')
}
else:
# 如果消息为空
response = {
'type': 'error',
'message': '消息不能为空',
'timestamp': datetime.now().strftime('%H:%M:%S')
}
await self.send(text_data=json.dumps(response, ensure_ascii=False))
except json.JSONDecodeError:
# JSON解析错误
error_response = {
'type': 'error',
'message': '无效的JSON格式',
'timestamp': datetime.now().strftime('%H:%M:%S')
}
await self.send(text_data=json.dumps(error_response, ensure_ascii=False))

View File

3
hertz_demo/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

7
hertz_demo/routing.py Normal file
View File

@@ -0,0 +1,7 @@
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
re_path(r'ws/echo/$', consumers.EchoConsumer.as_asgi()),
]

View File

@@ -0,0 +1,499 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hertz验证码演示 - Hertz Server Django</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
.demo-card {
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
overflow: hidden;
}
.demo-header {
background: #667eea;
color: white;
padding: 1rem;
text-align: center;
}
.demo-header h3 {
font-size: 1.3rem;
margin-bottom: 0.5rem;
}
.demo-content {
padding: 1.5rem;
}
@media (max-width: 768px) {
.demo-grid {
grid-template-columns: 1fr;
}
.header h1 {
font-size: 2rem;
}
body {
padding: 1rem;
}
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: #333;
}
.form-group input {
width: 100%;
padding: 0.8rem;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.captcha-container {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.captcha-image {
border: 2px solid #e1e5e9;
border-radius: 8px;
cursor: pointer;
transition: border-color 0.3s ease;
}
.captcha-image:hover {
border-color: #667eea;
}
.btn {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: background 0.3s ease;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5a6fd8;
}
.btn-success {
background: #4CAF50;
color: white;
}
.btn-success:hover {
background: #45a049;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.refresh-btn {
background: #667eea;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 5px;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.3s ease;
}
.refresh-btn:hover {
background: #5a6fd8;
}
.submit-btn {
width: 100%;
background: #667eea;
color: white;
border: none;
padding: 1rem;
border-radius: 5px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: background 0.3s ease;
}
.submit-btn:hover {
background: #5a6fd8;
}
.back-link {
text-align: center;
margin-top: 2rem;
}
.back-link a {
color: white;
text-decoration: none;
font-weight: 500;
font-size: 1.1rem;
}
.back-link a:hover {
text-decoration: underline;
}
.info-box {
background: #f8f9fa;
border-left: 4px solid #667eea;
padding: 1rem;
margin-bottom: 2rem;
border-radius: 0 8px 8px 0;
}
.info-box h3 {
color: #667eea;
margin-bottom: 0.5rem;
}
.info-box ul {
margin-left: 1.5rem;
color: #666;
}
.info-box li {
margin-bottom: 0.3rem;
}
.info-box code {
background: #e9ecef;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.captcha-types {
background: #f8f9fa;
border-left: 4px solid #28a745;
padding: 1rem;
margin-bottom: 2rem;
border-radius: 0 8px 8px 0;
}
.type-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 0.5rem;
}
.type-btn {
background: #e9ecef;
color: #495057;
border: 2px solid #dee2e6;
padding: 0.5rem 1rem;
border-radius: 5px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s ease;
flex: 1;
min-width: 120px;
}
.type-btn:hover {
background: #dee2e6;
border-color: #adb5bd;
}
.type-btn.active {
background: #28a745;
color: white;
border-color: #28a745;
}
.demo-section {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 1rem;
margin-bottom: 2rem;
border-radius: 0 8px 8px 0;
}
.demo-section h4 {
color: #856404;
margin-bottom: 0.5rem;
}
.demo-section p {
color: #856404;
margin-bottom: 0.3rem;
}
.success-message {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 Hertz验证码演示</h1>
<p>{{ demo_description }}</p>
</div>
<div class="demo-grid">
<!-- 验证码功能介绍 -->
<div class="demo-card">
<div class="demo-header">
<h3>🎯 Hertz验证码功能特性</h3>
<p>自定义验证码系统支持Redis缓存</p>
</div>
<div class="demo-content">
<div class="info-box">
<ul>
<li>🔤 随机字符验证码 - 生成随机字母数字组合</li>
<li>🎨 自定义样式配置 - 支持颜色、字体、噪声等设置</li>
<li>⚡ Ajax刷新功能 - 无需刷新页面</li>
<li>💾 Redis缓存 - 高性能数据存储</li>
<li>⏰ 超时自动失效 - 可配置过期时间</li>
<li>🔧 灵活配置 - 通过settings.py进行配置</li>
</ul>
</div>
<div class="captcha-types">
<h3 style="margin-bottom: 1rem; color: #667eea;">配置信息</h3>
<p style="color: #666; font-size: 0.9rem; margin-bottom: 0.5rem;">• 验证码长度: 可通过HERTZ_CAPTCHA_LENGTH配置</p>
<p style="color: #666; font-size: 0.9rem; margin-bottom: 0.5rem;">• 图片尺寸: 可通过HERTZ_CAPTCHA_WIDTH/HEIGHT配置</p>
<p style="color: #666; font-size: 0.9rem; margin-bottom: 0.5rem;">• 过期时间: 可通过HERTZ_CAPTCHA_TIMEOUT配置</p>
<p style="color: #666; font-size: 0.9rem;">• Redis前缀: 可通过HERTZ_CAPTCHA_REDIS_KEY_PREFIX配置</p>
</div>
</div>
</div>
<!-- 验证码测试 -->
<div class="demo-card">
<div class="demo-header">
<h3>🔒 Hertz验证码测试</h3>
<p>输入验证码进行功能测试</p>
</div>
<div class="demo-content">
{% if messages %}
{% for message in messages %}
<div class="success-message" style="{% if message.tags == 'error' %}background: #f8d7da; color: #721c24; border-color: #f5c6cb;{% endif %}">
{% if message.tags == 'success' %}✅{% elif message.tags == 'error' %}❌{% endif %} {{ message }}
</div>
{% endfor %}
{% endif %}
<div class="demo-section">
<h4>📋 Hertz验证码说明</h4>
<p>• 随机字符验证码:生成随机字母和数字组合</p>
<p>• 特点自定义样式支持噪声干扰Redis缓存存储</p>
<p>• 功能支持Ajax刷新自动过期失效</p>
</div>
<form method="post" id="captcha-form">
{% csrf_token %}
<div class="form-group">
<label for="captcha_input">验证码:</label>
<div class="captcha-container">
<img id="captcha-image" src="{{ initial_captcha.image_data }}"
alt="验证码" class="captcha-image" onclick="refreshCaptcha()"
style="cursor: pointer; border: 2px solid #e1e5e9; border-radius: 8px;">
<button type="button" class="refresh-btn" onclick="refreshCaptcha()">🔄 刷新</button>
</div>
<input type="text" id="captcha_input" name="captcha_input"
placeholder="请输入验证码" required
style="width: 100%; padding: 0.8rem; border: 2px solid #e1e5e9; border-radius: 8px; font-size: 1rem; margin-top: 0.5rem;">
<input type="hidden" id="captcha_id" name="captcha_id" value="{{ initial_captcha.captcha_id }}">
</div>
<button type="submit" class="submit-btn">验证提交</button>
</form>
<div style="margin-top: 1rem; padding: 1rem; background: #e7f3ff; border-radius: 8px; border-left: 4px solid #007bff;">
<h4 style="color: #007bff; margin-bottom: 0.5rem;">💡 使用提示</h4>
<p style="color: #004085; margin-bottom: 0.3rem;">• 点击验证码图片可以刷新</p>
<p style="color: #004085; margin-bottom: 0.3rem;">• 验证码不区分大小写</p>
<p style="color: #004085;">• 验证码有效期为5分钟</p>
</div>
</div>
</div>
</div>
<div class="back-link">
<a href="/" style="color: white; text-decoration: none; font-weight: 500; font-size: 1.1rem;">← 返回首页</a>
</div>
</div>
<script>
let currentCaptchaId = '{{ initial_captcha.captcha_id }}';
function refreshCaptcha() {
fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({
action: 'refresh',
captcha_id: currentCaptchaId
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('captcha-image').src = data.data.image_data;
document.getElementById('captcha_id').value = data.data.captcha_id;
document.getElementById('captcha_input').value = '';
currentCaptchaId = data.data.captcha_id;
} else {
console.error('刷新验证码失败:', data.error);
alert('刷新验证码失败,请重试');
}
})
.catch(error => {
console.error('刷新验证码失败:', error);
alert('刷新验证码失败,请重试');
});
}
// 表单提交处理
const captchaForm = document.getElementById('captcha-form');
if (captchaForm) {
captchaForm.addEventListener('submit', function(e) {
e.preventDefault();
const captchaId = document.getElementById('captcha_id').value;
const userInput = document.getElementById('captcha_input').value;
if (!userInput.trim()) {
alert('请输入验证码');
return;
}
fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: JSON.stringify({
action: 'verify',
captcha_id: captchaId,
user_input: userInput
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (data.valid) {
alert('✅ ' + data.message);
document.getElementById('captcha_input').value = '';
refreshCaptcha();
} else {
alert('❌ ' + data.message);
document.getElementById('captcha_input').value = '';
refreshCaptcha();
}
} else {
alert('❌ 验证失败: ' + (data.error || '未知错误'));
refreshCaptcha();
}
})
.catch(error => {
console.error('提交失败:', error);
alert('❌ 提交失败,请重试');
refreshCaptcha();
});
});
}
// 回车键提交
document.getElementById('captcha_input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
captchaForm.dispatchEvent(new Event('submit'));
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,520 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>邮件系统演示 - Hertz Server Django</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.back-link {
margin-bottom: 1rem;
}
.back-link a {
color: white;
text-decoration: none;
font-size: 1.1rem;
opacity: 0.9;
transition: opacity 0.3s ease;
}
.back-link a:hover {
opacity: 1;
text-decoration: underline;
}
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
.demo-card {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.demo-card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0,0,0,0.15);
}
.demo-card h3 {
color: #667eea;
margin-bottom: 1rem;
font-size: 1.5rem;
}
.demo-card p {
color: #666;
margin-bottom: 1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.8rem;
border: 2px solid #e1e5e9;
border-radius: 5px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.form-group textarea {
resize: vertical;
min-height: 120px;
}
.btn {
display: inline-block;
padding: 0.8rem 1.5rem;
border: none;
border-radius: 5px;
font-size: 1rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.3s ease;
margin-right: 0.5rem;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5a6fd8;
transform: translateY(-2px);
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
transform: translateY(-2px);
}
.btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
}
.alert {
padding: 1rem;
border-radius: 5px;
margin-bottom: 1rem;
font-weight: 500;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.email-types {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.type-btn {
padding: 0.5rem 1rem;
border: 2px solid #667eea;
background: white;
color: #667eea;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
}
.type-btn.active,
.type-btn:hover {
background: #667eea;
color: white;
}
.email-preview {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 5px;
padding: 1rem;
margin-top: 1rem;
}
.email-preview h4 {
color: #495057;
margin-bottom: 0.5rem;
}
.email-preview .preview-content {
background: white;
padding: 1rem;
border-radius: 3px;
border-left: 4px solid #667eea;
}
.loading {
display: none;
text-align: center;
padding: 1rem;
}
.loading.show {
display: block;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto 0.5rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.demo-grid {
grid-template-columns: 1fr;
}
.header h1 {
font-size: 2rem;
}
body {
padding: 1rem;
}
.email-types {
justify-content: center;
}
}
</style>
</head>
<body>
<div class="container">
<div class="back-link">
<a href="/">← 返回首页</a>
</div>
<div class="header">
<h1>📧 邮件系统演示</h1>
<p>体验Django邮件发送功能支持多种邮件类型和模板</p>
</div>
<div class="demo-grid">
<div class="demo-card">
<h3>📮 邮件发送功能</h3>
<p>本演示展示了Django邮件系统的核心功能</p>
<ul style="color: #666; margin-left: 1.5rem; margin-bottom: 1rem;">
<li>支持HTML和纯文本邮件</li>
<li>多种邮件模板类型</li>
<li>SMTP配置和发送状态</li>
<li>邮件预览和验证</li>
</ul>
<div class="email-types">
<button class="type-btn active" data-type="welcome">欢迎邮件</button>
<button class="type-btn" data-type="notification">通知邮件</button>
<button class="type-btn" data-type="verification">验证邮件</button>
<button class="type-btn" data-type="custom">自定义邮件</button>
</div>
</div>
<div class="demo-card">
<h3>✉️ 发送邮件测试</h3>
<div id="message-area"></div>
<form id="email-form" method="post">
{% csrf_token %}
<input type="hidden" id="email-type" name="email_type" value="welcome">
<div class="form-group">
<label for="recipient_email">收件人邮箱 *</label>
<input type="email" id="recipient_email" name="recipient_email" required
placeholder="请输入收件人邮箱地址">
</div>
<div class="form-group" id="subject-group">
<label for="subject">邮件主题</label>
<input type="text" id="subject" name="subject"
placeholder="邮件主题将根据类型自动生成">
</div>
<div class="form-group" id="message-group" style="display: none;">
<label for="message">邮件内容</label>
<textarea id="message" name="message"
placeholder="请输入自定义邮件内容"></textarea>
</div>
<div class="form-group">
<label for="recipient_name">收件人姓名</label>
<input type="text" id="recipient_name" name="recipient_name"
placeholder="收件人姓名(可选)">
</div>
<button type="submit" class="btn btn-primary" id="send-btn">
📤 发送邮件
</button>
<button type="button" class="btn btn-success" id="preview-btn">
👁️ 预览邮件
</button>
</form>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>正在发送邮件,请稍候...</p>
</div>
</div>
</div>
<div class="demo-card" id="preview-card" style="display: none;">
<h3>📋 邮件预览</h3>
<div class="email-preview">
<h4>邮件内容预览:</h4>
<div class="preview-content" id="preview-content">
<!-- 预览内容将在这里显示 -->
</div>
</div>
</div>
</div>
<script>
// 邮件类型切换
document.querySelectorAll('.type-btn').forEach(btn => {
btn.addEventListener('click', function() {
// 移除所有active类
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
// 添加active类到当前按钮
this.classList.add('active');
const type = this.dataset.type;
document.getElementById('email-type').value = type;
// 根据类型显示/隐藏字段
const subjectGroup = document.getElementById('subject-group');
const messageGroup = document.getElementById('message-group');
const subjectInput = document.getElementById('subject');
if (type === 'custom') {
messageGroup.style.display = 'block';
subjectInput.placeholder = '请输入邮件主题';
subjectInput.required = true;
} else {
messageGroup.style.display = 'none';
subjectInput.placeholder = '邮件主题将根据类型自动生成';
subjectInput.required = false;
}
// 更新预览
updatePreview();
});
});
// 表单提交
document.getElementById('email-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const sendBtn = document.getElementById('send-btn');
const loading = document.getElementById('loading');
const messageArea = document.getElementById('message-area');
// 显示加载状态
sendBtn.disabled = true;
loading.classList.add('show');
messageArea.innerHTML = '';
fetch('/demo/email/', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
loading.classList.remove('show');
sendBtn.disabled = false;
if (data.success) {
messageArea.innerHTML = `
<div class="alert alert-success">
${data.message}
</div>
`;
} else {
messageArea.innerHTML = `
<div class="alert alert-error">
${data.message}
</div>
`;
}
})
.catch(error => {
loading.classList.remove('show');
sendBtn.disabled = false;
messageArea.innerHTML = `
<div class="alert alert-error">
❌ 发送失败:网络错误
</div>
`;
});
});
// 预览功能
document.getElementById('preview-btn').addEventListener('click', function() {
updatePreview();
const previewCard = document.getElementById('preview-card');
previewCard.style.display = previewCard.style.display === 'none' ? 'block' : 'none';
});
function updatePreview() {
const type = document.getElementById('email-type').value;
const recipientName = document.getElementById('recipient_name').value || '用户';
const subject = document.getElementById('subject').value;
const message = document.getElementById('message').value;
let previewContent = '';
switch(type) {
case 'welcome':
previewContent = `
<h4>🎉 欢迎加入我们!</h4>
<p>亲爱的 ${recipientName}</p>
<p>欢迎您注册成为我们的用户!我们很高兴您能加入我们的大家庭。</p>
<p>在这里,您可以享受到优质的服务和丰富的功能。如果您有任何问题,请随时联系我们。</p>
<p>祝您使用愉快!</p>
<p>此致<br>Hertz Server Django 团队</p>
`;
break;
case 'notification':
previewContent = `
<h4>🔔 系统通知</h4>
<p>亲爱的 ${recipientName}</p>
<p>您有一条新的系统通知:</p>
<div style="background: #f8f9fa; padding: 1rem; border-left: 4px solid #007bff; margin: 1rem 0;">
<p>您的账户设置已更新,如果这不是您的操作,请立即联系我们。</p>
</div>
<p>如有疑问,请联系客服。</p>
<p>此致<br>Hertz Server Django 团队</p>
`;
break;
case 'verification':
previewContent = `
<h4>🔐 邮箱验证</h4>
<p>亲爱的 ${recipientName}</p>
<p>请点击下面的链接验证您的邮箱地址:</p>
<div style="text-align: center; margin: 2rem 0;">
<a href="#" style="background: #667eea; color: white; padding: 1rem 2rem; text-decoration: none; border-radius: 5px;">验证邮箱</a>
</div>
<p>如果您没有注册账户,请忽略此邮件。</p>
<p>此致<br>Hertz Server Django 团队</p>
`;
break;
case 'custom':
previewContent = `
<h4>${subject || '自定义邮件'}</h4>
<p>亲爱的 ${recipientName}</p>
<div style="margin: 1rem 0;">
${message ? message.replace(/\n/g, '<br>') : '请输入邮件内容'}
</div>
<p>此致<br>Hertz Server Django 团队</p>
`;
break;
}
document.getElementById('preview-content').innerHTML = previewContent;
}
// 初始化预览
updatePreview();
</script>
</body>
</html>

View File

@@ -0,0 +1,556 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket演示 - Hertz Server Django</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
.demo-card {
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
overflow: hidden;
}
.demo-header {
background: #667eea;
color: white;
padding: 1rem;
text-align: center;
}
.demo-header h3 {
font-size: 1.3rem;
margin-bottom: 0.5rem;
}
.demo-content {
padding: 1.5rem;
}
.connection-status {
display: inline-block;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
margin-bottom: 1rem;
}
.status-disconnected {
background: #f44336;
color: white;
}
.status-connecting {
background: #ff9800;
color: white;
}
.status-connected {
background: #4CAF50;
color: white;
}
.message-area {
border: 1px solid #ddd;
border-radius: 5px;
height: 300px;
overflow-y: auto;
padding: 1rem;
margin-bottom: 1rem;
background: #f9f9f9;
font-family: monospace;
font-size: 0.9rem;
}
.message {
margin-bottom: 0.5rem;
padding: 0.3rem;
border-radius: 3px;
}
.message-system {
background: #e3f2fd;
color: #1976d2;
}
.message-user {
background: #e8f5e8;
color: #2e7d32;
}
.message-echo {
background: #fff3e0;
color: #f57c00;
}
.message-error {
background: #ffebee;
color: #c62828;
}
.message-notification {
background: #f3e5f5;
color: #7b1fa2;
}
.input-group {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.input-field {
flex: 1;
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
}
.btn {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: background 0.3s ease;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5a6fd8;
}
.btn-success {
background: #4CAF50;
color: white;
}
.btn-success:hover {
background: #45a049;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #da190b;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.chat-controls {
margin-bottom: 1rem;
}
.chat-controls input {
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.back-link {
text-align: center;
margin-top: 2rem;
}
.back-link a {
color: white;
text-decoration: none;
font-weight: 500;
font-size: 1.1rem;
}
.back-link a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.demo-grid {
grid-template-columns: 1fr;
}
.header h1 {
font-size: 2rem;
}
body {
padding: 1rem;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🌐 WebSocket演示</h1>
<p>实时通信功能展示 - 支持聊天室和回声测试</p>
</div>
<div class="demo-grid">
<!-- 回声测试 -->
<div class="demo-card">
<div class="demo-header">
<h3>🔄 回声测试</h3>
<p>发送消息,服务器会回声返回</p>
</div>
<div class="demo-content">
<div class="connection-status status-disconnected" id="echo-status">未连接</div>
<div class="message-area" id="echo-messages"></div>
<div class="input-group">
<input type="text" class="input-field" id="echo-input" placeholder="输入消息..." disabled>
<button class="btn btn-primary" id="echo-send" disabled>发送</button>
</div>
<div class="input-group">
<button class="btn btn-success" id="echo-connect">连接</button>
<button class="btn btn-danger" id="echo-disconnect" disabled>断开</button>
<button class="btn btn-primary" id="echo-clear">清空</button>
</div>
</div>
</div>
<!-- 聊天室 -->
<div class="demo-card">
<div class="demo-header">
<h3>💬 聊天室</h3>
<p>多用户实时聊天功能</p>
</div>
<div class="demo-content">
<div class="connection-status status-disconnected" id="chat-status">未连接</div>
<div class="chat-controls">
<input type="text" class="input-field" id="username" placeholder="用户名" value="用户" style="width: 120px;">
<input type="text" class="input-field" id="room-name" placeholder="房间名" value="general" style="width: 120px;">
<button class="btn btn-success" id="chat-connect">加入聊天室</button>
<button class="btn btn-danger" id="chat-disconnect" disabled>离开聊天室</button>
</div>
<div class="message-area" id="chat-messages"></div>
<div class="input-group">
<input type="text" class="input-field" id="chat-input" placeholder="输入消息..." disabled>
<button class="btn btn-primary" id="chat-send" disabled>发送</button>
<button class="btn btn-primary" id="chat-clear">清空</button>
</div>
</div>
</div>
</div>
<div class="back-link">
<a href="/">← 返回首页</a>
</div>
</div>
<script>
// WebSocket连接管理
let echoSocket = null;
let chatSocket = null;
// DOM元素
const echoStatus = document.getElementById('echo-status');
const echoMessages = document.getElementById('echo-messages');
const echoInput = document.getElementById('echo-input');
const echoSendBtn = document.getElementById('echo-send');
const echoConnectBtn = document.getElementById('echo-connect');
const echoDisconnectBtn = document.getElementById('echo-disconnect');
const echoClearBtn = document.getElementById('echo-clear');
const chatStatus = document.getElementById('chat-status');
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const chatSendBtn = document.getElementById('chat-send');
const chatConnectBtn = document.getElementById('chat-connect');
const chatDisconnectBtn = document.getElementById('chat-disconnect');
const chatClearBtn = document.getElementById('chat-clear');
const usernameInput = document.getElementById('username');
const roomNameInput = document.getElementById('room-name');
// 工具函数
function addMessage(container, message, type = 'system') {
const messageDiv = document.createElement('div');
messageDiv.className = `message message-${type}`;
messageDiv.innerHTML = `<strong>[${message.timestamp}]</strong> ${message.message}`;
container.appendChild(messageDiv);
container.scrollTop = container.scrollHeight;
}
function updateStatus(statusElement, status, text) {
statusElement.className = `connection-status status-${status}`;
statusElement.textContent = text;
}
// 回声测试WebSocket
function connectEcho() {
updateStatus(echoStatus, 'connecting', '连接中...');
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/echo/`;
echoSocket = new WebSocket(wsUrl);
echoSocket.onopen = function(e) {
updateStatus(echoStatus, 'connected', '已连接');
echoInput.disabled = false;
echoSendBtn.disabled = false;
echoConnectBtn.disabled = true;
echoDisconnectBtn.disabled = false;
};
echoSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
console.log('收到WebSocket消息:', data); // 调试信息
let messageType = 'system';
let displayMessage = data;
if (data.type === 'echo_message') {
messageType = 'echo';
// 解码Unicode字符并创建显示用的消息对象
let echoText = data.echo_message;
if (typeof echoText === 'string') {
// 处理可能的Unicode编码
try {
echoText = decodeURIComponent(escape(echoText));
} catch (e) {
// 如果解码失败,使用原始文本
console.log('Unicode解码失败使用原始文本');
}
}
displayMessage = {
message: echoText || data.echo_message || '回声消息',
timestamp: data.timestamp || new Date().toLocaleTimeString()
};
console.log('处理回声消息:', displayMessage); // 调试信息
} else if (data.type === 'error') {
messageType = 'error';
displayMessage = {
message: data.message || '发生错误',
timestamp: data.timestamp || new Date().toLocaleTimeString()
};
} else {
// 处理其他类型的消息
displayMessage = {
message: data.message || JSON.stringify(data),
timestamp: data.timestamp || new Date().toLocaleTimeString()
};
}
addMessage(echoMessages, displayMessage, messageType);
};
echoSocket.onclose = function(e) {
updateStatus(echoStatus, 'disconnected', '已断开');
echoInput.disabled = true;
echoSendBtn.disabled = true;
echoConnectBtn.disabled = false;
echoDisconnectBtn.disabled = true;
};
echoSocket.onerror = function(e) {
updateStatus(echoStatus, 'disconnected', '连接错误');
addMessage(echoMessages, {
message: 'WebSocket连接发生错误',
timestamp: new Date().toLocaleTimeString()
}, 'error');
};
}
function disconnectEcho() {
if (echoSocket) {
echoSocket.close();
}
}
function sendEchoMessage() {
const message = echoInput.value.trim();
if (message && echoSocket && echoSocket.readyState === WebSocket.OPEN) {
echoSocket.send(JSON.stringify({
'message': message
}));
echoInput.value = '';
}
}
// 聊天室WebSocket
function connectChat() {
const username = usernameInput.value.trim() || '匿名用户';
const roomName = roomNameInput.value.trim() || 'general';
updateStatus(chatStatus, 'connecting', '连接中...');
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/chat/${roomName}/`;
chatSocket = new WebSocket(wsUrl);
chatSocket.onopen = function(e) {
updateStatus(chatStatus, 'connected', `已连接到房间: ${roomName}`);
chatInput.disabled = false;
chatSendBtn.disabled = false;
chatConnectBtn.disabled = true;
chatDisconnectBtn.disabled = false;
usernameInput.disabled = true;
roomNameInput.disabled = true;
// 发送加入通知
chatSocket.send(JSON.stringify({
'type': 'user_join',
'username': username
}));
};
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
let messageType = 'system';
if (data.type === 'chat_message') {
messageType = 'user';
data.message = `${data.username}: ${data.message}`;
} else if (data.type === 'user_notification') {
messageType = 'notification';
} else if (data.type === 'error') {
messageType = 'error';
}
addMessage(chatMessages, data, messageType);
};
chatSocket.onclose = function(e) {
updateStatus(chatStatus, 'disconnected', '已断开');
chatInput.disabled = true;
chatSendBtn.disabled = true;
chatConnectBtn.disabled = false;
chatDisconnectBtn.disabled = true;
usernameInput.disabled = false;
roomNameInput.disabled = false;
};
chatSocket.onerror = function(e) {
updateStatus(chatStatus, 'disconnected', '连接错误');
addMessage(chatMessages, {
message: 'WebSocket连接发生错误',
timestamp: new Date().toLocaleTimeString()
}, 'error');
};
}
function disconnectChat() {
if (chatSocket) {
const username = usernameInput.value.trim() || '匿名用户';
// 发送离开通知
chatSocket.send(JSON.stringify({
'type': 'user_leave',
'username': username
}));
setTimeout(() => {
chatSocket.close();
}, 100);
}
}
function sendChatMessage() {
const message = chatInput.value.trim();
const username = usernameInput.value.trim() || '匿名用户';
if (message && chatSocket && chatSocket.readyState === WebSocket.OPEN) {
chatSocket.send(JSON.stringify({
'type': 'chat_message',
'message': message,
'username': username
}));
chatInput.value = '';
}
}
// 事件监听器
echoConnectBtn.addEventListener('click', connectEcho);
echoDisconnectBtn.addEventListener('click', disconnectEcho);
echoSendBtn.addEventListener('click', sendEchoMessage);
echoClearBtn.addEventListener('click', () => {
echoMessages.innerHTML = '';
});
echoInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendEchoMessage();
}
});
chatConnectBtn.addEventListener('click', connectChat);
chatDisconnectBtn.addEventListener('click', disconnectChat);
chatSendBtn.addEventListener('click', sendChatMessage);
chatClearBtn.addEventListener('click', () => {
chatMessages.innerHTML = '';
});
chatInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendChatMessage();
}
});
// 页面卸载时断开连接
window.addEventListener('beforeunload', function() {
if (echoSocket) {
echoSocket.close();
}
if (chatSocket) {
disconnectChat();
}
});
</script>
</body>
</html>

3
hertz_demo/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
hertz_demo/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path
from . import views
app_name = 'hertz_demo'
urlpatterns = [
path('demo/captcha/', views.captcha_demo, name='captcha_demo'),
path('demo/websocket/', views.websocket_demo, name='websocket_demo'),
path('websocket/test/', views.websocket_test, name='websocket_test'),
path('demo/email/', views.email_demo, name='email_demo'),
]

331
hertz_demo/views.py Normal file
View File

@@ -0,0 +1,331 @@
from django.shortcuts import render, redirect
from django.http import JsonResponse, HttpResponse
from django.core.mail import send_mail, EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.contrib import messages
from django import forms
from hertz_studio_django_captcha.captcha_generator import HertzCaptchaGenerator
import json
from django.conf import settings
import random
import string
class HertzCaptchaForm(forms.Form):
"""Hertz验证码表单"""
captcha_input = forms.CharField(
max_length=10,
widget=forms.TextInput(attrs={
'placeholder': '请输入验证码',
'class': 'form-control',
'autocomplete': 'off'
}),
label='验证码'
)
captcha_id = forms.CharField(widget=forms.HiddenInput(), required=False)
def captcha_demo(request):
"""
验证码演示页面
展示多种验证码功能的使用方法
"""
# 获取请求的验证码类型
captcha_type = request.GET.get('type', 'random_char')
# 初始化验证码生成器
captcha_generator = HertzCaptchaGenerator()
if request.method == 'POST':
# 检查是否是Ajax请求
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
try:
data = json.loads(request.body)
action = data.get('action')
if action == 'refresh':
# 刷新验证码
captcha_data = captcha_generator.generate_captcha()
return JsonResponse({
'success': True,
'data': captcha_data
})
elif action == 'verify':
# 验证验证码
captcha_id = data.get('captcha_id', '')
user_input = data.get('user_input', '')
is_valid = captcha_generator.verify_captcha(captcha_id, user_input)
if is_valid:
return JsonResponse({
'success': True,
'valid': True,
'message': f'验证成功!验证码类型: {captcha_type}'
})
else:
return JsonResponse({
'success': True,
'valid': False,
'message': '验证码错误,请重新输入'
})
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'error': '请求数据格式错误'
})
else:
# 普通表单提交处理
form = HertzCaptchaForm(request.POST)
username = request.POST.get('username', '')
captcha_id = request.POST.get('captcha_id', '')
captcha_input = request.POST.get('captcha_input', '')
# 验证验证码
is_valid = captcha_generator.verify_captcha(captcha_id, captcha_input)
if is_valid and username:
# 生成新的验证码用于显示
initial_captcha = captcha_generator.generate_captcha()
return render(request, 'captcha_demo.html', {
'form': HertzCaptchaForm(),
'success_message': f'验证成功!用户名: {username},验证码类型: {captcha_type}',
'captcha_unavailable': False,
'current_type': captcha_type,
'initial_captcha': initial_captcha,
'captcha_types': {
'random_char': '随机字符验证码',
'math': '数学运算验证码',
'word': '单词验证码'
}
})
# GET请求或表单验证失败时生成初始验证码
form = HertzCaptchaForm()
initial_captcha = captcha_generator.generate_captcha()
return render(request, 'captcha_demo.html', {
'form': form,
'captcha_unavailable': False,
'current_type': captcha_type,
'initial_captcha': initial_captcha,
'captcha_types': {
'random_char': '随机字符验证码',
'math': '数学运算验证码',
'word': '单词验证码'
}
})
def websocket_demo(request):
"""WebSocket演示页面"""
return render(request, 'websocket_demo.html')
def websocket_test(request):
"""
WebSocket简单测试页面
"""
return render(request, 'websocket_test.html')
# 测试热重启功能 - 添加注释触发文件变化
def email_demo(request):
"""邮件系统演示页面"""
if request.method == 'GET':
return render(request, 'email_demo.html')
elif request.method == 'POST':
try:
# 获取表单数据
email_type = request.POST.get('email_type', 'welcome')
recipient_email = request.POST.get('recipient_email')
recipient_name = request.POST.get('recipient_name', '用户')
custom_subject = request.POST.get('subject', '')
custom_message = request.POST.get('message', '')
if not recipient_email:
return JsonResponse({
'success': False,
'message': '请输入收件人邮箱地址'
})
# 根据邮件类型生成内容
email_content = generate_email_content(email_type, recipient_name, custom_subject, custom_message)
# 发送邮件
success = send_demo_email(
recipient_email=recipient_email,
subject=email_content['subject'],
html_content=email_content['html_content'],
text_content=email_content['text_content']
)
if success:
return JsonResponse({
'success': True,
'message': f'邮件已成功发送到 {recipient_email}'
})
else:
return JsonResponse({
'success': False,
'message': '邮件发送失败,请检查邮件配置'
})
except Exception as e:
return JsonResponse({
'success': False,
'message': f'发送失败:{str(e)}'
})
def generate_email_content(email_type, recipient_name, custom_subject='', custom_message=''):
"""根据邮件类型生成邮件内容"""
email_templates = {
'welcome': {
'subject': '🎉 欢迎加入 Hertz Server Django',
'html_template': f'''
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="margin: 0; font-size: 28px;">🎉 欢迎加入我们!</h1>
</div>
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong></p>
<p>欢迎您注册成为我们的用户!我们很高兴您能加入我们的大家庭。</p>
<p>在这里,您可以享受到:</p>
<ul style="color: #666;">
<li>🔐 安全的验证码系统</li>
<li>🌐 实时WebSocket通信</li>
<li>📧 完善的邮件服务</li>
<li>📚 详细的API文档</li>
</ul>
<p>如果您有任何问题,请随时联系我们。</p>
<div style="text-align: center; margin: 30px 0;">
<a href="http://127.0.0.1:8000/" style="background: #667eea; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">开始使用</a>
</div>
<p>祝您使用愉快!</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
</div>
</div>
</body>
</html>
'''
},
'notification': {
'subject': '🔔 系统通知 - Hertz Server Django',
'html_template': f'''
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: #007bff; color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="margin: 0; font-size: 24px;">🔔 系统通知</h1>
</div>
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong></p>
<p>您有一条新的系统通知:</p>
<div style="background: #f8f9fa; padding: 20px; border-left: 4px solid #007bff; margin: 20px 0;">
<p style="margin: 0; font-weight: 500;">您的账户设置已更新,如果这不是您的操作,请立即联系我们。</p>
</div>
<p>系统会持续为您提供安全保障,如有疑问请联系客服。</p>
<div style="text-align: center; margin: 30px 0;">
<a href="http://127.0.0.1:8000/" style="background: #007bff; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block;">查看详情</a>
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
</div>
</div>
</body>
</html>
'''
},
'verification': {
'subject': '🔐 邮箱验证 - Hertz Server Django',
'html_template': f'''
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: #28a745; color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="margin: 0; font-size: 24px;">🔐 邮箱验证</h1>
</div>
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong></p>
<p>感谢您注册 Hertz Server Django请点击下面的按钮验证您的邮箱地址</p>
<div style="text-align: center; margin: 40px 0;">
<a href="http://127.0.0.1:8000/verify?token=demo_token" style="background: #28a745; color: white; padding: 15px 40px; text-decoration: none; border-radius: 5px; display: inline-block; font-size: 16px; font-weight: 500;">验证邮箱地址</a>
</div>
<p style="color: #666; font-size: 14px;">如果按钮无法点击,请复制以下链接到浏览器:<br>
<code style="background: #f8f9fa; padding: 5px; border-radius: 3px;">http://127.0.0.1:8000/verify?token=demo_token</code></p>
<p style="color: #666;">如果您没有注册账户请忽略此邮件。此验证链接将在24小时后失效。</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
</div>
</div>
</body>
</html>
'''
},
'custom': {
'subject': custom_subject or '自定义邮件 - Hertz Server Django',
'html_template': f'''
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; border-radius: 10px 10px 0 0;">
<h1 style="margin: 0; font-size: 24px;">{custom_subject or '自定义邮件'}</h1>
</div>
<div style="background: white; padding: 30px; border: 1px solid #e1e5e9; border-radius: 0 0 10px 10px;">
<p style="font-size: 16px;">亲爱的 <strong>{recipient_name}</strong></p>
<div style="margin: 20px 0; font-size: 16px;">
{custom_message.replace(chr(10), '<br>') if custom_message else '这是一封自定义邮件。'}
</div>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #666; font-size: 14px;">此致<br><strong>Hertz Server Django 团队</strong></p>
</div>
</div>
</body>
</html>
'''
}
}
template = email_templates.get(email_type, email_templates['welcome'])
html_content = template['html_template']
text_content = strip_tags(html_content)
return {
'subject': template['subject'],
'html_content': html_content,
'text_content': text_content
}
def send_demo_email(recipient_email, subject, html_content, text_content):
"""发送演示邮件"""
try:
# 检查邮件配置
if not settings.EMAIL_HOST_USER or not settings.EMAIL_HOST_PASSWORD:
print("邮件配置不完整,使用控制台输出模式")
print(f"收件人: {recipient_email}")
print(f"主题: {subject}")
print(f"内容: {text_content[:200]}...")
return True
# 创建邮件
email = EmailMultiAlternatives(
subject=subject,
body=text_content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[recipient_email]
)
# 添加HTML内容
email.attach_alternative(html_content, "text/html")
# 发送邮件
email.send()
return True
except Exception as e:
print(f"邮件发送失败: {str(e)}")
return False

View File

@@ -0,0 +1,25 @@
# EditorConfig配置文件
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[*.{js,ts,vue}]
indent_size = 2
[*.json]
indent_size = 2
[*.{css,scss,sass}]
indent_size = 2

24
hertz_server_diango_ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,109 @@
{
"hash": "300f9ef5",
"configHash": "f25ca102",
"lockfileHash": "b99b8a02",
"browserHash": "68dc98de",
"optimized": {
"@ant-design/icons-vue": {
"src": "../../node_modules/@ant-design/icons-vue/es/index.js",
"file": "@ant-design_icons-vue.js",
"fileHash": "9f30893d",
"needsInterop": false
},
"ant-design-vue": {
"src": "../../node_modules/ant-design-vue/es/index.js",
"file": "ant-design-vue.js",
"fileHash": "19e51532",
"needsInterop": false
},
"ant-design-vue/es/locale/en_US": {
"src": "../../node_modules/ant-design-vue/es/locale/en_US.js",
"file": "ant-design-vue_es_locale_en_US.js",
"fileHash": "2bb8ba2e",
"needsInterop": false
},
"ant-design-vue/es/locale/zh_CN": {
"src": "../../node_modules/ant-design-vue/es/locale/zh_CN.js",
"file": "ant-design-vue_es_locale_zh_CN.js",
"fileHash": "a20449b1",
"needsInterop": false
},
"axios": {
"src": "../../node_modules/axios/index.js",
"file": "axios.js",
"fileHash": "4d6a6f42",
"needsInterop": false
},
"dayjs": {
"src": "../../node_modules/dayjs/dayjs.min.js",
"file": "dayjs.js",
"fileHash": "ab9a8365",
"needsInterop": true
},
"jszip": {
"src": "../../node_modules/jszip/dist/jszip.min.js",
"file": "jszip.js",
"fileHash": "8a18241e",
"needsInterop": true
},
"pinia": {
"src": "../../node_modules/pinia/dist/pinia.mjs",
"file": "pinia.js",
"fileHash": "bbe3ba96",
"needsInterop": false
},
"vue": {
"src": "../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "a721dff8",
"needsInterop": false
},
"vue-i18n": {
"src": "../../node_modules/vue-i18n/dist/vue-i18n.mjs",
"file": "vue-i18n.js",
"fileHash": "a5b2fc34",
"needsInterop": false
},
"vue-router": {
"src": "../../node_modules/vue-router/dist/vue-router.mjs",
"file": "vue-router.js",
"fileHash": "62efce1c",
"needsInterop": false
},
"ant-design-vue/es": {
"src": "../../node_modules/ant-design-vue/es/index.js",
"file": "ant-design-vue_es.js",
"fileHash": "726b9d57",
"needsInterop": false
}
},
"chunks": {
"chunk-AYVSL3LM": {
"file": "chunk-AYVSL3LM.js"
},
"chunk-UD4KTM7R": {
"file": "chunk-UD4KTM7R.js"
},
"chunk-ZC474HKL": {
"file": "chunk-ZC474HKL.js"
},
"chunk-F5SAIAJ6": {
"file": "chunk-F5SAIAJ6.js"
},
"chunk-YODRZMZT": {
"file": "chunk-YODRZMZT.js"
},
"chunk-IJSSJBZ4": {
"file": "chunk-IJSSJBZ4.js"
},
"chunk-XCUFKJYR": {
"file": "chunk-XCUFKJYR.js"
},
"chunk-Y7TKRIWE": {
"file": "chunk-Y7TKRIWE.js"
},
"chunk-PR4QN5HX": {
"file": "chunk-PR4QN5HX.js"
}
}
}

View File

@@ -0,0 +1,275 @@
import {
AnchorLink_default,
AutoCompleteOptGroup,
AutoCompleteOption,
Avatar_default,
BreadcrumbItem_default,
BreadcrumbSeparator_default,
Button_default,
CheckableTag_default,
CollapsePanel_default,
ColumnGroup_default,
Column_default,
DescriptionsItem,
DirectoryTree_default,
Divider_default,
FormItemContext_default,
FormItem_default,
Grid_default,
Group_default,
Group_default2,
Group_default3,
Group_default4,
Image_default,
Input_default,
ItemGroup_default,
ItemMeta_default,
Item_default,
LayoutContent,
LayoutFooter,
LayoutHeader,
LayoutSider,
Link_default,
MentionsOption,
MenuItem_default,
Meta_default,
MonthPicker,
Paragraph_default,
Password_default,
PreviewGroup_default,
QuarterPicker,
RadioButton_default,
RangePicker,
Ribbon_default,
Search_default,
SelectOptGroup,
SelectOption,
StatisticCountdown,
Step,
SubMenu_default,
TabPane_default,
TableSummary,
TableSummaryCell,
TableSummaryRow,
TextArea_default,
Text_default,
TimeRangePicker,
TimelineItem_default,
Title_default,
Title_default2,
TreeNode,
TreeSelectNode,
UploadDragger,
WeekPicker,
affix_default,
alert_default,
anchor_default,
auto_complete_default,
avatar_default,
back_top_default,
badge_default,
breadcrumb_default,
button_default,
button_group_default,
calendar_default,
card_default,
carousel_default,
cascader_default,
checkbox_default,
col_default,
collapse_default,
comment_default,
config_provider_default,
date_picker_default,
descriptions_default,
divider_default,
drawer_default,
dropdown_button_default,
dropdown_default,
empty_default,
es_default,
form_default,
grid_default,
image_default,
input_default,
input_number_default,
install,
layout_default,
list_default,
locale_provider_default,
mentions_default,
menu_default,
message_default,
modal_default,
notification_default,
page_header_default,
pagination_default,
popconfirm_default,
popover_default,
progress_default,
radio_default,
rate_default,
result_default,
row_default,
select_default,
skeleton_default,
slider_default,
space_default,
spin_default,
statistic_default,
steps_default,
switch_default,
table_default,
tabs_default,
tag_default,
time_picker_default,
timeline_default,
tooltip_default,
transfer_default,
tree_default,
tree_select_default,
typography_default,
upload_default,
version_default
} from "./chunk-UD4KTM7R.js";
import "./chunk-ZC474HKL.js";
import "./chunk-F5SAIAJ6.js";
import "./chunk-YODRZMZT.js";
import "./chunk-IJSSJBZ4.js";
import "./chunk-XCUFKJYR.js";
import "./chunk-Y7TKRIWE.js";
import "./chunk-PR4QN5HX.js";
export {
affix_default as Affix,
alert_default as Alert,
anchor_default as Anchor,
AnchorLink_default as AnchorLink,
auto_complete_default as AutoComplete,
AutoCompleteOptGroup,
AutoCompleteOption,
avatar_default as Avatar,
Group_default as AvatarGroup,
back_top_default as BackTop,
badge_default as Badge,
Ribbon_default as BadgeRibbon,
breadcrumb_default as Breadcrumb,
BreadcrumbItem_default as BreadcrumbItem,
BreadcrumbSeparator_default as BreadcrumbSeparator,
button_default as Button,
button_group_default as ButtonGroup,
calendar_default as Calendar,
card_default as Card,
Grid_default as CardGrid,
Meta_default as CardMeta,
carousel_default as Carousel,
cascader_default as Cascader,
CheckableTag_default as CheckableTag,
checkbox_default as Checkbox,
Group_default3 as CheckboxGroup,
col_default as Col,
collapse_default as Collapse,
CollapsePanel_default as CollapsePanel,
comment_default as Comment,
config_provider_default as ConfigProvider,
date_picker_default as DatePicker,
descriptions_default as Descriptions,
DescriptionsItem,
DirectoryTree_default as DirectoryTree,
divider_default as Divider,
drawer_default as Drawer,
dropdown_default as Dropdown,
dropdown_button_default as DropdownButton,
empty_default as Empty,
form_default as Form,
FormItem_default as FormItem,
FormItemContext_default as FormItemRest,
grid_default as Grid,
image_default as Image,
PreviewGroup_default as ImagePreviewGroup,
input_default as Input,
Group_default4 as InputGroup,
input_number_default as InputNumber,
Password_default as InputPassword,
Search_default as InputSearch,
layout_default as Layout,
LayoutContent,
LayoutFooter,
LayoutHeader,
LayoutSider,
list_default as List,
Item_default as ListItem,
ItemMeta_default as ListItemMeta,
locale_provider_default as LocaleProvider,
mentions_default as Mentions,
MentionsOption,
menu_default as Menu,
Divider_default as MenuDivider,
MenuItem_default as MenuItem,
ItemGroup_default as MenuItemGroup,
modal_default as Modal,
MonthPicker,
page_header_default as PageHeader,
pagination_default as Pagination,
popconfirm_default as Popconfirm,
popover_default as Popover,
progress_default as Progress,
QuarterPicker,
radio_default as Radio,
RadioButton_default as RadioButton,
Group_default2 as RadioGroup,
RangePicker,
rate_default as Rate,
result_default as Result,
row_default as Row,
select_default as Select,
SelectOptGroup,
SelectOption,
skeleton_default as Skeleton,
Avatar_default as SkeletonAvatar,
Button_default as SkeletonButton,
Image_default as SkeletonImage,
Input_default as SkeletonInput,
Title_default as SkeletonTitle,
slider_default as Slider,
space_default as Space,
spin_default as Spin,
statistic_default as Statistic,
StatisticCountdown,
Step,
steps_default as Steps,
SubMenu_default as SubMenu,
switch_default as Switch,
TabPane_default as TabPane,
table_default as Table,
Column_default as TableColumn,
ColumnGroup_default as TableColumnGroup,
TableSummary,
TableSummaryCell,
TableSummaryRow,
tabs_default as Tabs,
tag_default as Tag,
TextArea_default as Textarea,
time_picker_default as TimePicker,
TimeRangePicker,
timeline_default as Timeline,
TimelineItem_default as TimelineItem,
tooltip_default as Tooltip,
transfer_default as Transfer,
tree_default as Tree,
TreeNode,
tree_select_default as TreeSelect,
TreeSelectNode,
typography_default as Typography,
Link_default as TypographyLink,
Paragraph_default as TypographyParagraph,
Text_default as TypographyText,
Title_default2 as TypographyTitle,
upload_default as Upload,
UploadDragger,
WeekPicker,
es_default as default,
install,
message_default as message,
notification_default as notification,
version_default as version
};

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -0,0 +1,275 @@
import {
AnchorLink_default,
AutoCompleteOptGroup,
AutoCompleteOption,
Avatar_default,
BreadcrumbItem_default,
BreadcrumbSeparator_default,
Button_default,
CheckableTag_default,
CollapsePanel_default,
ColumnGroup_default,
Column_default,
DescriptionsItem,
DirectoryTree_default,
Divider_default,
FormItemContext_default,
FormItem_default,
Grid_default,
Group_default,
Group_default2,
Group_default3,
Group_default4,
Image_default,
Input_default,
ItemGroup_default,
ItemMeta_default,
Item_default,
LayoutContent,
LayoutFooter,
LayoutHeader,
LayoutSider,
Link_default,
MentionsOption,
MenuItem_default,
Meta_default,
MonthPicker,
Paragraph_default,
Password_default,
PreviewGroup_default,
QuarterPicker,
RadioButton_default,
RangePicker,
Ribbon_default,
Search_default,
SelectOptGroup,
SelectOption,
StatisticCountdown,
Step,
SubMenu_default,
TabPane_default,
TableSummary,
TableSummaryCell,
TableSummaryRow,
TextArea_default,
Text_default,
TimeRangePicker,
TimelineItem_default,
Title_default,
Title_default2,
TreeNode,
TreeSelectNode,
UploadDragger,
WeekPicker,
affix_default,
alert_default,
anchor_default,
auto_complete_default,
avatar_default,
back_top_default,
badge_default,
breadcrumb_default,
button_default,
button_group_default,
calendar_default,
card_default,
carousel_default,
cascader_default,
checkbox_default,
col_default,
collapse_default,
comment_default,
config_provider_default,
date_picker_default,
descriptions_default,
divider_default,
drawer_default,
dropdown_button_default,
dropdown_default,
empty_default,
es_default,
form_default,
grid_default,
image_default,
input_default,
input_number_default,
install,
layout_default,
list_default,
locale_provider_default,
mentions_default,
menu_default,
message_default,
modal_default,
notification_default,
page_header_default,
pagination_default,
popconfirm_default,
popover_default,
progress_default,
radio_default,
rate_default,
result_default,
row_default,
select_default,
skeleton_default,
slider_default,
space_default,
spin_default,
statistic_default,
steps_default,
switch_default,
table_default,
tabs_default,
tag_default,
time_picker_default,
timeline_default,
tooltip_default,
transfer_default,
tree_default,
tree_select_default,
typography_default,
upload_default,
version_default
} from "./chunk-UD4KTM7R.js";
import "./chunk-ZC474HKL.js";
import "./chunk-F5SAIAJ6.js";
import "./chunk-YODRZMZT.js";
import "./chunk-IJSSJBZ4.js";
import "./chunk-XCUFKJYR.js";
import "./chunk-Y7TKRIWE.js";
import "./chunk-PR4QN5HX.js";
export {
affix_default as Affix,
alert_default as Alert,
anchor_default as Anchor,
AnchorLink_default as AnchorLink,
auto_complete_default as AutoComplete,
AutoCompleteOptGroup,
AutoCompleteOption,
avatar_default as Avatar,
Group_default as AvatarGroup,
back_top_default as BackTop,
badge_default as Badge,
Ribbon_default as BadgeRibbon,
breadcrumb_default as Breadcrumb,
BreadcrumbItem_default as BreadcrumbItem,
BreadcrumbSeparator_default as BreadcrumbSeparator,
button_default as Button,
button_group_default as ButtonGroup,
calendar_default as Calendar,
card_default as Card,
Grid_default as CardGrid,
Meta_default as CardMeta,
carousel_default as Carousel,
cascader_default as Cascader,
CheckableTag_default as CheckableTag,
checkbox_default as Checkbox,
Group_default3 as CheckboxGroup,
col_default as Col,
collapse_default as Collapse,
CollapsePanel_default as CollapsePanel,
comment_default as Comment,
config_provider_default as ConfigProvider,
date_picker_default as DatePicker,
descriptions_default as Descriptions,
DescriptionsItem,
DirectoryTree_default as DirectoryTree,
divider_default as Divider,
drawer_default as Drawer,
dropdown_default as Dropdown,
dropdown_button_default as DropdownButton,
empty_default as Empty,
form_default as Form,
FormItem_default as FormItem,
FormItemContext_default as FormItemRest,
grid_default as Grid,
image_default as Image,
PreviewGroup_default as ImagePreviewGroup,
input_default as Input,
Group_default4 as InputGroup,
input_number_default as InputNumber,
Password_default as InputPassword,
Search_default as InputSearch,
layout_default as Layout,
LayoutContent,
LayoutFooter,
LayoutHeader,
LayoutSider,
list_default as List,
Item_default as ListItem,
ItemMeta_default as ListItemMeta,
locale_provider_default as LocaleProvider,
mentions_default as Mentions,
MentionsOption,
menu_default as Menu,
Divider_default as MenuDivider,
MenuItem_default as MenuItem,
ItemGroup_default as MenuItemGroup,
modal_default as Modal,
MonthPicker,
page_header_default as PageHeader,
pagination_default as Pagination,
popconfirm_default as Popconfirm,
popover_default as Popover,
progress_default as Progress,
QuarterPicker,
radio_default as Radio,
RadioButton_default as RadioButton,
Group_default2 as RadioGroup,
RangePicker,
rate_default as Rate,
result_default as Result,
row_default as Row,
select_default as Select,
SelectOptGroup,
SelectOption,
skeleton_default as Skeleton,
Avatar_default as SkeletonAvatar,
Button_default as SkeletonButton,
Image_default as SkeletonImage,
Input_default as SkeletonInput,
Title_default as SkeletonTitle,
slider_default as Slider,
space_default as Space,
spin_default as Spin,
statistic_default as Statistic,
StatisticCountdown,
Step,
steps_default as Steps,
SubMenu_default as SubMenu,
switch_default as Switch,
TabPane_default as TabPane,
table_default as Table,
Column_default as TableColumn,
ColumnGroup_default as TableColumnGroup,
TableSummary,
TableSummaryCell,
TableSummaryRow,
tabs_default as Tabs,
tag_default as Tag,
TextArea_default as Textarea,
time_picker_default as TimePicker,
TimeRangePicker,
timeline_default as Timeline,
TimelineItem_default as TimelineItem,
tooltip_default as Tooltip,
transfer_default as Transfer,
tree_default as Tree,
TreeNode,
tree_select_default as TreeSelect,
TreeSelectNode,
typography_default as Typography,
Link_default as TypographyLink,
Paragraph_default as TypographyParagraph,
Text_default as TypographyText,
Title_default2 as TypographyTitle,
upload_default as Upload,
UploadDragger,
WeekPicker,
es_default as default,
install,
message_default as message,
notification_default as notification,
version_default as version
};

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -0,0 +1,8 @@
import {
en_US_default4 as en_US_default
} from "./chunk-F5SAIAJ6.js";
import "./chunk-IJSSJBZ4.js";
import "./chunk-PR4QN5HX.js";
export {
en_US_default as default
};

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -0,0 +1,199 @@
import {
zh_CN_default
} from "./chunk-YODRZMZT.js";
import {
_objectSpread2
} from "./chunk-IJSSJBZ4.js";
import "./chunk-PR4QN5HX.js";
// node_modules/ant-design-vue/es/vc-picker/locale/zh_CN.js
var locale = {
locale: "zh_CN",
today: "今天",
now: "此刻",
backToToday: "返回今天",
ok: "确定",
timeSelect: "选择时间",
dateSelect: "选择日期",
weekSelect: "选择周",
clear: "清除",
month: "月",
year: "年",
previousMonth: "上个月 (翻页上键)",
nextMonth: "下个月 (翻页下键)",
monthSelect: "选择月份",
yearSelect: "选择年份",
decadeSelect: "选择年代",
yearFormat: "YYYY年",
dayFormat: "D日",
dateFormat: "YYYY年M月D日",
dateTimeFormat: "YYYY年M月D日 HH时mm分ss秒",
previousYear: "上一年 (Control键加左方向键)",
nextYear: "下一年 (Control键加右方向键)",
previousDecade: "上一年代",
nextDecade: "下一年代",
previousCentury: "上一世纪",
nextCentury: "下一世纪"
};
var zh_CN_default2 = locale;
// node_modules/ant-design-vue/es/time-picker/locale/zh_CN.js
var locale2 = {
placeholder: "请选择时间",
rangePlaceholder: ["开始时间", "结束时间"]
};
var zh_CN_default3 = locale2;
// node_modules/ant-design-vue/es/date-picker/locale/zh_CN.js
var locale3 = {
lang: _objectSpread2({
placeholder: "请选择日期",
yearPlaceholder: "请选择年份",
quarterPlaceholder: "请选择季度",
monthPlaceholder: "请选择月份",
weekPlaceholder: "请选择周",
rangePlaceholder: ["开始日期", "结束日期"],
rangeYearPlaceholder: ["开始年份", "结束年份"],
rangeMonthPlaceholder: ["开始月份", "结束月份"],
rangeQuarterPlaceholder: ["开始季度", "结束季度"],
rangeWeekPlaceholder: ["开始周", "结束周"]
}, zh_CN_default2),
timePickerLocale: _objectSpread2({}, zh_CN_default3)
};
locale3.lang.ok = "确定";
var zh_CN_default4 = locale3;
// node_modules/ant-design-vue/es/calendar/locale/zh_CN.js
var zh_CN_default5 = zh_CN_default4;
// node_modules/ant-design-vue/es/locale/zh_CN.js
var typeTemplate = "${label}不是一个有效的${type}";
var localeValues = {
locale: "zh-cn",
Pagination: zh_CN_default,
DatePicker: zh_CN_default4,
TimePicker: zh_CN_default3,
Calendar: zh_CN_default5,
// locales for all components
global: {
placeholder: "请选择"
},
Table: {
filterTitle: "筛选",
filterConfirm: "确定",
filterReset: "重置",
filterEmptyText: "无筛选项",
filterCheckall: "全选",
filterSearchPlaceholder: "在筛选项中搜索",
selectAll: "全选当页",
selectInvert: "反选当页",
selectNone: "清空所有",
selectionAll: "全选所有",
sortTitle: "排序",
expand: "展开行",
collapse: "关闭行",
triggerDesc: "点击降序",
triggerAsc: "点击升序",
cancelSort: "取消排序"
},
Modal: {
okText: "确定",
cancelText: "取消",
justOkText: "知道了"
},
Popconfirm: {
cancelText: "取消",
okText: "确定"
},
Transfer: {
searchPlaceholder: "请输入搜索内容",
itemUnit: "项",
itemsUnit: "项",
remove: "删除",
selectCurrent: "全选当页",
removeCurrent: "删除当页",
selectAll: "全选所有",
removeAll: "删除全部",
selectInvert: "反选当页"
},
Upload: {
uploading: "文件上传中",
removeFile: "删除文件",
uploadError: "上传错误",
previewFile: "预览文件",
downloadFile: "下载文件"
},
Empty: {
description: "暂无数据"
},
Icon: {
icon: "图标"
},
Text: {
edit: "编辑",
copy: "复制",
copied: "复制成功",
expand: "展开"
},
PageHeader: {
back: "返回"
},
Form: {
optional: "(可选)",
defaultValidateMessages: {
default: "字段验证错误${label}",
required: "请输入${label}",
enum: "${label}必须是其中一个[${enum}]",
whitespace: "${label}不能为空字符",
date: {
format: "${label}日期格式无效",
parse: "${label}不能转换为日期",
invalid: "${label}是一个无效日期"
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
url: typeTemplate,
hex: typeTemplate
},
string: {
len: "${label}须为${len}个字符",
min: "${label}最少${min}个字符",
max: "${label}最多${max}个字符",
range: "${label}须在${min}-${max}字符之间"
},
number: {
len: "${label}必须等于${len}",
min: "${label}最小值为${min}",
max: "${label}最大值为${max}",
range: "${label}须在${min}-${max}之间"
},
array: {
len: "须为${len}个${label}",
min: "最少${min}个${label}",
max: "最多${max}个${label}",
range: "${label}数量须在${min}-${max}之间"
},
pattern: {
mismatch: "${label}与模式不匹配${pattern}"
}
}
},
Image: {
preview: "预览"
}
};
var zh_CN_default6 = localeValues;
export {
zh_CN_default6 as default
};
//# sourceMappingURL=ant-design-vue_es_locale_zh_CN.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,162 @@
// node_modules/@vue/devtools-api/lib/esm/env.js
function getDevtoolsGlobalHook() {
return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__;
}
function getTarget() {
return typeof navigator !== "undefined" && typeof window !== "undefined" ? window : typeof globalThis !== "undefined" ? globalThis : {};
}
var isProxyAvailable = typeof Proxy === "function";
// node_modules/@vue/devtools-api/lib/esm/const.js
var HOOK_SETUP = "devtools-plugin:setup";
var HOOK_PLUGIN_SETTINGS_SET = "plugin:settings:set";
// node_modules/@vue/devtools-api/lib/esm/time.js
var supported;
var perf;
function isPerformanceSupported() {
var _a;
if (supported !== void 0) {
return supported;
}
if (typeof window !== "undefined" && window.performance) {
supported = true;
perf = window.performance;
} else if (typeof globalThis !== "undefined" && ((_a = globalThis.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) {
supported = true;
perf = globalThis.perf_hooks.performance;
} else {
supported = false;
}
return supported;
}
function now() {
return isPerformanceSupported() ? perf.now() : Date.now();
}
// node_modules/@vue/devtools-api/lib/esm/proxy.js
var ApiProxy = class {
constructor(plugin, hook) {
this.target = null;
this.targetQueue = [];
this.onQueue = [];
this.plugin = plugin;
this.hook = hook;
const defaultSettings = {};
if (plugin.settings) {
for (const id in plugin.settings) {
const item = plugin.settings[id];
defaultSettings[id] = item.defaultValue;
}
}
const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`;
let currentSettings = Object.assign({}, defaultSettings);
try {
const raw = localStorage.getItem(localSettingsSaveId);
const data = JSON.parse(raw);
Object.assign(currentSettings, data);
} catch (e) {
}
this.fallbacks = {
getSettings() {
return currentSettings;
},
setSettings(value) {
try {
localStorage.setItem(localSettingsSaveId, JSON.stringify(value));
} catch (e) {
}
currentSettings = value;
},
now() {
return now();
}
};
if (hook) {
hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => {
if (pluginId === this.plugin.id) {
this.fallbacks.setSettings(value);
}
});
}
this.proxiedOn = new Proxy({}, {
get: (_target, prop) => {
if (this.target) {
return this.target.on[prop];
} else {
return (...args) => {
this.onQueue.push({
method: prop,
args
});
};
}
}
});
this.proxiedTarget = new Proxy({}, {
get: (_target, prop) => {
if (this.target) {
return this.target[prop];
} else if (prop === "on") {
return this.proxiedOn;
} else if (Object.keys(this.fallbacks).includes(prop)) {
return (...args) => {
this.targetQueue.push({
method: prop,
args,
resolve: () => {
}
});
return this.fallbacks[prop](...args);
};
} else {
return (...args) => {
return new Promise((resolve) => {
this.targetQueue.push({
method: prop,
args,
resolve
});
});
};
}
}
});
}
async setRealTarget(target) {
this.target = target;
for (const item of this.onQueue) {
this.target.on[item.method](...item.args);
}
for (const item of this.targetQueue) {
item.resolve(await this.target[item.method](...item.args));
}
}
};
// node_modules/@vue/devtools-api/lib/esm/index.js
function setupDevtoolsPlugin(pluginDescriptor, setupFn) {
const descriptor = pluginDescriptor;
const target = getTarget();
const hook = getDevtoolsGlobalHook();
const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy;
if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) {
hook.emit(HOOK_SETUP, pluginDescriptor, setupFn);
} else {
const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null;
const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || [];
list.push({
pluginDescriptor: descriptor,
setupFn,
proxy
});
if (proxy) {
setupFn(proxy.proxiedTarget);
}
}
}
export {
setupDevtoolsPlugin
};
//# sourceMappingURL=chunk-AYVSL3LM.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,220 @@
import {
_objectSpread2
} from "./chunk-IJSSJBZ4.js";
// node_modules/ant-design-vue/es/vc-pagination/locale/en_US.js
var en_US_default = {
// Options.jsx
items_per_page: "/ page",
jump_to: "Go to",
jump_to_confirm: "confirm",
page: "",
// Pagination.jsx
prev_page: "Previous Page",
next_page: "Next Page",
prev_5: "Previous 5 Pages",
next_5: "Next 5 Pages",
prev_3: "Previous 3 Pages",
next_3: "Next 3 Pages"
};
// node_modules/ant-design-vue/es/vc-picker/locale/en_US.js
var locale = {
locale: "en_US",
today: "Today",
now: "Now",
backToToday: "Back to today",
ok: "Ok",
clear: "Clear",
month: "Month",
year: "Year",
timeSelect: "select time",
dateSelect: "select date",
weekSelect: "Choose a week",
monthSelect: "Choose a month",
yearSelect: "Choose a year",
decadeSelect: "Choose a decade",
yearFormat: "YYYY",
dateFormat: "M/D/YYYY",
dayFormat: "D",
dateTimeFormat: "M/D/YYYY HH:mm:ss",
monthBeforeYear: true,
previousMonth: "Previous month (PageUp)",
nextMonth: "Next month (PageDown)",
previousYear: "Last year (Control + left)",
nextYear: "Next year (Control + right)",
previousDecade: "Last decade",
nextDecade: "Next decade",
previousCentury: "Last century",
nextCentury: "Next century"
};
var en_US_default2 = locale;
// node_modules/ant-design-vue/es/time-picker/locale/en_US.js
var locale2 = {
placeholder: "Select time",
rangePlaceholder: ["Start time", "End time"]
};
var en_US_default3 = locale2;
// node_modules/ant-design-vue/es/date-picker/locale/en_US.js
var locale3 = {
lang: _objectSpread2({
placeholder: "Select date",
yearPlaceholder: "Select year",
quarterPlaceholder: "Select quarter",
monthPlaceholder: "Select month",
weekPlaceholder: "Select week",
rangePlaceholder: ["Start date", "End date"],
rangeYearPlaceholder: ["Start year", "End year"],
rangeQuarterPlaceholder: ["Start quarter", "End quarter"],
rangeMonthPlaceholder: ["Start month", "End month"],
rangeWeekPlaceholder: ["Start week", "End week"]
}, en_US_default2),
timePickerLocale: _objectSpread2({}, en_US_default3)
};
var en_US_default4 = locale3;
// node_modules/ant-design-vue/es/calendar/locale/en_US.js
var en_US_default5 = en_US_default4;
// node_modules/ant-design-vue/es/locale/default.js
var typeTemplate = "${label} is not a valid ${type}";
var localeValues = {
locale: "en",
Pagination: en_US_default,
DatePicker: en_US_default4,
TimePicker: en_US_default3,
Calendar: en_US_default5,
global: {
placeholder: "Please select"
},
Table: {
filterTitle: "Filter menu",
filterConfirm: "OK",
filterReset: "Reset",
filterEmptyText: "No filters",
filterCheckall: "Select all items",
filterSearchPlaceholder: "Search in filters",
emptyText: "No data",
selectAll: "Select current page",
selectInvert: "Invert current page",
selectNone: "Clear all data",
selectionAll: "Select all data",
sortTitle: "Sort",
expand: "Expand row",
collapse: "Collapse row",
triggerDesc: "Click to sort descending",
triggerAsc: "Click to sort ascending",
cancelSort: "Click to cancel sorting"
},
Modal: {
okText: "OK",
cancelText: "Cancel",
justOkText: "OK"
},
Popconfirm: {
okText: "OK",
cancelText: "Cancel"
},
Transfer: {
titles: ["", ""],
searchPlaceholder: "Search here",
itemUnit: "item",
itemsUnit: "items",
remove: "Remove",
selectCurrent: "Select current page",
removeCurrent: "Remove current page",
selectAll: "Select all data",
removeAll: "Remove all data",
selectInvert: "Invert current page"
},
Upload: {
uploading: "Uploading...",
removeFile: "Remove file",
uploadError: "Upload error",
previewFile: "Preview file",
downloadFile: "Download file"
},
Empty: {
description: "No Data"
},
Icon: {
icon: "icon"
},
Text: {
edit: "Edit",
copy: "Copy",
copied: "Copied",
expand: "Expand"
},
PageHeader: {
back: "Back"
},
Form: {
optional: "(optional)",
defaultValidateMessages: {
default: "Field validation error for ${label}",
required: "Please enter ${label}",
enum: "${label} must be one of [${enum}]",
whitespace: "${label} cannot be a blank character",
date: {
format: "${label} date format is invalid",
parse: "${label} cannot be converted to a date",
invalid: "${label} is an invalid date"
},
types: {
string: typeTemplate,
method: typeTemplate,
array: typeTemplate,
object: typeTemplate,
number: typeTemplate,
date: typeTemplate,
boolean: typeTemplate,
integer: typeTemplate,
float: typeTemplate,
regexp: typeTemplate,
email: typeTemplate,
url: typeTemplate,
hex: typeTemplate
},
string: {
len: "${label} must be ${len} characters",
min: "${label} must be at least ${min} characters",
max: "${label} must be up to ${max} characters",
range: "${label} must be between ${min}-${max} characters"
},
number: {
len: "${label} must be equal to ${len}",
min: "${label} must be minimum ${min}",
max: "${label} must be maximum ${max}",
range: "${label} must be between ${min}-${max}"
},
array: {
len: "Must be ${len} ${label}",
min: "At least ${min} ${label}",
max: "At most ${max} ${label}",
range: "The amount of ${label} must be between ${min}-${max}"
},
pattern: {
mismatch: "${label} does not match the pattern ${pattern}"
}
}
},
Image: {
preview: "Preview"
}
};
var default_default = localeValues;
// node_modules/ant-design-vue/es/locale/en_US.js
var en_US_default6 = default_default;
export {
en_US_default,
en_US_default4 as en_US_default2,
en_US_default5 as en_US_default3,
default_default,
en_US_default6 as en_US_default4
};
//# sourceMappingURL=chunk-F5SAIAJ6.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,68 @@
// node_modules/@babel/runtime/helpers/esm/typeof.js
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o2) {
return typeof o2;
} : function(o2) {
return o2 && "function" == typeof Symbol && o2.constructor === Symbol && o2 !== Symbol.prototype ? "symbol" : typeof o2;
}, _typeof(o);
}
// node_modules/@babel/runtime/helpers/esm/toPrimitive.js
function toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
// node_modules/@babel/runtime/helpers/esm/toPropertyKey.js
function toPropertyKey(t) {
var i = toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : i + "";
}
// node_modules/@babel/runtime/helpers/esm/defineProperty.js
function _defineProperty(e, r, t) {
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: true,
configurable: true,
writable: true
}) : e[r] = t, e;
}
// node_modules/@babel/runtime/helpers/esm/objectSpread2.js
function ownKeys(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r && (o = o.filter(function(r2) {
return Object.getOwnPropertyDescriptor(e, r2).enumerable;
})), t.push.apply(t, o);
}
return t;
}
function _objectSpread2(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2 ? ownKeys(Object(t), true).forEach(function(r2) {
_defineProperty(e, r2, t[r2]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function(r2) {
Object.defineProperty(e, r2, Object.getOwnPropertyDescriptor(t, r2));
});
}
return e;
}
export {
_typeof,
toPropertyKey,
_defineProperty,
_objectSpread2
};
//# sourceMappingURL=chunk-IJSSJBZ4.js.map

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../node_modules/@babel/runtime/helpers/esm/typeof.js", "../../node_modules/@babel/runtime/helpers/esm/toPrimitive.js", "../../node_modules/@babel/runtime/helpers/esm/toPropertyKey.js", "../../node_modules/@babel/runtime/helpers/esm/defineProperty.js", "../../node_modules/@babel/runtime/helpers/esm/objectSpread2.js"],
"sourcesContent": ["function _typeof(o) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (o) {\n return typeof o;\n } : function (o) {\n return o && \"function\" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? \"symbol\" : typeof o;\n }, _typeof(o);\n}\nexport { _typeof as default };", "import _typeof from \"./typeof.js\";\nfunction toPrimitive(t, r) {\n if (\"object\" != _typeof(t) || !t) return t;\n var e = t[Symbol.toPrimitive];\n if (void 0 !== e) {\n var i = e.call(t, r || \"default\");\n if (\"object\" != _typeof(i)) return i;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (\"string\" === r ? String : Number)(t);\n}\nexport { toPrimitive as default };", "import _typeof from \"./typeof.js\";\nimport toPrimitive from \"./toPrimitive.js\";\nfunction toPropertyKey(t) {\n var i = toPrimitive(t, \"string\");\n return \"symbol\" == _typeof(i) ? i : i + \"\";\n}\nexport { toPropertyKey as default };", "import toPropertyKey from \"./toPropertyKey.js\";\nfunction _defineProperty(e, r, t) {\n return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {\n value: t,\n enumerable: !0,\n configurable: !0,\n writable: !0\n }) : e[r] = t, e;\n}\nexport { _defineProperty as default };", "import defineProperty from \"./defineProperty.js\";\nfunction ownKeys(e, r) {\n var t = Object.keys(e);\n if (Object.getOwnPropertySymbols) {\n var o = Object.getOwnPropertySymbols(e);\n r && (o = o.filter(function (r) {\n return Object.getOwnPropertyDescriptor(e, r).enumerable;\n })), t.push.apply(t, o);\n }\n return t;\n}\nfunction _objectSpread2(e) {\n for (var r = 1; r < arguments.length; r++) {\n var t = null != arguments[r] ? arguments[r] : {};\n r % 2 ? ownKeys(Object(t), !0).forEach(function (r) {\n defineProperty(e, r, t[r]);\n }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {\n Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));\n });\n }\n return e;\n}\nexport { _objectSpread2 as default };"],
"mappings": ";AAAA,SAAS,QAAQ,GAAG;AAClB;AAEA,SAAO,UAAU,cAAc,OAAO,UAAU,YAAY,OAAO,OAAO,WAAW,SAAUA,IAAG;AAChG,WAAO,OAAOA;AAAA,EAChB,IAAI,SAAUA,IAAG;AACf,WAAOA,MAAK,cAAc,OAAO,UAAUA,GAAE,gBAAgB,UAAUA,OAAM,OAAO,YAAY,WAAW,OAAOA;AAAA,EACpH,GAAG,QAAQ,CAAC;AACd;;;ACPA,SAAS,YAAY,GAAG,GAAG;AACzB,MAAI,YAAY,QAAQ,CAAC,KAAK,CAAC,EAAG,QAAO;AACzC,MAAI,IAAI,EAAE,OAAO,WAAW;AAC5B,MAAI,WAAW,GAAG;AAChB,QAAI,IAAI,EAAE,KAAK,GAAG,KAAK,SAAS;AAChC,QAAI,YAAY,QAAQ,CAAC,EAAG,QAAO;AACnC,UAAM,IAAI,UAAU,8CAA8C;AAAA,EACpE;AACA,UAAQ,aAAa,IAAI,SAAS,QAAQ,CAAC;AAC7C;;;ACRA,SAAS,cAAc,GAAG;AACxB,MAAI,IAAI,YAAY,GAAG,QAAQ;AAC/B,SAAO,YAAY,QAAQ,CAAC,IAAI,IAAI,IAAI;AAC1C;;;ACJA,SAAS,gBAAgB,GAAG,GAAG,GAAG;AAChC,UAAQ,IAAI,cAAc,CAAC,MAAM,IAAI,OAAO,eAAe,GAAG,GAAG;AAAA,IAC/D,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,UAAU;AAAA,EACZ,CAAC,IAAI,EAAE,CAAC,IAAI,GAAG;AACjB;;;ACPA,SAAS,QAAQ,GAAG,GAAG;AACrB,MAAI,IAAI,OAAO,KAAK,CAAC;AACrB,MAAI,OAAO,uBAAuB;AAChC,QAAI,IAAI,OAAO,sBAAsB,CAAC;AACtC,UAAM,IAAI,EAAE,OAAO,SAAUC,IAAG;AAC9B,aAAO,OAAO,yBAAyB,GAAGA,EAAC,EAAE;AAAA,IAC/C,CAAC,IAAI,EAAE,KAAK,MAAM,GAAG,CAAC;AAAA,EACxB;AACA,SAAO;AACT;AACA,SAAS,eAAe,GAAG;AACzB,WAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;AACzC,QAAI,IAAI,QAAQ,UAAU,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC;AAC/C,QAAI,IAAI,QAAQ,OAAO,CAAC,GAAG,IAAE,EAAE,QAAQ,SAAUA,IAAG;AAClD,sBAAe,GAAGA,IAAG,EAAEA,EAAC,CAAC;AAAA,IAC3B,CAAC,IAAI,OAAO,4BAA4B,OAAO,iBAAiB,GAAG,OAAO,0BAA0B,CAAC,CAAC,IAAI,QAAQ,OAAO,CAAC,CAAC,EAAE,QAAQ,SAAUA,IAAG;AAChJ,aAAO,eAAe,GAAGA,IAAG,OAAO,yBAAyB,GAAGA,EAAC,CAAC;AAAA,IACnE,CAAC;AAAA,EACH;AACA,SAAO;AACT;",
"names": ["o", "r"]
}

View File

@@ -0,0 +1,42 @@
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
var __commonJS = (cb, mod) => function __require2() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
export {
__require,
__commonJS,
__export,
__toESM
};

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,288 @@
import {
__commonJS
} from "./chunk-PR4QN5HX.js";
// node_modules/dayjs/dayjs.min.js
var require_dayjs_min = __commonJS({
"node_modules/dayjs/dayjs.min.js"(exports, module) {
!(function(t, e) {
"object" == typeof exports && "undefined" != typeof module ? module.exports = e() : "function" == typeof define && define.amd ? define(e) : (t = "undefined" != typeof globalThis ? globalThis : t || self).dayjs = e();
})(exports, (function() {
"use strict";
var t = 1e3, e = 6e4, n = 36e5, r = "millisecond", i = "second", s = "minute", u = "hour", a = "day", o = "week", c = "month", f = "quarter", h = "year", d = "date", l = "Invalid Date", $ = /^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/, y = /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g, M = { name: "en", weekdays: "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), months: "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), ordinal: function(t2) {
var e2 = ["th", "st", "nd", "rd"], n2 = t2 % 100;
return "[" + t2 + (e2[(n2 - 20) % 10] || e2[n2] || e2[0]) + "]";
} }, m = function(t2, e2, n2) {
var r2 = String(t2);
return !r2 || r2.length >= e2 ? t2 : "" + Array(e2 + 1 - r2.length).join(n2) + t2;
}, v = { s: m, z: function(t2) {
var e2 = -t2.utcOffset(), n2 = Math.abs(e2), r2 = Math.floor(n2 / 60), i2 = n2 % 60;
return (e2 <= 0 ? "+" : "-") + m(r2, 2, "0") + ":" + m(i2, 2, "0");
}, m: function t2(e2, n2) {
if (e2.date() < n2.date()) return -t2(n2, e2);
var r2 = 12 * (n2.year() - e2.year()) + (n2.month() - e2.month()), i2 = e2.clone().add(r2, c), s2 = n2 - i2 < 0, u2 = e2.clone().add(r2 + (s2 ? -1 : 1), c);
return +(-(r2 + (n2 - i2) / (s2 ? i2 - u2 : u2 - i2)) || 0);
}, a: function(t2) {
return t2 < 0 ? Math.ceil(t2) || 0 : Math.floor(t2);
}, p: function(t2) {
return { M: c, y: h, w: o, d: a, D: d, h: u, m: s, s: i, ms: r, Q: f }[t2] || String(t2 || "").toLowerCase().replace(/s$/, "");
}, u: function(t2) {
return void 0 === t2;
} }, g = "en", D = {};
D[g] = M;
var p = "$isDayjsObject", S = function(t2) {
return t2 instanceof _ || !(!t2 || !t2[p]);
}, w = function t2(e2, n2, r2) {
var i2;
if (!e2) return g;
if ("string" == typeof e2) {
var s2 = e2.toLowerCase();
D[s2] && (i2 = s2), n2 && (D[s2] = n2, i2 = s2);
var u2 = e2.split("-");
if (!i2 && u2.length > 1) return t2(u2[0]);
} else {
var a2 = e2.name;
D[a2] = e2, i2 = a2;
}
return !r2 && i2 && (g = i2), i2 || !r2 && g;
}, O = function(t2, e2) {
if (S(t2)) return t2.clone();
var n2 = "object" == typeof e2 ? e2 : {};
return n2.date = t2, n2.args = arguments, new _(n2);
}, b = v;
b.l = w, b.i = S, b.w = function(t2, e2) {
return O(t2, { locale: e2.$L, utc: e2.$u, x: e2.$x, $offset: e2.$offset });
};
var _ = (function() {
function M2(t2) {
this.$L = w(t2.locale, null, true), this.parse(t2), this.$x = this.$x || t2.x || {}, this[p] = true;
}
var m2 = M2.prototype;
return m2.parse = function(t2) {
this.$d = (function(t3) {
var e2 = t3.date, n2 = t3.utc;
if (null === e2) return /* @__PURE__ */ new Date(NaN);
if (b.u(e2)) return /* @__PURE__ */ new Date();
if (e2 instanceof Date) return new Date(e2);
if ("string" == typeof e2 && !/Z$/i.test(e2)) {
var r2 = e2.match($);
if (r2) {
var i2 = r2[2] - 1 || 0, s2 = (r2[7] || "0").substring(0, 3);
return n2 ? new Date(Date.UTC(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2)) : new Date(r2[1], i2, r2[3] || 1, r2[4] || 0, r2[5] || 0, r2[6] || 0, s2);
}
}
return new Date(e2);
})(t2), this.init();
}, m2.init = function() {
var t2 = this.$d;
this.$y = t2.getFullYear(), this.$M = t2.getMonth(), this.$D = t2.getDate(), this.$W = t2.getDay(), this.$H = t2.getHours(), this.$m = t2.getMinutes(), this.$s = t2.getSeconds(), this.$ms = t2.getMilliseconds();
}, m2.$utils = function() {
return b;
}, m2.isValid = function() {
return !(this.$d.toString() === l);
}, m2.isSame = function(t2, e2) {
var n2 = O(t2);
return this.startOf(e2) <= n2 && n2 <= this.endOf(e2);
}, m2.isAfter = function(t2, e2) {
return O(t2) < this.startOf(e2);
}, m2.isBefore = function(t2, e2) {
return this.endOf(e2) < O(t2);
}, m2.$g = function(t2, e2, n2) {
return b.u(t2) ? this[e2] : this.set(n2, t2);
}, m2.unix = function() {
return Math.floor(this.valueOf() / 1e3);
}, m2.valueOf = function() {
return this.$d.getTime();
}, m2.startOf = function(t2, e2) {
var n2 = this, r2 = !!b.u(e2) || e2, f2 = b.p(t2), l2 = function(t3, e3) {
var i2 = b.w(n2.$u ? Date.UTC(n2.$y, e3, t3) : new Date(n2.$y, e3, t3), n2);
return r2 ? i2 : i2.endOf(a);
}, $2 = function(t3, e3) {
return b.w(n2.toDate()[t3].apply(n2.toDate("s"), (r2 ? [0, 0, 0, 0] : [23, 59, 59, 999]).slice(e3)), n2);
}, y2 = this.$W, M3 = this.$M, m3 = this.$D, v2 = "set" + (this.$u ? "UTC" : "");
switch (f2) {
case h:
return r2 ? l2(1, 0) : l2(31, 11);
case c:
return r2 ? l2(1, M3) : l2(0, M3 + 1);
case o:
var g2 = this.$locale().weekStart || 0, D2 = (y2 < g2 ? y2 + 7 : y2) - g2;
return l2(r2 ? m3 - D2 : m3 + (6 - D2), M3);
case a:
case d:
return $2(v2 + "Hours", 0);
case u:
return $2(v2 + "Minutes", 1);
case s:
return $2(v2 + "Seconds", 2);
case i:
return $2(v2 + "Milliseconds", 3);
default:
return this.clone();
}
}, m2.endOf = function(t2) {
return this.startOf(t2, false);
}, m2.$set = function(t2, e2) {
var n2, o2 = b.p(t2), f2 = "set" + (this.$u ? "UTC" : ""), l2 = (n2 = {}, n2[a] = f2 + "Date", n2[d] = f2 + "Date", n2[c] = f2 + "Month", n2[h] = f2 + "FullYear", n2[u] = f2 + "Hours", n2[s] = f2 + "Minutes", n2[i] = f2 + "Seconds", n2[r] = f2 + "Milliseconds", n2)[o2], $2 = o2 === a ? this.$D + (e2 - this.$W) : e2;
if (o2 === c || o2 === h) {
var y2 = this.clone().set(d, 1);
y2.$d[l2]($2), y2.init(), this.$d = y2.set(d, Math.min(this.$D, y2.daysInMonth())).$d;
} else l2 && this.$d[l2]($2);
return this.init(), this;
}, m2.set = function(t2, e2) {
return this.clone().$set(t2, e2);
}, m2.get = function(t2) {
return this[b.p(t2)]();
}, m2.add = function(r2, f2) {
var d2, l2 = this;
r2 = Number(r2);
var $2 = b.p(f2), y2 = function(t2) {
var e2 = O(l2);
return b.w(e2.date(e2.date() + Math.round(t2 * r2)), l2);
};
if ($2 === c) return this.set(c, this.$M + r2);
if ($2 === h) return this.set(h, this.$y + r2);
if ($2 === a) return y2(1);
if ($2 === o) return y2(7);
var M3 = (d2 = {}, d2[s] = e, d2[u] = n, d2[i] = t, d2)[$2] || 1, m3 = this.$d.getTime() + r2 * M3;
return b.w(m3, this);
}, m2.subtract = function(t2, e2) {
return this.add(-1 * t2, e2);
}, m2.format = function(t2) {
var e2 = this, n2 = this.$locale();
if (!this.isValid()) return n2.invalidDate || l;
var r2 = t2 || "YYYY-MM-DDTHH:mm:ssZ", i2 = b.z(this), s2 = this.$H, u2 = this.$m, a2 = this.$M, o2 = n2.weekdays, c2 = n2.months, f2 = n2.meridiem, h2 = function(t3, n3, i3, s3) {
return t3 && (t3[n3] || t3(e2, r2)) || i3[n3].slice(0, s3);
}, d2 = function(t3) {
return b.s(s2 % 12 || 12, t3, "0");
}, $2 = f2 || function(t3, e3, n3) {
var r3 = t3 < 12 ? "AM" : "PM";
return n3 ? r3.toLowerCase() : r3;
};
return r2.replace(y, (function(t3, r3) {
return r3 || (function(t4) {
switch (t4) {
case "YY":
return String(e2.$y).slice(-2);
case "YYYY":
return b.s(e2.$y, 4, "0");
case "M":
return a2 + 1;
case "MM":
return b.s(a2 + 1, 2, "0");
case "MMM":
return h2(n2.monthsShort, a2, c2, 3);
case "MMMM":
return h2(c2, a2);
case "D":
return e2.$D;
case "DD":
return b.s(e2.$D, 2, "0");
case "d":
return String(e2.$W);
case "dd":
return h2(n2.weekdaysMin, e2.$W, o2, 2);
case "ddd":
return h2(n2.weekdaysShort, e2.$W, o2, 3);
case "dddd":
return o2[e2.$W];
case "H":
return String(s2);
case "HH":
return b.s(s2, 2, "0");
case "h":
return d2(1);
case "hh":
return d2(2);
case "a":
return $2(s2, u2, true);
case "A":
return $2(s2, u2, false);
case "m":
return String(u2);
case "mm":
return b.s(u2, 2, "0");
case "s":
return String(e2.$s);
case "ss":
return b.s(e2.$s, 2, "0");
case "SSS":
return b.s(e2.$ms, 3, "0");
case "Z":
return i2;
}
return null;
})(t3) || i2.replace(":", "");
}));
}, m2.utcOffset = function() {
return 15 * -Math.round(this.$d.getTimezoneOffset() / 15);
}, m2.diff = function(r2, d2, l2) {
var $2, y2 = this, M3 = b.p(d2), m3 = O(r2), v2 = (m3.utcOffset() - this.utcOffset()) * e, g2 = this - m3, D2 = function() {
return b.m(y2, m3);
};
switch (M3) {
case h:
$2 = D2() / 12;
break;
case c:
$2 = D2();
break;
case f:
$2 = D2() / 3;
break;
case o:
$2 = (g2 - v2) / 6048e5;
break;
case a:
$2 = (g2 - v2) / 864e5;
break;
case u:
$2 = g2 / n;
break;
case s:
$2 = g2 / e;
break;
case i:
$2 = g2 / t;
break;
default:
$2 = g2;
}
return l2 ? $2 : b.a($2);
}, m2.daysInMonth = function() {
return this.endOf(c).$D;
}, m2.$locale = function() {
return D[this.$L];
}, m2.locale = function(t2, e2) {
if (!t2) return this.$L;
var n2 = this.clone(), r2 = w(t2, e2, true);
return r2 && (n2.$L = r2), n2;
}, m2.clone = function() {
return b.w(this.$d, this);
}, m2.toDate = function() {
return new Date(this.valueOf());
}, m2.toJSON = function() {
return this.isValid() ? this.toISOString() : null;
}, m2.toISOString = function() {
return this.$d.toISOString();
}, m2.toString = function() {
return this.$d.toUTCString();
}, M2;
})(), k = _.prototype;
return O.prototype = k, [["$ms", r], ["$s", i], ["$m", s], ["$H", u], ["$W", a], ["$M", c], ["$y", h], ["$D", d]].forEach((function(t2) {
k[t2[1]] = function(e2) {
return this.$g(e2, t2[0], t2[1]);
};
})), O.extend = function(t2, e2) {
return t2.$i || (t2(e2, _, O), t2.$i = true), O;
}, O.locale = w, O.isDayjs = S, O.unix = function(t2) {
return O(1e3 * t2);
}, O.en = D[g], O.Ls = D, O.p = {}, O;
}));
}
});
export {
require_dayjs_min
};
//# sourceMappingURL=chunk-XCUFKJYR.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,20 @@
// node_modules/ant-design-vue/es/vc-pagination/locale/zh_CN.js
var zh_CN_default = {
// Options.jsx
items_per_page: "条/页",
jump_to: "跳至",
jump_to_confirm: "确定",
page: "页",
// Pagination.jsx
prev_page: "上一页",
next_page: "下一页",
prev_5: "向前 5 页",
next_5: "向后 5 页",
prev_3: "向前 3 页",
next_3: "向后 3 页"
};
export {
zh_CN_default
};
//# sourceMappingURL=chunk-YODRZMZT.js.map

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../node_modules/ant-design-vue/es/vc-pagination/locale/zh_CN.js"],
"sourcesContent": ["export default {\n // Options.jsx\n items_per_page: '条/页',\n jump_to: '跳至',\n jump_to_confirm: '确定',\n page: '页',\n // Pagination.jsx\n prev_page: '上一页',\n next_page: '下一页',\n prev_5: '向前 5 页',\n next_5: '向后 5 页',\n prev_3: '向前 3 页',\n next_3: '向后 3 页'\n};"],
"mappings": ";AAAA,IAAO,gBAAQ;AAAA;AAAA,EAEb,gBAAgB;AAAA,EAChB,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB,MAAM;AAAA;AAAA,EAEN,WAAW;AAAA,EACX,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AACV;",
"names": []
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
import {
require_dayjs_min
} from "./chunk-XCUFKJYR.js";
import "./chunk-PR4QN5HX.js";
export default require_dayjs_min();

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,343 @@
import {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBaseVNode,
createBlock,
createCommentVNode,
createElementBlock,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
} from "./chunk-Y7TKRIWE.js";
import "./chunk-PR4QN5HX.js";
export {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createBaseVNode as createElementVNode,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
};

View File

@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -0,0 +1,137 @@
<div align="center">
<h1>通用大模型模板 · Hertz Admin + AI</h1>
现代化、可即用的管理后台前端模板。聚焦“工程化 + 体验”内置账号体系、权限路由、主题美化、知识库、YOLO 模型全流程(管理/类别/告警/历史)等典型模块。
<p>
基于 Vite + Vue 3 + TypeScript + Ant Design Vue + Pinia + Vue Router 构建
</p>
</div>
---
## ✨ 特性一览
- 设计统一:全局“苹果风格”主题(卡片/弹窗/按钮/输入/分页),开箱即用且风格一致
- 工程规范TypeScript 强类型、请求与错误拦截、模块化 API、权限化菜单/路由
- 典型业务:
- 知识库管理:分类树、列表搜索、编辑/发布,已优化分类切换闪烁
- YOLO 模型:模型管理、模型类别管理、告警处理中心、检测历史管理
- 认证体系:登录/注册(字段对齐、错误信息透出),可扩展验证码
- 体验友好:延迟 loading 避免闪烁、毛玻璃质感、统一按钮与交互反馈
## 🧩 技术栈
- 构建Vite
- 语言TypeScript
- 框架Vue 3Composition API
- UIAnt Design Vue
- 状态Pinia
- 路由Vue Router
## 📦 目录结构(核心)
```
通用大模型模板/
└─ hertz_server_diango_ui_2/ # 前端工程Vite
├─ public/ # 公共静态资源
├─ src/
│ ├─ api/ # 接口定义auth、yolo、knowledge、…
│ ├─ locales/ # 国际化
│ ├─ router/ # 路由与菜单admin_menu.ts 自动化)
│ ├─ stores/ # Pinia
│ ├─ styles/ # 全局样式与变量index.scss、variables.scss
│ ├─ utils/ # 工具请求、权限、URL 等)
│ └─ views/ # 页面
│ ├─ admin_page/ # 管理端模块
│ ├─ user_pages/ # 用户端模块
│ └─ register.vue / Login.vue # 登录注册
├─ index.html
├─ package.json
└─ vite.config.ts
```
## 🚀 快速开始
```bash
# 进入工程目录
cd hertz_server_diango_ui_2
# 安装依赖
npm i
# 开发启动
npm run dev
## ⚙️ 环境与请求
- 默认使用同域代理或反向代理。按需要在环境文件中设置:
```bash
# .env.development示例
VITE_API_BASE=/api
```
- 请求封装:`src/utils/hertz_request.ts`(拦截器、错误提示、统一 header
⚠️注:所有后端IP都更改为“localhost:3000”需根据具体项目与对应后端开发对接
## 🔧 关键模块说明
- 主题美化Design System
- `src/styles/index.scss``src/styles/variables.scss`
- 全局统一Modal/Drawer/Button/Input/Select/Table/Pagination…
- 专门处理闪烁与焦点态的视觉细节
- 菜单与路由
- `src/router/admin_menu.ts`:单文件维护菜单与路由,支持权限过滤与自动生成
- 统一面包屑:已移除“首页/”的冗余展示,仅保留当前层级
- 知识库管理
- `src/views/admin_page/KnowledgeBaseManagement.vue`
- 分类树 + 列表搜索 + 编辑/发布
- 已优化分类切换闪烁(分类卡片不 Loading、表格 Loading 延迟)
- YOLO 模块
- 模型管理:上传/列表/启用禁用(苹果风格拖拽区与卡片)
- 模型类别管理:别名编辑、等级切换
- 告警处理中心:统计卡片、筛选、批量处理、详情预览
- 检测历史管理:搜索、筛选、对比查看(图片/视频),已移除“下载结果”按钮(后端未实现)
- 认证模块
- API`src/api/auth.ts`
- 注册页:`src/views/register.vue` 已与后端对齐字段
- 提交字段:`username, password, confirm_password, email, phone, real_name`
- 兼容字段:`captcha, captcha_id`(未启用可传空串)
- 统一错误提示透出
## 🧪 常见问题FAQ
- 按钮样式与其他页面不一致?
- 已在 `src/styles/index.scss``.ant-btn` 全局统一。若仍不一致,检查局部覆盖或第三方样式。
- 分类切换时闪烁?
- 左侧分类卡片不再受列表 Loading 影响;表格 Loading 使用 `{ spinning, delay: 200 }`。仍抖动可增加骨架屏或请求防抖。
- 接口没有请求或字段不匹配?
- 检查 `src/api/*.ts` 与页面 `payload` 是否一致;打开浏览器 Network 面板查看请求/响应详情。
## 🛠️ 二次开发建议
- 新增模块:在 `src/views/admin_page/` 增加页面,并在 `src/router/admin_menu.ts` 添加菜单与路由映射
- 改造主题:在 `styles/variables.scss` 修改色板与圆角/阴影;在 `index.scss` 扩展组件级风格
- 对接后端:在 `src/api/` 创建对应接口文件,使用统一 `request` 封装
## 📜 脚本列表
```json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
```

80
hertz_server_diango_ui/components.d.ts vendored Normal file
View File

@@ -0,0 +1,80 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABadge: typeof import('ant-design-vue/es')['Badge']
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
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']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

View File

@@ -0,0 +1,81 @@
import js from '@eslint/js'
import vue from 'eslint-plugin-vue'
import tseslint from 'typescript-eslint'
export default [
// JavaScript 推荐规则
js.configs.recommended,
// TypeScript 推荐规则
...tseslint.configs.recommended,
// Vue 推荐规则
...vue.configs['flat/recommended'],
// 项目特定配置
{
files: ['**/*.{js,mjs,cjs,ts,vue}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
// 浏览器环境
console: 'readonly',
window: 'readonly',
document: 'readonly',
navigator: 'readonly',
fetch: 'readonly',
// Node.js环境
process: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
module: 'readonly',
require: 'readonly',
global: 'readonly',
// Vite环境
import: 'readonly',
// Vue环境
Vue: 'readonly',
}
},
rules: {
// 禁用所有可能导致WebStorm警告的规则
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-console': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-debugger': 'off',
'no-alert': 'off',
'no-prototype-builtins': 'off',
// Vue相关规则
'vue/multi-word-component-names': 'off',
'vue/no-unused-vars': 'off',
'vue/no-unused-components': 'off',
'vue/no-unused-properties': 'off',
'vue/require-v-for-key': 'off',
'vue/no-use-v-if-with-v-for': 'off',
// TypeScript规则
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/prefer-const': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
}
},
// 忽略文件
{
ignores: [
'node_modules/**',
'dist/**',
'.git/**',
'coverage/**',
'*.config.js',
'*.config.ts',
]
}
]

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>管理系统模板</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

5692
hertz_server_diango_ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
{
"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"
},
"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",
"daisyui": "^5.1.13",
"eslint": "^9.36.0",
"eslint-plugin-vue": "^10.4.0",
"postcss": "^8.5.6",
"sass-embedded": "^1.93.0",
"tailwindcss": "^4.1.13",
"typescript": "~5.8.3",
"typescript-eslint": "^8.44.0",
"unplugin-vue-components": "^29.1.0",
"vite": "^7.1.6",
"vue-tsc": "^3.0.7"
}
}

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { useUserStore } from './stores/hertz_user'
import { RouterView } from 'vue-router'
import { ConfigProvider } from 'ant-design-vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import enUS from 'ant-design-vue/es/locale/en_US'
const userStore = useUserStore()
// 主题配置 - 简约现代风格
const theme = ref({
algorithm: 'default' as 'default' | 'dark' | 'compact',
token: {
colorPrimary: '#2563eb',
colorSuccess: '#10b981',
colorWarning: '#f59e0b',
colorError: '#ef4444',
borderRadius: 8,
fontSize: 14,
},
})
// 语言配置
const locale = ref(zhCN)
// 主题切换
const toggleTheme = () => {
const currentTheme = localStorage.getItem('theme') || 'light'
const newTheme = currentTheme === 'light' ? 'dark' : 'light'
localStorage.setItem('theme', newTheme)
if (newTheme === 'dark') {
theme.value.algorithm = 'dark'
document.documentElement.setAttribute('data-theme', 'dark')
} else {
theme.value.algorithm = 'default'
document.documentElement.setAttribute('data-theme', 'light')
}
}
// 初始化主题
onMounted(() => {
const savedTheme = localStorage.getItem('theme') || 'light'
if (savedTheme === 'dark') {
theme.value.algorithm = 'dark'
document.documentElement.setAttribute('data-theme', 'dark')
} else {
theme.value.algorithm = 'default'
document.documentElement.setAttribute('data-theme', 'light')
}
})
const showLayout = computed(() => {
return userStore.isLoggedIn
})
</script>
<template>
<div id="app">
<ConfigProvider :theme="theme" :locale="locale">
<RouterView />
</ConfigProvider>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
padding: 0;
}
</style>

View File

@@ -0,0 +1,96 @@
import { request } from '@/utils/hertz_request'
// 通用响应类型
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
}
// 会话与消息类型
export interface AIChatItem {
id: number
title: string
created_at: string
updated_at: string
latest_message?: string
}
export interface AIChatDetail {
id: number
title: string
created_at: string
updated_at: string
}
export interface AIChatMessage {
id: number
role: 'user' | 'assistant' | 'system'
content: string
created_at: string
}
export interface ChatListData {
total: number
page: number
page_size: number
chats: AIChatItem[]
}
export interface ChatDetailData {
chat: AIChatDetail
messages: AIChatMessage[]
}
export interface SendMessageData {
user_message: AIChatMessage
ai_message: AIChatMessage
}
// 将后端可能返回的 chat_id 统一规范为 id
const normalizeChatItem = (raw: any): AIChatItem => ({
id: typeof raw?.id === 'number' ? raw.id : Number(raw?.chat_id),
title: raw?.title,
created_at: raw?.created_at,
updated_at: raw?.updated_at,
latest_message: raw?.latest_message,
})
const normalizeChatDetail = (raw: any): AIChatDetail => ({
id: typeof raw?.id === 'number' ? raw.id : Number(raw?.chat_id),
title: raw?.title,
created_at: raw?.created_at,
updated_at: raw?.updated_at,
})
export const aiApi = {
listChats: (params?: { query?: string; page?: number; page_size?: number }): Promise<ApiResponse<ChatListData>> =>
request.get('/api/ai/chats/', { params, showError: false }).then((resp: any) => {
if (resp?.data?.chats && Array.isArray(resp.data.chats)) {
resp.data.chats = resp.data.chats.map((c: any) => normalizeChatItem(c))
}
return resp as ApiResponse<ChatListData>
}),
createChat: (body?: { title?: string }): Promise<ApiResponse<AIChatDetail>> =>
request.post('/api/ai/chats/create/', body || { title: '新对话' }).then((resp: any) => {
if (resp?.data) resp.data = normalizeChatDetail(resp.data)
return resp as ApiResponse<AIChatDetail>
}),
getChatDetail: (chatId: number): Promise<ApiResponse<ChatDetailData>> =>
request.get(`/api/ai/chats/${chatId}/`).then((resp: any) => {
if (resp?.data?.chat) resp.data.chat = normalizeChatDetail(resp.data.chat)
return resp as ApiResponse<ChatDetailData>
}),
updateChat: (chatId: number, body: { title: string }): Promise<ApiResponse<null>> =>
request.put(`/api/ai/chats/${chatId}/update/`, body),
deleteChats: (chatIds: number[]): Promise<ApiResponse<null>> =>
request.post('/api/ai/chats/delete/', { chat_ids: chatIds }),
sendMessage: (chatId: number, body: { content: string }): Promise<ApiResponse<SendMessageData>> =>
request.post(`/api/ai/chats/${chatId}/send/`, body),
}

View File

@@ -0,0 +1,47 @@
import { request } from '@/utils/hertz_request'
// 注册接口数据类型
export interface RegisterData {
username: string
password: string
confirm_password: string
email: string
phone: string
real_name: string
captcha: string
captcha_id: string
}
// 发送邮箱验证码数据类型
export interface SendEmailCodeData {
email: string
code_type: string
}
// 登录接口数据类型
export interface LoginData {
username: string
password: string
captcha_code: string
captcha_key: string
}
// 注册API
export const registerUser = (data: RegisterData) => {
return request.post('/api/auth/register/', data)
}
// 登录API
export const loginUser = (data: LoginData) => {
return request.post('/api/auth/login/', data)
}
// 发送邮箱验证码API
export const sendEmailCode = (data: SendEmailCodeData) => {
return request.post('/api/auth/email/code/', data)
}
// 登出API
export const logoutUser = () => {
return request.post('/api/auth/logout/')
}

View File

@@ -0,0 +1,89 @@
import { request } from '@/utils/hertz_request'
// 验证码相关接口类型定义
export interface CaptchaResponse {
captcha_id: string
image_data: string // base64编码的图片
expires_in: number // 过期时间(秒)
}
export interface CaptchaRefreshResponse {
captcha_id: string
image_data: string // base64编码的图片
expires_in: number // 过期时间(秒)
}
/**
* 生成验证码
*/
export const generateCaptcha = async (): Promise<CaptchaResponse> => {
console.log('🚀 开始发送验证码生成请求...')
console.log('📍 请求URL:', `${import.meta.env.VITE_API_BASE_URL}/api/captcha/generate/`)
console.log('🌐 环境变量 VITE_API_BASE_URL:', import.meta.env.VITE_API_BASE_URL)
try {
const response = await request.post<{
code: number
message: string
data: CaptchaResponse
}>('/api/captcha/generate/')
console.log('✅ 验证码生成请求成功:', response)
return response.data
} catch (error: any) {
console.error('❌ 验证码生成请求失败 - 完整错误信息:')
console.error('错误对象:', error)
console.error('错误类型:', typeof error)
console.error('错误消息:', error?.message)
console.error('错误代码:', error?.code)
console.error('错误状态:', error?.status)
console.error('错误响应:', error?.response)
console.error('错误请求:', error?.request)
console.error('错误配置:', error?.config)
// 检查是否是网络错误
if (error?.code === 'NETWORK_ERROR' || error?.message?.includes('Network Error')) {
console.error('🌐 网络连接错误 - 可能的原因:')
console.error('1. 后端服务器未启动')
console.error('2. API地址不正确')
console.error('3. CORS配置问题')
console.error('4. 防火墙阻止连接')
}
throw error
}
}
/**
* 刷新验证码
*/
export const refreshCaptcha = async (captcha_id: string): Promise<CaptchaRefreshResponse> => {
console.log('🔄 开始发送验证码刷新请求...')
console.log('📍 请求URL:', `${import.meta.env.VITE_API_BASE_URL}/api/captcha/refresh/`)
console.log('📦 请求数据:', { captcha_id })
try {
const response = await request.post<{
code: number
message: string
data: CaptchaRefreshResponse
}>('/api/captcha/refresh/', {
captcha_id
})
console.log('✅ 验证码刷新请求成功:', response)
return response.data
} catch (error: any) {
console.error('❌ 验证码刷新请求失败 - 完整错误信息:')
console.error('错误对象:', error)
console.error('错误类型:', typeof error)
console.error('错误消息:', error?.message)
console.error('错误代码:', error?.code)
console.error('错误状态:', error?.status)
console.error('错误响应:', error?.response)
console.error('错误请求:', error?.request)
console.error('错误配置:', error?.config)
throw error
}
}

View File

@@ -0,0 +1,393 @@
import { request } from '@/utils/hertz_request'
import { logApi, type OperationLogListItem } from './log'
import { systemMonitorApi, type SystemInfo, type CpuInfo, type MemoryInfo, type DiskInfo } from './system_monitor'
import { noticeUserApi } from './notice_user'
import { knowledgeApi } from './knowledge'
// 仪表盘统计数据类型定义
export interface DashboardStats {
totalUsers: number
totalNotifications: number
totalLogs: number
totalKnowledge: number
userGrowthRate: number
notificationGrowthRate: number
logGrowthRate: number
knowledgeGrowthRate: number
}
// 最近活动数据类型
export interface RecentActivity {
id: number
action: string
time: string
user: string
type: 'login' | 'create' | 'update' | 'system' | 'register'
}
// 系统状态数据类型
export interface SystemStatus {
cpuUsage: number
memoryUsage: number
diskUsage: number
networkStatus: 'normal' | 'warning' | 'error'
}
// 访问趋势数据类型
export interface VisitTrend {
date: string
visits: number
users: number
}
// 仪表盘数据汇总类型
export interface DashboardData {
stats: DashboardStats
recentActivities: RecentActivity[]
systemStatus: SystemStatus
visitTrends: VisitTrend[]
}
// API响应类型
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
}
// 仪表盘API接口
export const dashboardApi = {
// 获取仪表盘统计数据
getStats: (): Promise<ApiResponse<DashboardStats>> => {
return request.get('/api/dashboard/stats/')
},
// 获取真实统计数据
getRealStats: async (): Promise<ApiResponse<DashboardStats>> => {
try {
// 并行获取各种统计数据
const [notificationStats, logStats, knowledgeStats] = await Promise.all([
noticeUserApi.statistics().catch(() => ({ success: false, data: { total_count: 0, unread_count: 0 } })),
logApi.getList({ page: 1, page_size: 1 }).catch(() => ({ success: false, data: { count: 0 } })),
knowledgeApi.getArticles({ page: 1, page_size: 1 }).catch(() => ({ success: false, data: { total: 0 } }))
])
// 计算统计数据
const totalNotifications = notificationStats.success ? (notificationStats.data.total_count || 0) : 0
// 处理日志数据 - 兼容多种返回结构
let totalLogs = 0
if (logStats.success && logStats.data) {
const logData = logStats.data as any
console.log('日志API响应数据:', logData)
// 兼容DRF标准结构{ count, next, previous, results }
if ('count' in logData) {
totalLogs = Number(logData.count) || 0
} else if ('total' in logData) {
totalLogs = Number(logData.total) || 0
} else if ('total_count' in logData) {
totalLogs = Number(logData.total_count) || 0
} else if (logData.pagination && logData.pagination.total_count) {
totalLogs = Number(logData.pagination.total_count) || 0
}
console.log('解析出的日志总数:', totalLogs)
} else {
console.log('日志API调用失败:', logStats)
}
const totalKnowledge = knowledgeStats.success ? (knowledgeStats.data.total || 0) : 0
console.log('统计数据汇总:', { totalNotifications, totalLogs, totalKnowledge })
// 模拟增长率(实际项目中应该从后端获取)
const stats: DashboardStats = {
totalUsers: 0, // 暂时设为0需要用户管理API
totalNotifications,
totalLogs,
totalKnowledge,
userGrowthRate: 0,
notificationGrowthRate: Math.floor(Math.random() * 20) - 10, // 模拟 -10% 到 +10%
logGrowthRate: Math.floor(Math.random() * 30) - 15, // 模拟 -15% 到 +15%
knowledgeGrowthRate: Math.floor(Math.random() * 25) - 12 // 模拟 -12% 到 +13%
}
return {
success: true,
code: 200,
message: 'success',
data: stats
}
} catch (error) {
console.error('获取真实统计数据失败:', error)
return {
success: false,
code: 500,
message: '获取统计数据失败',
data: {
totalUsers: 0,
totalNotifications: 0,
totalLogs: 0,
totalKnowledge: 0,
userGrowthRate: 0,
notificationGrowthRate: 0,
logGrowthRate: 0,
knowledgeGrowthRate: 0
}
}
}
},
// 获取最近活动(从日志接口)
getRecentActivities: async (limit: number = 10): Promise<ApiResponse<RecentActivity[]>> => {
try {
const response = await logApi.getList({ page: 1, page_size: limit })
if (response.success && response.data) {
// 根据实际API响应结构数据可能在data.logs或data.results中
const logs = (response.data as any).logs || (response.data as any).results || []
const activities: RecentActivity[] = logs.map((log: any) => ({
id: log.log_id || log.id,
action: log.description || log.operation_description || `${log.action_type_display || log.operation_type} - ${log.module || log.operation_module}`,
time: formatTimeAgo(log.created_at),
user: log.username || log.user?.username || '未知用户',
type: mapLogTypeToActivityType(log.action_type || log.operation_type)
}))
return {
success: true,
code: 200,
message: 'success',
data: activities
}
}
return {
success: false,
code: 500,
message: '获取活动数据失败',
data: []
}
} catch (error) {
console.error('获取最近活动失败:', error)
return {
success: false,
code: 500,
message: '获取活动数据失败',
data: []
}
}
},
// 获取系统状态(从系统监控接口)
getSystemStatus: async (): Promise<ApiResponse<SystemStatus>> => {
try {
const [cpuResponse, memoryResponse, disksResponse] = await Promise.all([
systemMonitorApi.getCpu(),
systemMonitorApi.getMemory(),
systemMonitorApi.getDisks()
])
if (cpuResponse.success && memoryResponse.success && disksResponse.success) {
// 根据实际API响应结构映射数据
const systemStatus: SystemStatus = {
// CPU使用率从 cpu_percent 字段获取
cpuUsage: Math.round(cpuResponse.data.cpu_percent || 0),
// 内存使用率:从 percent 字段获取
memoryUsage: Math.round(memoryResponse.data.percent || 0),
// 磁盘使用率:从磁盘数组的第一个磁盘的 percent 字段获取
diskUsage: disksResponse.data.length > 0 ? Math.round(disksResponse.data[0].percent || 0) : 0,
networkStatus: 'normal' as const
}
return {
success: true,
code: 200,
message: 'success',
data: systemStatus
}
}
return {
success: false,
code: 500,
message: '获取系统状态失败',
data: {
cpuUsage: 0,
memoryUsage: 0,
diskUsage: 0,
networkStatus: 'error' as const
}
}
} catch (error) {
console.error('获取系统状态失败:', error)
return {
success: false,
code: 500,
message: '获取系统状态失败',
data: {
cpuUsage: 0,
memoryUsage: 0,
diskUsage: 0,
networkStatus: 'error' as const
}
}
}
},
// 获取访问趋势
getVisitTrends: (period: 'week' | 'month' | 'year' = 'week'): Promise<ApiResponse<VisitTrend[]>> => {
return request.get('/api/dashboard/visit-trends/', { params: { period } })
},
// 获取完整仪表盘数据
getDashboardData: (): Promise<ApiResponse<DashboardData>> => {
return request.get('/api/dashboard/overview/')
},
// 模拟数据方法(用于开发阶段)
getMockStats: (): Promise<DashboardStats> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
totalUsers: 1128,
todayVisits: 893,
totalOrders: 234,
totalRevenue: 12560.50,
userGrowthRate: 12,
visitGrowthRate: 8,
orderGrowthRate: -3,
revenueGrowthRate: 15
})
}, 500)
})
},
getMockActivities: (): Promise<RecentActivity[]> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{
id: 1,
action: '用户 张三 登录了系统',
time: '2分钟前',
user: '张三',
type: 'login'
},
{
id: 2,
action: '管理员 李四 创建了新部门',
time: '5分钟前',
user: '李四',
type: 'create'
},
{
id: 3,
action: '用户 王五 修改了个人信息',
time: '10分钟前',
user: '王五',
type: 'update'
},
{
id: 4,
action: '系统自动备份完成',
time: '1小时前',
user: '系统',
type: 'system'
},
{
id: 5,
action: '新用户 赵六 注册成功',
time: '2小时前',
user: '赵六',
type: 'register'
}
])
}, 300)
})
},
getMockSystemStatus: (): Promise<SystemStatus> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
cpuUsage: 45,
memoryUsage: 67,
diskUsage: 32,
networkStatus: 'normal'
})
}, 200)
})
},
getMockVisitTrends: (period: 'week' | 'month' | 'year' = 'week'): Promise<VisitTrend[]> => {
return new Promise((resolve) => {
setTimeout(() => {
const data = {
week: [
{ date: '周一', visits: 120, users: 80 },
{ date: '周二', visits: 150, users: 95 },
{ date: '周三', visits: 180, users: 110 },
{ date: '周四', visits: 200, users: 130 },
{ date: '周五', visits: 250, users: 160 },
{ date: '周六', visits: 180, users: 120 },
{ date: '周日', visits: 160, users: 100 }
],
month: [
{ date: '第1周', visits: 800, users: 500 },
{ date: '第2周', visits: 950, users: 600 },
{ date: '第3周', visits: 1100, users: 700 },
{ date: '第4周', visits: 1200, users: 750 }
],
year: [
{ date: '1月', visits: 3200, users: 2000 },
{ date: '2月', visits: 3800, users: 2400 },
{ date: '3月', visits: 4200, users: 2600 },
{ date: '4月', visits: 3900, users: 2300 },
{ date: '5月', visits: 4500, users: 2800 },
{ date: '6月', visits: 5000, users: 3100 }
]
}
resolve(data[period])
}, 400)
})
}
}
// 辅助函数:格式化时间为相对时间
function formatTimeAgo(dateString: string): string {
const now = new Date()
const date = new Date(dateString)
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
if (diffInSeconds < 60) {
return `${diffInSeconds}秒前`
} else if (diffInSeconds < 3600) {
const minutes = Math.floor(diffInSeconds / 60)
return `${minutes}分钟前`
} else if (diffInSeconds < 86400) {
const hours = Math.floor(diffInSeconds / 3600)
return `${hours}小时前`
} else {
const days = Math.floor(diffInSeconds / 86400)
return `${days}天前`
}
}
// 辅助函数:将日志操作类型映射为活动类型
function mapLogTypeToActivityType(operationType: string): RecentActivity['type'] {
if (!operationType) return 'system'
const lowerType = operationType.toLowerCase()
if (lowerType.includes('login') || lowerType.includes('登录')) {
return 'login'
} else if (lowerType.includes('create') || lowerType.includes('创建') || lowerType.includes('add') || lowerType.includes('新增')) {
return 'create'
} else if (lowerType.includes('update') || lowerType.includes('修改') || lowerType.includes('edit') || lowerType.includes('更新')) {
return 'update'
} else if (lowerType.includes('register') || lowerType.includes('注册')) {
return 'register'
} else if (lowerType.includes('view') || lowerType.includes('查看') || lowerType.includes('get') || lowerType.includes('获取')) {
return 'system'
} else {
return 'system'
}
}

View File

@@ -0,0 +1,93 @@
import { request } from '@/utils/hertz_request'
// 部门数据类型定义
export interface Department {
dept_id: number
parent_id: number | null
dept_name: string
dept_code: string
leader: string
phone: string | null
email: string | null
status: number
sort_order: number
created_at: string
updated_at: string
children?: Department[]
user_count?: number
}
// API响应类型
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
}
// 部门列表数据类型
export interface DepartmentListData {
list: Department[]
total: number
page: number
page_size: number
}
export type DepartmentListResponse = ApiResponse<DepartmentListData>
// 部门列表查询参数
export interface DepartmentListParams {
page?: number
page_size?: number
search?: string
status?: number
parent_id?: number
}
// 创建部门参数
export interface CreateDepartmentParams {
parent_id: null
dept_name: string
dept_code: string
leader: string
phone: string
email: string
status: number
sort_order: number
}
// 更新部门参数
export type UpdateDepartmentParams = Partial<CreateDepartmentParams>
// 部门API接口
export const departmentApi = {
// 获取部门列表
getDepartmentList: (params?: DepartmentListParams): Promise<ApiResponse<Department[]>> => {
return request.get('/api/departments/', { params })
},
// 获取部门详情
getDepartment: (id: number): Promise<ApiResponse<Department>> => {
return request.get(`/api/departments/${id}/`)
},
// 创建部门
createDepartment: (data: CreateDepartmentParams): Promise<ApiResponse<Department>> => {
return request.post('/api/departments/create/', data)
},
// 更新部门
updateDepartment: (id: number, data: UpdateDepartmentParams): Promise<ApiResponse<Department>> => {
return request.put(`/api/departments/${id}/update/`, data)
},
// 删除部门
deleteDepartment: (id: number): Promise<ApiResponse<any>> => {
return request.delete(`/api/departments/${id}/delete/`)
},
// 获取部门树
getDepartmentTree: (): Promise<ApiResponse<Department[]>> => {
return request.get('/api/departments/tree/')
}
}

View File

@@ -0,0 +1,16 @@
// 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'

View File

@@ -0,0 +1,173 @@
import { request } from '@/utils/hertz_request'
// 通用响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
}
// 分类类型
export interface KnowledgeCategory {
id: number
name: string
description?: string
parent?: number | null
parent_name?: string | null
sort_order?: number
is_active?: boolean
created_at?: string
updated_at?: string
children_count?: number
articles_count?: number
full_path?: string
children?: KnowledgeCategory[]
}
export interface CategoryListData {
list: KnowledgeCategory[]
total: number
page: number
page_size: number
}
export interface CategoryListParams {
page?: number
page_size?: number
name?: string
parent_id?: number
is_active?: boolean
}
// 文章类型
export interface KnowledgeArticleListItem {
id: number
title: string
summary?: string | null
image?: string | null
category_name: string
author_name: string
status: 'draft' | 'published' | 'archived'
status_display: string
view_count?: number
created_at: string
updated_at: string
published_at?: string | null
}
export interface KnowledgeArticleDetail extends KnowledgeArticleListItem {
content: string
category: number
author: number
tags?: string
tags_list?: string[]
sort_order?: number
}
export interface ArticleListData {
list: KnowledgeArticleListItem[]
total: number
page: number
page_size: number
}
export interface ArticleListParams {
page?: number
page_size?: number
title?: string
category_id?: number
author_id?: number
status?: 'draft' | 'published' | 'archived'
tags?: string
}
export interface CreateArticlePayload {
title: string
content: string
summary?: string
image?: string
category: number
status?: 'draft' | 'published'
tags?: string
sort_order?: number
}
export interface UpdateArticlePayload {
title?: string
content?: string
summary?: string
image?: string
category?: number
status?: 'draft' | 'published' | 'archived'
tags?: string
sort_order?: number
}
// 知识库 API
export const knowledgeApi = {
// 分类:列表
getCategories: (params?: CategoryListParams): Promise<ApiResponse<CategoryListData>> => {
return request.get('/api/wiki/categories/', { params })
},
// 分类:树形
getCategoryTree: (): Promise<ApiResponse<KnowledgeCategory[]>> => {
return request.get('/api/wiki/categories/tree/')
},
// 分类:详情
getCategory: (id: number): Promise<ApiResponse<KnowledgeCategory>> => {
return request.get(`/api/wiki/categories/${id}/`)
},
// 分类:创建
createCategory: (data: Partial<KnowledgeCategory>): Promise<ApiResponse<KnowledgeCategory>> => {
return request.post('/api/wiki/categories/create/', data)
},
// 分类:更新
updateCategory: (id: number, data: Partial<KnowledgeCategory>): Promise<ApiResponse<KnowledgeCategory>> => {
return request.put(`/api/wiki/categories/${id}/update/`, data)
},
// 分类:删除
deleteCategory: (id: number): Promise<ApiResponse<null>> => {
return request.delete(`/api/wiki/categories/${id}/delete/`)
},
// 文章:列表
getArticles: (params?: ArticleListParams): Promise<ApiResponse<ArticleListData>> => {
return request.get('/api/wiki/articles/', { params })
},
// 文章:详情
getArticle: (id: number): Promise<ApiResponse<KnowledgeArticleDetail>> => {
return request.get(`/api/wiki/articles/${id}/`)
},
// 文章:创建
createArticle: (data: CreateArticlePayload): Promise<ApiResponse<KnowledgeArticleDetail>> => {
return request.post('/api/wiki/articles/create/', data)
},
// 文章:更新
updateArticle: (id: number, data: UpdateArticlePayload): Promise<ApiResponse<KnowledgeArticleDetail>> => {
return request.put(`/api/wiki/articles/${id}/update/`, data)
},
// 文章:删除
deleteArticle: (id: number): Promise<ApiResponse<null>> => {
return request.delete(`/api/wiki/articles/${id}/delete/`)
},
// 文章:发布
publishArticle: (id: number): Promise<ApiResponse<null>> => {
return request.post(`/api/wiki/articles/${id}/publish/`)
},
// 文章:归档
archiveArticle: (id: number): Promise<ApiResponse<null>> => {
return request.post(`/api/wiki/articles/${id}/archive/`)
},
}

View File

@@ -0,0 +1,110 @@
import { request } from '@/utils/hertz_request'
// 通用 API 响应结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
}
// 列表查询参数
export interface LogListParams {
page?: number
page_size?: number
user_id?: number
operation_type?: string
operation_module?: string
start_date?: string // YYYY-MM-DD
end_date?: string // YYYY-MM-DD
ip_address?: string
status?: number
// 新增:按请求方法与路径、关键字筛选(与后端保持可选兼容)
request_method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | string
request_path?: string
keyword?: string
}
// 列表项(精简字段)
export interface OperationLogItem {
id: number
user?: {
id: number
username: string
email?: string
} | null
operation_type: string
// 展示字段
action_type_display?: string
operation_module: string
operation_description?: string
target_model?: string
target_object_id?: string
ip_address?: string
request_method: string
request_path: string
response_status: number
// 结果与状态展示
status_display?: string
is_success?: boolean
execution_time?: number
created_at: string
}
// 列表响应 data 结构
export interface LogListData {
count: number
next: string | null
previous: string | null
results: OperationLogItem[]
}
export type LogListResponse = ApiResponse<LogListData>
// 详情数据(完整字段)
export interface OperationLogDetail {
id: number
user?: {
id: number
username: string
email?: string
} | null
operation_type: string
action_type_display?: string
operation_module: string
operation_description: string
target_model?: string
target_object_id?: string
ip_address?: string
user_agent?: string
request_method: string
request_path: string
request_data?: Record<string, any>
response_status: number
status_display?: string
is_success?: boolean
response_data?: Record<string, any>
execution_time?: number
created_at: string
updated_at?: string
}
export type LogDetailResponse = ApiResponse<OperationLogDetail>
export const logApi = {
// 获取操作日志列表
getList: (params: LogListParams, options?: { signal?: AbortSignal }): Promise<LogListResponse> => {
// 关闭统一错误弹窗,由页面自行处理
return request.get('/api/log/list/', { params, showError: false, signal: options?.signal })
},
// 获取操作日志详情
getDetail: (logId: number): Promise<LogDetailResponse> => {
return request.get(`/api/log/detail/${logId}/`)
},
// 兼容查询参数方式的详情(部分后端实现为 /api/log/detail/?id=xx 或 ?log_id=xx
getDetailByQuery: (logId: number): Promise<LogDetailResponse> => {
return request.get('/api/log/detail/', { params: { id: logId, log_id: logId } })
},
}

View File

@@ -0,0 +1,361 @@
import { request } from '@/utils/hertz_request'
// 后端返回的原始菜单数据格式
export interface RawMenu {
menu_id: number
menu_name: string
menu_code: string
menu_type: number // 后端返回数字1=菜单, 2=按钮, 3=接口
parent_id?: number | null
path?: string
component?: string | null
icon?: string
permission?: string
sort_order?: number
description?: string
status?: number
is_external?: boolean
is_cache?: boolean
is_visible?: boolean
created_at?: string
updated_at?: string
children?: RawMenu[]
}
// 前端使用的菜单接口类型定义
export interface Menu {
menu_id: number
menu_name: string
menu_code: string
menu_type: number // 1=菜单, 2=按钮, 3=接口
parent_id?: number
path?: string
component?: string
icon?: string
permission?: string
sort_order?: number
status?: number
is_external?: boolean
is_cache?: boolean
is_visible?: boolean
created_at?: string
updated_at?: string
children?: Menu[]
}
// API响应基础结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
}
// 菜单列表数据结构
export interface MenuListData {
list: Menu[]
total: number
page: number
page_size: number
}
// 菜单列表响应类型
export type MenuListResponse = ApiResponse<MenuListData>
// 菜单列表查询参数
export interface MenuListParams {
page?: number
page_size?: number
search?: string
status?: number
menu_type?: string
parent_id?: number
}
// 创建菜单参数
export interface CreateMenuParams {
menu_name: string
menu_code: string
menu_type: number // 1=菜单, 2=按钮, 3=接口
parent_id?: number
path?: string
component?: string
icon?: string
permission?: string
sort_order?: number
status?: number
is_external?: boolean
is_cache?: boolean
is_visible?: boolean
}
// 更新菜单参数
export type UpdateMenuParams = Partial<CreateMenuParams>
// 菜单树响应类型
export type MenuTreeResponse = ApiResponse<Menu[]>
// 数据转换工具函数
const convertMenuType = (type: number): 'menu' | 'button' | 'api' => {
switch (type) {
case 1: return 'menu'
case 2: return 'button'
case 3: return 'api'
default: return 'menu'
}
}
// 解码Unicode字符串
const decodeUnicode = (str: string): string => {
try {
return str.replace(/\\u[\dA-F]{4}/gi, (match) => {
return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16))
})
} catch (error) {
return str
}
}
// 转换原始菜单数据为前端格式
const transformRawMenu = (rawMenu: RawMenu): Menu => {
// 确保status字段被正确转换
let statusValue: number
if (rawMenu.status === undefined || rawMenu.status === null) {
// 如果status缺失默认为启用1
statusValue = 1
} else {
// 如果有值,转换为数字
if (typeof rawMenu.status === 'string') {
const parsed = parseInt(rawMenu.status, 10)
statusValue = isNaN(parsed) ? 1 : parsed
} else {
statusValue = Number(rawMenu.status)
// 如果转换失败,默认为启用
if (isNaN(statusValue)) {
statusValue = 1
}
}
}
return {
menu_id: rawMenu.menu_id,
menu_name: decodeUnicode(rawMenu.menu_name),
menu_code: rawMenu.menu_code,
menu_type: rawMenu.menu_type,
parent_id: rawMenu.parent_id || undefined,
path: rawMenu.path,
component: rawMenu.component,
icon: rawMenu.icon,
permission: rawMenu.permission,
sort_order: rawMenu.sort_order,
status: statusValue, // 使用转换后的值
is_external: rawMenu.is_external,
is_cache: rawMenu.is_cache,
is_visible: rawMenu.is_visible,
created_at: rawMenu.created_at,
updated_at: rawMenu.updated_at,
children: rawMenu.children ? rawMenu.children.map(transformRawMenu) : []
}
}
// 将菜单数据数组转换为列表格式
const transformToMenuList = (rawMenus: RawMenu[]): MenuListData => {
const transformedMenus = rawMenus.map(transformRawMenu)
// 递归收集所有菜单项
const collectAllMenus = (menu: Menu): Menu[] => {
const result = [menu]
if (menu.children && menu.children.length > 0) {
menu.children.forEach(child => {
result.push(...collectAllMenus(child))
})
}
return result
}
// 收集所有菜单项
const allMenus: Menu[] = []
transformedMenus.forEach(menu => {
allMenus.push(...collectAllMenus(menu))
})
return {
list: allMenus,
total: allMenus.length,
page: 1,
page_size: allMenus.length
}
}
// 构建菜单树结构
const buildMenuTree = (rawMenus: RawMenu[]): Menu[] => {
const transformedMenus = rawMenus.map(transformRawMenu)
// 创建菜单映射
const menuMap = new Map<number, Menu>()
transformedMenus.forEach(menu => {
menuMap.set(menu.menu_id, { ...menu, children: [] })
})
// 构建树结构
const rootMenus: Menu[] = []
transformedMenus.forEach(menu => {
const menuItem = menuMap.get(menu.menu_id)!
if (menu.parent_id && menuMap.has(menu.parent_id)) {
const parent = menuMap.get(menu.parent_id)!
if (!parent.children) parent.children = []
parent.children.push(menuItem)
} else {
rootMenus.push(menuItem)
}
})
return rootMenus
}
// 菜单API
export const menuApi = {
// 获取菜单列表
getMenuList: async (params?: MenuListParams): Promise<MenuListResponse> => {
try {
const response = await request.get<ApiResponse<RawMenu[]>>('/api/menus/', { params })
if (response.success && response.data && Array.isArray(response.data)) {
const menuListData = transformToMenuList(response.data)
return {
success: true,
code: response.code,
message: response.message,
data: menuListData
}
}
return {
success: false,
code: response.code || 500,
message: response.message || '获取菜单数据失败',
data: {
list: [],
total: 0,
page: 1,
page_size: 10
}
}
} catch (error) {
console.error('获取菜单列表失败:', error)
return {
success: false,
code: 500,
message: '网络请求失败',
data: {
list: [],
total: 0,
page: 1,
page_size: 10
}
}
}
},
// 获取菜单树
getMenuTree: async (): Promise<MenuTreeResponse> => {
try {
const response = await request.get<ApiResponse<RawMenu[]>>('/api/menus/tree/')
if (response.success && response.data && Array.isArray(response.data)) {
// 调试检查原始数据中的status值
if (response.data.length > 0) {
console.log('🔍 原始菜单数据status检查前5条:', response.data.slice(0, 5).map((m: RawMenu) => ({
menu_name: m.menu_name,
menu_id: m.menu_id,
status: m.status,
statusType: typeof m.status
})))
}
// 后端已经返回树形结构,直接转换数据格式即可
const transformedData = response.data.map(transformRawMenu)
// 调试检查转换后的status值
if (transformedData.length > 0) {
console.log('🔍 转换后菜单数据status检查前5条:', transformedData.slice(0, 5).map((m: Menu) => ({
menu_name: m.menu_name,
menu_id: m.menu_id,
status: m.status,
statusType: typeof m.status
})))
}
return {
success: true,
code: response.code,
message: response.message,
data: transformedData
}
}
return {
success: false,
code: response.code || 500,
message: response.message || '获取菜单树失败',
data: []
}
} catch (error) {
console.error('获取菜单树失败:', error)
return {
success: false,
code: 500,
message: '网络请求失败',
data: []
}
}
},
// 获取单个菜单
getMenu: async (id: number): Promise<ApiResponse<Menu>> => {
try {
const response = await request.get<ApiResponse<RawMenu>>(`/api/menus/${id}/`)
if (response.success && response.data) {
const transformedMenu = transformRawMenu(response.data)
return {
success: true,
code: response.code,
message: response.message,
data: transformedMenu
}
}
return response as ApiResponse<Menu>
} catch (error) {
console.error('获取菜单详情失败:', error)
return {
success: false,
code: 500,
message: '网络请求失败',
data: {} as Menu
}
}
},
// 创建菜单
createMenu: (data: CreateMenuParams): Promise<ApiResponse<Menu>> => {
return request.post('/api/menus/create/', data)
},
// 更新菜单
updateMenu: (id: number, data: UpdateMenuParams): Promise<ApiResponse<Menu>> => {
return request.put(`/api/menus/${id}/update/`, data)
},
// 删除菜单
deleteMenu: (id: number): Promise<ApiResponse<any>> => {
return request.delete(`/api/menus/${id}/delete/`)
},
// 批量删除菜单
batchDeleteMenus: (ids: number[]): Promise<ApiResponse<any>> => {
return request.post('/api/menus/batch-delete/', { menu_ids: ids })
}
}

View File

@@ -0,0 +1,87 @@
import { request } from '@/utils/hertz_request'
// 用户端通知模块 API 类型定义
export interface UserNoticeListItem {
notice: number
title: string
notice_type_display: string
priority_display: string
is_top: boolean
publish_time: string
is_read: boolean
read_time: string | null
is_starred: boolean
starred_time: string | null
is_expired: boolean
created_at: string
}
export interface UserNoticeListData {
notices: UserNoticeListItem[]
pagination: {
current_page: number
page_size: number
total_pages: number
total_count: number
has_next: boolean
has_previous: boolean
}
statistics: {
total_count: number
unread_count: number
starred_count: number
}
}
export interface ApiResponse<T = any> {
success: boolean
code: number
message: string
data: T
}
export interface UserNoticeDetailData {
notice: number
title: string
content: string
notice_type_display: string
priority_display: string
attachment_url: string | null
publish_time: string
expire_time: string
is_top: boolean
is_expired: boolean
publisher_name: string | null
is_read: boolean
read_time: string
is_starred: boolean
starred_time: string | null
created_at: string
updated_at: string
}
export const noticeUserApi = {
// 查看通知列表
list: (params?: { page?: number; page_size?: number }): Promise<ApiResponse<UserNoticeListData>> =>
request.get('/api/notice/user/list/', { params }),
// 查看通知详情
detail: (notice_id: number | string): Promise<ApiResponse<UserNoticeDetailData>> =>
request.get(`/api/notice/user/detail/${notice_id}/`),
// 标记通知已读
markRead: (notice_id: number | string): Promise<ApiResponse<null>> =>
request.post('/api/notice/user/mark-read/', { notice_id }),
// 批量标记通知已读
batchMarkRead: (notice_ids: Array<number | string>): Promise<ApiResponse<{ updated_count: number }>> =>
request.post('/api/notice/user/batch-mark-read/', { notice_ids }),
// 用户获取通知统计
statistics: (): Promise<ApiResponse<{ total_count: number; unread_count: number; read_count: number; starred_count: number; type_statistics?: Record<string, number>; priority_statistics?: Record<string, number> }>> =>
request.get('/api/notice/user/statistics/'),
// 收藏/取消收藏通知
toggleStar: (notice_id: number | string, is_starred: boolean): Promise<ApiResponse<null>> =>
request.post('/api/notice/user/toggle-star/', { notice_id, is_starred }),
}

View File

@@ -0,0 +1,31 @@
import { request } from '@/utils/hertz_request'
// 修改密码接口参数
export interface ChangePasswordParams {
old_password: string
new_password: string
confirm_password: string
}
// 重置密码接口参数
export interface ResetPasswordParams {
email: string
email_code: string
new_password: string
confirm_password: string
}
// 修改密码
export const changePassword = (params: ChangePasswordParams) => {
return request.post('/api/auth/password/change/', params)
}
// 重置密码
export const resetPassword = (params: ResetPasswordParams) => {
return request.post('/api/auth/password/reset/', params)
}
// 发送重置密码邮箱验证码
export const sendResetPasswordCode = (email: string) => {
return request.post('/api/auth/password/reset/code/', { email })
}

View File

@@ -0,0 +1,130 @@
import { request } from '@/utils/hertz_request'
// 权限接口类型定义
export interface Permission {
permission_id: number
permission_name: string
permission_code: string
permission_type: 'menu' | 'button' | 'api'
parent_id?: number
path?: string
icon?: string
sort_order?: number
description?: string
status?: number
children?: Permission[]
}
// 角色接口类型定义
export interface Role {
role_id: number
role_name: string
role_code: string
description?: string
status?: number
created_at?: string
updated_at?: string
permissions?: Permission[]
}
// API响应基础结构
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
}
// 角色列表数据结构
export interface RoleListData {
list: Role[]
total: number
page: number
page_size: number
}
// 角色列表响应类型
export type RoleListResponse = ApiResponse<RoleListData>
// 角色列表查询参数
export interface RoleListParams {
page?: number
page_size?: number
search?: string
status?: number
}
// 创建角色参数
export interface CreateRoleParams {
role_name: string
role_code: string
description?: string
status?: number
}
// 更新角色参数
export type UpdateRoleParams = Partial<CreateRoleParams>
// 角色权限分配参数
export interface AssignRolePermissionsParams {
role_id: number
menu_ids: number[]
user_type?: number
department_id?: number
}
// 权限列表响应类型
export type PermissionListResponse = ApiResponse<Permission[]>
// 角色API
export const roleApi = {
// 获取角色列表
getRoleList: (params?: RoleListParams): Promise<RoleListResponse> => {
return request.get('/api/roles/', { params })
},
// 获取单个角色
getRole: (id: number): Promise<ApiResponse<Role>> => {
return request.get(`/api/roles/${id}/`)
},
// 创建角色
createRole: (data: CreateRoleParams): Promise<ApiResponse<Role>> => {
return request.post('/api/roles/create/', data)
},
// 更新角色
updateRole: (id: number, data: UpdateRoleParams): Promise<ApiResponse<Role>> => {
return request.put(`/api/roles/${id}/update/`, data)
},
// 删除角色
deleteRole: (id: number): Promise<ApiResponse<any>> => {
return request.delete(`/api/roles/${id}/delete/`)
},
// 批量删除角色
batchDeleteRoles: (ids: number[]): Promise<ApiResponse<any>> => {
return request.post('/api/roles/batch-delete/', { role_ids: ids })
},
// 获取角色权限
getRolePermissions: (id: number): Promise<ApiResponse<Permission[]>> => {
return request.get(`/api/roles/${id}/menus/`)
},
// 分配角色权限
assignRolePermissions: (data: AssignRolePermissionsParams): Promise<ApiResponse<any>> => {
return request.post(`/api/roles/assign-menus/`, data)
},
// 获取所有权限列表
getPermissionList: (): Promise<PermissionListResponse> => {
return request.get('/api/menus/')
},
// 获取权限树
getPermissionTree: (): Promise<PermissionListResponse> => {
return request.get('/api/menus/tree/')
}
}

View File

@@ -0,0 +1,114 @@
import { request } from '@/utils/hertz_request'
// 通用响应类型
export interface ApiResponse<T> {
success: boolean
code: number
message: string
data: T
}
// 1. 系统信息
export interface SystemInfo {
hostname: string
platform: string
architecture: string
boot_time: string
uptime: string
}
// 2. CPU 信息
export interface CpuInfo {
cpu_count: number
cpu_percent: number
cpu_freq: {
current: number
min: number
max: number
}
load_avg: number[]
}
// 3. 内存信息
export interface MemoryInfo {
total: number
available: number
used: number
percent: number
free: number
}
// 4. 磁盘信息
export interface DiskInfo {
device: string
mountpoint: string
fstype: string
total: number
used: number
free: number
percent: number
}
// 5. 网络信息
export interface NetworkInfo {
interface: string
bytes_sent: number
bytes_recv: number
packets_sent: number
packets_recv: number
}
// 6. 进程信息
export interface ProcessInfo {
pid: number
name: string
status: string
cpu_percent: number
memory_percent: number
memory_info: {
rss: number
vms: number
}
create_time: string
cmdline: string[]
}
// 7. GPU 信息
export interface GpuInfoItem {
id: number
name: string
load: number
memory_total: number
memory_used: number
memory_util: number
temperature: number
}
export interface GpuInfoResponse {
gpu_available: boolean
gpu_info?: GpuInfoItem[]
message?: string
timestamp: string
}
// 8. 综合监测信息
export interface MonitorData {
system: SystemInfo
cpu: CpuInfo
memory: MemoryInfo
disks: DiskInfo[]
network: NetworkInfo[]
processes: ProcessInfo[]
gpus: Array<{ gpu_available: boolean; message?: string; timestamp: string }>
}
export const systemMonitorApi = {
getSystem: (): Promise<ApiResponse<SystemInfo>> => request.get('/api/system/system/'),
getCpu: (): Promise<ApiResponse<CpuInfo>> => request.get('/api/system/cpu/'),
getMemory: (): Promise<ApiResponse<MemoryInfo>> => request.get('/api/system/memory/'),
getDisks: (): Promise<ApiResponse<DiskInfo[]>> => request.get('/api/system/disks/'),
getNetwork: (): Promise<ApiResponse<NetworkInfo[]>> => request.get('/api/system/network/'),
getProcesses: (): Promise<ApiResponse<ProcessInfo[]>> => request.get('/api/system/processes/'),
getGpu: (): Promise<ApiResponse<GpuInfoResponse>> => request.get('/api/system/gpu/'),
getMonitor: (): Promise<ApiResponse<MonitorData>> => request.get('/api/system/monitor/'),
}

View File

@@ -0,0 +1,115 @@
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)
},
// 分配用户角色
assignRoles: (data: AssignRolesParams): Promise<ApiResponse<any>> => {
return request.post('/api/users/assign-roles/', data)
},
// 获取所有角色列表
getRoleList: (): Promise<ApiResponse<Role[]>> => {
return request.get('/api/roles/')
}
}

View File

@@ -0,0 +1,428 @@
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
}
}
// 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 }> {
return request.get('/api/yolo/models/enabled/')
},
// 获取模型详情
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 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

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,55 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script setup lang="ts">
defineProps<{ msg: string }>()
</script>
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -0,0 +1,159 @@
export default {
common: {
confirm: 'Confirm',
cancel: 'Cancel',
save: 'Save',
delete: 'Delete',
edit: 'Edit',
add: 'Add',
search: 'Search',
reset: 'Reset',
loading: 'Loading...',
noData: 'No Data',
success: 'Success',
error: 'Error',
warning: 'Warning',
info: 'Info',
},
nav: {
home: 'Home',
dashboard: 'Dashboard',
user: 'User Management',
role: 'Role Management',
menu: 'Menu Management',
settings: 'System Settings',
profile: 'Profile',
logout: 'Logout',
},
login: {
title: 'Login',
username: 'Username',
password: 'Password',
login: 'Login',
forgotPassword: 'Forgot Password?',
rememberMe: 'Remember Me',
},
success: {
// General success messages
operationSuccess: 'Operation Successful',
saveSuccess: 'Save Successful',
deleteSuccess: 'Delete Successful',
updateSuccess: 'Update Successful',
// Login and registration related success messages
loginSuccess: 'Login Successful',
registerSuccess: 'Registration Successful! Please Login',
logoutSuccess: 'Logout Successful',
emailCodeSent: 'Verification Code Sent to Your Email',
// User management related success messages
userCreated: 'User Created Successfully',
userUpdated: 'User Information Updated Successfully',
userDeleted: 'User Deleted Successfully',
roleAssigned: 'Role Assigned Successfully',
// Other operation success messages
uploadSuccess: 'File Upload Successful',
downloadSuccess: 'File Download Successful',
copySuccess: 'Copy Successful',
},
error: {
// General errors
// 404: 'Page Not Found',
403: 'Access Denied, Please Contact Administrator',
500: 'Internal Server Error, Please Try Again Later',
networkError: 'Network Connection Failed, Please Check Network Settings',
timeout: 'Request Timeout, Please Try Again Later',
// Login related errors
loginFailed: 'Login Failed, Please Check Username and Password',
usernameRequired: 'Please Enter Username',
passwordRequired: 'Please Enter Password',
captchaRequired: 'Please Enter Captcha',
captchaError: 'Captcha Error, Please Re-enter (Case Sensitive)',
captchaExpired: 'Captcha Expired, Please Refresh and Re-enter',
accountLocked: 'Account Locked, Please Contact Administrator',
accountDisabled: 'Account Disabled, Please Contact Administrator',
passwordExpired: 'Password Expired, Please Change Password',
loginAttemptsExceeded: 'Too Many Login Attempts, Account Temporarily Locked',
// Registration related errors
registerFailed: 'Registration Failed, Please Check Input Information',
usernameExists: 'Username Already Exists, Please Choose Another',
emailExists: 'Email Already Registered, Please Use Another Email',
phoneExists: 'Phone Number Already Registered, Please Use Another',
emailFormatError: 'Invalid Email Format, Please Enter Valid Email',
phoneFormatError: 'Invalid Phone Format, Please Enter 11-digit Phone Number',
passwordTooWeak: 'Password Too Weak, Please Include Uppercase, Lowercase, Numbers and Special Characters',
passwordMismatch: 'Passwords Do Not Match',
emailCodeError: 'Email Verification Code Error or Expired',
emailCodeRequired: 'Please Enter Email Verification Code',
emailCodeLength: 'Verification Code Must Be 6 Digits',
emailRequired: 'Please Enter Email',
usernameLength: 'Username Length Must Be 3-20 Characters',
passwordLength: 'Password Length Must Be 6-20 Characters',
confirmPasswordRequired: 'Please Confirm Password',
phoneRequired: 'Please Enter Phone Number',
realNameRequired: 'Please Enter Real Name',
realNameLength: 'Name Length Must Be 2-10 Characters',
// Permission related errors
accessDenied: 'Access Denied, You Do Not Have Permission to Perform This Action',
roleNotFound: 'Role Not Found or Deleted',
permissionDenied: 'Permission Denied, Cannot Perform This Action',
tokenExpired: 'Login Expired, Please Login Again',
tokenInvalid: 'Invalid Login Status, Please Login Again',
// User management related errors
userNotFound: 'User Not Found or Deleted',
userCreateFailed: 'Failed to Create User, Please Check Input Information',
userUpdateFailed: 'Failed to Update User Information',
userDeleteFailed: 'Failed to Delete User, User May Be In Use',
cannotDeleteSelf: 'Cannot Delete Your Own Account',
cannotDeleteAdmin: 'Cannot Delete Administrator Account',
// Department management related errors
departmentNotFound: 'Department Not Found or Deleted',
departmentNameExists: 'Department Name Already Exists',
departmentHasUsers: 'Department Has Users, Cannot Delete',
departmentCreateFailed: 'Failed to Create Department',
departmentUpdateFailed: 'Failed to Update Department Information',
departmentDeleteFailed: 'Failed to Delete Department',
// Role management related errors
roleNameExists: 'Role Name Already Exists',
roleCreateFailed: 'Failed to Create Role',
roleUpdateFailed: 'Failed to Update Role Information',
roleDeleteFailed: 'Failed to Delete Role',
roleInUse: 'Role In Use, Cannot Delete',
// File upload related errors
fileUploadFailed: 'File Upload Failed',
fileSizeExceeded: 'File Size Exceeded Limit',
fileTypeNotSupported: 'File Type Not Supported',
fileRequired: 'Please Select File to Upload',
// Data validation related errors
invalidInput: 'Invalid Input Data Format',
requiredFieldMissing: 'Required Field Cannot Be Empty',
fieldTooLong: 'Input Content Exceeds Length Limit',
fieldTooShort: 'Input Content Length Insufficient',
invalidDate: 'Invalid Date Format',
invalidNumber: 'Invalid Number Format',
// Operation related errors
operationFailed: 'Operation Failed, Please Try Again Later',
saveSuccess: 'Save Successful',
saveFailed: 'Save Failed, Please Check Input Information',
deleteSuccess: 'Delete Successful',
deleteFailed: 'Delete Failed, Please Try Again Later',
updateSuccess: 'Update Successful',
updateFailed: 'Update Failed, Please Check Input Information',
// System related errors
systemMaintenance: 'System Under Maintenance, Please Visit Later',
serviceUnavailable: 'Service Temporarily Unavailable, Please Try Again Later',
databaseError: 'Database Connection Error, Please Contact Technical Support',
configError: 'System Configuration Error, Please Contact Administrator',
},
}

View File

@@ -0,0 +1,18 @@
import { createI18n } from 'vue-i18n'
import zhCN from './zh-CN'
import enUS from './en-US'
const messages = {
'zh-CN': zhCN,
'en-US': enUS,
}
export const i18n = createI18n({
locale: 'zh-CN',
fallbackLocale: 'en-US',
messages,
legacy: false,
globalInjection: true,
})
export default i18n

View File

@@ -0,0 +1,172 @@
export default {
common: {
confirm: '确定',
cancel: '取消',
save: '保存',
delete: '删除',
edit: '编辑',
add: '添 加',
search: '搜索',
reset: '重置',
loading: '加载中...',
noData: '暂无数据',
success: '成功',
error: '错误',
warning: '警告',
info: '提示',
},
nav: {
home: '首页',
dashboard: '仪表板',
user: '用户管理',
role: '角色管理',
menu: '菜单管理',
settings: '系统设置',
profile: '个人资料',
logout: '退出登录',
},
login: {
title: '登录',
username: '用户名',
password: '密码',
login: '登录',
forgotPassword: '忘记密码?',
rememberMe: '记住我',
},
register: {
title: '注册',
username: '用户名',
email: '邮箱',
password: '密码',
confirmPassword: '确认密码',
register: '注册',
agreement: '我已阅读并同意',
userAgreement: '用户协议',
privacyPolicy: '隐私政策',
hasAccount: '已有账号?',
goToLogin: '立即登录',
},
success: {
// 通用成功提示
operationSuccess: '操作成功',
saveSuccess: '保存成功',
deleteSuccess: '删除成功',
updateSuccess: '更新成功',
// 登录注册相关成功提示
loginSuccess: '登录成功',
registerSuccess: '注册成功!请前往登录',
logoutSuccess: '退出登录成功',
emailCodeSent: '验证码已发送到您的邮箱',
// 用户管理相关成功提示
userCreated: '用户创建成功',
userUpdated: '用户信息更新成功',
userDeleted: '用户删除成功',
roleAssigned: '角色分配成功',
// 其他操作成功提示
uploadSuccess: '文件上传成功',
downloadSuccess: '文件下载成功',
copySuccess: '复制成功',
},
error: {
// 通用错误
// 404: '页面未找到',
403: '权限不足,请联系管理员',
500: '服务器内部错误,请稍后重试',
networkError: '网络连接失败,请检查网络设置',
timeout: '请求超时,请稍后重试',
// 登录相关错误
loginFailed: '登录失败,请检查用户名和密码',
usernameRequired: '请输入用户名',
passwordRequired: '请输入密码',
captchaRequired: '请输入验证码',
captchaError: '验证码错误,请重新输入(区分大小写)',
captchaExpired: '验证码已过期,请刷新后重新输入',
accountLocked: '账户已被锁定,请联系管理员',
accountDisabled: '账户已被禁用,请联系管理员',
passwordExpired: '密码已过期,请修改密码',
loginAttemptsExceeded: '登录尝试次数过多,账户已被临时锁定',
// 注册相关错误
registerFailed: '注册失败,请检查输入信息',
usernameExists: '用户名已存在,请选择其他用户名',
emailExists: '邮箱已被注册,请使用其他邮箱',
phoneExists: '手机号已被注册,请使用其他手机号',
emailFormatError: '邮箱格式不正确,请输入有效的邮箱地址',
phoneFormatError: '手机号格式不正确请输入11位手机号',
passwordTooWeak: '密码强度不足,请包含大小写字母、数字和特殊字符',
passwordMismatch: '两次输入的密码不一致',
emailCodeError: '邮箱验证码错误或已过期',
emailCodeRequired: '请输入邮箱验证码',
emailCodeLength: '验证码长度为6位',
emailRequired: '请输入邮箱',
usernameLength: '用户名长度为3-20个字符',
passwordLength: '密码长度为6-20个字符',
confirmPasswordRequired: '请确认密码',
phoneRequired: '请输入手机号',
realNameRequired: '请输入真实姓名',
realNameLength: '姓名长度为2-10个字符',
// 权限相关错误
accessDenied: '访问被拒绝,您没有执行此操作的权限',
roleNotFound: '角色不存在或已被删除',
permissionDenied: '权限不足,无法执行此操作',
tokenExpired: '登录已过期,请重新登录',
tokenInvalid: '登录状态无效,请重新登录',
// 用户管理相关错误
userNotFound: '用户不存在或已被删除',
userCreateFailed: '创建用户失败,请检查输入信息',
userUpdateFailed: '更新用户信息失败',
userDeleteFailed: '删除用户失败,该用户可能正在使用中',
cannotDeleteSelf: '不能删除自己的账户',
cannotDeleteAdmin: '不能删除管理员账户',
// 部门管理相关错误
departmentNotFound: '部门不存在或已被删除',
departmentNameExists: '部门名称已存在',
departmentHasUsers: '部门下还有用户,无法删除',
departmentCreateFailed: '创建部门失败',
departmentUpdateFailed: '更新部门信息失败',
departmentDeleteFailed: '删除部门失败',
// 角色管理相关错误
roleNameExists: '角色名称已存在',
roleCreateFailed: '创建角色失败',
roleUpdateFailed: '更新角色信息失败',
roleDeleteFailed: '删除角色失败',
roleInUse: '角色正在使用中,无法删除',
// 文件上传相关错误
fileUploadFailed: '文件上传失败',
fileSizeExceeded: '文件大小超出限制',
fileTypeNotSupported: '不支持的文件类型',
fileRequired: '请选择要上传的文件',
// 数据验证相关错误
invalidInput: '输入数据格式不正确',
requiredFieldMissing: '必填字段不能为空',
fieldTooLong: '输入内容超出长度限制',
fieldTooShort: '输入内容长度不足',
invalidDate: '日期格式不正确',
invalidNumber: '数字格式不正确',
// 操作相关错误
operationFailed: '操作失败,请稍后重试',
saveSuccess: '保存成功',
saveFailed: '保存失败,请检查输入信息',
deleteSuccess: '删除成功',
deleteFailed: '删除失败,请稍后重试',
updateSuccess: '更新成功',
updateFailed: '更新失败,请检查输入信息',
// 系统相关错误
systemMaintenance: '系统正在维护中,请稍后访问',
serviceUnavailable: '服务暂时不可用,请稍后重试',
databaseError: '数据库连接错误,请联系技术支持',
configError: '系统配置错误,请联系管理员',
},
}

View File

@@ -0,0 +1,47 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { i18n } from './locales'
import { checkEnvironmentVariables, validateEnvironment } from './utils/hertz_env'
import './styles/index.scss'
// 导入Ant Design Vue
import 'ant-design-vue/dist/antd.css'
// 开发环境检查
if (import.meta.env.DEV) {
checkEnvironmentVariables()
validateEnvironment()
}
// 创建Vue应用实例
const app = createApp(App)
// 使用Pinia状态管理
const pinia = createPinia()
app.use(pinia)
// 使用路由
app.use(router)
// 使用国际化
app.use(i18n)
// 初始化应用设置
import { useAppStore } from './stores/hertz_app'
const appStore = useAppStore()
appStore.initAppSettings()
// 检查用户认证状态
import { useUserStore } from './stores/hertz_user'
const userStore = useUserStore()
userStore.checkAuth()
// 初始化主题(必须在挂载前加载)
import { useThemeStore } from './stores/hertz_theme'
const themeStore = useThemeStore()
themeStore.loadTheme()
// 挂载应用
app.mount('#app')

View File

@@ -0,0 +1,69 @@
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
}
export const aiApi = {
listChats: (params?: { query?: string; page?: number; page_size?: number }): Promise<ApiResponse<ChatListData>> =>
request.get('/api/ai/chats/', { params, showError: false }),
createChat: (body?: { title?: string }): Promise<ApiResponse<AIChatDetail>> =>
request.post('/api/ai/chats/create/', body || { title: '新对话' }),
getChatDetail: (chatId: number): Promise<ApiResponse<ChatDetailData>> =>
request.get(`/api/ai/chats/${chatId}/`),
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),
}

View File

@@ -0,0 +1,231 @@
import type { RouteRecordRaw } from 'vue-router'
import { defineAsyncComponent } from 'vue'
// 统一的菜单项配置接口
export interface UserMenuConfig {
key: string
label: string
icon?: string
path: string
component: string // 组件路径,相对于 @/views/user_pages/
children?: UserMenuConfig[]
disabled?: boolean
meta?: {
title?: string
requiresAuth?: boolean
roles?: string[]
[key: string]: any
}
}
// 菜单项接口定义(用于前端显示)
export interface MenuItem {
key: string
label: string
icon?: string
path?: string
children?: MenuItem[]
disabled?: boolean
}
// 统一配置 - 同时用于菜单和路由
export const userMenuConfigs: UserMenuConfig[] = [
{
key: 'dashboard',
label: '首页',
icon: 'DashboardOutlined',
path: '/dashboard',
component: 'index.vue',
meta: { title: '用户首页', requiresAuth: true }
},
{
key: 'profile',
label: '个人信息',
icon: 'UserOutlined',
path: '/user/profile',
component: 'Profile.vue',
meta: { title: '个人信息', requiresAuth: true }
},
{
key: 'documents',
label: '文档管理',
icon: 'FileTextOutlined',
path: '/user/documents',
component: 'Documents.vue',
meta: { title: '文档管理', requiresAuth: true }
},
{
key: 'messages',
label: '消息中心',
icon: 'MessageOutlined',
path: '/user/messages',
component: 'Messages.vue',
meta: { title: '消息中心', requiresAuth: true }
},
{
key: 'system-monitor',
label: '系统监控',
icon: 'DashboardOutlined',
path: '/user/system-monitor',
component: 'SystemMonitor.vue',
meta: { title: '系统监控', requiresAuth: true }
},
{
key: 'ai-chat',
label: 'AI助手',
icon: 'MessageOutlined',
path: '/user/ai-chat',
component: 'AiChat.vue',
meta: { title: 'AI助手', requiresAuth: true }
},
]
// 显式组件映射 - 避免Vite动态导入限制
const explicitComponentMap: Record<string, any> = {
'index.vue': defineAsyncComponent(() => import('@/views/user_pages/index.vue')),
'Profile.vue': defineAsyncComponent(() => import('@/views/user_pages/Profile.vue')),
'Documents.vue': defineAsyncComponent(() => import('@/views/user_pages/Documents.vue')),
'Messages.vue': defineAsyncComponent(() => import('@/views/user_pages/Messages.vue')),
'SystemMonitor.vue': defineAsyncComponent(() => import('@/views/user_pages/SystemMonitor.vue')),
'AiChat.vue': defineAsyncComponent(() => import('@/views/user_pages/AiChat.vue')),
}
// 自动生成菜单项(用于前端显示)
export const userMenuItems: MenuItem[] = userMenuConfigs.map(config => ({
key: config.key,
label: config.label,
icon: config.icon,
path: config.path,
disabled: config.disabled,
children: config.children?.map(child => ({
key: child.key,
label: child.label,
icon: child.icon,
path: child.path,
disabled: child.disabled
}))
}))
// 组件映射表 - 用于解决Vite动态导入限制
const componentMap: Record<string, () => Promise<any>> = {
'index.vue': () => import('@/views/user_pages/index.vue'),
'Profile.vue': () => import('@/views/user_pages/Profile.vue'),
'Documents.vue': () => import('@/views/user_pages/Documents.vue'),
'Messages.vue': () => import('@/views/user_pages/Messages.vue'),
'SystemMonitor.vue': () => import('@/views/user_pages/SystemMonitor.vue'),
'AiChat.vue': () => import('@/views/user_pages/AiChat.vue'),
}
// 自动生成路由配置
export const userRoutes: RouteRecordRaw[] = userMenuConfigs.map(config => {
const route: RouteRecordRaw = {
path: config.path,
name: `User${config.key.charAt(0).toUpperCase() + config.key.slice(1)}`,
component: componentMap[config.component] || (() => import('@/views/NotFound.vue')),
meta: {
title: config.meta?.title || config.label,
requiresAuth: config.meta?.requiresAuth ?? true,
...config.meta
}
}
if (config.children && config.children.length > 0) {
route.children = config.children.map(child => ({
path: child.path,
name: `User${child.key.charAt(0).toUpperCase() + child.key.slice(1)}`,
component: componentMap[child.component] || (() => import('@/views/NotFound.vue')),
meta: {
title: child.meta?.title || child.label,
requiresAuth: child.meta?.requiresAuth ?? true,
...child.meta
}
}))
}
return route
})
// 根据菜单项生成路由路径
export function getMenuPath(menuKey: string): string {
const findPath = (items: MenuItem[], key: string): string | null => {
for (const item of items) {
if (item.key === key && item.path) return item.path
if (item.children) {
const childPath = findPath(item.children, key)
if (childPath) return childPath
}
}
return null
}
return findPath(userMenuItems, menuKey) || '/dashboard'
}
// 获取菜单的面包屑路径
export function getMenuBreadcrumb(menuKey: string): string[] {
const findBreadcrumb = (items: MenuItem[], key: string, path: string[] = []): string[] | null => {
for (const item of items) {
const currentPath = [...path, item.label]
if (item.key === menuKey) return currentPath
if (item.children) {
const childPath = findBreadcrumb(item.children, key, currentPath)
if (childPath) return childPath
}
}
return null
}
return findBreadcrumb(userMenuItems, menuKey) || ['仪表盘']
}
// 自动生成组件映射(基于配置和显式映射)
export const generateComponentMap = () => {
const map: Record<string, any> = {}
const processConfigs = (configs: UserMenuConfig[]) => {
configs.forEach(config => {
if (explicitComponentMap[config.component]) {
map[config.key] = explicitComponentMap[config.component]
} else {
map[config.key] = defineAsyncComponent(() => import('@/views/NotFound.vue'))
}
if (config.children) processConfigs(config.children)
})
}
processConfigs(userMenuConfigs)
return map
}
// 导出自动生成的组件映射
export const userComponentMap = generateComponentMap()
// 根据用户权限过滤菜单项
export const getFilteredUserMenuItems = (userRoles: string[], userPermissions: string[]): MenuItem[] => {
return userMenuConfigs
.filter(config => {
if (!config.meta?.roles || config.meta.roles.length === 0) return true
return config.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
})
.map(config => ({
key: config.key,
label: config.label,
icon: config.icon,
path: config.path,
disabled: config.disabled,
children: config.children?.filter(child => {
if (!child.meta?.roles || child.meta.roles.length === 0) return true
return child.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
}).map(child => ({
key: child.key,
label: child.label,
icon: child.icon,
path: child.path,
disabled: child.disabled
}))
}))
}
// 检查用户是否有访问特定菜单的权限
export const hasUserMenuPermission = (menuKey: string, userRoles: string[]): boolean => {
const menuConfig = userMenuConfigs.find(config => config.key === menuKey)
if (!menuConfig) return false
if (!menuConfig.meta?.roles || menuConfig.meta.roles.length === 0) return true
return menuConfig.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
}

View File

@@ -0,0 +1,261 @@
<template>
<div class="ai-chat-page">
<a-page-header title="AI助手" sub-title=" AI 进行智能对话">
<template #extra>
<a-space>
<a-input-search v-model:value="query" placeholder="搜索会话标题" style="width: 240px" @search="fetchChats" />
<a-button @click="fetchChats" :loading="loadingChats">
<ReloadOutlined />
刷新
</a-button>
<a-button type="primary" @click="createChat" :loading="creating">
<PlusOutlined />
新建对话
</a-button>
</a-space>
</template>
</a-page-header>
<a-row :gutter="16">
<!-- 左侧会话列表 -->
<a-col :xs="24" :md="8" :lg="6">
<a-card title="我的对话" bordered>
<a-list
:data-source="chatList"
item-layout="horizontal"
:loading="loadingChats"
:pagination="{ pageSize: pageSize, total: total, current: page, onChange: onPageChange }"
>
<template #renderItem="{ item }">
<a-list-item :class="{ active: item.id === currentChatId }" @click="selectChat(item.id)">
<a-list-item-meta>
<template #title>
<div class="chat-title-row">
<span class="chat-title">{{ item.title }}</span>
<a-space>
<a-button size="small" type="text" @click.stop="openRename(item)">
<EditOutlined />
</a-button>
<a-popconfirm title="确认删除该对话?" @confirm="deleteChat(item.id)">
<a-button size="small" danger type="text" @click.stop>
<DeleteOutlined />
</a-button>
</a-popconfirm>
</a-space>
</div>
</template>
<template #description>
<div class="chat-desc">{{ item.latest_message || '暂无消息' }}</div>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
<!-- 右侧消息区 -->
<a-col :xs="24" :md="16" :lg="18">
<a-card :title="currentChat?.title || '请选择或新建对话'" bordered class="chat-card">
<div class="messages" ref="messagesEl">
<template v-if="messages.length">
<div v-for="m in messages" :key="m.id" :class="['msg', m.role]">
<div class="bubble">
<div class="content" v-html="renderContent(m.content)"></div>
<div class="time">{{ formatTime(m.created_at) }}</div>
</div>
</div>
</template>
<a-empty v-else description="暂无消息" />
</div>
<template #footer>
<div class="composer">
<a-textarea v-model:value="input" :rows="3" placeholder="输入你的问题..." :disabled="!currentChatId" />
<a-space style="margin-top: 8px;">
<a-button type="primary" :disabled="!canSend" :loading="sending" @click="send">
<SendOutlined />
发送
</a-button>
</a-space>
</div>
</template>
</a-card>
</a-col>
</a-row>
<!-- 重命名对话 -->
<a-modal v-model:open="renameOpen" title="重命名对话" @ok="doRename" :confirm-loading="renaming">
<a-input v-model:value="renameTitle" placeholder="请输入新标题" />
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, SendOutlined } from '@ant-design/icons-vue'
import dayjs from 'dayjs'
import { aiApi, type AIChatItem, type AIChatDetail, type AIChatMessage } from '@/api/ai'
// 会话列表状态
const chatList = ref<AIChatItem[]>([])
const query = ref('')
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const loadingChats = ref(false)
const creating = ref(false)
// 当前会话与消息
const currentChatId = ref<number | null>(null)
const currentChat = ref<AIChatDetail | null>(null)
const messages = ref<AIChatMessage[]>([])
const loadingMessages = ref(false)
const sending = ref(false)
// 重命名
const renameOpen = ref(false)
const renameTitle = ref('')
const renaming = ref(false)
let renameTargetId: number | null = null
const messagesEl = ref<HTMLElement | null>(null)
const canSend = computed(() => !!currentChatId.value && !!input.value.trim())
const input = ref('')
const fetchChats = async () => {
loadingChats.value = true
try {
const res = await aiApi.listChats({ query: query.value || undefined, page: page.value, page_size: pageSize.value })
if (res.success) {
chatList.value = res.data.chats || []
total.value = res.data.total || 0
// 保持选择
if (!currentChatId.value && chatList.value.length) selectChat(chatList.value[0].id)
} else {
message.error(res.message || '获取对话列表失败')
}
} catch (e: any) {
if (e?.response?.status === 403) {
message.warning('暂无权限访问AI助手请联系管理员开通权限')
} else {
message.error(e?.message || '网络错误')
}
} finally {
loadingChats.value = false
}
}
const onPageChange = (p: number) => { page.value = p; fetchChats() }
const selectChat = async (id: number) => {
if (currentChatId.value === id && messages.value.length) return
currentChatId.value = id
loadingMessages.value = true
try {
const res = await aiApi.getChatDetail(id)
if (res.success) {
currentChat.value = res.data.chat
messages.value = res.data.messages || []
await nextTick(); scrollToBottom()
} else {
message.error(res.message || '获取会话详情失败')
}
} catch (e: any) {
message.error(e?.message || '网络错误')
} finally {
loadingMessages.value = false
}
}
const createChat = async () => {
creating.value = true
try {
const res = await aiApi.createChat({ title: '新对话' })
if (res.success) {
message.success('创建成功')
await fetchChats()
selectChat(res.data.id)
} else {
message.error(res.message || '创建失败')
}
} catch (e: any) {
message.error(e?.message || '网络错误')
} finally {
creating.value = false
}
}
const openRename = (item: AIChatItem) => {
renameTargetId = item.id
renameTitle.value = item.title
renameOpen.value = true
}
const doRename = async () => {
if (!renameTargetId || !renameTitle.value.trim()) { message.warning('标题不能为空'); return }
renaming.value = true
try {
const res = await aiApi.updateChat(renameTargetId, { title: renameTitle.value.trim() })
if (res.success) { message.success('重命名成功'); renameOpen.value = false; await fetchChats() }
else { message.error(res.message || '重命名失败') }
} catch (e: any) { message.error(e?.message || '网络错误') }
finally { renaming.value = false }
}
const deleteChat = async (id: number) => {
try {
const res = await aiApi.deleteChats([id])
if (res.success) {
message.success('删除成功')
await fetchChats()
if (currentChatId.value === id) { currentChatId.value = null; currentChat.value = null; messages.value = [] }
} else {
message.error(res.message || '删除失败')
}
} catch (e: any) { message.error(e?.message || '网络错误') }
}
const send = async () => {
if (!canSend.value || !currentChatId.value) return
const content = input.value.trim()
sending.value = true
try {
const res = await aiApi.sendMessage(currentChatId.value, { content })
if (res.success) {
messages.value.push(res.data.user_message, res.data.ai_message)
input.value = ''
await nextTick(); scrollToBottom(); fetchChats() // 更新列表预览
} else {
message.error(res.message || '发送失败')
}
} catch (e: any) { message.error(e?.message || '网络错误') }
finally { sending.value = false }
}
const formatTime = (t: string) => dayjs(t).format('YYYY-MM-DD HH:mm:ss')
const renderContent = (c: string) => c.replace(/\n/g, '<br/>')
const scrollToBottom = () => { const el = messagesEl.value; if (el) el.scrollTop = el.scrollHeight }
onMounted(() => { fetchChats() })
</script>
<style scoped lang="scss">
.ai-chat-page { padding: 16px; }
.chat-title-row { display: flex; justify-content: space-between; align-items: center; }
.chat-desc { color: #666; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.messages { min-height: 420px; max-height: 60vh; overflow-y: auto; padding: 8px; background: #fafafa; border-radius: 8px; }
.msg { display: flex; margin-bottom: 12px; }
.msg .bubble { max-width: 80%; padding: 10px 12px; border-radius: 8px; position: relative; }
.msg .time { margin-top: 6px; font-size: 12px; color: #999; }
.msg.user { justify-content: flex-end; }
.msg.user .bubble { background: #e6f7ff; }
.msg.assistant { justify-content: flex-start; }
.msg.assistant .bubble { background: #f6ffed; }
.composer { margin-top: 8px; }
.chat-card :deep(.ant-card-head) { background: #fff; }
.active { background: #f0f7ff; }
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,424 @@
import type { RouteRecordRaw } from "vue-router";
// 角色权限枚举
export enum UserRole {
ADMIN = 'admin',
SYSTEM_ADMIN = 'system_admin',
NORMAL_USER = 'normal_user',
SUPER_ADMIN = 'super_admin'
}
// 统一菜单配置接口 - 只需要在这里配置一次
export interface AdminMenuItem {
key: string; // 菜单唯一标识
title: string; // 菜单标题
icon?: string; // 菜单图标
path: string; // 路由路径
component: string; // 组件路径(相对于@/views/admin_page/
isDefault?: boolean; // 是否为默认路由(首页)
roles?: UserRole[]; // 允许访问的角色,不设置则使用默认管理员角色
permission?: string; // 所需权限标识符
children?: AdminMenuItem[]; // 子菜单
}
// 🎯 统一配置中心 - 只需要在这里修改菜单配置
export const ADMIN_MENU_CONFIG: AdminMenuItem[] = [
{
key: "dashboard",
title: "仪表盘",
icon: "DashboardOutlined",
path: "/admin",
component: "Dashboard.vue",
isDefault: true, // 标记为默认首页
},
{
key: "user-management",
title: "用户管理",
icon: "UserOutlined",
path: "/admin/user-management",
component: "UserManagement.vue",
permission: "system:user:list", // 需要用户列表权限
},
{
key: "department-management",
title: "部门管理",
icon: "SettingOutlined",
path: "/admin/department-management",
component: "DepartmentManagement.vue",
permission: "system:dept:list", // 需要部门列表权限
},
{
key: "menu-management",
title: "菜单管理",
icon: "SettingOutlined",
path: "/admin/menu-management",
component: "MenuManagement.vue",
permission: "system:menu:list", // 需要菜单列表权限
},
{
key: "teacher",
title: "角色管理",
icon: "UserOutlined",
path: "/admin/teacher",
component: "Role.vue",
permission: "system:role:list", // 需要角色列表权限
},
{
key: "notification-management",
title: "通知管理",
icon: "UserOutlined",
path: "/admin/notification-management",
component: "NotificationManagement.vue",
permission: "studio:notice:list", // 需要通知列表权限
},
{
key: "log-management",
title: "日志管理",
icon: "FileSearchOutlined",
path: "/admin/log-management",
component: "LogManagement.vue",
permission: "log.view_operationlog", // 查看操作日志权限
},
{
key: "knowledge-base",
title: "知识库管理",
icon: "DatabaseOutlined",
path: "/admin/knowledge-base",
component: "KnowledgeBaseManagement.vue",
// 菜单访问权限:需要具备文章列表权限
permission: "system:knowledge:article:list",
},
{
key: "yolo-model",
title: "YOLO模型",
icon: "ClusterOutlined",
path: "/admin/yolo-model",
component: "ModelManagement.vue", // 默认显示模型管理页面
// 父菜单不设置权限,由子菜单的权限决定是否显示
children: [
{
key: "model-management",
title: "模型管理",
icon: "RobotOutlined",
path: "/admin/model-management",
component: "ModelManagement.vue",
permission: "system:yolo:model:list",
},
{
key: "alert-level-management",
title: "模型类别管理",
icon: "WarningOutlined",
path: "/admin/alert-level-management",
component: "AlertLevelManagement.vue",
permission: "system:yolo:alert:list",
},
{
key: "alert-processing-center",
title: "告警处理中心",
icon: "BellOutlined",
path: "/admin/alert-processing-center",
component: "AlertProcessingCenter.vue",
permission: "system:yolo:alert:process",
},
{
key: "detection-history-management",
title: "检测历史管理",
icon: "HistoryOutlined",
path: "/admin/detection-history-management",
component: "DetectionHistoryManagement.vue",
permission: "system:yolo:history:list",
},
],
},
];
// 默认管理员角色 - 修改为空数组,通过自定义权限检查函数处理
const DEFAULT_ADMIN_ROLES: UserRole[] = [];
// 组件映射 - 静态导入以支持Vite分析
const COMPONENT_MAP: { [key: string]: () => Promise<any> } = {
'Dashboard.vue': () => import("@/views/admin_page/Dashboard.vue"),
'UserManagement.vue': () => import("@/views/admin_page/UserManagement.vue"),
'DepartmentManagement.vue': () => import("@/views/admin_page/DepartmentManagement.vue"),
'Role.vue': () => import("@/views/admin_page/Role.vue"),
'MenuManagement.vue': () => import("@/views/admin_page/MenuManagement.vue"),
'NotificationManagement.vue': () => import("@/views/admin_page/NotificationManagement.vue"),
'LogManagement.vue': () => import("@/views/admin_page/LogManagement.vue"),
'KnowledgeBaseManagement.vue': () => import("@/views/admin_page/KnowledgeBaseManagement.vue"),
'ModelManagement.vue': () => import("@/views/admin_page/ModelManagement.vue"),
'AlertLevelManagement.vue': () => import("@/views/admin_page/AlertLevelManagement.vue"),
'AlertProcessingCenter.vue': () => import("@/views/admin_page/AlertProcessingCenter.vue"),
'DetectionHistoryManagement.vue': () => import("@/views/admin_page/DetectionHistoryManagement.vue"),
};
// 🚀 自动生成路由配置
function generateAdminRoutes(): RouteRecordRaw {
const children: RouteRecordRaw[] = [];
ADMIN_MENU_CONFIG.forEach(item => {
// 如果有子菜单,将子菜单作为独立的路由项
if (item.children && item.children.length > 0) {
// 为每个子菜单创建独立的路由
item.children.forEach(child => {
children.push({
path: child.path.replace("/admin/", ""),
name: child.key,
component: COMPONENT_MAP[child.component] || (() => import("@/views/admin_page/Dashboard.vue")),
meta: {
title: child.title,
requiresAuth: true,
roles: child.roles || DEFAULT_ADMIN_ROLES,
},
});
});
} else {
// 没有子菜单的普通菜单项
children.push({
path: item.isDefault ? "" : item.path.replace("/admin/", ""),
name: item.key,
component: COMPONENT_MAP[item.component] || (() => import("@/views/admin_page/Dashboard.vue")),
meta: {
title: item.title,
requiresAuth: true,
roles: item.roles || DEFAULT_ADMIN_ROLES,
},
});
}
});
console.log('🛣️ 生成的管理端路由配置:', children.map(child => ({
path: child.path,
name: child.name,
title: child.meta?.title
})));
return {
path: "/admin",
name: "Admin",
component: () => import("@/views/admin_page/index.vue"),
meta: {
title: "管理后台",
requiresAuth: true,
roles: DEFAULT_ADMIN_ROLES,
},
children,
};
}
// 🚀 自动生成菜单配置
export interface MenuConfig {
key: string;
title: string;
icon?: string;
path: string;
children?: MenuConfig[];
}
function generateMenuConfig(): MenuConfig[] {
return ADMIN_MENU_CONFIG.map(item => ({
key: item.key,
title: item.title,
icon: item.icon,
path: item.path,
children: item.children?.map(child => ({
key: child.key,
title: child.title,
icon: child.icon,
path: child.path,
})),
}));
}
// 🚀 自动生成路径映射函数
function generatePathKeyMapping(): { [path: string]: string } {
const mapping: { [path: string]: string } = {};
function addToMapping(items: AdminMenuItem[], parentPath = '') {
items.forEach(item => {
mapping[item.path] = item.key;
if (item.children) {
addToMapping(item.children, item.path);
}
});
}
addToMapping(ADMIN_MENU_CONFIG);
return mapping;
}
// 导出的配置和函数
export const adminMenuRoutes: RouteRecordRaw = generateAdminRoutes();
export const adminMenuConfig: MenuConfig[] = generateMenuConfig();
// 路径到key的映射
const pathKeyMapping = generatePathKeyMapping();
// 🎯 根据路径获取菜单key - 自动生成
export const getMenuKeyByPath = (path: string): string => {
// 精确匹配
if (pathKeyMapping[path]) {
return pathKeyMapping[path];
}
// 模糊匹配
for (const [mappedPath, key] of Object.entries(pathKeyMapping)) {
if (path.includes(mappedPath) && mappedPath !== '/admin') {
return key;
}
}
// 默认返回dashboard
return 'dashboard';
};
// 🎯 根据菜单key获取路径 - 自动生成
export const getPathByMenuKey = (key: string): string => {
console.log('🔍 查找菜单路径:', key);
const menuItem = ADMIN_MENU_CONFIG.find(item => item.key === key);
if (menuItem) {
console.log('✅ 找到父菜单路径:', menuItem.path);
return menuItem.path;
}
// 在子菜单中查找
for (const item of ADMIN_MENU_CONFIG) {
if (item.children) {
const childItem = item.children.find(child => child.key === key);
if (childItem) {
console.log('✅ 找到子菜单路径:', childItem.path);
return childItem.path;
}
}
}
console.log('❌ 未找到菜单路径,返回默认路径');
return '/admin';
};
// 🎯 根据菜单key获取标题 - 自动生成
export const getTitleByMenuKey = (key: string): string => {
const menuItem = ADMIN_MENU_CONFIG.find(item => item.key === key);
if (menuItem) return menuItem.title;
// 在子菜单中查找
for (const item of ADMIN_MENU_CONFIG) {
if (item.children) {
const childItem = item.children.find(child => child.key === key);
if (childItem) return childItem.title;
}
}
return '仪表盘';
};
// 菜单权限检查
export const hasMenuPermission = (menuKey: string, userRole: string): boolean => {
const menuItem = ADMIN_MENU_CONFIG.find(item => item.key === menuKey);
if (!menuItem) return false;
return menuItem.roles ? menuItem.roles.includes(userRole as UserRole) : DEFAULT_ADMIN_ROLES.includes(userRole as UserRole);
};
// 🎯 新增:根据用户权限过滤菜单配置
export const getFilteredMenuConfig = (userRoles: string[], userPermissions: string[], userMenuPermissions?: number[]): MenuConfig[] => {
const userRole = userRoles[0]; // 取第一个角色作为主要角色
// 仅管理员角色显示管理端菜单
const adminRoles = ['admin', 'system_admin', 'super_admin'];
const isAdminRole = userRoles.some(r => adminRoles.includes(r));
if (!isAdminRole) {
return [];
}
// 对 super_admin / system_admin 开放所有管理菜单(忽略权限字符串过滤)
const isPrivilegedAdmin = userRoles.includes('super_admin') || userRoles.includes('system_admin');
// 过滤菜单项 - 基于权限字符串检查
const filteredMenus = ADMIN_MENU_CONFIG.filter(menuItem => {
console.log(`🔍 检查菜单项: ${menuItem.title} (${menuItem.key})`, {
hasPermission: !!menuItem.permission,
permission: menuItem.permission,
hasChildren: !!(menuItem.children && menuItem.children.length > 0),
childrenCount: menuItem.children?.length || 0
});
// 如果菜单没有配置权限要求,则默认允许访问(如仪表盘)
if (!menuItem.permission) {
console.log(`✅ 菜单 ${menuItem.title} 无权限要求,允许访问`);
return true;
}
// 检查用户是否有该菜单所需的权限
const hasMenuPermission = isPrivilegedAdmin ? true : hasPermission(menuItem.permission, userPermissions);
if (!hasMenuPermission) {
console.log(`❌ 菜单 ${menuItem.title} 权限不足,拒绝访问`);
return false;
}
// 如果有子菜单,过滤子菜单
if (menuItem.children && menuItem.children.length > 0) {
const filteredChildren = menuItem.children.filter(child => {
// 如果子菜单没有配置权限要求,则默认允许访问
if (!child.permission) {
console.log(`✅ 子菜单 ${child.title} 无权限要求,允许访问`);
return true;
}
const childHasPermission = hasPermission(child.permission, userPermissions);
console.log(`🔍 子菜单 ${child.title} 权限检查:`, {
permission: child.permission,
hasPermission: childHasPermission
});
return childHasPermission;
});
console.log(`📊 菜单 ${menuItem.title} 子菜单过滤结果:`, {
originalCount: menuItem.children.length,
filteredCount: filteredChildren.length,
filteredChildren: filteredChildren.map(c => c.title)
});
// 如果没有任何子菜单有权限,则不显示父菜单
if (filteredChildren.length === 0) {
console.log(`❌ 菜单 ${menuItem.title} 所有子菜单都无权限,隐藏父菜单`);
return false;
}
// 更新子菜单列表
menuItem.children = filteredChildren;
}
console.log(`✅ 菜单 ${menuItem.title} 通过权限检查`);
return true;
}).map(menuItem => ({
key: menuItem.key,
title: menuItem.title,
icon: menuItem.icon,
path: menuItem.path,
children: menuItem.children?.map(child => ({
key: child.key,
title: child.title,
icon: child.icon,
path: child.path
}))
}));
return filteredMenus;
};
// 🎯 新增:检查用户是否有任何管理员菜单权限
// 修改逻辑只有normal_user角色不能访问管理端其他所有角色都可以访问
export const hasAnyAdminPermission = (userRoles: string[]): boolean => {
// 仅当包含 admin/system_admin/super_admin 之一才视为管理员
const adminRoles = ['admin', 'system_admin', 'super_admin'];
return userRoles.some(role => adminRoles.includes(role));
};
/**
* 检查用户是否有指定权限
*/
const hasPermission = (permission: string, userPermissions: string[]): boolean => {
return userPermissions.includes(permission);
};

View File

@@ -0,0 +1,275 @@
import { createRouter, createWebHistory } from "vue-router";
import type { RouteRecordRaw } from "vue-router";
import { useUserStore } from "@/stores/hertz_user";
import { adminMenuRoutes, UserRole } from "./admin_menu";
import { userRoutes } from "./user_menu_ai";
// 固定路由配置
const fixedRoutes: RouteRecordRaw[] = [
{
path: "/",
name: "Home",
component: () => import("@/views/Home.vue"),
meta: {
title: "首页",
requiresAuth: false,
},
children: [...generateDynamicRoutes("public")],
},
{
path: "/login",
name: "Login",
component: () => import("@/views/Login.vue"),
meta: {
title: "登录",
requiresAuth: false,
},
},
{
path: "/register",
name: "Register",
component: () => import("@/views/register.vue"),
meta: {
title: "注册",
requiresAuth: false,
},
},
// 管理端路由 - 从admin_menu.ts导入
adminMenuRoutes,
];
// 动态生成路由配置
function generateDynamicRoutes(targetDir: string = ""): RouteRecordRaw[] {
if (!targetDir) {
return [];
}
const viewsContext = import.meta.glob("@/views/**/*.vue", { eager: true });
return Object.entries(viewsContext)
.map(([path, component]) => {
const relativePath = path.match(/\/views\/(.+?)\.vue$/)?.[1];
if (!relativePath) return null;
const fileName = relativePath.replace(".vue", "");
const routeName = fileName.split("/").pop()!;
// 过滤条件
if (targetDir && !fileName.startsWith(targetDir)) {
return null;
}
// 生成路径和标题
const routePath = `/${fileName.replace(/([A-Z])/g, "$1").toLowerCase()}`;
const requiresAuth =
(!routePath.startsWith("/demo") && !routePath.startsWith("/public")) || routePath.startsWith("/user_pages")&& routePath.startsWith("/admin_page");
const pageTitle = (component as any)?.default?.title;
// 根据路径设置角色权限
let roles: UserRole[] = [];
if (routePath.startsWith("/admin_page")) {
roles = [UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SUPER_ADMIN];
} else if (routePath.startsWith("/user_pages")) {
roles = [UserRole.NORMAL_USER, UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SUPER_ADMIN];
} else if (routePath.startsWith("/demo")) {
roles = []; // demo页面不需要特定角色
}
return {
path: routePath,
name: routeName,
component: () => import(/* @vite-ignore */ path),
meta: {
title: pageTitle,
requiresAuth,
roles: requiresAuth ? roles : []
},
};
})
.filter(Boolean) as RouteRecordRaw[];
}
// 合并固定路由和动态路由
const routes: RouteRecordRaw[] = [
...fixedRoutes,
...userRoutes, // 用户菜单路由 - 现在通过统一配置自动生成
...generateDynamicRoutes("demo"), // 生成demo文件夹的路由
...generateDynamicRoutes("admin_page"),//生成admin_page文件夹的路由
// 404页面始终放在最后
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () => import("@/views/NotFound.vue"),
meta: {
title: "页面未找到",
requiresAuth: false,
},
},
];
// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(_to, _from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { top: 0 };
}
},
});
// 递归打印路由信息
function printRoute(route: RouteRecordRaw, level: number = 0) {
const indent = " ".repeat(level);
const icon = route.meta.requiresAuth ? "🔒" : "🔓";
const auth = route.meta.requiresAuth ? "需要登录" : "公开访问";
console.log(`${indent}${icon} ${route.path}${route.meta.title} (${auth})`);
// 递归打印子路由
if (route.children && route.children.length > 0) {
route.children.forEach((child) => printRoute(child, level + 1));
}
}
// 路由调试信息
function logRouteInfo() {
console.log("🚀 管理系统 路由配置:");
console.log("📋 路由列表:");
routes.forEach((route) => printRoute(route));
console.log(" ❓ /:pathMatch(.*)* → NotFound (页面未找到)");
console.log("✅ 路由配置完成!");
}
// 重定向计数器,防止无限重定向
let redirectCount = 0;
const MAX_REDIRECTS = 3;
// 路由守卫
router.beforeEach((to, _from, next) => {
const userStore = useUserStore();
// 调试信息
console.log('🛡️ 路由守卫检查');
console.log('📍 目标路由:', to.path, to.name);
console.log('🔐 需要认证:', to.meta.requiresAuth);
console.log('👤 用户登录状态:', userStore.isLoggedIn);
console.log('🎫 Token:', userStore.token ? '存在' : '不存在');
console.log('📋 用户信息:', userStore.userInfo);
console.log('🔄 重定向计数:', redirectCount);
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - 管理系统`;
}
// 检查是否需要登录
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
console.log('❌ 需要登录但用户未登录,重定向到登录页');
redirectCount++;
if (redirectCount > MAX_REDIRECTS) {
console.log('⚠️ 重定向次数过多,强制跳转到首页');
redirectCount = 0;
next({ name: "Home" });
return;
}
next({ name: "Login", query: { redirect: to.fullPath } });
return;
}
// 已登录用户访问登录页,根据角色重定向到对应首页
if (to.name === "Login" && userStore.isLoggedIn) {
const userRole = userStore.userInfo?.roles?.[0]?.role_code;
console.log('🔄 路由守卫 - 已登录用户访问登录页');
console.log('👤 当前用户角色:', userRole);
console.log('📋 用户信息:', userStore.userInfo);
// 重置重定向计数器
redirectCount = 0;
// 仅管理员角色进入管理端,其余(含未定义)进入用户端
const adminRoles = [UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SUPER_ADMIN];
const isAdmin = adminRoles.includes(userRole as UserRole);
if (isAdmin) {
console.log('➡️ 重定向到管理端首页');
next({ name: "Admin" });
} else {
console.log('➡️ 重定向到用户端首页');
next({ name: "UserDashboard" });
}
return;
}
// 检查角色权限
if (to.meta.requiresAuth && to.meta.roles && Array.isArray(to.meta.roles)) {
const userRole = userStore.userInfo?.roles?.[0]?.role_code;
// 特殊处理:如果是管理端路由,使用自定义权限检查
let hasPermission = false;
if (to.path.startsWith('/admin')) {
// 管理端路由:仅 admin/system_admin/super_admin 可访问
const adminRoles = [UserRole.ADMIN, UserRole.SYSTEM_ADMIN, UserRole.SUPER_ADMIN];
hasPermission = adminRoles.includes(userRole as UserRole);
} else {
// 其他路由:使用原有的角色检查逻辑
hasPermission = to.meta.roles.length === 0 || to.meta.roles.includes(userRole as UserRole);
}
console.log('🔐 路由权限检查');
console.log('📍 目标路由:', to.path, to.name);
console.log('🎭 需要的角色:', to.meta.roles);
console.log('👤 用户角色:', userRole);
console.log('🏢 是否为管理端路由:', to.path.startsWith('/admin'));
console.log('✅ 是否有权限:', hasPermission);
if (!hasPermission) {
console.log('❌ 权限不足,准备重定向');
// 增加重定向计数
redirectCount++;
// 防止无限重定向
if (redirectCount > MAX_REDIRECTS) {
console.log('⚠️ 重定向次数过多,强制跳转到首页');
redirectCount = 0;
next({ name: "Home" });
return;
}
// 防止无限重定向:检查是否已经在重定向过程中
if (to.name === 'Admin' || to.name === 'UserDashboard') {
console.log('⚠️ 检测到重定向循环,强制跳转到首页');
redirectCount = 0;
next({ name: "Home" });
return;
}
// 没有权限,根据用户角色重定向到对应首页
// 只有normal_user角色跳转到用户端其他角色包括未定义的都跳转到管理端
if (userRole === 'normal_user') {
console.log('➡️ 重定向到用户端首页');
next({ name: "UserDashboard" });
} else {
console.log('➡️ 重定向到管理端首页 (角色:', userRole || '未定义', ')');
next({ name: "Admin" });
}
return;
}
}
// 成功通过所有检查,重置重定向计数器
redirectCount = 0;
next();
});
// 路由错误处理
router.onError((error) => {
console.error("路由错误:", error);
});
// 输出路由信息
logRouteInfo();
export default router;

View File

@@ -0,0 +1,183 @@
import type { RouteRecordRaw } from 'vue-router'
import { defineAsyncComponent } from 'vue'
export interface UserMenuConfig {
key: string
label: string
icon?: string
path: string
component: string
children?: UserMenuConfig[]
disabled?: boolean
meta?: {
title?: string
requiresAuth?: boolean
roles?: string[]
[key: string]: any
}
}
export interface MenuItem {
key: string
label: string
icon?: string
path?: string
children?: MenuItem[]
disabled?: boolean
}
export const userMenuConfigs: UserMenuConfig[] = [
{ key: 'dashboard', label: '首页', icon: 'DashboardOutlined', path: '/dashboard', component: 'index.vue', meta: { title: '用户首页', requiresAuth: true } },
{ key: 'profile', label: '个人信息', icon: 'UserOutlined', path: '/user/profile', component: 'Profile.vue', meta: { title: '个人信息', requiresAuth: true, hideInMenu: true } },
// { key: 'documents', label: '文档管理', icon: 'FileTextOutlined', path: '/user/documents', component: 'Documents.vue', meta: { title: '文档管理', requiresAuth: true } },
{ key: 'system-monitor', label: '系统监控', icon: 'DashboardOutlined', path: '/user/system-monitor', component: 'SystemMonitor.vue', meta: { title: '系统监控', requiresAuth: true } },
{ key: 'ai-chat', label: 'AI助手', icon: 'MessageOutlined', path: '/user/ai-chat', component: 'AiChat.vue', meta: { title: 'AI助手', requiresAuth: true } },
{ key: 'yolo-detection', label: 'YOLO检测', icon: 'ScanOutlined', path: '/user/yolo-detection', component: 'YoloDetection.vue', meta: { title: 'YOLO检测中心', requiresAuth: true } },
{ key: 'live-detection', label: '实时检测', icon: 'VideoCameraOutlined', path: '/user/live-detection', component: 'LiveDetection.vue', meta: { title: '实时检测', requiresAuth: true } },
{ key: 'detection-history', label: '检测历史', icon: 'HistoryOutlined', path: '/user/detection-history', component: 'DetectionHistory.vue', meta: { title: '检测历史记录', requiresAuth: true } },
{ key: 'alert-center', label: '告警中心', icon: 'ExclamationCircleOutlined', path: '/user/alert-center', component: 'AlertCenter.vue', meta: { title: '告警中心', requiresAuth: true } },
{ key: 'notice-center', label: '通知中心', icon: 'BellOutlined', path: '/user/notice', component: 'NoticeCenter.vue', meta: { title: '通知中心', requiresAuth: true } },
{ key: 'knowledge-center', label: '知识库中心', icon: 'DatabaseOutlined', path: '/user/knowledge', component: 'KnowledgeCenter.vue', meta: { title: '知识库中心', requiresAuth: true } },
]
const explicitComponentMap: Record<string, any> = {
'index.vue': defineAsyncComponent(() => import('@/views/user_pages/index.vue')),
'Profile.vue': defineAsyncComponent(() => import('@/views/user_pages/Profile.vue')),
'Documents.vue': defineAsyncComponent(() => import('@/views/user_pages/Documents.vue')),
'Messages.vue': defineAsyncComponent(() => import('@/views/user_pages/Messages.vue')),
'SystemMonitor.vue': defineAsyncComponent(() => import('@/views/user_pages/SystemMonitor.vue')),
'AiChat.vue': defineAsyncComponent(() => import('@/views/user_pages/AiChat.vue')),
'YoloDetection.vue': defineAsyncComponent(() => import('@/views/user_pages/YoloDetection.vue')),
'LiveDetection.vue': defineAsyncComponent(() => import('@/views/user_pages/LiveDetection.vue')),
'DetectionHistory.vue': defineAsyncComponent(() => import('@/views/user_pages/DetectionHistory.vue')),
'AlertCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/AlertCenter.vue')),
'NoticeCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/NoticeCenter.vue')),
'KnowledgeCenter.vue': defineAsyncComponent(() => import('@/views/user_pages/KnowledgeCenter.vue')),
}
export const userMenuItems: MenuItem[] = userMenuConfigs.map(config => ({
key: config.key,
label: config.label,
icon: config.icon,
path: config.path,
disabled: config.disabled,
children: config.children?.map(child => ({ key: child.key, label: child.label, icon: child.icon, path: child.path, disabled: child.disabled }))
}))
const componentMap: Record<string, () => Promise<any>> = {
'index.vue': () => import('@/views/user_pages/index.vue'),
'Profile.vue': () => import('@/views/user_pages/Profile.vue'),
'Documents.vue': () => import('@/views/user_pages/Documents.vue'),
'Messages.vue': () => import('@/views/user_pages/Messages.vue'),
'SystemMonitor.vue': () => import('@/views/user_pages/SystemMonitor.vue'),
'AiChat.vue': () => import('@/views/user_pages/AiChat.vue'),
'YoloDetection.vue': () => import('@/views/user_pages/YoloDetection.vue'),
'LiveDetection.vue': () => import('@/views/user_pages/LiveDetection.vue'),
'DetectionHistory.vue': () => import('@/views/user_pages/DetectionHistory.vue'),
'AlertCenter.vue': () => import('@/views/user_pages/AlertCenter.vue'),
'NoticeCenter.vue': () => import('@/views/user_pages/NoticeCenter.vue'),
'KnowledgeCenter.vue': () => import('@/views/user_pages/KnowledgeCenter.vue'),
}
const baseRoutes: RouteRecordRaw[] = userMenuConfigs.map(config => {
const route: RouteRecordRaw = {
path: config.path,
name: `User${config.key.charAt(0).toUpperCase() + config.key.slice(1)}`,
component: componentMap[config.component] || (() => import('@/views/NotFound.vue')),
meta: { title: config.meta?.title || config.label, requiresAuth: config.meta?.requiresAuth ?? true, ...config.meta }
}
if (config.children && config.children.length > 0) {
route.children = config.children.map(child => ({
path: child.path,
name: `User${child.key.charAt(0).toUpperCase() + child.key.slice(1)}`,
component: componentMap[child.component] || (() => import('@/views/NotFound.vue')),
meta: { title: child.meta?.title || child.label, requiresAuth: child.meta?.requiresAuth ?? true, ...child.meta }
}))
}
return route
})
// 文章详情独立页面(不在菜单展示)
const knowledgeDetailRoute: RouteRecordRaw = {
path: '/user/knowledge/:id',
name: 'UserKnowledgeDetail',
component: () => import('@/views/user_pages/KnowledgeDetail.vue'),
meta: { title: '文章详情', requiresAuth: true, hideInMenu: true }
}
export const userRoutes: RouteRecordRaw[] = [...baseRoutes, knowledgeDetailRoute]
export function getMenuPath(menuKey: string): string {
const findPath = (items: MenuItem[], key: string): string | null => {
for (const item of items) {
if (item.key === key && item.path) return item.path
if (item.children) {
const childPath = findPath(item.children, key)
if (childPath) return childPath
}
}
return null
}
return findPath(userMenuItems, menuKey) || '/dashboard'
}
export function getMenuBreadcrumb(menuKey: string): string[] {
const findBreadcrumb = (items: MenuItem[], key: string, path: string[] = []): string[] | null => {
for (const item of items) {
const currentPath = [...path, item.label]
if (item.key === menuKey) return currentPath
if (item.children) {
const childPath = findBreadcrumb(item.children, key, currentPath)
if (childPath) return childPath
}
}
return null
}
return findBreadcrumb(userMenuItems, menuKey) || ['仪表盘']
}
export const generateComponentMap = () => {
const map: Record<string, any> = {}
const processConfigs = (configs: UserMenuConfig[]) => {
configs.forEach(config => {
if (explicitComponentMap[config.component]) {
map[config.key] = explicitComponentMap[config.component]
} else {
map[config.key] = defineAsyncComponent(() => import('@/views/NotFound.vue'))
}
if (config.children) processConfigs(config.children)
})
}
processConfigs(userMenuConfigs)
return map
}
export const userComponentMap = generateComponentMap()
export const getFilteredUserMenuItems = (userRoles: string[], userPermissions: string[]): MenuItem[] => {
return userMenuConfigs
.filter(config => {
// 隐藏菜单中不显示的项(如个人信息,只在用户下拉菜单中显示)
if (config.meta?.hideInMenu) return false
if (!config.meta?.roles || config.meta.roles.length === 0) return true
return config.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
})
.map(config => ({
key: config.key,
label: config.label,
icon: config.icon,
path: config.path,
disabled: config.disabled,
children: config.children?.filter(child => {
if (!child.meta?.roles || child.meta.roles.length === 0) return true
return child.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
}).map(child => ({ key: child.key, label: child.label, icon: child.icon, path: child.path, disabled: child.disabled }))
}))
}
export const hasUserMenuPermission = (menuKey: string, userRoles: string[]): boolean => {
const menuConfig = userMenuConfigs.find(config => config.key === menuKey)
if (!menuConfig) return false
if (!menuConfig.meta?.roles || menuConfig.meta.roles.length === 0) return true
return menuConfig.meta.roles.some(requiredRole => userRoles.includes(requiredRole))
}

View File

@@ -0,0 +1,98 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { i18n } from '@/locales'
// 主题类型
export type Theme = 'light' | 'dark' | 'auto'
// 语言类型
export type Language = 'zh-CN' | 'en-US'
export const useAppStore = defineStore('app', () => {
// 状态
const theme = ref<Theme>('light')
const language = ref<Language>('zh-CN')
const collapsed = ref<boolean>(false)
const loading = ref<boolean>(false)
// 计算属性
const isDark = computed(() => {
if (theme.value === 'auto') {
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
return theme.value === 'dark'
})
const currentLanguage = computed(() => language.value)
// 方法
const setTheme = (newTheme: Theme) => {
theme.value = newTheme
localStorage.setItem('theme', newTheme)
// 应用主题到HTML
const html = document.documentElement
if (newTheme === 'dark' || (newTheme === 'auto' && isDark.value)) {
html.classList.add('dark')
} else {
html.classList.remove('dark')
}
}
const setLanguage = (newLanguage: Language) => {
language.value = newLanguage
localStorage.setItem('language', newLanguage)
// 设置i18n语言
i18n.global.locale.value = newLanguage
}
const toggleCollapsed = () => {
collapsed.value = !collapsed.value
}
const setLoading = (state: boolean) => {
loading.value = state
}
const initAppSettings = () => {
// 从本地存储恢复设置
const savedTheme = localStorage.getItem('theme') as Theme
const savedLanguage = localStorage.getItem('language') as Language
if (savedTheme) {
setTheme(savedTheme)
}
if (savedLanguage) {
setLanguage(savedLanguage)
} else {
// 根据浏览器语言自动设置
const browserLang = navigator.language
if (browserLang.startsWith('zh')) {
setLanguage('zh-CN')
} else {
setLanguage('en-US')
}
}
}
return {
// 状态
theme,
language,
collapsed,
loading,
// 计算属性
isDark,
currentLanguage,
// 方法
setTheme,
setLanguage,
toggleCollapsed,
setLoading,
initAppSettings,
}
})

Some files were not shown because too many files have changed in this diff Show More