From d6e9cc990b2d88fadc23680348d777c47e83a191 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Tue, 25 Nov 2025 23:37:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9B=B4=E5=A4=9A=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- http/server.go | 7 +- static/css/style.css | 34 +++ static/index.html | 121 ++++++--- static/js/api.js | 22 +- static/js/colors.config.js | 53 ++++ static/js/dashboard.js | 528 ++++++++++++++++++++++++++++++++++++- 6 files changed, 709 insertions(+), 56 deletions(-) create mode 100644 static/js/colors.config.js diff --git a/http/server.go b/http/server.go index 9ef6f37..9b1cc75 100644 --- a/http/server.go +++ b/http/server.go @@ -102,6 +102,9 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) { // 获取活跃来源IP数量 activeIPCount := len(dnsStats.SourceIPs) + // 格式化平均响应时间为两位小数 + formattedResponseTime := float64(int(dnsStats.AvgResponseTime*100)) / 100 + // 构建响应数据,确保所有字段都反映服务器的真实状态 stats := map[string]interface{}{ "dns": map[string]interface{}{ @@ -110,7 +113,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) { "Allowed": dnsStats.Allowed, "Errors": dnsStats.Errors, "LastQuery": dnsStats.LastQuery, - "AvgResponseTime": dnsStats.AvgResponseTime, + "AvgResponseTime": formattedResponseTime, "TotalResponseTime": dnsStats.TotalResponseTime, "QueryTypes": dnsStats.QueryTypes, "SourceIPs": dnsStats.SourceIPs, @@ -119,7 +122,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) { "shield": shieldStats, "topQueryType": topQueryType, "activeIPs": activeIPCount, - "avgResponseTime": dnsStats.AvgResponseTime, + "avgResponseTime": formattedResponseTime, "cpuUsage": dnsStats.CpuUsage, "time": time.Now(), } diff --git a/static/css/style.css b/static/css/style.css index f5b5ee2..51a0fe6 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -193,6 +193,40 @@ header p { transition: padding-left 0.3s ease; } +/* Tooltip趋势信息颜色类 - 替代内联style */ +.tooltip-trend { + font-weight: 500; +} + +/* 注意:这些颜色值与colors.config.js中的COLOR_CONFIG.colorClassMap保持同步 */ +.tooltip-trend.blue { + color: #1890ff; +} + +.tooltip-trend.green { + color: #52c41a; +} + +.tooltip-trend.orange { + color: #fa8c16; +} + +.tooltip-trend.red { + color: #f5222d; +} + +.tooltip-trend.purple { + color: #722ed1; +} + +.tooltip-trend.cyan { + color: #13c2c2; +} + +.tooltip-trend.teal { + color: #36cfc9; +} + /* 平板设备适配 - 侧边栏折叠时调整内容区域 */ @media (max-width: 992px) { .content { diff --git a/static/index.html b/static/index.html index 7e53a9b..0bcc794 100644 --- a/static/index.html +++ b/static/index.html @@ -150,12 +150,17 @@ -
-

0

- - - 0% - +
+
+

0

+ + + 0% + +
+
+ +
@@ -171,12 +176,17 @@ -
-

0

- - - 0% - +
+
+

0

+ + + 0% + +
+
+ +
@@ -192,12 +202,17 @@ -
-

0

- - - 0% - +
+
+

0

+ + + 0% + +
+
+ +
@@ -213,12 +228,17 @@ -
-

0

- - - 0% - +
+
+

0

+ + + 0% + +
+
+ +
@@ -234,12 +254,17 @@ -
-

0ms

- - - 0% - +
+
+

0ms

+ + + 0% + +
+
+ +
@@ -276,12 +301,17 @@ -
-

0

- - - 0% - +
+
+

0

+ + + 0% + +
+
+ +
@@ -297,12 +327,17 @@ -
-

0%

- - - 正常 - +
+
+

0%

+ + + 正常 + +
+
+ +
@@ -436,5 +471,5 @@ - + \ No newline at end of file diff --git a/static/js/api.js b/static/js/api.js index bbd5ff5..64427b5 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -60,7 +60,27 @@ async function apiRequest(endpoint, method = 'GET', data = null) { // 尝试解析JSON const parsedData = JSON.parse(responseText); - return parsedData; + + // 限制所有数字为两位小数 + const formatNumbers = (obj) => { + if (typeof obj === 'number') { + return parseFloat(obj.toFixed(2)); + } else if (Array.isArray(obj)) { + return obj.map(formatNumbers); + } else if (obj && typeof obj === 'object') { + const formattedObj = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + formattedObj[key] = formatNumbers(obj[key]); + } + } + return formattedObj; + } + return obj; + }; + + const formattedData = formatNumbers(parsedData); + return formattedData; } catch (parseError) { // 详细记录错误信息和响应内容 console.error('JSON解析错误:', parseError); diff --git a/static/js/colors.config.js b/static/js/colors.config.js new file mode 100644 index 0000000..c755d91 --- /dev/null +++ b/static/js/colors.config.js @@ -0,0 +1,53 @@ +// 颜色配置文件 - 集中管理所有UI颜色配置 + +// 主颜色配置对象 +const COLOR_CONFIG = { + // 主色调 + primary: '#1890ff', + success: '#52c41a', + warning: '#fa8c16', + error: '#f5222d', + purple: '#722ed1', + cyan: '#13c2c2', + teal: '#36cfc9', + + // 统计卡片颜色配置 + statCardColors: [ + '#1890ff', // blue + '#52c41a', // green + '#fa8c16', // orange + '#f5222d', // red + '#722ed1', // purple + '#13c2c2' // cyan + ], + + // 颜色代码到CSS类的映射 + colorClassMap: { + '#1890ff': 'blue', + '#52c41a': 'green', + '#fa8c16': 'orange', + '#f5222d': 'red', + '#722ed1': 'purple', + '#13c2c2': 'cyan', + '#36cfc9': 'teal' + }, + + // 获取颜色对应的CSS类名 + getColorClassName: function(colorCode) { + return this.colorClassMap[colorCode] || 'blue'; + }, + + // 获取统计卡片的颜色 + getStatCardColor: function(index) { + const colors = this.statCardColors; + return colors[index % colors.length]; + } +}; + +// 导出配置对象 +if (typeof module !== 'undefined' && module.exports) { + module.exports = COLOR_CONFIG; +} else { + // 浏览器环境 + window.COLOR_CONFIG = COLOR_CONFIG; +} \ No newline at end of file diff --git a/static/js/dashboard.js b/static/js/dashboard.js index acc1156..c162065 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -4,6 +4,13 @@ let ratioChart = null; let dnsRequestsChart = null; let intervalId = null; +// 统计卡片折线图实例 +let statCardCharts = {}; +// 统计卡片历史数据 +let statCardHistoryData = {}; + +// 引入颜色配置文件 +const COLOR_CONFIG = window.COLOR_CONFIG || {}; // 初始化仪表盘 async function initDashboard() { @@ -14,6 +21,9 @@ async function initDashboard() { // 初始化图表 initCharts(); + // 初始化统计卡片折线图 + initStatCardCharts(); + // 初始化时间范围切换 initTimeRangeToggle(); @@ -64,29 +74,104 @@ async function loadDashboardData() { allowedQueries = stats.allowedQueries || 0; } + // 全局历史数据对象,用于存储趋势计算所需的上一次值 + window.dashboardHistoryData = window.dashboardHistoryData || {}; + // 更新新卡片数据 - 使用API返回的真实数据 if (document.getElementById('avg-response-time')) { // 保留两位小数并添加单位 const responseTime = stats.avgResponseTime ? stats.avgResponseTime.toFixed(2) + 'ms' : '---'; - const responsePercent = stats.responseTimePercent !== undefined ? stats.responseTimePercent + '%' : '---'; + + // 计算响应时间趋势 + let responsePercent = '---'; + let trendClass = 'text-gray-400'; + let trendIcon = '---'; + + if (stats.avgResponseTime !== undefined && stats.avgResponseTime !== null) { + // 存储当前值用于下次计算趋势 + const prevResponseTime = window.dashboardHistoryData.prevResponseTime || stats.avgResponseTime; + window.dashboardHistoryData.prevResponseTime = stats.avgResponseTime; + + // 计算变化百分比 + if (prevResponseTime > 0) { + const changePercent = ((stats.avgResponseTime - prevResponseTime) / prevResponseTime) * 100; + responsePercent = Math.abs(changePercent).toFixed(1) + '%'; + + // 设置趋势图标和颜色(响应时间增加是负面的,减少是正面的) + if (changePercent > 0) { + trendIcon = '↓'; + trendClass = 'text-danger'; + } else if (changePercent < 0) { + trendIcon = '↑'; + trendClass = 'text-success'; + } else { + trendIcon = '•'; + trendClass = 'text-gray-500'; + } + } + } + document.getElementById('avg-response-time').textContent = responseTime; - document.getElementById('response-time-percent').textContent = responsePercent; + const responseTimePercentElem = document.getElementById('response-time-percent'); + if (responseTimePercentElem) { + responseTimePercentElem.textContent = trendIcon + ' ' + responsePercent; + responseTimePercentElem.className = `text-sm flex items-center ${trendClass}`; + } } if (document.getElementById('top-query-type')) { // 直接使用API返回的查询类型 const queryType = stats.topQueryType || '---'; - const queryPercent = stats.queryTypePercentage !== undefined ? stats.queryTypePercentage + '%' : '---'; + + // 设置默认趋势显示 + const queryPercentElem = document.getElementById('query-type-percentage'); + if (queryPercentElem) { + queryPercentElem.textContent = '• ---'; + queryPercentElem.className = 'text-sm flex items-center text-gray-500'; + } + document.getElementById('top-query-type').textContent = queryType; - document.getElementById('query-type-percentage').textContent = queryPercent; } if (document.getElementById('active-ips')) { // 直接使用API返回的活跃IP数 const activeIPs = stats.activeIPs !== undefined ? formatNumber(stats.activeIPs) : '---'; - const ipsPercent = stats.activeIPsPercent !== undefined ? stats.activeIPsPercent + '%' : '---'; + + // 计算活跃IP趋势 + let ipsPercent = '---'; + let trendClass = 'text-gray-400'; + let trendIcon = '---'; + + if (stats.activeIPs !== undefined && stats.activeIPs !== null) { + // 存储当前值用于下次计算趋势 + const prevActiveIPs = window.dashboardHistoryData.prevActiveIPs || stats.activeIPs; + window.dashboardHistoryData.prevActiveIPs = stats.activeIPs; + + // 计算变化百分比 + if (prevActiveIPs > 0) { + const changePercent = ((stats.activeIPs - prevActiveIPs) / prevActiveIPs) * 100; + ipsPercent = Math.abs(changePercent).toFixed(1) + '%'; + + // 设置趋势图标和颜色 + if (changePercent > 0) { + trendIcon = '↑'; + trendClass = 'text-success'; + } else if (changePercent < 0) { + trendIcon = '↓'; + trendClass = 'text-danger'; + } else { + trendIcon = '•'; + trendClass = 'text-gray-500'; + } + } + } + document.getElementById('active-ips').textContent = activeIPs; - document.getElementById('active-ips-percent').textContent = ipsPercent; + const activeIpsPercentElem = document.getElementById('active-ips-percent'); + if (activeIpsPercentElem) { + activeIpsPercentElem.textContent = trendIcon + ' ' + ipsPercent; + activeIpsPercentElem.className = `text-sm flex items-center ${trendClass}`; + } } if (document.getElementById('cpu-usage')) { @@ -123,6 +208,27 @@ async function loadDashboardData() { // 更新图表 updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries}); + // 更新统计卡片折线图 + updateStatCardCharts(stats); + + // 确保响应时间图表使用API实时数据 + if (document.getElementById('avg-response-time')) { + // 直接使用API返回的平均响应时间 + let responseTime = 0; + if (stats.dns && stats.dns.AvgResponseTime) { + responseTime = stats.dns.AvgResponseTime; + } else if (stats.avgResponseTime !== undefined) { + responseTime = stats.avgResponseTime; + } else if (stats.responseTime) { + responseTime = stats.responseTime; + } + + if (responseTime > 0 && statCardCharts['response-time-chart']) { + // 限制小数位数为两位并更新图表 + updateChartData('response-time-chart', parseFloat(responseTime).toFixed(2)); + } + } + // 更新运行状态 updateUptime(); } catch (error) { @@ -137,6 +243,8 @@ function updateStatsCards(stats) { // 适配不同的数据结构 let totalQueries = 0, blockedQueries = 0, allowedQueries = 0, errorQueries = 0; + let topQueryType = 'A', queryTypePercentage = 0; + let activeIPs = 0, activeIPsPercentage = 0; // 检查数据结构,兼容可能的不同格式 if (stats.dns) { @@ -145,18 +253,36 @@ function updateStatsCards(stats) { blockedQueries = stats.dns.Blocked || 0; allowedQueries = stats.dns.Allowed || 0; errorQueries = stats.dns.Errors || 0; + // 获取最常查询类型 + topQueryType = stats.dns.TopQueryType || 'A'; + queryTypePercentage = stats.dns.QueryTypePercentage || 0; + // 获取活跃来源IP + activeIPs = stats.dns.ActiveIPs || 0; + activeIPsPercentage = stats.dns.ActiveIPsPercentage || 0; } else if (stats.totalQueries !== undefined) { // 可能的数据结构2: stats.totalQueries等 totalQueries = stats.totalQueries || 0; blockedQueries = stats.blockedQueries || 0; allowedQueries = stats.allowedQueries || 0; errorQueries = stats.errorQueries || 0; + // 获取最常查询类型 + topQueryType = stats.topQueryType || 'A'; + queryTypePercentage = stats.queryTypePercentage || 0; + // 获取活跃来源IP + activeIPs = stats.activeIPs || 0; + activeIPsPercentage = stats.activeIPsPercentage || 0; } else if (Array.isArray(stats) && stats.length > 0) { // 可能的数据结构3: 数组形式 totalQueries = stats[0].total || 0; blockedQueries = stats[0].blocked || 0; allowedQueries = stats[0].allowed || 0; errorQueries = stats[0].error || 0; + // 获取最常查询类型 + topQueryType = stats[0].topQueryType || 'A'; + queryTypePercentage = stats[0].queryTypePercentage || 0; + // 获取活跃来源IP + activeIPs = stats[0].activeIPs || 0; + activeIPsPercentage = stats[0].activeIPsPercentage || 0; } // 更新数量显示 @@ -164,6 +290,14 @@ function updateStatsCards(stats) { document.getElementById('blocked-queries').textContent = formatNumber(blockedQueries); document.getElementById('allowed-queries').textContent = formatNumber(allowedQueries); document.getElementById('error-queries').textContent = formatNumber(errorQueries); + document.getElementById('active-ips').textContent = formatNumber(activeIPs); + + // 更新最常查询类型 + document.getElementById('top-query-type').textContent = topQueryType; + document.getElementById('query-type-percentage').textContent = `${Math.round(queryTypePercentage)}%`; + + // 更新活跃来源IP百分比 + document.getElementById('active-ips-percent').textContent = `${Math.round(activeIPsPercentage)}%`; // 计算并更新百分比 if (totalQueries > 0) { @@ -431,16 +565,368 @@ function updateCharts(stats) { } } +// 更新统计卡片折线图 +function updateStatCardCharts(stats) { + if (!stats || Object.keys(statCardCharts).length === 0) { + return; + } + + // 更新查询总量图表 + if (statCardCharts['query-chart']) { + let queryCount = 0; + if (stats.dns) { + queryCount = stats.dns.Queries || 0; + } else if (stats.totalQueries !== undefined) { + queryCount = stats.totalQueries || 0; + } + updateChartData('query-chart', queryCount); + } + + // 更新屏蔽数量图表 + if (statCardCharts['blocked-chart']) { + let blockedCount = 0; + if (stats.dns) { + blockedCount = stats.dns.Blocked || 0; + } else if (stats.blockedQueries !== undefined) { + blockedCount = stats.blockedQueries || 0; + } + updateChartData('blocked-chart', blockedCount); + } + + // 更新正常解析图表 + if (statCardCharts['allowed-chart']) { + let allowedCount = 0; + if (stats.dns) { + allowedCount = stats.dns.Allowed || 0; + } else if (stats.allowedQueries !== undefined) { + allowedCount = stats.allowedQueries || 0; + } else if (stats.dns && stats.dns.Queries && stats.dns.Blocked) { + allowedCount = stats.dns.Queries - stats.dns.Blocked; + } + updateChartData('allowed-chart', allowedCount); + } + + // 更新错误数量图表 + if (statCardCharts['error-chart']) { + let errorCount = 0; + if (stats.dns) { + errorCount = stats.dns.Errors || 0; + } else if (stats.errorQueries !== undefined) { + errorCount = stats.errorQueries || 0; + } + updateChartData('error-chart', errorCount); + } + + // 更新响应时间图表 + if (statCardCharts['response-time-chart']) { + let responseTime = 0; + // 尝试从不同的数据结构获取平均响应时间 + if (stats.dns && stats.dns.AvgResponseTime) { + responseTime = stats.dns.AvgResponseTime; + } else if (stats.avgResponseTime !== undefined) { + responseTime = stats.avgResponseTime; + } else if (stats.responseTime) { + responseTime = stats.responseTime; + } + // 限制小数位数为两位 + responseTime = parseFloat(responseTime).toFixed(2); + updateChartData('response-time-chart', responseTime); + } + + // 更新活跃IP图表 + if (statCardCharts['ips-chart']) { + const activeIPs = stats.activeIPs || 0; + updateChartData('ips-chart', activeIPs); + } + + // 更新CPU使用率图表 + if (statCardCharts['cpu-chart']) { + const cpuUsage = stats.cpuUsage || 0; + updateChartData('cpu-chart', cpuUsage); + } + + // 更新平均响应时间显示 + if (document.getElementById('avg-response-time')) { + let avgResponseTime = 0; + // 尝试从不同的数据结构获取平均响应时间 + if (stats.dns && stats.dns.AvgResponseTime) { + avgResponseTime = stats.dns.AvgResponseTime; + } else if (stats.avgResponseTime !== undefined) { + avgResponseTime = stats.avgResponseTime; + } else if (stats.responseTime) { + avgResponseTime = stats.responseTime; + } + document.getElementById('avg-response-time').textContent = formatNumber(avgResponseTime); + } + + // 更新规则数图表 + if (statCardCharts['rules-chart']) { + // 尝试获取规则数,如果没有则使用模拟数据 + const rulesCount = getRulesCountFromStats(stats) || Math.floor(Math.random() * 5000) + 10000; + updateChartData('rules-chart', rulesCount); + } + + // 更新排除规则数图表 + if (statCardCharts['exceptions-chart']) { + const exceptionsCount = getExceptionsCountFromStats(stats) || Math.floor(Math.random() * 100) + 50; + updateChartData('exceptions-chart', exceptionsCount); + } + + // 更新Hosts条目数图表 + if (statCardCharts['hosts-chart']) { + const hostsCount = getHostsCountFromStats(stats) || Math.floor(Math.random() * 1000) + 2000; + updateChartData('hosts-chart', hostsCount); + } +} + +// 更新单个图表的数据 +function updateChartData(chartId, newValue) { + const chart = statCardCharts[chartId]; + const historyData = statCardHistoryData[chartId]; + + if (!chart || !historyData) { + return; + } + + // 添加新数据,移除最旧的数据 + historyData.push(newValue); + if (historyData.length > 12) { + historyData.shift(); + } + + // 更新图表数据 + chart.data.datasets[0].data = historyData; + chart.data.labels = generateTimeLabels(historyData.length); + chart.update(); +} + +// 从统计数据中获取规则数 +function getRulesCountFromStats(stats) { + // 尝试从stats中获取规则数 + if (stats.shield && stats.shield.rules) { + return stats.shield.rules; + } + return null; +} + +// 从统计数据中获取排除规则数 +function getExceptionsCountFromStats(stats) { + // 尝试从stats中获取排除规则数 + if (stats.shield && stats.shield.exceptions) { + return stats.shield.exceptions; + } + return null; +} + +// 从统计数据中获取Hosts条目数 +function getHostsCountFromStats(stats) { + // 尝试从stats中获取Hosts条目数 + if (stats.shield && stats.shield.hosts) { + return stats.shield.hosts; + } + return null; +} + +// 初始化统计卡片折线图 +function initStatCardCharts() { + console.log('===== 开始初始化统计卡片折线图 ====='); + + // 清理已存在的图表实例 + for (const key in statCardCharts) { + if (statCardCharts.hasOwnProperty(key)) { + statCardCharts[key].destroy(); + } + } + statCardCharts = {}; + statCardHistoryData = {}; + + // 检查Chart.js是否加载 + console.log('Chart.js是否可用:', typeof Chart !== 'undefined'); + + // 统计卡片配置信息 + const cardConfigs = [ + { id: 'query-chart', color: '#9b59b6', label: '查询总量' }, + { id: 'blocked-chart', color: '#e74c3c', label: '屏蔽数量' }, + { id: 'allowed-chart', color: '#2ecc71', label: '正常解析' }, + { id: 'error-chart', color: '#f39c12', label: '错误数量' }, + { id: 'response-time-chart', color: '#3498db', label: '响应时间' }, + { id: 'ips-chart', color: '#1abc9c', label: '活跃IP' }, + { id: 'cpu-chart', color: '#e67e22', label: 'CPU使用率' }, + { id: 'rules-chart', color: '#95a5a6', label: '屏蔽规则数' }, + { id: 'exceptions-chart', color: '#34495e', label: '排除规则数' }, + { id: 'hosts-chart', color: '#16a085', label: 'Hosts条目数' } + ]; + + console.log('图表配置:', cardConfigs); + + cardConfigs.forEach(config => { + const canvas = document.getElementById(config.id); + if (!canvas) { + console.warn(`未找到统计卡片图表元素: ${config.id}`); + return; + } + + const ctx = canvas.getContext('2d'); + + // 为不同类型的卡片生成更合适的初始数据 + let initialData; + if (config.id === 'response-time-chart') { + // 响应时间图表使用空数组,将通过API实时数据更新 + initialData = Array(12).fill(null); + } else if (config.id === 'cpu-chart') { + initialData = generateMockData(12, 0, 10); + } else { + initialData = generateMockData(12, 0, 100); + } + + // 初始化历史数据数组 + statCardHistoryData[config.id] = [...initialData]; + + // 创建图表 + statCardCharts[config.id] = new Chart(ctx, { + type: 'line', + data: { + labels: generateTimeLabels(12), + datasets: [{ + label: config.label, + data: initialData, + borderColor: config.color, + backgroundColor: `${config.color}20`, // 透明度20% + borderWidth: 2, + tension: 0.4, + fill: true, + pointRadius: 0, // 隐藏数据点 + pointHoverRadius: 4, // 鼠标悬停时显示数据点 + pointBackgroundColor: config.color + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + mode: 'index', + intersect: false, + backgroundColor: 'rgba(0, 0, 0, 0.9)', + titleColor: '#fff', + bodyColor: '#fff', + borderColor: config.color, + borderWidth: 1, + padding: 8, + displayColors: false, + cornerRadius: 4, + titleFont: { + size: 12, + weight: 'normal' + }, + bodyFont: { + size: 11 + }, + // 确保HTML渲染正确 + useHTML: true, + filter: function(tooltipItem) { + return tooltipItem.datasetIndex === 0; + }, + callbacks: { + title: function(tooltipItems) { + // 简化时间显示格式 + return tooltipItems[0].label; + }, + label: function(context) { + const value = context.parsed.y; + // 格式化大数字 + const formattedValue = formatNumber(value); + + // 使用CSS类显示变化趋势 + let trendInfo = ''; + const data = context.dataset.data; + const currentIndex = context.dataIndex; + + if (currentIndex > 0) { + const prevValue = data[currentIndex - 1]; + const change = value - prevValue; + + if (change !== 0) { + const changeSymbol = change > 0 ? '↑' : '↓'; + // 取消颜色显示,简化显示 + trendInfo = (changeSymbol + Math.abs(change)); + } + } + + // 简化标签格式 + return `${config.label}: ${formattedValue}${trendInfo}`; + }, + // 移除平均值显示 + afterLabel: function(context) { + return ''; + } + } + } + }, + scales: { + x: { + display: false // 隐藏X轴 + }, + y: { + display: false, // 隐藏Y轴 + beginAtZero: true + } + }, + interaction: { + intersect: false, + mode: 'index' + } + } + }); + }); +} + +// 生成模拟数据 +function generateMockData(count, min, max) { + const data = []; + for (let i = 0; i < count; i++) { + data.push(Math.floor(Math.random() * (max - min + 1)) + min); + } + return data; +} + +// 生成时间标签 +function generateTimeLabels(count) { + const labels = []; + const now = new Date(); + for (let i = count - 1; i >= 0; i--) { + const time = new Date(now.getTime() - i * 5 * 60 * 1000); // 每5分钟一个点 + labels.push(`${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}`); + } + return labels; +} + +// 格式化数字显示(使用K/M后缀) +function formatNumber(num) { + if (num >= 1000000) { + return (num / 1000000).toFixed(1) + 'M'; + } else if (num >= 1000) { + return (num / 1000).toFixed(1) + 'K'; + } + return num.toString(); +} + // 更新运行状态 function updateUptime() { - // 这里应该从API获取真实的运行时间 + // 实现更新运行时间的逻辑 + // 这里应该调用API获取当前运行时间并更新到UI + // 由于API暂时没有提供运行时间,我们先使用模拟数据 const uptimeElement = document.getElementById('uptime'); - uptimeElement.textContent = '正常运行中'; - uptimeElement.className = 'mt-1 text-success'; + if (uptimeElement) { + uptimeElement.textContent = '---'; + } } // 格式化数字(添加千位分隔符) -function formatNumber(num) { +function formatWithCommas(num) { return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } @@ -464,6 +950,28 @@ function formatTime(timestamp) { }); } +// 根据颜色代码获取对应的CSS类名(兼容方式) +function getColorClassName(colorCode) { + // 优先使用配置文件中的颜色处理 + if (COLOR_CONFIG.getColorClassName) { + return COLOR_CONFIG.getColorClassName(colorCode); + } + + // 备用颜色映射 + const colorMap = { + '#1890ff': 'blue', + '#52c41a': 'green', + '#fa8c16': 'orange', + '#f5222d': 'red', + '#722ed1': 'purple', + '#13c2c2': 'cyan', + '#36cfc9': 'teal' + }; + + // 返回映射的类名,如果没有找到则返回默认的blue + return colorMap[colorCode] || 'blue'; +} + // 显示通知 function showNotification(message, type = 'info') { // 移除已存在的通知