后台管理第三阶段开发

This commit is contained in:
rjb
2025-08-30 00:01:49 +08:00
parent fee4c339a2
commit ea7570ba80
32 changed files with 3826 additions and 0 deletions

Binary file not shown.

View File

@@ -13,6 +13,12 @@ from .views.user_admin import UserAdminView
from .views.prompt_admin import PromptAdminView
from .views.template_admin import TemplateAdminView
from .views.system_admin import SystemAdminView
from .views.analytics_admin import AnalyticsAdminView
from .views.batch_admin import BatchAdminView
from .views.monitor_admin import MonitorAdminView
from .views.report_admin import ReportAdminView
from .views.backup_admin import BackupAdminView
from .views.api_admin import ApiAdminView
from datetime import datetime
# 创建登录管理器
@@ -38,6 +44,12 @@ def init_admin(app):
admin.add_view(PromptAdminView(Prompt, db.session, name='提示词管理', endpoint='admin_prompt'))
admin.add_view(TemplateAdminView(PromptTemplate, db.session, name='模板管理', endpoint='admin_template'))
admin.add_view(SystemAdminView(name='系统管理', endpoint='admin_system'))
admin.add_view(AnalyticsAdminView(name='数据分析', endpoint='analytics_admin'))
admin.add_view(BatchAdminView(name='批量操作', endpoint='batch_admin'))
admin.add_view(MonitorAdminView(name='系统监控', endpoint='monitor_admin'))
admin.add_view(ReportAdminView(name='高级报表', endpoint='report_admin'))
admin.add_view(BackupAdminView(name='数据备份', endpoint='backup_admin'))
admin.add_view(ApiAdminView(name='API管理', endpoint='api_admin'))
# 注册登录路由
app.add_url_rule('/admin/login', 'admin.login', admin_login, methods=['GET', 'POST'])

View File

@@ -0,0 +1,191 @@
# -*- coding: utf-8 -*-
"""
数据分析管理视图
"""
from flask_admin import BaseView, expose
from flask_login import login_required, current_user
from src.flask_prompt_master.models.models import User, Prompt, PromptTemplate
from src.flask_prompt_master import db
from sqlalchemy import func, extract
from datetime import datetime, timedelta
import plotly.graph_objs as go
import plotly.utils
import json
class AnalyticsAdminView(BaseView):
"""数据分析管理视图"""
@expose('/')
@login_required
def index(self):
"""数据分析首页"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return self.render('admin/403.html')
# 获取统计数据
stats = self._get_analytics_data()
return self.render('admin/analytics_dashboard.html', stats=stats)
@expose('/charts')
@login_required
def charts(self):
"""图表页面"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return self.render('admin/403.html')
# 生成图表数据
charts_data = self._generate_charts()
return self.render('admin/analytics_charts.html', charts_data=charts_data)
def _get_analytics_data(self):
"""获取分析数据"""
try:
# 用户统计
total_users = User.query.count()
active_users = User.query.filter_by(status=1).count()
new_users_today = User.query.filter(
User.created_time >= datetime.now().date()
).count()
new_users_week = User.query.filter(
User.created_time >= datetime.now() - timedelta(days=7)
).count()
# 提示词统计
total_prompts = Prompt.query.count()
today_prompts = Prompt.query.filter(
Prompt.created_at >= datetime.now().date()
).count()
week_prompts = Prompt.query.filter(
Prompt.created_at >= datetime.now() - timedelta(days=7)
).count()
# 模板统计
total_templates = PromptTemplate.query.count()
default_templates = PromptTemplate.query.filter_by(is_default=True).count()
# 用户活跃度
active_users_week = db.session.query(func.count(func.distinct(Prompt.user_id))).filter(
Prompt.created_at >= datetime.now() - timedelta(days=7)
).scalar()
return {
'total_users': total_users,
'active_users': active_users,
'new_users_today': new_users_today,
'new_users_week': new_users_week,
'total_prompts': total_prompts,
'today_prompts': today_prompts,
'week_prompts': week_prompts,
'total_templates': total_templates,
'default_templates': default_templates,
'active_users_week': active_users_week or 0
}
except Exception as e:
return {
'total_users': 0,
'active_users': 0,
'new_users_today': 0,
'new_users_week': 0,
'total_prompts': 0,
'today_prompts': 0,
'week_prompts': 0,
'total_templates': 0,
'default_templates': 0,
'active_users_week': 0
}
def _generate_charts(self):
"""生成图表数据"""
try:
# 用户注册趋势最近30天
thirty_days_ago = datetime.now() - timedelta(days=30)
daily_registrations = db.session.query(
func.date(User.created_time).label('date'),
func.count(User.uid).label('count')
).filter(
User.created_time >= thirty_days_ago
).group_by(
func.date(User.created_time)
).all()
# 提示词生成趋势最近30天
daily_prompts = db.session.query(
func.date(Prompt.created_at).label('date'),
func.count(Prompt.id).label('count')
).filter(
Prompt.created_at >= thirty_days_ago
).group_by(
func.date(Prompt.created_at)
).all()
# 用户活跃度饼图
active_status = db.session.query(
User.status,
func.count(User.uid).label('count')
).group_by(User.status).all()
# 生成图表JSON
charts = {
'user_trend': self._create_line_chart(daily_registrations, '用户注册趋势', '日期', '注册人数'),
'prompt_trend': self._create_line_chart(daily_prompts, '提示词生成趋势', '日期', '生成数量'),
'user_status': self._create_pie_chart(active_status, '用户状态分布')
}
return charts
except Exception as e:
return {}
def _create_line_chart(self, data, title, x_label, y_label):
"""创建折线图"""
if not data:
return json.dumps({})
x_values = [str(item.date) for item in data]
y_values = [item.count for item in data]
trace = go.Scatter(
x=x_values,
y=y_values,
mode='lines+markers',
name=title,
line=dict(color='#4e73df', width=3),
marker=dict(size=8)
)
layout = go.Layout(
title=title,
xaxis=dict(title=x_label),
yaxis=dict(title=y_label),
height=400,
margin=dict(l=50, r=50, t=50, b=50)
)
fig = go.Figure(data=[trace], layout=layout)
return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
def _create_pie_chart(self, data, title):
"""创建饼图"""
if not data:
return json.dumps({})
labels = ['活跃用户' if item.status == 1 else '禁用用户' for item in data]
values = [item.count for item in data]
colors = ['#28a745', '#dc3545']
trace = go.Pie(
labels=labels,
values=values,
marker=dict(colors=colors),
textinfo='label+percent'
)
layout = go.Layout(
title=title,
height=400,
margin=dict(l=50, r=50, t=50, b=50)
)
fig = go.Figure(data=[trace], layout=layout)
return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)

View File

