safe: Repository 层 + 旧代码清理 + tasks.md(从 v2.0 分支安全提取)

This commit is contained in:
2026-04-08 09:49:36 +08:00
parent 96177eddf3
commit b4aa4c8d02
33 changed files with 328 additions and 8414 deletions

View File

@@ -0,0 +1,88 @@
# 架构演进任务清单
## 概述
基于两轮架构审查发现的结构性问题,按优先级排列的演进任务。每个任务独立可交付,不依赖其他任务的完成。
## Tasks
- [-] 1. 引入 Repository 层,分离数据访问逻辑
- [x] 1.1 创建 `src/repositories/` 目录,为核心模型创建 Repository 类
- WorkOrderRepository: 封装工单的 CRUD + 按 tenant_id 过滤
- KnowledgeRepository: 封装知识库的 CRUD + 按 tenant_id 过滤
- ConversationRepository: 封装对话/会话的 CRUD + 按 tenant_id 过滤
- AlertRepository: 封装预警的 CRUD + 按 tenant_id 过滤
- [ ] 1.2 将 blueprint 中的直接 DB 查询迁移到 Repository
- workorders.py 的 get_workorders、create_workorder、delete_workorder
- knowledge.py 的 get_knowledge、add_knowledge、delete_knowledge
- conversations.py 的所有端点
- alerts.py 的所有端点
- [x] 1.3 在 Repository 基类中统一添加 tenant_id 过滤
- 所有查询方法自动附加 tenant_id 条件
- 写操作自动设置 tenant_id
- [ ] 2. 统一 LLM 客户端
- [ ] 2.1 将 `src/agent/llm_client.py` 的异步能力合并到 `src/core/llm_client.py`
- LLMClient 同时支持同步和异步调用
- 统一超时、重试、token 统计逻辑
- [ ] 2.2 让 agent_assistant.py 使用统一的 LLMClient
- 删除 `src/agent/llm_client.py` 中的 LLMManager/OpenAIClient 等重复类
- [ ] 2.3 统一 LLM 配置入口
- 所有 LLM 调用从 unified_config 读取配置
- [ ] 3. 引入 MessagePipeline 统一消息处理
- [ ] 3.1 创建 `src/dialogue/message_pipeline.py`
- 定义统一的消息处理流程:接收 → 租户解析 → 会话管理 → 知识搜索 → LLM 调用 → 保存 → 回复
- 各入口WebSocket、HTTP、飞书 bot、飞书长连接只负责协议适配
- [ ] 3.2 重构 realtime_chat.py 使用 Pipeline
- process_message 和 process_message_stream 委托给 Pipeline
- [ ] 3.3 重构飞书 bot/longconn 使用 Pipeline
- 消除 feishu_bot.py 和 feishu_longconn_service.py 中的重复逻辑
- [ ] 4. 引入 Alembic 数据库迁移
- [ ] 4.1 初始化 Alembic 配置
- alembic init, 配置 env.py 连接 unified_config
- [ ] 4.2 生成初始迁移脚本
- 从当前 models.py 生成 baseline migration
- [ ] 4.3 移除 database.py 中的 _run_migrations 手动迁移逻辑
- 改为启动时运行 alembic upgrade head
- [ ] 5. 统一配置管理
- [ ] 5.1 定义配置优先级:环境变量 > system_settings.json > 代码默认值
- [ ] 5.2 创建 ConfigService 统一读写接口
- get(key, default) / set(key, value) / get_section(section)
- 底层自动合并三个来源
- [ ] 5.3 迁移 SystemOptimizer、PerformanceConfig 使用 ConfigService
- [ ] 6. API 契约定义
- [ ] 6.1 引入 Flask-RESTX 或 apispec 生成 OpenAPI 文档
- [ ] 6.2 为所有 blueprint 端点添加 schema 定义
- [ ] 6.3 统一所有端点使用 api_response() 标准格式
- [ ] 7. 会话状态迁移到 Redis
- [ ] 7.1 将 RealtimeChatManager.active_sessions 迁移到 Redis Hash
- [ ] 7.2 将消息去重从内存缓存迁移到 Redis SET支持多进程
- [ ] 7.3 支持多实例部署(无状态 Flask + 共享 Redis
- [ ] 8. 密码哈希升级
- [ ] 8.1 将 SHA-256 替换为 bcryptpip install bcrypt
- [ ] 8.2 兼容旧密码:登录时检测旧格式,自动升级为 bcrypt
- [ ] 9. 前端状态管理优化
- [ ] 9.1 引入简易事件总线EventEmitter 模式)
- 模块间通过事件通信,不直接读写共享状态
- [ ] 9.2 将 this.xxxCurrentTenantId 等状态封装为 Store 对象
- [x] 10. 清理旧代码
- [x] 10.1 删除 src/web/static/js/core/ 目录(旧的未完成重构)
- [x] 10.2 删除 src/web/static/js/services/ 目录
- [x] 10.3 删除 src/web/static/js/components/ 目录
- [x] 10.4 删除 src/web/static/js/pages/ 目录
- [x] 10.5 清理 index.html、chat.html、chat_http.html 中对已删除 JS 的引用
## Notes
- 每个任务独立可交付,按 1 → 2 → 3 的顺序做收益最大
- 任务 4Alembic可以随时做不依赖其他任务
- 任务 7Redis 会话)只在需要多实例部署时才有必要
- 任务 8密码升级安全性高但影响面小可以穿插做

