diff --git a/src/core/query_optimizer.py b/src/core/query_optimizer.py index d2a1722..3aab75d 100644 --- a/src/core/query_optimizer.py +++ b/src/core/query_optimizer.py @@ -287,14 +287,47 @@ class QueryOptimizer: status_counts = Counter([wo.status for wo in workorders]) category_counts = Counter([wo.category for wo in workorders]) priority_counts = Counter([wo.priority for wo in workorders]) - resolved_count = status_counts.get('resolved', 0) + + # 调试信息 + logger.info(f"工单状态统计: {dict(status_counts)}") + logger.info(f"工单总数: {total}") + + # 处理状态映射(支持中英文状态) + status_mapping = { + 'open': ['open', '待处理', '新建', 'new'], + 'in_progress': ['in_progress', '处理中', '进行中', 'progress', 'processing'], + 'resolved': ['resolved', '已解决', '已完成'], + 'closed': ['closed', '已关闭', '关闭'] + } + + # 统计各状态的数量 + mapped_counts = {'open': 0, 'in_progress': 0, 'resolved': 0, 'closed': 0} + + for status, count in status_counts.items(): + if status is None: + continue + status_lower = str(status).lower() + mapped = False + for mapped_status, possible_values in status_mapping.items(): + if status_lower in [v.lower() for v in possible_values]: + mapped_counts[mapped_status] += count + mapped = True + break + + if not mapped: + logger.warning(f"未映射的状态: '{status}' (数量: {count})") + + # 调试信息 + logger.info(f"映射后的状态统计: {mapped_counts}") + + resolved_count = mapped_counts['resolved'] workorders_stats = { 'total': total, - 'open': status_counts.get('open', 0), - 'in_progress': status_counts.get('in_progress', 0), - 'resolved': resolved_count, - 'closed': status_counts.get('closed', 0), + 'open': mapped_counts['open'], + 'in_progress': mapped_counts['in_progress'], + 'resolved': mapped_counts['resolved'], + 'closed': mapped_counts['closed'], 'by_category': dict(category_counts), 'by_priority': dict(priority_counts) } diff --git a/src/web/blueprints/core.py b/src/web/blueprints/core.py index e6baf20..3c676c9 100644 --- a/src/web/blueprints/core.py +++ b/src/web/blueprints/core.py @@ -190,6 +190,133 @@ def get_analytics() -> Dict[str, Any]: return jsonify(analytics) +@core_bp.route('/workorders/by-status/') +@handle_api_errors +def get_workorders_by_status(status: str) -> Dict[str, Any]: + """根据状态获取工单列表""" + try: + with db_manager.get_session() as session: + # 状态映射 + status_mapping = { + 'open': ['open', '待处理', '新建', 'new'], + 'in_progress': ['in_progress', '处理中', '进行中', 'progress', 'processing'], + 'resolved': ['resolved', '已解决', '已完成'], + 'closed': ['closed', '已关闭', '关闭'] + } + + # 处理特殊状态 + if status == 'all': + # 查询所有工单 + workorders = session.query(WorkOrder).order_by(WorkOrder.created_at.desc()).limit(50).all() + else: + # 查找匹配的状态值 + actual_statuses = [] + for mapped_status, possible_values in status_mapping.items(): + if mapped_status == status: + actual_statuses = possible_values + break + + if not actual_statuses: + return create_error_response(f"无效的状态: {status}") + + # 查询工单 + workorders = session.query(WorkOrder).filter( + WorkOrder.status.in_(actual_statuses) + ).order_by(WorkOrder.created_at.desc()).limit(50).all() + + result = [] + for wo in workorders: + result.append({ + "id": wo.id, + "order_id": wo.order_id, + "title": wo.title, + "description": wo.description, + "category": wo.category, + "priority": wo.priority, + "status": wo.status, + "created_at": wo.created_at.isoformat() if wo.created_at else None, + "updated_at": wo.updated_at.isoformat() if wo.updated_at else None, + "resolution": wo.resolution, + "satisfaction_score": wo.satisfaction_score + }) + + return create_success_response({ + "workorders": result, + "count": len(result), + "status": status + }) + + except Exception as e: + return create_error_response(f"获取工单失败: {str(e)}") + + +@core_bp.route('/alerts/by-level/') +@handle_api_errors +def get_alerts_by_level(level: str) -> Dict[str, Any]: + """根据级别获取预警列表""" + try: + with db_manager.get_session() as session: + alerts = session.query(Alert).filter( + Alert.level == level, + Alert.is_active == True + ).order_by(Alert.created_at.desc()).limit(50).all() + + result = [] + for alert in alerts: + result.append({ + "id": alert.id, + "message": alert.message, + "level": alert.level, + "alert_type": alert.alert_type, + "created_at": alert.created_at.isoformat() if alert.created_at else None, + "is_active": alert.is_active + }) + + return create_success_response({ + "alerts": result, + "count": len(result), + "level": level + }) + + except Exception as e: + return create_error_response(f"获取预警失败: {str(e)}") + + +@core_bp.route('/knowledge/by-status/') +@handle_api_errors +def get_knowledge_by_status(status: str) -> Dict[str, Any]: + """根据验证状态获取知识库条目""" + try: + with db_manager.get_session() as session: + from src.core.models import KnowledgeEntry + + is_verified = status == 'verified' + knowledge_entries = session.query(KnowledgeEntry).filter( + KnowledgeEntry.is_verified == is_verified + ).order_by(KnowledgeEntry.created_at.desc()).limit(50).all() + + result = [] + for entry in knowledge_entries: + result.append({ + "id": entry.id, + "title": entry.title, + "content": entry.content, + "category": entry.category, + "is_verified": entry.is_verified, + "created_at": entry.created_at.isoformat() if entry.created_at else None, + "updated_at": entry.updated_at.isoformat() if entry.updated_at else None + }) + + return create_success_response({ + "knowledge": result, + "count": len(result), + "status": status + }) + + except Exception as e: + return create_error_response(f"获取知识库条目失败: {str(e)}") + + @core_bp.route('/batch-delete/workorders', methods=['POST']) @handle_api_errors def batch_delete_workorders() -> Dict[str, Any]: diff --git a/src/web/static/css/style.css b/src/web/static/css/style.css index 061407c..5c8f11b 100644 --- a/src/web/static/css/style.css +++ b/src/web/static/css/style.css @@ -11,6 +11,256 @@ body { color: var(--text-primary); } +/* ===== 图标和按钮优化样式 ===== */ +/* 操作按钮组优化 */ +.btn-group .btn { + position: relative; + transition: all 0.2s ease; +} + +.btn-group .btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0,0,0,0.15); +} + +/* 小尺寸按钮优化 */ +.btn-sm { + padding: 0.375rem 0.5rem; + font-size: 0.75rem; + border-radius: 0.375rem; + min-width: 2rem; + height: 2rem; + display: inline-flex; + align-items: center; + justify-content: center; +} + +/* 操作按钮图标优化 */ +.btn-sm i { + font-size: 0.875rem; + line-height: 1; +} + +/* 按钮组间距优化 */ +.btn-group .btn:not(:last-child) { + margin-right: 0.25rem; +} + +/* 表格操作按钮优化 */ +.table .btn-group { + white-space: nowrap; +} + +.table .btn-group .btn { + margin: 0 0.125rem; +} + +/* 悬停效果优化 */ +.btn-outline-info:hover { + background-color: #0dcaf0; + border-color: #0dcaf0; + color: white; +} + +.btn-outline-primary:hover { + background-color: #0d6efd; + border-color: #0d6efd; + color: white; +} + +.btn-outline-danger:hover { + background-color: #dc3545; + border-color: #dc3545; + color: white; +} + +.btn-outline-success:hover { + background-color: #198754; + border-color: #198754; + color: white; +} + +.btn-outline-warning:hover { + background-color: #ffc107; + border-color: #ffc107; + color: #000; +} + +/* 工具提示优化 */ +.btn[title] { + position: relative; +} + +.btn[title]:hover::after { + content: attr(title); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background-color: rgba(0,0,0,0.8); + color: white; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + white-space: nowrap; + z-index: 1000; + margin-bottom: 0.25rem; +} + +/* 快速操作按钮优化 */ +.quick-action-btn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + color: white; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + font-size: 0.875rem; + transition: all 0.3s ease; + margin: 0.25rem; + cursor: pointer; +} + +.quick-action-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); + background: linear-gradient(135deg, #764ba2 0%, #667eea 100%); +} + +/* 批量操作按钮优化 */ +.btn-group .btn-sm { + font-weight: 500; +} + +.btn-group .btn-sm i { + margin-right: 0.25rem; +} + +/* 空状态图标优化 */ +.empty-state i { + font-size: 3rem; + color: #6c757d; + margin-bottom: 1rem; + opacity: 0.5; +} + +/* 状态指示器优化 */ +.health-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background-color: #ffc107; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +.health-dot.normal { + background-color: #28a745; +} + +.health-dot.warning { + background-color: #ffc107; +} + +.health-dot.error { + background-color: #dc3545; +} + +/* 加载动画优化 */ +.loading-spinner i { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* 可点击统计数字样式 */ +.clickable-stat { + transition: all 0.2s ease; + border-radius: 0.375rem; + padding: 0.25rem 0.5rem; + margin: -0.25rem -0.5rem; +} + +.clickable-stat:hover { + background-color: rgba(255, 255, 255, 0.1); + transform: scale(1.05); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.clickable-stat:active { + transform: scale(0.95); +} + +/* 预览模态框优化 */ +.modal-xl { + max-width: 90vw; +} + +.modal-xl .modal-body { + max-height: 70vh; + overflow-y: auto; +} + +/* 表格预览优化 */ +.table-responsive { + border-radius: 0.5rem; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.table-hover tbody tr:hover { + background-color: rgba(0, 123, 255, 0.05); +} + +/* 空状态样式 */ +.empty-state { + padding: 3rem 1rem; + text-align: center; + color: #6c757d; +} + +.empty-state i { + font-size: 4rem; + margin-bottom: 1rem; + opacity: 0.3; +} + +/* 响应式图标优化 */ +@media (max-width: 768px) { + .btn-sm { + min-width: 1.75rem; + height: 1.75rem; + padding: 0.25rem 0.375rem; + } + + .btn-sm i { + font-size: 0.75rem; + } + + .btn-group .btn:not(:last-child) { + margin-right: 0.125rem; + } + + .table .btn-group .btn { + margin: 0 0.0625rem; + } + + .clickable-stat { + font-size: 1.5rem; + } + + .modal-xl { + max-width: 95vw; + } +} + .navbar-brand { font-size: var(--font-size-2xl); font-weight: var(--font-weight-bold); diff --git a/src/web/static/js/dashboard.js b/src/web/static/js/dashboard.js index e76aeff..49a7b57 100644 --- a/src/web/static/js/dashboard.js +++ b/src/web/static/js/dashboard.js @@ -175,6 +175,23 @@ class TSPDashboard { this.cache = new Map(); this.cacheTimeout = 30000; // 30秒缓存 + // 智能更新机制 + this.lastUpdateTimes = { + alerts: 0, + workorders: 0, + health: 0, + analytics: 0 + }; + + this.updateThresholds = { + alerts: 10000, // 10秒 + workorders: 30000, // 30秒 + health: 30000, // 30秒 + analytics: 60000 // 60秒 + }; + + this.isPageVisible = true; + // 分页配置 this.paginationConfig = { defaultPageSize: 10, @@ -185,10 +202,12 @@ class TSPDashboard { this.init(); this.restorePageState(); this.initLanguage(); + this.initSmartUpdate(); // 添加页面卸载时的清理逻辑 window.addEventListener('beforeunload', () => { this.destroyAllCharts(); + this.cleanupConnections(); }); } @@ -568,6 +587,15 @@ class TSPDashboard { document.getElementById('refresh-alerts').addEventListener('click', () => this.loadAlerts()); document.getElementById('alert-filter').addEventListener('change', () => this.updateAlertsDisplay()); + // 统计数字点击事件 + document.addEventListener('click', (e) => { + if (e.target.classList.contains('clickable-stat')) { + const type = e.target.dataset.type; + const status = e.target.dataset.status || e.target.dataset.level; + this.showStatPreview(type, status); + } + }); + // 知识库管理 document.getElementById('search-knowledge').addEventListener('click', () => this.searchKnowledge()); document.getElementById('knowledge-search').addEventListener('keypress', (e) => { @@ -728,15 +756,146 @@ class TSPDashboard { ]); } - startAutoRefresh() { - // 每30秒刷新健康状态(减少重复请求) - this.refreshIntervals.health = setInterval(() => { + // 初始化智能更新机制 + initSmartUpdate() { + // 页面可见性检测 + document.addEventListener('visibilitychange', () => { + this.isPageVisible = !document.hidden; + if (this.isPageVisible) { + // 页面重新可见时,立即更新数据 + this.smartRefresh(); + } + }); + + // 尝试连接WebSocket获取实时更新 + this.initWebSocketConnection(); + + // 智能定时刷新 + this.startSmartRefresh(); + } + + // 初始化WebSocket连接 + initWebSocketConnection() { + try { + const wsUrl = `ws://localhost:8765/dashboard`; + this.websocket = new WebSocket(wsUrl); + + this.websocket.onopen = () => { + console.log('WebSocket连接已建立'); + // 发送订阅消息 + this.websocket.send(JSON.stringify({ + type: 'subscribe', + topics: ['alerts', 'workorders', 'health'] + })); + }; + + this.websocket.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + this.handleRealtimeUpdate(data); + } catch (error) { + console.error('WebSocket消息解析失败:', error); + } + }; + + this.websocket.onclose = () => { + console.log('WebSocket连接已关闭'); + // 5秒后重连 + setTimeout(() => { + if (this.isPageVisible) { + this.initWebSocketConnection(); + } + }, 5000); + }; + + this.websocket.onerror = (error) => { + console.error('WebSocket连接错误:', error); + }; + } catch (error) { + console.log('WebSocket连接失败,使用轮询模式:', error); + } + } + + // 处理实时更新 + handleRealtimeUpdate(data) { + switch (data.type) { + case 'alert_update': + // 直接更新预警统计,无需API调用 + this.updateAlertStatistics(data.alerts); + this.lastUpdateTimes.alerts = Date.now(); + break; + case 'workorder_update': + // 更新工单统计 + if (this.currentTab === 'workorders') { + this.loadWorkOrders(); + } + this.lastUpdateTimes.workorders = Date.now(); + break; + case 'health_update': + // 更新健康状态 + this.updateHealthDisplay(data.health); + this.lastUpdateTimes.health = Date.now(); + break; + } + } + + // 智能刷新机制 + startSmartRefresh() { + // 每5秒检查一次是否需要更新 + this.refreshIntervals.smart = setInterval(() => { + if (this.isPageVisible) { + this.smartRefresh(); + } + }, 5000); + } + + // 智能刷新逻辑 + smartRefresh() { + const now = Date.now(); + + // 如果最近有用户操作,跳过自动更新 + if (now - this.lastUpdateTimes.alerts < 5000) { // 5秒内不自动更新 + return; + } + + // 检查预警数据是否需要更新 + if (now - this.lastUpdateTimes.alerts > this.updateThresholds.alerts) { + this.refreshAlertStats(); + this.lastUpdateTimes.alerts = now; + } + + // 检查健康状态是否需要更新 + if (now - this.lastUpdateTimes.health > this.updateThresholds.health) { this.loadHealth(); + this.lastUpdateTimes.health = now; + } + + // 检查分析数据是否需要更新 + if (now - this.lastUpdateTimes.analytics > this.updateThresholds.analytics) { + this.loadAnalytics(); + this.lastUpdateTimes.analytics = now; + } + + // 根据当前标签页决定是否更新工单数据 + if (this.currentTab === 'workorders' && + now - this.lastUpdateTimes.workorders > this.updateThresholds.workorders) { + this.loadWorkOrders(); + this.lastUpdateTimes.workorders = now; + } + } + + startAutoRefresh() { + // 保留原有的刷新机制作为备用 + this.refreshIntervals.health = setInterval(() => { + if (this.isPageVisible) { + this.loadHealth(); + } }, 30000); - // 每30秒刷新当前标签页数据(减少重复请求) this.refreshIntervals.currentTab = setInterval(() => { - this.refreshCurrentTab(); + if (this.isPageVisible) { + this.refreshCurrentTab(); + } }, 30000); } @@ -754,6 +913,56 @@ class TSPDashboard { } } + // 刷新预警统计(优化版) + async refreshAlertStats() { + try { + // 检查缓存 + const cacheKey = 'alerts_stats'; + const cachedData = this.cache.get(cacheKey); + const now = Date.now(); + + if (cachedData && (now - cachedData.timestamp) < this.cacheTimeout) { + // 使用缓存数据 + this.updateAlertStatistics(cachedData.data); + return; + } + + const response = await fetch('/api/alerts?per_page=1000'); // 获取全部数据 + const data = await response.json(); + + if (data.alerts) { + // 更新缓存 + this.cache.set(cacheKey, { + data: data.alerts, + timestamp: now + }); + + this.updateAlertStatistics(data.alerts); + } + } catch (error) { + console.error('刷新预警统计失败:', error); + // 静默处理错误,避免频繁的错误日志 + } + } + + // 更新健康状态显示 + updateHealthDisplay(healthData) { + if (healthData.status) { + const statusElement = document.getElementById('health-status'); + if (statusElement) { + statusElement.textContent = healthData.status; + statusElement.className = `badge ${this.getHealthBadgeClass(healthData.status)}`; + } + } + + if (healthData.details) { + const detailsElement = document.getElementById('health-details'); + if (detailsElement) { + detailsElement.innerHTML = healthData.details; + } + } + } + async loadHealth() { try { const response = await fetch('/api/health'); @@ -829,7 +1038,7 @@ class TSPDashboard { try { const [sessionsResponse, alertsResponse, workordersResponse, knowledgeResponse] = await Promise.all([ fetch('/api/chat/sessions'), - fetch('/api/alerts'), + fetch('/api/alerts?per_page=1000'), // 获取全部预警数据 fetch('/api/workorders'), fetch('/api/knowledge/stats') ]); @@ -845,6 +1054,11 @@ class TSPDashboard { document.getElementById('total-workorders').textContent = workorders.workorders?.filter(w => w.status === 'open').length || 0; document.getElementById('knowledge-count').textContent = knowledge.total_entries || 0; + // 更新预警统计数字 + if (alerts.alerts) { + this.updateAlertStatistics(alerts.alerts); + } + // 更新知识库详细统计 document.getElementById('knowledge-total').textContent = knowledge.total_entries || 0; document.getElementById('knowledge-active').textContent = knowledge.active_entries || 0; @@ -1329,7 +1543,10 @@ class TSPDashboard {
${success}% - +
`; @@ -1518,6 +1735,7 @@ class TSPDashboard { const cachedData = this.cache.get(cacheKey); this.updateAlertsDisplay(cachedData.alerts); this.updateAlertsPagination(cachedData); + this.updateAlertStatistics(cachedData.alerts); // 添加统计更新 return; } @@ -1529,6 +1747,7 @@ class TSPDashboard { this.cache.set(cacheKey, data); this.updateAlertsDisplay(data.alerts); this.updateAlertsPagination(data); + this.updateAlertStatistics(data.alerts); // 添加统计更新 } catch (error) { console.error('加载预警失败:', error); this.showNotification('加载预警失败', 'error'); @@ -1583,8 +1802,9 @@ class TSPDashboard {
-
@@ -1599,7 +1819,19 @@ class TSPDashboard { } updateAlertStatistics(alerts) { - const stats = alerts.reduce((acc, alert) => { + // 如果传入的是分页数据,需要重新获取全部数据来计算统计 + if (alerts && alerts.length > 0) { + // 检查是否是分页数据(通常分页数据少于50条) + const pageSize = this.getPageSize('alerts-pagination'); + if (alerts.length <= pageSize) { + // 可能是分页数据,需要获取全部数据 + this.updateAlertStatisticsFromAPI(); + return; + } + } + + // 使用传入的数据计算统计 + const stats = (alerts || []).reduce((acc, alert) => { acc[alert.level] = (acc[alert.level] || 0) + 1; acc.total = (acc.total || 0) + 1; return acc; @@ -1611,6 +1843,36 @@ class TSPDashboard { document.getElementById('total-alerts-count').textContent = stats.total || 0; } + // 从API获取全部预警数据来计算统计 + async updateAlertStatisticsFromAPI() { + try { + // 获取全部预警数据(不分页) + const response = await fetch('/api/alerts?per_page=1000'); // 获取大量数据 + const data = await response.json(); + + if (data.alerts) { + const stats = data.alerts.reduce((acc, alert) => { + acc[alert.level] = (acc[alert.level] || 0) + 1; + acc.total = (acc.total || 0) + 1; + return acc; + }, {}); + + document.getElementById('critical-alerts').textContent = stats.critical || 0; + document.getElementById('warning-alerts').textContent = stats.warning || 0; + document.getElementById('info-alerts').textContent = stats.info || 0; + document.getElementById('total-alerts-count').textContent = stats.total || 0; + + // 更新缓存 + this.cache.set('alerts_stats', { + data: data.alerts, + timestamp: Date.now() + }); + } + } catch (error) { + console.error('获取全部预警统计失败:', error); + } + } + // 预警批量删除功能 toggleSelectAllAlerts() { const selectAllCheckbox = document.getElementById('select-all-alerts'); @@ -1662,8 +1924,14 @@ class TSPDashboard { if (data.success) { this.showNotification(data.message, 'success'); - // 清除缓存并强制刷新 + // 清除所有相关缓存 this.cache.delete('alerts'); + this.cache.delete('alerts_stats'); + + // 立即更新统计数字,避免跳动 + await this.updateStatsAfterDelete(selectedIds.length); + + // 重新加载预警列表 await this.loadAlerts(); // 重置批量删除按钮状态 @@ -1677,6 +1945,109 @@ class TSPDashboard { } } + // 删除后更新统计数字(平滑更新) + async updateStatsAfterDelete(deletedCount) { + try { + // 直接调用API获取最新统计,不依赖页面显示数据 + await this.updateAlertStatisticsFromAPI(); + + // 更新最后更新时间,避免智能更新机制干扰 + this.lastUpdateTimes.alerts = Date.now(); + + } catch (error) { + console.error('更新删除后统计失败:', error); + // 如果计算失败,直接刷新 + await this.refreshAlertStats(); + } + } + + // 获取当前显示的预警数据 + getCurrentDisplayedAlerts() { + const alertElements = document.querySelectorAll('.alert-item'); + const alerts = []; + + alertElements.forEach(element => { + const level = element.querySelector('.alert-level')?.textContent?.trim(); + if (level) { + alerts.push({ level: level.toLowerCase() }); + } + }); + + return alerts; + } + + // 计算预警统计 + calculateAlertStats(alerts) { + const stats = { critical: 0, warning: 0, info: 0, total: 0 }; + + alerts.forEach(alert => { + const level = alert.level.toLowerCase(); + if (stats.hasOwnProperty(level)) { + stats[level]++; + } + stats.total++; + }); + + return stats; + } + + // 平滑更新预警统计数字 + smoothUpdateAlertStats(newStats) { + // 获取当前显示的数字 + const currentCritical = parseInt(document.getElementById('critical-alerts')?.textContent || '0'); + const currentWarning = parseInt(document.getElementById('warning-alerts')?.textContent || '0'); + const currentInfo = parseInt(document.getElementById('info-alerts')?.textContent || '0'); + const currentTotal = parseInt(document.getElementById('total-alerts-count')?.textContent || '0'); + + // 平滑过渡到新数字 + this.animateNumberChange('critical-alerts', currentCritical, newStats.critical); + this.animateNumberChange('warning-alerts', currentWarning, newStats.warning); + this.animateNumberChange('info-alerts', currentInfo, newStats.info); + this.animateNumberChange('total-alerts-count', currentTotal, newStats.total); + } + + // 数字变化动画 + animateNumberChange(elementId, from, to) { + const element = document.getElementById(elementId); + if (!element) return; + + const duration = 300; // 300ms动画 + const startTime = Date.now(); + + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + + // 使用缓动函数 + const easeOut = 1 - Math.pow(1 - progress, 3); + const currentValue = Math.round(from + (to - from) * easeOut); + + element.textContent = currentValue; + + if (progress < 1) { + requestAnimationFrame(animate); + } + }; + + requestAnimationFrame(animate); + } + + // 解决预警后更新统计数字 + async updateStatsAfterResolve(alertId) { + try { + // 直接调用API获取最新统计,不依赖页面显示数据 + await this.updateAlertStatisticsFromAPI(); + + // 更新最后更新时间,避免智能更新机制干扰 + this.lastUpdateTimes.alerts = Date.now(); + + } catch (error) { + console.error('更新解决后统计失败:', error); + // 如果计算失败,直接刷新 + await this.refreshAlertStats(); + } + } + async resolveAlert(alertId) { try { const response = await fetch(`/api/alerts/${alertId}/resolve`, { method: 'POST' }); @@ -1684,6 +2055,15 @@ class TSPDashboard { if (data.success) { this.showNotification('预警已解决', 'success'); + + // 清除缓存 + this.cache.delete('alerts'); + this.cache.delete('alerts_stats'); + + // 立即更新统计数字 + await this.updateStatsAfterResolve(alertId); + + // 重新加载预警列表 this.loadAlerts(); } else { this.showNotification('解决预警失败', 'error'); @@ -1759,13 +2139,16 @@ class TSPDashboard { ${item.is_verified ? `` : `` } @@ -2063,6 +2446,7 @@ class TSPDashboard { const cachedData = this.cache.get(cacheKey); this.updateWorkOrdersDisplay(cachedData.workorders); this.updateWorkOrdersPagination(cachedData); + // 不在这里更新统计,因为分页数据不完整 return; } @@ -2100,6 +2484,7 @@ class TSPDashboard { const data = await response.json(); this.updateWorkOrdersDisplay(data.workorders); this.updateWorkOrdersPagination(data); + // 不在这里更新统计,因为分页数据不完整 // 更新缓存 this.cache.set(cacheKey, data); @@ -2153,12 +2538,15 @@ class TSPDashboard {
@@ -2180,10 +2568,48 @@ class TSPDashboard { return acc; }, {}); + // 状态映射 + const statusMapping = { + 'open': ['open', '待处理', '新建', 'new'], + 'in_progress': ['in_progress', '处理中', '进行中', 'progress', 'processing'], + 'resolved': ['resolved', '已解决', '已完成'], + 'closed': ['closed', '已关闭', '关闭'] + }; + + // 统计各状态的数量 + const mapped_counts = {'open': 0, 'in_progress': 0, 'resolved': 0, 'closed': 0}; + + for (const [status, count] of Object.entries(stats)) { + if (status === 'total') continue; + + const status_lower = String(status).toLowerCase(); + let mapped = false; + + for (const [mapped_status, possible_values] of Object.entries(statusMapping)) { + if (possible_values.some(v => v.toLowerCase() === status_lower)) { + mapped_counts[mapped_status] += count; + mapped = true; + break; + } + } + + if (!mapped) { + console.warn(`未映射的状态: '${status}' (数量: ${count})`); + } + } + document.getElementById('workorders-total').textContent = stats.total || 0; - document.getElementById('workorders-open').textContent = stats.open || 0; - document.getElementById('workorders-progress').textContent = stats.in_progress || 0; - document.getElementById('workorders-resolved').textContent = stats.resolved || 0; + document.getElementById('workorders-open').textContent = mapped_counts['open']; + document.getElementById('workorders-progress').textContent = mapped_counts['in_progress']; + document.getElementById('workorders-resolved').textContent = mapped_counts['resolved']; + + console.log('工单统计更新:', { + total: stats.total, + open: mapped_counts['open'], + in_progress: mapped_counts['in_progress'], + resolved: mapped_counts['resolved'], + closed: mapped_counts['closed'] + }); } // 工单批量删除功能 @@ -3711,6 +4137,7 @@ class TSPDashboard { const response = await fetch('/api/analytics'); const analytics = await response.json(); this.updateAnalyticsDisplay(analytics); + this.updateStatisticsCards(analytics); // 添加统计卡片更新 this.initializeCharts(); } catch (error) { console.error('加载分析数据失败:', error); @@ -3725,6 +4152,21 @@ class TSPDashboard { this.updateCharts(); } + // 清理连接 + cleanupConnections() { + // 关闭WebSocket连接 + if (this.websocket) { + this.websocket.close(); + this.websocket = null; + } + + // 清理所有定时器 + Object.values(this.refreshIntervals).forEach(interval => { + clearInterval(interval); + }); + this.refreshIntervals = {}; + } + // 销毁所有图表 destroyAllCharts() { if (!this.charts) return; @@ -3815,10 +4257,30 @@ class TSPDashboard { const resolved = data.workorders?.resolved || 0; const avgSatisfaction = data.satisfaction?.average || 0; - document.getElementById('totalWorkorders').textContent = total; - document.getElementById('openWorkorders').textContent = open; - document.getElementById('workorders-progress').textContent = inProgress; - document.getElementById('resolvedWorkorders').textContent = resolved; + // 更新工单统计数字(使用正确的元素ID) + if (document.getElementById('workorders-total')) { + document.getElementById('workorders-total').textContent = total; + } + if (document.getElementById('workorders-open')) { + document.getElementById('workorders-open').textContent = open; + } + if (document.getElementById('workorders-progress')) { + document.getElementById('workorders-progress').textContent = inProgress; + } + if (document.getElementById('workorders-resolved')) { + document.getElementById('workorders-resolved').textContent = resolved; + } + + // 同时更新其他可能的元素ID + if (document.getElementById('totalWorkorders')) { + document.getElementById('totalWorkorders').textContent = total; + } + if (document.getElementById('openWorkorders')) { + document.getElementById('openWorkorders').textContent = open; + } + if (document.getElementById('resolvedWorkorders')) { + document.getElementById('resolvedWorkorders').textContent = resolved; + } document.getElementById('avgSatisfaction').textContent = avgSatisfaction.toFixed(1); // 更新预警统计 @@ -4963,6 +5425,416 @@ class TSPDashboard { return statusMap[status] || status; } + // 统计数字预览功能 + async showStatPreview(type, status) { + try { + let title = ''; + let data = []; + let apiUrl = ''; + + switch (type) { + case 'workorder': + title = this.getWorkorderPreviewTitle(status); + apiUrl = status === 'all' ? '/api/workorders' : `/api/workorders/by-status/${status}`; + break; + case 'alert': + title = this.getAlertPreviewTitle(status); + apiUrl = `/api/alerts/by-level/${status}`; + break; + case 'knowledge': + title = this.getKnowledgePreviewTitle(status); + apiUrl = `/api/knowledge/by-status/${status}`; + break; + default: + return; + } + + // 显示加载状态 + this.showLoadingModal(title); + + const response = await fetch(apiUrl); + const result = await response.json(); + + // 处理不同的API响应结构 + if (result.success !== false) { + if (result.success === true) { + // 新API结构: {success: true, data: {workorders: [...]}} + data = result.data[type + 's'] || result.data.knowledge || []; + } else if (result.workorders) { + // 旧API结构: {workorders: [...], page: 1, ...} + data = result.workorders || []; + } else if (result.alerts) { + // 预警API结构 + data = result.alerts || []; + } else if (result.knowledge) { + // 知识库API结构 + data = result.knowledge || []; + } else { + data = []; + } + this.showPreviewModal(title, type, data); + } else { + const errorMsg = result.error || result.message || '未知错误'; + this.showNotification('获取数据失败: ' + errorMsg, 'error'); + } + } catch (error) { + console.error('预览失败:', error); + this.showNotification('预览失败: ' + error.message, 'error'); + } + } + + getWorkorderPreviewTitle(status) { + const titles = { + 'all': '所有工单', + 'open': '待处理工单', + 'in_progress': '处理中工单', + 'resolved': '已解决工单', + 'closed': '已关闭工单' + }; + return titles[status] || '工单列表'; + } + + getAlertPreviewTitle(level) { + const titles = { + 'critical': '严重预警', + 'warning': '警告预警', + 'info': '信息预警' + }; + return titles[level] || '预警列表'; + } + + getKnowledgePreviewTitle(status) { + const titles = { + 'verified': '已验证知识', + 'unverified': '未验证知识' + }; + return titles[status] || '知识库条目'; + } + + showLoadingModal(title) { + const modalHtml = ` + + `; + + // 移除已存在的模态框和遮罩 + this.removeExistingModal(); + + document.body.insertAdjacentHTML('beforeend', modalHtml); + const modalElement = document.getElementById('statPreviewModal'); + const modal = new bootstrap.Modal(modalElement, { + backdrop: true, + keyboard: true + }); + + // 添加事件监听器确保正确清理 + modalElement.addEventListener('hidden.bs.modal', () => { + this.cleanupModal(); + }); + + modal.show(); + } + + showPreviewModal(title, type, data) { + let contentHtml = ''; + + if (data.length === 0) { + contentHtml = ` +
+ +
暂无数据
+