@@ -0,0 +1,184 @@
# -*- coding: utf-8 -*-
"""
API管理视图
"""
from flask_admin import BaseView, expose
from flask_login import login_required, current_user
from flask import request, jsonify
from src.flask_prompt_master.models.models import User, Prompt
from src.flask_prompt_master import db
from sqlalchemy import func
from datetime import datetime, timedelta
import json
class ApiAdminView(BaseView):
"""API管理视图"""
@expose('/')
@login_required
def index(self):
"""API管理首页"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return self.render('admin/403.html')
# 获取API统计信息
api_stats = self._get_api_stats()
return self.render('admin/api_dashboard.html', api_stats=api_stats)
@expose('/api/stats')
@login_required
def api_stats(self):
"""获取API统计信息"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
stats = self._get_api_stats()
return jsonify({'success': True, 'data': stats})
except Exception as e:
return jsonify({'success': False, 'message': f'获取统计信息失败: {str(e)}'})
@expose('/api/calls')
@login_required
def api_calls(self):
"""获取API调用记录"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
# 获取查询参数
start_date = request.args.get('start_date', (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d'))
end_date = request.args.get('end_date', datetime.now().strftime('%Y-%m-%d'))
api_type = request.args.get('type', 'all')
# 获取API调用记录
calls = self._get_api_calls(start_date, end_date, api_type)
return jsonify({'success': True, 'data': calls})
except Exception as e:
return jsonify({'success': False, 'message': f'获取调用记录失败: {str(e)}'})
@expose('/api/rate-limits')
@login_required
def rate_limits(self):
"""API限流管理"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return self.render('admin/403.html')
return self.render('admin/api_rate_limits.html')
@expose('/api/keys')
@login_required
def api_keys(self):
"""API密钥管理"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return self.render('admin/403.html')
return self.render('admin/api_keys.html')
def _get_api_stats(self):
"""获取API统计信息"""
try:
# 今日API调用次数
today = datetime.now().date()
today_calls = Prompt.query.filter(
func.date(Prompt.created_at) == today
).count()
# 本周API调用次数
week_ago = datetime.now() - timedelta(days=7)
week_calls = Prompt.query.filter(
Prompt.created_at >= week_ago
).count()
# 本月API调用次数
month_ago = datetime.now() - timedelta(days=30)
month_calls = Prompt.query.filter(
Prompt.created_at >= month_ago
).count()
# 总API调用次数
total_calls = Prompt.query.count()
# 活跃用户数有API调用的用户
active_users = db.session.query(func.count(func.distinct(Prompt.user_id))).scalar() or 0
# 平均响应时间(模拟数据)
avg_response_time = 1.2 # 秒
# 成功率(模拟数据)
success_rate = 98.5 # 百分比
# 最近7天的调用趋势
daily_calls = []
for i in range(7):
date = datetime.now() - timedelta(days=i)
count = Prompt.query.filter(
func.date(Prompt.created_at) == date.date()
).count()
daily_calls.append({
'date': date.strftime('%Y-%m-%d'),
'count': count
})
daily_calls.reverse()
return {
'today_calls': today_calls,
'week_calls': week_calls,
'month_calls': month_calls,
'total_calls': total_calls,
'active_users': active_users,
'avg_response_time': avg_response_time,
'success_rate': success_rate,
'daily_calls': daily_calls
}
except Exception as e:
return {
'today_calls': 0,
'week_calls': 0,
'month_calls': 0,
'total_calls': 0,
'active_users': 0,
'avg_response_time': 0,
'success_rate': 0,
'daily_calls': []
}
def _get_api_calls(self, start_date, end_date, api_type):
"""获取API调用记录"""
try:
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
end_dt = datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)
# 查询API调用记录
query = Prompt.query.filter(
Prompt.created_at >= start_dt,
Prompt.created_at < end_dt
)
if api_type != 'all':
# 这里可以根据实际需求添加API类型过滤
pass
calls = query.order_by(Prompt.created_at.desc()).limit(100).all()
# 格式化数据
call_records = []
for call in calls:
call_records.append({
'id': call.id,
'user_id': call.user_id,
'api_type': 'prompt_generation',
'input_text': call.input_text[:50] + '...' if len(call.input_text) > 50 else call.input_text,
'status': 'success',
'response_time': 1.2, # 模拟响应时间
'created_at': call.created_at.strftime('%Y-%m-%d %H:%M:%S') if call.created_at else '',
'ip_address': '127.0.0.1' # 模拟IP地址
})
return call_records
except Exception as e:
return []

View File

@@ -0,0 +1,479 @@
# -*- coding: utf-8 -*-
"""
数据备份管理视图
"""
from flask_admin import BaseView, expose
from flask_login import login_required, current_user
from flask import request, jsonify, send_file
from src.flask_prompt_master import db
from src.flask_prompt_master.models.models import User, Prompt, PromptTemplate
import os
import json
import zipfile
import io
from datetime import datetime, timedelta
import threading
import schedule
import time
import shutil
class BackupAdminView(BaseView):
"""数据备份管理视图"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.backup_dir = os.path.join(os.getcwd(), 'backups')
self.auto_backup_enabled = False
self.backup_thread = None
# 确保备份目录存在
if not os.path.exists(self.backup_dir):
os.makedirs(self.backup_dir)
@expose('/')
@login_required
def index(self):
"""备份管理首页"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return self.render('admin/403.html')
# 获取备份文件列表
backup_files = self._get_backup_files()
return self.render('admin/backup_dashboard.html', backup_files=backup_files)
@expose('/create-backup', methods=['POST'])
@login_required
def create_backup(self):
"""创建备份"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
backup_type = request.json.get('type', 'full')
backup_name = request.json.get('name', '')
# 生成备份文件名
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
if backup_name:
filename = f"{backup_name}_{timestamp}.zip"
else:
filename = f"backup_{backup_type}_{timestamp}.zip"
backup_path = os.path.join(self.backup_dir, filename)
# 创建备份
if backup_type == 'full':
success = self._create_full_backup(backup_path)
elif backup_type == 'data':
success = self._create_data_backup(backup_path)
elif backup_type == 'config':
success = self._create_config_backup(backup_path)
else:
return jsonify({'success': False, 'message': '不支持的备份类型'})
if success:
return jsonify({
'success': True,
'message': f'{backup_type}备份创建成功',
'filename': filename
})
else:
return jsonify({'success': False, 'message': '备份创建失败'})
except Exception as e:
return jsonify({'success': False, 'message': f'备份创建失败: {str(e)}'})
@expose('/download-backup/<filename>')
@login_required
def download_backup(self, filename):
"""下载备份文件"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
backup_path = os.path.join(self.backup_dir, filename)
if not os.path.exists(backup_path):
return jsonify({'success': False, 'message': '备份文件不存在'})
return send_file(
backup_path,
as_attachment=True,
download_name=filename
)
except Exception as e:
return jsonify({'success': False, 'message': f'下载失败: {str(e)}'})
@expose('/restore-backup', methods=['POST'])
@login_required
def restore_backup(self):
"""恢复备份"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
filename = request.json.get('filename')
if not filename:
return jsonify({'success': False, 'message': '请选择要恢复的备份文件'})
backup_path = os.path.join(self.backup_dir, filename)
if not os.path.exists(backup_path):
return jsonify({'success': False, 'message': '备份文件不存在'})
# 执行恢复
success = self._restore_backup(backup_path)
if success:
return jsonify({'success': True, 'message': '备份恢复成功'})
else:
return jsonify({'success': False, 'message': '备份恢复失败'})
except Exception as e:
return jsonify({'success': False, 'message': f'恢复失败: {str(e)}'})
@expose('/delete-backup', methods=['POST'])
@login_required
def delete_backup(self):
"""删除备份"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
filename = request.json.get('filename')
if not filename:
return jsonify({'success': False, 'message': '请选择要删除的备份文件'})
backup_path = os.path.join(self.backup_dir, filename)
if not os.path.exists(backup_path):
return jsonify({'success': False, 'message': '备份文件不存在'})
# 删除文件
os.remove(backup_path)
return jsonify({'success': True, 'message': '备份文件删除成功'})
except Exception as e:
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
@expose('/auto-backup', methods=['POST'])
@login_required
def toggle_auto_backup(self):
"""切换自动备份"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
enabled = request.json.get('enabled', False)
if enabled and not self.auto_backup_enabled:
# 启动自动备份
self.auto_backup_enabled = True
self.backup_thread = threading.Thread(target=self._auto_backup_worker, daemon=True)
self.backup_thread.start()
return jsonify({'success': True, 'message': '自动备份已启动'})
elif not enabled and self.auto_backup_enabled:
# 停止自动备份
self.auto_backup_enabled = False
return jsonify({'success': True, 'message': '自动备份已停止'})
else:
return jsonify({'success': False, 'message': '自动备份状态未改变'})
except Exception as e:
return jsonify({'success': False, 'message': f'操作失败: {str(e)}'})
def _get_backup_files(self):
"""获取备份文件列表"""
backup_files = []
try:
for filename in os.listdir(self.backup_dir):
if filename.endswith('.zip'):
file_path = os.path.join(self.backup_dir, filename)
stat = os.stat(file_path)
backup_files.append({
'filename': filename,
'size': self._format_file_size(stat.st_size),
'created_time': datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S'),
'modified_time': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
})
# 按修改时间排序
backup_files.sort(key=lambda x: x['modified_time'], reverse=True)
except Exception as e:
print(f"获取备份文件列表失败: {str(e)}")
return backup_files
def _create_full_backup(self, backup_path):
"""创建完整备份"""
try:
with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 备份数据库数据
self._backup_database_data(zipf)
# 备份配置文件
self._backup_config_files(zipf)
# 备份日志文件
self._backup_log_files(zipf)
# 创建备份信息文件
backup_info = {
'type': 'full',
'created_time': datetime.now().isoformat(),
'created_by': current_user.username,
'description': '完整系统备份'
}
zipf.writestr('backup_info.json', json.dumps(backup_info, indent=2))
return True
except Exception as e:
print(f"创建完整备份失败: {str(e)}")
return False
def _create_data_backup(self, backup_path):
"""创建数据备份"""
try:
with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 只备份数据库数据
self._backup_database_data(zipf)
# 创建备份信息文件
backup_info = {
'type': 'data',
'created_time': datetime.now().isoformat(),
'created_by': current_user.username,
'description': '数据备份'
}
zipf.writestr('backup_info.json', json.dumps(backup_info, indent=2))
return True
except Exception as e:
print(f"创建数据备份失败: {str(e)}")
return False
def _create_config_backup(self, backup_path):
"""创建配置备份"""
try:
with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 只备份配置文件
self._backup_config_files(zipf)
# 创建备份信息文件
backup_info = {
'type': 'config',
'created_time': datetime.now().isoformat(),
'created_by': current_user.username,
'description': '配置备份'
}
zipf.writestr('backup_info.json', json.dumps(backup_info, indent=2))
return True
except Exception as e:
print(f"创建配置备份失败: {str(e)}")
return False
def _backup_database_data(self, zipf):
"""备份数据库数据"""
try:
# 备份用户数据
users = User.query.all()
user_data = []
for user in users:
user_data.append({
'uid': user.uid,
'login_name': user.login_name,
'nickname': user.nickname,
'email': user.email,
'mobile': user.mobile,
'sex': user.sex,
'status': user.status,
'created_time': user.created_time.isoformat() if user.created_time else None,
'updated_time': user.updated_time.isoformat() if user.updated_time else None
})
zipf.writestr('data/users.json', json.dumps(user_data, indent=2, ensure_ascii=False))
# 备份提示词数据
prompts = Prompt.query.all()
prompt_data = []
for prompt in prompts:
prompt_data.append({
'id': prompt.id,
'user_id': prompt.user_id,
'input_text': prompt.input_text,
'generated_text': prompt.generated_text,
'created_at': prompt.created_at.isoformat() if prompt.created_at else None
})
zipf.writestr('data/prompts.json', json.dumps(prompt_data, indent=2, ensure_ascii=False))
# 备份模板数据
templates = PromptTemplate.query.all()
template_data = []
for template in templates:
template_data.append({
'id': template.id,
'name': template.name,
'category': template.category,
'industry': template.industry,
'profession': template.profession,
'description': template.description,
'system_prompt': template.system_prompt,
'is_default': template.is_default,
'created_at': template.created_at.isoformat() if template.created_at else None,
'updated_at': template.updated_at.isoformat() if template.updated_at else None
})
zipf.writestr('data/templates.json', json.dumps(template_data, indent=2, ensure_ascii=False))
except Exception as e:
print(f"备份数据库数据失败: {str(e)}")
def _backup_config_files(self, zipf):
"""备份配置文件"""
try:
config_files = ['config.py', '.env', 'requirements.txt']
for config_file in config_files:
if os.path.exists(config_file):
zipf.write(config_file, f'config/{config_file}')
# 备份Flask配置
from src.flask_prompt_master import create_app
app = create_app()
config_data = {
'SQLALCHEMY_DATABASE_URI': app.config.get('SQLALCHEMY_DATABASE_URI', ''),
'SECRET_KEY': app.config.get('SECRET_KEY', ''),
'DEBUG': app.config.get('DEBUG', False)
}
zipf.writestr('config/app_config.json', json.dumps(config_data, indent=2))
except Exception as e:
print(f"备份配置文件失败: {str(e)}")
def _backup_log_files(self, zipf):
"""备份日志文件"""
try:
log_dir = 'logs'
if os.path.exists(log_dir):
for log_file in os.listdir(log_dir):
if log_file.endswith('.log'):
log_path = os.path.join(log_dir, log_file)
zipf.write(log_path, f'logs/{log_file}')
except Exception as e:
print(f"备份日志文件失败: {str(e)}")
def _restore_backup(self, backup_path):
"""恢复备份"""
try:
with zipfile.ZipFile(backup_path, 'r') as zipf:
# 读取备份信息
backup_info = json.loads(zipf.read('backup_info.json'))
backup_type = backup_info.get('type', 'unknown')
if backup_type == 'data' or backup_type == 'full':
# 恢复数据库数据
self._restore_database_data(zipf)
if backup_type == 'config' or backup_type == 'full':
# 恢复配置文件
self._restore_config_files(zipf)
return True
except Exception as e:
print(f"恢复备份失败: {str(e)}")
return False
def _restore_database_data(self, zipf):
"""恢复数据库数据"""
try:
# 恢复用户数据
if 'data/users.json' in zipf.namelist():
user_data = json.loads(zipf.read('data/users.json'))
for user_info in user_data:
# 这里需要根据实际情况实现数据恢复逻辑
pass
# 恢复提示词数据
if 'data/prompts.json' in zipf.namelist():
prompt_data = json.loads(zipf.read('data/prompts.json'))
for prompt_info in prompt_data:
# 这里需要根据实际情况实现数据恢复逻辑
pass
# 恢复模板数据
if 'data/templates.json' in zipf.namelist():
template_data = json.loads(zipf.read('data/templates.json'))
for template_info in template_data:
# 这里需要根据实际情况实现数据恢复逻辑
pass
except Exception as e:
print(f"恢复数据库数据失败: {str(e)}")
def _restore_config_files(self, zipf):
"""恢复配置文件"""
try:
# 恢复配置文件
for file_info in zipf.filelist:
if file_info.filename.startswith('config/'):
filename = file_info.filename.replace('config/', '')
if filename not in ['.env']: # 不覆盖敏感配置文件
zipf.extract(file_info, '.')
except Exception as e:
print(f"恢复配置文件失败: {str(e)}")
def _auto_backup_worker(self):
"""自动备份工作线程"""
schedule.every().day.at("02:00").do(self._perform_auto_backup)
while self.auto_backup_enabled:
schedule.run_pending()
time.sleep(60)
def _perform_auto_backup(self):
"""执行自动备份"""
try:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f"auto_backup_{timestamp}.zip"
backup_path = os.path.join(self.backup_dir, filename)
# 创建数据备份
self._create_data_backup(backup_path)
# 清理旧备份保留最近7天
self._cleanup_old_backups()
print(f"自动备份完成: {filename}")
except Exception as e:
print(f"自动备份失败: {str(e)}")
def _cleanup_old_backups(self):
"""清理旧备份文件"""
try:
cutoff_time = datetime.now() - timedelta(days=7)
for filename in os.listdir(self.backup_dir):
if filename.startswith('auto_backup_') and filename.endswith('.zip'):
file_path = os.path.join(self.backup_dir, filename)
if os.path.getctime(file_path) < cutoff_time.timestamp():
os.remove(file_path)
print(f"删除旧备份文件: {filename}")
except Exception as e:
print(f"清理旧备份失败: {str(e)}")
def _format_file_size(self, size_bytes):
"""格式化文件大小"""
if size_bytes == 0:
return "0B"
size_names = ["B", "KB", "MB", "GB"]
i = 0
while size_bytes >= 1024 and i < len(size_names) - 1:
size_bytes /= 1024.0
i += 1
return f"{size_bytes:.1f}{size_names[i]}"