View File

@@ -0,0 +1 @@
# Repository 层 — 统一数据访问,自动 tenant_id 过滤

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from sqlalchemy import desc
from src.core.models import Alert
from .base import BaseRepository
class AlertRepository(BaseRepository):
model_class = Alert
def list_alerts(self, tenant_id=None, page=1, per_page=20, level=None, is_active=None):
filters = {}
if level:
filters['level'] = level
if is_active is not None:
filters['is_active'] = is_active
return self.list(tenant_id=tenant_id, page=page, per_page=per_page,
filters=filters, order_by=desc(Alert.created_at))
def resolve(self, alert_id: int, tenant_id=None):
from datetime import datetime
return self.update(alert_id, {'is_active': False, 'resolved_at': datetime.now()}, tenant_id=tenant_id)
alert_repo = AlertRepository()

121
src/repositories/base.py Normal file
View File

@@ -0,0 +1,121 @@
# -*- coding: utf-8 -*-
"""
Repository 基类
所有数据访问通过 Repository 层,自动附加 tenant_id 过滤。
"""
import logging
from typing import Any, Dict, List, Optional, Type
from sqlalchemy.orm import Session
from src.core.database import db_manager
from src.core.models import DEFAULT_TENANT
logger = logging.getLogger(__name__)
class BaseRepository:
"""
Repository 基类。子类只需指定 model_class 和 tenant_field。
所有查询自动按 tenant_id 过滤(如果模型有该字段)。
"""
model_class = None # 子类必须设置
tenant_field = 'tenant_id' # 默认租户字段名
def _base_query(self, session: Session, tenant_id: str = None):
"""构建带 tenant_id 过滤的基础查询"""
q = session.query(self.model_class)
if tenant_id and hasattr(self.model_class, self.tenant_field):
q = q.filter(getattr(self.model_class, self.tenant_field) == tenant_id)
return q
def get_by_id(self, id: int, tenant_id: str = None) -> Optional[Dict]:
"""按 ID 查询"""
with db_manager.get_session() as session:
q = self._base_query(session, tenant_id).filter(self.model_class.id == id)
obj = q.first()
return self._to_dict(obj) if obj else None
def list(self, tenant_id: str = None, page: int = 1, per_page: int = 20,
filters: Dict = None, order_by=None) -> Dict[str, Any]:
"""分页列表查询"""
with db_manager.get_session() as session:
q = self._base_query(session, tenant_id)
if filters:
for field, value in filters.items():
if value is not None and hasattr(self.model_class, field):
q = q.filter(getattr(self.model_class, field) == value)
if order_by is not None:
q = q.order_by(order_by)
total = q.count()
items = q.offset((page - 1) * per_page).limit(per_page).all()
return {
'items': [self._to_dict(item) for item in items],
'page': page, 'per_page': per_page,
'total': total, 'total_pages': (total + per_page - 1) // per_page
}
def create(self, data: Dict, tenant_id: str = None) -> Dict:
"""创建记录,自动设置 tenant_id"""
with db_manager.get_session() as session:
if tenant_id and hasattr(self.model_class, self.tenant_field):
data[self.tenant_field] = tenant_id
elif hasattr(self.model_class, self.tenant_field) and self.tenant_field not in data:
data[self.tenant_field] = DEFAULT_TENANT
# 只保留模型有的字段
valid = {k: v for k, v in data.items() if hasattr(self.model_class, k) and not isinstance(v, (dict, list))}
obj = self.model_class(**valid)
session.add(obj)
session.flush()
result = self._to_dict(obj)
return result
def update(self, id: int, data: Dict, tenant_id: str = None) -> Optional[Dict]:
"""更新记录"""
with db_manager.get_session() as session:
q = self._base_query(session, tenant_id).filter(self.model_class.id == id)
obj = q.first()
if not obj:
return None
for k, v in data.items():
if hasattr(obj, k) and k not in ('id', 'tenant_id'):
setattr(obj, k, v)
session.flush()
return self._to_dict(obj)
def delete(self, id: int, tenant_id: str = None) -> bool:
"""删除记录"""
with db_manager.get_session() as session:
q = self._base_query(session, tenant_id).filter(self.model_class.id == id)
obj = q.first()
if not obj:
return False
session.delete(obj)
return True
def batch_delete(self, ids: List[int], tenant_id: str = None) -> int:
"""批量删除,返回实际删除数量"""
with db_manager.get_session() as session:
q = self._base_query(session, tenant_id).filter(self.model_class.id.in_(ids))
count = q.delete(synchronize_session='fetch')
return count
def count(self, tenant_id: str = None, filters: Dict = None) -> int:
"""计数"""
with db_manager.get_session() as session:
q = self._base_query(session, tenant_id)
if filters:
for field, value in filters.items():
if value is not None and hasattr(self.model_class, field):
q = q.filter(getattr(self.model_class, field) == value)
return q.count()
def _to_dict(self, obj) -> Dict:
"""将 ORM 对象转为字典。子类可覆盖。"""
if hasattr(obj, 'to_dict'):
return obj.to_dict()
result = {}
for col in obj.__table__.columns:
val = getattr(obj, col.name)
if hasattr(val, 'isoformat'):
val = val.isoformat()
result[col.name] = val
return result

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from sqlalchemy import desc
from src.core.models import ChatSession, Conversation
from src.core.database import db_manager
from .base import BaseRepository
class ChatSessionRepository(BaseRepository):
model_class = ChatSession
def list_sessions(self, tenant_id=None, page=1, per_page=20, status=None, search=None):
with db_manager.get_session() as session:
q = self._base_query(session, tenant_id)
if status:
q = q.filter(ChatSession.status == status)
if search:
from sqlalchemy import or_
q = q.filter(or_(
ChatSession.title.contains(search),
ChatSession.session_id.contains(search)
))
q = q.order_by(desc(ChatSession.updated_at))
total = q.count()
items = q.offset((page - 1) * per_page).limit(per_page).all()
return {
'sessions': [self._to_dict(s) for s in items],
'page': page, 'per_page': per_page,
'total': total, 'total_pages': (total + per_page - 1) // per_page
}
class ConversationRepository(BaseRepository):
model_class = Conversation
def get_by_session_id(self, session_id: str, tenant_id=None):
with db_manager.get_session() as session:
q = self._base_query(session, tenant_id).filter(Conversation.session_id == session_id)
items = q.order_by(Conversation.timestamp).all()
return [self._to_dict(c) for c in items]
chat_session_repo = ChatSessionRepository()
conversation_repo = ConversationRepository()

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from sqlalchemy import desc
from src.core.models import KnowledgeEntry
from .base import BaseRepository
class KnowledgeRepository(BaseRepository):
model_class = KnowledgeEntry
def list_knowledge(self, tenant_id=None, page=1, per_page=20, category=None, verified=None):
filters = {}
if category:
filters['category'] = category
if verified is not None:
filters['is_verified'] = verified
return self.list(tenant_id=tenant_id, page=page, per_page=per_page,
filters=filters, order_by=desc(KnowledgeEntry.updated_at))
knowledge_repo = KnowledgeRepository()

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from sqlalchemy import desc
from src.core.models import WorkOrder
from .base import BaseRepository
class WorkOrderRepository(BaseRepository):
model_class = WorkOrder
def list_workorders(self, tenant_id=None, page=1, per_page=20, status=None, priority=None):
"""工单列表(带状态和优先级过滤)"""
filters = {}
if status:
filters['status'] = status
if priority:
filters['priority'] = priority
return self.list(tenant_id=tenant_id, page=page, per_page=per_page,
filters=filters, order_by=desc(WorkOrder.created_at))
def find_by_feishu_record_id(self, feishu_record_id: str):
"""按飞书记录 ID 查找"""
from src.core.database import db_manager
with db_manager.get_session() as session:
obj = session.query(WorkOrder).filter(WorkOrder.feishu_record_id == feishu_record_id).first()
return self._to_dict(obj) if obj else None
workorder_repo = WorkOrderRepository()

