后台管理第三阶段开发
This commit is contained in:
BIN
backups/backup_data_20250829_235838.zip
Normal file
BIN
backups/backup_data_20250829_235838.zip
Normal file
Binary file not shown.
Binary file not shown.
@@ -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'])
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
191
src/flask_prompt_master/admin/views/analytics_admin.py
Normal file
191
src/flask_prompt_master/admin/views/analytics_admin.py
Normal 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)
|
||||
184
src/flask_prompt_master/admin/views/api_admin.py
Normal file
184
src/flask_prompt_master/admin/views/api_admin.py
Normal 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 []
|
||||
479
src/flask_prompt_master/admin/views/backup_admin.py
Normal file
479
src/flask_prompt_master/admin/views/backup_admin.py
Normal 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]}"
|
||||
185
src/flask_prompt_master/admin/views/batch_admin.py
Normal file
185
src/flask_prompt_master/admin/views/batch_admin.py
Normal 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)}'})
|
||||
187
src/flask_prompt_master/admin/views/monitor_admin.py
Normal file
187
src/flask_prompt_master/admin/views/monitor_admin.py
Normal 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分钟再重试
|
||||
254
src/flask_prompt_master/admin/views/report_admin.py
Normal file
254
src/flask_prompt_master/admin/views/report_admin.py
Normal 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
|
||||
}
|
||||
177
src/flask_prompt_master/templates/admin/analytics_charts.html
Normal file
177
src/flask_prompt_master/templates/admin/analytics_charts.html
Normal 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 %}
|
||||
239
src/flask_prompt_master/templates/admin/analytics_dashboard.html
Normal file
239
src/flask_prompt_master/templates/admin/analytics_dashboard.html
Normal 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 %}
|
||||
444
src/flask_prompt_master/templates/admin/api_dashboard.html
Normal file
444
src/flask_prompt_master/templates/admin/api_dashboard.html
Normal 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>×</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> </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 %}
|
||||
418
src/flask_prompt_master/templates/admin/backup_dashboard.html
Normal file
418
src/flask_prompt_master/templates/admin/backup_dashboard.html
Normal 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>×</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>×</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>×</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.querySelector('.container-fluid').insertBefore(alertDiv, document.querySelector('.row'));
|
||||
|
||||
// 自动消失
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
300
src/flask_prompt_master/templates/admin/batch_operations.html
Normal file
300
src/flask_prompt_master/templates/admin/batch_operations.html
Normal 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 %}
|
||||
437
src/flask_prompt_master/templates/admin/monitor_dashboard.html
Normal file
437
src/flask_prompt_master/templates/admin/monitor_dashboard.html
Normal 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>×</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.querySelector('.container-fluid').insertBefore(alertDiv, document.querySelector('.row'));
|
||||
|
||||
// 自动消失
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
319
src/flask_prompt_master/templates/admin/report_dashboard.html
Normal file
319
src/flask_prompt_master/templates/admin/report_dashboard.html
Normal 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>×</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>×</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
document.querySelector('.container-fluid').insertBefore(alertDiv, document.querySelector('.row'));
|
||||
|
||||
// 自动消失
|
||||
setTimeout(() => {
|
||||
alertDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user