View File

@@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
"""
批量操作管理视图
"""
from flask_admin import BaseView, expose
from flask_login import login_required, current_user
from flask import request, jsonify, send_file
from src.flask_prompt_master.models.models import User, Prompt, PromptTemplate
from src.flask_prompt_master import db
import csv
import io
from datetime import datetime
class BatchAdminView(BaseView):
"""批量操作管理视图"""
@expose('/')
@login_required
def index(self):
"""批量操作首页"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return self.render('admin/403.html')
return self.render('admin/batch_operations.html')
@expose('/export/users')
@login_required
def export_users(self):
"""导出用户数据"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
# 获取用户数据
users = User.query.all()
# 创建CSV文件
output = io.StringIO()
writer = csv.writer(output)
# 写入表头
writer.writerow(['用户ID', '用户名', '昵称', '邮箱', '手机号', '性别', '状态', '注册时间'])
# 写入数据
for user in users:
writer.writerow([
user.uid,
user.login_name,
user.nickname,
user.email,
user.mobile,
'' if user.sex == 1 else '' if user.sex == 2 else '保密',
'正常' if user.status == 1 else '禁用',
user.created_time.strftime('%Y-%m-%d %H:%M:%S') if user.created_time else ''
])
# 创建文件响应
output.seek(0)
return send_file(
io.BytesIO(output.getvalue().encode('utf-8-sig')),
mimetype='text/csv',
as_attachment=True,
download_name=f'users_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
)
except Exception as e:
return jsonify({'success': False, 'message': f'导出失败: {str(e)}'})
@expose('/export/prompts')
@login_required
def export_prompts(self):
"""导出提示词数据"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'content_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
# 获取提示词数据
prompts = Prompt.query.all()
# 创建CSV文件
output = io.StringIO()
writer = csv.writer(output)
# 写入表头
writer.writerow(['ID', '用户ID', '输入文本', '生成文本', '创建时间'])
# 写入数据
for prompt in prompts:
writer.writerow([
prompt.id,
prompt.user_id,
prompt.input_text[:100] + '...' if len(prompt.input_text) > 100 else prompt.input_text,
prompt.generated_text[:100] + '...' if len(prompt.generated_text) > 100 else prompt.generated_text,
prompt.created_at.strftime('%Y-%m-%d %H:%M:%S') if prompt.created_at else ''
])
# 创建文件响应
output.seek(0)
return send_file(
io.BytesIO(output.getvalue().encode('utf-8-sig')),
mimetype='text/csv',
as_attachment=True,
download_name=f'prompts_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
)
except Exception as e:
return jsonify({'success': False, 'message': f'导出失败: {str(e)}'})
@expose('/batch/disable-users', methods=['POST'])
@login_required
def batch_disable_users(self):
"""批量禁用用户"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'user_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
data = request.get_json()
user_ids = data.get('user_ids', [])
if not user_ids:
return jsonify({'success': False, 'message': '请选择要禁用的用户'})
# 批量更新用户状态
User.query.filter(User.uid.in_(user_ids)).update(
{'status': 0, 'updated_time': datetime.now()},
synchronize_session=False
)
db.session.commit()
return jsonify({'success': True, 'message': f'成功禁用 {len(user_ids)} 个用户'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'操作失败: {str(e)}'})
@expose('/batch/enable-users', methods=['POST'])
@login_required
def batch_enable_users(self):
"""批量启用用户"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'user_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
data = request.get_json()
user_ids = data.get('user_ids', [])
if not user_ids:
return jsonify({'success': False, 'message': '请选择要启用的用户'})
# 批量更新用户状态
User.query.filter(User.uid.in_(user_ids)).update(
{'status': 1, 'updated_time': datetime.now()},
synchronize_session=False
)
db.session.commit()
return jsonify({'success': True, 'message': f'成功启用 {len(user_ids)} 个用户'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'操作失败: {str(e)}'})
@expose('/batch/delete-prompts', methods=['POST'])
@login_required
def batch_delete_prompts(self):
"""批量删除提示词"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'content_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
data = request.get_json()
prompt_ids = data.get('prompt_ids', [])
if not prompt_ids:
return jsonify({'success': False, 'message': '请选择要删除的提示词'})
# 批量删除提示词
Prompt.query.filter(Prompt.id.in_(prompt_ids)).delete(synchronize_session=False)
db.session.commit()
return jsonify({'success': True, 'message': f'成功删除 {len(prompt_ids)} 个提示词'})
except Exception as e:
db.session.rollback()
return jsonify({'success': False, 'message': f'操作失败: {str(e)}'})

View File