View File

@@ -1,355 +0,0 @@
/**
* 预警管理组件
* 专门处理预警相关的功能
*/
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

@@ -1,137 +0,0 @@
/**
* 通知管理组件
* 统一处理应用内通知显示
*/
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

@@ -1,418 +0,0 @@
/**
* 模态框组件
*/
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

@@ -1,414 +0,0 @@
/**
* 导航栏组件
*/
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

@@ -1,235 +0,0 @@
/**
* 侧边栏组件
*/
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

@@ -1,472 +0,0 @@
/**
* 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

@@ -1,466 +0,0 @@
/**
* 路由管理模块
*/
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

@@ -1,203 +0,0 @@
/**
* 全局状态管理
* 集中管理应用状态,避免状态分散
*/
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

@@ -1,431 +0,0 @@
/**
* 工具函数集合
*/
// 防抖函数
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

@@ -1,441 +0,0 @@
/**
* 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;

View File

@@ -1,560 +0,0 @@
/**
* 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

@@ -1,738 +0,0 @@
/**
* 预警管理页面组件
*/
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

@@ -1,33 +0,0 @@
/**
* 聊天页面组件
*/
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

@@ -1,454 +0,0 @@
/**
* 仪表板页面组件
*/
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

@@ -1,451 +0,0 @@
/**
* 飞书同步页面组件
*/
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

