refactor: 移除冗余文件并优化代码结构

- 删除多个不再使用的脚本和配置文件,包括 `auto_push.bat`, `check_and_fix_users.py`, `init.sql` 等。
- 新增 `git_push.bat` 和 `git_push.sh` 脚本以简化 Git 推送流程。
- 更新 `README.md` 以反映最新的功能和结构变化。
- 优化前端代码,添加新的页面和组件,提升用户体验。

此提交旨在清理项目结构并增强代码可维护性。
This commit is contained in:
2025-12-08 00:53:23 +08:00
parent 65d69358d7
commit 2026007045
171 changed files with 19316 additions and 19520 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -32,6 +32,11 @@ from src.web.blueprints.monitoring import monitoring_bp
from src.web.blueprints.system import system_bp
from src.web.blueprints.feishu_sync import feishu_sync_bp
from src.web.blueprints.core import core_bp
from src.web.blueprints.auth import auth_bp
from src.web.blueprints.agent import agent_bp
from src.web.blueprints.vehicle import vehicle_bp
from src.web.blueprints.analytics import analytics_bp
from src.web.blueprints.test import test_bp
# 配置日志
logger = logging.getLogger(__name__)
@@ -69,6 +74,11 @@ app.register_blueprint(monitoring_bp)
app.register_blueprint(system_bp)
app.register_blueprint(feishu_sync_bp)
app.register_blueprint(core_bp)
app.register_blueprint(auth_bp)
app.register_blueprint(agent_bp)
app.register_blueprint(vehicle_bp)
app.register_blueprint(analytics_bp)
app.register_blueprint(test_bp)
# 页面路由
@app.route('/')
@@ -100,10 +110,12 @@ def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
# ============================================================================
# 核心API路由 - 已迁移到蓝图
# 核心API路由
# ============================================================================
# 健康检查、预警规则、监控状态等核心功能已迁移到 core 蓝图
# 分析数据相关功能也已迁移到 core 蓝图
# 以下路由因功能特殊性保留在主应用中:
# - Chat相关路由使用RealtimeChatManager进行实时对话
# - 健康检查、预警规则、监控状态等核心功能已迁移到 core 蓝图
# - 分析数据相关功能已迁移到 analytics 蓝图
# ============================================================================
# 实时对话相关路由
@@ -115,9 +127,9 @@ def create_chat_session():
data = request.get_json()
user_id = data.get('user_id', 'anonymous')
work_order_id = data.get('work_order_id')
session_id = service_manager.get_chat_manager().create_session(user_id, work_order_id)
return jsonify({
"success": True,
"session_id": session_id,
@@ -133,10 +145,10 @@ def send_chat_message():
data = request.get_json()
session_id = data.get('session_id')
message = data.get('message')
if not session_id or not message:
return jsonify({"error": "缺少必要参数"}), 400
result = service_manager.get_chat_manager().process_message(session_id, message)
return jsonify(result)
except Exception as e:
@@ -164,10 +176,10 @@ def create_work_order():
description = data.get('description')
category = data.get('category', '技术问题')
priority = data.get('priority', 'medium')
if not session_id or not title or not description:
return jsonify({"error": "缺少必要参数"}), 400
result = service_manager.get_chat_manager().create_work_order(session_id, title, description, category, priority)
return jsonify(result)
except Exception as e:
@@ -209,388 +221,13 @@ def get_active_sessions():
logger.error(f"获取活跃会话失败: {e}")
return jsonify({"error": str(e)}), 500
# ============================================================================
# Agent相关API
# ============================================================================
@app.route('/api/agent/status')
def get_agent_status():
"""获取Agent状态"""
try:
status = service_manager.get_agent_assistant().get_agent_status()
return jsonify({"success": True, **status})
except Exception as e:
# 返回默认状态避免500错误
return jsonify({
"success": False,
"status": "inactive",
"active_goals": 0,
"available_tools": 0,
"error": "Agent服务暂时不可用"
})
# Agent相关路由已移动到 agent_bp 蓝图
@app.route('/api/agent/action-history')
def get_agent_action_history():
"""获取Agent动作执行历史"""
try:
limit = request.args.get('limit', 50, type=int)
history = service_manager.get_agent_assistant().get_action_history(limit)
return jsonify({
"success": True,
"history": history,
"count": len(history)
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# 分析相关路由已移动到 analytics_bp 蓝图
@app.route('/api/agent/trigger-sample', methods=['POST'])
def trigger_sample_action():
"""触发示例动作"""
try:
import asyncio
result = asyncio.run(service_manager.get_agent_assistant().trigger_sample_actions())
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
# 车辆数据相关路由已移动到 vehicle_bp 蓝图
@app.route('/api/agent/clear-history', methods=['POST'])
def clear_agent_history():
"""清空Agent执行历史"""
try:
result = service_manager.get_agent_assistant().clear_execution_history()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/llm-stats')
def get_llm_stats():
"""获取LLM使用统计"""
try:
stats = service_manager.get_agent_assistant().get_llm_usage_stats()
return jsonify({
"success": True,
"stats": stats
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/toggle', methods=['POST'])
def toggle_agent_mode():
"""切换Agent模式"""
try:
data = request.get_json()
enabled = data.get('enabled', True)
success = service_manager.get_agent_assistant().toggle_agent_mode(enabled)
return jsonify({
"success": success,
"message": f"Agent模式已{'启用' if enabled else '禁用'}"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/monitoring/start', methods=['POST'])
def start_agent_monitoring():
"""启动Agent监控"""
try:
success = service_manager.get_agent_assistant().start_proactive_monitoring()
return jsonify({
"success": success,
"message": "Agent监控已启动" if success else "启动失败"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/monitoring/stop', methods=['POST'])
def stop_agent_monitoring():
"""停止Agent监控"""
try:
success = service_manager.get_agent_assistant().stop_proactive_monitoring()
return jsonify({
"success": success,
"message": "Agent监控已停止" if success else "停止失败"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/proactive-monitoring', methods=['POST'])
def proactive_monitoring():
"""主动监控检查"""
try:
result = service_manager.get_agent_assistant().run_proactive_monitoring()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/intelligent-analysis', methods=['POST'])
def intelligent_analysis():
"""智能分析"""
try:
analysis = service_manager.get_agent_assistant().run_intelligent_analysis()
return jsonify({"success": True, "analysis": analysis})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/chat', methods=['POST'])
def agent_chat():
"""Agent对话接口"""
try:
data = request.get_json()
message = data.get('message', '')
context = data.get('context', {})
if not message:
return jsonify({"error": "消息不能为空"}), 400
# 使用Agent助手处理消息
agent_assistant = service_manager.get_agent_assistant()
# 模拟Agent处理实际应该调用真正的Agent处理逻辑
import asyncio
result = asyncio.run(agent_assistant.process_message_agent(
message=message,
user_id=context.get('user_id', 'admin'),
work_order_id=None,
enable_proactive=True
))
return jsonify({
"success": True,
"response": result.get('response', 'Agent已处理您的请求'),
"actions": result.get('actions', []),
"status": result.get('status', 'completed')
})
except Exception as e:
return jsonify({"error": str(e)}), 500
# ============================================================================
# Agent 工具统计与自定义工具
# ============================================================================
@app.route('/api/agent/tools/stats')
def get_agent_tools_stats():
try:
agent_assistant = service_manager.get_agent_assistant()
tools = agent_assistant.agent_core.tool_manager.get_available_tools()
performance = agent_assistant.agent_core.tool_manager.get_tool_performance_report()
return jsonify({
"success": True,
"tools": tools,
"performance": performance
})
except Exception as e:
# 返回默认工具列表避免500错误
return jsonify({
"success": False,
"tools": [],
"performance": {},
"error": "工具统计暂时不可用"
})
@app.route('/api/agent/tools/execute', methods=['POST'])
def execute_agent_tool():
"""执行指定的Agent工具"""
try:
data = request.get_json() or {}
tool_name = data.get('tool') or data.get('name')
parameters = data.get('parameters') or {}
if not tool_name:
return jsonify({"error": "缺少工具名称tool"}), 400
import asyncio
result = asyncio.run(service_manager.get_agent_assistant().agent_core.tool_manager.execute_tool(tool_name, parameters))
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/tools/register', methods=['POST'])
def register_custom_tool():
"""注册自定义工具(仅登记元数据,函数为占位符)"""
try:
data = request.get_json() or {}
name = data.get('name')
description = data.get('description', '')
if not name:
return jsonify({"error": "缺少工具名称"}), 400
def _placeholder_tool(**kwargs):
return {"message": f"自定义工具 {name} 已登记(占位),当前不可执行", "params": kwargs}
service_manager.get_agent_assistant().agent_core.tool_manager.register_tool(
name,
_placeholder_tool,
metadata={"description": description, "custom": True}
)
return jsonify({"success": True, "message": "工具已注册"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/tools/unregister/<name>', methods=['DELETE'])
def unregister_custom_tool(name):
try:
success = service_manager.get_agent_assistant().agent_core.tool_manager.unregister_tool(name)
return jsonify({"success": success})
except Exception as e:
return jsonify({"error": str(e)}), 500
# ============================================================================
# 分析相关API - 已迁移到 core 蓝图
# ============================================================================
@app.route('/api/analytics/export')
def export_analytics():
"""导出分析报告"""
try:
# 生成Excel报告使用数据库真实数据
analytics = query_optimizer.get_analytics_optimized(30)
# 创建工作簿
from openpyxl import Workbook
from openpyxl.styles import Font
wb = Workbook()
ws = wb.active
ws.title = "分析报告"
# 添加标题
ws['A1'] = 'TSP智能助手分析报告'
ws['A1'].font = Font(size=16, bold=True)
# 添加工单统计
ws['A3'] = '工单统计'
ws['A3'].font = Font(bold=True)
ws['A4'] = '总工单数'
ws['B4'] = analytics['workorders']['total']
ws['A5'] = '待处理'
ws['B5'] = analytics['workorders']['open']
ws['A6'] = '已解决'
ws['B6'] = analytics['workorders']['resolved']
# 保存文件
report_path = 'uploads/analytics_report.xlsx'
os.makedirs('uploads', exist_ok=True)
wb.save(report_path)
from flask import send_file
return send_file(report_path, as_attachment=True, download_name='analytics_report.xlsx')
except Exception as e:
return jsonify({"error": str(e)}), 500
# ============================================================================
# 车辆数据相关API
# ============================================================================
@app.route('/api/vehicle/data')
def get_vehicle_data():
"""获取车辆数据"""
try:
vehicle_id = request.args.get('vehicle_id')
vehicle_vin = request.args.get('vehicle_vin')
data_type = request.args.get('data_type')
limit = request.args.get('limit', 10, type=int)
vehicle_mgr = service_manager.get_vehicle_manager()
if vehicle_vin:
data = vehicle_mgr.get_vehicle_data_by_vin(vehicle_vin, data_type, limit)
elif vehicle_id:
data = vehicle_mgr.get_vehicle_data(vehicle_id, data_type, limit)
else:
data = vehicle_mgr.search_vehicle_data(limit=limit)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/vehicle/data/vin/<vehicle_vin>/latest')
def get_latest_vehicle_data_by_vin(vehicle_vin):
"""按VIN获取车辆最新数据"""
try:
data = service_manager.get_vehicle_manager().get_latest_vehicle_data_by_vin(vehicle_vin)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/vehicle/data/<vehicle_id>/latest')
def get_latest_vehicle_data(vehicle_id):
"""获取车辆最新数据"""
try:
data = service_manager.get_vehicle_manager().get_latest_vehicle_data(vehicle_id)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/vehicle/data/<vehicle_id>/summary')
def get_vehicle_summary(vehicle_id):
"""获取车辆数据摘要"""
try:
summary = service_manager.get_vehicle_manager().get_vehicle_summary(vehicle_id)
return jsonify(summary)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/vehicle/data', methods=['POST'])
def add_vehicle_data():
"""添加车辆数据"""
try:
data = request.get_json()
success = service_manager.get_vehicle_manager().add_vehicle_data(
vehicle_id=data['vehicle_id'],
data_type=data['data_type'],
data_value=data['data_value'],
vehicle_vin=data.get('vehicle_vin')
)
return jsonify({"success": success, "message": "数据添加成功" if success else "添加失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/vehicle/init-sample-data', methods=['POST'])
def init_sample_vehicle_data():
"""初始化示例车辆数据"""
try:
success = service_manager.get_vehicle_manager().add_sample_vehicle_data()
return jsonify({"success": success, "message": "示例数据初始化成功" if success else "初始化失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500
# ============================================================================
# API测试相关接口
# ============================================================================
@app.route('/api/test/connection', methods=['POST'])
def test_api_connection():
"""测试API连接"""
try:
data = request.get_json()
api_provider = data.get('api_provider', 'openai')
api_base_url = data.get('api_base_url', '')
api_key = data.get('api_key', '')
model_name = data.get('model_name', 'qwen-turbo')
# 这里可以调用LLM客户端进行连接测试
# 暂时返回模拟结果
return jsonify({
"success": True,
"message": f"API连接测试成功 - {api_provider}",
"response_time": "150ms",
"model_status": "可用"
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/test/model', methods=['POST'])
def test_model_response():
"""测试模型回答"""
try:
data = request.get_json()
test_message = data.get('test_message', '你好,请简单介绍一下你自己')
# 这里可以调用LLM客户端进行回答测试
# 暂时返回模拟结果
return jsonify({
"success": True,
"test_message": test_message,
"response": "你好我是TSP智能助手基于大语言模型构建的智能客服系统。我可以帮助您解决车辆相关问题提供技术支持和服务。",
"response_time": "1.2s",
"tokens_used": 45
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
# API测试相关路由已移动到 test_bp 蓝图
# ============================================================================
# 应用启动配置

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

257
src/web/blueprints/agent.py Normal file
View File

@@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
"""
Agent相关API蓝图
处理智能代理、工具执行、监控等功能
"""
from flask import Blueprint, request, jsonify
import asyncio
agent_bp = Blueprint('agent', __name__, url_prefix='/api/agent')
@agent_bp.route('/status')
def get_agent_status():
"""获取Agent状态"""
try:
from src.web.service_manager import service_manager
status = service_manager.get_agent_assistant().get_agent_status()
return jsonify({"success": True, **status})
except Exception as e:
# 返回默认状态避免500错误
return jsonify({
"success": False,
"status": "inactive",
"active_goals": 0,
"available_tools": 0,
"error": "Agent服务暂时不可用"
})
@agent_bp.route('/action-history')
def get_agent_action_history():
"""获取Agent动作执行历史"""
try:
from src.web.service_manager import service_manager
limit = request.args.get('limit', 50, type=int)
history = service_manager.get_agent_assistant().get_action_history(limit)
return jsonify({
"success": True,
"history": history,
"count": len(history)
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/trigger-sample', methods=['POST'])
def trigger_sample_action():
"""触发示例动作"""
try:
from src.web.service_manager import service_manager
import asyncio
result = asyncio.run(service_manager.get_agent_assistant().trigger_sample_actions())
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/clear-history', methods=['POST'])
def clear_agent_history():
"""清空Agent执行历史"""
try:
from src.web.service_manager import service_manager
result = service_manager.get_agent_assistant().clear_execution_history()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/llm-stats')
def get_llm_stats():
"""获取LLM使用统计"""
try:
from src.web.service_manager import service_manager
stats = service_manager.get_agent_assistant().get_llm_usage_stats()
return jsonify({
"success": True,
"stats": stats
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/toggle', methods=['POST'])
def toggle_agent_mode():
"""切换Agent模式"""
try:
from src.web.service_manager import service_manager
data = request.get_json()
enabled = data.get('enabled', True)
success = service_manager.get_agent_assistant().toggle_agent_mode(enabled)
return jsonify({
"success": success,
"message": f"Agent模式已{'启用' if enabled else '禁用'}"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/monitoring/start', methods=['POST'])
def start_agent_monitoring():
"""启动Agent监控"""
try:
from src.web.service_manager import service_manager
success = service_manager.get_agent_assistant().start_proactive_monitoring()
return jsonify({
"success": success,
"message": "Agent监控已启动" if success else "启动失败"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/monitoring/stop', methods=['POST'])
def stop_agent_monitoring():
"""停止Agent监控"""
try:
from src.web.service_manager import service_manager
success = service_manager.get_agent_assistant().stop_proactive_monitoring()
return jsonify({
"success": success,
"message": "Agent监控已停止" if success else "停止失败"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/proactive-monitoring', methods=['POST'])
def proactive_monitoring():
"""主动监控检查"""
try:
from src.web.service_manager import service_manager
result = service_manager.get_agent_assistant().run_proactive_monitoring()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/intelligent-analysis', methods=['POST'])
def intelligent_analysis():
"""智能分析"""
try:
from src.web.service_manager import service_manager
analysis = service_manager.get_agent_assistant().run_intelligent_analysis()
return jsonify({"success": True, "analysis": analysis})
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/chat', methods=['POST'])
def agent_chat():
"""Agent对话接口"""
try:
from src.web.service_manager import service_manager
data = request.get_json()
message = data.get('message', '')
context = data.get('context', {})
if not message:
return jsonify({"error": "消息不能为空"}), 400
# 使用Agent助手处理消息
agent_assistant = service_manager.get_agent_assistant()
# 模拟Agent处理实际应该调用真正的Agent处理逻辑
import asyncio
result = asyncio.run(agent_assistant.process_message_agent(
message=message,
user_id=context.get('user_id', 'admin'),
work_order_id=None,
enable_proactive=True
))
return jsonify({
"success": True,
"response": result.get('response', 'Agent已处理您的请求'),
"actions": result.get('actions', []),
"status": result.get('status', 'completed')
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/tools/stats')
def get_agent_tools_stats():
"""获取Agent工具统计"""
try:
from src.web.service_manager import service_manager
agent_assistant = service_manager.get_agent_assistant()
tools = agent_assistant.agent_core.tool_manager.get_available_tools()
performance = agent_assistant.agent_core.tool_manager.get_tool_performance_report()
return jsonify({
"success": True,
"tools": tools,
"performance": performance
})
except Exception as e:
# 返回默认工具列表避免500错误
return jsonify({
"success": False,
"tools": [],
"performance": {},
"error": "工具统计暂时不可用"
})
@agent_bp.route('/tools/execute', methods=['POST'])
def execute_agent_tool():
"""执行指定的Agent工具"""
try:
from src.web.service_manager import service_manager
data = request.get_json() or {}
tool_name = data.get('tool') or data.get('name')
parameters = data.get('parameters') or {}
if not tool_name:
return jsonify({"error": "缺少工具名称tool"}), 400
import asyncio
result = asyncio.run(service_manager.get_agent_assistant().agent_core.tool_manager.execute_tool(tool_name, parameters))
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/tools/register', methods=['POST'])
def register_custom_tool():
"""注册自定义工具(仅登记元数据,函数为占位符)"""
try:
from src.web.service_manager import service_manager
data = request.get_json() or {}
name = data.get('name')
description = data.get('description', '')
if not name:
return jsonify({"error": "缺少工具名称"}), 400
def _placeholder_tool(**kwargs):
return {"message": f"自定义工具 {name} 已登记(占位),当前不可执行", "params": kwargs}
service_manager.get_agent_assistant().agent_core.tool_manager.register_tool(
name,
_placeholder_tool,
metadata={"description": description, "custom": True}
)
return jsonify({"success": True, "message": "工具已注册"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@agent_bp.route('/tools/unregister/<name>', methods=['DELETE'])
def unregister_custom_tool(name):
"""注销自定义工具"""
try:
from src.web.service_manager import service_manager
success = service_manager.get_agent_assistant().agent_core.tool_manager.unregister_tool(name)
return jsonify({"success": success})
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
"""
分析相关API蓝图
处理数据分析、报告生成等功能
"""
from flask import Blueprint, request, jsonify, send_file
import os
analytics_bp = Blueprint('analytics', __name__, url_prefix='/api/analytics')
@analytics_bp.route('/export')
def export_analytics():
"""导出分析报告"""
try:
from src.web.service_manager import service_manager
from src.core.query_optimizer import query_optimizer
from openpyxl import Workbook
from openpyxl.styles import Font
# 生成Excel报告使用数据库真实数据
analytics = query_optimizer.get_analytics_optimized(30)
# 创建工作簿
wb = Workbook()
ws = wb.active
ws.title = "分析报告"
# 添加标题
ws['A1'] = 'TSP智能助手分析报告'
ws['A1'].font = Font(size=16, bold=True)
# 添加工单统计
ws['A3'] = '工单统计'
ws['A3'].font = Font(bold=True)
ws['A4'] = '总工单数'
ws['B4'] = analytics['workorders']['total']
ws['A5'] = '待处理'
ws['B5'] = analytics['workorders']['open']
ws['A6'] = '已解决'
ws['B6'] = analytics['workorders']['resolved']
# 保存文件
report_path = 'uploads/analytics_report.xlsx'
os.makedirs('uploads', exist_ok=True)
wb.save(report_path)
return send_file(report_path, as_attachment=True, download_name='analytics_report.xlsx')
except Exception as e:
return jsonify({"error": str(e)}), 500

150
src/web/blueprints/auth.py Normal file
View File

@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
"""
认证蓝图
处理用户登录、注册、注销等功能
"""
from flask import Blueprint, request, jsonify, session, redirect, url_for
from src.core.auth_manager import auth_manager
from src.web.error_handlers import handle_api_errors, create_error_response, create_success_response
auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth')
@auth_bp.route('/login', methods=['POST'])
@handle_api_errors
def login():
"""用户登录"""
data = request.get_json()
if not data or not data.get('username') or not data.get('password'):
return jsonify({"success": False, "message": "用户名和密码不能为空"}), 400
username = data['username']
password = data['password']
remember = data.get('remember', False)
# 认证用户
user_data = auth_manager.authenticate_user(username, password)
if user_data:
# 生成token
token = auth_manager.generate_token(user_data)
# 存储到session
session['user_id'] = user_data['id']
session['username'] = user_data['username']
session['user_info'] = user_data
session['token'] = token
if remember:
session.permanent = True
# 构建响应
response_data = {
"success": True,
"message": "登录成功",
"user": {
"id": user_data['id'],
"username": user_data['username'],
"name": user_data['name'],
"email": user_data['email'],
"role": user_data['role']
},
"token": token
}
return jsonify(response_data)
else:
return jsonify({"success": False, "message": "用户名或密码错误"}), 401
@auth_bp.route('/logout', methods=['POST'])
@handle_api_errors
def logout():
"""用户注销"""
# 清除session
session.clear()
return jsonify(create_success_response(message="注销成功"))
@auth_bp.route('/status')
@handle_api_errors
def get_auth_status():
"""获取认证状态"""
if 'user_id' in session and 'user_info' in session:
return jsonify({
"authenticated": True,
"user": session['user_info'],
"token": session.get('token')
})
else:
return jsonify({
"authenticated": False,
"user": None,
"token": None
})
@auth_bp.route('/register', methods=['POST'])
@handle_api_errors
def register():
"""用户注册(仅管理员可用)"""
# 检查当前用户是否为管理员
if not session.get('user_info') or session['user_info'].get('role') != 'admin':
return create_error_response("权限不足", 403)
data = request.get_json()
required_fields = ['username', 'password', 'name']
if not all(data.get(field) for field in required_fields):
return create_error_response("缺少必要字段", 400)
user = auth_manager.create_user(
username=data['username'],
password=data['password'],
name=data['name'],
email=data.get('email'),
role=data.get('role', 'user')
)
if not user:
return create_error_response("用户创建失败,用户名可能已存在", 400)
return jsonify(create_success_response(
data={
"id": user.id,
"username": user.username,
"name": user.name,
"email": user.email,
"role": user.role
},
message="用户创建成功"
))
@auth_bp.route('/user/profile')
@handle_api_errors
def get_user_profile():
"""获取用户信息"""
if 'user_info' not in session:
return create_error_response("未登录", 401)
return jsonify(create_success_response(data=session['user_info']))
@auth_bp.route('/user/profile', methods=['PUT'])
@handle_api_errors
def update_user_profile():
"""更新用户信息"""
if 'user_info' not in session:
return create_error_response("未登录", 401)
data = request.get_json()
user_id = session['user_info']['id']
# 这里应该实现用户信息的更新逻辑
# 暂时只返回成功响应
return jsonify(create_success_response(message="用户信息更新成功"))

View File

@@ -14,10 +14,9 @@ from src.web.error_handlers import handle_api_errors, create_error_response, cre
knowledge_bp = Blueprint('knowledge', __name__, url_prefix='/api/knowledge')
def get_agent_assistant():
"""获取Agent助手实例"""
"""获取Agent助手实例懒加载"""
global _agent_assistant
if '_agent_assistant' not in globals():
from src.agent_assistant import TSPAgentAssistant
_agent_assistant = TSPAgentAssistant()
return _agent_assistant

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
"""
API测试相关蓝图
处理API连接测试、模型测试等功能
"""
from flask import Blueprint, request, jsonify
test_bp = Blueprint('test', __name__, url_prefix='/api/test')
@test_bp.route('/connection', methods=['POST'])
def test_api_connection():
"""测试API连接"""
try:
data = request.get_json()
api_provider = data.get('api_provider', 'openai')
api_base_url = data.get('api_base_url', '')
api_key = data.get('api_key', '')
model_name = data.get('model_name', 'qwen-turbo')
# 这里可以调用LLM客户端进行连接测试
# 暂时返回模拟结果
return jsonify({
"success": True,
"message": f"API连接测试成功 - {api_provider}",
"response_time": "150ms",
"model_status": "可用"
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@test_bp.route('/model', methods=['POST'])
def test_model_response():
"""测试模型回答"""
try:
data = request.get_json()
test_message = data.get('test_message', '你好,请简单介绍一下你自己')
# 这里可以调用LLM客户端进行回答测试
# 暂时返回模拟结果
return jsonify({
"success": True,
"test_message": test_message,
"response": "你好我是TSP智能助手基于大语言模型构建的智能客服系统。我可以帮助您解决车辆相关问题提供技术支持和服务。",
"response_time": "1.2s",
"tokens_used": 45
})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500

View File

@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""
车辆数据相关API蓝图
处理车辆数据查询、添加、监控等功能
"""
from flask import Blueprint, request, jsonify
vehicle_bp = Blueprint('vehicle', __name__, url_prefix='/api/vehicle')
@vehicle_bp.route('/data')
def get_vehicle_data():
"""获取车辆数据"""
try:
from src.web.service_manager import service_manager
vehicle_id = request.args.get('vehicle_id')
vehicle_vin = request.args.get('vehicle_vin')
data_type = request.args.get('data_type')
limit = request.args.get('limit', 10, type=int)
vehicle_mgr = service_manager.get_vehicle_manager()
if vehicle_vin:
data = vehicle_mgr.get_vehicle_data_by_vin(vehicle_vin, data_type, limit)
elif vehicle_id:
data = vehicle_mgr.get_vehicle_data(vehicle_id, data_type, limit)
else:
data = vehicle_mgr.search_vehicle_data(limit=limit)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@vehicle_bp.route('/data/vin/<vehicle_vin>/latest')
def get_latest_vehicle_data_by_vin(vehicle_vin):
"""按VIN获取车辆最新数据"""
try:
from src.web.service_manager import service_manager
data = service_manager.get_vehicle_manager().get_latest_vehicle_data_by_vin(vehicle_vin)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@vehicle_bp.route('/data/<vehicle_id>/latest')
def get_latest_vehicle_data(vehicle_id):
"""获取车辆最新数据"""
try:
from src.web.service_manager import service_manager
data = service_manager.get_vehicle_manager().get_latest_vehicle_data(vehicle_id)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@vehicle_bp.route('/data/<vehicle_id>/summary')
def get_vehicle_summary(vehicle_id):
"""获取车辆数据摘要"""
try:
from src.web.service_manager import service_manager
summary = service_manager.get_vehicle_manager().get_vehicle_summary(vehicle_id)
return jsonify(summary)
except Exception as e:
return jsonify({"error": str(e)}), 500
@vehicle_bp.route('/data', methods=['POST'])
def add_vehicle_data():
"""添加车辆数据"""
try:
from src.web.service_manager import service_manager
data = request.get_json()
success = service_manager.get_vehicle_manager().add_vehicle_data(
vehicle_id=data['vehicle_id'],
data_type=data['data_type'],
data_value=data['data_value'],
vehicle_vin=data.get('vehicle_vin')
)
return jsonify({"success": success, "message": "数据添加成功" if success else "添加失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@vehicle_bp.route('/init-sample-data', methods=['POST'])
def init_sample_vehicle_data():
"""初始化示例车辆数据"""
try:
from src.web.service_manager import service_manager
success = service_manager.get_vehicle_manager().add_sample_vehicle_data()
return jsonify({"success": success, "message": "示例数据初始化成功" if success else "初始化失败"})
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -10,7 +10,6 @@ import logging
import uuid
import time
from datetime import datetime
from typing import Optional
from flask import Blueprint, request, jsonify, send_file
from werkzeug.utils import secure_filename
from sqlalchemy import text
@@ -26,13 +25,13 @@ class SimpleAIAccuracyConfig:
self.manual_review_threshold = 0.80
self.ai_suggestion_confidence = 0.95
self.human_resolution_confidence = 0.90
def should_auto_approve(self, similarity: float) -> bool:
return similarity >= self.auto_approve_threshold
def should_use_human_resolution(self, similarity: float) -> bool:
return similarity < self.use_human_resolution_threshold
def get_confidence_score(self, similarity: float, use_human: bool = False) -> float:
if use_human:
return self.human_resolution_confidence
@@ -41,83 +40,12 @@ class SimpleAIAccuracyConfig:
from src.main import TSPAssistant
from src.core.database import db_manager
from src.core.models import WorkOrder, Conversation, WorkOrderSuggestion, KnowledgeEntry, WorkOrderProcessHistory
from src.core.models import WorkOrder, Conversation, WorkOrderSuggestion, KnowledgeEntry
from src.core.query_optimizer import query_optimizer
from src.web.service_manager import service_manager
from src.core.workorder_permissions import (
WorkOrderPermissionManager, WorkOrderDispatchManager,
UserRole, WorkOrderModule
)
workorders_bp = Blueprint('workorders', __name__, url_prefix='/api/workorders')
def get_current_user_role() -> UserRole:
"""获取当前用户角色(临时实现,实际需要集成认证系统)"""
# TODO: 从session或token中获取用户信息
# 在没有认证系统之前默认返回ADMIN以便可以查看所有工单
# 实际实现时需要从认证系统获取真实角色
role_str = request.headers.get('X-User-Role', 'admin') # 临时改为admin避免VIEWER无法查看数据
try:
return UserRole(role_str)
except ValueError:
return UserRole.ADMIN # 临时返回ADMIN避免VIEWER无法查看数据
def get_current_user_name() -> str:
"""获取当前用户名(临时实现,实际需要集成认证系统)"""
# TODO: 从session或token中获取用户信息
return request.headers.get('X-User-Name', 'anonymous')
def add_process_history(
workorder_id: int,
processor_name: str,
process_content: str,
action_type: str,
processor_role: Optional[str] = None,
processor_region: Optional[str] = None,
previous_status: Optional[str] = None,
new_status: Optional[str] = None,
assigned_module: Optional[str] = None
) -> WorkOrderProcessHistory:
"""
添加工单处理过程记录
Args:
workorder_id: 工单ID
processor_name: 处理人员姓名
process_content: 处理内容
action_type: 操作类型dispatch、process、close、reassign等
processor_role: 处理人员角色
processor_region: 处理人员区域
previous_status: 处理前的状态
new_status: 处理后的状态
assigned_module: 分配的模块
Returns:
创建的处理记录对象
"""
try:
with db_manager.get_session() as session:
history = WorkOrderProcessHistory(
work_order_id=workorder_id,
processor_name=processor_name,
processor_role=processor_role,
processor_region=processor_region,
process_content=process_content,
action_type=action_type,
previous_status=previous_status,
new_status=new_status,
assigned_module=assigned_module,
process_time=datetime.now()
)
session.add(history)
session.commit()
session.refresh(history)
logger.info(f"工单 {workorder_id} 添加处理记录: {action_type} by {processor_name}")
return history
except Exception as e:
logger.error(f"添加处理记录失败: {e}")
raise
# 移除get_assistant函数使用service_manager
def _ensure_workorder_template_file() -> str:
@@ -125,14 +53,14 @@ def _ensure_workorder_template_file() -> str:
# 获取项目根目录
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(current_dir, '..', '..', '..'))
# 模板文件路径项目根目录下的uploads
template_path = os.path.join(project_root, 'uploads', 'workorder_template.xlsx')
# 确保目录存在
uploads_dir = os.path.join(project_root, 'uploads')
os.makedirs(uploads_dir, exist_ok=True)
if not os.path.exists(template_path):
# 尝试从其他可能的位置复制模板
possible_locations = [
@@ -140,7 +68,7 @@ def _ensure_workorder_template_file() -> str:
os.path.join(current_dir, 'uploads', 'workorder_template.xlsx'),
os.path.join(os.getcwd(), 'uploads', 'workorder_template.xlsx')
]
source_found = False
for source_path in possible_locations:
if os.path.exists(source_path):
@@ -151,7 +79,7 @@ def _ensure_workorder_template_file() -> str:
break
except Exception as e:
logger.warning(f"复制模板文件失败: {e}")
if not source_found:
# 自动生成一个最小可用模板
try:
@@ -163,66 +91,42 @@ def _ensure_workorder_template_file() -> str:
logger.info(f"自动生成模板文件: {template_path}")
except Exception as gen_err:
raise FileNotFoundError('模板文件缺失且自动生成失败请检查依赖openpyxl/pandas') from gen_err
return template_path
@workorders_bp.route('')
def get_workorders():
"""获取工单列表(分页,带权限过滤"""
"""获取工单列表(分页)"""
try:
# 获取当前用户角色和权限
current_role = get_current_user_role()
# 获取分页参数
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
status_filter = request.args.get('status', '')
priority_filter = request.args.get('priority', '')
module_filter = request.args.get('module', '') # 模块过滤
# 从数据库获取分页数据
from src.core.database import db_manager
from src.core.models import WorkOrder
with db_manager.get_session() as session:
# 构建查询
query = session.query(WorkOrder)
# 权限过滤:业务方只能看到自己模块的工单
if not WorkOrderPermissionManager.can_view_all_workorders(current_role):
# 获取用户可访问的模块
accessible_modules = WorkOrderPermissionManager.get_accessible_modules(current_role)
if accessible_modules:
# 构建模块列表过滤条件
module_names = [m.value for m in accessible_modules]
query = query.filter(WorkOrder.assigned_module.in_(module_names))
else:
# 如果没有可访问的模块,返回空列表
return jsonify({
"workorders": [],
"total": 0,
"page": page,
"per_page": per_page,
"total_pages": 0 # 统一使用total_pages字段
})
# 应用过滤器
if status_filter:
query = query.filter(WorkOrder.status == status_filter)
if priority_filter:
query = query.filter(WorkOrder.priority == priority_filter)
if module_filter:
query = query.filter(WorkOrder.assigned_module == module_filter)
# 按创建时间倒序排列
query = query.order_by(WorkOrder.created_at.desc())
# 计算总数
total = query.count()
# 分页查询
workorders = query.offset((page - 1) * per_page).limit(per_page).all()
# 转换为字典
workorders_data = []
for workorder in workorders:
@@ -231,11 +135,6 @@ def get_workorders():
'order_id': workorder.order_id,
'title': workorder.title,
'description': workorder.description,
'assigned_module': workorder.assigned_module,
'module_owner': workorder.module_owner,
'dispatcher': workorder.dispatcher,
'dispatch_time': workorder.dispatch_time.isoformat() if workorder.dispatch_time else None,
'region': workorder.region,
'category': workorder.category,
'priority': workorder.priority,
'status': workorder.status,
@@ -247,10 +146,10 @@ def get_workorders():
'updated_at': workorder.updated_at.isoformat() if workorder.updated_at else None,
'date_of_close': workorder.date_of_close.isoformat() if workorder.date_of_close else None
})
# 计算分页信息
total_pages = (total + per_page - 1) // per_page
return jsonify({
'workorders': workorders_data,
'page': page,
@@ -258,13 +157,13 @@ def get_workorders():
'total': total,
'total_pages': total_pages
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@workorders_bp.route('', methods=['POST'])
def create_workorder():
"""创建工单(初始状态为待分发)"""
"""创建工单"""
try:
data = request.get_json()
result = service_manager.get_assistant().create_work_order(
@@ -273,72 +172,23 @@ def create_workorder():
category=data['category'],
priority=data['priority']
)
# 获取当前用户信息(用于记录创建人)
current_user = get_current_user_name()
current_role = get_current_user_role()
# 创建工单后,设置为待分发状态(未分配模块)
if result and 'id' in result:
workorder_id = result.get('id')
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if workorder:
# 初始状态为待分发
workorder.assigned_module = WorkOrderModule.UNASSIGNED.value
workorder.status = "pending" # 待处理/待分发
workorder.created_by = current_user # 记录创建人
session.commit()
# 记录创建工单的处理历史
processor_region = "overseas" if current_role == UserRole.OVERSEAS_OPS else "domestic"
add_process_history(
workorder_id=workorder_id,
processor_name=current_user,
process_content=f"工单已创建:{data.get('title', '')[:50]}",
action_type="create",
processor_role=current_role.value,
processor_region=processor_region,
previous_status=None,
new_status="pending"
)
# 清除工单相关缓存
from src.core.cache_manager import cache_manager
cache_manager.clear() # 清除所有缓存
return jsonify({"success": True, "workorder": result})
except Exception as e:
return jsonify({"error": str(e)}), 500
@workorders_bp.route('/<int:workorder_id>')
def get_workorder_details(workorder_id):
"""获取工单详情(含数据库对话记录,带权限检查"""
"""获取工单详情(含数据库对话记录)"""
try:
# 获取当前用户角色和权限
current_role = get_current_user_role()
with db_manager.get_session() as session:
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not w:
return jsonify({"error": "工单不存在"}), 404
# 权限检查:业务方只能访问自己模块的工单
if not WorkOrderPermissionManager.can_view_all_workorders(current_role):
# 检查是否有权限访问该工单
assigned_module_str = w.assigned_module
if not assigned_module_str or assigned_module_str == WorkOrderModule.UNASSIGNED.value:
# 未分配的工单,业务方不能访问
return jsonify({"error": "无权访问该工单"}), 403
try:
assigned_module = WorkOrderModule(assigned_module_str)
accessible_modules = WorkOrderPermissionManager.get_accessible_modules(current_role)
if assigned_module not in accessible_modules:
return jsonify({"error": "无权访问该工单"}), 403
except ValueError:
# 如果模块值无效,业务方不能访问
return jsonify({"error": "无权访问该工单"}), 403
convs = session.query(Conversation).filter(Conversation.work_order_id == w.id).order_by(Conversation.timestamp.asc()).all()
conv_list = []
for c in convs:
@@ -348,27 +198,6 @@ def get_workorder_details(workorder_id):
"assistant_response": c.assistant_response,
"timestamp": c.timestamp.isoformat() if c.timestamp else None
})
# 获取处理过程记录
process_history_list = session.query(WorkOrderProcessHistory).filter(
WorkOrderProcessHistory.work_order_id == w.id
).order_by(WorkOrderProcessHistory.process_time.asc()).all()
process_history_data = []
for ph in process_history_list:
process_history_data.append({
"id": ph.id,
"processor_name": ph.processor_name,
"processor_role": ph.processor_role,
"processor_region": ph.processor_region,
"process_content": ph.process_content,
"action_type": ph.action_type,
"previous_status": ph.previous_status,
"new_status": ph.new_status,
"assigned_module": ph.assigned_module,
"process_time": ph.process_time.isoformat() if ph.process_time else None
})
# 在会话内构建工单数据
workorder = {
"id": w.id,
@@ -382,13 +211,7 @@ def get_workorder_details(workorder_id):
"updated_at": w.updated_at.isoformat() if w.updated_at else None,
"resolution": w.resolution,
"satisfaction_score": w.satisfaction_score,
"assigned_module": w.assigned_module,
"module_owner": w.module_owner,
"dispatcher": w.dispatcher,
"dispatch_time": w.dispatch_time.isoformat() if w.dispatch_time else None,
"region": w.region,
"conversations": conv_list,
"process_history": process_history_data # 处理过程记录
"conversations": conv_list
}
return jsonify(workorder)
except Exception as e:
@@ -396,72 +219,29 @@ def get_workorder_details(workorder_id):
@workorders_bp.route('/<int:workorder_id>', methods=['PUT'])
def update_workorder(workorder_id):
"""更新工单(写入数据库,自动记录处理历史"""
"""更新工单(写入数据库)"""
try:
# 获取当前用户信息
current_user = get_current_user_name()
current_role = get_current_user_role()
data = request.get_json()
if not data.get('title') or not data.get('description'):
return jsonify({"error": "标题和描述不能为空"}), 400
with db_manager.get_session() as session:
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not w:
return jsonify({"error": "工单不存在"}), 404
# 记录更新前的状态
previous_status = w.status
previous_priority = w.priority
# 更新工单信息
w.title = data.get('title', w.title)
w.description = data.get('description', w.description)
w.category = data.get('category', w.category)
w.priority = data.get('priority', w.priority)
new_status = data.get('status', w.status)
w.status = new_status
w.status = data.get('status', w.status)
w.resolution = data.get('resolution', w.resolution)
w.satisfaction_score = data.get('satisfaction_score', w.satisfaction_score)
w.updated_at = datetime.now()
session.commit()
# 如果状态或优先级发生变化,记录处理历史
has_status_change = previous_status != new_status
has_priority_change = previous_priority != data.get('priority', w.priority)
if has_status_change or has_priority_change:
# 构建处理内容
change_items = []
if has_status_change:
change_items.append(f"状态变更:{previous_status}{new_status}")
if has_priority_change:
change_items.append(f"优先级变更:{previous_priority}{data.get('priority', w.priority)}")
process_content = "".join(change_items)
if data.get('resolution'):
process_content += f";解决方案:{data.get('resolution', '')[:100]}"
# 判断区域
processor_region = "overseas" if current_role == UserRole.OVERSEAS_OPS else "domestic"
add_process_history(
workorder_id=workorder_id,
processor_name=current_user,
process_content=process_content or "更新工单信息",
action_type="update",
processor_role=current_role.value,
processor_region=processor_region,
previous_status=previous_status,
new_status=new_status
)
# 清除工单相关缓存
from src.core.cache_manager import cache_manager
cache_manager.clear() # 清除所有缓存
updated = {
"id": w.id,
"title": w.title,
@@ -485,25 +265,25 @@ def delete_workorder(workorder_id):
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return jsonify({"error": "工单不存在"}), 404
# 先删除所有相关的子记录(按外键依赖顺序)
# 1. 删除工单建议记录
try:
session.execute(text("DELETE FROM work_order_suggestions WHERE work_order_id = :id"), {"id": workorder_id})
except Exception as e:
print(f"删除工单建议记录失败: {e}")
# 2. 删除对话记录
session.query(Conversation).filter(Conversation.work_order_id == workorder_id).delete()
# 3. 删除工单
session.delete(workorder)
session.commit()
# 清除工单相关缓存
from src.core.cache_manager import cache_manager
cache_manager.clear() # 清除所有缓存
return jsonify({
"success": True,
"message": "工单删除成功"
@@ -511,6 +291,39 @@ def delete_workorder(workorder_id):
except Exception as e:
return jsonify({"error": str(e)}), 500
@workorders_bp.route('/generate-ai-suggestion', methods=['POST'])
def generate_ai_suggestion():
"""通用AI建议生成API - 不需要先创建工单"""
try:
data = request.get_json()
if not data or 'tr_description' not in data:
return jsonify({"error": "缺少tr_description参数"}), 400
tr_description = data['tr_description']
vin = data.get('vin')
process_history = data.get('process_history')
# 使用AI建议服务生成建议
from src.integrations.ai_suggestion_service import AISuggestionService
ai_service = AISuggestionService()
suggestion = ai_service.generate_suggestion(
tr_description=tr_description,
process_history=process_history,
vin=vin
)
return jsonify({
"success": True,
"suggestion": suggestion,
"message": "AI建议生成成功"
})
except Exception as e:
logger.error(f"AI建议生成失败: {e}")
return jsonify({"error": f"AI建议生成失败: {str(e)}"}), 500
@workorders_bp.route('/<int:workorder_id>/ai-suggestion', methods=['POST'])
def generate_workorder_ai_suggestion(workorder_id):
"""根据工单描述与知识库生成AI建议草稿"""
@@ -582,22 +395,22 @@ def save_workorder_human_resolution(workorder_id):
except Exception:
sim = 0.0
rec.ai_similarity = sim
# 使用简化的配置
config = SimpleAIAccuracyConfig()
# 自动审批条件
approved = config.should_auto_approve(sim)
rec.approved = approved
# 记录使用人工描述入库的标记当AI准确率低于阈值时
use_human_resolution = config.should_use_human_resolution(sim)
rec.use_human_resolution = use_human_resolution
session.commit()
return jsonify({
"success": True,
"similarity": sim,
"success": True,
"similarity": sim,
"approved": approved,
"use_human_resolution": use_human_resolution
})
@@ -612,14 +425,14 @@ def approve_workorder_to_knowledge(workorder_id):
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not w:
return jsonify({"error": "工单不存在"}), 404
rec = session.query(WorkOrderSuggestion).filter(WorkOrderSuggestion.work_order_id == w.id).first()
if not rec:
return jsonify({"error": "未找到工单建议记录"}), 400
# 使用简化的配置
config = SimpleAIAccuracyConfig()
# 确定使用哪个内容入库
if rec.use_human_resolution and rec.human_resolution:
# AI准确率低于阈值使用人工描述入库
@@ -635,7 +448,7 @@ def approve_workorder_to_knowledge(workorder_id):
logger.info(f"工单 {workorder_id} 使用AI建议入库相似度: {rec.ai_similarity:.4f}")
else:
return jsonify({"error": "未找到可入库的内容"}), 400
# 入库为知识条目
entry = KnowledgeEntry(
question=w.title or (w.description[:20] if w.description else '工单问题'),
@@ -649,9 +462,9 @@ def approve_workorder_to_knowledge(workorder_id):
)
session.add(entry)
session.commit()
return jsonify({
"success": True,
"success": True,
"knowledge_id": entry.id,
"used_content": "human_resolution" if rec.use_human_resolution else "ai_suggestion",
"confidence_score": confidence_score
@@ -667,25 +480,25 @@ def import_workorders():
# 检查是否有文件上传
if 'file' not in request.files:
return jsonify({"error": "没有上传文件"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "没有选择文件"}), 400
if not file.filename.endswith(('.xlsx', '.xls')):
return jsonify({"error": "只支持Excel文件(.xlsx, .xls)"}), 400
# 保存上传的文件
filename = secure_filename(file.filename)
upload_path = os.path.join('uploads', filename)
os.makedirs('uploads', exist_ok=True)
file.save(upload_path)
# 解析Excel文件
try:
df = pd.read_excel(upload_path)
imported_workorders = []
# 处理每一行数据
for index, row in df.iterrows():
# 根据Excel列名映射到工单字段
@@ -694,16 +507,16 @@ def import_workorders():
category = str(row.get('分类', row.get('category', '技术问题')))
priority = str(row.get('优先级', row.get('priority', 'medium')))
status = str(row.get('状态', row.get('status', 'open')))
# 验证必填字段
if not title or title.strip() == '':
continue
# 生成唯一的工单ID
timestamp = int(time.time())
unique_id = str(uuid.uuid4())[:8]
order_id = f"IMP_{timestamp}_{unique_id}"
# 创建工单到数据库
try:
with db_manager.get_session() as session:
@@ -717,26 +530,26 @@ def import_workorders():
created_at=datetime.now(),
updated_at=datetime.now()
)
# 处理可选字段
if pd.notna(row.get('解决方案', row.get('resolution'))):
workorder.resolution = str(row.get('解决方案', row.get('resolution')))
if pd.notna(row.get('满意度', row.get('satisfaction_score'))):
try:
workorder.satisfaction_score = int(row.get('满意度', row.get('satisfaction_score')))
except (ValueError, TypeError):
workorder.satisfaction_score = None
session.add(workorder)
session.commit()
logger.info(f"成功导入工单: {order_id} - {title}")
except Exception as db_error:
logger.error(f"导入工单到数据库失败: {db_error}")
continue
# 添加到返回列表
imported_workorders.append({
"id": workorder.id,
@@ -751,23 +564,23 @@ def import_workorders():
"resolution": workorder.resolution,
"satisfaction_score": workorder.satisfaction_score
})
# 清理上传的文件
os.remove(upload_path)
return jsonify({
"success": True,
"message": f"成功导入 {len(imported_workorders)} 个工单",
"imported_count": len(imported_workorders),
"workorders": imported_workorders
})
except Exception as e:
# 清理上传的文件
if os.path.exists(upload_path):
os.remove(upload_path)
return jsonify({"error": f"解析Excel文件失败: {str(e)}"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -780,7 +593,7 @@ def download_import_template():
"success": True,
"template_url": f"/uploads/workorder_template.xlsx"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -789,256 +602,27 @@ def download_import_template_file():
"""直接返回工单导入模板文件(下载)"""
try:
template_path = _ensure_workorder_template_file()
# 检查文件是否存在
if not os.path.exists(template_path):
logger.error(f"模板文件不存在: {template_path}")
return jsonify({"error": "模板文件不存在"}), 404
# 检查文件大小
file_size = os.path.getsize(template_path)
if file_size == 0:
logger.error(f"模板文件为空: {template_path}")
return jsonify({"error": "模板文件为空"}), 500
logger.info(f"准备下载模板文件: {template_path}, 大小: {file_size} bytes")
try:
# Flask>=2 使用 download_name
return send_file(template_path, as_attachment=True, download_name='工单导入模板.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
except TypeError:
# 兼容 Flask<2 的 attachment_filename
return send_file(template_path, as_attachment=True, attachment_filename='工单导入模板.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
except Exception as e:
logger.error(f"下载模板文件失败: {e}")
return jsonify({"error": f"下载失败: {str(e)}"}), 500
@workorders_bp.route('/<int:workorder_id>/dispatch', methods=['POST'])
def dispatch_workorder(workorder_id):
"""工单分发:运维将工单分配给业务模块"""
try:
# 获取当前用户角色和权限
current_role = get_current_user_role()
current_user = get_current_user_name()
# 检查分发权限
if not WorkOrderPermissionManager.can_dispatch_workorder(current_role):
return jsonify({
"success": False,
"error": "无权进行工单分发,只有属地运维和管理员可以分发工单"
}), 403
# 获取请求数据
data = request.get_json() or {}
target_module_str = data.get('target_module', '')
if not target_module_str:
return jsonify({"success": False, "error": "请指定目标模块"}), 400
# 验证模块
try:
target_module = WorkOrderModule(target_module_str)
except ValueError:
return jsonify({"success": False, "error": f"无效的模块: {target_module_str}"}), 400
# 获取工单
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return jsonify({"success": False, "error": "工单不存在"}), 404
# 执行分发
module_owner = WorkOrderDispatchManager.get_module_owner(target_module)
# 记录分发前的状态
previous_status = workorder.status
# 更新工单信息
workorder.assigned_module = target_module.value
workorder.module_owner = module_owner
workorder.dispatcher = current_user
workorder.dispatch_time = datetime.now()
workorder.status = "assigned" # 更新状态为已分配
# 根据区域自动设置可以从工单source或其他字段判断
# 这里简化处理,可以根据实际需求调整
if not workorder.region:
# 如果source包含特定关键词可以判断区域
source = workorder.source or ""
if any(keyword in source.lower() for keyword in ["overseas", "abroad", "海外"]):
workorder.region = "overseas"
else:
workorder.region = "domestic"
session.commit()
# 记录处理历史:工单分发
processor_region = "overseas" if current_role == UserRole.OVERSEAS_OPS else "domestic"
add_process_history(
workorder_id=workorder_id,
processor_name=current_user,
process_content=f"工单已分发到{target_module.value}模块,业务接口人:{module_owner}",
action_type="dispatch",
processor_role=current_role.value,
processor_region=processor_region,
previous_status=previous_status,
new_status="assigned",
assigned_module=target_module.value
)
logger.info(f"工单 {workorder_id} 已分发到 {target_module.value} 模块,分发人: {current_user}")
return jsonify({
"success": True,
"message": f"工单已成功分发到{target_module.value}模块",
"workorder": {
"id": workorder.id,
"assigned_module": workorder.assigned_module,
"module_owner": workorder.module_owner,
"dispatcher": workorder.dispatcher,
"dispatch_time": workorder.dispatch_time.isoformat() if workorder.dispatch_time else None,
"status": workorder.status
}
})
except Exception as e:
logger.error(f"工单分发失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@workorders_bp.route('/<int:workorder_id>/suggest-module', methods=['POST'])
def suggest_workorder_module(workorder_id):
"""AI建议工单应该分配的模块"""
try:
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return jsonify({"success": False, "error": "工单不存在"}), 404
# 使用AI分析建议模块
suggested_module = WorkOrderDispatchManager.suggest_module(
description=workorder.description or "",
title=workorder.title or ""
)
return jsonify({
"success": True,
"suggested_module": suggested_module.value if suggested_module else None,
"module_owner": WorkOrderDispatchManager.get_module_owner(suggested_module) if suggested_module else None
})
except Exception as e:
logger.error(f"模块建议失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@workorders_bp.route('/modules', methods=['GET'])
def get_available_modules():
"""获取所有可用的模块列表"""
try:
modules = [
{"value": m.value, "name": m.name, "owner": WorkOrderDispatchManager.get_module_owner(m)}
for m in WorkOrderModule
if m != WorkOrderModule.UNASSIGNED
]
return jsonify({"success": True, "modules": modules})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@workorders_bp.route('/<int:workorder_id>/process-history', methods=['GET'])
def get_workorder_process_history(workorder_id):
"""获取工单处理过程记录"""
try:
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return jsonify({"error": "工单不存在"}), 404
# 获取处理历史
history_list = session.query(WorkOrderProcessHistory).filter(
WorkOrderProcessHistory.work_order_id == workorder_id
).order_by(WorkOrderProcessHistory.process_time.asc()).all()
history_data = []
for ph in history_list:
history_data.append({
"id": ph.id,
"processor_name": ph.processor_name,
"processor_role": ph.processor_role,
"processor_region": ph.processor_region,
"process_content": ph.process_content,
"action_type": ph.action_type,
"previous_status": ph.previous_status,
"new_status": ph.new_status,
"assigned_module": ph.assigned_module,
"process_time": ph.process_time.isoformat() if ph.process_time else None
})
return jsonify({
"success": True,
"workorder_id": workorder_id,
"process_history": history_data,
"total": len(history_data)
})
except Exception as e:
logger.error(f"获取处理历史失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@workorders_bp.route('/<int:workorder_id>/process-history', methods=['POST'])
def add_workorder_process_history(workorder_id):
"""手动添加工单处理过程记录"""
try:
# 获取当前用户信息
current_user = get_current_user_name()
current_role = get_current_user_role()
data = request.get_json() or {}
process_content = data.get('process_content', '').strip()
if not process_content:
return jsonify({"success": False, "error": "处理内容不能为空"}), 400
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return jsonify({"success": False, "error": "工单不存在"}), 404
# 获取可选参数
action_type = data.get('action_type', 'process') # 默认操作类型为process
processor_role = data.get('processor_role', current_role.value)
processor_region = data.get('processor_region')
if not processor_region:
# 根据角色自动判断区域
processor_region = "overseas" if current_role == UserRole.OVERSEAS_OPS else "domestic"
previous_status = data.get('previous_status', workorder.status)
new_status = data.get('new_status', workorder.status)
assigned_module = data.get('assigned_module', workorder.assigned_module)
# 添加处理记录
history = add_process_history(
workorder_id=workorder_id,
processor_name=data.get('processor_name', current_user),
process_content=process_content,
action_type=action_type,
processor_role=processor_role,
processor_region=processor_region,
previous_status=previous_status,
new_status=new_status,
assigned_module=assigned_module
)
return jsonify({
"success": True,
"message": "处理记录已添加",
"history": {
"id": history.id,
"processor_name": history.processor_name,
"processor_role": history.processor_role,
"process_content": history.process_content,
"action_type": history.action_type,
"process_time": history.process_time.isoformat() if history.process_time else None
}
})
except Exception as e:
logger.error(f"添加处理记录失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500

79
src/web/decorators.py Normal file
View File

@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""
通用装饰器
提供统一的错误处理、服务管理等装饰器
"""
from functools import wraps
from flask import jsonify
from src.web.service_manager import service_manager
def handle_errors(default_response=None):
"""统一错误处理装饰器"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
return f(*args, **kwargs)
except Exception as e:
if default_response:
return default_response
return jsonify({"error": str(e)}), 500
return decorated_function
return decorator
def with_service(service_name):
"""服务注入装饰器"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# 将服务管理器注入到函数参数中
kwargs['service_manager'] = service_manager
return f(*args, **kwargs)
return decorated_function
return decorator
def require_json(required_fields=None):
"""JSON请求验证装饰器"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
from flask import request
try:
data = request.get_json()
if data is None:
return jsonify({"error": "请求必须是JSON格式"}), 400
if required_fields:
missing_fields = [field for field in required_fields if field not in data]
if missing_fields:
return jsonify({"error": f"缺少必要字段: {', '.join(missing_fields)}"}), 400
kwargs['data'] = data
return f(*args, **kwargs)
except Exception as e:
return jsonify({"error": "JSON格式错误"}), 400
return decorated_function
return decorator
def cache_response(timeout=300):
"""响应缓存装饰器"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
from flask import make_response
response = f(*args, **kwargs)
if isinstance(response, tuple):
response, status = response
else:
status = 200
if hasattr(response, 'headers'):
response.headers['Cache-Control'] = f'public, max-age={timeout}'
return response
return decorated_function
return decorator

View File

@@ -41,7 +41,7 @@ def handle_database_errors(func: Callable) -> Callable:
return func(*args, **kwargs)
except Exception as e:
logger.error(f"数据库错误 {func.__name__}: {e}")
return jsonify({"error": "数据库操作失败"}), 500
return jsonify({"error": f"数据库操作失败: {str(e)}"}), 500
return wrapper

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""
服务管理器
统一管理各种服务的懒加载实例
"""
from typing import Optional, Dict, Any
@@ -10,14 +11,17 @@ logger = logging.getLogger(__name__)
class ServiceManager:
"""服务管理器 - 统一管理各种服务的懒加载实例"""
def __init__(self):
self._services: Dict[str, Any] = {}
def get_service(self, service_name: str, factory_func):
"""获取服务实例"""
"""获取服务实例(懒加载)"""
if service_name not in self._services:
try:
self._services[service_name] = factory_func()
logger.info(f"服务 {service_name} 已初始化")
except Exception as e:
logger.error(f"初始化服务 {service_name} 失败: {e}")
raise
@@ -55,11 +59,12 @@ class ServiceManager:
"""清除指定服务实例"""
if service_name in self._services:
del self._services[service_name]
logger.info(f"服务 {service_name} 已清除")
def clear_all_services(self):
"""清除所有服务实例"""
self._services.clear()
logger.info("所有服务实例已清除")
# 全局服务管理器实例

View File

@@ -0,0 +1,519 @@
/* 组件样式 */
/* 按钮样式增强 */
.btn {
font-weight: 500;
transition: var(--transition);
border: none;
padding: 0.5rem 1.25rem;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.btn-icon {
padding: 0.5rem;
line-height: 1;
}
/* 卡片样式增强 */
.card {
border: none;
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-sm);
transition: var(--transition);
}
.card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.card-header {
background: transparent;
border-bottom: 1px solid var(--border-light);
padding: var(--spacing-lg);
font-weight: 600;
}
/* 统计卡片 */
.stat-card {
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
color: var(--text-white);
border-radius: var(--border-radius-lg);
padding: var(--spacing-lg);
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100px;
background: rgba(255,255,255,0.1);
border-radius: 50%;
transform: translate(30px, -30px);
}
.stat-card.success {
background: linear-gradient(135deg, var(--success-color), #1e7e34);
}
.stat-card.warning {
background: linear-gradient(135deg, var(--warning-color), #e0a800);
}
.stat-card.danger {
background: linear-gradient(135deg, var(--danger-color), #bd2130);
}
.stat-card.info {
background: linear-gradient(135deg, var(--info-color), #138496);
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
line-height: 1;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: var(--font-size-sm);
opacity: 0.9;
}
/* 数据表格 */
.data-table {
background: var(--bg-primary);
border-radius: var(--border-radius);
overflow: hidden;
}
.data-table table {
margin-bottom: 0;
}
.data-table th {
background: var(--bg-secondary);
border-bottom: 2px solid var(--border-color);
font-weight: 600;
text-transform: uppercase;
font-size: var(--font-size-sm);
padding: var(--spacing);
}
.data-table td {
padding: var(--spacing);
vertical-align: middle;
}
.data-table tbody tr:hover {
background: var(--bg-secondary);
}
/* 表格工具栏 */
.table-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing);
flex-wrap: wrap;
gap: var(--spacing);
}
.table-search {
position: relative;
flex: 1;
max-width: 300px;
}
.table-search input {
padding-left: 2.5rem;
}
.table-search i {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
}
/* 分页 */
.pagination {
margin-bottom: 0;
}
.pagination .page-link {
border: none;
color: var(--primary-color);
padding: 0.5rem 0.75rem;
}
.pagination .page-item.active .page-link {
background: var(--primary-color);
border-color: var(--primary-color);
}
/* 模态框增强 */
.modal-content {
border: none;
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-lg);
}
.modal-header {
border-bottom: 1px solid var(--border-light);
padding: var(--spacing-lg);
}
.modal-footer {
border-top: 1px solid var(--border-light);
padding: var(--spacing-lg);
}
/* 表单样式 */
.form-label {
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.form-control, .form-select {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 0.75rem;
transition: var(--transition-fast);
}
.form-control:focus, .form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(0,123,255,0.25);
}
/* 徽章 */
.badge {
font-weight: 500;
padding: 0.375em 0.75em;
}
/* 进度条 */
.progress {
height: 8px;
border-radius: 4px;
background: var(--bg-light);
}
.progress-bar {
border-radius: 4px;
}
/* Toast 通知 */
.toast-container {
position: fixed;
top: var(--navbar-height);
right: var(--spacing);
z-index: var(--z-tooltip);
}
.toast {
min-width: 300px;
border: none;
box-shadow: var(--shadow-md);
margin-bottom: var(--spacing);
}
.toast.success {
background: var(--success-color);
color: var(--text-white);
}
.toast.error {
background: var(--danger-color);
color: var(--text-white);
}
.toast.warning {
background: var(--warning-color);
color: var(--text-primary);
}
.toast.info {
background: var(--info-color);
color: var(--text-white);
}
/* 加载动画 */
.spinner {
display: inline-block;
width: 2rem;
height: 2rem;
border: 3px solid var(--border-light);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 骨架屏 */
.skeleton {
background: linear-gradient(90deg, var(--bg-light) 25%, var(--border-light) 50%, var(--bg-light) 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* 空状态 */
.empty-state {
text-align: center;
padding: var(--spacing-xl);
color: var(--text-muted);
}
.empty-state i {
font-size: 4rem;
margin-bottom: var(--spacing);
opacity: 0.5;
}
/* 状态指示器 */
.status-indicator {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: var(--font-size-sm);
}
.status-indicator::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
}
.status-indicator.online {
color: var(--success-color);
}
.status-indicator.offline {
color: var(--danger-color);
}
.status-indicator.pending {
color: var(--warning-color);
}
/* 标签页 */
.nav-tabs {
border-bottom: 2px solid var(--border-light);
}
.nav-tabs .nav-link {
border: none;
color: var(--text-secondary);
padding: var(--spacing) var(--spacing-lg);
transition: var(--transition-fast);
}
.nav-tabs .nav-link:hover {
color: var(--primary-color);
border: none;
}
.nav-tabs .nav-link.active {
color: var(--primary-color);
background: transparent;
border: none;
border-bottom: 2px solid var(--primary-color);
}
/* 折叠/展开 */
.collapsible {
cursor: pointer;
user-select: none;
}
.collapsible-icon {
transition: transform 0.3s ease;
}
.collapsible.collapsed .collapsible-icon {
transform: rotate(-90deg);
}
/* 代码块 */
.code-block {
background: var(--bg-dark);
color: var(--text-white);
padding: var(--spacing);
border-radius: var(--border-radius);
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: var(--font-size-sm);
}
/* 时间线 */
.timeline {
position: relative;
padding-left: 2rem;
}
.timeline::before {
content: '';
position: absolute;
left: 0.5rem;
top: 0;
bottom: 0;
width: 2px;
background: var(--border-color);
}
.timeline-item {
position: relative;
margin-bottom: var(--spacing-lg);
}
.timeline-item::before {
content: '';
position: absolute;
left: -1.5rem;
top: 0.5rem;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--primary-color);
border: 2px solid var(--bg-primary);
}
/* 响应式调整 */
@media (max-width: 768px) {
.table-toolbar {
flex-direction: column;
align-items: stretch;
}
.table-search {
max-width: 100%;
}
.stat-number {
font-size: 2rem;
}
.toast-container {
left: var(--spacing);
right: var(--spacing);
}
.toast {
min-width: auto;
}
}
/* 导航栏样式 */
.navbar {
height: var(--navbar-height);
background: var(--bg-primary);
border-bottom: 1px solid var(--border-light);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing-lg);
position: relative;
z-index: var(--z-sticky);
}
.navbar-brand {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: 1.25rem;
font-weight: 700;
color: var(--primary-color);
text-decoration: none;
}
.navbar-brand i {
font-size: 1.5rem;
}
.navbar-toggler {
display: none;
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.25rem;
padding: var(--spacing-sm);
border-radius: var(--border-radius);
transition: var(--transition);
cursor: pointer;
}
.navbar-toggler:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.navbar-nav {
display: flex;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
gap: var(--spacing);
}
.nav-item {
position: relative;
}
.nav-link {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing);
color: var(--text-secondary);
text-decoration: none;
border-radius: var(--border-radius);
transition: var(--transition);
font-weight: 500;
}
.nav-link:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.nav-link i {
font-size: 1rem;
}
/* 响应式导航栏 */
@media (max-width: 768px) {
.navbar-toggler {
display: block;
order: -1;
}
.navbar-brand span {
display: none;
}
.navbar-nav {
display: none;
}
}

View File

@@ -0,0 +1,590 @@
/* 布局样式 */
/* 全局样式 */
* {
box-sizing: border-box;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
}
body {
font-family: var(--font-family);
font-size: var(--font-size);
line-height: 1.6;
color: var(--text-primary);
background: var(--bg-secondary);
}
/* 应用容器 */
.app-container {
display: flex;
min-height: 100vh;
position: relative;
}
/* 主要内容区域 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
margin-left: var(--sidebar-width);
transition: margin-left 0.3s ease;
}
.sidebar.collapsed + .main-content {
margin-left: var(--sidebar-collapsed-width);
}
/* 页面容器 */
.page-container {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; /* 防止flex item超出容器 */
}
/* 页面头部统一样式 */
.page-header {
background: var(--bg-primary);
border-bottom: 1px solid var(--border-light);
padding: var(--spacing-lg) var(--spacing-xl);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.page-title {
font-size: 1.75rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.page-subtitle {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin: var(--spacing-xs) 0 0 0;
}
.page-actions {
display: flex;
gap: var(--spacing);
align-items: center;
}
/* 页面内容区域 */
.page-content {
flex: 1;
padding: var(--spacing-xl);
overflow-y: auto;
min-height: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-content {
margin-left: 0;
}
.sidebar {
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.page-header {
padding: var(--spacing) var(--spacing-lg);
flex-direction: column;
align-items: flex-start;
gap: var(--spacing);
}
.page-title {
font-size: 1.5rem;
}
.page-content {
padding: var(--spacing-lg);
}
}
@media (max-width: 576px) {
.page-header {
padding: var(--spacing-sm) var(--spacing);
}
.page-content {
padding: var(--spacing);
}
}
/* 侧边栏 */
.sidebar {
width: var(--sidebar-width);
background: var(--bg-dark);
color: var(--text-white);
transition: all 0.3s ease;
position: fixed;
top: 0;
left: 0;
bottom: 0;
z-index: var(--z-fixed);
overflow-x: hidden;
overflow-y: auto;
box-shadow: var(--shadow-lg);
}
.sidebar.collapsed {
width: var(--sidebar-collapsed-width);
}
/* 移动端侧边栏遮罩 */
.sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: calc(var(--z-fixed) - 1);
}
.sidebar-overlay.show {
display: block;
}
.sidebar-header {
padding: var(--spacing-lg);
border-bottom: 1px solid rgba(255,255,255,0.1);
display: flex;
align-items: center;
gap: var(--spacing);
}
.sidebar-logo {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-white);
text-decoration: none;
white-space: nowrap;
}
.sidebar.collapsed .sidebar-logo span {
display: none;
}
.sidebar-nav {
padding: var(--spacing) 0;
list-style: none;
margin: 0;
}
.sidebar-nav-item {
display: flex;
align-items: center;
padding: 0.875rem var(--spacing-lg);
color: rgba(255,255,255,0.8);
text-decoration: none;
transition: all 0.2s ease;
position: relative;
white-space: nowrap;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
margin: 0 var(--spacing-sm);
font-weight: 500;
}
.sidebar-nav-item:hover {
color: var(--text-white);
background: rgba(255,255,255,0.1);
transform: translateX(4px);
}
.sidebar-nav-item.active {
color: var(--text-white);
background: linear-gradient(90deg, var(--primary-color), var(--primary-dark));
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.sidebar-nav-item.active::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--primary-light);
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.sidebar-nav-item i {
width: 20px;
margin-right: var(--spacing);
text-align: center;
}
.sidebar.collapsed .sidebar-nav-item span {
display: none;
}
.sidebar.collapsed .sidebar-nav-item i {
margin-right: 0;
}
.sidebar-toggle {
position: absolute;
right: -15px;
top: var(--spacing-xl);
background: var(--primary-color);
color: var(--text-white);
border: 2px solid var(--bg-secondary);
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition-fast);
}
.sidebar-toggle:hover {
background: var(--primary-dark);
}
/* 主内容区 */
.main-content {
flex: 1;
margin-left: var(--sidebar-width);
transition: margin-left 0.3s ease;
display: flex;
flex-direction: column;
}
.sidebar.collapsed + .main-content {
margin-left: var(--sidebar-collapsed-width);
}
/* 顶部导航栏 */
.navbar {
background: var(--bg-primary);
box-shadow: var(--shadow-sm);
padding: 0 var(--spacing-lg);
height: var(--navbar-height);
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: var(--z-sticky);
}
.navbar-brand {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
text-decoration: none;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.navbar-nav {
display: flex;
align-items: center;
gap: var(--spacing);
list-style: none;
margin: 0;
padding: 0;
}
.navbar-nav .nav-item {
position: relative;
}
.navbar-nav .nav-link {
color: var(--text-secondary);
padding: 0.5rem var(--spacing);
border-radius: var(--border-radius);
text-decoration: none;
transition: var(--transition-fast);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.navbar-nav .nav-link:hover {
color: var(--primary-color);
background: var(--bg-secondary);
}
.navbar-nav .nav-link.active {
color: var(--primary-color);
background: rgba(0,123,255,0.1);
}
/* 用户菜单 */
.user-menu {
position: relative;
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--primary-color);
color: var(--text-white);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: 500;
}
.user-dropdown {
position: absolute;
top: 100%;
right: 0;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow-md);
min-width: 200px;
padding: var(--spacing-sm) 0;
margin-top: var(--spacing-sm);
display: none;
z-index: var(--z-dropdown);
}
.user-dropdown.show {
display: block;
}
.user-dropdown-item {
display: block;
padding: 0.5rem var(--spacing);
color: var(--text-primary);
text-decoration: none;
transition: var(--transition-fast);
}
.user-dropdown-item:hover {
background: var(--bg-secondary);
}
.user-dropdown-divider {
border-top: 1px solid var(--border-light);
margin: var(--spacing-sm) 0;
}
/* 页面内容 */
.page-content {
flex: 1;
padding: var(--spacing-lg);
overflow-y: auto;
}
/* 页面头部 */
.page-header {
margin-bottom: var(--spacing-lg);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing);
}
.page-title {
font-size: 1.75rem;
font-weight: 700;
color: var(--text-primary);
margin: 0;
}
.page-subtitle {
font-size: var(--font-size);
color: var(--text-muted);
margin: var(--spacing-sm) 0 0 0;
}
.page-actions {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
/* 面包屑导航 */
.breadcrumb {
background: transparent;
padding: 0;
margin-bottom: var(--spacing);
}
.breadcrumb-item {
color: var(--text-muted);
}
.breadcrumb-item a {
color: var(--text-secondary);
text-decoration: none;
}
.breadcrumb-item a:hover {
color: var(--primary-color);
}
.breadcrumb-item.active {
color: var(--text-primary);
}
/* 网格系统增强 */
.row {
margin-left: -0.5rem;
margin-right: -0.5rem;
}
.row > [class*="col"] {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
/* 间距工具类 */
.gap-0 { gap: 0; }
.gap-1 { gap: var(--spacing-xs); }
.gap-2 { gap: var(--spacing-sm); }
.gap-3 { gap: var(--spacing); }
.gap-4 { gap: var(--spacing-lg); }
.gap-5 { gap: var(--spacing-xl); }
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* 加载遮罩 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal);
}
.loading-content {
background: var(--bg-primary);
padding: var(--spacing-lg);
border-radius: var(--border-radius);
text-align: center;
box-shadow: var(--shadow-lg);
}
/* 响应式布局 */
@media (max-width: 992px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
.page-header {
flex-direction: column;
align-items: flex-start;
}
}
@media (max-width: 768px) {
.navbar {
padding: 0 var(--spacing);
}
.page-content {
padding: var(--spacing);
}
.row {
margin-left: -0.25rem;
margin-right: -0.25rem;
}
.row > [class*="col"] {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.page-actions {
width: 100%;
justify-content: flex-start;
}
}
@media (max-width: 576px) {
.page-title {
font-size: 1.5rem;
}
.stat-number {
font-size: 1.75rem;
}
.btn {
padding: 0.375rem 0.75rem;
font-size: var(--font-size-sm);
}
}
/* 打印样式 */
@media print {
.sidebar,
.navbar,
.page-actions,
.btn,
.modal {
display: none !important;
}
.main-content {
margin-left: 0 !important;
}
.page-content {
padding: 0;
}
.card {
box-shadow: none;
border: 1px solid var(--border-color);
}
}

417
src/web/static/css/main.css Normal file
View File

@@ -0,0 +1,417 @@
/* 主样式文件 - 导入所有模块 */
@import url('variables.css');
@import url('components.css');
@import url('layout.css');
/* 全局动画 */
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
transition: opacity 0.3s ease;
}
.fade-exit {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
transition: opacity 0.3s ease;
}
.slide-enter {
transform: translateX(100%);
}
.slide-enter-active {
transform: translateX(0);
transition: transform 0.3s ease;
}
.slide-exit {
transform: translateX(0);
}
.slide-exit-active {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
/* 实用工具类 */
.text-truncate-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.text-truncate-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.hover-scale {
transition: transform 0.2s ease;
}
.hover-scale:hover {
transform: scale(1.05);
}
.hover-shadow {
transition: box-shadow 0.2s ease;
}
.hover-shadow:hover {
box-shadow: var(--shadow-md);
}
/* 自定义滚动条区域 */
.custom-scrollbar {
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border-color) var(--bg-secondary);
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* 文件上传区域 */
.file-upload-area {
border: 2px dashed var(--border-color);
border-radius: var(--border-radius);
padding: var(--spacing-xl);
text-align: center;
transition: var(--transition-fast);
cursor: pointer;
}
.file-upload-area:hover {
border-color: var(--primary-color);
background: var(--bg-secondary);
}
.file-upload-area.dragover {
border-color: var(--primary-color);
background: rgba(0,123,255,0.05);
}
/* 代码高亮 */
.highlight {
background: var(--warning-color);
color: var(--text-primary);
padding: 0.125rem 0.25rem;
border-radius: var(--border-radius-sm);
font-size: 0.875em;
}
/* 搜索高亮 */
.search-highlight {
background: yellow;
padding: 0.125rem 0.25rem;
border-radius: 2px;
}
/* 浮动操作按钮 */
.fab {
position: fixed;
bottom: var(--spacing-lg);
right: var(--spacing-lg);
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--primary-color);
color: var(--text-white);
border: none;
box-shadow: var(--shadow-lg);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition);
z-index: var(--z-fixed);
}
.fab:hover {
transform: scale(1.1);
box-shadow: 0 8px 32px rgba(0,123,255,0.3);
}
/* 标签输入 */
.tag-input {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
min-height: 38px;
cursor: text;
}
.tag-input input {
border: none;
outline: none;
flex: 1;
min-width: 100px;
padding: 0.25rem;
}
.tag {
background: var(--primary-color);
color: var(--text-white);
padding: 0.25rem 0.5rem;
border-radius: var(--border-radius-sm);
display: flex;
align-items: center;
gap: 0.25rem;
font-size: var(--font-size-sm);
}
.tag-remove {
cursor: pointer;
opacity: 0.8;
transition: opacity 0.2s;
}
.tag-remove:hover {
opacity: 1;
}
/* 开关切换 */
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.switch-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--border-color);
transition: var(--transition);
border-radius: 24px;
}
.switch-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background: white;
transition: var(--transition);
border-radius: 50%;
}
input:checked + .switch-slider {
background: var(--primary-color);
}
input:checked + .switch-slider:before {
transform: translateX(20px);
}
/* 评分组件 */
.rating {
display: inline-flex;
gap: 0.25rem;
}
.rating-star {
color: var(--border-color);
cursor: pointer;
transition: color 0.2s;
}
.rating-star.filled {
color: var(--warning-color);
}
.rating-star:hover {
color: var(--warning-color);
}
/* 步骤条 */
.steps {
display: flex;
justify-content: space-between;
position: relative;
margin: var(--spacing-lg) 0;
}
.step {
flex: 1;
text-align: center;
position: relative;
}
.step::before {
content: '';
position: absolute;
top: 20px;
left: 50%;
right: -50%;
height: 2px;
background: var(--border-color);
z-index: -1;
}
.step:last-child::before {
display: none;
}
.step.active::before {
background: var(--primary-color);
}
.step-circle {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--border-color);
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: var(--spacing-sm);
transition: var(--transition);
}
.step.active .step-circle,
.step.completed .step-circle {
background: var(--primary-color);
color: var(--text-white);
}
.step-label {
font-size: var(--font-size-sm);
color: var(--text-muted);
}
.step.active .step-label {
color: var(--text-primary);
font-weight: 500;
}
/* 穿梭框 */
.transfer {
display: flex;
gap: var(--spacing);
align-items: center;
}
_transfer-panel {
flex: 1;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--bg-primary);
}
_transfer-header {
padding: var(--spacing);
border-bottom: 1px solid var(--border-light);
background: var(--bg-secondary);
font-weight: 500;
}
_transfer-body {
max-height: 300px;
overflow-y: auto;
}
_transfer-item {
padding: var(--spacing-sm) var(--spacing);
cursor: pointer;
transition: var(--transition-fast);
}
_transfer-item:hover {
background: var(--bg-secondary);
}
_transfer-item.selected {
background: rgba(0,123,255,0.1);
color: var(--primary-color);
}
_transfer-controls {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
/* 统计图表容器 */
.chart-container {
position: relative;
height: 300px;
margin: var(--spacing) 0;
}
.chart-canvas {
max-height: 100%;
}
/* 焦点样式 */
.focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
/* 可访问性 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
/* 选择禁用 */
.user-select-none {
user-select: none;
}
/* 性能优化 */
.will-change-transform {
will-change: transform;
}
.gpu-accelerated {
transform: translateZ(0);
}

View File

@@ -0,0 +1,90 @@
/* CSS变量定义 */
:root {
/* 主色调 */
--primary-color: #007bff;
--primary-dark: #0056b3;
--primary-light: #66b3ff;
/* 辅助色 */
--secondary-color: #6c757d;
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--info-color: #17a2b8;
/* 背景色 */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-dark: #343a40;
--bg-light: #e9ecef;
--bg-hover: #f8f9fa;
/* 文字色 */
--text-primary: #212529;
--text-secondary: #6c757d;
--text-muted: #868e96;
--text-white: #ffffff;
/* 边框色 */
--border-color: #dee2e6;
--border-light: #e9ecef;
/* 阴影 */
--shadow-sm: 0 2px 4px rgba(0,0,0,0.08);
--shadow-md: 0 4px 12px rgba(0,0,0,0.12);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.16);
/* 圆角 */
--border-radius-sm: 0.25rem;
--border-radius: 0.5rem;
--border-radius-lg: 1rem;
/* 间距 */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 3rem;
/* 过渡 */
--transition: all 0.3s ease;
--transition-fast: all 0.15s ease;
/* 字体 */
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-size-sm: 0.875rem;
--font-size: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
/* 侧边栏 */
--sidebar-width: 250px;
--sidebar-collapsed-width: 70px;
/* 头部导航栏 */
--navbar-height: 60px;
/* Z-index层级 */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
/* 深色模式变量 */
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-dark: #0d1117;
--bg-light: #3d3d3d;
--text-primary: #e6e6e6;
--text-secondary: #b3b3b3;
--text-muted: #808080;
--border-color: #404040;
--border-light: #4d4d4d;
}

View File

@@ -0,0 +1,130 @@
/**
* 重构后的主应用文件
* 使用模块化架构整合所有功能
*/
// 全局变量声明
let alertManager;
let healthMonitor;
let agentMonitor;
// DOM加载完成后初始化应用
document.addEventListener('DOMContentLoaded', async () => {
try {
// 初始化各个管理器
alertManager = new AlertManager();
healthMonitor = new HealthMonitor();
agentMonitor = new AgentMonitor();
// 启动自动刷新
healthMonitor.startMonitoring();
console.log('TSP助手应用初始化完成');
} catch (error) {
console.error('应用初始化失败:', error);
notificationManager.error('应用初始化失败,请刷新页面重试');
}
});
// 健康监控组件
class HealthMonitor {
constructor() {
this.interval = null;
}
startMonitoring() {
// 每5秒检查一次健康状态和监控状态
this.interval = setInterval(async () => {
try {
const [healthData, monitorData] = await Promise.all([
apiService.getHealth(),
apiService.getMonitorStatus()
]);
store.updateHealth(healthData);
store.updateMonitorStatus(monitorData.monitor_status);
} catch (error) {
console.error('健康检查失败:', error);
}
}, 5000);
}
stopMonitoring() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
}
// Agent监控组件
class AgentMonitor {
constructor() {
this.interval = null;
this.init();
}
init() {
// 监听Agent相关按钮
this.bindAgentControls();
this.loadAgentStatus();
}
bindAgentControls() {
const toggleBtn = document.getElementById('toggle-agent');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => this.toggleAgent());
}
}
async loadAgentStatus() {
try {
const status = await apiService.getAgentStatus();
store.updateAgentStatus(status);
this.updateAgentDisplay();
} catch (error) {
console.error('加载Agent状态失败:', error);
}
}
async toggleAgent() {
try {
const currentStatus = store.getState().agentStatus;
const enabled = currentStatus.status === 'inactive';
const result = await apiService.toggleAgent(enabled);
if (result.success) {
notificationManager.success(`Agent已${enabled ? '启用' : '禁用'}`);
await this.loadAgentStatus();
} else {
notificationManager.error(result.message || '操作失败');
}
} catch (error) {
console.error('切换Agent状态失败:', error);
notificationManager.error('操作失败');
}
}
updateAgentDisplay() {
const status = store.getState().agentStatus;
const statusElement = document.getElementById('agent-status');
if (statusElement) {
const statusText = status.status === 'active' ? '运行中' : '未运行';
const statusClass = status.status === 'active' ? 'text-success' : 'text-secondary';
statusElement.innerHTML = `
<i class="fas fa-robot me-1 ${statusClass}"></i>
Agent: ${statusText}
<small class="text-muted">(${status.active_goals} 个活跃目标, ${status.available_tools} 个工具)</small>
`;
}
}
}
// 导出全局对象供HTML访问
window.alertManager = alertManager;
window.apiService = apiService;
window.store = store;
window.notificationManager = notificationManager;

View File

@@ -6,7 +6,7 @@ class ChatClient {
this.sessionId = null;
this.isConnected = false;
this.messageCount = 0;
this.init();
}
@@ -18,24 +18,24 @@ class ChatClient {
bindEvents() {
// 开始对话
document.getElementById('start-chat').addEventListener('click', () => this.startChat());
// 结束对话
document.getElementById('end-chat').addEventListener('click', () => this.endChat());
// 发送消息
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
// 回车发送
document.getElementById('message-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
// 创建工单
document.getElementById('create-work-order').addEventListener('click', () => this.showWorkOrderModal());
document.getElementById('create-work-order-btn').addEventListener('click', () => this.createWorkOrder());
// 快速操作按钮
document.querySelectorAll('.quick-action-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
@@ -50,17 +50,17 @@ class ChatClient {
try {
// 连接WebSocket
await this.connectWebSocket();
// 创建会话
const userId = document.getElementById('user-id').value || 'anonymous';
const workOrderId = document.getElementById('work-order-id').value || null;
const response = await this.sendWebSocketMessage({
type: 'create_session',
user_id: userId,
work_order_id: workOrderId ? parseInt(workOrderId) : null
});
if (response.type === 'session_created') {
this.sessionId = response.session_id;
this.updateSessionInfo();
@@ -69,7 +69,7 @@ class ChatClient {
} else {
this.showError('创建会话失败');
}
} catch (error) {
console.error('启动对话失败:', error);
this.showError('启动对话失败: ' + error.message);
@@ -84,11 +84,11 @@ class ChatClient {
session_id: this.sessionId
});
}
this.sessionId = null;
this.disableChat();
this.addSystemMessage('对话已结束。');
} catch (error) {
console.error('结束对话失败:', error);
}
@@ -97,48 +97,48 @@ class ChatClient {
async sendMessage() {
const input = document.getElementById('message-input');
const message = input.value.trim();
if (!message || !this.sessionId) {
return;
}
// 清空输入框
input.value = '';
// 添加用户消息
this.addMessage('user', message);
// 显示打字指示器
this.showTypingIndicator();
try {
const response = await this.sendWebSocketMessage({
type: 'send_message',
session_id: this.sessionId,
message: message
});
this.hideTypingIndicator();
if (response.type === 'message_response' && response.result.success) {
const result = response.result;
// 添加助手回复
this.addMessage('assistant', result.content, {
knowledge_used: result.knowledge_used,
confidence_score: result.confidence_score,
work_order_id: result.work_order_id
});
// 更新工单ID
if (result.work_order_id) {
document.getElementById('work-order-id').value = result.work_order_id;
}
} else {
this.addMessage('assistant', '抱歉,我暂时无法处理您的问题。请稍后再试。');
}
} catch (error) {
this.hideTypingIndicator();
console.error('发送消息失败:', error);
@@ -151,12 +151,12 @@ class ChatClient {
const description = document.getElementById('wo-description').value;
const category = document.getElementById('wo-category').value;
const priority = document.getElementById('wo-priority').value;
if (!title || !description) {
this.showError('请填写工单标题和描述');
return;
}
try {
const response = await this.sendWebSocketMessage({
type: 'create_work_order',
@@ -166,23 +166,23 @@ class ChatClient {
category: category,
priority: priority
});
if (response.type === 'work_order_created' && response.result.success) {
const workOrderId = response.result.work_order_id;
document.getElementById('work-order-id').value = workOrderId;
this.addSystemMessage(`工单创建成功!工单号: ${response.result.order_id}`);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('workOrderModal'));
modal.hide();
// 清空表单
document.getElementById('work-order-form').reset();
} else {
this.showError('创建工单失败: ' + (response.result.error || '未知错误'));
}
} catch (error) {
console.error('创建工单失败:', error);
this.showError('创建工单失败: ' + error.message);
@@ -193,7 +193,7 @@ class ChatClient {
return new Promise((resolve, reject) => {
try {
this.websocket = new WebSocket('ws://localhost:8765');
// 设置连接超时
const timeout = setTimeout(() => {
if (this.websocket.readyState !== WebSocket.OPEN) {
@@ -201,26 +201,26 @@ class ChatClient {
reject(new Error('WebSocket连接超时请检查服务器是否启动'));
}
}, 5000); // 5秒超时
this.websocket.onopen = () => {
clearTimeout(timeout);
this.isConnected = true;
this.updateConnectionStatus(true);
resolve();
};
this.websocket.onclose = () => {
clearTimeout(timeout);
this.isConnected = false;
this.updateConnectionStatus(false);
};
this.websocket.onerror = (error) => {
clearTimeout(timeout);
console.error('WebSocket错误:', error);
reject(new Error('WebSocket连接失败请检查服务器是否启动'));
};
this.websocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
@@ -229,7 +229,7 @@ class ChatClient {
console.error('解析WebSocket消息失败:', error);
}
};
} catch (error) {
reject(error);
}
@@ -242,15 +242,15 @@ class ChatClient {
reject(new Error('WebSocket未连接'));
return;
}
const messageId = 'msg_' + Date.now();
message.messageId = messageId;
// 设置超时
const timeout = setTimeout(() => {
reject(new Error('请求超时'));
}, 10000);
// 监听响应
const handleResponse = (event) => {
try {
@@ -264,7 +264,7 @@ class ChatClient {
// 忽略解析错误
}
};
this.websocket.addEventListener('message', handleResponse);
this.websocket.send(JSON.stringify(message));
});
@@ -277,29 +277,29 @@ class ChatClient {
addMessage(role, content, metadata = {}) {
const messagesContainer = document.getElementById('chat-messages');
// 如果是第一条消息,清空欢迎信息
if (this.messageCount === 0) {
messagesContainer.innerHTML = '';
}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = role === 'user' ? 'U' : 'A';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = content;
// 添加时间戳
const timeDiv = document.createElement('div');
timeDiv.className = 'message-time';
timeDiv.textContent = new Date().toLocaleTimeString();
contentDiv.appendChild(timeDiv);
// 添加元数据
if (metadata.knowledge_used && metadata.knowledge_used.length > 0) {
const knowledgeDiv = document.createElement('div');
@@ -307,21 +307,21 @@ class ChatClient {
knowledgeDiv.innerHTML = `<i class="fas fa-lightbulb me-1"></i>基于 ${metadata.knowledge_used.length} 条知识库信息生成`;
contentDiv.appendChild(knowledgeDiv);
}
if (metadata.confidence_score) {
const confidenceDiv = document.createElement('div');
confidenceDiv.className = 'confidence-score';
confidenceDiv.textContent = `置信度: ${(metadata.confidence_score * 100).toFixed(1)}%`;
contentDiv.appendChild(confidenceDiv);
}
if (metadata.work_order_id) {
const workOrderDiv = document.createElement('div');
workOrderDiv.className = 'work-order-info';
workOrderDiv.innerHTML = `<i class="fas fa-ticket-alt me-1"></i>关联工单: ${metadata.work_order_id}`;
contentDiv.appendChild(workOrderDiv);
}
if (role === 'user') {
messageDiv.appendChild(contentDiv);
messageDiv.appendChild(avatar);
@@ -329,20 +329,20 @@ class ChatClient {
messageDiv.appendChild(avatar);
messageDiv.appendChild(contentDiv);
}
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
this.messageCount++;
}
addSystemMessage(content) {
const messagesContainer = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = 'text-center text-muted py-2';
messageDiv.innerHTML = `<small><i class="fas fa-info-circle me-1"></i>${content}</small>`;
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
@@ -394,7 +394,7 @@ class ChatClient {
this.showError('请先开始对话');
return;
}
const modal = new bootstrap.Modal(document.getElementById('workOrderModal'));
modal.show();
}

View File

@@ -0,0 +1,355 @@
/**
* 预警管理组件
* 专门处理预警相关的功能
*/
class AlertManager {
constructor() {
this.refreshInterval = null;
this.init();
}
init() {
this.bindEvents();
this.loadInitialData();
this.startAutoRefresh();
}
bindEvents() {
// 监控控制按钮
this.bindButton('start-monitor', () => this.startMonitoring());
this.bindButton('stop-monitor', () => this.stopMonitoring());
this.bindButton('check-alerts', () => this.checkAlerts());
this.bindButton('refresh-alerts', () => this.loadAlerts());
// 预警过滤和排序
this.bindSelect('alert-filter', () => this.updateAlertsDisplay());
this.bindSelect('alert-sort', () => this.updateAlertsDisplay());
}
bindButton(id, handler) {
const element = document.getElementById(id);
if (element) {
element.addEventListener('click', handler);
}
}
bindSelect(id, handler) {
const element = document.getElementById(id);
if (element) {
element.addEventListener('change', handler);
}
}
async loadInitialData() {
store.setLoading(true);
try {
await Promise.all([
this.loadAlerts(),
this.loadRules(),
this.loadMonitorStatus()
]);
} catch (error) {
console.error('加载初始数据失败:', error);
notificationManager.error('加载数据失败,请刷新页面重试');
} finally {
store.setLoading(false);
}
}
startAutoRefresh() {
// 清除现有定时器
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
// 每10秒刷新一次预警
this.refreshInterval = setInterval(() => {
this.loadAlerts();
}, 10000);
}
// 监控控制方法
async startMonitoring() {
try {
store.setLoading(true);
const result = await apiService.startMonitoring();
if (result.success) {
notificationManager.success('监控已启动');
await this.loadMonitorStatus();
} else {
notificationManager.error(result.message || '启动监控失败');
}
} catch (error) {
console.error('启动监控失败:', error);
notificationManager.error('启动监控失败');
} finally {
store.setLoading(false);
}
}
async stopMonitoring() {
try {
store.setLoading(true);
const result = await apiService.stopMonitoring();
if (result.success) {
notificationManager.success('监控已停止');
await this.loadMonitorStatus();
} else {
notificationManager.error(result.message || '停止监控失败');
}
} catch (error) {
console.error('停止监控失败:', error);
notificationManager.error('停止监控失败');
} finally {
store.setLoading(false);
}
}
async checkAlerts() {
try {
store.setLoading(true);
await this.loadAlerts();
notificationManager.success('预警检查完成');
} catch (error) {
console.error('检查预警失败:', error);
notificationManager.error('检查预警失败');
} finally {
store.setLoading(false);
}
}
// 数据加载方法
async loadAlerts() {
try {
const data = await apiService.getAlerts();
store.updateAlerts(data);
this.updateAlertsDisplay();
} catch (error) {
console.error('加载预警失败:', error);
}
}
async loadRules() {
try {
const data = await apiService.getRules();
store.updateRules(data);
this.updateRulesDisplay();
} catch (error) {
console.error('加载规则失败:', error);
}
}
async loadMonitorStatus() {
try {
const data = await apiService.getMonitorStatus();
store.updateMonitorStatus(data.monitor_status);
this.updateMonitorStatusDisplay();
} catch (error) {
console.error('加载监控状态失败:', error);
}
}
// 显示更新方法
updateAlertsDisplay() {
const alerts = store.getSortedAlerts('timestamp', 'desc');
const container = document.getElementById('alerts-container');
if (!container) return;
if (alerts.length === 0) {
container.innerHTML = '<div class="text-center text-muted py-4"><i class="fas fa-info-circle fa-2x mb-2"></i><br>暂无预警</div>';
return;
}
container.innerHTML = alerts.map(alert => this.createAlertElement(alert)).join('');
}
createAlertElement(alert) {
const levelClass = this.getLevelClass(alert.level);
const typeText = this.getTypeText(alert.alert_type);
const levelText = this.getLevelText(alert.level);
const timeText = this.formatTime(alert.timestamp);
return `
<div class="alert-item card mb-2 border-${levelClass}">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-2">
<span class="badge bg-${levelClass} me-2">${levelText}</span>
<span class="badge bg-secondary">${typeText}</span>
<small class="text-muted ms-2">${timeText}</small>
</div>
<h6 class="card-title mb-1">${alert.title}</h6>
<p class="card-text small text-muted mb-2">${alert.description}</p>
<div class="alert-actions">
<button class="btn btn-sm btn-outline-primary me-1" onclick="alertManager.acknowledgeAlert('${alert.id}')">
<i class="fas fa-check"></i> 确认
</button>
<button class="btn btn-sm btn-outline-info" onclick="alertManager.viewAlertDetail('${alert.id}')">
<i class="fas fa-eye"></i> 详情
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
updateRulesDisplay() {
const rules = store.getState().rules;
const container = document.getElementById('rules-container');
if (!container) return;
if (rules.length === 0) {
container.innerHTML = '<div class="text-center text-muted py-4"><i class="fas fa-list fa-2x mb-2"></i><br>暂无规则</div>';
return;
}
container.innerHTML = rules.map(rule => this.createRuleElement(rule)).join('');
}
createRuleElement(rule) {
const enabledText = rule.enabled ? '<span class="badge bg-success">启用</span>' : '<span class="badge bg-secondary">禁用</span>';
return `
<tr>
<td>${rule.name}</td>
<td>${rule.alert_type}</td>
<td>${rule.level}</td>
<td>${rule.threshold}</td>
<td>${enabledText}</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1" onclick="alertManager.editRule('${rule.name}')">
<i class="fas fa-edit"></i> 编辑
</button>
<button class="btn btn-sm btn-outline-danger" onclick="alertManager.deleteRule('${rule.name}')">
<i class="fas fa-trash"></i> 删除
</button>
</td>
</tr>
`;
}
updateMonitorStatusDisplay() {
const status = store.getState().monitorStatus;
const element = document.getElementById('monitor-status');
if (!element) return;
const statusConfig = {
'running': { icon: 'text-success', text: '运行中' },
'stopped': { icon: 'text-danger', text: '已停止' },
'unknown': { icon: 'text-warning', text: '未知' }
};
const config = statusConfig[status] || statusConfig.unknown;
element.innerHTML = `
<i class="fas fa-circle ${config.icon}"></i> 监控状态: ${config.text}
`;
}
// 工具方法
getLevelClass(level) {
const levelMap = {
'critical': 'danger',
'error': 'danger',
'warning': 'warning',
'info': 'info'
};
return levelMap[level] || 'secondary';
}
getLevelText(level) {
const levelMap = {
'critical': '严重',
'error': '错误',
'warning': '警告',
'info': '信息'
};
return levelMap[level] || level;
}
getTypeText(type) {
const typeMap = {
'performance': '性能',
'quality': '质量',
'volume': '量级',
'system': '系统',
'business': '业务'
};
return typeMap[type] || type;
}
formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`;
return date.toLocaleDateString();
}
// 预警操作方法
async acknowledgeAlert(alertId) {
try {
await apiService.updateAlert(alertId, { acknowledged: true });
notificationManager.success('预警已确认');
await this.loadAlerts();
} catch (error) {
console.error('确认预警失败:', error);
notificationManager.error('确认预警失败');
}
}
viewAlertDetail(alertId) {
// 这里可以实现查看详情的逻辑
notificationManager.info('详情查看功能开发中');
}
// 规则操作方法
editRule(ruleName) {
const rule = store.getState().rules.find(r => r.name === ruleName);
if (!rule) {
notificationManager.error('规则不存在');
return;
}
// 填充编辑表单
document.getElementById('edit-rule-name-original').value = rule.name;
document.getElementById('edit-rule-name').value = rule.name;
document.getElementById('edit-rule-type').value = rule.alert_type;
document.getElementById('edit-rule-level').value = rule.level;
document.getElementById('edit-rule-threshold').value = rule.threshold;
document.getElementById('edit-rule-description').value = rule.description || '';
document.getElementById('edit-rule-condition').value = rule.condition;
document.getElementById('edit-rule-interval').value = rule.check_interval;
document.getElementById('edit-rule-cooldown').value = rule.cooldown;
document.getElementById('edit-rule-enabled').checked = rule.enabled;
// 显示编辑模态框
const modal = new bootstrap.Modal(document.getElementById('editRuleModal'));
modal.show();
}
async deleteRule(ruleName) {
if (!confirm(`确定要删除规则 "${ruleName}" 吗?`)) return;
try {
await apiService.deleteRule(ruleName);
notificationManager.success('规则删除成功');
await this.loadRules();
} catch (error) {
console.error('删除规则失败:', error);
notificationManager.error('删除规则失败');
}
}
}

View File

@@ -0,0 +1,137 @@
/**
* 通知管理组件
* 统一处理应用内通知显示
*/
class NotificationManager {
constructor() {
this.container = null;
this.init();
}
init() {
// 创建通知容器
this.container = document.createElement('div');
this.container.className = 'notification-container position-fixed';
this.container.style.cssText = `
top: 20px;
right: 20px;
z-index: 9999;
max-width: 400px;
`;
document.body.appendChild(this.container);
// 监听状态变化
store.subscribe((prevState, newState) => {
if (prevState.notifications !== newState.notifications) {
this.renderNotifications(newState.notifications);
}
});
}
renderNotifications(notifications) {
this.container.innerHTML = '';
notifications.forEach(notification => {
const notificationEl = this.createNotificationElement(notification);
this.container.appendChild(notificationEl);
});
}
createNotificationElement(notification) {
const div = document.createElement('div');
const typeClass = this.getTypeClass(notification.type);
div.className = `alert alert-${typeClass} alert-dismissible fade show shadow`;
div.style.cssText = `
margin-bottom: 10px;
border-radius: 8px;
border: none;
`;
div.innerHTML = `
<div class="d-flex align-items-center">
<i class="${this.getIconClass(notification.type)} me-2"></i>
<div class="flex-grow-1">
<strong>${notification.title || this.getDefaultTitle(notification.type)}</strong>
<div class="small mt-1">${notification.message}</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
// 添加关闭事件
const closeBtn = div.querySelector('.btn-close');
closeBtn.addEventListener('click', () => {
store.removeNotification(notification.id);
});
return div;
}
getTypeClass(type) {
const typeMap = {
'success': 'success',
'error': 'danger',
'warning': 'warning',
'info': 'info'
};
return typeMap[type] || 'info';
}
getIconClass(type) {
const iconMap = {
'success': 'fas fa-check-circle',
'error': 'fas fa-exclamation-triangle',
'warning': 'fas fa-exclamation-circle',
'info': 'fas fa-info-circle'
};
return iconMap[type] || 'fas fa-info-circle';
}
getDefaultTitle(type) {
const titleMap = {
'success': '成功',
'error': '错误',
'warning': '警告',
'info': '提示'
};
return titleMap[type] || '通知';
}
// 便捷方法
success(message, title = null) {
store.addNotification({
type: 'success',
message,
title
});
}
error(message, title = null) {
store.addNotification({
type: 'error',
message,
title
});
}
warning(message, title = null) {
store.addNotification({
type: 'warning',
message,
title
});
}
info(message, title = null) {
store.addNotification({
type: 'info',
message,
title
});
}
}
// 创建全局通知管理器实例
const notificationManager = new NotificationManager();

View File

@@ -0,0 +1,418 @@
/**
* 模态框组件
*/
import { addClass, removeClass, hasClass } from '../core/utils.js';
export class Modal {
constructor(options = {}) {
this.id = options.id || `modal-${Date.now()}`;
this.title = options.title || '';
this.content = options.content || '';
this.size = options.size || ''; // sm, lg, xl
this.backdrop = options.backdrop !== false;
this.keyboard = options.keyboard !== false;
this.centered = options.centered || false;
this.scrollable = options.scrollable || false;
this.static = options.static || false;
this.className = options.className || '';
this.footer = options.footer || null;
this.show = false;
this.onShow = options.onShow || (() => {});
this.onShown = options.onShown || (() => {});
this.onHide = options.onHide || (() => {});
this.onHidden = options.onHidden || (() => {});
this.init();
}
init() {
this.createModal();
this.bindEvents();
}
createModal() {
// 创建模态框容器
this.modal = document.createElement('div');
this.modal.className = 'modal fade';
this.modal.id = this.id;
this.modal.setAttribute('tabindex', '-1');
this.modal.setAttribute('aria-labelledby', `${this.id}-label`);
this.modal.setAttribute('aria-hidden', 'true');
// 模态框对话框
const dialog = document.createElement('div');
dialog.className = `modal-dialog ${this.size ? `modal-${this.size}` : ''} ${this.centered ? 'modal-dialog-centered' : ''} ${this.scrollable ? 'modal-dialog-scrollable' : ''}`;
// 模态框内容
const content = document.createElement('div');
content.className = 'modal-content';
if (this.className) {
addClass(content, this.className);
}
// 构建模态框HTML
let modalHTML = `
<div class="modal-header">
<h5 class="modal-title" id="${this.id}-label">${this.title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
${this.content}
</div>
`;
// 添加底部按钮
if (this.footer) {
modalHTML += `
<div class="modal-footer">
${typeof this.footer === 'string' ? this.footer : this.renderFooter()}
</div>
`;
}
content.innerHTML = modalHTML;
dialog.appendChild(content);
this.modal.appendChild(dialog);
// 添加到页面
document.body.appendChild(this.modal);
}
renderFooter() {
if (!this.footer) return '';
if (Array.isArray(this.footer)) {
return this.footer.map(btn => {
const attrs = Object.keys(btn)
.filter(key => key !== 'text')
.map(key => `${key}="${btn[key]}"`)
.join(' ');
return `<button ${attrs}>${btn.text}</button>`;
}).join('');
}
return '';
}
bindEvents() {
// 关闭按钮
const closeBtn = this.modal.querySelector('.btn-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.hide());
}
// 背景点击
if (!this.static) {
this.modal.addEventListener('click', (e) => {
if (e.target === this.modal) {
this.hide();
}
});
}
// ESC键关闭
if (this.keyboard) {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.show) {
this.hide();
}
});
}
}
show() {
if (this.show) return;
// 触发显示前事件
this.onShow();
// 添加到页面
if (!this.modal.parentNode) {
document.body.appendChild(this.modal);
}
// 显示模态框
this.modal.style.display = 'block';
addClass(this.modal, 'show');
this.modal.setAttribute('aria-hidden', 'false');
// 防止背景滚动
document.body.style.overflow = 'hidden';
this.show = true;
// 聚焦到第一个可聚焦元素
setTimeout(() => {
const focusable = this.modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusable) {
focusable.focus();
}
}, 100);
// 触发显示后事件
setTimeout(() => {
this.onShown();
}, 150);
}
hide() {
if (!this.show) return;
// 触发隐藏前事件
this.onHide();
// 隐藏模态框
removeClass(this.modal, 'show');
this.modal.setAttribute('aria-hidden', 'true');
// 恢复背景滚动
document.body.style.overflow = '';
this.show = false;
// 延迟移除DOM
setTimeout(() => {
if (this.modal) {
this.modal.style.display = 'none';
if (this.modal.parentNode) {
this.modal.parentNode.removeChild(this.modal);
}
}
// 触发隐藏后事件
this.onHidden();
}, 150);
}
toggle() {
if (this.show) {
this.hide();
} else {
this.show();
}
}
update(options) {
if (options.title) {
this.title = options.title;
const titleEl = this.modal.querySelector('.modal-title');
if (titleEl) {
titleEl.textContent = this.title;
}
}
if (options.content) {
this.content = options.content;
const bodyEl = this.modal.querySelector('.modal-body');
if (bodyEl) {
bodyEl.innerHTML = this.content;
}
}
if (options.footer !== undefined) {
this.footer = options.footer;
const footerEl = this.modal.querySelector('.modal-footer');
if (footerEl) {
if (this.footer) {
footerEl.style.display = 'block';
footerEl.innerHTML = typeof this.footer === 'string' ? this.footer : this.renderFooter();
// 重新绑定底部按钮事件
this.bindFooterEvents();
} else {
footerEl.style.display = 'none';
}
}
}
}
bindFooterEvents() {
const footer = this.modal.querySelector('.modal-footer');
if (!footer) return;
footer.querySelectorAll('button').forEach(btn => {
const dataDismiss = btn.getAttribute('data-bs-dismiss');
if (dataDismiss === 'modal') {
btn.addEventListener('click', () => this.hide());
}
});
}
getModal() {
return this.modal;
}
getElement(selector) {
return this.modal.querySelector(selector);
}
destroy() {
this.hide();
setTimeout(() => {
if (this.modal && this.modal.parentNode) {
this.modal.parentNode.removeChild(this.modal);
}
this.modal = null;
}, 200);
}
}
// 确认对话框
export function confirm(options = {}) {
return new Promise((resolve) => {
const modal = new Modal({
title: options.title || '确认',
content: `
<div class="text-center">
<i class="fas fa-question-circle fa-3x text-warning mb-3"></i>
<p>${options.message || '确定要执行此操作吗?'}</p>
</div>
`,
size: 'sm',
centered: true,
footer: [
{ text: '取消', class: 'btn btn-secondary', 'data-bs-dismiss': 'modal' },
{ text: '确定', class: 'btn btn-primary', id: 'confirm-btn' }
]
});
modal.onHidden = () => {
modal.destroy();
};
modal.getElement('#confirm-btn').addEventListener('click', () => {
modal.hide();
resolve(true);
});
modal.onHidden = () => {
resolve(false);
modal.destroy();
};
modal.show();
});
}
// 警告对话框
export function alert(options = {}) {
return new Promise((resolve) => {
const modal = new Modal({
title: options.title || '提示',
content: `
<div class="text-center">
<i class="fas fa-${options.type === 'error' ? 'exclamation-circle text-danger' : 'info-circle text-info'} fa-3x mb-3"></i>
<p>${options.message || ''}</p>
</div>
`,
size: 'sm',
centered: true,
footer: [
{ text: '确定', class: 'btn btn-primary', id: 'alert-btn' }
]
});
modal.onHidden = () => {
modal.destroy();
resolve();
};
modal.getElement('#alert-btn').addEventListener('click', () => {
modal.hide();
});
modal.show();
});
}
// Toast通知
export class Toast {
constructor(options = {}) {
this.id = `toast-${Date.now()}`;
this.type = options.type || 'info';
this.message = options.message || '';
this.duration = options.duration || 3000;
this.closable = options.closable !== false;
this.autoHide = options.autoHide !== false;
this.init();
}
init() {
this.createToast();
this.show();
}
createToast() {
// 查找或创建toast容器
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container';
document.body.appendChild(container);
}
// 创建toast元素
this.toast = document.createElement('div');
this.toast.className = `toast ${this.type}`;
this.toast.id = this.id;
const iconMap = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
this.toast.innerHTML = `
<div class="toast-content">
<i class="fas ${iconMap[this.type] || iconMap.info} me-2"></i>
<span>${this.message}</span>
${this.closable ? '<button type="button" class="btn-close ms-2" aria-label="Close"></button>' : ''}
</div>
`;
container.appendChild(this.toast);
// 绑定关闭事件
if (this.closable) {
const closeBtn = this.toast.querySelector('.btn-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.hide());
}
}
// 自动隐藏
if (this.autoHide) {
setTimeout(() => this.hide(), this.duration);
}
}
show() {
setTimeout(() => {
addClass(this.toast, 'show');
}, 10);
}
hide() {
removeClass(this.toast, 'show');
setTimeout(() => {
if (this.toast && this.toast.parentNode) {
this.toast.parentNode.removeChild(this.toast);
}
}, 300);
}
}
// 创建全局toast函数
export function showToast(options) {
if (typeof options === 'string') {
options = { message: options };
}
return new Toast(options);
}
// 导出
export default Modal;

View File

@@ -0,0 +1,414 @@
/**
* 导航栏组件
*/
import { addClass, removeClass, hasClass, toggleClass } from '../core/utils.js';
import store from '../core/store.js';
import router from '../core/router.js';
export class Navbar {
constructor(container) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.userMenuOpen = false;
this.init();
}
init() {
this.render();
this.bindEvents();
}
render() {
this.container.innerHTML = `
<nav class="navbar">
<!-- 移动端菜单按钮 -->
<button class="navbar-toggler" id="sidebar-toggle" type="button">
<i class="fas fa-bars"></i>
</button>
<div class="navbar-brand">
<i class="fas fa-shield-alt"></i>
<span>TSP智能助手</span>
</div>
<ul class="navbar-nav">
<!-- 监控状态 -->
<li class="nav-item">
<span class="nav-link" id="monitor-status">
<i class="fas fa-circle" id="status-indicator"></i>
<span id="status-text">检查中...</span>
</span>
</li>
<!-- 通知 -->
<li class="nav-item dropdown" id="notifications-dropdown">
<a href="#" class="nav-link" data-toggle="dropdown">
<i class="fas fa-bell"></i>
<span class="badge bg-danger" id="notification-count">0</span>
</a>
<div class="dropdown-menu dropdown-menu-end">
<div class="dropdown-header">
<h6>通知</h6>
<a href="#" class="btn btn-sm btn-link" id="clear-notifications">清空</a>
</div>
<div class="dropdown-divider"></div>
<div id="notification-list" class="notification-list">
<div class="dropdown-item text-muted">暂无通知</div>
</div>
</div>
</li>
<!-- 用户菜单 -->
<li class="nav-item dropdown user-menu">
<a href="#" class="nav-link" id="user-menu-toggle">
<div class="user-avatar" id="user-avatar">
${this.getUserInitial()}
</div>
</a>
<div class="user-dropdown" id="user-dropdown">
<div class="dropdown-header">
<div class="d-flex align-items-center">
<div class="user-avatar me-2">
${this.getUserInitial()}
</div>
<div>
<div class="fw-bold" id="user-name">${this.getUserName()}</div>
<div class="small text-muted" id="user-role">${this.getUserRole()}</div>
</div>
</div>
</div>
<div class="dropdown-divider"></div>
<a href="#" class="user-dropdown-item" data-route="/profile">
<i class="fas fa-user me-2"></i>个人资料
</a>
<a href="#" class="user-dropdown-item" data-route="/settings">
<i class="fas fa-cog me-2"></i>系统设置
</a>
<div class="user-dropdown-divider"></div>
<a href="#" class="user-dropdown-item text-danger" id="logout-btn">
<i class="fas fa-sign-out-alt me-2"></i>退出登录
</a>
</div>
</li>
<!-- 主题切换 -->
<li class="nav-item">
<button class="nav-link btn btn-link" id="theme-toggle">
<i class="fas fa-moon" id="theme-icon"></i>
</button>
</li>
</ul>
</nav>
`;
}
bindEvents() {
// 侧边栏切换(移动端)
const sidebarToggle = this.container.querySelector('#sidebar-toggle');
if (sidebarToggle) {
sidebarToggle.addEventListener('click', () => {
this.toggleSidebar();
});
}
// 用户菜单切换
const userMenuToggle = this.container.querySelector('#user-menu-toggle');
if (userMenuToggle) {
userMenuToggle.addEventListener('click', (e) => {
e.preventDefault();
this.toggleUserMenu();
});
}
// 通知下拉菜单
const notificationsDropdown = this.container.querySelector('#notifications-dropdown');
if (notificationsDropdown) {
const toggle = notificationsDropdown.querySelector('[data-toggle="dropdown"]');
if (toggle) {
toggle.addEventListener('click', (e) => {
e.preventDefault();
this.toggleNotifications();
});
}
}
// 主题切换
const themeToggle = this.container.querySelector('#theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => {
this.toggleTheme();
});
}
// 退出登录
const logoutBtn = this.container.querySelector('#logout-btn');
if (logoutBtn) {
logoutBtn.addEventListener('click', (e) => {
e.preventDefault();
this.handleLogout();
});
}
// 清空通知
const clearNotifications = this.container.querySelector('#clear-notifications');
if (clearNotifications) {
clearNotifications.addEventListener('click', (e) => {
e.preventDefault();
this.clearNotifications();
});
}
// 路由链接
this.container.querySelectorAll('[data-route]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const route = e.currentTarget.getAttribute('data-route');
if (route) {
router.push(route);
}
});
});
// 点击外部关闭下拉菜单
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target)) {
this.closeUserMenu();
this.closeNotifications();
}
});
// 监听store变化
store.subscribe((state) => {
this.updateUser(state.user);
this.updateNotifications(state.ui.notifications);
this.updateMonitorStatus(state.monitor);
});
}
toggleUserMenu() {
const dropdown = this.container.querySelector('#user-dropdown');
if (dropdown) {
this.userMenuOpen = !this.userMenuOpen;
toggleClass(dropdown, 'show');
}
}
closeUserMenu() {
const dropdown = this.container.querySelector('#user-dropdown');
if (dropdown && hasClass(dropdown, 'show')) {
this.userMenuOpen = false;
removeClass(dropdown, 'show');
}
}
toggleNotifications() {
const dropdown = this.container.querySelector('#notifications-dropdown .dropdown-menu');
if (dropdown) {
toggleClass(dropdown, 'show');
}
}
closeNotifications() {
const dropdown = this.container.querySelector('#notifications-dropdown .dropdown-menu');
if (dropdown && hasClass(dropdown, 'show')) {
removeClass(dropdown, 'show');
}
}
toggleTheme() {
const currentTheme = store.getState('app.theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
store.commit('SET_THEME', newTheme);
const icon = this.container.querySelector('#theme-icon');
if (icon) {
icon.className = newTheme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
}
}
handleLogout() {
if (confirm('确定要退出登录吗?')) {
// 调用注销API
fetch('/api/logout', { method: 'POST' })
.then(() => {
// 清除应用状态
store.commit('SET_USER', null);
store.commit('SET_LOGIN', false);
store.commit('SET_TOKEN', null);
// 清除本地存储和会话存储
localStorage.removeItem('user');
localStorage.removeItem('token');
localStorage.removeItem('remember');
sessionStorage.removeItem('token');
// 显示提示
if (window.showToast) {
window.showToast('已退出登录', 'info');
}
// 跳转到登录页
router.push('/login');
})
.catch(error => {
console.error('注销失败:', error);
// 即使API调用失败也要清除本地状态
store.commit('SET_USER', null);
store.commit('SET_LOGIN', false);
store.commit('SET_TOKEN', null);
localStorage.clear();
sessionStorage.clear();
router.push('/login');
});
}
}
clearNotifications() {
store.setState({
ui: {
...store.getState('ui'),
notifications: []
}
});
}
updateNotifications(notifications) {
const countEl = this.container.querySelector('#notification-count');
const listEl = this.container.querySelector('#notification-list');
if (!countEl || !listEl) return;
const count = notifications.length;
countEl.textContent = count;
countEl.style.display = count > 0 ? 'inline-block' : 'none';
if (count === 0) {
listEl.innerHTML = '<div class="dropdown-item text-muted">暂无通知</div>';
} else {
listEl.innerHTML = notifications.slice(0, 5).map(notification => `
<a href="#" class="dropdown-item ${notification.read ? '' : 'unread'}">
<div class="d-flex">
<div class="flex-shrink-0">
<i class="fas ${this.getNotificationIcon(notification.type)} text-${notification.type}"></i>
</div>
<div class="flex-grow-1 ms-2">
<div class="small">${notification.message}</div>
<div class="text-muted small">${this.formatTime(notification.time)}</div>
</div>
</div>
</a>
`).join('');
}
}
getNotificationIcon(type) {
const icons = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
return icons[type] || 'fa-bell';
}
updateMonitorStatus(monitor) {
const indicator = this.container.querySelector('#status-indicator');
const text = this.container.querySelector('#status-text');
if (!indicator || !text) return;
if (monitor.status === 'running') {
indicator.className = 'fas fa-circle text-success';
text.textContent = '监控运行中';
} else {
indicator.className = 'fas fa-circle text-warning';
text.textContent = '监控已停止';
}
}
updateUser(user) {
const avatar = this.container.querySelector('#user-avatar');
const name = this.container.querySelector('#user-name');
const role = this.container.querySelector('#user-role');
if (user && user.info) {
const initial = this.getInitial(user.info.name);
if (avatar) avatar.textContent = initial;
if (name) name.textContent = user.info.name;
if (role) role.textContent = user.info.role || '用户';
} else {
if (avatar) avatar.textContent = 'U';
if (name) name.textContent = '未登录';
if (role) role.textContent = '访客';
}
}
getUserInitial() {
const user = store.getState('user.info');
return user ? this.getInitial(user.name) : 'U';
}
getUserName() {
const user = store.getState('user.info');
return user ? user.name : '未登录';
}
getUserRole() {
const user = store.getState('user.info');
return user ? (user.role || '用户') : '访客';
}
getInitial(name) {
if (!name) return 'U';
const chars = name.trim().split(/\s+/);
if (chars.length >= 2) {
return chars[0][0] + chars[chars.length - 1][0];
}
return name[0].toUpperCase();
}
toggleSidebar() {
const sidebar = document.querySelector('.sidebar');
const overlay = document.querySelector('.sidebar-overlay') || this.createOverlay();
const isMobile = window.innerWidth <= 768;
if (isMobile) {
// 移动端:切换显示
toggleClass(sidebar, 'open');
toggleClass(overlay, 'show');
} else {
// 桌面端:切换折叠
toggleClass(sidebar, 'collapsed');
}
}
createOverlay() {
const overlay = document.createElement('div');
overlay.className = 'sidebar-overlay';
overlay.addEventListener('click', () => {
this.toggleSidebar();
});
document.body.appendChild(overlay);
return overlay;
}
formatTime(time) {
const date = new Date(time);
const now = new Date();
const diff = now - date;
if (diff < 60000) {
return '刚刚';
} else if (diff < 3600000) {
return `${Math.floor(diff / 60000)}分钟前`;
} else if (diff < 86400000) {
return `${Math.floor(diff / 3600000)}小时前`;
} else {
return date.toLocaleDateString();
}
}
}
// 导出组件
export default Navbar;

View File

@@ -0,0 +1,235 @@
/**
* 侧边栏组件
*/
import { addClass, removeClass, hasClass, toggleClass } from '../core/utils.js';
import router from '../core/router.js';
export class Sidebar {
constructor(container) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.collapsed = false;
this.init();
}
init() {
this.render();
this.bindEvents();
}
render() {
this.container.innerHTML = `
<div class="sidebar" id="sidebar">
<!-- Logo -->
<div class="sidebar-header">
<a href="/" class="sidebar-logo">
<i class="fas fa-shield-alt"></i>
<span>TSP助手</span>
</a>
<button class="btn btn-link sidebar-toggle" id="sidebar-toggle">
<i class="fas fa-bars"></i>
</button>
</div>
<!-- Navigation -->
<nav class="sidebar-nav">
${this.renderMenuItems()}
</nav>
</div>
`;
// 初始化折叠状态
const isCollapsed = localStorage.getItem('sidebar-collapsed') === 'true';
if (isCollapsed) {
this.collapsed = true;
addClass(this.container.querySelector('#sidebar'), 'collapsed');
}
}
renderMenuItems() {
const menuItems = [
{
path: '/',
icon: 'fas fa-tachometer-alt',
title: '仪表板',
badge: null
},
{
path: '/workorders',
icon: 'fas fa-tasks',
title: '工单管理',
badge: 'workorders'
},
{
path: '/alerts',
icon: 'fas fa-bell',
title: '预警管理',
badge: 'alerts'
},
{
path: '/knowledge',
icon: 'fas fa-book',
title: '知识库',
badge: null
},
{
path: '/chat',
icon: 'fas fa-comments',
title: '智能对话',
badge: null
},
{
path: '/chat-http',
icon: 'fas fa-comment-dots',
title: 'HTTP对话',
badge: null
},
{
path: '/monitoring',
icon: 'fas fa-chart-line',
title: '系统监控',
badge: null
},
{
path: '/feishu',
icon: 'fab fa-lark',
title: '飞书同步',
badge: null
},
{
path: '/agent',
icon: 'fas fa-robot',
title: '智能Agent',
badge: null
},
{
path: '/vehicle',
icon: 'fas fa-car',
title: '车辆数据',
badge: null
},
{
path: '/settings',
icon: 'fas fa-cog',
title: '系统设置',
badge: null
}
];
return menuItems.map(item => `
<a href="${item.path}" class="sidebar-nav-item" data-route="${item.path}">
<i class="${item.icon}"></i>
<span>${item.title}</span>
${item.badge ? `<span class="badge bg-danger ms-auto" id="sidebar-badge-${item.badge}">0</span>` : ''}
</a>
`).join('');
}
bindEvents() {
// 折叠切换
const toggleBtn = this.container.querySelector('#sidebar-toggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', (e) => {
e.preventDefault();
this.toggle();
});
}
// 菜单项点击
this.container.querySelectorAll('.sidebar-nav-item').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const route = e.currentTarget.getAttribute('data-route');
if (route) {
router.push(route);
}
// 移动端点击后自动收起侧边栏
if (window.innerWidth < 992 && !this.collapsed) {
this.toggle();
}
});
});
// 监听路由变化
router.afterEach((to) => {
this.updateActiveMenu(to.path);
});
// 监听窗口大小变化
window.addEventListener('resize', () => {
this.handleResize();
});
// 初始化激活状态
this.updateActiveMenu(window.location.pathname);
}
toggle() {
const sidebar = this.container.querySelector('#sidebar');
if (sidebar) {
this.collapsed = !this.collapsed;
toggleClass(sidebar, 'collapsed');
localStorage.setItem('sidebar-collapsed', this.collapsed);
}
}
expand() {
const sidebar = this.container.querySelector('#sidebar');
if (sidebar && hasClass(sidebar, 'collapsed')) {
this.collapsed = false;
removeClass(sidebar, 'collapsed');
localStorage.setItem('sidebar-collapsed', 'false');
}
}
collapse() {
const sidebar = this.container.querySelector('#sidebar');
if (sidebar && !hasClass(sidebar, 'collapsed')) {
this.collapsed = true;
addClass(sidebar, 'collapsed');
localStorage.setItem('sidebar-collapsed', 'true');
}
}
updateActiveMenu(path) {
this.container.querySelectorAll('.sidebar-nav-item').forEach(item => {
const route = item.getAttribute('data-route');
if (route === path) {
addClass(item, 'active');
} else {
removeClass(item, 'active');
}
});
}
updateBadge(type, count) {
const badge = this.container.querySelector(`#sidebar-badge-${type}`);
if (badge) {
badge.textContent = count;
badge.style.display = count > 0 ? 'inline-block' : 'none';
}
}
handleResize() {
if (window.innerWidth >= 992) {
// 桌面端,恢复之前的折叠状态
const isCollapsed = localStorage.getItem('sidebar-collapsed') === 'true';
if (isCollapsed !== this.collapsed) {
if (isCollapsed) {
this.collapse();
} else {
this.expand();
}
}
} else {
// 移动端,默认收起
if (!this.collapsed) {
this.collapse();
}
}
}
}
// 导出组件
export default Sidebar;

View File

@@ -0,0 +1,472 @@
/**
* API统一管理模块
*/
import { defaultConfig, debounce, storage, handleError } from './utils.js';
// API配置
const config = {
...defaultConfig,
timeout: 10000, // 请求超时时间
retryTimes: 3, // 重试次数
retryDelay: 1000 // 重试延迟
};
// 请求拦截器
const requestInterceptors = [];
const responseInterceptors = [];
// 添加请求拦截器
export function addRequestInterceptor(interceptor) {
requestInterceptors.push(interceptor);
}
// 添加响应拦截器
export function addResponseInterceptor(interceptor) {
responseInterceptors.push(interceptor);
}
// 默认请求拦截器
addRequestInterceptor(async (options) => {
// 添加认证头
const token = storage.get('authToken');
if (token) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${token}`
};
}
// 添加用户信息头
const userInfo = storage.get('userInfo');
if (userInfo) {
options.headers = {
...options.headers,
'X-User-Name': userInfo.name || '',
'X-User-Role': userInfo.role || ''
};
}
// 添加请求ID
options.headers = {
...options.headers,
'X-Request-ID': generateRequestId()
};
return options;
});
// 默认响应拦截器
addResponseInterceptor(async (response) => {
// 处理通用错误
if (response.status === 401) {
// 未授权,清除本地存储并跳转到登录页
storage.remove('authToken');
storage.remove('userInfo');
window.location.href = '/login';
throw new Error('未授权,请重新登录');
}
if (response.status >= 500) {
throw new Error('服务器错误,请稍后重试');
}
return response;
});
// 生成请求ID
function generateRequestId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// 基础请求函数
async function request(url, options = {}) {
// 合并配置
const finalOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
// 执行请求拦截器
for (const interceptor of requestInterceptors) {
Object.assign(finalOptions, await interceptor(finalOptions));
}
// 构建完整URL
const fullUrl = url.startsWith('http') ? url : `${config.apiBaseUrl}${url}`;
// 创建AbortController用于超时控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
finalOptions.signal = controller.signal;
try {
let lastError;
// 重试机制
for (let i = 0; i <= config.retryTimes; i++) {
try {
const response = await fetch(fullUrl, finalOptions);
// 执行响应拦截器
let processedResponse = response;
for (const interceptor of responseInterceptors) {
processedResponse = await interceptor(processedResponse);
}
// 解析响应
const data = await parseResponse(processedResponse);
// 如果响应表示失败,抛出错误
if (data.code && data.code !== 200) {
throw new Error(data.message || '请求失败');
}
return data;
} catch (error) {
lastError = error;
// 如果是网络错误或超时,且还有重试次数,则延迟后重试
if (i < config.retryTimes && (error.name === 'TypeError' || error.name === 'AbortError')) {
await new Promise(resolve => setTimeout(resolve, config.retryDelay * Math.pow(2, i)));
continue;
}
// 其他错误直接抛出
throw error;
}
}
throw lastError;
} catch (error) {
handleError(error, `API Request: ${finalOptions.method} ${url}`);
throw error;
} finally {
clearTimeout(timeoutId);
}
}
// 解析响应
async function parseResponse(response) {
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else if (contentType && contentType.includes('text/')) {
return {
code: response.ok ? 200 : response.status,
data: await response.text(),
message: response.statusText
};
} else {
return {
code: response.ok ? 200 : response.status,
data: await response.blob(),
message: response.statusText
};
}
}
// HTTP方法封装
export const http = {
get(url, params = {}, options = {}) {
const queryString = new URLSearchParams(params).toString();
const fullUrl = queryString ? `${url}?${queryString}` : url;
return request(fullUrl, { ...options, method: 'GET' });
},
post(url, data = {}, options = {}) {
return request(url, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
},
put(url, data = {}, options = {}) {
return request(url, {
...options,
method: 'PUT',
body: JSON.stringify(data)
});
},
patch(url, data = {}, options = {}) {
return request(url, {
...options,
method: 'PATCH',
body: JSON.stringify(data)
});
},
delete(url, options = {}) {
return request(url, { ...options, method: 'DELETE' });
},
upload(url, formData, options = {}) {
return request(url, {
...options,
method: 'POST',
headers: {
// 不要设置Content-Type让浏览器自动设置multipart/form-data
},
body: formData
});
},
download(url, params = {}, options = {}) {
const queryString = new URLSearchParams(params).toString();
const fullUrl = queryString ? `${url}?${queryString}` : url;
return request(fullUrl, {
...options,
method: 'GET'
}).then(response => {
// 创建下载链接
const blob = response.data;
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
// 从响应头获取文件名
const contentDisposition = response.headers?.get('content-disposition');
if (contentDisposition) {
const filename = contentDisposition.match(/filename="?([^"]+)"?/);
link.download = filename ? filename[1] : 'download';
}
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(downloadUrl);
return response;
});
}
};
// API接口定义
export const api = {
// 系统相关
system: {
health: () => http.get('/health'),
info: () => http.get('/system/info'),
settings: () => http.get('/settings'),
saveSettings: (data) => http.post('/settings', data)
},
// 工单管理
workorders: {
list: (params) => http.get('/workorders', params),
create: (data) => http.post('/workorders', data),
get: (id) => http.get(`/workorders/${id}`),
update: (id, data) => http.put(`/workorders/${id}`, data),
delete: (id) => http.delete(`/workorders/${id}`),
dispatch: (id, module) => http.post(`/workorders/${id}/dispatch`, { target_module: module }),
suggestModule: (id) => http.post(`/workorders/${id}/suggest-module`),
aiSuggestion: (id) => http.post(`/workorders/${id}/ai-suggestion`),
humanResolution: (id, data) => http.post(`/workorders/${id}/human-resolution`, data),
approveToKnowledge: (id, data) => http.post(`/workorders/${id}/approve-to-knowledge`, data),
processHistory: (id) => http.get(`/workorders/${id}/process-history`),
addProcessHistory: (id, data) => http.post(`/workorders/${id}/process-history`, data),
import: (file) => {
const formData = new FormData();
formData.append('file', file);
return http.upload('/workorders/import', formData);
},
export: (params) => http.download('/workorders/export', params),
getTemplate: () => http.get('/workorders/import/template'),
downloadTemplate: () => http.download('/workorders/import/template/file'),
modules: () => http.get('/workorders/modules'),
byStatus: (status) => http.get(`/workorders/by-status/${status}`),
batchDelete: (ids) => http.post('/batch-delete/workorders', { ids })
},
// 对话管理
conversations: {
list: (params) => http.get('/conversations', params),
get: (id) => http.get(`/conversations/${id}`),
delete: (id) => http.delete(`/conversations/${id}`),
clear: () => http.delete('/conversations/clear'),
search: (params) => http.get('/conversations/search', params),
analytics: () => http.get('/conversations/analytics'),
migrateMerge: (data) => http.post('/conversations/migrate-merge', data),
timeline: (workOrderId, params) => http.get(`/conversations/workorder/${workOrderId}/timeline`, params),
context: (workOrderId) => http.get(`/conversations/workorder/${workOrderId}/context`),
summary: (workOrderId) => http.get(`/conversations/workorder/${workOrderId}/summary`)
},
// 聊天接口
chat: {
createSession: (data) => http.post('/chat/session', data),
sendMessage: (data) => http.post('/chat/message', data),
getHistory: (sessionId) => http.get(`/chat/history/${sessionId}`),
createWorkOrder: (data) => http.post('/chat/work-order', data),
getWorkOrderStatus: (workOrderId) => http.get(`/chat/work-order/${workOrderId}`),
endSession: (sessionId) => http.delete(`/chat/session/${sessionId}`),
sessions: () => http.get('/chat/sessions')
},
// 知识库
knowledge: {
list: (params) => http.get('/knowledge', params),
search: (params) => http.get('/knowledge/search', params),
create: (data) => http.post('/knowledge', data),
get: (id) => http.get(`/knowledge/${id}`),
update: (id, data) => http.put(`/knowledge/${id}`, data),
delete: (id) => http.delete(`/knowledge/delete/${id}`),
verify: (id) => http.post(`/knowledge/verify/${id}`),
unverify: (id) => http.post(`/knowledge/unverify/${id}`),
stats: () => http.get('/knowledge/stats'),
upload: (file, data) => {
const formData = new FormData();
formData.append('file', file);
Object.keys(data).forEach(key => {
formData.append(key, data[key]);
});
return http.upload('/knowledge/upload', formData);
},
byStatus: (status) => http.get(`/knowledge/by-status/${status}`),
batchDelete: (ids) => http.post('/batch-delete/knowledge', { ids })
},
// 预警管理
alerts: {
list: (params) => http.get('/alerts', params),
create: (data) => http.post('/alerts', data),
get: (id) => http.get(`/alerts/${id}`),
update: (id, data) => http.put(`/alerts/${id}`, data),
delete: (id) => http.delete(`/alerts/${id}`),
resolve: (id, data) => http.post(`/alerts/${id}/resolve`, data),
statistics: () => http.get('/alerts/statistics'),
byLevel: (level) => http.get(`/alerts/by-level/${level}`),
batchDelete: (ids) => http.post('/batch-delete/alerts', { ids })
},
// 预警规则
rules: {
list: () => http.get('/rules'),
create: (data) => http.post('/rules', data),
update: (name, data) => http.put(`/rules/${name}`, data),
delete: (name) => http.delete(`/rules/${name}`)
},
// 监控管理
monitor: {
start: () => http.post('/monitor/start'),
stop: () => http.post('/monitor/stop'),
status: () => http.get('/monitor/status'),
checkAlerts: () => http.post('/check-alerts'),
analytics: (params) => http.get('/analytics', params)
},
// Token监控
tokenMonitor: {
stats: () => http.get('/token-monitor/stats'),
chart: (params) => http.get('/token-monitor/chart', params),
records: (params) => http.get('/token-monitor/records', params),
settings: (data) => http.post('/token-monitor/settings', data),
export: (params) => http.download('/token-monitor/export', params)
},
// AI监控
aiMonitor: {
stats: () => http.get('/ai-monitor/stats'),
modelComparison: () => http.get('/ai-monitor/model-comparison'),
errorDistribution: () => http.get('/ai-monitor/error-distribution'),
errorLog: (params) => http.get('/ai-monitor/error-log', params),
clearErrorLog: () => http.delete('/ai-monitor/error-log')
},
// Agent相关
agent: {
status: () => http.get('/agent/status'),
toggle: () => http.post('/agent/toggle'),
chat: (data) => http.post('/agent/chat', data),
actionHistory: () => http.get('/agent/action-history'),
clearHistory: () => http.post('/agent/clear-history'),
tools: {
stats: () => http.get('/agent/tools/stats'),
execute: (data) => http.post('/agent/tools/execute', data),
register: (data) => http.post('/agent/tools/register', data),
unregister: (name) => http.delete(`/agent/tools/unregister/${name}`)
},
monitoring: {
start: () => http.post('/agent/monitoring/start'),
stop: () => http.post('/agent/monitoring/stop'),
proactiveCheck: () => http.post('/agent/proactive-monitoring'),
intelligentAnalysis: () => http.post('/agent/intelligent-analysis')
},
llmStats: () => http.get('/agent/llm-stats'),
triggerSample: () => http.post('/agent/trigger-sample')
},
// 车辆数据
vehicle: {
data: (params) => http.get('/vehicle/data', params),
latestById: (id) => http.get(`/vehicle/data/${id}/latest`),
latestByVin: (vin) => http.get(`/vehicle/data/vin/${vin}/latest`),
summary: (id) => http.get(`/vehicle/data/${id}/summary`),
add: (data) => http.post('/vehicle/data', data),
initSample: () => http.post('/vehicle/init-sample-data')
},
// 飞书同步
feishu: {
config: {
get: () => http.get('/feishu-sync/config'),
save: (data) => http.post('/feishu-sync/config', data)
},
testConnection: () => http.get('/feishu-sync/test-connection'),
checkPermissions: () => http.get('/feishu-sync/check-permissions'),
syncFromFeishu: (data) => http.post('/feishu-sync/sync-from-feishu', data),
syncToFeishu: (workOrderId) => http.post(`/feishu-sync/sync-to-feishu/${workOrderId}`),
status: () => http.get('/feishu-sync/status'),
createWorkorder: (data) => http.post('/feishu-sync/create-workorder', data),
fieldMapping: {
status: () => http.get('/feishu-sync/field-mapping/status'),
discover: () => http.post('/feishu-sync/field-mapping/discover'),
add: (data) => http.post('/feishu-sync/field-mapping/add', data),
remove: (data) => http.post('/feishu-sync/field-mapping/remove', data)
},
previewData: (params) => http.get('/feishu-sync/preview-feishu-data', params),
config: {
export: () => http.download('/feishu-sync/config/export'),
import: (file) => {
const formData = new FormData();
formData.append('file', file);
return http.upload('/feishu-sync/config/import', formData);
},
reset: () => http.post('/feishu-sync/config/reset')
}
},
// 系统优化
systemOptimizer: {
status: () => http.get('/system-optimizer/status'),
optimizeCpu: () => http.post('/system-optimizer/optimize-cpu'),
optimizeMemory: () => http.post('/system-optimizer/optimize-memory'),
optimizeDisk: () => http.post('/system-optimizer/optimize-disk'),
clearCache: () => http.post('/system-optimizer/clear-cache'),
optimizeAll: () => http.post('/system-optimizer/optimize-all'),
securitySettings: (data) => http.post('/system-optimizer/security-settings', data),
trafficSettings: (data) => http.post('/system-optimizer/traffic-settings', data),
costSettings: (data) => http.post('/system-optimizer/cost-settings', data),
healthCheck: () => http.post('/system-optimizer/health-check')
},
// 数据库备份
backup: {
info: () => http.get('/backup/info'),
create: (data) => http.post('/backup/create', data),
restore: (data) => http.post('/backup/restore', data)
}
};
// 导出默认配置和请求方法
export { config };
export default { http, api, config };

View File

@@ -0,0 +1,466 @@
/**
* 路由管理模块
*/
import { parseQueryString, serializeQueryString } from './utils.js';
import store from './store.js';
// 路由配置
class Router {
constructor() {
this.routes = new Map();
this.currentRoute = null;
this.beforeEachHooks = [];
this.afterEachHooks = [];
this.mode = 'history'; // 'history' 或 'hash'
this.base = '/';
this.fallback = true;
// 绑定事件处理器
this.handlePopState = this.handlePopState.bind(this);
this.handleHashChange = this.handleHashChange.bind(this);
}
// 配置路由
config(options = {}) {
if (options.mode) this.mode = options.mode;
if (options.base) this.base = options.base;
if (options.fallback !== undefined) this.fallback = options.fallback;
return this;
}
// 添加路由
addRoute(path, component, options = {}) {
const route = {
path,
component,
name: options.name || path,
meta: options.meta || {},
props: options.props || false,
children: options.children || [],
beforeEnter: options.beforeEnter
};
// 转换路径为正则表达式
route.regex = this.pathToRegex(path);
route.keys = [];
// 提取动态参数
const paramNames = path.match(/:\w+/g);
if (paramNames) {
route.keys = paramNames.map(name => name.slice(1));
}
this.routes.set(path, route);
return this;
}
// 批量添加路由
addRoutes(routes) {
routes.forEach(route => {
this.addRoute(route.path, route.component, route);
});
return this;
}
// 路径转正则
pathToRegex(path) {
const regexPath = path
.replace(/\//g, '\\/')
.replace(/:\w+/g, '([^\\/]+)')
.replace(/\*/g, '(.*)');
return new RegExp(`^${regexPath}$`);
}
// 匹配路由
match(path) {
for (const [routePath, route] of this.routes) {
const match = path.match(route.regex);
if (match) {
const params = {};
route.keys.forEach((key, index) => {
params[key] = match[index + 1];
});
return {
route,
params,
path,
query: this.parseQuery(path)
};
}
}
// 404处理
return {
route: { path: '/404', component: 'notfound' },
params: {},
path,
query: {}
};
}
// 解析查询字符串
parseQuery(path) {
const queryIndex = path.indexOf('?');
if (queryIndex === -1) return {};
const queryString = path.slice(queryIndex + 1);
return parseQueryString(queryString);
}
// 构建路径
buildPath(route, params = {}, query = {}) {
let path = route.path;
// 替换动态参数
Object.keys(params).forEach(key => {
path = path.replace(`:${key}`, params[key]);
});
// 添加查询字符串
const queryString = serializeQueryString(query);
if (queryString) {
path += `?${queryString}`;
}
return path;
}
// 导航到指定路径
push(path, data = {}) {
return this.navigateTo(path, 'push', data);
}
// 替换当前路径
replace(path, data = {}) {
return this.navigateTo(path, 'replace', data);
}
// 返回上一页
go(n) {
window.history.go(n);
}
// 返回
back() {
this.go(-1);
}
// 前进
forward() {
this.go(1);
}
// 执行导航
async navigateTo(path, type = 'push', data = {}) {
// 匹配路由
const matched = this.match(path);
// 创建路由对象
const route = {
path: matched.path,
name: matched.route.name,
params: matched.params,
query: matched.query,
meta: matched.route.meta,
hash: this.parseHash(path),
...data
};
// 执行前置守卫
const guards = [...this.beforeEachHooks, matched.route.beforeEnter].filter(Boolean);
for (const guard of guards) {
const result = await guard(route, this.currentRoute);
if (result === false) {
return Promise.reject(new Error('Navigation cancelled'));
}
if (typeof result === 'string') {
return this.navigateTo(result, type, data);
}
if (result && typeof result === 'object') {
return this.navigateTo(result.path || result, type, result);
}
}
// 保存当前路由
const prevRoute = this.currentRoute;
this.currentRoute = route;
// 更新URL
this.updateURL(path, type);
// 执行后置守卫
this.afterEachHooks.forEach(hook => {
try {
hook(route, prevRoute);
} catch (error) {
console.error('After each hook error:', error);
}
});
// 更新store
store.commit('SET_CURRENT_ROUTE', route);
return route;
}
// 更新URL
updateURL(path, type) {
if (this.mode === 'history') {
const url = this.base === '/' ? path : `${this.base}${path}`.replace('//', '/');
if (type === 'replace') {
window.history.replaceState({ path }, '', url);
} else {
window.history.pushState({ path }, '', url);
}
} else {
const hash = this.mode === 'hash' ? `#${path}` : `#${this.base}${path}`.replace('//', '/');
if (type === 'replace') {
window.location.replace(hash);
} else {
window.location.hash = hash;
}
}
}
// 解析hash
parseHash(path) {
const hashIndex = path.indexOf('#');
return hashIndex === -1 ? '' : path.slice(hashIndex + 1);
}
// 处理popstate事件
handlePopState(event) {
if (event.state && event.state.path) {
this.navigateTo(event.state.path, 'push', { replace: true });
} else {
const path = this.getCurrentPath();
this.navigateTo(path, 'push', { replace: true });
}
}
// 处理hashchange事件
handleHashChange() {
const path = this.getCurrentPath();
this.navigateTo(path, 'push', { replace: true });
}
// 获取当前路径
getCurrentPath() {
if (this.mode === 'history') {
const path = window.location.pathname;
return path.startsWith(this.base) ? path.slice(this.base.length) : path;
} else {
const hash = window.location.hash.slice(1);
return hash.startsWith(this.base) ? hash.slice(this.base.length) : hash;
}
}
// 全局前置守卫
beforeEach(hook) {
this.beforeEachHooks.push(hook);
}
// 全局后置守卫
afterEach(hook) {
this.afterEachHooks.push(hook);
}
// 启动路由
start() {
// 监听事件
if (this.mode === 'history') {
window.addEventListener('popstate', this.handlePopState);
} else {
window.addEventListener('hashchange', this.handleHashChange);
}
// 处理初始路由
const path = this.getCurrentPath();
this.navigateTo(path, 'push', { replace: true });
// 拦截链接点击
this.interceptLinks();
return this;
}
// 拦截链接
interceptLinks() {
document.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href || href.startsWith('http') || href.startsWith('mailto:') || href.startsWith('tel:')) {
return;
}
e.preventDefault();
this.push(href);
});
}
// 停止路由
stop() {
window.removeEventListener('popstate', this.handlePopState);
window.removeEventListener('hashchange', this.handleHashChange);
return this;
}
}
// 创建路由实例
export const router = new Router();
// 路由配置
router.config({
mode: 'history',
base: '/',
fallback: true
});
// 添加路由
router.addRoutes([
{
path: '/',
name: 'dashboard',
component: 'Dashboard',
meta: { title: '仪表板', icon: 'fas fa-tachometer-alt' }
},
{
path: '/workorders',
name: 'workorders',
component: 'WorkOrders',
meta: { title: '工单管理', icon: 'fas fa-tasks' }
},
{
path: '/workorders/:id',
name: 'workorder-detail',
component: 'WorkOrderDetail',
meta: { title: '工单详情' }
},
{
path: '/alerts',
name: 'alerts',
component: 'Alerts',
meta: { title: '预警管理', icon: 'fas fa-bell' }
},
{
path: '/knowledge',
name: 'knowledge',
component: 'Knowledge',
meta: { title: '知识库', icon: 'fas fa-book' }
},
{
path: '/knowledge/:id',
name: 'knowledge-detail',
component: 'KnowledgeDetail',
meta: { title: '知识详情' }
},
{
path: '/chat',
name: 'chat',
component: 'Chat',
meta: { title: '智能对话', icon: 'fas fa-comments' }
},
{
path: '/chat-http',
name: 'chat-http',
component: 'ChatHttp',
meta: { title: '对话(HTTP)', icon: 'fas fa-comment-dots' }
},
{
path: '/monitoring',
name: 'monitoring',
component: 'Monitoring',
meta: { title: '系统监控', icon: 'fas fa-chart-line' }
},
{
path: '/settings',
name: 'settings',
component: 'Settings',
meta: { title: '系统设置', icon: 'fas fa-cog' }
},
{
path: '/feishu',
name: 'feishu',
component: 'Feishu',
meta: { title: '飞书同步', icon: 'fab fa-lark' }
},
{
path: '/agent',
name: 'agent',
component: 'Agent',
meta: { title: '智能Agent', icon: 'fas fa-robot' }
},
{
path: '/vehicle',
name: 'vehicle',
component: 'Vehicle',
meta: { title: '车辆数据', icon: 'fas fa-car' }
},
{
path: '/profile',
name: 'profile',
component: 'Profile',
meta: { title: '个人资料' }
},
{
path: '/login',
name: 'login',
component: 'Login',
meta: { title: '登录', requiresAuth: false }
},
{
path: '/404',
name: '404',
component: 'notfound',
meta: { title: '页面未找到' }
},
{
path: '*',
redirect: '/404'
}
]);
// 全局前置守卫
router.beforeEach((to, from) => {
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - TSP智能助手`;
}
// 权限检查
if (to.meta.requiresAuth !== false && !store.getState('user.isLogin')) {
return '/login';
}
// 管理员权限检查
if (to.meta.requiresAdmin && !store.getState('user.info.isAdmin')) {
return '/403';
}
});
// 导出路由实例和辅助函数
export function push(path, data) {
return router.push(path, data);
}
export function replace(path, data) {
return router.replace(path, data);
}
export function go(n) {
return router.go(n);
}
export function back() {
return router.back();
}
export function forward() {
return router.forward();
}
export default router;

View File

@@ -0,0 +1,203 @@
/**
* 全局状态管理
* 集中管理应用状态,避免状态分散
*/
class Store {
constructor() {
this.state = {
// 预警相关
alerts: [],
alertFilters: {
level: 'all',
type: 'all',
status: 'all'
},
alertStats: {
total: 0,
critical: 0,
warning: 0,
info: 0
},
// 规则相关
rules: [],
// 系统状态
health: {},
monitorStatus: 'unknown',
// Agent相关
agentStatus: {
status: 'inactive',
active_goals: 0,
available_tools: 0
},
agentHistory: [],
// 车辆数据
vehicleData: [],
// UI状态
loading: false,
notifications: []
};
this.listeners = [];
this.debounceTimers = new Map();
}
// 获取状态
getState() {
return { ...this.state };
}
// 更新状态
setState(updates) {
const prevState = { ...this.state };
this.state = { ...this.state, ...updates };
// 通知监听器
this.notifyListeners(prevState, this.state);
}
// 订阅状态变化
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
// 通知监听器
notifyListeners(prevState, newState) {
this.listeners.forEach(listener => {
try {
listener(prevState, newState);
} catch (error) {
console.error('状态监听器错误:', error);
}
});
}
// 防抖更新状态
setStateDebounced(updates, delay = 300) {
const key = JSON.stringify(updates);
if (this.debounceTimers.has(key)) {
clearTimeout(this.debounceTimers.get(key));
}
this.debounceTimers.set(key, setTimeout(() => {
this.setState(updates);
this.debounceTimers.delete(key);
}, delay));
}
// 预警相关方法
updateAlerts(alerts) {
this.setState({ alerts });
// 更新统计信息
const stats = {
total: alerts.length,
critical: alerts.filter(a => a.level === 'critical').length,
warning: alerts.filter(a => a.level === 'warning').length,
info: alerts.filter(a => a.level === 'info').length
};
this.setState({ alertStats: stats });
}
updateAlertFilters(filters) {
this.setState({ alertFilters: { ...this.state.alertFilters, ...filters } });
}
// 规则相关方法
updateRules(rules) {
this.setState({ rules });
}
// 系统状态相关方法
updateHealth(health) {
this.setState({ health });
}
updateMonitorStatus(status) {
this.setState({ monitorStatus: status });
}
// Agent相关方法
updateAgentStatus(status) {
this.setState({ agentStatus: status });
}
updateAgentHistory(history) {
this.setState({ agentHistory: history });
}
// 车辆数据相关方法
updateVehicleData(data) {
this.setState({ vehicleData: data });
}
// UI状态相关方法
setLoading(loading) {
this.setState({ loading });
}
// 通知相关方法
addNotification(notification) {
const notifications = [...this.state.notifications, {
id: Date.now(),
timestamp: new Date(),
...notification
}];
this.setState({ notifications });
// 3秒后自动移除
setTimeout(() => {
this.removeNotification(notification.id || notifications[notifications.length - 1].id);
}, 3000);
}
removeNotification(id) {
const notifications = this.state.notifications.filter(n => n.id !== id);
this.setState({ notifications });
}
// 获取过滤后的预警
getFilteredAlerts() {
const { alerts, alertFilters } = this.state;
return alerts.filter(alert => {
if (alertFilters.level !== 'all' && alert.level !== alertFilters.level) return false;
if (alertFilters.type !== 'all' && alert.alert_type !== alertFilters.type) return false;
if (alertFilters.status !== 'all' && alert.status !== alertFilters.status) return false;
return true;
});
}
// 获取排序后的预警
getSortedAlerts(sortBy = 'timestamp', sortOrder = 'desc') {
const filtered = this.getFilteredAlerts();
return filtered.sort((a, b) => {
let aVal = a[sortBy];
let bVal = b[sortBy];
if (sortBy === 'timestamp') {
aVal = new Date(aVal);
bVal = new Date(bVal);
}
if (sortOrder === 'asc') {
return aVal > bVal ? 1 : -1;
} else {
return aVal < bVal ? 1 : -1;
}
});
}
}
// 创建全局状态管理实例
const store = new Store();

View File

@@ -0,0 +1,431 @@
/**
* 工具函数集合
*/
// 防抖函数
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 节流函数
export function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// 格式化日期
export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
if (!date) return '';
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
}
// 相对时间格式化
export function formatRelativeTime(date) {
if (!date) return '';
const now = new Date();
const target = new Date(date);
const diff = now - target;
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 30) {
return formatDate(date, 'YYYY-MM-DD');
} else if (days > 0) {
return `${days}天前`;
} else if (hours > 0) {
return `${hours}小时前`;
} else if (minutes > 0) {
return `${minutes}分钟前`;
} else {
return '刚刚';
}
}
// 文件大小格式化
export function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 数字千分位格式化
export function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// 深拷贝
export function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map(item => deepClone(item));
}
if (typeof obj === 'object') {
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
}
// 生成唯一ID
export function generateId(prefix = '') {
const timestamp = Date.now().toString(36);
const randomStr = Math.random().toString(36).substr(2, 5);
return `${prefix}${timestamp}${randomStr}`;
}
// 查询参数解析
export function parseQueryString(queryString) {
const params = new URLSearchParams(queryString);
const result = {};
for (const [key, value] of params) {
result[key] = value;
}
return result;
}
// 查询参数序列化
export function serializeQueryString(params) {
return Object.entries(params)
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
}
// 获取查询参数
export function getQueryParam(name) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
// 设置查询参数
export function setQueryParam(name, value) {
const params = new URLSearchParams(window.location.search);
params.set(name, value);
const newUrl = `${window.location.pathname}?${params.toString()}${window.location.hash}`;
window.history.replaceState(null, '', newUrl);
}
// 删除查询参数
export function removeQueryParam(name) {
const params = new URLSearchParams(window.location.search);
params.delete(name);
const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}${window.location.hash}`;
window.history.replaceState(null, '', newUrl);
}
// 本地存储封装
export const storage = {
set(key, value, isSession = false) {
try {
const storage = isSession ? sessionStorage : localStorage;
storage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('Storage set error:', e);
}
},
get(key, defaultValue = null, isSession = false) {
try {
const storage = isSession ? sessionStorage : localStorage;
const item = storage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
console.error('Storage get error:', e);
return defaultValue;
}
},
remove(key, isSession = false) {
try {
const storage = isSession ? sessionStorage : localStorage;
storage.removeItem(key);
} catch (e) {
console.error('Storage remove error:', e);
}
},
clear(isSession = false) {
try {
const storage = isSession ? sessionStorage : localStorage;
storage.clear();
} catch (e) {
console.error('Storage clear error:', e);
}
}
};
// Cookie操作
export const cookie = {
set(name, value, days = 7) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = `expires=${date.toUTCString()}`;
document.cookie = `${name}=${value};${expires};path=/`;
},
get(name) {
const nameEQ = `${name}=`;
const ca = document.cookie.split(';');
for (let c of ca) {
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(nameEQ) === 0) {
return c.substring(nameEQ.length, c.length);
}
}
return null;
},
remove(name) {
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`;
}
};
// 类名操作
export function addClass(element, className) {
if (element.classList) {
element.classList.add(className);
} else {
element.className += ` ${className}`;
}
}
export function removeClass(element, className) {
if (element.classList) {
element.classList.remove(className);
} else {
element.className = element.className.replace(
new RegExp(`(^|\\b)${className.split(' ').join('|')}(\\b|$)`, 'gi'),
' '
);
}
}
export function hasClass(element, className) {
if (element.classList) {
return element.classList.contains(className);
} else {
return new RegExp(`(^| )${className}( |$)`, 'gi').test(element.className);
}
}
export function toggleClass(element, className) {
if (hasClass(element, className)) {
removeClass(element, className);
} else {
addClass(element, className);
}
}
// 事件委托
export function delegate(parent, selector, event, handler) {
parent.addEventListener(event, function(e) {
if (e.target.matches(selector)) {
handler(e);
}
});
}
// DOM就绪
export function ready(fn) {
if (document.readyState !== 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
// 动画帧
export const raf = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback) { return setTimeout(callback, 1000 / 60); };
export const caf = window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
function(id) { clearTimeout(id); };
// 错误处理
export function handleError(error, context = '') {
console.error(`Error in ${context}:`, error);
// 发送错误到服务器(可选)
if (window.errorReporting) {
window.errorReporting.report(error, context);
}
// 显示用户友好的错误信息
showToast('发生错误,请稍后重试', 'error');
}
// 安全的JSON解析
export function safeJsonParse(str, defaultValue = null) {
try {
return JSON.parse(str);
} catch (e) {
return defaultValue;
}
}
// XSS防护 - HTML转义
export function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
// 验证函数
export const validators = {
email: (email) => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
},
phone: (phone) => {
const re = /^1[3-9]\d{9}$/;
return re.test(phone);
},
url: (url) => {
try {
new URL(url);
return true;
} catch {
return false;
}
},
idCard: (idCard) => {
const re = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
return re.test(idCard);
}
};
// URL构建器
export class URLBuilder {
constructor(baseURL = '') {
this.baseURL = baseURL;
this.params = {};
}
path(path) {
this.baseURL = this.baseURL.replace(/\/$/, '') + '/' + path.replace(/^\//, '');
return this;
}
query(key, value) {
if (value !== undefined && value !== null) {
this.params[key] = value;
}
return this;
}
build() {
const queryString = serializeQueryString(this.params);
return queryString ? `${this.baseURL}?${queryString}` : this.baseURL;
}
}
// 图片压缩
export function compressImage(file, options = {}) {
const {
maxWidth = 800,
maxHeight = 800,
quality = 0.8,
mimeType = 'image/jpeg'
} = options;
return new Promise((resolve) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
// 计算新尺寸
let { width, height } = img;
if (width > maxWidth || height > maxHeight) {
const ratio = Math.min(maxWidth / width, maxHeight / height);
width *= ratio;
height *= ratio;
}
canvas.width = width;
canvas.height = height;
// 绘制并压缩
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(resolve, mimeType, quality);
};
img.src = URL.createObjectURL(file);
});
}
// 导出默认配置
export const defaultConfig = {
apiBaseUrl: '/api',
wsUrl: `ws://${window.location.host}:8765`,
toastDuration: 3000,
paginationSize: 10,
debounceDelay: 300,
throttleDelay: 100
};

View File

@@ -0,0 +1,441 @@
/**
* WebSocket管理模块
*/
import { defaultConfig, storage, debounce } from './utils.js';
import store from './store.js';
// WebSocket配置
const config = {
...defaultConfig,
reconnectInterval: 3000, // 重连间隔
maxReconnectAttempts: 5, // 最大重连次数
heartbeatInterval: 30000, // 心跳间隔
heartbeatTimeout: 5000, // 心跳超时
messageQueue: [], // 消息队列
debug: false // 调试模式
};
// WebSocket状态枚举
export const WebSocketState = {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3
};
// WebSocket管理器类
class WebSocketManager {
constructor(url = config.wsUrl) {
this.url = url;
this.ws = null;
this.state = WebSocketState.CLOSED;
this.reconnectAttempts = 0;
this.reconnectTimer = null;
this.heartbeatTimer = null;
this.heartbeatTimeoutTimer = null;
this.messageHandlers = new Map();
this.readyStateHandlers = [];
this.messageId = 0;
this.pendingRequests = new Map();
// 绑定方法
this.onOpen = this.onOpen.bind(this);
this.onClose = this.onClose.bind(this);
this.onMessage = this.onMessage.bind(this);
this.onError = this.onError.bind(this);
}
// 连接WebSocket
connect() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.log('WebSocket already connected');
return;
}
// 清除之前的重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.log('Connecting to WebSocket...');
this.setState(WebSocketState.CONNECTING);
try {
// 构建WebSocket URL添加认证信息
const token = storage.get('authToken');
const wsUrl = token ? `${this.url}?token=${token}` : this.url;
this.ws = new WebSocket(wsUrl);
// 绑定事件处理器
this.ws.onopen = this.onOpen;
this.ws.onclose = this.onClose;
this.ws.onmessage = this.onMessage;
this.ws.onerror = this.onError;
} catch (error) {
this.log('WebSocket connection error:', error);
this.handleError(error);
}
}
// 断开连接
disconnect() {
this.log('Disconnecting WebSocket...');
this.setState(WebSocketState.CLOSING);
// 停止重连
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
// 停止心跳
this.stopHeartbeat();
// 关闭连接
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.setState(WebSocketState.CLOSED);
}
// 发送消息
send(type, data = {}, options = {}) {
return new Promise((resolve, reject) => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const message = {
id: this.messageId++,
type,
data,
timestamp: Date.now()
};
// 如果需要响应,保存回调
if (options.expectResponse) {
this.pendingRequests.set(message.id, {
resolve,
reject,
timeout: setTimeout(() => {
this.pendingRequests.delete(message.id);
reject(new Error('Request timeout'));
}, options.timeout || 30000)
});
}
// 发送消息
this.ws.send(JSON.stringify(message));
this.log('Sent message:', message);
// 如果不需要响应,立即解决
if (!options.expectResponse) {
resolve(message.id);
}
} else {
// 连接未就绪,加入队列
config.messageQueue.push({ type, data, options, resolve, reject });
reject(new Error('WebSocket not connected'));
}
});
}
// 注册消息处理器
on(type, handler) {
if (!this.messageHandlers.has(type)) {
this.messageHandlers.set(type, []);
}
this.messageHandlers.get(type).push(handler);
}
// 取消注册消息处理器
off(type, handler) {
if (this.messageHandlers.has(type)) {
const handlers = this.messageHandlers.get(type);
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}
// 注册状态变化处理器
onReadyStateChange(handler) {
this.readyStateHandlers.push(handler);
}
// 事件处理器
onOpen() {
this.log('WebSocket connected');
this.setState(WebSocketState.OPEN);
this.reconnectAttempts = 0;
// 启动心跳
this.startHeartbeat();
// 发送队列中的消息
this.flushMessageQueue();
// 通知状态变化
this.notifyReadyStateChange();
// 更新store状态
store.commit('SET_WS_CONNECTED', true);
}
onClose(event) {
this.log('WebSocket closed:', event);
this.setState(WebSocketState.CLOSED);
// 停止心跳
this.stopHeartbeat();
// 拒绝所有待处理的请求
this.pendingRequests.forEach(({ reject, timeout }) => {
clearTimeout(timeout);
reject(new Error('WebSocket closed'));
});
this.pendingRequests.clear();
// 通知状态变化
this.notifyReadyStateChange();
// 更新store状态
store.commit('SET_WS_CONNECTED', false);
// 自动重连
if (!event.wasClean && this.reconnectAttempts < config.maxReconnectAttempts) {
this.reconnect();
}
}
onMessage(event) {
try {
const message = JSON.parse(event.data);
this.log('Received message:', message);
// 处理响应消息
if (message.id && this.pendingRequests.has(message.id)) {
const { resolve, reject, timeout } = this.pendingRequests.get(message.id);
clearTimeout(timeout);
this.pendingRequests.delete(message.id);
if (message.error) {
reject(new Error(message.error));
} else {
resolve(message.data);
}
return;
}
// 处理业务消息
const handlers = this.messageHandlers.get(message.type);
if (handlers) {
handlers.forEach(handler => {
try {
handler(message.data, message);
} catch (error) {
this.log('Handler error:', error);
}
});
} else {
this.log('No handler for message type:', message.type);
}
// 处理心跳响应
if (message.type === 'pong') {
this.handlePong();
}
} catch (error) {
this.log('Message parse error:', error);
}
}
onError(error) {
this.log('WebSocket error:', error);
this.handleError(error);
}
// 重连
reconnect() {
if (this.reconnectAttempts >= config.maxReconnectAttempts) {
this.log('Max reconnect attempts reached');
return;
}
this.reconnectAttempts++;
this.log(`Reconnecting... Attempt ${this.reconnectAttempts}`);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, config.reconnectInterval);
}
// 启动心跳
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
this.send('ping').catch(error => {
this.log('Heartbeat error:', error);
});
// 设置心跳超时
this.heartbeatTimeoutTimer = setTimeout(() => {
this.log('Heartbeat timeout');
this.disconnect();
this.reconnect();
}, config.heartbeatTimeout);
}, config.heartbeatInterval);
}
// 停止心跳
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
// 处理pong响应
handlePong() {
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
// 清空消息队列
flushMessageQueue() {
while (config.messageQueue.length > 0) {
const { type, data, options, resolve, reject } = config.messageQueue.shift();
this.send(type, data, options).then(resolve).catch(reject);
}
}
// 设置状态
setState(state) {
this.state = state;
this.notifyReadyStateChange();
}
// 通知状态变化
notifyReadyStateChange() {
this.readyStateHandlers.forEach(handler => {
try {
handler(this.state);
} catch (error) {
this.log('ReadyState handler error:', error);
}
});
}
// 处理错误
handleError(error) {
this.log('WebSocket error:', error);
// 显示错误提示
store.dispatch('showToast', {
type: 'error',
message: 'WebSocket连接失败'
});
}
// 日志输出
log(...args) {
if (config.debug) {
console.log('[WebSocket]', ...args);
}
}
// 获取连接状态
getReadyState() {
return this.state;
}
// 检查是否已连接
isConnected() {
return this.state === WebSocket.OPEN;
}
}
// 创建全局WebSocket实例
export const wsManager = new WebSocketManager();
// 扩展WebSocket管理器添加业务方法
wsManager.on('connected', (data) => {
store.dispatch('showToast', {
type: 'success',
message: 'WebSocket已连接'
});
});
wsManager.on('disconnected', (data) => {
store.dispatch('showToast', {
type: 'warning',
message: 'WebSocket已断开'
});
});
// 业务方法封装
export const wsApi = {
// 创建会话
createSession: (data) => wsManager.send('create_session', data, { expectResponse: true }),
// 发送消息
sendMessage: (data) => wsManager.send('send_message', data, { expectResponse: true }),
// 获取历史记录
getHistory: (sessionId) => wsManager.send('get_history', { session_id: sessionId }, { expectResponse: true }),
// 创建工单
createWorkOrder: (data) => wsManager.send('create_work_order', data, { expectResponse: true }),
// 获取工单状态
getWorkOrderStatus: (workOrderId) => wsManager.send('get_work_order_status', { work_order_id: workOrderId }, { expectResponse: true }),
// 结束会话
endSession: (sessionId) => wsManager.send('end_session', { session_id: sessionId }, { expectResponse: true })
};
// 初始化WebSocket连接
export function initWebSocket() {
// 页面可见时连接
document.addEventListener('visibilitychange', () => {
if (!document.hidden && !wsManager.isConnected()) {
wsManager.connect();
}
});
// 窗口获得焦点时检查连接
window.addEventListener('focus', () => {
if (!wsManager.isConnected()) {
wsManager.connect();
}
});
// 页面卸载时断开连接
window.addEventListener('beforeunload', () => {
wsManager.disconnect();
});
// 网络状态变化时重连
window.addEventListener('online', () => {
if (!wsManager.isConnected()) {
wsManager.connect();
}
});
// 自动连接
wsManager.connect();
}
// 导出配置和管理器
export { config };
export default wsManager;

File diff suppressed because it is too large Load Diff

406
src/web/static/js/main.js Normal file
View File

@@ -0,0 +1,406 @@
/**
* 主入口文件
*/
import { ready, storage } from './core/utils.js';
import store from './core/store.js';
import router from './core/router.js';
import { initWebSocket } from './core/websocket.js';
import Navbar from './components/navbar.js';
import Sidebar from './components/sidebar.js';
import { showToast } from './components/modal.js';
// 应用主类
class App {
constructor() {
this.components = {};
this.currentRoute = null;
}
// 初始化应用
async init() {
try {
// 显示加载状态
this.showLoading();
// 初始化路由
router.start();
// 初始化UI组件
this.initComponents();
// 恢复应用状态
this.restoreAppState();
// 初始化WebSocket
initWebSocket();
// 绑定全局事件
this.bindGlobalEvents();
// 注册服务工作者PWA支持
this.registerServiceWorker();
// 隐藏加载状态
this.hideLoading();
console.log('App initialized successfully');
} catch (error) {
console.error('App initialization failed:', error);
this.handleInitError(error);
}
}
// 初始化组件
initComponents() {
// 初始化导航栏
const navbarContainer = document.querySelector('#navbar');
if (navbarContainer) {
this.components.navbar = new Navbar(navbarContainer);
}
// 初始化侧边栏
const sidebarContainer = document.querySelector('#sidebar-container');
if (sidebarContainer) {
this.components.sidebar = new Sidebar(sidebarContainer);
}
// 初始化其他组件...
}
// 恢复应用状态
restoreAppState() {
// 恢复主题
const savedTheme = storage.get('app.theme', 'light');
store.commit('SET_THEME', savedTheme);
// 恢复用户信息(如果有)
const userInfo = storage.get('userInfo');
if (userInfo) {
store.commit('SET_USER', userInfo);
store.commit('SET_LOGIN', true);
}
// 恢复其他设置...
}
// 绑定全局事件
bindGlobalEvents() {
// 监听路由变化
router.afterEach((to) => {
this.handleRouteChange(to);
});
// 监听网络状态
window.addEventListener('online', () => {
showToast('网络已连接', 'success');
});
window.addEventListener('offline', () => {
showToast('网络已断开', 'warning');
});
// 监听页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
store.commit('SET_APP_ACTIVE', false);
} else {
store.commit('SET_APP_ACTIVE', true);
}
});
// 监听存储变化(多标签页同步)
window.addEventListener('storage', (e) => {
this.handleStorageChange(e);
});
// 监听未捕获的错误
window.addEventListener('error', (e) => {
this.handleError(e.error);
});
window.addEventListener('unhandledrejection', (e) => {
this.handleError(e.reason);
});
}
// 处理路由变化
async handleRouteChange(to) {
this.currentRoute = to;
// 更新页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - TSP智能助手`;
}
// 加载页面组件
await this.loadPage(to);
// 更新导航状态
this.updateNavigation(to);
// 滚动到顶部
window.scrollTo(0, 0);
}
// 加载页面组件
async loadPage(route) {
const pageContainer = document.querySelector('#page-content');
if (!pageContainer) return;
// 显示加载状态
pageContainer.innerHTML = this.createLoadingHTML();
try {
// 映射路由到页面文件
const pageFile = this.getPageFile(route.name);
// 动态导入页面组件
const pageModule = await import(`./pages/${pageFile}.js`);
const PageComponent = pageModule.default;
// 实例化页面组件
const page = new PageComponent(pageContainer, route);
// 保存页面实例
this.components.currentPage = page;
// 将页面实例暴露到全局(供内联事件使用)
if (route.name === 'alerts') {
window.alertsPage = page;
}
} catch (error) {
console.error('Failed to load page:', error);
pageContainer.innerHTML = this.createErrorHTML(error);
}
}
// 获取页面文件名
getPageFile(routeName) {
const pageMap = {
'dashboard': 'dashboard',
'workorders': 'workorders',
'workorder-detail': 'workorders',
'alerts': 'alerts',
'knowledge': 'knowledge',
'knowledge-detail': 'knowledge',
'chat': 'chat',
'chat-http': 'chat',
'monitoring': 'monitoring',
'settings': 'settings',
'profile': 'settings',
'login': 'login',
'feishu': 'feishu',
'agent': 'agent',
'vehicle': 'vehicle'
};
return pageMap[routeName] || 'dashboard';
}
// 更新导航状态
updateNavigation(route) {
// 更新侧边栏激活状态
if (this.components.sidebar) {
this.components.sidebar.updateActiveMenu(route.path);
}
// 更新导航栏面包屑
this.updateBreadcrumb(route);
}
// 更新面包屑
updateBreadcrumb(route) {
const breadcrumbContainer = document.querySelector('#breadcrumb');
if (!breadcrumbContainer) return;
const items = [
{ text: '首页', link: '/' }
];
// 根据路由构建面包屑
if (route.path !== '/') {
const pathSegments = route.path.split('/').filter(Boolean);
let currentPath = '';
pathSegments.forEach((segment, index) => {
currentPath += `/${segment}`;
const isLast = index === pathSegments.length - 1;
// 这里可以根据路由配置获取更友好的名称
const name = this.getPathName(segment);
items.push({
text: name,
link: isLast ? null : currentPath
});
});
}
breadcrumbContainer.innerHTML = this.createBreadcrumbHTML(items);
}
// 获取路径名称
getPathName(segment) {
const names = {
workorders: '工单管理',
alerts: '预警管理',
knowledge: '知识库',
chat: '智能对话',
'chat-http': 'HTTP对话',
monitoring: '系统监控',
settings: '系统设置'
};
return names[segment] || segment;
}
// 处理存储变化
handleStorageChange(e) {
// 处理多标签页之间的状态同步
if (e.key === 'tsp_assistant_store') {
const newState = JSON.parse(e.newValue);
store.setState(newState, false);
}
}
// 处理错误
handleError(error) {
console.error('Application error:', error);
// 显示错误提示
showToast('发生错误,请刷新页面重试', 'error');
// 发送错误报告如果配置了且有report方法
if (window.errorReporting && window.errorReporting.enabled && window.errorReporting.report) {
try {
window.errorReporting.report(error);
} catch (reportError) {
console.error('Error reporting failed:', reportError);
}
}
}
// 处理初始化错误
handleInitError(error) {
this.hideLoading();
const pageContainer = document.querySelector('#page-content');
if (pageContainer) {
pageContainer.innerHTML = `
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
<h4>应用初始化失败</h4>
<p class="text-muted">${error.message || '未知错误'}</p>
<button class="btn btn-primary" onclick="location.reload()">
<i class="fas fa-redo me-2"></i>重新加载
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
}
// 注册服务工作者
registerServiceWorker() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('ServiceWorker registered:', registration);
})
.catch(error => {
console.log('ServiceWorker registration failed:', error);
});
}
}
// 显示加载状态
showLoading() {
const loadingHTML = `
<div id="loading-overlay" class="loading-overlay">
<div class="loading-content">
<div class="spinner"></div>
<p class="mt-3">加载中...</p>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', loadingHTML);
}
// 隐藏加载状态
hideLoading() {
const overlay = document.getElementById('loading-overlay');
if (overlay) {
overlay.remove();
}
}
// 创建加载HTML
createLoadingHTML() {
return `
<div class="d-flex justify-content-center align-items-center" style="min-height: 400px;">
<div class="text-center">
<div class="spinner"></div>
<p class="mt-3">加载中...</p>
</div>
</div>
`;
}
// 创建错误HTML
createErrorHTML(error) {
return `
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
<h4>页面加载失败</h4>
<p class="text-muted">${error.message || '未知错误'}</p>
<button class="btn btn-primary" onclick="location.reload()">
<i class="fas fa-redo me-2"></i>重新加载
</button>
</div>
</div>
</div>
</div>
</div>
`;
}
// 创建面包屑HTML
createBreadcrumbHTML(items) {
return `
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
${items.map((item, index) => `
<li class="breadcrumb-item ${index === items.length - 1 ? 'active' : ''}">
${item.link ? `<a href="${item.link}">${item.text}</a>` : item.text}
</li>
`).join('')}
</ol>
</nav>
`;
}
}
// 创建应用实例
const app = new App();
// DOM加载完成后初始化应用
ready(() => {
app.init();
});
// 暴露到全局(便于调试)
window.app = app;
window.store = store;
window.router = router;

View File

@@ -1,142 +0,0 @@
/**
* 通用导航管理脚本
* 用于处理页面间导航和活动状态
*/
class NavigationManager {
constructor() {
this.currentPage = this.getCurrentPage();
this.init();
}
init() {
// 设置当前页面的活动状态
this.setActiveNavigation();
// 添加导航点击事件监听
this.addNavigationListeners();
// 添加页面加载完成后的处理
this.addPageLoadHandlers();
}
getCurrentPage() {
const path = window.location.pathname;
if (path === '/' || path === '/dashboard') return 'dashboard';
if (path === '/alerts') return 'alerts';
if (path === '/chat') return 'chat';
if (path === '/chat-http') return 'chat-http';
return 'dashboard';
}
setActiveNavigation() {
// 清除所有活动状态
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
// 设置当前页面的活动状态
const activeSelectors = {
'dashboard': 'a.nav-link[href="/dashboard"]',
'alerts': 'a.nav-link[href="/alerts"]',
'chat': 'a.nav-link[href="/chat"]',
'chat-http': 'a.nav-link[href="/chat-http"]'
};
const selector = activeSelectors[this.currentPage];
if (selector) {
document.querySelectorAll(selector).forEach(link => {
link.classList.add('active');
});
}
}
addNavigationListeners() {
// 对导航链接添加点击处理
document.querySelectorAll('a.nav-link[href^="/"]').forEach(link => {
link.addEventListener('click', (e) => {
const href = link.getAttribute('href');
// 如果是当前页面,阻止默认行为
if (href === window.location.pathname) {
e.preventDefault();
return;
}
// 显示加载状态
this.showLoadingState();
// 正常跳转
// 注意:这里不阻止默认行为,让浏览器正常跳转
});
});
// 对 dashboard.html 的侧边栏导航特殊处理
document.querySelectorAll('.sidebar a.nav-link[href^="/"]').forEach(link => {
link.addEventListener('click', (e) => {
this.showLoadingState();
});
});
}
showLoadingState() {
// 显示加载提示
const loadingHtml = `
<div id="navigation-loading" class="position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center"
style="background: rgba(255,255,255,0.9); z-index: 9999;">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2 mb-0">页面跳转中...</p>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', loadingHtml);
}
addPageLoadHandlers() {
// 页面加载完成后移除加载状态
window.addEventListener('load', () => {
const loading = document.getElementById('navigation-loading');
if (loading) {
loading.remove();
}
});
// 处理浏览器前进后退
window.addEventListener('popstate', () => {
this.currentPage = this.getCurrentPage();
this.setActiveNavigation();
});
}
// 手动导航到指定页面
navigateTo(page) {
const urls = {
'dashboard': '/dashboard',
'alerts': '/alerts',
'chat': '/chat',
'chat-http': '/chat-http'
};
const url = urls[page];
if (url && url !== window.location.pathname) {
this.showLoadingState();
window.location.href = url;
}
}
}
// 页面加载完成后初始化导航管理器
document.addEventListener('DOMContentLoaded', () => {
window.navigationManager = new NavigationManager();
});
// 导航函数,可以在控制台或页面中使用
window.navigateTo = (page) => {
if (window.navigationManager) {
window.navigationManager.navigateTo(page);
}
};

View File

@@ -0,0 +1,560 @@
/**
* Agent页面组件
*/
export default class Agent {
constructor(container, route) {
this.container = container;
this.route = route;
this.init();
}
async init() {
try {
this.render();
this.bindEvents();
this.loadAgentStatus();
this.loadActionHistory();
} catch (error) {
console.error('Agent init error:', error);
this.showError(error);
}
}
render() {
this.container.innerHTML = `
<div class="page-header">
<div>
<h1 class="page-title">智能Agent</h1>
<p class="page-subtitle">AI助手自动监控和任务执行</p>
</div>
</div>
<div class="row">
<!-- Agent状态 -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-robot me-2"></i>Agent状态
</h5>
</div>
<div class="card-body">
<div id="agent-status" class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<div class="mt-2">加载Agent状态...</div>
</div>
</div>
</div>
</div>
<!-- 控制面板 -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-sliders-h me-2"></i>控制面板
</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-success" id="start-monitoring-btn">
<i class="fas fa-play me-2"></i>启动监控
</button>
<button class="btn btn-warning" id="stop-monitoring-btn">
<i class="fas fa-stop me-2"></i>停止监控
</button>
<button class="btn btn-info" id="run-analysis-btn">
<i class="fas fa-chart-line me-2"></i>运行分析
</button>
<button class="btn btn-primary" id="trigger-sample-btn">
<i class="fas fa-magic me-2"></i>触发示例动作
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Agent对话 -->
<div class="row mt-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-comments me-2"></i>Agent对话
</h5>
</div>
<div class="card-body">
<div id="chat-messages" class="chat-messages mb-3" style="height: 300px; overflow-y: auto;">
<div class="text-muted text-center">暂无对话记录</div>
</div>
<div class="input-group">
<input type="text" class="form-control" id="chat-input"
placeholder="输入消息与Agent对话..." maxlength="500">
<button class="btn btn-primary" id="send-chat-btn">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 工具统计 -->
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-tools me-2"></i>工具统计
</h5>
</div>
<div class="card-body">
<div id="tools-stats" class="text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>加载中...
</div>
</div>
</div>
<!-- LLM统计 -->
<div class="card mt-3">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-brain me-2"></i>LLM使用统计
</h5>
</div>
<div class="card-body">
<div id="llm-stats" class="text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>加载中...
</div>
</div>
</div>
</div>
</div>
<!-- 执行历史 -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-history me-2"></i>执行历史
</h5>
<button class="btn btn-sm btn-outline-danger" id="clear-history-btn">
<i class="fas fa-trash me-2"></i>清空历史
</button>
</div>
<div class="card-body">
<div id="action-history" class="text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>加载中...
</div>
</div>
</div>
</div>
</div>
`;
}
bindEvents() {
// 控制按钮事件
document.getElementById('start-monitoring-btn').addEventListener('click', () => {
this.startMonitoring();
});
document.getElementById('stop-monitoring-btn').addEventListener('click', () => {
this.stopMonitoring();
});
document.getElementById('run-analysis-btn').addEventListener('click', () => {
this.runAnalysis();
});
document.getElementById('trigger-sample-btn').addEventListener('click', () => {
this.triggerSampleActions();
});
// 对话事件
document.getElementById('send-chat-btn').addEventListener('click', () => {
this.sendChatMessage();
});
document.getElementById('chat-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendChatMessage();
}
});
// 清空历史
document.getElementById('clear-history-btn').addEventListener('click', () => {
this.clearHistory();
});
// 定期刷新状态
this.statusInterval = setInterval(() => {
this.loadAgentStatus();
}, 5000);
}
async loadAgentStatus() {
try {
const response = await fetch('/api/agent/status');
const data = await response.json();
const statusDiv = document.getElementById('agent-status');
if (data.success) {
const status = data.status || 'unknown';
const activeGoals = data.active_goals || 0;
const availableTools = data.available_tools || 0;
let statusClass = 'text-warning';
let statusText = '未知状态';
switch (status) {
case 'active':
statusClass = 'text-success';
statusText = '运行中';
break;
case 'inactive':
statusClass = 'text-secondary';
statusText = '未激活';
break;
case 'error':
statusClass = 'text-danger';
statusText = '错误';
break;
}
statusDiv.innerHTML = `
<div class="mb-3">
<i class="fas fa-circle ${statusClass} me-2"></i>
<span class="h5 mb-0 ${statusClass}">${statusText}</span>
</div>
<div class="row text-center">
<div class="col-6">
<div class="h4 mb-1">${activeGoals}</div>
<small class="text-muted">活跃目标</small>
</div>
<div class="col-6">
<div class="h4 mb-1">${availableTools}</div>
<small class="text-muted">可用工具</small>
</div>
</div>
`;
} else {
statusDiv.innerHTML = `
<div class="text-center">
<i class="fas fa-exclamation-triangle text-warning fa-2x mb-2"></i>
<div>Agent服务不可用</div>
</div>
`;
}
} catch (error) {
console.error('加载Agent状态失败:', error);
document.getElementById('agent-status').innerHTML = `
<div class="text-center">
<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2"></i>
<div>加载状态失败</div>
</div>
`;
}
}
async loadActionHistory() {
try {
const response = await fetch('/api/agent/action-history?limit=20');
const data = await response.json();
const historyDiv = document.getElementById('action-history');
if (data.success && data.history.length > 0) {
let html = '<div class="table-responsive"><table class="table table-sm">';
html += '<thead><tr><th>时间</th><th>动作</th><th>状态</th><th>详情</th></tr></thead><tbody>';
data.history.forEach(action => {
const timestamp = new Date(action.timestamp).toLocaleString();
const statusClass = action.success ? 'text-success' : 'text-danger';
const statusText = action.success ? '成功' : '失败';
html += `<tr>
<td>${timestamp}</td>
<td>${action.action_type || '未知'}</td>
<td><span class="${statusClass}">${statusText}</span></td>
<td><small class="text-muted">${action.details || ''}</small></td>
</tr>`;
});
html += '</tbody></table></div>';
historyDiv.innerHTML = html;
} else {
historyDiv.innerHTML = '<div class="text-muted text-center">暂无执行历史</div>';
}
} catch (error) {
console.error('加载执行历史失败:', error);
document.getElementById('action-history').innerHTML = '<div class="text-danger text-center">加载历史失败</div>';
}
}
async loadToolsStats() {
try {
const response = await fetch('/api/agent/tools/stats');
const data = await response.json();
const statsDiv = document.getElementById('tools-stats');
if (data.success) {
const tools = data.tools || [];
const performance = data.performance || {};
let html = `<div class="mb-2"><strong>工具数量:</strong> ${tools.length}</div>`;
if (tools.length > 0) {
html += '<div class="small"><strong>可用工具:</strong></div><ul class="list-unstyled small">';
tools.slice(0, 5).forEach(tool => {
html += `<li>• ${tool.name}</li>`;
});
if (tools.length > 5) {
html += `<li class="text-muted">... 还有 ${tools.length - 5} 个工具</li>`;
}
html += '</ul>';
}
statsDiv.innerHTML = html;
} else {
statsDiv.innerHTML = '<div class="text-muted">获取工具统计失败</div>';
}
} catch (error) {
console.error('加载工具统计失败:', error);
document.getElementById('tools-stats').innerHTML = '<div class="text-danger">加载失败</div>';
}
}
async loadLLMStats() {
try {
const response = await fetch('/api/agent/llm-stats');
const data = await response.json();
const statsDiv = document.getElementById('llm-stats');
if (data.success) {
const stats = data.stats || {};
let html = '';
if (stats.total_requests) {
html += `<div class="mb-1"><strong>总请求数:</strong> ${stats.total_requests}</div>`;
}
if (stats.success_rate !== undefined) {
html += `<div class="mb-1"><strong>成功率:</strong> ${(stats.success_rate * 100).toFixed(1)}%</div>`;
}
if (stats.average_response_time) {
html += `<div class="mb-1"><strong>平均响应时间:</strong> ${stats.average_response_time.toFixed(2)}s</div>`;
}
if (!html) {
html = '<div class="text-muted">暂无统计数据</div>';
}
statsDiv.innerHTML = html;
} else {
statsDiv.innerHTML = '<div class="text-muted">获取LLM统计失败</div>';
}
} catch (error) {
console.error('加载LLM统计失败:', error);
document.getElementById('llm-stats').innerHTML = '<div class="text-danger">加载失败</div>';
}
}
async startMonitoring() {
try {
const response = await fetch('/api/agent/monitoring/start', { method: 'POST' });
const data = await response.json();
if (data.success) {
if (window.showToast) {
window.showToast('监控已启动', 'success');
}
this.loadAgentStatus();
} else {
if (window.showToast) {
window.showToast(data.message || '启动监控失败', 'error');
}
}
} catch (error) {
console.error('启动监控失败:', error);
if (window.showToast) {
window.showToast('网络错误', 'error');
}
}
}
async stopMonitoring() {
try {
const response = await fetch('/api/agent/monitoring/stop', { method: 'POST' });
const data = await response.json();
if (data.success) {
if (window.showToast) {
window.showToast('监控已停止', 'success');
}
this.loadAgentStatus();
} else {
if (window.showToast) {
window.showToast(data.message || '停止监控失败', 'error');
}
}
} catch (error) {
console.error('停止监控失败:', error);
if (window.showToast) {
window.showToast('网络错误', 'error');
}
}
}
async runAnalysis() {
try {
const response = await fetch('/api/agent/intelligent-analysis', { method: 'POST' });
const data = await response.json();
if (data.success) {
if (window.showToast) {
window.showToast('智能分析完成', 'success');
}
this.loadActionHistory();
} else {
if (window.showToast) {
window.showToast('分析失败', 'error');
}
}
} catch (error) {
console.error('运行分析失败:', error);
if (window.showToast) {
window.showToast('网络错误', 'error');
}
}
}
async triggerSampleActions() {
try {
const response = await fetch('/api/agent/trigger-sample', { method: 'POST' });
const data = await response.json();
if (data.success) {
if (window.showToast) {
window.showToast('示例动作已触发', 'success');
}
this.loadActionHistory();
} else {
if (window.showToast) {
window.showToast('触发示例动作失败', 'error');
}
}
} catch (error) {
console.error('触发示例动作失败:', error);
if (window.showToast) {
window.showToast('网络错误', 'error');
}
}
}
async sendChatMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) {
return;
}
// 添加用户消息到界面
this.addMessageToChat('user', message);
input.value = '';
try {
const response = await fetch('/api/agent/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: message,
context: { user_id: 'admin' }
})
});
const data = await response.json();
if (data.success) {
// 添加Agent回复到界面
this.addMessageToChat('agent', data.response);
} else {
this.addMessageToChat('agent', '抱歉,处理您的请求时出现错误。');
}
} catch (error) {
console.error('发送消息失败:', error);
this.addMessageToChat('agent', '网络错误,请稍后重试。');
}
}
addMessageToChat(sender, message) {
const chatMessages = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message ${sender === 'user' ? 'text-end' : ''}`;
const time = new Date().toLocaleTimeString();
messageDiv.innerHTML = `
<div class="d-inline-block p-2 mb-2 rounded ${sender === 'user' ? 'bg-primary text-white' : 'bg-light'}">
<div class="small">${message}</div>
<div class="small opacity-75">${time}</div>
</div>
`;
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
async clearHistory() {
if (!confirm('确定要清空所有执行历史吗?')) {
return;
}
try {
const response = await fetch('/api/agent/clear-history', { method: 'POST' });
const data = await response.json();
if (data.success) {
if (window.showToast) {
window.showToast('历史已清空', 'success');
}
this.loadActionHistory();
} else {
if (window.showToast) {
window.showToast('清空历史失败', 'error');
}
}
} catch (error) {
console.error('清空历史失败:', error);
if (window.showToast) {
window.showToast('网络错误', 'error');
}
}
}
showError(error) {
this.container.innerHTML = `
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
<h4>页面加载失败</h4>
<p class="text-muted">${error.message || '未知错误'}</p>
<button class="btn btn-primary" onclick="location.reload()">
<i class="fas fa-redo me-2"></i>重新加载
</button>
</div>
</div>
</div>
</div>
`;
}
destroy() {
if (this.statusInterval) {
clearInterval(this.statusInterval);
}
}
}

View File

@@ -0,0 +1,738 @@
/**
* 预警管理页面组件
*/
import { api } from '../core/api.js';
import { formatDate, formatRelativeTime } from '../core/utils.js';
import { confirm, alert } from '../components/modal.js';
import store from '../core/store.js';
export default class Alerts {
constructor(container, route) {
this.container = container;
this.route = route;
this.filters = {
level: '',
status: '',
type: '',
page: 1,
per_page: 10
};
this.init();
}
async init() {
try {
this.render();
await this.loadData();
this.bindEvents();
} catch (error) {
console.error('Alerts page init error:', error);
this.showError(error);
}
}
render() {
this.container.innerHTML = `
<div class="page-header">
<div>
<h1 class="page-title">预警管理</h1>
<p class="page-subtitle">系统预警规则与实时监控</p>
</div>
<div class="page-actions">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#ruleModal">
<i class="fas fa-plus me-2"></i>添加规则
</button>
<button class="btn btn-success" id="check-alerts">
<i class="fas fa-search me-2"></i>检查预警
</button>
</div>
</div>
<!-- 预警统计 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stat-card danger">
<div class="stat-content">
<div class="stat-number" id="critical-alerts">0</div>
<div class="stat-label">严重预警</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card warning">
<div class="stat-content">
<div class="stat-number" id="warning-alerts">0</div>
<div class="stat-label">警告预警</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card info">
<div class="stat-content">
<div class="stat-number" id="info-alerts">0</div>
<div class="stat-label">信息预警</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="stat-content">
<div class="stat-number" id="total-alerts">0</div>
<div class="stat-label">总预警数</div>
</div>
</div>
</div>
</div>
<!-- 监控控制 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">监控控制</h5>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-6">
<div class="d-flex align-items-center">
<span class="me-3">监控状态:</span>
<span class="status-indicator" id="monitor-status">
<span id="monitor-text">检查中...</span>
</span>
</div>
</div>
<div class="col-md-6 text-end">
<button class="btn btn-success me-2" id="start-monitor">
<i class="fas fa-play me-1"></i>启动监控
</button>
<button class="btn btn-danger" id="stop-monitor">
<i class="fas fa-stop me-1"></i>停止监控
</button>
</div>
</div>
</div>
</div>
<!-- 预警规则 -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">预警规则</h5>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#ruleModal">
<i class="fas fa-plus me-1"></i>添加规则
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>规则名称</th>
<th>类型</th>
<th>级别</th>
<th>阈值</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="rules-table">
<tr>
<td colspan="6" class="text-center">
<div class="spinner spinner-sm"></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 预警列表 -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">预警历史</h5>
</div>
<div class="card-body">
<!-- 筛选器 -->
<div class="row mb-3">
<div class="col-md-3">
<select class="form-select" id="filter-level">
<option value="">所有级别</option>
<option value="critical">严重</option>
<option value="error">错误</option>
<option value="warning">警告</option>
<option value="info">信息</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="filter-status">
<option value="">所有状态</option>
<option value="active">活跃</option>
<option value="resolved">已解决</option>
</select>
</div>
<div class="col-md-4">
<input type="text" class="form-control" id="filter-search" placeholder="搜索预警...">
</div>
<div class="col-md-2">
<button class="btn btn-outline-secondary w-100" id="reset-filters">
重置
</button>
</div>
</div>
<!-- 预警列表 -->
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>时间</th>
<th>级别</th>
<th>规则</th>
<th>消息</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="alerts-table">
<tr>
<td colspan="6" class="text-center">
<div class="spinner spinner-sm"></div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<nav class="mt-3">
<ul class="pagination justify-content-center" id="pagination">
<!-- 分页将在这里生成 -->
</ul>
</nav>
</div>
</div>
`;
// 渲染规则模态框
this.renderRuleModal();
}
renderRuleModal() {
const modalHTML = `
<div class="modal fade" id="ruleModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加预警规则</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="rule-form">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">规则名称</label>
<input type="text" class="form-control" name="name" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">预警类型</label>
<select class="form-select" name="alert_type" required>
<option value="performance">性能预警</option>
<option value="quality">质量预警</option>
<option value="volume">量级预警</option>
<option value="system">系统预警</option>
<option value="business">业务预警</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">预警级别</label>
<select class="form-select" name="level" required>
<option value="info">信息</option>
<option value="warning">警告</option>
<option value="error">错误</option>
<option value="critical">严重</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">阈值</label>
<input type="number" class="form-control" name="threshold" step="0.01" required>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">规则描述</label>
<textarea class="form-control" name="description" rows="2"></textarea>
</div>
<div class="mb-3">
<label class="form-label">条件表达式</label>
<input type="text" class="form-control" name="condition"
placeholder="例如: satisfaction_avg < threshold" required>
</div>
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">检查间隔(秒)</label>
<input type="number" class="form-control" name="check_interval" value="300">
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">冷却时间(秒)</label>
<input type="number" class="form-control" name="cooldown" value="3600">
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<div class="form-check mt-4">
<input class="form-check-input" type="checkbox" name="enabled" checked>
<label class="form-check-label">启用规则</label>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="save-rule">保存规则</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
async loadData() {
try {
// 并行加载数据
const [alertsRes, rulesRes, statistics, monitorStatus] = await Promise.all([
api.alerts.list(this.filters),
api.rules.list(),
api.alerts.statistics(),
api.monitor.status()
]);
// 更新统计数据
this.updateStatistics(statistics);
// 更新监控状态
this.updateMonitorStatus(monitorStatus);
// 更新规则列表
this.updateRulesList(rulesRes);
// 更新预警列表
this.updateAlertsList(alertsRes);
} catch (error) {
console.error('Load alerts data error:', error);
this.showError(error);
}
}
updateStatistics(statistics) {
document.getElementById('critical-alerts').textContent = statistics.critical || 0;
document.getElementById('warning-alerts').textContent = statistics.warning || 0;
document.getElementById('info-alerts').textContent = statistics.info || 0;
document.getElementById('total-alerts').textContent = statistics.total || 0;
}
updateMonitorStatus(status) {
const statusEl = document.getElementById('monitor-status');
const textEl = document.getElementById('monitor-text');
if (status.status === 'running') {
statusEl.className = 'status-indicator online';
textEl.textContent = '监控运行中';
} else {
statusEl.className = 'status-indicator offline';
textEl.textContent = '监控已停止';
}
}
updateRulesList(rules) {
const tbody = document.getElementById('rules-table');
if (!tbody) return;
if (!rules || rules.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted">暂无预警规则</td>
</tr>
`;
return;
}
tbody.innerHTML = rules.map(rule => `
<tr>
<td>${rule.name}</td>
<td><span class="badge bg-secondary">${rule.alert_type}</span></td>
<td><span class="badge bg-${this.getLevelColor(rule.level)}">${rule.level}</span></td>
<td>${rule.threshold}</td>
<td>
<span class="status-indicator ${rule.enabled ? 'online' : 'offline'}">
${rule.enabled ? '启用' : '禁用'}
</span>
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="alertsPage.editRule('${rule.name}')">
编辑
</button>
<button class="btn btn-outline-danger" onclick="alertsPage.deleteRule('${rule.name}')">
删除
</button>
</div>
</td>
</tr>
`).join('');
}
updateAlertsList(response) {
const tbody = document.getElementById('alerts-table');
if (!tbody) return;
const alerts = response.alerts || [];
if (alerts.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted">暂无预警记录</td>
</tr>
`;
return;
}
tbody.innerHTML = alerts.map(alert => `
<tr>
<td>${formatDate(alert.created_at, 'MM-DD HH:mm')}</td>
<td><span class="badge bg-${this.getLevelColor(alert.level)}">${alert.level}</span></td>
<td>${alert.rule_name}</td>
<td>${alert.message}</td>
<td>
<span class="status-indicator ${alert.is_active ? 'online' : 'offline'}">
${alert.is_active ? '活跃' : '已解决'}
</span>
</td>
<td>
${alert.is_active ? `
<button class="btn btn-sm btn-success" onclick="alertsPage.resolveAlert('${alert.id}')">
解决
</button>
` : '-'}
</td>
</tr>
`).join('');
// 更新分页
this.updatePagination(response);
}
updatePagination(response) {
const pagination = document.getElementById('pagination');
if (!pagination) return;
const { page = 1, total_pages = 1 } = response;
if (total_pages <= 1) {
pagination.innerHTML = '';
return;
}
let html = '';
// 上一页
if (page > 1) {
html += `<li class="page-item">
<a class="page-link" href="#" data-page="${page - 1}">上一页</a>
</li>`;
}
// 页码
for (let i = Math.max(1, page - 2); i <= Math.min(total_pages, page + 2); i++) {
html += `<li class="page-item ${i === page ? 'active' : ''}">
<a class="page-link" href="#" data-page="${i}">${i}</a>
</li>`;
}
// 下一页
if (page < total_pages) {
html += `<li class="page-item">
<a class="page-link" href="#" data-page="${page + 1}">下一页</a>
</li>`;
}
pagination.innerHTML = html;
}
getLevelColor(level) {
const colors = {
'critical': 'danger',
'error': 'danger',
'warning': 'warning',
'info': 'info'
};
return colors[level] || 'secondary';
}
bindEvents() {
// 监控控制
document.getElementById('start-monitor')?.addEventListener('click', () => {
this.startMonitor();
});
document.getElementById('stop-monitor')?.addEventListener('click', () => {
this.stopMonitor();
});
document.getElementById('check-alerts')?.addEventListener('click', () => {
this.checkAlerts();
});
// 规则表单
document.getElementById('save-rule')?.addEventListener('click', () => {
this.saveRule();
});
// 筛选器
document.getElementById('filter-level')?.addEventListener('change', (e) => {
this.filters.level = e.target.value;
this.filters.page = 1;
this.loadAlerts();
});
document.getElementById('filter-status')?.addEventListener('change', (e) => {
this.filters.status = e.target.value === 'active' ? 'active' :
e.target.value === 'resolved' ? 'resolved' : '';
this.filters.page = 1;
this.loadAlerts();
});
document.getElementById('filter-search')?.addEventListener('input',
this.debounce((e) => {
this.filters.search = e.target.value;
this.filters.page = 1;
this.loadAlerts();
}, 300)
);
document.getElementById('reset-filters')?.addEventListener('click', () => {
this.resetFilters();
});
// 分页
document.getElementById('pagination')?.addEventListener('click', (e) => {
if (e.target.classList.contains('page-link')) {
e.preventDefault();
const page = parseInt(e.target.dataset.page);
if (page) {
this.filters.page = page;
this.loadAlerts();
}
}
});
}
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
async startMonitor() {
try {
await api.monitor.start();
await this.loadData();
store.dispatch('showToast', {
type: 'success',
message: '监控已启动'
});
} catch (error) {
console.error('Start monitor error:', error);
store.dispatch('showToast', {
type: 'error',
message: '启动监控失败'
});
}
}
async stopMonitor() {
try {
await api.monitor.stop();
await this.loadData();
store.dispatch('showToast', {
type: 'success',
message: '监控已停止'
});
} catch (error) {
console.error('Stop monitor error:', error);
store.dispatch('showToast', {
type: 'error',
message: '停止监控失败'
});
}
}
async checkAlerts() {
try {
const result = await api.monitor.checkAlerts();
await this.loadData();
store.dispatch('showToast', {
type: 'success',
message: `检查完成,发现 ${result.alerts_count || 0} 个新预警`
});
} catch (error) {
console.error('Check alerts error:', error);
store.dispatch('showToast', {
type: 'error',
message: '检查预警失败'
});
}
}
async saveRule() {
const form = document.getElementById('rule-form');
const formData = new FormData(form);
const data = {
name: formData.get('name'),
alert_type: formData.get('alert_type'),
level: formData.get('level'),
threshold: parseFloat(formData.get('threshold')),
description: formData.get('description'),
condition: formData.get('condition'),
check_interval: parseInt(formData.get('check_interval')),
cooldown: parseInt(formData.get('cooldown')),
enabled: formData.has('enabled')
};
try {
await api.rules.create(data);
await this.loadData();
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('ruleModal'));
modal.hide();
// 重置表单
form.reset();
store.dispatch('showToast', {
type: 'success',
message: '规则创建成功'
});
} catch (error) {
console.error('Save rule error:', error);
store.dispatch('showToast', {
type: 'error',
message: '创建规则失败'
});
}
}
async editRule(name) {
// TODO: 实现编辑规则功能
store.dispatch('showToast', {
type: 'info',
message: '编辑功能开发中'
});
}
async deleteRule(name) {
const confirmed = await confirm({
title: '删除规则',
message: `确定要删除规则 "${name}" 吗?`
});
if (!confirmed) return;
try {
await api.rules.delete(name);
await this.loadData();
store.dispatch('showToast', {
type: 'success',
message: '规则已删除'
});
} catch (error) {
console.error('Delete rule error:', error);
store.dispatch('showToast', {
type: 'error',
message: '删除规则失败'
});
}
}
async resolveAlert(alertId) {
try {
await api.alerts.resolve(alertId, { resolved_by: 'admin', resolution: '手动解决' });
await this.loadAlerts();
store.dispatch('showToast', {
type: 'success',
message: '预警已解决'
});
} catch (error) {
console.error('Resolve alert error:', error);
store.dispatch('showToast', {
type: 'error',
message: '解决预警失败'
});
}
}
resetFilters() {
this.filters = {
level: '',
status: '',
search: '',
page: 1,
per_page: 10
};
// 重置筛选器UI
document.getElementById('filter-level').value = '';
document.getElementById('filter-status').value = '';
document.getElementById('filter-search').value = '';
// 重新加载数据
this.loadAlerts();
}
async loadAlerts() {
try {
const response = await api.alerts.list(this.filters);
this.updateAlertsList(response);
} catch (error) {
console.error('Load alerts error:', error);
}
}
showError(error) {
this.container.innerHTML = `
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">加载失败</h4>
<p>${error.message || '未知错误'}</p>
<hr>
<button class="btn btn-outline-danger" onclick="location.reload()">
重新加载
</button>
</div>
`;
}
}
// 暴露到全局供内联事件使用
window.alertsPage = null;

View File

@@ -0,0 +1,33 @@
/**
* 聊天页面组件
*/
export default class Chat {
constructor(container, route) {
this.container = container;
this.route = route;
this.init();
}
async init() {
this.render();
}
render() {
this.container.innerHTML = `
<div class="page-header">
<h1 class="page-title">智能对话</h1>
<p class="page-subtitle">WebSocket实时聊天</p>
</div>
<div class="card">
<div class="card-body">
<div class="text-center py-5">
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
<h4 class="text-muted">聊天页面</h4>
<p class="text-muted">该功能正在开发中...</p>
</div>
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,454 @@
/**
* 仪表板页面组件
*/
import { api } from '../core/api.js';
import { formatDate, formatRelativeTime } from '../core/utils.js';
import store from '../core/store.js';
export default class Dashboard {
constructor(container, route) {
this.container = container;
this.route = route;
this.charts = {};
this.init();
}
async init() {
try {
this.render();
await this.loadData();
this.bindEvents();
} catch (error) {
console.error('Dashboard init error:', error);
this.showError(error);
}
}
render() {
this.container.innerHTML = `
<div class="page-container">
<div class="page-header">
<div>
<h1 class="page-title">仪表板</h1>
<p class="page-subtitle">系统概览与实时监控</p>
</div>
<div class="page-actions">
<button class="btn btn-primary" id="refresh-dashboard">
<i class="fas fa-sync-alt me-2"></i>刷新
</button>
</div>
</div>
<div class="page-content">
<!-- 统计卡片 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stat-card success">
<div class="stat-content">
<div class="stat-number" id="resolved-orders">0</div>
<div class="stat-label">今日已解决工单</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card warning">
<div class="stat-content">
<div class="stat-number" id="pending-orders">0</div>
<div class="stat-label">待处理工单</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card info">
<div class="stat-content">
<div class="stat-number" id="active-alerts">0</div>
<div class="stat-label">活跃预警</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="stat-content">
<div class="stat-number" id="satisfaction-rate">0%</div>
<div class="stat-label">满意度</div>
</div>
</div>
</div>
</div>
<!-- 图表区域 -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">工单趋势</h5>
<select class="form-select form-select-sm" id="trend-period" style="width: auto;">
<option value="7">最近7天</option>
<option value="30" selected>最近30天</option>
<option value="90">最近90天</option>
</select>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="orders-trend-chart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">工单分类分布</h5>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="category-chart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- 最新活动 -->
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">最新工单</h5>
<a href="/workorders" class="btn btn-sm btn-outline-primary">查看全部</a>
</div>
<div class="card-body">
<div class="list-group list-group-flush" id="recent-workorders">
<div class="text-center py-3">
<div class="spinner spinner-sm"></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">最新预警</h5>
<a href="/alerts" class="btn btn-sm btn-outline-primary">查看全部</a>
</div>
<div class="card-body">
<div class="list-group list-group-flush" id="recent-alerts">
<div class="text-center py-3">
<div class="spinner spinner-sm"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
}
async loadData() {
try {
// 加载仪表板数据
const [analytics, recentWorkorders, recentAlerts, health] = await Promise.all([
api.monitor.analytics({ days: 30 }),
api.workorders.list({ page: 1, per_page: 5, sort: 'created_at', order: 'desc' }),
api.alerts.list({ page: 1, per_page: 5, sort: 'created_at', order: 'desc' }),
api.system.health()
]);
// 更新统计数据
this.updateStats(analytics, health);
// 更新图表
this.updateCharts(analytics);
// 更新最新工单列表
this.updateRecentWorkorders(recentWorkorders.workorders || []);
// 更新最新预警列表
this.updateRecentAlerts(recentAlerts.alerts || []);
} catch (error) {
console.error('Load dashboard data error:', error);
this.showError(error);
}
}
updateStats(analytics, health) {
// 更新统计卡片
const stats = analytics.data || {};
document.getElementById('resolved-orders').textContent = stats.resolved_orders || 0;
document.getElementById('pending-orders').textContent = health.open_workorders || 0;
document.getElementById('active-alerts').textContent = Object.values(health.active_alerts_by_level || {}).reduce((a, b) => a + b, 0);
const satisfaction = stats.satisfaction_avg ? (stats.satisfaction_avg * 100).toFixed(1) : 0;
document.getElementById('satisfaction-rate').textContent = `${satisfaction}%`;
}
updateCharts(analytics) {
const data = analytics.data || {};
// 更新工单趋势图
this.updateTrendChart(data.trend || []);
// 更新分类分布图
this.updateCategoryChart(data.categories || {});
}
updateTrendChart(trendData) {
const ctx = document.getElementById('orders-trend-chart');
if (!ctx) return;
// 销毁旧图表
if (this.charts.trend) {
this.charts.trend.destroy();
}
this.charts.trend = new Chart(ctx, {
type: 'line',
data: {
labels: trendData.map(item => item.date),
datasets: [{
label: '工单数',
data: trendData.map(item => item.count),
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
updateCategoryChart(categoryData) {
const ctx = document.getElementById('category-chart');
if (!ctx) return;
// 销毁旧图表
if (this.charts.category) {
this.charts.category.destroy();
}
const labels = Object.keys(categoryData);
const data = Object.values(categoryData);
this.charts.category = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: [
'#007bff',
'#28a745',
'#ffc107',
'#dc3545',
'#6c757d'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
updateRecentWorkorders(workorders) {
const container = document.getElementById('recent-workorders');
if (!container) return;
if (!workorders.length) {
container.innerHTML = `
<div class="text-center py-3 text-muted">
<i class="fas fa-inbox fa-2x mb-2"></i>
<p>暂无工单</p>
</div>
`;
return;
}
container.innerHTML = workorders.map(workorder => `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">
<a href="/workorders/${workorder.id}" class="text-decoration-none">
${workorder.title}
</a>
</h6>
<small class="text-muted">
${workorder.category || '未分类'}
${formatRelativeTime(workorder.created_at)}
</small>
</div>
<span class="badge bg-${this.getStatusColor(workorder.status)}">
${this.getStatusText(workorder.status)}
</span>
</div>
</div>
`).join('');
}
updateRecentAlerts(alerts) {
const container = document.getElementById('recent-alerts');
if (!container) return;
if (!alerts.length) {
container.innerHTML = `
<div class="text-center py-3 text-muted">
<i class="fas fa-check-circle fa-2x mb-2"></i>
<p>暂无预警</p>
</div>
`;
return;
}
container.innerHTML = alerts.map(alert => `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">${alert.message}</h6>
<small class="text-muted">
${alert.rule_name}
${formatRelativeTime(alert.created_at)}
</small>
</div>
<span class="badge bg-${this.getLevelColor(alert.level)}">
${alert.level}
</span>
</div>
</div>
`).join('');
}
getStatusColor(status) {
const colors = {
'open': 'danger',
'in_progress': 'warning',
'resolved': 'success',
'closed': 'secondary'
};
return colors[status] || 'secondary';
}
getStatusText(status) {
const texts = {
'open': '待处理',
'in_progress': '处理中',
'resolved': '已解决',
'closed': '已关闭'
};
return texts[status] || status;
}
getLevelColor(level) {
const colors = {
'critical': 'danger',
'error': 'danger',
'warning': 'warning',
'info': 'info'
};
return colors[level] || 'secondary';
}
bindEvents() {
// 刷新按钮
const refreshBtn = document.getElementById('refresh-dashboard');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.refresh();
});
}
// 趋势周期选择
const periodSelect = document.getElementById('trend-period');
if (periodSelect) {
periodSelect.addEventListener('change', (e) => {
this.changeTrendPeriod(e.target.value);
});
}
// 定时刷新每5分钟
this.refreshTimer = setInterval(() => {
this.refresh();
}, 5 * 60 * 1000);
}
async refresh() {
const refreshBtn = document.getElementById('refresh-dashboard');
if (refreshBtn) {
refreshBtn.disabled = true;
const icon = refreshBtn.querySelector('i');
icon.classList.add('fa-spin');
}
try {
await this.loadData();
} catch (error) {
console.error('Refresh error:', error);
} finally {
if (refreshBtn) {
refreshBtn.disabled = false;
const icon = refreshBtn.querySelector('i');
icon.classList.remove('fa-spin');
}
}
}
async changeTrendPeriod(days) {
try {
const analytics = await api.monitor.analytics({ days: parseInt(days) });
this.updateTrendChart(analytics.data?.trend || []);
} catch (error) {
console.error('Change trend period error:', error);
}
}
showError(error) {
this.container.innerHTML = `
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">加载失败</h4>
<p>${error.message || '未知错误'}</p>
<hr>
<button class="btn btn-outline-danger" onclick="location.reload()">
重新加载
</button>
</div>
`;
}
destroy() {
// 清理图表
Object.values(this.charts).forEach(chart => {
if (chart) chart.destroy();
});
// 清理定时器
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
}
}
}

View File

@@ -0,0 +1,451 @@
/**
* 飞书同步页面组件
*/
export default class Feishu {
constructor(container, route) {
this.container = container;
this.route = route;
this.init();
}
async init() {
try {
this.render();
this.bindEvents();
this.loadSyncStatus();
} catch (error) {
console.error('Feishu init error:', error);
this.showError(error);
}
}
render() {
this.container.innerHTML = `
<div class="page-header">
<div>
<h1 class="page-title">飞书同步</h1>
<p class="page-subtitle">与飞书多维表格进行数据同步</p>
</div>
</div>
<div class="row">
<!-- 同步配置 -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-cog me-2"></i>同步配置
</h5>
</div>
<div class="card-body">
<form id="feishu-config-form">
<div class="mb-3">
<label for="app_id" class="form-label">App ID</label>
<input type="text" class="form-control" id="app_id" name="app_id"
placeholder="飞书应用的App ID">
</div>
<div class="mb-3">
<label for="app_secret" class="form-label">App Secret</label>
<input type="password" class="form-control" id="app_secret" name="app_secret"
placeholder="飞书应用的App Secret">
</div>
<div class="mb-3">
<label for="app_token" class="form-label">App Token</label>
<input type="text" class="form-control" id="app_token" name="app_token"
placeholder="多维表格的App Token">
</div>
<div class="mb-3">
<label for="table_id" class="form-label">Table ID</label>
<input type="text" class="form-control" id="table_id" name="table_id"
placeholder="数据表的Table ID">
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary" id="save-config-btn">
<i class="fas fa-save me-2"></i>保存配置
</button>
<button type="button" class="btn btn-outline-secondary" id="test-connection-btn">
<i class="fas fa-plug me-2"></i>测试连接
</button>
</div>
</form>
</div>
</div>
</div>
<!-- 同步状态 -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>同步状态
</h5>
</div>
<div class="card-body">
<div id="sync-status" class="text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>加载中...
</div>
</div>
</div>
<!-- 同步操作 -->
<div class="card mt-3">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-sync me-2"></i>同步操作
</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<button class="btn btn-success" id="sync-from-feishu-btn">
<i class="fas fa-download me-2"></i>从飞书同步
</button>
<button class="btn btn-primary" id="preview-data-btn">
<i class="fas fa-eye me-2"></i>预览飞书数据
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 字段映射 -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-link me-2"></i>字段映射
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<button class="btn btn-outline-primary mb-3" id="discover-fields-btn">
<i class="fas fa-search me-2"></i>发现字段
</button>
<div id="field-discovery-result"></div>
</div>
<div class="col-md-6">
<button class="btn btn-outline-secondary mb-3" id="mapping-status-btn">
<i class="fas fa-list me-2"></i>映射状态
</button>
<div id="mapping-status-result"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 数据预览 -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-table me-2"></i>数据预览
</h5>
</div>
<div class="card-body">
<div id="data-preview" class="text-muted">
点击"预览飞书数据"查看数据
</div>
</div>
</div>
</div>
</div>
`;
}
bindEvents() {
// 配置表单提交
const configForm = document.getElementById('feishu-config-form');
configForm.addEventListener('submit', (e) => {
e.preventDefault();
this.saveConfig();
});
// 测试连接
document.getElementById('test-connection-btn').addEventListener('click', () => {
this.testConnection();
});
// 从飞书同步
document.getElementById('sync-from-feishu-btn').addEventListener('click', () => {
this.syncFromFeishu();
});
// 预览数据
document.getElementById('preview-data-btn').addEventListener('click', () => {
this.previewData();
});
// 发现字段
document.getElementById('discover-fields-btn').addEventListener('click', () => {
this.discoverFields();
});
// 映射状态
document.getElementById('mapping-status-btn').addEventListener('click', () => {
this.getMappingStatus();
});
// 加载配置
this.loadConfig();
}
async loadConfig() {
try {
const response = await fetch('/api/feishu-sync/config');
const data = await response.json();
if (data.success) {
document.getElementById('app_id').value = data.config.app_id || '';
document.getElementById('app_secret').value = data.config.app_secret || '';
document.getElementById('app_token').value = data.config.app_token || '';
document.getElementById('table_id').value = data.config.table_id || '';
}
} catch (error) {
console.error('加载配置失败:', error);
}
}
async saveConfig() {
const formData = new FormData(document.getElementById('feishu-config-form'));
const config = {
app_id: formData.get('app_id'),
app_secret: formData.get('app_secret'),
app_token: formData.get('app_token'),
table_id: formData.get('table_id')
};
try {
const response = await fetch('/api/feishu-sync/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const data = await response.json();
if (data.success) {
if (window.showToast) {
window.showToast('配置保存成功', 'success');
}
} else {
if (window.showToast) {
window.showToast(data.error || '配置保存失败', 'error');
}
}
} catch (error) {
console.error('保存配置失败:', error);
if (window.showToast) {
window.showToast('网络错误', 'error');
}
}
}
async testConnection() {
try {
const response = await fetch('/api/feishu-sync/test-connection');
const data = await response.json();
if (data.success) {
if (window.showToast) {
window.showToast('连接测试成功', 'success');
}
// 显示字段信息
if (data.fields) {
console.log('飞书表格字段:', data.fields);
}
} else {
if (window.showToast) {
window.showToast(data.message || '连接测试失败', 'error');
}
}
} catch (error) {
console.error('测试连接失败:', error);
if (window.showToast) {
window.showToast('网络错误', 'error');
}
}
}
async loadSyncStatus() {
try {
const response = await fetch('/api/feishu-sync/status');
const data = await response.json();
const statusDiv = document.getElementById('sync-status');
if (data.success) {
const status = data.status;
statusDiv.innerHTML = `
<div class="mb-2">
<strong>最后同步:</strong> ${status.last_sync || '从未同步'}
</div>
<div class="mb-2">
<strong>同步状态:</strong> ${status.is_syncing ? '同步中' : '空闲'}
</div>
<div class="mb-2">
<strong>总记录数:</strong> ${status.total_records || 0}
</div>
`;
} else {
statusDiv.innerHTML = '<span class="text-danger">获取状态失败</span>';
}
} catch (error) {
console.error('获取同步状态失败:', error);
document.getElementById('sync-status').innerHTML = '<span class="text-danger">获取状态失败</span>';
}
}
async syncFromFeishu() {
try {
const response = await fetch('/api/feishu-sync/sync-from-feishu', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
generate_ai_suggestions: true,
limit: 50
})
});
const data = await response.json();
if (data.success) {
if (window.showToast) {
window.showToast(data.message, 'success');
}
this.loadSyncStatus(); // 重新加载状态
} else {
if (window.showToast) {
window.showToast(data.error || '同步失败', 'error');
}
}
} catch (error) {
console.error('同步失败:', error);
if (window.showToast) {
window.showToast('网络错误', 'error');
}
}
}
async previewData() {
try {
const response = await fetch('/api/feishu-sync/preview-feishu-data');
const data = await response.json();
const previewDiv = document.getElementById('data-preview');
if (data.success && data.preview_data.length > 0) {
let html = `<div class="table-responsive"><table class="table table-sm">`;
html += '<thead><tr><th>记录ID</th><th>字段数据</th></tr></thead><tbody>';
data.preview_data.forEach(item => {
html += `<tr>
<td>${item.record_id}</td>
<td><pre class="small">${JSON.stringify(item.fields, null, 2)}</pre></td>
</tr>`;
});
html += '</tbody></table></div>';
previewDiv.innerHTML = html;
} else {
previewDiv.innerHTML = '<span class="text-muted">暂无数据</span>';
}
} catch (error) {
console.error('预览数据失败:', error);
document.getElementById('data-preview').innerHTML = '<span class="text-danger">预览失败</span>';
}
}
async discoverFields() {
try {
const response = await fetch('/api/feishu-sync/field-mapping/discover', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ limit: 5 })
});
const data = await response.json();
const resultDiv = document.getElementById('field-discovery-result');
if (data.success) {
const report = data.discovery_report;
let html = '<h6>字段发现报告</h6>';
if (Object.keys(report).length > 0) {
html += '<ul class="list-group list-group-flush">';
Object.entries(report).forEach(([field, info]) => {
html += `<li class="list-group-item">
<strong>${field}</strong>: ${info.suggestion || '未知'}
<br><small class="text-muted">置信度: ${(info.confidence * 100).toFixed(1)}%</small>
</li>`;
});
html += '</ul>';
} else {
html += '<p class="text-muted">未发现可映射的字段</p>';
}
resultDiv.innerHTML = html;
} else {
resultDiv.innerHTML = '<span class="text-danger">字段发现失败</span>';
}
} catch (error) {
console.error('字段发现失败:', error);
document.getElementById('field-discovery-result').innerHTML = '<span class="text-danger">字段发现失败</span>';
}
}
async getMappingStatus() {
try {
const response = await fetch('/api/feishu-sync/field-mapping/status');
const data = await response.json();
const resultDiv = document.getElementById('mapping-status-result');
if (data.success) {
const status = data.status;
let html = '<h6>映射状态</h6>';
if (status.mappings && status.mappings.length > 0) {
html += '<ul class="list-group list-group-flush">';
status.mappings.forEach(mapping => {
html += `<li class="list-group-item">
<strong>${mapping.feishu_field}</strong> → ${mapping.local_field}
<br><small class="text-muted">优先级: ${mapping.priority}</small>
</li>`;
});
html += '</ul>';
} else {
html += '<p class="text-muted">暂无字段映射</p>';
}
resultDiv.innerHTML = html;
} else {
resultDiv.innerHTML = '<span class="text-danger">获取映射状态失败</span>';
}
} catch (error) {
console.error('获取映射状态失败:', error);
document.getElementById('mapping-status-result').innerHTML = '<span class="text-danger">获取映射状态失败</span>';
}
}
showError(error) {
this.container.innerHTML = `
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
<h4>页面加载失败</h4>
<p class="text-muted">${error.message || '未知错误'}</p>
<button class="btn btn-primary" onclick="location.reload()">
<i class="fas fa-redo me-2"></i>重新加载
</button>
</div>
</div>
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,33 @@
/**
* 知识库页面组件
*/
export default class Knowledge {
constructor(container, route) {
this.container = container;
this.route = route;
this.init();
}
async init() {
this.render();
}
render() {
this.container.innerHTML = `
<div class="page-header">
<h1 class="page-title">知识库</h1>
<p class="page-subtitle">知识条目管理</p>
</div>
<div class="card">
<div class="card-body">
<div class="text-center py-5">
<i class="fas fa-book fa-3x text-muted mb-3"></i>
<h4 class="text-muted">知识库页面</h4>
<p class="text-muted">该功能正在开发中...</p>
</div>
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,216 @@
/**
* 登录页面组件
*/
export default class Login {
constructor(container, route) {
this.container = container;
this.route = route;
this.init();
}
async init() {
try {
this.render();
this.bindEvents();
} catch (error) {
console.error('Login init error:', error);
this.showError(error);
}
}
render() {
this.container.innerHTML = `
<div class="page-container">
<div class="page-header">
<div>
<h1 class="page-title">用户登录</h1>
<p class="page-subtitle">请输入您的账号信息</p>
</div>
</div>
<div class="page-content">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<form id="login-form">
<!-- 用户名 -->
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-user"></i>
</span>
<input type="text" class="form-control" id="username"
name="username" required autofocus>
</div>
</div>
<!-- 密码 -->
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-lock"></i>
</span>
<input type="password" class="form-control" id="password"
name="password" required>
</div>
</div>
<!-- 记住我 -->
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember"
name="remember">
<label class="form-check-label" for="remember">
记住我
</label>
</div>
<!-- 错误提示 -->
<div id="login-error" class="alert alert-danger d-none"></div>
<!-- 提交按钮 -->
<div class="d-grid">
<button type="submit" class="btn btn-primary" id="login-btn">
<i class="fas fa-sign-in-alt me-2"></i>登录
</button>
</div>
</form>
</div>
</div>
<!-- 其他登录方式 -->
<div class="text-center mt-3">
<p class="text-muted">
还没有账号?<a href="#" class="text-decoration-none">立即注册</a>
</p>
<p class="text-muted">
<a href="#" class="text-decoration-none">忘记密码?</a>
</p>
</div>
</div>
</div>
</div>
`;
}
bindEvents() {
const form = document.getElementById('login-form');
const loginBtn = document.getElementById('login-btn');
const errorDiv = document.getElementById('login-error');
form.addEventListener('submit', async (e) => {
e.preventDefault();
// 获取表单数据
const formData = new FormData(form);
const username = formData.get('username');
const password = formData.get('password');
const remember = formData.get('remember');
// 显示加载状态
loginBtn.disabled = true;
loginBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>登录中...';
errorDiv.classList.add('d-none');
try {
// 调用登录API
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password,
remember
}),
credentials: 'same-origin'
});
const data = await response.json();
if (response.ok && data.success) {
// 登录成功
// 保存token到sessionStorage会话级别
sessionStorage.setItem('token', data.token);
// 如果选择记住我也保存到localStorage
if (remember) {
localStorage.setItem('user', JSON.stringify(data.user));
localStorage.setItem('token', data.token);
localStorage.setItem('remember', 'true');
}
// 更新应用状态
if (window.store) {
window.store.commit('SET_USER', data.user);
window.store.commit('SET_LOGIN', true);
window.store.commit('SET_TOKEN', data.token);
}
// 显示成功提示
if (window.showToast) {
window.showToast('登录成功', 'success');
}
// 跳转到仪表板
if (window.router) {
window.router.push('/');
} else {
// 如果路由器还没初始化,直接跳转
window.location.href = '/';
}
} else {
// 登录失败
errorDiv.textContent = data.message || '用户名或密码错误';
errorDiv.classList.remove('d-none');
}
} catch (error) {
console.error('Login error:', error);
errorDiv.textContent = '网络错误,请稍后重试';
errorDiv.classList.remove('d-none');
} finally {
// 恢复按钮状态
loginBtn.disabled = false;
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt me-2"></i>登录';
}
});
// 检查本地存储中的登录状态
const rememberedUser = localStorage.getItem('remember');
if (rememberedUser === 'true') {
const user = localStorage.getItem('user');
if (user) {
try {
const userData = JSON.parse(user);
document.getElementById('username').value = userData.username || '';
document.getElementById('remember').checked = true;
} catch (e) {
console.error('Error parsing remembered user:', e);
}
}
}
}
showError(error) {
this.container.innerHTML = `
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
<h4>页面加载失败</h4>
<p class="text-muted">${error.message || '未知错误'}</p>
<button class="btn btn-primary" onclick="location.reload()">
<i class="fas fa-redo me-2"></i>重新加载
</button>
</div>
</div>
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,33 @@
/**
* 系统监控页面组件
*/
export default class Monitoring {
constructor(container, route) {
this.container = container;
this.route = route;
this.init();
}
async init() {
this.render();
}
render() {
this.container.innerHTML = `
<div class="page-header">
<h1 class="page-title">系统监控</h1>
<p class="page-subtitle">系统性能与状态监控</p>
</div>
<div class="card">
<div class="card-body">
<div class="text-center py-5">
<i class="fas fa-chart-line fa-3x text-muted mb-3"></i>
<h4 class="text-muted">系统监控页面</h4>
<p class="text-muted">该功能正在开发中...</p>
</div>
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,142 @@
/**
* 404页面组件
*/
export default class NotFound {
constructor(container, route) {
this.container = container;
this.route = route;
this.init();
}
async init() {
try {
this.render();
this.bindEvents();
} catch (error) {
console.error('NotFound init error:', error);
this.showError(error);
}
}
render() {
this.container.innerHTML = `
<div class="page-header">
<div>
<h1 class="page-title">404 - 页面未找到</h1>
<p class="page-subtitle">抱歉,您访问的页面不存在</p>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-6 text-center">
<div class="card">
<div class="card-body py-5">
<div class="error-illustration mb-4">
<i class="fas fa-search fa-5x text-muted"></i>
</div>
<h4 class="mb-3">页面不存在</h4>
<p class="text-muted mb-4">
看起来您访问的页面已经被移除、名称已更改或暂时不可用。
</p>
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-primary" onclick="window.router && window.router.push('/') || (window.location.href = '/')">
<i class="fas fa-home me-2"></i>返回首页
</button>
<button class="btn btn-outline-secondary" onclick="window.history.back()">
<i class="fas fa-arrow-left me-2"></i>返回上一页
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 快速导航 -->
<div class="row mt-5">
<div class="col-12">
<h5 class="mb-3">快速导航</h5>
<div class="row">
<div class="col-md-3 col-sm-6 mb-3">
<a href="#" class="text-decoration-none">
<div class="card h-100 border-0 shadow-sm hover-shadow">
<div class="card-body text-center">
<i class="fas fa-tachometer-alt fa-2x text-primary mb-2"></i>
<h6 class="card-title">仪表板</h6>
<p class="card-text text-muted small">系统概览</p>
</div>
</div>
</a>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<a href="#" class="text-decoration-none">
<div class="card h-100 border-0 shadow-sm hover-shadow">
<div class="card-body text-center">
<i class="fas fa-tasks fa-2x text-success mb-2"></i>
<h6 class="card-title">工单管理</h6>
<p class="card-text text-muted small">工单处理</p>
</div>
</div>
</a>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<a href="#" class="text-decoration-none">
<div class="card h-100 border-0 shadow-sm hover-shadow">
<div class="card-body text-center">
<i class="fas fa-bell fa-2x text-warning mb-2"></i>
<h6 class="card-title">预警管理</h6>
<p class="card-text text-muted small">系统预警</p>
</div>
</div>
</a>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<a href="#" class="text-decoration-none">
<div class="card h-100 border-0 shadow-sm hover-shadow">
<div class="card-body text-center">
<i class="fas fa-book fa-2x text-info mb-2"></i>
<h6 class="card-title">知识库</h6>
<p class="card-text text-muted small">知识文档</p>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
`;
}
bindEvents() {
// 绑定导航链接事件
const links = this.container.querySelectorAll('a[href^="#"]');
links.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const href = link.getAttribute('href');
if (href === '#home' && window.router) {
window.router.push('/');
}
});
});
}
showError(error) {
this.container.innerHTML = `
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
<h4>页面加载失败</h4>
<p class="text-muted">${error.message || '未知错误'}</p>
<button class="btn btn-primary" onclick="location.reload()">
<i class="fas fa-redo me-2"></i>重新加载
</button>
</div>
</div>
</div>
</div>
`;
}
}

View File

@@ -0,0 +1,428 @@
/**
* 系统设置页面组件
*/
export default class Settings {
constructor(container, route) {
this.container = container;
this.route = route;
this.init();
}
async init() {
this.render();
this.bindEvents();
this.loadSettings();
}
render() {
this.container.innerHTML = `
<div class="page-header">
<div>
<h1 class="page-title">系统设置</h1>
<p class="page-subtitle">系统配置与管理</p>
</div>
</div>
<div class="row">
<!-- 系统信息 -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle me-2"></i>系统信息
</h5>
</div>
<div class="card-body">
<div id="system-info" class="text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>加载中...
</div>
</div>
</div>
</div>
<!-- 数据库状态 -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-database me-2"></i>数据库状态
</h5>
</div>
<div class="card-body">
<div id="db-status" class="text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>加载中...
</div>
</div>
</div>
</div>
</div>
<!-- 系统设置 -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-sliders-h me-2"></i>系统配置
</h5>
</div>
<div class="card-body">
<form id="system-settings-form">
<!-- 预警规则设置 -->
<div class="mb-4">
<h6>预警规则设置</h6>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">默认检查间隔 (秒)</label>
<input type="number" class="form-control" id="check-interval" min="30" max="3600">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">默认冷却时间 (秒)</label>
<input type="number" class="form-control" id="cooldown" min="60" max="86400">
</div>
</div>
</div>
</div>
<!-- LLM配置 -->
<div class="mb-4">
<h6>LLM配置</h6>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">API提供商</label>
<select class="form-select" id="llm-provider">
<option value="openai">OpenAI</option>
<option value="qwen">通义千问</option>
<option value="claude">Claude</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">默认模型</label>
<input type="text" class="form-control" id="default-model" placeholder="gpt-3.5-turbo">
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">API密钥</label>
<input type="password" class="form-control" id="api-key" placeholder="输入API密钥">
</div>
</div>
<!-- 其他设置 -->
<div class="mb-4">
<h6>其他设置</h6>
<div class="row">
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enable-analytics">
<label class="form-check-label" for="enable-analytics">
启用数据分析
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enable-websocket">
<label class="form-check-label" for="enable-websocket">
启用WebSocket实时通信
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enable-pwa">
<label class="form-check-label" for="enable-pwa">
启用PWA离线支持
</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="enable-error-reporting">
<label class="form-check-label" for="enable-error-reporting">
启用错误报告
</label>
</div>
</div>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>保存设置
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 系统操作 -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-tools me-2"></i>系统操作
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6>数据管理</h6>
<div class="d-grid gap-2">
<button class="btn btn-warning" id="backup-data-btn">
<i class="fas fa-download me-2"></i>备份数据
</button>
<button class="btn btn-danger" id="clear-cache-btn">
<i class="fas fa-trash me-2"></i>清理缓存
</button>
</div>
</div>
<div class="col-md-6">
<h6>系统维护</h6>
<div class="d-grid gap-2">
<button class="btn btn-info" id="check-health-btn">
<i class="fas fa-heartbeat me-2"></i>健康检查
</button>
<button class="btn btn-secondary" id="restart-services-btn">
<i class="fas fa-redo me-2"></i>重启服务
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
}
bindEvents() {
// 设置表单提交
const settingsForm = document.getElementById('system-settings-form');
settingsForm.addEventListener('submit', (e) => {
e.preventDefault();
this.saveSettings();
});
// 系统操作按钮
document.getElementById('backup-data-btn').addEventListener('click', () => {
this.backupData();
});
document.getElementById('clear-cache-btn').addEventListener('click', () => {
this.clearCache();
});
document.getElementById('check-health-btn').addEventListener('click', () => {
this.checkHealth();
});
document.getElementById('restart-services-btn').addEventListener('click', () => {
this.restartServices();
});
}
async loadSettings() {
try {
// 加载系统信息
const healthResponse = await fetch('/api/health');
const healthData = await healthResponse.json();
const systemInfoDiv = document.getElementById('system-info');
if (healthData) {
systemInfoDiv.innerHTML = `
<div class="mb-2"><strong>版本:</strong> ${healthData.version || '1.0.0'}</div>
<div class="mb-2"><strong>运行时间:</strong> ${this.formatUptime(healthData.uptime || 0)}</div>
<div class="mb-2"><strong>健康评分:</strong> ${healthData.health_score || 0}/100</div>
<div class="mb-2"><strong>活跃会话:</strong> ${healthData.active_sessions || 0}</div>
<div class="mb-2"><strong>处理中的工单:</strong> ${healthData.processing_workorders || 0}</div>
`;
}
// 加载数据库状态
const dbStatusDiv = document.getElementById('db-status');
if (healthData.db_status) {
const dbStatus = healthData.db_status;
dbStatusDiv.innerHTML = `
<div class="mb-2"><strong>状态:</strong> <span class="text-${dbStatus.connection_ok ? 'success' : 'danger'}">${dbStatus.connection_ok ? '正常' : '异常'}</span></div>
<div class="mb-2"><strong>类型:</strong> ${dbStatus.type || '未知'}</div>
<div class="mb-2"><strong>版本:</strong> ${dbStatus.version || '未知'}</div>
<div class="mb-2"><strong>连接数:</strong> ${dbStatus.active_connections || 0}</div>
`;
} else {
dbStatusDiv.innerHTML = '<div class="text-muted">无法获取数据库状态</div>';
}
// 加载配置设置 (这里应该从后端API获取)
this.loadConfigSettings();
} catch (error) {
console.error('加载设置失败:', error);
document.getElementById('system-info').innerHTML = '<div class="text-danger">加载失败</div>';
document.getElementById('db-status').innerHTML = '<div class="text-danger">加载失败</div>';
}
}
async loadConfigSettings() {
// 这里应该从后端API加载配置设置
// 暂时设置一些默认值
try {
document.getElementById('check-interval').value = '300';
document.getElementById('cooldown').value = '3600';
document.getElementById('llm-provider').value = 'qwen';
document.getElementById('default-model').value = 'qwen-turbo';
document.getElementById('enable-analytics').checked = true;
document.getElementById('enable-websocket').checked = true;
document.getElementById('enable-pwa').checked = true;
document.getElementById('enable-error-reporting').checked = false;
} catch (error) {
console.error('加载配置设置失败:', error);
}
}
async saveSettings() {
const settings = {
alert_rules: {
check_interval: parseInt(document.getElementById('check-interval').value),
cooldown: parseInt(document.getElementById('cooldown').value)
},
llm: {
provider: document.getElementById('llm-provider').value,
model: document.getElementById('default-model').value,
api_key: document.getElementById('api-key').value
},
features: {
analytics: document.getElementById('enable-analytics').checked,
websocket: document.getElementById('enable-websocket').checked,
pwa: document.getElementById('enable-pwa').checked,
error_reporting: document.getElementById('enable-error-reporting').checked
}
};
try {
// 这里应该调用后端API保存设置
console.log('保存设置:', settings);
if (window.showToast) {
window.showToast('设置保存成功', 'success');
}
} catch (error) {
console.error('保存设置失败:', error);
if (window.showToast) {
window.showToast('设置保存失败', 'error');
}
}
}
async backupData() {
try {
// 这里应该调用后端备份API
console.log('开始备份数据...');
if (window.showToast) {
window.showToast('数据备份功能开发中', 'info');
}
} catch (error) {
console.error('备份数据失败:', error);
if (window.showToast) {
window.showToast('备份失败', 'error');
}
}
}
async clearCache() {
if (confirm('确定要清理所有缓存吗?')) {
try {
// 清理本地存储
localStorage.clear();
sessionStorage.clear();
// 清理Service Worker缓存
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
}
if (window.showToast) {
window.showToast('缓存清理完成', 'success');
}
// 刷新页面
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error) {
console.error('清理缓存失败:', error);
if (window.showToast) {
window.showToast('清理缓存失败', 'error');
}
}
}
}
async checkHealth() {
try {
const response = await fetch('/api/health');
const data = await response.json();
if (data) {
const healthScore = data.health_score || 0;
const status = healthScore >= 80 ? 'success' : healthScore >= 60 ? 'warning' : 'error';
const message = `系统健康评分: ${healthScore}/100`;
if (window.showToast) {
window.showToast(message, status);
}
} else {
if (window.showToast) {
window.showToast('健康检查失败', 'error');
}
}
} catch (error) {
console.error('健康检查失败:', error);
if (window.showToast) {
window.showToast('健康检查失败', 'error');
}
}
}
async restartServices() {
if (confirm('确定要重启系统服务吗?这可能会暂时中断服务。')) {
try {
// 这里应该调用后端重启API
console.log('重启服务...');
if (window.showToast) {
window.showToast('服务重启功能开发中', 'info');
}
} catch (error) {
console.error('重启服务失败:', error);
if (window.showToast) {
window.showToast('重启失败', 'error');
}
}
}
}
formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}${hours}小时 ${minutes}分钟`;
} else if (hours > 0) {
return `${hours}小时 ${minutes}分钟`;
} else {
return `${minutes}分钟`;
}
}
}

View File

@@ -0,0 +1,402 @@
/**
* 车辆数据页面组件
*/
export default class Vehicle {
constructor(container, route) {
this.container = container;
this.route = route;
this.init();
}
async init() {
try {
this.render();
this.bindEvents();
this.loadVehicleData();
} catch (error) {
console.error('Vehicle init error:', error);
this.showError(error);
}
}
render() {
this.container.innerHTML = `
<div class="page-header">
<div>
<h1 class="page-title">车辆数据管理</h1>
<p class="page-subtitle">查看和管理车辆实时数据</p>
</div>
</div>
<!-- 查询条件 -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-search me-2"></i>数据查询
</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label for="vehicle_id" class="form-label">车辆ID</label>
<input type="text" class="form-control" id="vehicle_id"
placeholder="输入车辆ID">
</div>
<div class="col-md-3">
<label for="vehicle_vin" class="form-label">车架号(VIN)</label>
<input type="text" class="form-control" id="vehicle_vin"
placeholder="输入车架号">
</div>
<div class="col-md-3">
<label for="data_type" class="form-label">数据类型</label>
<select class="form-select" id="data_type">
<option value="">全部类型</option>
<option value="location">位置信息</option>
<option value="status">状态信息</option>
<option value="fault">故障信息</option>
<option value="performance">性能数据</option>
</select>
</div>
<div class="col-md-3">
<label for="limit" class="form-label">显示数量</label>
<select class="form-select" id="limit">
<option value="10">10条</option>
<option value="50">50条</option>
<option value="100">100条</option>
<option value="500">500条</option>
</select>
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<button class="btn btn-primary me-2" id="search-btn">
<i class="fas fa-search me-2"></i>查询
</button>
<button class="btn btn-success me-2" id="init-sample-btn">
<i class="fas fa-plus me-2"></i>初始化示例数据
</button>
<button class="btn btn-info" id="add-data-btn">
<i class="fas fa-plus-circle me-2"></i>添加数据
</button>
</div>
</div>
</div>
</div>
<!-- 数据表格 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-table me-2"></i>车辆数据列表
</h5>
<div id="data-count" class="text-muted small">共 0 条数据</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="vehicle-data-table">
<thead>
<tr>
<th>ID</th>
<th>车辆ID</th>
<th>车架号</th>
<th>数据类型</th>
<th>数据内容</th>
<th>时间戳</th>
<th>操作</th>
</tr>
</thead>
<tbody id="vehicle-data-body">
<tr>
<td colspan="7" class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>加载中...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 添加数据模态框 -->
<div class="modal fade" id="addDataModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">添加车辆数据</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="add-data-form">
<div class="row g-3">
<div class="col-md-6">
<label for="add_vehicle_id" class="form-label">车辆ID *</label>
<input type="text" class="form-control" id="add_vehicle_id"
name="vehicle_id" required>
</div>
<div class="col-md-6">
<label for="add_vehicle_vin" class="form-label">车架号(VIN)</label>
<input type="text" class="form-control" id="add_vehicle_vin"
name="vehicle_vin">
</div>
<div class="col-md-6">
<label for="add_data_type" class="form-label">数据类型 *</label>
<select class="form-select" id="add_data_type" name="data_type" required>
<option value="location">位置信息</option>
<option value="status">状态信息</option>
<option value="fault">故障信息</option>
<option value="performance">性能数据</option>
</select>
</div>
<div class="col-md-12">
<label for="add_data_value" class="form-label">数据内容 (JSON格式) *</label>
<textarea class="form-control" id="add_data_value" name="data_value"
rows="6" placeholder='例如:{"latitude": 39.9042, "longitude": 116.4074}'
required></textarea>
<div class="form-text">请输入有效的JSON格式数据</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="save-data-btn">保存</button>
</div>
</div>
</div>
</div>
`;
}
bindEvents() {
// 查询按钮
document.getElementById('search-btn').addEventListener('click', () => {
this.loadVehicleData();
});
// 初始化示例数据
document.getElementById('init-sample-btn').addEventListener('click', () => {
this.initSampleData();
});
// 添加数据按钮
document.getElementById('add-data-btn').addEventListener('click', () => {
this.showAddDataModal();
});
// 保存数据
document.getElementById('save-data-btn').addEventListener('click', () => {
this.saveVehicleData();
});
// 输入框回车查询
['vehicle_id', 'vehicle_vin'].forEach(id => {
document.getElementById(id).addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.loadVehicleData();
}
});
});
}
async loadVehicleData() {
const vehicleId = document.getElementById('vehicle_id').value.trim();
const vehicleVin = document.getElementById('vehicle_vin').value.trim();
const dataType = document.getElementById('data_type').value;
const limit = document.getElementById('limit').value;
const params = new URLSearchParams();
if (vehicleId) params.append('vehicle_id', vehicleId);
if (vehicleVin) params.append('vehicle_vin', vehicleVin);
if (dataType) params.append('data_type', dataType);
if (limit) params.append('limit', limit);
try {
const response = await fetch(`/api/vehicle/data?${params}`);
const data = await response.json();
if (Array.isArray(data)) {
this.renderVehicleData(data);
document.getElementById('data-count').textContent = `${data.length} 条数据`;
} else {
this.showErrorInTable('数据格式错误');
}
} catch (error) {
console.error('加载车辆数据失败:', error);
this.showErrorInTable('加载数据失败');
}
}
renderVehicleData(data) {
const tbody = document.getElementById('vehicle-data-body');
if (data.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center text-muted">
<i class="fas fa-inbox me-2"></i>暂无数据
</td>
</tr>
`;
return;
}
tbody.innerHTML = data.map(item => {
const timestamp = new Date(item.timestamp).toLocaleString();
let dataValue = item.data_value;
try {
const parsed = JSON.parse(dataValue);
dataValue = JSON.stringify(parsed, null, 2);
} catch (e) {
// 如果不是JSON格式保持原样
}
return `
<tr>
<td>${item.id}</td>
<td>${item.vehicle_id}</td>
<td>${item.vehicle_vin || '-'}</td>
<td><span class="badge bg-primary">${item.data_type}</span></td>
<td>
<pre class="small mb-0" style="max-height: 100px; overflow: hidden;">${dataValue}</pre>
</td>
<td>${timestamp}</td>
<td>
<button class="btn btn-sm btn-outline-info me-1" onclick="showDataDetails(${item.id})">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
`;
}).join('');
}
showErrorInTable(message) {
const tbody = document.getElementById('vehicle-data-body');
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>${message}
</td>
</tr>
`;
}
async initSampleData() {
if (!confirm('确定要初始化示例车辆数据吗?这将会添加一些测试数据。')) {
return;
}
try {
const response = await fetch('/api/vehicle/init-sample-data', { method: 'POST' });
const data = await response.json();
if (data.success) {
if (window.showToast) {
window.showToast('示例数据初始化成功', 'success');
}
this.loadVehicleData();
} else {
if (window.showToast) {
window.showToast(data.message || '初始化失败', 'error');
}
}
} catch (error) {
console.error('初始化示例数据失败:', error);
if (window.showToast) {
window.showToast('网络错误', 'error');
}
}
}
showAddDataModal() {
const modal = new bootstrap.Modal(document.getElementById('addDataModal'));
modal.show();
}
async saveVehicleData() {
const form = document.getElementById('add-data-form');
const formData = new FormData(form);
const data = {
vehicle_id: formData.get('vehicle_id'),
vehicle_vin: formData.get('vehicle_vin') || null,
data_type: formData.get('data_type'),
data_value: formData.get('data_value')
};
// 验证JSON格式
try {
JSON.parse(data.data_value);
} catch (e) {
if (window.showToast) {
window.showToast('数据内容必须是有效的JSON格式', 'error');
}
return;
}
try {
const response = await fetch('/api/vehicle/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
if (window.showToast) {
window.showToast('数据添加成功', 'success');
}
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('addDataModal'));
modal.hide();
// 清空表单
form.reset();
// 重新加载数据
this.loadVehicleData();
} else {
if (window.showToast) {
window.showToast(result.message || '添加失败', 'error');
}
}
} catch (error) {
console.error('保存数据失败:', error);
if (window.showToast) {
window.showToast('网络错误', 'error');
}
}
}
showError(error) {
this.container.innerHTML = `
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
<h4>页面加载失败</h4>
<p class="text-muted">${error.message || '未知错误'}</p>
<button class="btn btn-primary" onclick="location.reload()">
<i class="fas fa-redo me-2"></i>重新加载
</button>
</div>
</div>
</div>
</div>
`;
}
}
// 全局函数,用于查看数据详情
window.showDataDetails = function(id) {
// 这里可以实现查看详细数据的功能
console.log('查看数据详情:', id);
if (window.showToast) {
window.showToast('详情查看功能开发中', 'info');
}
};

View File

@@ -0,0 +1,329 @@
/**
* 工单管理页面组件
*/
export default class WorkOrders {
constructor(container, route) {
this.container = container;
this.route = route;
this.currentPage = 1;
this.perPage = 20;
this.currentStatus = 'all';
this.searchQuery = '';
this.init();
}
async init() {
try {
this.render();
this.bindEvents();
await this.loadWorkOrders();
} catch (error) {
console.error('WorkOrders init error:', error);
this.showError(error);
}
}
render() {
this.container.innerHTML = `
<div class="page-container">
<div class="page-header">
<div>
<h1 class="page-title">工单管理</h1>
<p class="page-subtitle">工单列表与管理</p>
</div>
<div class="page-actions">
<button class="btn btn-primary" id="create-workorder-btn">
<i class="fas fa-plus me-2"></i>新建工单
</button>
</div>
</div>
<div class="page-content">
<!-- 筛选和搜索 -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">状态筛选</label>
<select class="form-select" id="status-filter">
<option value="all">全部状态</option>
<option value="open">待处理</option>
<option value="in_progress">处理中</option>
<option value="resolved">已解决</option>
<option value="closed">已关闭</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">搜索</label>
<input type="text" class="form-control" id="search-input"
placeholder="搜索工单标题、描述或ID...">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-outline-secondary w-100" id="search-btn">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
</div>
<!-- 工单列表 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">工单列表</h5>
<div id="workorder-count" class="text-muted small">共 0 个工单</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="workorders-table">
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>类别</th>
<th>优先级</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="workorders-tbody">
<tr>
<td colspan="7" class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>加载中...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 分页 -->
<nav id="pagination-nav" class="mt-4" style="display: none;">
<ul class="pagination justify-content-center" id="pagination-list">
</ul>
</nav>
</div>
</div>
`;
}
bindEvents() {
// 状态筛选
document.getElementById('status-filter').addEventListener('change', () => {
this.currentStatus = document.getElementById('status-filter').value;
this.currentPage = 1;
this.loadWorkOrders();
});
// 搜索
document.getElementById('search-btn').addEventListener('click', () => {
this.searchQuery = document.getElementById('search-input').value.trim();
this.currentPage = 1;
this.loadWorkOrders();
});
document.getElementById('search-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('search-btn').click();
}
});
// 新建工单
document.getElementById('create-workorder-btn').addEventListener('click', () => {
this.showCreateWorkOrderModal();
});
}
async loadWorkOrders() {
try {
const params = new URLSearchParams({
page: this.currentPage,
per_page: this.perPage,
status: this.currentStatus,
search: this.searchQuery
});
const response = await fetch(`/api/workorders?${params}`);
const data = await response.json();
if (response.ok && data.success) {
this.renderWorkOrders(data.workorders || []);
this.renderPagination(data.pagination || {});
document.getElementById('workorder-count').textContent = `${data.total || 0} 个工单`;
} else {
this.showErrorInTable(data.message || '加载工单失败');
}
} catch (error) {
console.error('加载工单失败:', error);
this.showErrorInTable('网络错误');
}
}
renderWorkOrders(workorders) {
const tbody = document.getElementById('workorders-tbody');
if (workorders.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center text-muted">
<i class="fas fa-inbox me-2"></i>暂无工单
</td>
</tr>
`;
return;
}
tbody.innerHTML = workorders.map(workorder => {
const statusBadge = this.getStatusBadge(workorder.status);
const priorityBadge = this.getPriorityBadge(workorder.priority);
const createTime = new Date(workorder.created_at).toLocaleString();
return `
<tr>
<td>${workorder.order_id || workorder.id}</td>
<td>
<div class="fw-bold">${workorder.title}</div>
<small class="text-muted">${workorder.description?.substring(0, 50) || ''}...</small>
</td>
<td><span class="badge bg-secondary">${workorder.category || '未分类'}</span></td>
<td>${priorityBadge}</td>
<td>${statusBadge}</td>
<td>${createTime}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="viewWorkOrder(${workorder.id})">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-outline-secondary" onclick="editWorkOrder(${workorder.id})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteWorkOrder(${workorder.id})">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`;
}).join('');
}
renderPagination(pagination) {
const nav = document.getElementById('pagination-nav');
const list = document.getElementById('pagination-list');
if (!pagination || pagination.total_pages <= 1) {
nav.style.display = 'none';
return;
}
nav.style.display = 'block';
let html = '';
// 上一页
if (pagination.has_prev) {
html += `<li class="page-item"><a class="page-link" href="#" onclick="changePage(${pagination.page - 1})">上一页</a></li>`;
}
// 页码
for (let i = Math.max(1, pagination.page - 2); i <= Math.min(pagination.total_pages, pagination.page + 2); i++) {
const activeClass = i === pagination.page ? 'active' : '';
html += `<li class="page-item ${activeClass}"><a class="page-link" href="#" onclick="changePage(${i})">${i}</a></li>`;
}
// 下一页
if (pagination.has_next) {
html += `<li class="page-item"><a class="page-link" href="#" onclick="changePage(${pagination.page + 1})">下一页</a></li>`;
}
list.innerHTML = html;
}
getStatusBadge(status) {
const statusMap = {
'open': '<span class="badge bg-danger">待处理</span>',
'in_progress': '<span class="badge bg-warning">处理中</span>',
'resolved': '<span class="badge bg-success">已解决</span>',
'closed': '<span class="badge bg-secondary">已关闭</span>'
};
return statusMap[status] || `<span class="badge bg-light">${status}</span>`;
}
getPriorityBadge(priority) {
const priorityMap = {
'low': '<span class="badge bg-info">低</span>',
'medium': '<span class="badge bg-warning">中</span>',
'high': '<span class="badge bg-danger">高</span>',
'urgent': '<span class="badge bg-dark">紧急</span>'
};
return priorityMap[priority] || `<span class="badge bg-light">${priority}</span>`;
}
showErrorInTable(message) {
const tbody = document.getElementById('workorders-tbody');
tbody.innerHTML = `
<tr>
<td colspan="7" class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>${message}
</td>
</tr>
`;
}
showCreateWorkOrderModal() {
// 这里应该显示创建工单的模态框
if (window.showToast) {
window.showToast('创建工单功能开发中', 'info');
}
}
showError(error) {
this.container.innerHTML = `
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-body text-center">
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
<h4>页面加载失败</h4>
<p class="text-muted">${error.message || '未知错误'}</p>
<button class="btn btn-primary" onclick="location.reload()">
<i class="fas fa-redo me-2"></i>重新加载
</button>
</div>
</div>
</div>
</div>
`;
}
}
// 全局函数供表格操作使用
window.viewWorkOrder = function(id) {
if (window.showToast) {
window.showToast(`查看工单 ${id} 功能开发中`, 'info');
}
};
window.editWorkOrder = function(id) {
if (window.showToast) {
window.showToast(`编辑工单 ${id} 功能开发中`, 'info');
}
};
window.deleteWorkOrder = function(id) {
if (confirm(`确定要删除工单 ${id} 吗?`)) {
if (window.showToast) {
window.showToast('删除功能开发中', 'info');
}
}
};
window.changePage = function(page) {
// 重新加载当前页面实例
const event = new CustomEvent('changePage', { detail: { page } });
document.dispatchEvent(event);
};

View File

@@ -0,0 +1,160 @@
/**
* 统一API服务
* 提供所有API调用的统一接口
*/
class ApiService {
constructor() {
this.baseURL = '';
this.defaultTimeout = 30000; // 30秒超时
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
headers: {
'Content-Type': 'application/json',
...options.headers
},
timeout: options.timeout || this.defaultTimeout,
...options
};
try {
const response = await fetch(url, config);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}: ${response.statusText}`);
}
return data;
} catch (error) {
console.error(`API请求失败: ${endpoint}`, error);
throw error;
}
}
// 健康检查相关
async getHealth() {
return this.request('/api/health');
}
// 预警相关
async getAlerts() {
return this.request('/api/alerts');
}
async createAlert(alertData) {
return this.request('/api/alerts', {
method: 'POST',
body: JSON.stringify(alertData)
});
}
async updateAlert(alertId, alertData) {
return this.request(`/api/alerts/${alertId}`, {
method: 'PUT',
body: JSON.stringify(alertData)
});
}
async deleteAlert(alertId) {
return this.request(`/api/alerts/${alertId}`, {
method: 'DELETE'
});
}
// 规则相关
async getRules() {
return this.request('/api/rules');
}
async createRule(ruleData) {
return this.request('/api/rules', {
method: 'POST',
body: JSON.stringify(ruleData)
});
}
async updateRule(ruleId, ruleData) {
return this.request(`/api/rules/${ruleId}`, {
method: 'PUT',
body: JSON.stringify(ruleData)
});
}
async deleteRule(ruleId) {
return this.request(`/api/rules/${ruleId}`, {
method: 'DELETE'
});
}
// 监控相关
async getMonitorStatus() {
return this.request('/api/monitor/status');
}
async startMonitoring() {
return this.request('/api/monitor/start', {
method: 'POST'
});
}
async stopMonitoring() {
return this.request('/api/monitor/stop', {
method: 'POST'
});
}
// Agent相关
async getAgentStatus() {
return this.request('/api/agent/status');
}
async toggleAgent(enabled) {
return this.request('/api/agent/toggle', {
method: 'POST',
body: JSON.stringify({ enabled })
});
}
async getAgentHistory(limit = 50) {
return this.request(`/api/agent/action-history?limit=${limit}`);
}
// 车辆数据相关
async getVehicleData(params = {}) {
const queryString = new URLSearchParams(params).toString();
return this.request(`/api/vehicle/data?${queryString}`);
}
async addVehicleData(data) {
return this.request('/api/vehicle/data', {
method: 'POST',
body: JSON.stringify(data)
});
}
// 对话相关
async createChatSession(data) {
return this.request('/api/chat/session', {
method: 'POST',
body: JSON.stringify(data)
});
}
async sendChatMessage(data) {
return this.request('/api/chat/message', {
method: 'POST',
body: JSON.stringify(data)
});
}
async getChatHistory(sessionId) {
return this.request(`/api/chat/history/${sessionId}`);
}
}
// 创建全局API服务实例
const apiService = new ApiService();

View File

@@ -0,0 +1,77 @@
{
"name": "TSP智能助手",
"short_name": "TSP助手",
"description": "基于大语言模型的智能客服系统",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#007bff",
"orientation": "portrait-primary",
"icons": [
{
"src": "/static/images/icon-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/static/images/icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/static/images/icon-128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/static/images/icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/static/images/icon-152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/static/images/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/static/images/icon-384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/static/images/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"shortcuts": [
{
"name": "仪表板",
"short_name": "仪表板",
"description": "查看系统概览",
"url": "/dashboard",
"icons": [{ "src": "/static/images/icon-96.png", "sizes": "96x96" }]
},
{
"name": "工单管理",
"short_name": "工单",
"description": "管理工单",
"url": "/workorders",
"icons": [{ "src": "/static/images/icon-96.png", "sizes": "96x96" }]
},
{
"name": "智能对话",
"short_name": "对话",
"description": "AI智能对话",
"url": "/chat",
"icons": [{ "src": "/static/images/icon-96.png", "sizes": "96x96" }]
}
],
"categories": ["productivity", "business", "utilities"]
}

93
src/web/static/sw.js Normal file
View File

@@ -0,0 +1,93 @@
/**
* Service Worker
*/
const CACHE_NAME = 'tsp-assistant-v1.0.0';
const urlsToCache = [
'/',
'/static/css/main.css',
'/static/js/main.js',
'/static/js/core/utils.js',
'/static/js/core/api.js',
'/static/js/core/store.js',
'/static/js/core/websocket.js',
'/static/js/core/router.js',
'/static/js/components/navbar.js',
'/static/js/components/sidebar.js',
'/static/js/components/modal.js',
'/static/js/pages/dashboard.js',
'/static/js/pages/alerts.js',
'/static/js/pages/workorders.js',
'/static/js/pages/knowledge.js',
'/static/js/pages/chat.js',
'/static/js/pages/monitoring.js',
'/static/js/pages/settings.js'
];
// 安装事件
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
return cache.addAll(urlsToCache);
})
);
});
// 激活事件
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
// 请求拦截
self.addEventListener('fetch', (event) => {
// 只缓存GET请求
if (event.request.method !== 'GET') {
return;
}
// 不缓存API请求
if (event.request.url.startsWith('/api/')) {
return;
}
event.respondWith(
caches.match(event.request)
.then((response) => {
// 缓存命中,返回缓存的资源
if (response) {
return response;
}
// 缓存未命中,发起网络请求
return fetch(event.request).then(
(response) => {
// 检查是否是有效响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应,因为响应是流,只能使用一次
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});

View File

@@ -179,50 +179,6 @@
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="/dashboard">
<i class="fas fa-shield-alt me-2"></i>
TSP助手预警管理
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/dashboard">
<i class="fas fa-tachometer-alt me-1"></i>仪表板
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/alerts">
<i class="fas fa-bell me-1"></i>预警管理
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/chat">
<i class="fas fa-comments me-1"></i>智能对话
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/chat-http">
<i class="fas fa-comment-dots me-1"></i>对话(HTTP)
</a>
</li>
</ul>
<div class="navbar-nav ms-auto">
<span class="navbar-text">
<i class="fas fa-circle text-success"></i> WebSocket连接
</span>
</div>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<!-- 侧边栏 -->
@@ -371,7 +327,6 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/navigation.js') }}"></script>
<script src="{{ url_for('static', filename='js/chat.js') }}"></script>
</body>
</html>

View File

@@ -179,50 +179,6 @@
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="/dashboard">
<i class="fas fa-shield-alt me-2"></i>
TSP助手预警管理
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/dashboard">
<i class="fas fa-tachometer-alt me-1"></i>仪表板
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/alerts">
<i class="fas fa-bell me-1"></i>预警管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/chat">
<i class="fas fa-comments me-1"></i>智能对话
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/chat-http">
<i class="fas fa-comment-dots me-1"></i>对话(HTTP)
</a>
</li>
</ul>
<div class="navbar-nav ms-auto">
<span class="navbar-text">
<i class="fas fa-circle text-info"></i> HTTP轮询
</span>
</div>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<!-- 侧边栏 -->
@@ -371,7 +327,6 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/navigation.js') }}"></script>
<script src="{{ url_for('static', filename='js/chat_http.js') }}"></script>
</body>
</html>

View File

@@ -387,11 +387,11 @@
控制面板
</h5>
<nav class="nav flex-column">
<a class="nav-link active" href="/dashboard" data-i18n="sidebar-dashboard">
<a class="nav-link active" href="#dashboard" data-tab="dashboard" data-i18n="sidebar-dashboard">
<i class="fas fa-home"></i>
仪表板
</a>
<a class="nav-link" href="/chat" data-i18n="sidebar-conversations">
<a class="nav-link" href="#chat" data-tab="chat" data-i18n="sidebar-conversations">
<i class="fas fa-comments"></i>
智能对话
</a>
@@ -399,7 +399,7 @@
<i class="fas fa-brain"></i>
Agent管理
</a>
<a class="nav-link" href="/alerts" data-i18n="sidebar-alerts">
<a class="nav-link" href="#alerts" data-tab="alerts" data-i18n="sidebar-alerts">
<i class="fas fa-exclamation-triangle"></i>
预警管理
</a>
@@ -415,9 +415,9 @@
<i class="fas fa-sync"></i>
飞书同步
</a>
<a class="nav-link" href="/chat-http" data-i18n="sidebar-conversation-history">
<i class="fas fa-comment-dots"></i>
HTTP对话
<a class="nav-link" href="#conversation-history" data-tab="conversation-history" data-i18n="sidebar-conversation-history">
<i class="fas fa-history"></i>
对话历史
</a>
<a class="nav-link" href="#token-monitor" data-tab="token-monitor" data-i18n="sidebar-token-monitor">
<i class="fas fa-coins"></i>
@@ -572,12 +572,12 @@
<label class="form-label">用户ID</label>
<input type="text" class="form-control" id="user-id" value="user_001">
</div>
<div class="mb-3">
<label class="form-label">工单ID (可选)</label>
<input type="number" class="form-control" id="work-order-id" placeholder="留空则自动创建">
</div>
<div class="d-grid gap-2">
<button class="btn btn-primary" id="start-chat">
<i class="fas fa-play me-2"></i>开始对话
@@ -589,9 +589,9 @@
<i class="fas fa-plus me-2"></i>创建工单
</button>
</div>
<hr>
<div class="mb-3">
<h6>快速操作</h6>
<div class="quick-actions">
@@ -601,7 +601,7 @@
<button class="quick-action-btn" data-message="如何解绑车辆">解绑车辆</button>
</div>
</div>
<div class="mb-3">
<h6>会话信息</h6>
<div id="session-info" class="text-muted">
@@ -611,7 +611,7 @@
</div>
</div>
</div>
<div class="col-md-9">
<div class="card chat-container">
<div class="chat-header">
@@ -625,7 +625,7 @@
</div>
</div>
</div>
<div class="chat-messages" id="chat-messages">
<div class="text-center text-muted py-5">
<i class="fas fa-comments fa-3x mb-3"></i>
@@ -633,10 +633,10 @@
<p>请点击"开始对话"按钮开始聊天</p>
</div>
</div>
<div class="chat-input">
<div class="input-group">
<input type="text" class="form-control" id="message-input"
<input type="text" class="form-control" id="message-input"
placeholder="请输入您的问题..." disabled>
<button class="btn btn-primary" id="send-button" disabled>
<i class="fas fa-paper-plane"></i>
@@ -721,7 +721,7 @@
</button>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<button class="btn btn-agent w-100" id="proactive-monitoring">
@@ -1155,7 +1155,7 @@
<i class="fas fa-refresh me-1"></i>刷新状态
</button>
</div>
<!-- 字段映射管理区域 -->
<div class="row mb-4" id="fieldMappingSection" style="display: none;">
<div class="col-12">
@@ -1196,13 +1196,13 @@
<option value="100">前100条</option>
</select>
</div>
<!-- 同步进度 -->
<div class="progress mb-3" id="syncProgress" style="display: none;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%"></div>
</div>
<!-- 同步日志 -->
<div class="mt-3">
<h6>同步日志</h6>
@@ -1327,7 +1327,7 @@
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h5><i class="fas fa-memory me-2"></i>对话记忆</h5>
@@ -2230,13 +2230,13 @@
<i class="fas fa-info-circle me-2"></i>
请先下载模板文件按照模板格式填写工单信息然后上传Excel文件进行导入。
</div>
<div class="mb-3">
<label class="form-label">选择Excel文件</label>
<input type="file" class="form-control" id="excel-file-input" accept=".xlsx,.xls">
<div class="form-text">支持 .xlsx 和 .xls 格式文件大小不超过16MB</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<span>Excel文件列名说明</span>
@@ -2301,7 +2301,7 @@
</table>
</div>
</div>
<div id="import-progress" class="d-none">
<div class="progress mb-3">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
@@ -2311,7 +2311,7 @@
<span id="import-status">正在导入工单...</span>
</div>
</div>
<div id="import-result" class="d-none">
<div class="alert alert-success">
<i class="fas fa-check-circle me-2"></i>
@@ -2472,7 +2472,19 @@
<!-- 脚本 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script src="{{ url_for('static', filename='js/navigation.js') }}"></script>
<!-- 核心模块 -->
<script src="{{ url_for('static', filename='js/core/store.js') }}"></script>
<script src="{{ url_for('static', filename='js/services/api.js') }}"></script>
<script src="{{ url_for('static', filename='js/components/NotificationManager.js') }}"></script>
<!-- 功能组件 -->
<script src="{{ url_for('static', filename='js/components/AlertManager.js') }}"></script>
<!-- 主应用文件 -->
<script src="{{ url_for('static', filename='js/app-new.js') }}"></script>
<!-- 原有dashboard.js保持兼容 -->
<script src="{{ url_for('static', filename='js/dashboard.js') }}?v=1.0.9"></script>
</body>
</html>

View File

@@ -12,43 +12,14 @@
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="/dashboard">
<a class="navbar-brand" href="#">
<i class="fas fa-shield-alt me-2"></i>
TSP助手预警管理
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/dashboard">
<i class="fas fa-tachometer-alt me-1"></i>仪表板
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/alerts">
<i class="fas fa-bell me-1"></i>预警管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/chat">
<i class="fas fa-comments me-1"></i>智能对话
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/chat-http">
<i class="fas fa-comment-dots me-1"></i>对话(HTTP)
</a>
</li>
</ul>
<div class="navbar-nav ms-auto">
<span class="navbar-text" id="monitor-status">
<i class="fas fa-circle text-warning"></i> 监控状态检查中...
</span>
</div>
<div class="navbar-nav ms-auto">
<span class="navbar-text" id="monitor-status">
<i class="fas fa-circle text-warning"></i> 监控状态检查中...
</span>
</div>
</div>
</nav>
@@ -679,7 +650,6 @@
<!-- 脚本 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{ url_for('static', filename='js/navigation.js') }}"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
</body>
</html>

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
WebSocket实时通信服务器
@@ -19,30 +18,30 @@ logger = logging.getLogger(__name__)
class WebSocketServer:
"""WebSocket服务器"""
def __init__(self, host: str = "localhost", port: int = 8765):
self.host = host
self.port = port
self.chat_manager = RealtimeChatManager()
self.connected_clients: Set[WebSocketServerProtocol] = set()
async def register_client(self, websocket: WebSocketServerProtocol):
"""注册客户端"""
self.connected_clients.add(websocket)
logger.info(f"客户端连接: {websocket.remote_address}")
async def unregister_client(self, websocket: WebSocketServerProtocol):
"""注销客户端"""
self.connected_clients.discard(websocket)
logger.info(f"客户端断开: {websocket.remote_address}")
async def handle_message(self, websocket: WebSocketServerProtocol, message: str):
"""处理客户端消息"""
try:
data = json.loads(message)
message_type = data.get("type")
message_id = data.get("messageId") # 获取消息ID
if message_type == "create_session":
await self._handle_create_session(websocket, data, message_id)
elif message_type == "send_message":
@@ -57,77 +56,77 @@ class WebSocketServer:
await self._handle_end_session(websocket, data, message_id)
else:
await self._send_error(websocket, "未知消息类型", message_id)
except json.JSONDecodeError:
await self._send_error(websocket, "JSON格式错误")
except Exception as e:
logger.error(f"处理消息失败: {e}")
await self._send_error(websocket, f"处理消息失败: {str(e)}")
async def _handle_create_session(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
"""处理创建会话请求"""
user_id = data.get("user_id", "anonymous")
work_order_id = data.get("work_order_id")
session_id = self.chat_manager.create_session(user_id, work_order_id)
response = {
"type": "session_created",
"session_id": session_id,
"timestamp": datetime.now().isoformat()
}
if message_id:
response["messageId"] = message_id
await websocket.send(json.dumps(response, ensure_ascii=False))
async def _handle_send_message(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
"""处理发送消息请求"""
session_id = data.get("session_id")
message = data.get("message")
if not session_id or not message:
await self._send_error(websocket, "缺少必要参数", message_id)
return
# 处理消息
result = self.chat_manager.process_message(session_id, message)
response = {
"type": "message_response",
"session_id": session_id,
"result": result,
"timestamp": datetime.now().isoformat()
}
if message_id:
response["messageId"] = message_id
await websocket.send(json.dumps(response, ensure_ascii=False))
async def _handle_get_history(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
"""处理获取历史记录请求"""
session_id = data.get("session_id")
if not session_id:
await self._send_error(websocket, "缺少会话ID", message_id)
return
history = self.chat_manager.get_session_history(session_id)
response = {
"type": "history_response",
"session_id": session_id,
"history": history,
"timestamp": datetime.now().isoformat()
}
if message_id:
response["messageId"] = message_id
await websocket.send(json.dumps(response, ensure_ascii=False))
async def _handle_create_work_order(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
"""处理创建工单请求"""
session_id = data.get("session_id")
@@ -135,69 +134,69 @@ class WebSocketServer:
description = data.get("description")
category = data.get("category", "技术问题")
priority = data.get("priority", "medium")
if not session_id or not title or not description:
await self._send_error(websocket, "缺少必要参数", message_id)
return
result = self.chat_manager.create_work_order(session_id, title, description, category, priority)
response = {
"type": "work_order_created",
"session_id": session_id,
"result": result,
"timestamp": datetime.now().isoformat()
}
if message_id:
response["messageId"] = message_id
await websocket.send(json.dumps(response, ensure_ascii=False))
async def _handle_get_work_order_status(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
"""处理获取工单状态请求"""
work_order_id = data.get("work_order_id")
if not work_order_id:
await self._send_error(websocket, "缺少工单ID", message_id)
return
result = self.chat_manager.get_work_order_status(work_order_id)
response = {
"type": "work_order_status",
"work_order_id": work_order_id,
"result": result,
"timestamp": datetime.now().isoformat()
}
if message_id:
response["messageId"] = message_id
await websocket.send(json.dumps(response, ensure_ascii=False))
async def _handle_end_session(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
"""处理结束会话请求"""
session_id = data.get("session_id")
if not session_id:
await self._send_error(websocket, "缺少会话ID", message_id)
return
success = self.chat_manager.end_session(session_id)
response = {
"type": "session_ended",
"session_id": session_id,
"success": success,
"timestamp": datetime.now().isoformat()
}
if message_id:
response["messageId"] = message_id
await websocket.send(json.dumps(response, ensure_ascii=False))
async def _send_error(self, websocket: WebSocketServerProtocol, error_message: str, message_id: str = None):
"""发送错误消息"""
response = {
@@ -205,33 +204,29 @@ class WebSocketServer:
"message": error_message,
"timestamp": datetime.now().isoformat()
}
if message_id:
response["messageId"] = message_id
await websocket.send(json.dumps(response, ensure_ascii=False))
async def handle_client(self, websocket: WebSocketServerProtocol, path: str):
"""处理客户端连接"""
# 检查连接头(如果可用)
try:
if hasattr(websocket, 'request_headers'):
headers = websocket.request_headers
connection = headers.get("Connection", "").lower()
# 处理不同的连接头格式
if "upgrade" not in connection and "keep-alive" in connection:
logger.warning(f"收到非标准连接头: {connection}")
# 对于keep-alive连接头我们仍然接受连接
elif "upgrade" not in connection:
logger.warning(f"连接头不包含upgrade: {connection}")
# 在websockets 15.x中连接已经在serve时验证所以这里只记录警告
except AttributeError:
# websockets 15.x版本可能没有request_headers属性跳过检查
pass
# 检查连接头
headers = websocket.request_headers
connection = headers.get("Connection", "").lower()
# 处理不同的连接头格式
if "upgrade" not in connection and "keep-alive" in connection:
logger.warning(f"收到非标准连接头: {connection}")
# 对于keep-alive连接头我们仍然接受连接
elif "upgrade" not in connection:
logger.warning(f"连接头不包含upgrade: {connection}")
await websocket.close(code=1002, reason="Invalid connection header")
return
await self.register_client(websocket)
try:
async for message in websocket:
await self.handle_message(websocket, message)
@@ -241,30 +236,44 @@ class WebSocketServer:
logger.error(f"WebSocket连接错误: {e}")
finally:
await self.unregister_client(websocket)
async def start_server(self):
"""启动WebSocket服务器"""
logger.info(f"启动WebSocket服务器: ws://{self.host}:{self.port}")
# 添加CORS支持
async def handle_client_with_cors(websocket: WebSocketServerProtocol, path: str = None):
# CORS处理websockets库默认允许所有来源连接
# 如果需要限制可以在serve时使用additional_headers参数
await self.handle_client(websocket, path or "")
async def handle_client_with_cors(websocket: WebSocketServerProtocol):
# 获取pathwebsockets在提供process_request时不会将path传递给handler
path = websocket.path
# 设置CORS头
if websocket.request_headers.get("Origin"):
# 允许跨域连接
pass
await self.handle_client(websocket, path)
async with websockets.serve(
handle_client_with_cors,
self.host,
self.port
handle_client_with_cors,
self.host,
self.port,
# 添加额外的服务器选项
process_request=self._process_request
):
await asyncio.Future() # 保持服务器运行
def _process_request(self, path, request):
def _process_request(self, path, request_headers):
"""处理HTTP请求支持CORS"""
# 检查是否是WebSocket升级请求
if request.headers.get("Upgrade", "").lower() == "websocket":
# request_headers 可能是 Headers 对象或 Request 对象
if hasattr(request_headers, 'get'):
upgrade_header = request_headers.get("Upgrade", "").lower()
elif hasattr(request_headers, 'headers'):
upgrade_header = request_headers.headers.get("Upgrade", "").lower()
else:
upgrade_header = ""
if upgrade_header == "websocket":
return None # 允许WebSocket连接
# 对于非WebSocket请求返回简单的HTML页面
return (
200,
@@ -283,7 +292,7 @@ class WebSocketServer:
</html>
"""
)
def run(self):
"""运行服务器"""
asyncio.run(self.start_server())
@@ -291,7 +300,7 @@ class WebSocketServer:
if __name__ == "__main__":
# 设置日志
logging.basicConfig(level=logging.INFO)
# 启动服务器
server = WebSocketServer()
server.run()