@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
"""
系统监控管理视图
"""
from flask_admin import BaseView, expose
from flask_login import login_required, current_user
from flask import request, jsonify
import psutil
import os
import time
from datetime import datetime, timedelta
import threading
import queue
class MonitorAdminView(BaseView):
"""系统监控管理视图"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.alert_queue = queue.Queue()
self.monitoring = False
self.monitor_thread = None
@expose('/')
@login_required
def index(self):
"""监控首页"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return self.render('admin/403.html')
# 获取系统状态
system_status = self._get_system_status()
return self.render('admin/monitor_dashboard.html', system_status=system_status)
@expose('/api/system-status')
@login_required
def api_system_status(self):
"""获取系统状态API"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
status = self._get_system_status()
return jsonify({'success': True, 'data': status})
except Exception as e:
return jsonify({'success': False, 'message': f'获取系统状态失败: {str(e)}'})
@expose('/api/start-monitoring', methods=['POST'])
@login_required
def start_monitoring(self):
"""启动监控"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
if not self.monitoring:
self.monitoring = True
self.monitor_thread = threading.Thread(target=self._monitor_system, daemon=True)
self.monitor_thread.start()
return jsonify({'success': True, 'message': '监控已启动'})
else:
return jsonify({'success': False, 'message': '监控已在运行中'})
except Exception as e:
return jsonify({'success': False, 'message': f'启动监控失败: {str(e)}'})
@expose('/api/stop-monitoring', methods=['POST'])
@login_required
def stop_monitoring(self):
"""停止监控"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
self.monitoring = False
return jsonify({'success': True, 'message': '监控已停止'})
except Exception as e:
return jsonify({'success': False, 'message': f'停止监控失败: {str(e)}'})
@expose('/api/alerts')
@login_required
def get_alerts(self):
"""获取告警信息"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
alerts = []
while not self.alert_queue.empty():
alerts.append(self.alert_queue.get_nowait())
return jsonify({'success': True, 'data': alerts})
except Exception as e:
return jsonify({'success': False, 'message': f'获取告警失败: {str(e)}'})
def _get_system_status(self):
"""获取系统状态"""
try:
# CPU使用率
cpu_percent = psutil.cpu_percent(interval=1)
# 内存使用情况
memory = psutil.virtual_memory()
memory_percent = memory.percent
memory_used = memory.used / (1024**3) # GB
memory_total = memory.total / (1024**3) # GB
# 磁盘使用情况
disk = psutil.disk_usage('/')
disk_percent = disk.percent
disk_used = disk.used / (1024**3) # GB
disk_total = disk.total / (1024**3) # GB
# 网络使用情况
network = psutil.net_io_counters()
network_sent = network.bytes_sent / (1024**2) # MB
network_recv = network.bytes_recv / (1024**2) # MB
# 进程数量
process_count = len(psutil.pids())
# 系统运行时间
boot_time = datetime.fromtimestamp(psutil.boot_time())
uptime = datetime.now() - boot_time
# 当前时间
current_time = datetime.now()
return {
'cpu_percent': cpu_percent,
'memory_percent': memory_percent,
'memory_used': round(memory_used, 2),
'memory_total': round(memory_total, 2),
'disk_percent': disk_percent,
'disk_used': round(disk_used, 2),
'disk_total': round(disk_total, 2),
'network_sent': round(network_sent, 2),
'network_recv': round(network_recv, 2),
'process_count': process_count,
'uptime': str(uptime).split('.')[0],
'current_time': current_time.strftime('%Y-%m-%d %H:%M:%S'),
'monitoring': self.monitoring
}
except Exception as e:
return {
'error': str(e),
'monitoring': self.monitoring
}
def _monitor_system(self):
"""系统监控线程"""
while self.monitoring:
try:
status = self._get_system_status()
# 检查告警条件
if 'error' not in status:
if status['cpu_percent'] > 80:
self.alert_queue.put({
'level': 'warning',
'message': f'CPU使用率过高: {status["cpu_percent"]}%',
'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
if status['memory_percent'] > 85:
self.alert_queue.put({
'level': 'warning',
'message': f'内存使用率过高: {status["memory_percent"]}%',
'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
if status['disk_percent'] > 90:
self.alert_queue.put({
'level': 'danger',
'message': f'磁盘使用率过高: {status["disk_percent"]}%',
'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
time.sleep(30) # 每30秒检查一次
except Exception as e:
self.alert_queue.put({
'level': 'error',
'message': f'监控异常: {str(e)}',
'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
})
time.sleep(60) # 异常时等待1分钟再重试

View File

@@ -0,0 +1,254 @@
# -*- coding: utf-8 -*-
"""
高级报表管理视图
"""
from flask_admin import BaseView, expose
from flask_login import login_required, current_user
from flask import request, jsonify, send_file
from src.flask_prompt_master.models.models import User, Prompt, PromptTemplate
from src.flask_prompt_master import db
from sqlalchemy import func, extract
from datetime import datetime, timedelta
import csv
import io
import json
class ReportAdminView(BaseView):
"""高级报表管理视图"""
@expose('/')
@login_required
def index(self):
"""报表首页"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return self.render('admin/403.html')
return self.render('admin/report_dashboard.html')
@expose('/user-report')
@login_required
def user_report(self):
"""用户报表"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return self.render('admin/403.html')
# 获取报表参数
start_date = request.args.get('start_date', (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'))
end_date = request.args.get('end_date', datetime.now().strftime('%Y-%m-%d'))
report_type = request.args.get('type', 'daily')
# 生成报表数据
report_data = self._generate_user_report(start_date, end_date, report_type)
return self.render('admin/user_report.html',
report_data=report_data,
start_date=start_date,
end_date=end_date,
report_type=report_type)
@expose('/prompt-report')
@login_required
def prompt_report(self):
"""提示词报表"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'content_admin']:
return self.render('admin/403.html')
# 获取报表参数
start_date = request.args.get('start_date', (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'))
end_date = request.args.get('end_date', datetime.now().strftime('%Y-%m-%d'))
category = request.args.get('category', 'all')
# 生成报表数据
report_data = self._generate_prompt_report(start_date, end_date, category)
return self.render('admin/prompt_report.html',
report_data=report_data,
start_date=start_date,
end_date=end_date,
category=category)
@expose('/export-report')
@login_required
def export_report(self):
"""导出报表"""
if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']:
return jsonify({'success': False, 'message': '权限不足'})
try:
report_type = request.args.get('type', 'user')
start_date = request.args.get('start_date', (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'))
end_date = request.args.get('end_date', datetime.now().strftime('%Y-%m-%d'))
if report_type == 'user':
data = self._generate_user_report(start_date, end_date, 'daily')
filename = f'user_report_{start_date}_to_{end_date}.csv'
elif report_type == 'prompt':
data = self._generate_prompt_report(start_date, end_date, 'all')
filename = f'prompt_report_{start_date}_to_{end_date}.csv'
else:
return jsonify({'success': False, 'message': '不支持的报表类型'})
# 创建CSV文件
output = io.StringIO()
writer = csv.writer(output)
# 写入表头
if report_type == 'user':
writer.writerow(['日期', '新增用户', '活跃用户', '总用户数'])
for item in data.get('daily_stats', []):
writer.writerow([item['date'], item['new_users'], item['active_users'], item['total_users']])
else:
writer.writerow(['日期', '生成数量', '平均长度', '用户数'])
for item in data.get('daily_stats', []):
writer.writerow([item['date'], item['count'], item['avg_length'], item['users']])
# 创建文件响应
output.seek(0)
return send_file(
io.BytesIO(output.getvalue().encode('utf-8-sig')),
mimetype='text/csv',
as_attachment=True,
download_name=filename
)
except Exception as e:
return jsonify({'success': False, 'message': f'导出失败: {str(e)}'})
def _generate_user_report(self, start_date, end_date, report_type):
"""生成用户报表"""
try:
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
# 总体统计
total_users = User.query.count()
active_users = User.query.filter_by(status=1).count()
new_users_period = User.query.filter(
User.created_time >= start_dt,
User.created_time <= end_dt
).count()
# 每日统计
daily_stats = []
current_date = start_dt
while current_date <= end_dt:
next_date = current_date + timedelta(days=1)
# 当日新增用户
new_users = User.query.filter(
User.created_time >= current_date,
User.created_time < next_date
).count()
# 当日活跃用户(有生成提示词的用户)
active_users = db.session.query(func.count(func.distinct(Prompt.user_id))).filter(
Prompt.created_at >= current_date,
Prompt.created_at < next_date
).scalar() or 0
# 累计用户数
total_users_date = User.query.filter(
User.created_time <= next_date
).count()
daily_stats.append({
'date': current_date.strftime('%Y-%m-%d'),
'new_users': new_users,
'active_users': active_users,
'total_users': total_users_date
})
current_date = next_date
return {
'total_users': total_users,
'active_users': active_users,
'new_users_period': new_users_period,
'daily_stats': daily_stats,
'period_days': (end_dt - start_dt).days + 1
}
except Exception as e:
return {
'error': str(e),
'total_users': 0,
'active_users': 0,
'new_users_period': 0,
'daily_stats': [],
'period_days': 0
}
def _generate_prompt_report(self, start_date, end_date, category):
"""生成提示词报表"""
try:
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
# 总体统计
total_prompts = Prompt.query.filter(
Prompt.created_at >= start_dt,
Prompt.created_at <= end_dt
).count()
# 平均长度
avg_length = db.session.query(func.avg(func.length(Prompt.input_text))).filter(
Prompt.created_at >= start_dt,
Prompt.created_at <= end_dt
).scalar() or 0
# 活跃用户数
active_users = db.session.query(func.count(func.distinct(Prompt.user_id))).filter(
Prompt.created_at >= start_dt,
Prompt.created_at <= end_dt
).scalar() or 0
# 每日统计
daily_stats = []
current_date = start_dt
while current_date <= end_dt:
next_date = current_date + timedelta(days=1)
# 当日生成数量
count = Prompt.query.filter(
Prompt.created_at >= current_date,
Prompt.created_at < next_date
).count()
# 当日平均长度
avg_len = db.session.query(func.avg(func.length(Prompt.input_text))).filter(
Prompt.created_at >= current_date,
Prompt.created_at < next_date
).scalar() or 0
# 当日用户数
users = db.session.query(func.count(func.distinct(Prompt.user_id))).filter(
Prompt.created_at >= current_date,
Prompt.created_at < next_date
).scalar() or 0
daily_stats.append({
'date': current_date.strftime('%Y-%m-%d'),
'count': count,
'avg_length': round(avg_len, 2),
'users': users
})
current_date = next_date
return {
'total_prompts': total_prompts,
'avg_length': round(avg_length, 2),
'active_users': active_users,
'daily_stats': daily_stats,
'period_days': (end_dt - start_dt).days + 1
}
except Exception as e:
return {
'error': str(e),
'total_prompts': 0,
'avg_length': 0,
'active_users': 0,
'daily_stats': [],
'period_days': 0
}

View File

@@ -0,0 +1,177 @@
{% extends 'admin/master.html' %}
{% block title %}数据分析 - 图表{% endblock %}
{% block head %}
{{ super() }}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-chart-bar"></i> 数据分析图表
</h1>
</div>
</div>
<!-- 图表区域 -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-line"></i> 用户注册趋势
</h6>
</div>
<div class="card-body">
<div id="userTrendChart" style="height: 400px;"></div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-line"></i> 提示词生成趋势
</h6>
</div>
<div class="card-body">
<div id="promptTrendChart" style="height: 400px;"></div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-pie"></i> 用户状态分布
</h6>
</div>
<div class="card-body">
<div id="userStatusChart" style="height: 400px;"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-area"></i> 系统使用统计
</h6>
</div>
<div class="card-body">
<div id="systemStatsChart" style="height: 400px;"></div>
</div>
</div>
</div>
</div>
<!-- 返回按钮 -->
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-body">
<a href="{{ url_for('analytics_admin.index') }}" class="btn btn-primary">
<i class="fas fa-arrow-left"></i> 返回仪表板
</a>
<a href="{{ url_for('admin.index') }}" class="btn btn-secondary">
<i class="fas fa-home"></i> 返回首页
</a>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 模拟图表数据(实际项目中会从后端获取)
// 用户注册趋势图
var userTrendData = [{
x: ['2025-08-20', '2025-08-21', '2025-08-22', '2025-08-23', '2025-08-24', '2025-08-25', '2025-08-26', '2025-08-27', '2025-08-28', '2025-08-29'],
y: [5, 8, 12, 15, 10, 18, 22, 25, 30, 35],
type: 'scatter',
mode: 'lines+markers',
name: '注册用户',
line: {color: '#4e73df', width: 3},
marker: {size: 8}
}];
var userTrendLayout = {
title: '用户注册趋势最近10天',
xaxis: {title: '日期'},
yaxis: {title: '注册人数'},
height: 400,
margin: {l: 50, r: 50, t: 50, b: 50}
};
Plotly.newPlot('userTrendChart', userTrendData, userTrendLayout);
// 提示词生成趋势图
var promptTrendData = [{
x: ['2025-08-20', '2025-08-21', '2025-08-22', '2025-08-23', '2025-08-24', '2025-08-25', '2025-08-26', '2025-08-27', '2025-08-28', '2025-08-29'],
y: [20, 35, 45, 60, 40, 70, 85, 95, 110, 125],
type: 'scatter',
mode: 'lines+markers',
name: '生成提示词',
line: {color: '#1cc88a', width: 3},
marker: {size: 8}
}];
var promptTrendLayout = {
title: '提示词生成趋势最近10天',
xaxis: {title: '日期'},
yaxis: {title: '生成数量'},
height: 400,
margin: {l: 50, r: 50, t: 50, b: 50}
};
Plotly.newPlot('promptTrendChart', promptTrendData, promptTrendLayout);
// 用户状态分布图
var userStatusData = [{
values: [85, 15],
labels: ['活跃用户', '禁用用户'],
type: 'pie',
marker: {colors: ['#28a745', '#dc3545']},
textinfo: 'label+percent'
}];
var userStatusLayout = {
title: '用户状态分布',
height: 400,
margin: {l: 50, r: 50, t: 50, b: 50}
};
Plotly.newPlot('userStatusChart', userStatusData, userStatusLayout);
// 系统使用统计图
var systemStatsData = [{
x: ['用户管理', '提示词管理', '模板管理', '数据分析', '批量操作'],
y: [120, 85, 45, 65, 30],
type: 'bar',
marker: {color: ['#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b']}
}];
var systemStatsLayout = {
title: '各模块使用统计',
xaxis: {title: '功能模块'},
yaxis: {title: '使用次数'},
height: 400,
margin: {l: 50, r: 50, t: 50, b: 50}
};
Plotly.newPlot('systemStatsChart', systemStatsData, systemStatsLayout);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,239 @@
{% extends 'admin/master.html' %}
{% block title %}数据分析 - 仪表板{% endblock %}
{% block head %}
{{ super() }}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-chart-line"></i> 数据分析仪表板
</h1>
</div>
</div>
<!-- 统计卡片 -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
总用户数
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.total_users }}</div>
<div class="text-xs text-muted">今日新增: {{ stats.new_users_today }}</div>
</div>
<div class="col-auto">
<i class="fas fa-users fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
总提示词
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.total_prompts }}</div>
<div class="text-xs text-muted">今日生成: {{ stats.today_prompts }}</div>
</div>
<div class="col-auto">
<i class="fas fa-magic fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
活跃用户
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.active_users }}</div>
<div class="text-xs text-muted">本周活跃: {{ stats.active_users_week }}</div>
</div>
<div class="col-auto">
<i class="fas fa-user-check fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
模板数量
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ stats.total_templates }}</div>
<div class="text-xs text-muted">默认模板: {{ stats.default_templates }}</div>
</div>
<div class="col-auto">
<i class="fas fa-clipboard-list fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 趋势图表 -->
<div class="row mb-4">
<div class="col-xl-8 col-lg-7">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-area"></i> 用户注册趋势
</h6>
</div>
<div class="card-body">
<div id="userTrendChart" style="height: 300px;"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-5">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-pie"></i> 用户状态分布
</h6>
</div>
<div class="card-body">
<div id="userStatusChart" style="height: 300px;"></div>
</div>
</div>
</div>
</div>
<!-- 快速操作 -->
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-bolt"></i> 快速操作
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<a href="{{ url_for('analytics_admin.charts') }}" class="btn btn-primary btn-block">
<i class="fas fa-chart-bar"></i> 详细图表
</a>
</div>
<div class="col-md-3 mb-3">
<a href="{{ url_for('admin_user.index_view') }}" class="btn btn-success btn-block">
<i class="fas fa-users"></i> 用户管理
</a>
</div>
<div class="col-md-3 mb-3">
<a href="{{ url_for('admin_prompt.index_view') }}" class="btn btn-info btn-block">
<i class="fas fa-magic"></i> 提示词管理
</a>
</div>
<div class="col-md-3 mb-3">
<a href="{{ url_for('admin_system.index') }}" class="btn btn-warning btn-block">
<i class="fas fa-cogs"></i> 系统管理
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.border-left-primary {
border-left: 0.25rem solid #4e73df !important;
}
.border-left-success {
border-left: 0.25rem solid #1cc88a !important;
}
.border-left-info {
border-left: 0.25rem solid #36b9cc !important;
}
.border-left-warning {
border-left: 0.25rem solid #f6c23e !important;
}
.text-gray-300 {
color: #dddfeb !important;
}
.text-gray-800 {
color: #5a5c69 !important;
}
.font-weight-bold {
font-weight: 700 !important;
}
.text-xs {
font-size: 0.7rem;
}
.btn-block {
display: block;
width: 100%;
}
</style>
<script>
// 模拟图表数据(实际项目中会从后端获取)
document.addEventListener('DOMContentLoaded', function() {
// 用户注册趋势图
var userTrendData = [{
x: ['2025-08-20', '2025-08-21', '2025-08-22', '2025-08-23', '2025-08-24', '2025-08-25', '2025-08-26'],
y: [5, 8, 12, 15, 10, 18, 22],
type: 'scatter',
mode: 'lines+markers',
name: '注册用户',
line: {color: '#4e73df', width: 3},
marker: {size: 8}
}];
var userTrendLayout = {
title: '用户注册趋势',
xaxis: {title: '日期'},
yaxis: {title: '注册人数'},
height: 300,
margin: {l: 50, r: 50, t: 50, b: 50}
};
Plotly.newPlot('userTrendChart', userTrendData, userTrendLayout);
// 用户状态分布图
var userStatusData = [{
values: [{{ stats.active_users }}, {{ stats.total_users - stats.active_users }}],
labels: ['活跃用户', '禁用用户'],
type: 'pie',
marker: {colors: ['#28a745', '#dc3545']},
textinfo: 'label+percent'
}];
var userStatusLayout = {
title: '用户状态分布',
height: 300,
margin: {l: 50, r: 50, t: 50, b: 50}
};
Plotly.newPlot('userStatusChart', userStatusData, userStatusLayout);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,444 @@
{% extends 'admin/master.html' %}
{% block title %}API管理{% endblock %}
{% block head %}
{{ super() }}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-code"></i> API管理
</h1>
</div>
</div>
<!-- API统计卡片 -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
今日调用
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ api_stats.today_calls }}</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
本周调用
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ api_stats.week_calls }}</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar-week fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
平均响应时间
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ api_stats.avg_response_time }}s</div>
</div>
<div class="col-auto">
<i class="fas fa-clock fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
成功率
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ api_stats.success_rate }}%</div>
</div>
<div class="col-auto">
<i class="fas fa-percentage fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- API调用趋势 -->
<div class="row mb-4">
<div class="col-xl-8 col-lg-7">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-area"></i> API调用趋势
</h6>
</div>
<div class="card-body">
<div id="apiTrendChart" style="height: 300px;"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-5">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-info-circle"></i> API信息
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-12 mb-3">
<div class="text-center">
<h4 class="text-primary">{{ api_stats.total_calls }}</h4>
<p class="text-muted">总调用次数</p>
</div>
</div>
<div class="col-12 mb-3">
<div class="text-center">
<h4 class="text-success">{{ api_stats.active_users }}</h4>
<p class="text-muted">活跃用户数</p>
</div>
</div>
<div class="col-12">
<div class="text-center">
<h4 class="text-info">{{ api_stats.month_calls }}</h4>
<p class="text-muted">本月调用次数</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- API管理功能 -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-cogs"></i> API管理功能
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<div class="card border-left-primary">
<div class="card-body text-center">
<i class="fas fa-list fa-3x text-primary mb-3"></i>
<h5 class="card-title">调用记录</h5>
<p class="card-text">查看详细的API调用记录和日志</p>
<button class="btn btn-primary" onclick="showApiCalls()">
<i class="fas fa-eye"></i> 查看记录
</button>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card border-left-success">
<div class="card-body text-center">
<i class="fas fa-shield-alt fa-3x text-success mb-3"></i>
<h5 class="card-title">限流管理</h5>
<p class="card-text">配置API调用频率限制和访问控制</p>
<a href="{{ url_for('api_admin.rate_limits') }}" class="btn btn-success">
<i class="fas fa-cog"></i> 限流设置
</a>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card border-left-info">
<div class="card-body text-center">
<i class="fas fa-key fa-3x text-info mb-3"></i>
<h5 class="card-title">密钥管理</h5>
<p class="card-text">管理API访问密钥和权限</p>
<a href="{{ url_for('api_admin.api_keys') }}" class="btn btn-info">
<i class="fas fa-key"></i> 密钥管理
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- API调用记录表格 -->
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-table"></i> 最近API调用记录
</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="apiCallsTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>用户ID</th>
<th>API类型</th>
<th>输入内容</th>
<th>状态</th>
<th>响应时间</th>
<th>IP地址</th>
<th>调用时间</th>
</tr>
</thead>
<tbody id="apiCallsTableBody">
<tr>
<td colspan="8" class="text-center text-muted">
<i class="fas fa-spinner fa-spin"></i> 加载中...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- API调用记录模态框 -->
<div class="modal fade" id="apiCallsModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-list"></i> API调用记录
</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-3">
<label for="startDate">开始日期</label>
<input type="date" class="form-control" id="startDate">
</div>
<div class="col-md-3">
<label for="endDate">结束日期</label>
<input type="date" class="form-control" id="endDate">
</div>
<div class="col-md-3">
<label for="apiType">API类型</label>
<select class="form-control" id="apiType">
<option value="all">全部</option>
<option value="prompt_generation">提示词生成</option>
</select>
</div>
<div class="col-md-3">
<label>&nbsp;</label>
<button class="btn btn-primary btn-block" onclick="loadApiCalls()">
<i class="fas fa-search"></i> 查询
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered" id="detailedApiCallsTable">
<thead>
<tr>
<th>ID</th>
<th>用户ID</th>
<th>API类型</th>
<th>输入内容</th>
<th>状态</th>
<th>响应时间</th>
<th>IP地址</th>
<th>调用时间</th>
</tr>
</thead>
<tbody id="detailedApiCallsTableBody">
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<style>
.border-left-primary {
border-left: 0.25rem solid #4e73df !important;
}
.border-left-success {
border-left: 0.25rem solid #1cc88a !important;
}
.border-left-info {
border-left: 0.25rem solid #36b9cc !important;
}
.border-left-warning {
border-left: 0.25rem solid #f6c23e !important;
}
.text-gray-300 {
color: #dddfeb !important;
}
.text-gray-800 {
color: #5a5c69 !important;
}
.font-weight-bold {
font-weight: 700 !important;
}
.text-xs {
font-size: 0.7rem;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 初始化图表
initApiTrendChart();
// 加载API调用记录
loadRecentApiCalls();
// 设置默认日期
const today = new Date();
const weekAgo = new Date(today.getTime() - (7 * 24 * 60 * 60 * 1000));
document.getElementById('startDate').value = weekAgo.toISOString().split('T')[0];
document.getElementById('endDate').value = today.toISOString().split('T')[0];
});
function initApiTrendChart() {
const dailyCalls = {{ api_stats.daily_calls | tojson }};
if (dailyCalls.length > 0) {
const dates = dailyCalls.map(item => item.date);
const counts = dailyCalls.map(item => item.count);
var data = [{
x: dates,
y: counts,
type: 'scatter',
mode: 'lines+markers',
name: 'API调用次数',
line: {color: '#4e73df', width: 3},
marker: {size: 8}
}];
var layout = {
title: 'API调用趋势最近7天',
xaxis: {title: '日期'},
yaxis: {title: '调用次数'},
height: 300,
margin: {l: 50, r: 50, t: 50, b: 50}
};
Plotly.newPlot('apiTrendChart', data, layout);
}
}
function loadRecentApiCalls() {
fetch('{{ url_for("api_admin.api_calls") }}')
.then(response => response.json())
.then(data => {
if (data.success) {
displayApiCalls(data.data, 'apiCallsTableBody');
} else {
document.getElementById('apiCallsTableBody').innerHTML =
'<tr><td colspan="8" class="text-center text-muted">加载失败</td></tr>';
}
})
.catch(error => {
document.getElementById('apiCallsTableBody').innerHTML =
'<tr><td colspan="8" class="text-center text-muted">加载失败</td></tr>';
});
}
function showApiCalls() {
$('#apiCallsModal').modal('show');
loadApiCalls();
}
function loadApiCalls() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const apiType = document.getElementById('apiType').value;
const url = `{{ url_for("api_admin.api_calls") }}?start_date=${startDate}&end_date=${endDate}&type=${apiType}`;
fetch(url)
.then(response => response.json())
.then(data => {
if (data.success) {
displayApiCalls(data.data, 'detailedApiCallsTableBody');
} else {
document.getElementById('detailedApiCallsTableBody').innerHTML =
'<tr><td colspan="8" class="text-center text-muted">加载失败</td></tr>';
}
})
.catch(error => {
document.getElementById('detailedApiCallsTableBody').innerHTML =
'<tr><td colspan="8" class="text-center text-muted">加载失败</td></tr>';
});
}
function displayApiCalls(calls, tableBodyId) {
const tableBody = document.getElementById(tableBodyId);
if (calls.length === 0) {
tableBody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无数据</td></tr>';
return;
}
let html = '';
calls.forEach(call => {
const statusClass = call.status === 'success' ? 'text-success' : 'text-danger';
const statusIcon = call.status === 'success' ? 'fa-check' : 'fa-times';
html += `
<tr>
<td>${call.id}</td>
<td>${call.user_id}</td>
<td><span class="badge badge-primary">${call.api_type}</span></td>
<td>${call.input_text}</td>
<td><i class="fas ${statusIcon} ${statusClass}"></i> ${call.status}</td>
<td>${call.response_time}s</td>
<td>${call.ip_address}</td>
<td>${call.created_at}</td>
</tr>
`;
});
tableBody.innerHTML = html;
}
</script>
{% endblock %}

View File

@@ -0,0 +1,418 @@
{% extends 'admin/master.html' %}
{% block title %}数据备份管理{% endblock %}
{% block head %}
{{ super() }}
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-database"></i> 数据备份管理
</h1>
</div>
</div>
<!-- 备份操作 -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-plus"></i> 创建备份
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<div class="card border-left-primary">
<div class="card-body text-center">
<i class="fas fa-database fa-3x text-primary mb-3"></i>
<h5 class="card-title">完整备份</h5>
<p class="card-text">备份所有数据、配置和日志文件</p>
<button class="btn btn-primary" onclick="createBackup('full')">
<i class="fas fa-download"></i> 创建完整备份
</button>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card border-left-success">
<div class="card-body text-center">
<i class="fas fa-table fa-3x text-success mb-3"></i>
<h5 class="card-title">数据备份</h5>
<p class="card-text">仅备份数据库数据</p>
<button class="btn btn-success" onclick="createBackup('data')">
<i class="fas fa-download"></i> 创建数据备份
</button>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card border-left-info">
<div class="card-body text-center">
<i class="fas fa-cogs fa-3x text-info mb-3"></i>
<h5 class="card-title">配置备份</h5>
<p class="card-text">仅备份系统配置文件</p>
<button class="btn btn-info" onclick="createBackup('config')">
<i class="fas fa-download"></i> 创建配置备份
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 自动备份设置 -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-clock"></i> 自动备份设置
</h6>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<h6>自动备份</h6>
<p class="text-muted mb-0">每天凌晨2点自动创建数据备份保留最近7天的备份文件</p>
</div>
<div class="col-md-4 text-right">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="autoBackupSwitch">
<label class="custom-control-label" for="autoBackupSwitch">启用自动备份</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 备份文件列表 -->
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-list"></i> 备份文件列表
</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="backupTable" width="100%" cellspacing="0">
<thead>
<tr>
<th>文件名</th>
<th>大小</th>
<th>创建时间</th>
<th>修改时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for backup in backup_files %}
<tr>
<td>{{ backup.filename }}</td>
<td>{{ backup.size }}</td>
<td>{{ backup.created_time }}</td>
<td>{{ backup.modified_time }}</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-primary" onclick="downloadBackup('{{ backup.filename }}')">
<i class="fas fa-download"></i> 下载
</button>
<button class="btn btn-sm btn-warning" onclick="restoreBackup('{{ backup.filename }}')">
<i class="fas fa-undo"></i> 恢复
</button>
<button class="btn btn-sm btn-danger" onclick="deleteBackup('{{ backup.filename }}')">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center text-muted">
<i class="fas fa-inbox fa-2x mb-2"></i><br>
暂无备份文件
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 创建备份模态框 -->
<div class="modal fade" id="createBackupModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-download"></i> 创建备份
</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<form id="createBackupForm">
<div class="form-group">
<label for="backupType">备份类型</label>
<select class="form-control" id="backupType" required>
<option value="full">完整备份</option>
<option value="data">数据备份</option>
<option value="config">配置备份</option>
</select>
</div>
<div class="form-group">
<label for="backupName">备份名称(可选)</label>
<input type="text" class="form-control" id="backupName" placeholder="例如: 重要更新前备份">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="confirmCreateBackup()">
<i class="fas fa-download"></i> 创建备份
</button>
</div>
</div>
</div>
</div>
<!-- 恢复备份确认模态框 -->
<div class="modal fade" id="restoreBackupModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle text-warning"></i> 确认恢复备份
</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>警告:</strong>恢复备份将覆盖当前数据,此操作不可逆!
</div>
<p>您确定要恢复备份文件 <strong id="restoreFileName"></strong> 吗?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" onclick="confirmRestoreBackup()">
<i class="fas fa-undo"></i> 确认恢复
</button>
</div>
</div>
</div>
</div>
<style>
.border-left-primary {
border-left: 0.25rem solid #4e73df !important;
}
.border-left-success {
border-left: 0.25rem solid #1cc88a !important;
}
.border-left-info {
border-left: 0.25rem solid #36b9cc !important;
}
.custom-switch .custom-control-label::before {
width: 2rem;
height: 1rem;
}
.custom-switch .custom-control-label::after {
width: calc(1rem - 4px);
height: calc(1rem - 4px);
}
.btn-group .btn {
margin-right: 2px;
}
</style>
<script>
let currentBackupType = '';
let currentBackupName = '';
let currentRestoreFile = '';
document.addEventListener('DOMContentLoaded', function() {
// 绑定自动备份开关事件
document.getElementById('autoBackupSwitch').addEventListener('change', function() {
toggleAutoBackup(this.checked);
});
});
function createBackup(type) {
currentBackupType = type;
document.getElementById('backupType').value = type;
document.getElementById('backupName').value = '';
$('#createBackupModal').modal('show');
}
function confirmCreateBackup() {
const backupType = document.getElementById('backupType').value;
const backupName = document.getElementById('backupName').value;
$('#createBackupModal').modal('hide');
// 显示加载状态
showAlert('正在创建备份,请稍候...', 'info');
fetch('{{ url_for("backup_admin.create_backup") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: backupType,
name: backupName
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
// 刷新页面以显示新备份
setTimeout(() => {
location.reload();
}, 2000);
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
showAlert('创建备份失败: ' + error.message, 'error');
});
}
function downloadBackup(filename) {
window.open(`{{ url_for("backup_admin.download_backup", filename="") }}${filename}`, '_blank');
}
function restoreBackup(filename) {
currentRestoreFile = filename;
document.getElementById('restoreFileName').textContent = filename;
$('#restoreBackupModal').modal('show');
}
function confirmRestoreBackup() {
$('#restoreBackupModal').modal('hide');
// 显示加载状态
showAlert('正在恢复备份,请稍候...', 'info');
fetch('{{ url_for("backup_admin.restore_backup") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: currentRestoreFile
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
showAlert('恢复备份失败: ' + error.message, 'error');
});
}
function deleteBackup(filename) {
if (!confirm(`确定要删除备份文件 ${filename} 吗?`)) {
return;
}
fetch('{{ url_for("backup_admin.delete_backup") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: filename
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
// 刷新页面
setTimeout(() => {
location.reload();
}, 1000);
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
showAlert('删除备份失败: ' + error.message, 'error');
});
}
function toggleAutoBackup(enabled) {
fetch('{{ url_for("backup_admin.toggle_auto_backup") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: enabled
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.message, 'success');
} else {
showAlert(data.message, 'error');
// 恢复开关状态
document.getElementById('autoBackupSwitch').checked = !enabled;
}
})
.catch(error => {
showAlert('操作失败: ' + error.message, 'error');
// 恢复开关状态
document.getElementById('autoBackupSwitch').checked = !enabled;
});
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="close" data-dismiss="alert">
<span>&times;</span>
</button>
`;
document.querySelector('.container-fluid').insertBefore(alertDiv, document.querySelector('.row'));
// 自动消失
setTimeout(() => {
alertDiv.remove();
}, 5000);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,300 @@
{% extends 'admin/master.html' %}
{% block title %}批量操作{% endblock %}
{% block head %}
{{ super() }}
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-tasks"></i> 批量操作管理
</h1>
</div>
</div>
<!-- 数据导出 -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-download"></i> 数据导出
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<div class="card border-left-primary">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-users"></i> 用户数据导出
</h5>
<p class="card-text">导出所有用户信息到CSV文件</p>
<a href="{{ url_for('batch_admin.export_users') }}" class="btn btn-primary">
<i class="fas fa-download"></i> 导出用户数据
</a>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card border-left-success">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-magic"></i> 提示词数据导出
</h5>
<p class="card-text">导出所有提示词信息到CSV文件</p>
<a href="{{ url_for('batch_admin.export_prompts') }}" class="btn btn-success">
<i class="fas fa-download"></i> 导出提示词数据
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 批量操作 -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-cogs"></i> 批量操作
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<div class="card border-left-warning">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-user-times"></i> 批量禁用用户
</h5>
<p class="card-text">选择用户ID进行批量禁用操作</p>
<div class="form-group">
<label for="disableUserIds">用户ID用逗号分隔</label>
<input type="text" class="form-control" id="disableUserIds" placeholder="例如: 1,2,3,4,5">
</div>
<button class="btn btn-warning" onclick="batchDisableUsers()">
<i class="fas fa-ban"></i> 批量禁用
</button>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card border-left-info">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-user-check"></i> 批量启用用户
</h5>
<p class="card-text">选择用户ID进行批量启用操作</p>
<div class="form-group">
<label for="enableUserIds">用户ID用逗号分隔</label>
<input type="text" class="form-control" id="enableUserIds" placeholder="例如: 1,2,3,4,5">
</div>
<button class="btn btn-info" onclick="batchEnableUsers()">
<i class="fas fa-check"></i> 批量启用
</button>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card border-left-danger">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-trash"></i> 批量删除提示词
</h5>
<p class="card-text">选择提示词ID进行批量删除操作</p>
<div class="form-group">
<label for="deletePromptIds">提示词ID用逗号分隔</label>
<input type="text" class="form-control" id="deletePromptIds" placeholder="例如: 1,2,3,4,5">
</div>
<button class="btn btn-danger" onclick="batchDeletePrompts()">
<i class="fas fa-trash"></i> 批量删除
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 操作日志 -->
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-history"></i> 操作日志
</h6>
</div>
<div class="card-body">
<div id="operationLog" class="alert alert-info">
<i class="fas fa-info-circle"></i> 操作日志将在这里显示
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.border-left-primary {
border-left: 0.25rem solid #4e73df !important;
}
.border-left-success {
border-left: 0.25rem solid #1cc88a !important;
}
.border-left-warning {
border-left: 0.25rem solid #f6c23e !important;
}
.border-left-info {
border-left: 0.25rem solid #36b9cc !important;
}
.border-left-danger {
border-left: 0.25rem solid #e74a3b !important;
}
</style>
<script>
function logOperation(message, type = 'info') {
const logDiv = document.getElementById('operationLog');
const timestamp = new Date().toLocaleString();
const logEntry = `<div class="mb-2"><strong>[${timestamp}]</strong> ${message}</div>`;
if (type === 'success') {
logDiv.innerHTML = logEntry + logDiv.innerHTML;
logDiv.className = 'alert alert-success';
} else if (type === 'error') {
logDiv.innerHTML = logEntry + logDiv.innerHTML;
logDiv.className = 'alert alert-danger';
} else {
logDiv.innerHTML = logEntry + logDiv.innerHTML;
logDiv.className = 'alert alert-info';
}
}
function batchDisableUsers() {
const userIds = document.getElementById('disableUserIds').value.trim();
if (!userIds) {
alert('请输入要禁用的用户ID');
return;
}
const idArray = userIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
if (idArray.length === 0) {
alert('请输入有效的用户ID');
return;
}
if (!confirm(`确定要禁用 ${idArray.length} 个用户吗?`)) {
return;
}
fetch('{{ url_for("batch_admin.batch_disable_users") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({user_ids: idArray})
})
.then(response => response.json())
.then(data => {
if (data.success) {
logOperation(data.message, 'success');
document.getElementById('disableUserIds').value = '';
} else {
logOperation(data.message, 'error');
}
})
.catch(error => {
logOperation('操作失败: ' + error.message, 'error');
});
}
function batchEnableUsers() {
const userIds = document.getElementById('enableUserIds').value.trim();
if (!userIds) {
alert('请输入要启用的用户ID');
return;
}
const idArray = userIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
if (idArray.length === 0) {
alert('请输入有效的用户ID');
return;
}
if (!confirm(`确定要启用 ${idArray.length} 个用户吗?`)) {
return;
}
fetch('{{ url_for("batch_admin.batch_enable_users") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({user_ids: idArray})
})
.then(response => response.json())
.then(data => {
if (data.success) {
logOperation(data.message, 'success');
document.getElementById('enableUserIds').value = '';
} else {
logOperation(data.message, 'error');
}
})
.catch(error => {
logOperation('操作失败: ' + error.message, 'error');
});
}
function batchDeletePrompts() {
const promptIds = document.getElementById('deletePromptIds').value.trim();
if (!promptIds) {
alert('请输入要删除的提示词ID');
return;
}
const idArray = promptIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
if (idArray.length === 0) {
alert('请输入有效的提示词ID');
return;
}
if (!confirm(`确定要删除 ${idArray.length} 个提示词吗?此操作不可恢复!`)) {
return;
}
fetch('{{ url_for("batch_admin.batch_delete_prompts") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({prompt_ids: idArray})
})
.then(response => response.json())
.then(data => {
if (data.success) {
logOperation(data.message, 'success');
document.getElementById('deletePromptIds').value = '';
} else {
logOperation(data.message, 'error');
}
})
.catch(error => {
logOperation('操作失败: ' + error.message, 'error');
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,437 @@
{% extends 'admin/master.html' %}
{% block title %}系统监控{% endblock %}
{% block head %}
{{ super() }}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-tachometer-alt"></i> 系统监控仪表板
</h1>
</div>
</div>
<!-- 监控控制 -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-cogs"></i> 监控控制
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<button id="startMonitoring" class="btn btn-success btn-lg">
<i class="fas fa-play"></i> 启动监控
</button>
<button id="stopMonitoring" class="btn btn-danger btn-lg" style="display: none;">
<i class="fas fa-stop"></i> 停止监控
</button>
</div>
<div class="col-md-6">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
监控状态: <span id="monitoringStatus">未启动</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 系统状态卡片 -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
CPU使用率
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800" id="cpuPercent">0%</div>
</div>
<div class="col-auto">
<i class="fas fa-microchip fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
内存使用率
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800" id="memoryPercent">0%</div>
<div class="text-xs text-muted" id="memoryInfo">0 GB / 0 GB</div>
</div>
<div class="col-auto">
<i class="fas fa-memory fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
磁盘使用率
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800" id="diskPercent">0%</div>
<div class="text-xs text-muted" id="diskInfo">0 GB / 0 GB</div>
</div>
<div class="col-auto">
<i class="fas fa-hdd fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
进程数量
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800" id="processCount">0</div>
</div>
<div class="col-auto">
<i class="fas fa-tasks fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 实时图表 -->
<div class="row mb-4">
<div class="col-xl-8 col-lg-7">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-area"></i> 系统资源使用趋势
</h6>
</div>
<div class="card-body">
<div id="systemTrendChart" style="height: 300px;"></div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-5">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-exclamation-triangle"></i> 系统告警
</h6>
</div>
<div class="card-body">
<div id="alertsContainer" style="max-height: 300px; overflow-y: auto;">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> 暂无告警信息
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 系统信息 -->
<div class="row">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-info-circle"></i> 系统信息
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<table class="table table-borderless">
<tr>
<td><strong>系统运行时间:</strong></td>
<td id="uptime">{{ system_status.uptime if system_status.uptime else '未知' }}</td>
</tr>
<tr>
<td><strong>当前时间:</strong></td>
<td id="currentTime">{{ system_status.current_time if system_status.current_time else '未知' }}</td>
</tr>
<tr>
<td><strong>网络发送:</strong></td>
<td id="networkSent">{{ system_status.network_sent if system_status.network_sent else 0 }} MB</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-borderless">
<tr>
<td><strong>网络接收:</strong></td>
<td id="networkRecv">{{ system_status.network_recv if system_status.network_recv else 0 }} MB</td>
</tr>
<tr>
<td><strong>监控状态:</strong></td>
<td id="monitoringStatusText">{{ '运行中' if system_status.monitoring else '未启动' }}</td>
</tr>
<tr>
<td><strong>最后更新:</strong></td>
<td id="lastUpdate">-</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.border-left-primary {
border-left: 0.25rem solid #4e73df !important;
}
.border-left-success {
border-left: 0.25rem solid #1cc88a !important;
}
.border-left-info {
border-left: 0.25rem solid #36b9cc !important;
}
.border-left-warning {
border-left: 0.25rem solid #f6c23e !important;
}
.text-gray-300 {
color: #dddfeb !important;
}
.text-gray-800 {
color: #5a5c69 !important;
}
.font-weight-bold {
font-weight: 700 !important;
}
.text-xs {
font-size: 0.7rem;
}
</style>
<script>
let monitoring = false;
let updateInterval;
document.addEventListener('DOMContentLoaded', function() {
// 初始化图表
initSystemChart();
// 绑定按钮事件
document.getElementById('startMonitoring').addEventListener('click', startMonitoring);
document.getElementById('stopMonitoring').addEventListener('click', stopMonitoring);
// 初始加载系统状态
updateSystemStatus();
});
function initSystemChart() {
// 初始化系统趋势图
var data = [{
x: [],
y: [],
type: 'scatter',
mode: 'lines',
name: 'CPU使用率',
line: {color: '#4e73df'}
}, {
x: [],
y: [],
type: 'scatter',
mode: 'lines',
name: '内存使用率',
line: {color: '#1cc88a'}
}];
var layout = {
title: '系统资源使用趋势',
xaxis: {title: '时间'},
yaxis: {title: '使用率 (%)'},
height: 300,
margin: {l: 50, r: 50, t: 50, b: 50}
};
Plotly.newPlot('systemTrendChart', data, layout);
}
function startMonitoring() {
fetch('{{ url_for("monitor_admin.start_monitoring") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
monitoring = true;
document.getElementById('startMonitoring').style.display = 'none';
document.getElementById('stopMonitoring').style.display = 'inline-block';
document.getElementById('monitoringStatus').textContent = '运行中';
document.getElementById('monitoringStatusText').textContent = '运行中';
// 开始定时更新
updateInterval = setInterval(updateSystemStatus, 5000);
showAlert('监控已启动', 'success');
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
showAlert('启动监控失败: ' + error.message, 'error');
});
}
function stopMonitoring() {
fetch('{{ url_for("monitor_admin.stop_monitoring") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
monitoring = false;
document.getElementById('startMonitoring').style.display = 'inline-block';
document.getElementById('stopMonitoring').style.display = 'none';
document.getElementById('monitoringStatus').textContent = '已停止';
document.getElementById('monitoringStatusText').textContent = '已停止';
// 停止定时更新
if (updateInterval) {
clearInterval(updateInterval);
}
showAlert('监控已停止', 'info');
} else {
showAlert(data.message, 'error');
}
})
.catch(error => {
showAlert('停止监控失败: ' + error.message, 'error');
});
}
function updateSystemStatus() {
fetch('{{ url_for("monitor_admin.api_system_status") }}')
.then(response => response.json())
.then(data => {
if (data.success) {
const status = data.data;
// 更新状态卡片
document.getElementById('cpuPercent').textContent = status.cpu_percent + '%';
document.getElementById('memoryPercent').textContent = status.memory_percent + '%';
document.getElementById('memoryInfo').textContent = status.memory_used + ' GB / ' + status.memory_total + ' GB';
document.getElementById('diskPercent').textContent = status.disk_percent + '%';
document.getElementById('diskInfo').textContent = status.disk_used + ' GB / ' + status.disk_total + ' GB';
document.getElementById('processCount').textContent = status.process_count;
document.getElementById('uptime').textContent = status.uptime;
document.getElementById('currentTime').textContent = status.current_time;
document.getElementById('networkSent').textContent = status.network_sent + ' MB';
document.getElementById('networkRecv').textContent = status.network_recv + ' MB';
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
// 更新图表
updateSystemChart(status);
// 检查告警
checkAlerts();
}
})
.catch(error => {
console.error('获取系统状态失败:', error);
});
}
function updateSystemChart(status) {
const now = new Date().toLocaleTimeString();
Plotly.extendTraces('systemTrendChart', {
x: [[now], [now]],
y: [[status.cpu_percent], [status.memory_percent]]
}, [0, 1]);
// 保持最近20个数据点
const maxPoints = 20;
const chart = document.getElementById('systemTrendChart');
if (chart.data[0].x.length > maxPoints) {
Plotly.relayout('systemTrendChart', {
xaxis: {
range: [chart.data[0].x[chart.data[0].x.length - maxPoints], chart.data[0].x[chart.data[0].x.length - 1]]
}
});
}
}
function checkAlerts() {
fetch('{{ url_for("monitor_admin.get_alerts") }}')
.then(response => response.json())
.then(data => {
if (data.success && data.data.length > 0) {
const alertsContainer = document.getElementById('alertsContainer');
alertsContainer.innerHTML = '';
data.data.forEach(alert => {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${alert.level === 'danger' ? 'danger' : alert.level === 'warning' ? 'warning' : 'info'}`;
alertDiv.innerHTML = `
<i class="fas fa-exclamation-triangle"></i>
<strong>[${alert.time}]</strong> ${alert.message}
`;
alertsContainer.appendChild(alertDiv);
});
}
})
.catch(error => {
console.error('获取告警失败:', error);
});
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="close" data-dismiss="alert">
<span>&times;</span>
</button>
`;
document.querySelector('.container-fluid').insertBefore(alertDiv, document.querySelector('.row'));
// 自动消失
setTimeout(() => {
alertDiv.remove();
}, 5000);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,319 @@
{% extends 'admin/master.html' %}
{% block title %}高级报表{% endblock %}
{% block head %}
{{ super() }}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
{% endblock %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-chart-bar"></i> 高级报表系统
</h1>
</div>
</div>
<!-- 报表类型选择 -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-file-alt"></i> 报表类型
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<div class="card border-left-primary">
<div class="card-body text-center">
<i class="fas fa-users fa-3x text-primary mb-3"></i>
<h5 class="card-title">用户报表</h5>
<p class="card-text">用户注册、活跃度、增长趋势分析</p>
<a href="{{ url_for('report_admin.user_report') }}" class="btn btn-primary">
<i class="fas fa-chart-line"></i> 查看用户报表
</a>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card border-left-success">
<div class="card-body text-center">
<i class="fas fa-magic fa-3x text-success mb-3"></i>
<h5 class="card-title">提示词报表</h5>
<p class="card-text">提示词生成量、使用情况、质量分析</p>
<a href="{{ url_for('report_admin.prompt_report') }}" class="btn btn-success">
<i class="fas fa-chart-area"></i> 查看提示词报表
</a>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card border-left-info">
<div class="card-body text-center">
<i class="fas fa-download fa-3x text-info mb-3"></i>
<h5 class="card-title">数据导出</h5>
<p class="card-text">导出各种格式的报表数据</p>
<button class="btn btn-info" onclick="showExportModal()">
<i class="fas fa-download"></i> 导出报表
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 快速统计 -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-tachometer-alt"></i> 快速统计
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<div class="text-center">
<h3 class="text-primary" id="totalUsers">-</h3>
<p class="text-muted">总用户数</p>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="text-center">
<h3 class="text-success" id="totalPrompts">-</h3>
<p class="text-muted">总提示词数</p>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="text-center">
<h3 class="text-info" id="activeUsers">-</h3>
<p class="text-muted">活跃用户</p>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="text-center">
<h3 class="text-warning" id="avgLength">-</h3>
<p class="text-muted">平均长度</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 趋势图表 -->
<div class="row">
<div class="col-xl-6">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-line"></i> 用户增长趋势
</h6>
</div>
<div class="card-body">
<div id="userTrendChart" style="height: 300px;"></div>
</div>
</div>
</div>
<div class="col-xl-6">
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-chart-area"></i> 提示词生成趋势
</h6>
</div>
<div class="card-body">
<div id="promptTrendChart" style="height: 300px;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 导出模态框 -->
<div class="modal fade" id="exportModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-download"></i> 导出报表
</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<form id="exportForm">
<div class="form-group">
<label for="reportType">报表类型</label>
<select class="form-control" id="reportType" required>
<option value="user">用户报表</option>
<option value="prompt">提示词报表</option>
</select>
</div>
<div class="form-group">
<label for="startDate">开始日期</label>
<input type="date" class="form-control" id="startDate" required>
</div>
<div class="form-group">
<label for="endDate">结束日期</label>
<input type="date" class="form-control" id="endDate" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="exportReport()">
<i class="fas fa-download"></i> 导出
</button>
</div>
</div>
</div>
</div>
<style>
.border-left-primary {
border-left: 0.25rem solid #4e73df !important;
}
.border-left-success {
border-left: 0.25rem solid #1cc88a !important;
}
.border-left-info {
border-left: 0.25rem solid #36b9cc !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 设置默认日期
const today = new Date();
const thirtyDaysAgo = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));
document.getElementById('startDate').value = thirtyDaysAgo.toISOString().split('T')[0];
document.getElementById('endDate').value = today.toISOString().split('T')[0];
// 加载统计数据
loadQuickStats();
// 初始化图表
initCharts();
});
function loadQuickStats() {
// 模拟统计数据(实际项目中会从后端获取)
document.getElementById('totalUsers').textContent = '1,234';
document.getElementById('totalPrompts').textContent = '5,678';
document.getElementById('activeUsers').textContent = '890';
document.getElementById('avgLength').textContent = '156';
}
function initCharts() {
// 用户增长趋势图
var userData = [{
x: ['2025-08-20', '2025-08-21', '2025-08-22', '2025-08-23', '2025-08-24', '2025-08-25', '2025-08-26'],
y: [1200, 1220, 1250, 1280, 1300, 1320, 1350],
type: 'scatter',
mode: 'lines+markers',
name: '用户总数',
line: {color: '#4e73df', width: 3},
marker: {size: 8}
}];
var userLayout = {
title: '用户增长趋势',
xaxis: {title: '日期'},
yaxis: {title: '用户数'},
height: 300,
margin: {l: 50, r: 50, t: 50, b: 50}
};
Plotly.newPlot('userTrendChart', userData, userLayout);
// 提示词生成趋势图
var promptData = [{
x: ['2025-08-20', '2025-08-21', '2025-08-22', '2025-08-23', '2025-08-24', '2025-08-25', '2025-08-26'],
y: [150, 180, 220, 280, 320, 380, 420],
type: 'scatter',
mode: 'lines+markers',
name: '生成数量',
line: {color: '#1cc88a', width: 3},
marker: {size: 8}
}];
var promptLayout = {
title: '提示词生成趋势',
xaxis: {title: '日期'},
yaxis: {title: '生成数量'},
height: 300,
margin: {l: 50, r: 50, t: 50, b: 50}
};
Plotly.newPlot('promptTrendChart', promptData, promptLayout);
}
function showExportModal() {
$('#exportModal').modal('show');
}
function exportReport() {
const reportType = document.getElementById('reportType').value;
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
if (!startDate || !endDate) {
alert('请选择开始和结束日期');
return;
}
if (new Date(startDate) > new Date(endDate)) {
alert('开始日期不能晚于结束日期');
return;
}
// 构建下载链接
const url = `{{ url_for('report_admin.export_report') }}?type=${reportType}&start_date=${startDate}&end_date=${endDate}`;
// 创建下载链接
const link = document.createElement('a');
link.href = url;
link.download = `${reportType}_report_${startDate}_to_${endDate}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 关闭模态框
$('#exportModal').modal('hide');
// 显示成功消息
showAlert('报表导出成功!', 'success');
}
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="close" data-dismiss="alert">
<span>&times;</span>
</button>
`;
document.querySelector('.container-fluid').insertBefore(alertDiv, document.querySelector('.row'));
// 自动消失
setTimeout(() => {
alertDiv.remove();
}, 5000);
}
</script>
{% endblock %}