@@ -1,673 +0,0 @@
/**
* 知识库页面组件
*/
export default class Knowledge {
constructor(container, route) {
this.container = container;
this.route = route;
this.currentPage = 1; // 初始化当前页码
this.init();
}
async init() {
this.render();
this.bindEvents();
await Promise.all([
this.loadKnowledgeList(),
this.loadStats()
]);
}
async loadStats() {
try {
const response = await fetch('/api/knowledge/stats');
if (response.ok) {
const stats = await response.json();
// 更新统计数据显示
const totalEl = this.container.querySelector('#stat-total');
const activeEl = this.container.querySelector('#stat-active');
const catsEl = this.container.querySelector('#stat-categories');
const confEl = this.container.querySelector('#stat-confidence');
if (totalEl) totalEl.textContent = stats.total_entries || 0;
if (activeEl) activeEl.textContent = stats.active_entries || 0; // 后端现在返回的是已验证数量
if (catsEl) catsEl.textContent = Object.keys(stats.category_distribution || {}).length;
if (confEl) confEl.textContent = ((stats.average_confidence || 0) * 100).toFixed(0) + '%';
}
} catch (error) {
console.error('加载统计信息失败:', error);
}
}
render() {
this.container.innerHTML = `
<div class="page-header d-flex justify-content-between align-items-center">
<div>
<h1 class="page-title">知识库</h1>
<p class="page-subtitle">管理和维护知识条目</p>
</div>
<div>
<button class="btn btn-primary" id="btn-import-file">
<i class="fas fa-file-import me-2"></i>导入文件
</button>
<input type="file" id="file-input" style="display: none;" accept=".txt,.md">
</div>
</div>
<!-- 统计卡片 -->
<div class="row row-cards mb-4">
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-primary text-white avatar">
<i class="fas fa-book"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium" id="stat-total">0</div>
<div class="text-muted">总条目</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-green text-white avatar">
<i class="fas fa-check"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium" id="stat-active">0</div>
<div class="text-muted">已验证</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-blue text-white avatar">
<i class="fas fa-tags"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium" id="stat-categories">0</div>
<div class="text-muted">分类数量</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-yellow text-white avatar">
<i class="fas fa-star"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium" id="stat-confidence">0%</div>
<div class="text-muted">平均置信度</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">知识条目列表</h3>
<div class="card-options">
<div class="btn-group me-2 d-none" id="batch-actions">
<button class="btn btn-success btn-sm" id="btn-batch-verify">
<i class="fas fa-check me-1"></i>验证
</button>
<button class="btn btn-warning btn-sm" id="btn-batch-unverify">
<i class="fas fa-times me-1"></i>取消验证
</button>
<button class="btn btn-danger btn-sm" id="btn-batch-delete">
<i class="fas fa-trash me-1"></i>删除
</button>
</div>
<div class="input-group">
<input type="text" class="form-control" placeholder="搜索知识..." id="search-input">
<button class="btn btn-secondary" id="btn-search">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover table-vcenter">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" class="form-check-input" id="check-all">
</th>
<th style="width: 50px;">#</th>
<th>问题/主题</th>
<th>内容预览</th>
<th style="width: 150px;">分类</th>
<th style="width: 100px;">置信度</th>
<th style="width: 100px;">操作</th>
</tr>
</thead>
<tbody id="knowledge-list-body">
<tr>
<td colspan="6" class="text-center py-4">正在加载数据...</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-center mt-3" id="pagination-container">
<!-- 分页控件 -->
</div>
</div>
</div>
`;
}
bindEvents() {
// 导入文件按钮
const fileInput = this.container.querySelector('#file-input');
const importBtn = this.container.querySelector('#btn-import-file');
importBtn.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', async (e) => {
if (e.target.files.length > 0) {
await this.uploadFile(e.target.files[0]);
// 清空选择,允许再次选择同名文件
fileInput.value = '';
}
});
// 搜索功能
const searchInput = this.container.querySelector('#search-input');
const searchBtn = this.container.querySelector('#btn-search');
const performSearch = () => {
const query = searchInput.value.trim();
this.loadKnowledgeList(1, query);
};
searchBtn.addEventListener('click', performSearch);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
performSearch();
}
});
// 批量操作按钮
const batchVerifyBtn = this.container.querySelector('#btn-batch-verify');
if (batchVerifyBtn) {
batchVerifyBtn.addEventListener('click', () => this.batchAction('verify'));
}
const batchUnverifyBtn = this.container.querySelector('#btn-batch-unverify');
if (batchUnverifyBtn) {
batchUnverifyBtn.addEventListener('click', () => this.batchAction('unverify'));
}
const batchDeleteBtn = this.container.querySelector('#btn-batch-delete');
if (batchDeleteBtn) {
batchDeleteBtn.addEventListener('click', () => this.batchAction('delete'));
}
// 全选复选框
const checkAll = this.container.querySelector('#check-all');
if (checkAll) {
checkAll.addEventListener('change', (e) => {
const checks = this.container.querySelectorAll('.item-check');
checks.forEach(check => check.checked = e.target.checked);
this.updateBatchButtons();
});
}
}
bindCheckboxEvents() {
const checks = this.container.querySelectorAll('.item-check');
checks.forEach(check => {
check.addEventListener('change', () => {
this.updateBatchButtons();
// 如果有一个未选中,取消全选选中状态
if (!check.checked) {
const checkAll = this.container.querySelector('#check-all');
if (checkAll) checkAll.checked = false;
}
});
});
}
updateBatchButtons() {
const checkedCount = this.container.querySelectorAll('.item-check:checked').length;
const actionsGroup = this.container.querySelector('#batch-actions');
if (actionsGroup) {
if (checkedCount > 0) {
actionsGroup.classList.remove('d-none');
// 更新删除按钮文本
const deleteBtn = this.container.querySelector('#btn-batch-delete');
if (deleteBtn) deleteBtn.innerHTML = `<i class="fas fa-trash me-1"></i>删除 (${checkedCount})`;
} else {
actionsGroup.classList.add('d-none');
}
}
}
async batchDeleteKnowledge() {
const checks = this.container.querySelectorAll('.item-check:checked');
const ids = Array.from(checks).map(check => parseInt(check.dataset.id));
console.log('Deleting IDs:', ids);
if (ids.length === 0) {
alert('请先选择要删除的知识条目');
return;
}
if (!confirm(`确定要删除选中的 ${ids.length} 条知识吗?`)) {
return;
}
try {
const response = await fetch('/api/knowledge/batch_delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ ids: ids })
});
const result = await response.json();
if (response.ok && result.success) {
alert(result.message || '删除成功');
// 重置全选状态
const checkAll = this.container.querySelector('#check-all');
if (checkAll) checkAll.checked = false;
this.updateBatchDeleteButton();
// 刷新列表和统计(保持当前页)
await Promise.all([
this.loadKnowledgeList(this.currentPage),
this.loadStats()
]);
} else {
alert(`删除失败: ${result.message || '未知错误'}`);
}
} catch (error) {
console.error('批量删除出错:', error);
alert('批量删除出错,请查看控制台');
}
}
async loadKnowledgeList(page = null, query = '') {
// 如果未指定页码,使用当前页码,默认为 1
const targetPage = page || this.currentPage || 1;
this.currentPage = targetPage;
const tbody = this.container.querySelector('#knowledge-list-body');
// 柔性加载:不立即清空,而是降低透明度并显示加载态
// 这可以防止表格高度塌陷导致的视觉跳动
tbody.style.opacity = '0.5';
tbody.style.transition = 'opacity 0.2s';
// 如果表格是空的(第一次加载),则显示加载占位符
if (!tbody.hasChildNodes() || tbody.children.length === 0 || tbody.querySelector('.text-center')) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4"><div class="spinner-border text-primary" role="status"></div><div class="mt-2">加载中...</div></td></tr>';
tbody.style.opacity = '1';
}
try {
let url = `/api/knowledge?page=${targetPage}&per_page=10`;
if (query) {
url = `/api/knowledge/search?q=${encodeURIComponent(query)}`;
}
const response = await fetch(url);
const result = await response.json();
tbody.innerHTML = '';
tbody.style.opacity = '1'; // 恢复不透明
// 处理搜索结果(通常是数组)和分页结果(包含 items的差异
let items = [];
if (Array.isArray(result)) {
items = result;
} else if (result.items) {
items = result.items;
}
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4 text-muted">暂无知识条目</td></tr>';
return;
}
items.forEach((item, index) => {
// ... (渲染逻辑保持不变)
const tr = document.createElement('tr');
// 验证状态图标
let statusBadge = '';
if (item.is_verified) {
statusBadge = '<span class="text-success ms-2" title="已验证"><i class="fas fa-check-circle"></i></span>';
}
// 验证操作按钮
let verifyBtn = '';
if (item.is_verified) {
verifyBtn = `
<button type="button" class="btn btn-sm btn-icon btn-outline-warning btn-unverify" data-id="${item.id}" title="取消验证">
<i class="fas fa-times"></i>
</button>
`;
} else {
verifyBtn = `
<button type="button" class="btn btn-sm btn-icon btn-outline-success btn-verify" data-id="${item.id}" title="验证通过">
<i class="fas fa-check"></i>
</button>
`;
}
tr.innerHTML = `
<td><input type="checkbox" class="form-check-input item-check" data-id="${item.id}"></td>
<td>${(targetPage - 1) * 10 + index + 1}</td>
<td>
<div class="text-truncate" style="max-width: 200px;" title="${item.question}">
${item.question}
${statusBadge}
</div>
</td>
<td><div class="text-truncate" style="max-width: 300px;" title="${item.answer}">${item.answer}</div></td>
<td><span class="badge bg-blue-lt">${item.category || '未分类'}</span></td>
<td>${(item.confidence_score * 100).toFixed(0)}%</td>
<td>
${verifyBtn}
<button type="button" class="btn btn-sm btn-icon btn-outline-danger btn-delete" data-id="${item.id}" title="删除">
<i class="fas fa-trash"></i>
</button>
</td>
`;
// 绑定验证/取消验证事件
const verifyActionBtn = tr.querySelector('.btn-verify, .btn-unverify');
if (verifyActionBtn) {
verifyActionBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const isVerify = verifyActionBtn.classList.contains('btn-verify');
await this.toggleVerify(item.id, isVerify, tr);
});
}
// 绑定删除事件
const deleteBtn = tr.querySelector('.btn-delete');
deleteBtn.addEventListener('click', (e) => {
e.preventDefault();
this.deleteKnowledge(item.id);
});
tbody.appendChild(tr);
});
// 重新绑定复选框事件
this.bindCheckboxEvents();
// 渲染分页
if (result.pages && result.pages > 1) {
this.renderPagination(result);
} else {
this.container.querySelector('#pagination-container').innerHTML = '';
}
} catch (error) {
console.error('加载知识列表失败:', error);
tbody.innerHTML = `<tr><td colspan="7" class="text-center py-4 text-danger">加载失败: ${error.message}</td></tr>`;
tbody.style.opacity = '1';
}
}
async uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
// 显示上传中提示
const importBtn = this.container.querySelector('#btn-import-file');
const originalText = importBtn.innerHTML;
importBtn.disabled = true;
importBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 上传中...';
try {
const response = await fetch('/api/knowledge/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok && result.success) {
alert(`文件上传成功!共提取 ${result.knowledge_count} 条知识。`);
// 刷新列表和统计
await Promise.all([
this.loadKnowledgeList(),
this.loadStats()
]);
} else {
alert(`上传失败: ${result.error || result.message || '未知错误'}`);
}
} catch (error) {
console.error('上传文件出错:', error);
alert('上传文件出错,请查看控制台');
} finally {
importBtn.disabled = false;
importBtn.innerHTML = originalText;
}
}
async toggleVerify(id, isVerify, trElement = null) {
const action = isVerify ? 'verify' : 'unverify';
const url = `/api/knowledge/${action}/${id}`;
try {
// 如果有 trElement先显示加载状态
let originalBtnHtml = '';
let actionBtn = null;
if (trElement) {
actionBtn = trElement.querySelector('.btn-verify, .btn-unverify');
if (actionBtn) {
originalBtnHtml = actionBtn.innerHTML;
actionBtn.disabled = true;
actionBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
}
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (response.ok && result.success) {
// 如果提供了 DOM 元素,直接更新 DOM避免刷新整个列表导致跳动
if (trElement) {
this.updateRowStatus(trElement, id, isVerify);
// 后台静默刷新统计数据
this.loadStats();
} else {
// 仅刷新列表和统计,不跳转页面
await Promise.all([
this.loadKnowledgeList(this.currentPage),
this.loadStats()
]);
}
} else {
alert(`${isVerify ? '验证' : '取消验证'}失败: ${result.message}`);
// 恢复按钮状态
if (actionBtn) {
actionBtn.disabled = false;
actionBtn.innerHTML = originalBtnHtml;
}
}
} catch (error) {
console.error('操作出错:', error);
// 恢复按钮状态
if (trElement) {
const actionBtn = trElement.querySelector('.btn-verify, .btn-unverify');
if (actionBtn) {
actionBtn.disabled = false;
// 简单恢复,无法精确还原之前的图标
actionBtn.innerHTML = isVerify ? '<i class="fas fa-check"></i>' : '<i class="fas fa-times"></i>';
}
}
}
}
updateRowStatus(tr, id, isVerified) {
// 1. 更新问题列的状态图标
const questionCell = tr.cells[2]; // 第3列是问题
const questionDiv = questionCell.querySelector('div');
// 移除旧的徽章
const oldBadge = questionDiv.querySelector('.text-success');
if (oldBadge) oldBadge.remove();
// 如果是验证通过,添加徽章
if (isVerified) {
const statusBadge = document.createElement('span');
statusBadge.className = 'text-success ms-2';
statusBadge.title = '已验证';
statusBadge.innerHTML = '<i class="fas fa-check-circle"></i>';
questionDiv.appendChild(statusBadge);
}
// 2. 更新操作按钮
const actionCell = tr.cells[6]; // 第7列是操作
const actionBtn = actionCell.querySelector('.btn-verify, .btn-unverify');
if (actionBtn) {
// 创建新按钮
const newBtn = document.createElement('button');
newBtn.type = 'button';
newBtn.className = `btn btn-sm btn-icon btn-outline-${isVerified ? 'warning' : 'success'} ${isVerified ? 'btn-unverify' : 'btn-verify'}`;
newBtn.dataset.id = id;
newBtn.title = isVerified ? '取消验证' : '验证通过';
newBtn.innerHTML = `<i class="fas fa-${isVerified ? 'times' : 'check'}"></i>`;
// 重新绑定事件
newBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await this.toggleVerify(id, !isVerified, tr);
});
// 替换旧按钮
actionBtn.replaceWith(newBtn);
}
}
async deleteKnowledge(id) {
if (!confirm('确定要删除这条知识吗?')) {
return;
}
try {
const response = await fetch(`/api/knowledge/delete/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok && result.success) {
// 刷新列表和统计(保持当前页)
await Promise.all([
this.loadKnowledgeList(this.currentPage),
this.loadStats()
]);
} else {
alert(`删除失败: ${result.message || '未知错误'}`);
}
} catch (error) {
console.error('删除出错:', error);
alert('删除出错,请查看控制台');
}
}
renderPagination(pagination) {
const { page, pages } = pagination;
const container = this.container.querySelector('#pagination-container');
let html = '<ul class="pagination">';
// 上一页
html += `
<li class="page-item ${page === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" data-page="${page - 1}" tabindex="-1">上一页</a>
</li>
`;
// 页码 (只显示当前页附近的页码)
const startPage = Math.max(1, page - 2);
const endPage = Math.min(pages, page + 2);
if (startPage > 1) {
html += '<li class="page-item"><a class="page-link" href="#" data-page="1">1</a></li>';
if (startPage > 2) {
html += '<li class="page-item disabled"><span class="page-link">...</span></li>';
}
}
for (let i = startPage; i <= endPage; i++) {
html += `
<li class="page-item ${i === page ? 'active' : ''}">
<a class="page-link" href="#" data-page="${i}">${i}</a>
</li>
`;
}
if (endPage < pages) {
if (endPage < pages - 1) {
html += '<li class="page-item disabled"><span class="page-link">...</span></li>';
}
html += `<li class="page-item"><a class="page-link" href="#" data-page="${pages}">${pages}</a></li>`;
}
// 下一页
html += `
<li class="page-item ${page === pages ? 'disabled' : ''}">
<a class="page-link" href="#" data-page="${page + 1}">下一页</a>
</li>
`;
html += '</ul>';
container.innerHTML = html;
// 绑定点击事件
container.querySelectorAll('.page-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const newPage = parseInt(e.target.dataset.page);
if (newPage && newPage !== page && newPage >= 1 && newPage <= pages) {
this.loadKnowledgeList(newPage);
}
});
});
}
}

View File

@@ -1,216 +0,0 @@
/**
* 登录页面组件
*/
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

@@ -1,33 +0,0 @@
/**
* 系统监控页面组件
*/
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

@@ -1,142 +0,0 @@
/**
* 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

@@ -1,428 +0,0 @@
/**
* 系统设置页面组件
*/
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

@@ -1,402 +0,0 @@
/**
* 车辆数据页面组件
*/
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

@@ -1,549 +0,0 @@
/**
* 工单管理页面组件
*/
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>
<!-- 工单详情模态框 -->
<div class="modal fade" id="workorder-detail-modal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-light">
<h5 class="modal-title">
<i class="fas fa-file-alt text-primary me-2"></i>工单详情
<span class="badge bg-secondary ms-2" id="modal-status-badge">加载中...</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<div class="row g-0 h-100">
<!-- 左侧:基本信息 -->
<div class="col-md-7 border-end p-4">
<h3 id="modal-title" class="mb-3">工单标题</h3>
<div class="row g-3 mb-4">
<div class="col-6 col-md-4">
<label class="form-label text-muted small">工单ID</label>
<div class="fw-bold" id="modal-id">-</div>
</div>
<div class="col-6 col-md-4">
<label class="form-label text-muted small">优先级</label>
<div id="modal-priority">-</div>
</div>
<div class="col-6 col-md-4">
<label class="form-label text-muted small">分类</label>
<div id="modal-category">-</div>
</div>
<div class="col-6 col-md-4">
<label class="form-label text-muted small">创建时间</label>
<div id="modal-created-at">-</div>
</div>
<div class="col-6 col-md-4">
<label class="form-label text-muted small">用户/VIN</label>
<div id="modal-user">-</div>
</div>
</div>
<div class="mb-4">
<label class="form-label fw-bold">问题描述</label>
<div class="bg-light p-3 rounded" id="modal-description" style="min-height: 80px;">
-
</div>
</div>
<div class="mb-4">
<label class="form-label fw-bold text-primary">
<i class="fas fa-robot me-1"></i>AI 智能分析与建议
</label>
<div class="card border-primary-lt">
<div class="card-body bg-azure-lt" id="modal-ai-analysis">
暂无 AI 分析
</div>
</div>
</div>
</div>
<!-- 右侧:对话历史/详细记录 -->
<div class="col-md-5 bg-light p-0 d-flex flex-column" style="height: 600px;">
<div class="p-3 border-bottom bg-white">
<h6 class="mb-0 fw-bold"><i class="fas fa-history me-2"></i>处理记录 / 对话历史</h6>
</div>
<div class="flex-grow-1 p-3 overflow-auto" id="modal-chat-history">
<!-- 聊天记录将动态插入这里 -->
<div class="text-center text-muted mt-5">
<i class="fas fa-comments fa-2x mb-3"></i>
<p>暂无相关对话记录</p>
</div>
</div>
</div>
</div>
</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="modal-edit-btn">编辑工单</button>
</div>
</div>
</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 = async function(id) {
try {
// 显示模态框(先显示加载状态)
const modalEl = document.getElementById('workorder-detail-modal');
const modal = new bootstrap.Modal(modalEl);
modal.show();
// 重置内容
document.getElementById('modal-status-badge').innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
document.getElementById('modal-ai-analysis').innerHTML = '<div class="spinner-border spinner-border-sm text-primary"></div> 正在分析...';
document.getElementById('modal-chat-history').innerHTML = '<div class="text-center mt-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div>';
// 获取详情
const response = await fetch(`/api/workorders/${id}`);
const result = await response.json();
if (!result.success) {
alert('获取工单详情失败');
return;
}
const wo = result.workorder;
// 填充基本信息
document.getElementById('modal-title').textContent = wo.title;
document.getElementById('modal-id').textContent = wo.order_id || wo.id;
document.getElementById('modal-category').textContent = wo.category || '-';
document.getElementById('modal-created-at').textContent = new Date(wo.created_at).toLocaleString();
document.getElementById('modal-user').textContent = wo.user_id || '-';
document.getElementById('modal-description').textContent = wo.description || '无描述';
// 状态徽章
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>'
};
document.getElementById('modal-status-badge').innerHTML = statusMap[wo.status] || wo.status;
// 优先级
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>'
};
document.getElementById('modal-priority').innerHTML = priorityMap[wo.priority] || wo.priority;
// AI 分析/建议
if (wo.resolution) {
document.getElementById('modal-ai-analysis').innerHTML = `
<div style="white-space: pre-wrap; font-family: inherit;">${wo.resolution}</div>
`;
} else {
document.getElementById('modal-ai-analysis').textContent = '暂无 AI 分析建议';
}
// 渲染对话/处理历史 (模拟数据或真实数据)
const historyContainer = document.getElementById('modal-chat-history');
// 这里假设后端返回的详情中包含 history 或 timeline
// 如果没有,暂时显示描述作为第一条记录
let historyHtml = '';
// 模拟一条初始记录
historyHtml += `
<div class="mb-3">
<div class="d-flex align-items-center mb-1">
<span class="badge bg-blue-lt me-2">用户</span>
<small class="text-muted">${new Date(wo.created_at).toLocaleString()}</small>
</div>
<div class="bg-white p-3 border rounded">
${wo.description}
</div>
</div>
`;
if (wo.timeline && wo.timeline.length > 0) {
// 如果有真实的时间轴数据
historyHtml = wo.timeline.map(item => `
<div class="mb-3">
<div class="d-flex align-items-center mb-1">
<span class="badge bg-${item.type === 'ai' ? 'purple-lt' : 'blue-lt'} me-2">
${item.author || (item.type === 'ai' ? 'AI 助手' : '系统')}
</span>
<small class="text-muted">${new Date(item.timestamp).toLocaleString()}</small>
</div>
<div class="bg-${item.type === 'ai' ? 'azure-lt' : 'white'} p-3 border rounded">
${item.content}
</div>
</div>
`).join('');
}
historyContainer.innerHTML = historyHtml;
// 绑定编辑按钮
document.getElementById('modal-edit-btn').onclick = () => {
modal.hide();
editWorkOrder(id);
};
} catch (e) {
console.error('查看工单详情失败', e);
alert('查看详情失败: ' + e.message);
}
};
window.editWorkOrder = function(id) {
if (window.showToast) {
window.showToast(`编辑工单 ${id} 功能开发中`, 'info');
}
};
window.deleteWorkOrder = function(id) {
if (!confirm(`确定要删除工单 ${id} 吗?`)) {
return;
}
(async () => {
try {
const response = await fetch(`/api/workorders/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (response.ok && result.success) {
if (window.showToast) {
window.showToast(result.message || `工单 ${id} 删除成功`, 'success');
}
// 通知当前页面刷新工单列表(保持当前页)
const event = new CustomEvent('workorder-deleted', { detail: { id } });
document.dispatchEvent(event);
} else {
if (window.showToast) {
window.showToast(result.message || '删除工单失败', 'error');
} else {
alert(result.message || '删除工单失败');
}
}
} catch (error) {
console.error('删除工单失败:', error);
if (window.showToast) {
window.showToast('删除失败,请检查网络或查看控制台日志', 'error');
} else {
alert('删除失败,请检查网络或查看控制台日志');
}
}
})();
};
window.changePage = function(page) {
// 重新加载当前页面实例
const event = new CustomEvent('changePage', { detail: { page } });
document.dispatchEvent(event);
};
// 监听删除事件,触发当前 WorkOrders 列表刷新(保持当前页)
document.addEventListener('workorder-deleted', () => {
const event = new CustomEvent('reloadWorkOrders');
document.dispatchEvent(event);
});

View File

@@ -1,160 +0,0 @@
/**
* 统一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

@@ -327,6 +327,6 @@
</div> </div>
<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/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/chat.js') }}"></script> <script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
</body> </body>
</html> </html>

View File

@@ -327,6 +327,6 @@
</div> </div>
<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/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/chat_http.js') }}"></script> <script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
</body> </body>
</html> </html>

View File

@@ -650,6 +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/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script> <script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
</body> </body>
</html> </html>