当前条件下没有找到相关记录

+
+ `; + } else { + switch (type) { + case 'workorder': + contentHtml = this.generateWorkorderPreviewHtml(data); + break; + case 'alert': + contentHtml = this.generateAlertPreviewHtml(data); + break; + case 'knowledge': + contentHtml = this.generateKnowledgePreviewHtml(data); + break; + } + } + + const modalHtml = ` + + `; + + // 移除已存在的模态框和遮罩 + this.removeExistingModal(); + + document.body.insertAdjacentHTML('beforeend', modalHtml); + const modalElement = document.getElementById('statPreviewModal'); + const modal = new bootstrap.Modal(modalElement, { + backdrop: true, + keyboard: true + }); + + // 添加事件监听器确保正确清理 + modalElement.addEventListener('hidden.bs.modal', () => { + this.cleanupModal(); + }); + + modal.show(); + } + + generateWorkorderPreviewHtml(workorders) { + return ` +
+ + + + + + + + + + + + + ${workorders.map(wo => ` + + + + + + + + + `).join('')} + +
工单ID标题状态优先级创建时间操作
${wo.order_id || wo.id} +
+ ${wo.title} +
+
+ + ${this.getStatusText(wo.status)} + + + + ${this.getPriorityText(wo.priority)} + + ${new Date(wo.created_at).toLocaleString()} + +
+
+ `; + } + + generateAlertPreviewHtml(alerts) { + return ` +
+ + + + + + + + + + + + + ${alerts.map(alert => ` + + + + + + + + + `).join('')} + +
预警ID消息级别类型创建时间操作
${alert.id} +
+ ${alert.message} +
+
+ + ${this.getLevelText(alert.level)} + + ${this.getTypeText(alert.alert_type)}${new Date(alert.created_at).toLocaleString()} + +
+
+ `; + } + + generateKnowledgePreviewHtml(knowledge) { + return ` +
+ + + + + + + + + + + + + ${knowledge.map(item => ` + + + + + + + + + `).join('')} + +
ID标题分类验证状态创建时间操作
${item.id} +
+ ${item.title} +
+
${item.category || '未分类'} + + ${item.is_verified ? '已验证' : '未验证'} + + ${new Date(item.created_at).toLocaleString()} +
+ ${item.is_verified ? + `` : + `` + } + +
+
+
+ `; + } + + getAlertLevelColor(level) { + const colorMap = { + 'critical': 'danger', + 'warning': 'warning', + 'info': 'info' + }; + return colorMap[level] || 'secondary'; + } + + goToFullView(type, status) { + // 关闭预览模态框 + const modal = bootstrap.Modal.getInstance(document.getElementById('statPreviewModal')); + if (modal) { + modal.hide(); + } + + // 切换到对应的标签页 + switch (type) { + case 'workorder': + this.switchTab('workorders'); + // 设置筛选器 + if (status !== 'all') { + setTimeout(() => { + const filter = document.getElementById('workorder-status-filter'); + if (filter) { + filter.value = status; + this.loadWorkOrders(); + } + }, 100); + } + break; + case 'alert': + this.switchTab('alerts'); + // 设置筛选器 + setTimeout(() => { + const filter = document.getElementById('alert-filter'); + if (filter) { + filter.value = status; + this.updateAlertsDisplay(); + } + }, 100); + break; + case 'knowledge': + this.switchTab('knowledge'); + break; + } + } + + // 模态框清理方法 + removeExistingModal() { + const existingModal = document.getElementById('statPreviewModal'); + if (existingModal) { + // 获取模态框实例并销毁 + const modalInstance = bootstrap.Modal.getInstance(existingModal); + if (modalInstance) { + modalInstance.dispose(); + } + existingModal.remove(); + } + + // 清理可能残留的遮罩 + const backdrops = document.querySelectorAll('.modal-backdrop'); + backdrops.forEach(backdrop => backdrop.remove()); + + // 恢复body的滚动 + document.body.classList.remove('modal-open'); + document.body.style.overflow = ''; + document.body.style.paddingRight = ''; + } + + cleanupModal() { + // 延迟清理,确保动画完成 + setTimeout(() => { + this.removeExistingModal(); + }, 300); + } + getStatusColor(status) { const colorMap = { 'open': 'warning', diff --git a/src/web/templates/dashboard.html b/src/web/templates/dashboard.html index cdc0fc1..63eee7a 100644 --- a/src/web/templates/dashboard.html +++ b/src/web/templates/dashboard.html @@ -805,7 +805,7 @@
-

0

+

0

严重预警

@@ -820,7 +820,7 @@
-

0

+

0

警告预警

@@ -835,7 +835,7 @@
-

0

+

0

信息预警

@@ -1018,19 +1018,19 @@
总工单数 -

0

+

0

待处理 -

0

+

0

处理中 -

0

+

0

已解决 -

0

+

0

@@ -2472,6 +2472,6 @@ - +