mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
feat(database): 添加用户角色权限系统及相关监控功能
- 创建用户表(users)包含基本信息和认证字段 - 创建角色表(roles)用于权限控制 - 创建权限表(permissions)定义系统权限 - 创建用户角色关联表(user_roles)建立用户与角色关系 - 创建角色权限关联表(role_permissions)建立角色与权限关系 - 创建迁移记录表(migrations)追踪数据库变更 - 添加AdminController提供管理员面板功能 - 实现系统监控、配置管理、缓存清理等功能 - 添加AOP切面编程支持的各种通知类型 - 实现告警管理AlertManager支持多渠道告警 - 添加文档注解接口规范
This commit is contained in:
9
public/index.php
Normal file
9
public/index.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../fendx-framework/fendx-starter/src/Bootstrap.php';
|
||||
|
||||
use Fendx\Starter\Bootstrap;
|
||||
|
||||
$app = new Bootstrap();
|
||||
$app->run();
|
||||
649
public/monitor/dashboard.js
Normal file
649
public/monitor/dashboard.js
Normal file
@@ -0,0 +1,649 @@
|
||||
// 全局变量
|
||||
let currentSection = 'dashboard';
|
||||
let refreshInterval = null;
|
||||
let charts = {};
|
||||
|
||||
// API基础URL
|
||||
const API_BASE = '/monitor';
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeDashboard();
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
// 初始化仪表盘
|
||||
function initializeDashboard() {
|
||||
showSection('dashboard');
|
||||
loadDashboardData();
|
||||
}
|
||||
|
||||
// 显示指定区域
|
||||
function showSection(section) {
|
||||
// 隐藏所有区域
|
||||
document.querySelectorAll('.section').forEach(el => {
|
||||
el.classList.add('hidden');
|
||||
});
|
||||
|
||||
// 显示指定区域
|
||||
document.getElementById(section + '-section').classList.remove('hidden');
|
||||
|
||||
// 更新侧边栏状态
|
||||
document.querySelectorAll('.sidebar-item').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
event.currentTarget.classList.add('active');
|
||||
|
||||
// 更新标题
|
||||
const titles = {
|
||||
'dashboard': '仪表盘',
|
||||
'health': '健康检查',
|
||||
'metrics': '性能指标',
|
||||
'errors': '错误追踪',
|
||||
'logs': '日志分析',
|
||||
'alerts': '告警管理',
|
||||
'settings': '系统设置'
|
||||
};
|
||||
document.getElementById('section-title').textContent = titles[section];
|
||||
|
||||
currentSection = section;
|
||||
|
||||
// 加载对应数据
|
||||
loadSectionData(section);
|
||||
}
|
||||
|
||||
// 加载区域数据
|
||||
function loadSectionData(section) {
|
||||
switch(section) {
|
||||
case 'dashboard':
|
||||
loadDashboardData();
|
||||
break;
|
||||
case 'health':
|
||||
loadHealthData();
|
||||
break;
|
||||
case 'metrics':
|
||||
loadMetricsData();
|
||||
break;
|
||||
case 'errors':
|
||||
loadErrorsData();
|
||||
break;
|
||||
case 'logs':
|
||||
// 日志数据需要用户主动搜索
|
||||
break;
|
||||
case 'alerts':
|
||||
loadAlertsData();
|
||||
break;
|
||||
case 'settings':
|
||||
loadSettingsData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载仪表盘数据
|
||||
async function loadDashboardData() {
|
||||
try {
|
||||
// 加载健康状态
|
||||
const healthResponse = await fetch(`${API_BASE}/health`);
|
||||
const health = await healthResponse.json();
|
||||
updateDashboardHealth(health.data);
|
||||
|
||||
// 加载指标数据
|
||||
const metricsResponse = await fetch(`${API_BASE}/metrics`);
|
||||
const metrics = await metricsResponse.json();
|
||||
updateDashboardMetrics(metrics.data);
|
||||
|
||||
// 加载告警数据
|
||||
const alertsResponse = await fetch(`${API_BASE}/alerts/active`);
|
||||
const alerts = await alertsResponse.json();
|
||||
updateDashboardAlerts(alerts.data);
|
||||
|
||||
// 加载图表数据
|
||||
loadDashboardCharts();
|
||||
|
||||
// 更新时间
|
||||
updateLastUpdateTime();
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载仪表盘数据失败:', error);
|
||||
showError('加载仪表盘数据失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新仪表盘健康状态
|
||||
function updateDashboardHealth(health) {
|
||||
const statusEl = document.getElementById('system-status');
|
||||
const statusClass = health.status === 'healthy' ? 'status-healthy' :
|
||||
health.status === 'warning' ? 'status-warning' : 'status-critical';
|
||||
|
||||
statusEl.textContent = health.status === 'healthy' ? '健康' :
|
||||
health.status === 'warning' ? '警告' : '严重';
|
||||
statusEl.className = `text-2xl font-bold ${statusClass}`;
|
||||
}
|
||||
|
||||
// 更新仪表盘指标
|
||||
function updateDashboardMetrics(metrics) {
|
||||
if (metrics.system) {
|
||||
const memoryUsage = metrics.system.memory_usage;
|
||||
const memoryLimit = metrics.system.memory_limit;
|
||||
const memoryPercent = memoryLimit > 0 ? (memoryUsage / memoryLimit * 100).toFixed(1) : 0;
|
||||
|
||||
document.getElementById('memory-usage').textContent = memoryPercent + '%';
|
||||
}
|
||||
|
||||
// 计算平均响应时间
|
||||
if (metrics.histograms && metrics.histograms.http_request_duration) {
|
||||
const avgTime = metrics.histograms.http_request_duration.mean || 0;
|
||||
document.getElementById('response-time').textContent = avgTime.toFixed(2) + 'ms';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新仪表盘告警
|
||||
function updateDashboardAlerts(alerts) {
|
||||
document.getElementById('active-alerts').textContent = alerts.length;
|
||||
|
||||
const recentAlertsEl = document.getElementById('recent-alerts');
|
||||
if (alerts.length === 0) {
|
||||
recentAlertsEl.innerHTML = '<p class="text-gray-500 text-center py-4">暂无告警</p>';
|
||||
} else {
|
||||
recentAlertsEl.innerHTML = alerts.slice(0, 5).map(alert => `
|
||||
<div class="alert-item flex items-center justify-between p-3 bg-${alert.severity === 'critical' ? 'red' : 'yellow'}-50 border border-${alert.severity === 'critical' ? 'red' : 'yellow'}-200 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<div class="w-2 h-2 bg-${alert.severity === 'critical' ? 'red' : 'yellow'}-500 rounded-full mr-3"></div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800">${alert.type}</p>
|
||||
<p class="text-sm text-gray-600">${alert.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-xs text-gray-500">${formatTime(alert.timestamp)}</p>
|
||||
<button onclick="acknowledgeAlert('${alert.id}')" class="text-xs text-blue-600 hover:text-blue-800">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载仪表盘图表
|
||||
async function loadDashboardCharts() {
|
||||
try {
|
||||
// 加载请求数据
|
||||
const statsResponse = await fetch(`${API_BASE}/stats`);
|
||||
const stats = await statsResponse.json();
|
||||
|
||||
// 请求趋势图
|
||||
if (charts.requests) {
|
||||
charts.requests.destroy();
|
||||
}
|
||||
|
||||
const requestsChart = {
|
||||
series: [{
|
||||
name: '请求数',
|
||||
data: generateTimeSeriesData()
|
||||
}],
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 300,
|
||||
toolbar: { show: false }
|
||||
},
|
||||
dataLabels: { enabled: false },
|
||||
stroke: { curve: 'smooth', width: 2 },
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
opacityFrom: 0.7,
|
||||
opacityTo: 0.3
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
categories: generateTimeLabels()
|
||||
},
|
||||
yaxis: { title: { text: '请求数' } },
|
||||
colors: ['#3b82f6']
|
||||
};
|
||||
|
||||
charts.requests = new ApexCharts(document.getElementById('requests-chart'), requestsChart);
|
||||
charts.requests.render();
|
||||
|
||||
// 错误分布图
|
||||
if (charts.errors) {
|
||||
charts.errors.destroy();
|
||||
}
|
||||
|
||||
const errorsData = stats.data?.system?.error_count || 0;
|
||||
const errorsChart = {
|
||||
series: [errorsData, Math.max(0, 100 - errorsData)],
|
||||
chart: {
|
||||
type: 'donut',
|
||||
height: 300
|
||||
},
|
||||
labels: ['错误', '正常'],
|
||||
colors: ['#ef4444', '#10b981'],
|
||||
legend: { position: 'bottom' },
|
||||
dataLabels: { enabled: true }
|
||||
};
|
||||
|
||||
charts.errors = new ApexCharts(document.getElementById('errors-chart'), errorsChart);
|
||||
charts.errors.render();
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载图表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载健康检查数据
|
||||
async function loadHealthData() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/health`);
|
||||
const result = await response.json();
|
||||
|
||||
const healthChecksEl = document.getElementById('health-checks');
|
||||
const checks = result.data.checks;
|
||||
|
||||
healthChecksEl.innerHTML = Object.entries(checks).map(([name, check]) => `
|
||||
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 bg-${check.status === 'healthy' ? 'green' : check.status === 'warning' ? 'yellow' : 'red'}-500 rounded-full mr-3"></div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-800">${getComponentName(name)}</p>
|
||||
<p class="text-sm text-gray-600">${check.message || '运行正常'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="px-2 py-1 text-xs font-medium rounded-full bg-${check.status === 'healthy' ? 'green' : check.status === 'warning' ? 'yellow' : 'red'}-100 text-${check.status === 'healthy' ? 'green' : check.status === 'warning' ? 'yellow' : 'red'}-800">
|
||||
${check.status === 'healthy' ? '健康' : check.status === 'warning' ? '警告' : '严重'}
|
||||
</span>
|
||||
<p class="text-xs text-gray-500 mt-1">${check.duration ? check.duration.toFixed(3) + 's' : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载健康检查数据失败:', error);
|
||||
showError('加载健康检查数据失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载指标数据
|
||||
async function loadMetricsData() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/metrics`);
|
||||
const result = await response.json();
|
||||
const metrics = result.data;
|
||||
|
||||
// 系统指标
|
||||
const systemMetricsEl = document.getElementById('system-metrics');
|
||||
if (metrics.system) {
|
||||
systemMetricsEl.innerHTML = `
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">内存使用</span>
|
||||
<span class="font-medium">${formatBytes(metrics.system.memory_usage)} / ${formatBytes(metrics.system.memory_limit)}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">CPU使用率</span>
|
||||
<span class="font-medium">${(metrics.system.cpu_usage * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">运行时间</span>
|
||||
<span class="font-medium">${formatUptime(metrics.system.uptime)}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">负载均衡</span>
|
||||
<span class="font-medium">${metrics.system.load_avg ? metrics.system.load_avg[0].toFixed(2) : 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 应用指标
|
||||
const appMetricsEl = document.getElementById('application-metrics');
|
||||
const totalRequests = Object.values(metrics.counters || {}).reduce((sum, count) => sum + count, 0);
|
||||
const totalErrors = Object.values(metrics.counters || {}).filter(key => key.includes('error')).reduce((sum, count) => sum + count, 0);
|
||||
|
||||
appMetricsEl.innerHTML = `
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">总请求数</span>
|
||||
<span class="font-medium">${totalRequests}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">总错误数</span>
|
||||
<span class="font-medium">${totalErrors}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">成功率</span>
|
||||
<span class="font-medium">${totalRequests > 0 ? ((totalRequests - totalErrors) / totalRequests * 100).toFixed(1) : 100}%</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">活跃连接</span>
|
||||
<span class="font-medium">${metrics.gauges?.requests_in_progress || 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载指标数据失败:', error);
|
||||
showError('加载指标数据失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载错误数据
|
||||
async function loadErrorsData() {
|
||||
try {
|
||||
const filter = document.getElementById('error-filter').value;
|
||||
const response = await fetch(`${API_BASE}/errors${filter ? '?severity=' + filter : ''}`);
|
||||
const result = await response.json();
|
||||
|
||||
const errorsListEl = document.getElementById('errors-list');
|
||||
if (result.data.length === 0) {
|
||||
errorsListEl.innerHTML = '<p class="text-gray-500 text-center py-4">暂无错误记录</p>';
|
||||
} else {
|
||||
errorsListEl.innerHTML = result.data.map(error => `
|
||||
<div class="border rounded-lg p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center mb-2">
|
||||
<span class="px-2 py-1 text-xs font-medium rounded-full bg-${error.severity === 'critical' ? 'red' : error.severity === 'warning' ? 'yellow' : 'blue'}-100 text-${error.severity === 'critical' ? 'red' : error.severity === 'warning' ? 'yellow' : 'blue'}-800">
|
||||
${error.severity.toUpperCase()}
|
||||
</span>
|
||||
<span class="ml-2 text-sm text-gray-500">${error.type}</span>
|
||||
</div>
|
||||
<p class="text-gray-800 mb-2">${error.message}</p>
|
||||
<div class="text-sm text-gray-600">
|
||||
<p>文件: ${error.file || 'N/A'}:${error.line || 'N/A'}</p>
|
||||
<p>时间: ${formatTime(error.timestamp)}</p>
|
||||
<p>Trace ID: ${error.trace_id || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载错误数据失败:', error);
|
||||
showError('加载错误数据失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索日志
|
||||
async function searchLogs() {
|
||||
const level = document.getElementById('log-level').value;
|
||||
const search = document.getElementById('log-search').value;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (level) params.append('level', level);
|
||||
if (search) params.append('message', search);
|
||||
params.append('limit', '50');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/logs/search?${params}`);
|
||||
const result = await response.json();
|
||||
|
||||
const logsContainerEl = document.getElementById('logs-container');
|
||||
if (result.data.logs.length === 0) {
|
||||
logsContainerEl.innerHTML = '<p class="text-gray-500 text-center py-4">未找到匹配的日志</p>';
|
||||
} else {
|
||||
logsContainerEl.innerHTML = result.data.logs.map(log => `
|
||||
<div class="border-l-4 border-${getLogLevelColor(log.level)} pl-4 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<span class="px-2 py-1 text-xs font-medium rounded bg-${getLogLevelColor(log.level)}-100 text-${getLogLevelColor(log.level)}-800">
|
||||
${log.level}
|
||||
</span>
|
||||
<span class="ml-2 text-sm text-gray-500">${log.datetime}</span>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">${log.trace_id || ''}</span>
|
||||
</div>
|
||||
<p class="text-gray-800 mt-1">${log.message}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('搜索日志失败:', error);
|
||||
showError('搜索日志失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载告警数据
|
||||
async function loadAlertsData() {
|
||||
try {
|
||||
const status = document.getElementById('alert-status').value;
|
||||
const response = await fetch(`${API_BASE}/alerts${status ? '?status=' + status : ''}`);
|
||||
const result = await response.json();
|
||||
|
||||
const alertsListEl = document.getElementById('alerts-list');
|
||||
if (result.data.length === 0) {
|
||||
alertsListEl.innerHTML = '<p class="text-gray-500 text-center py-4">暂无告警</p>';
|
||||
} else {
|
||||
alertsListEl.innerHTML = result.data.map(alert => `
|
||||
<div class="border rounded-lg p-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="w-3 h-3 bg-${alert.severity === 'critical' ? 'red' : 'yellow'}-500 rounded-full mr-2"></div>
|
||||
<span class="font-medium text-gray-800">${alert.type}</span>
|
||||
<span class="ml-2 px-2 py-1 text-xs font-medium rounded-full bg-${alert.status === 'active' ? 'red' : alert.status === 'acknowledged' ? 'yellow' : 'green'}-100 text-${alert.status === 'active' ? 'red' : alert.status === 'acknowledged' ? 'yellow' : 'green'}-800">
|
||||
${alert.status === 'active' ? '活跃' : alert.status === 'acknowledged' ? '已确认' : '已解决'}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-gray-800 mb-2">${alert.message}</p>
|
||||
<p class="text-sm text-gray-500">时间: ${formatTime(alert.timestamp)}</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
${alert.status === 'active' ? `
|
||||
<button onclick="acknowledgeAlert('${alert.id}')" class="text-sm text-blue-600 hover:text-blue-800">确认</button>
|
||||
<button onclick="resolveAlert('${alert.id}')" class="text-sm text-green-600 hover:text-green-800">解决</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载告警数据失败:', error);
|
||||
showError('加载告警数据失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设置数据
|
||||
async function loadSettingsData() {
|
||||
try {
|
||||
// 这里可以加载实际的配置数据
|
||||
// 暂时使用默认值
|
||||
document.getElementById('sample-rate').value = '1.0';
|
||||
document.getElementById('retention').value = '3600';
|
||||
document.getElementById('error-threshold').value = '0.05';
|
||||
document.getElementById('memory-threshold').value = '0.8';
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载设置数据失败:', error);
|
||||
showError('加载设置数据失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 确认告警
|
||||
async function acknowledgeAlert(alertId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/alerts/${alertId}/acknowledge`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ acknowledged_by: 'admin' })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showSuccess('告警已确认');
|
||||
if (currentSection === 'dashboard') {
|
||||
loadDashboardData();
|
||||
} else if (currentSection === 'alerts') {
|
||||
loadAlertsData();
|
||||
}
|
||||
} else {
|
||||
showError('确认告警失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('确认告警失败:', error);
|
||||
showError('确认告警失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 解决告警
|
||||
async function resolveAlert(alertId) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/alerts/${alertId}/resolve`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showSuccess('告警已解决');
|
||||
if (currentSection === 'dashboard') {
|
||||
loadDashboardData();
|
||||
} else if (currentSection === 'alerts') {
|
||||
loadAlertsData();
|
||||
}
|
||||
} else {
|
||||
showError('解决告警失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解决告警失败:', error);
|
||||
showError('解决告警失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
async function saveSettings() {
|
||||
try {
|
||||
const settings = {
|
||||
sample_rate: parseFloat(document.getElementById('sample-rate').value),
|
||||
retention: parseInt(document.getElementById('retention').value),
|
||||
error_threshold: parseFloat(document.getElementById('error-threshold').value),
|
||||
memory_threshold: parseFloat(document.getElementById('memory-threshold').value)
|
||||
};
|
||||
|
||||
// 这里应该发送到后端保存
|
||||
console.log('保存设置:', settings);
|
||||
showSuccess('设置已保存');
|
||||
|
||||
} catch (error) {
|
||||
console.error('保存设置失败:', error);
|
||||
showError('保存设置失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
function refreshData() {
|
||||
loadSectionData(currentSection);
|
||||
}
|
||||
|
||||
// 刷新错误数据
|
||||
function refreshErrors() {
|
||||
loadErrorsData();
|
||||
}
|
||||
|
||||
// 刷新告警数据
|
||||
function refreshAlerts() {
|
||||
loadAlertsData();
|
||||
}
|
||||
|
||||
// 开始自动刷新
|
||||
function startAutoRefresh() {
|
||||
refreshInterval = setInterval(() => {
|
||||
if (currentSection !== 'settings') {
|
||||
loadSectionData(currentSection);
|
||||
}
|
||||
}, 30000); // 30秒刷新一次
|
||||
}
|
||||
|
||||
// 停止自动刷新
|
||||
function stopAutoRefresh() {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新最后更新时间
|
||||
function updateLastUpdateTime() {
|
||||
document.getElementById('last-update').textContent = '最后更新: ' + new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function formatTime(timestamp) {
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function 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}小时`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}小时 ${minutes}分钟`;
|
||||
} else {
|
||||
return `${minutes}分钟`;
|
||||
}
|
||||
}
|
||||
|
||||
function getComponentName(name) {
|
||||
const names = {
|
||||
'database': '数据库',
|
||||
'cache': '缓存',
|
||||
'filesystem': '文件系统',
|
||||
'memory': '内存',
|
||||
'disk': '磁盘',
|
||||
'external_services': '外部服务'
|
||||
};
|
||||
return names[name] || name;
|
||||
}
|
||||
|
||||
function getLogLevelColor(level) {
|
||||
const colors = {
|
||||
'DEBUG': 'gray',
|
||||
'INFO': 'blue',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'red'
|
||||
};
|
||||
return colors[level] || 'gray';
|
||||
}
|
||||
|
||||
function generateTimeSeriesData() {
|
||||
// 生成模拟数据
|
||||
return Array.from({length: 24}, () => Math.floor(Math.random() * 100) + 50);
|
||||
}
|
||||
|
||||
function generateTimeLabels() {
|
||||
const labels = [];
|
||||
const now = new Date();
|
||||
for (let i = 23; i >= 0; i--) {
|
||||
const time = new Date(now.getTime() - i * 60 * 60 * 1000);
|
||||
labels.push(time.getHours() + ':00');
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
// 显示成功消息
|
||||
console.log('Success:', message);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
// 显示错误消息
|
||||
console.error('Error:', message);
|
||||
}
|
||||
|
||||
// 页面卸载时清理
|
||||
window.addEventListener('beforeunload', stopAutoRefresh);
|
||||
363
public/monitor/index.html
Normal file
363
public/monitor/index.html
Normal file
@@ -0,0 +1,363 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FendxPHP 运维管理面板</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/apexcharts@3.35.0/dist/apexcharts.css" rel="stylesheet">
|
||||
<style>
|
||||
.status-healthy { color: #10b981; }
|
||||
.status-warning { color: #f59e0b; }
|
||||
.status-critical { color: #ef4444; }
|
||||
.chart-container { position: relative; height: 300px; }
|
||||
.metric-card { transition: all 0.3s ease; }
|
||||
.metric-card:hover { transform: translateY(-2px); box-shadow: 0 10px 25px rgba(0,0,0,0.1); }
|
||||
.sidebar-item { transition: all 0.2s ease; }
|
||||
.sidebar-item:hover { background-color: #f3f4f6; }
|
||||
.sidebar-item.active { background-color: #3b82f6; color: white; }
|
||||
.refresh-btn { animation: spin 2s linear infinite; }
|
||||
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
.alert-item { animation: slideIn 0.3s ease; }
|
||||
@keyframes slideIn { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<div class="flex h-screen">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="w-64 bg-white shadow-lg">
|
||||
<div class="p-6 border-b">
|
||||
<h1 class="text-xl font-bold text-gray-800">FendxPHP 监控</h1>
|
||||
<p class="text-sm text-gray-600">运维管理面板</p>
|
||||
</div>
|
||||
<nav class="p-4">
|
||||
<div class="sidebar-item active rounded-lg p-3 mb-2 cursor-pointer" onclick="showSection('dashboard')">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
<span>仪表盘</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-item rounded-lg p-3 mb-2 cursor-pointer" onclick="showSection('health')">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>健康检查</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-item rounded-lg p-3 mb-2 cursor-pointer" onclick="showSection('metrics')">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
<span>性能指标</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-item rounded-lg p-3 mb-2 cursor-pointer" onclick="showSection('errors')">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>错误追踪</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-item rounded-lg p-3 mb-2 cursor-pointer" onclick="showSection('logs')">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
<span>日志分析</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-item rounded-lg p-3 mb-2 cursor-pointer" onclick="showSection('alerts')">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
|
||||
</svg>
|
||||
<span>告警管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-item rounded-lg p-3 mb-2 cursor-pointer" onclick="showSection('settings')">
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span>系统设置</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<!-- 顶部栏 -->
|
||||
<div class="bg-white shadow-sm border-b px-6 py-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center">
|
||||
<h2 id="section-title" class="text-xl font-semibold text-gray-800">仪表盘</h2>
|
||||
<span id="last-update" class="ml-4 text-sm text-gray-500">最后更新: --</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button onclick="refreshData()" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
刷新
|
||||
</button>
|
||||
<div class="flex items-center">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
|
||||
<span class="text-sm text-gray-600">系统正常</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="p-6">
|
||||
<!-- 仪表盘 -->
|
||||
<div id="dashboard-section" class="section">
|
||||
<!-- 概览卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="metric-card bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">系统状态</p>
|
||||
<p id="system-status" class="text-2xl font-bold status-healthy">健康</p>
|
||||
</div>
|
||||
<div class="p-3 bg-green-100 rounded-full">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">活跃告警</p>
|
||||
<p id="active-alerts" class="text-2xl font-bold text-yellow-600">0</p>
|
||||
</div>
|
||||
<div class="p-3 bg-yellow-100 rounded-full">
|
||||
<svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">内存使用</p>
|
||||
<p id="memory-usage" class="text-2xl font-bold text-blue-600">0%</p>
|
||||
</div>
|
||||
<div class="p-3 bg-blue-100 rounded-full">
|
||||
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">响应时间</p>
|
||||
<p id="response-time" class="text-2xl font-bold text-green-600">0ms</p>
|
||||
</div>
|
||||
<div class="p-3 bg-green-100 rounded-full">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">请求趋势</h3>
|
||||
<div class="chart-container">
|
||||
<div id="requests-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">错误分布</h3>
|
||||
<div class="chart-container">
|
||||
<div id="errors-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近告警 -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">最近告警</h3>
|
||||
<div id="recent-alerts" class="space-y-3">
|
||||
<p class="text-gray-500 text-center py-4">暂无告警</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 健康检查 -->
|
||||
<div id="health-section" class="section hidden">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">系统健康检查</h3>
|
||||
<div id="health-checks" class="space-y-4">
|
||||
<div class="text-center py-8">
|
||||
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
<p class="text-gray-500">正在加载健康检查数据...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能指标 -->
|
||||
<div id="metrics-section" class="section hidden">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">系统指标</h3>
|
||||
<div id="system-metrics" class="space-y-4">
|
||||
<div class="text-center py-8">
|
||||
<p class="text-gray-500">正在加载系统指标...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">应用指标</h3>
|
||||
<div id="application-metrics" class="space-y-4">
|
||||
<div class="text-center py-8">
|
||||
<p class="text-gray-500">正在加载应用指标...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误追踪 -->
|
||||
<div id="errors-section" class="section hidden">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">错误追踪</h3>
|
||||
<div class="flex space-x-2">
|
||||
<select id="error-filter" class="px-3 py-2 border rounded-lg">
|
||||
<option value="">所有级别</option>
|
||||
<option value="critical">严重</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="info">信息</option>
|
||||
</select>
|
||||
<button onclick="refreshErrors()" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="errors-list" class="space-y-3">
|
||||
<div class="text-center py-8">
|
||||
<p class="text-gray-500">正在加载错误数据...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志分析 -->
|
||||
<div id="logs-section" class="section hidden">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">日志分析</h3>
|
||||
<div class="flex space-x-2">
|
||||
<select id="log-level" class="px-3 py-2 border rounded-lg">
|
||||
<option value="">所有级别</option>
|
||||
<option value="ERROR">错误</option>
|
||||
<option value="WARNING">警告</option>
|
||||
<option value="INFO">信息</option>
|
||||
<option value="DEBUG">调试</option>
|
||||
</select>
|
||||
<input type="text" id="log-search" placeholder="搜索日志..." class="px-3 py-2 border rounded-lg">
|
||||
<button onclick="searchLogs()" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logs-container" class="space-y-2">
|
||||
<div class="text-center py-8">
|
||||
<p class="text-gray-500">请输入搜索条件</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 告警管理 -->
|
||||
<div id="alerts-section" class="section hidden">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">告警管理</h3>
|
||||
<div class="flex space-x-2">
|
||||
<select id="alert-status" class="px-3 py-2 border rounded-lg">
|
||||
<option value="">所有状态</option>
|
||||
<option value="active">活跃</option>
|
||||
<option value="acknowledged">已确认</option>
|
||||
<option value="resolved">已解决</option>
|
||||
</select>
|
||||
<button onclick="refreshAlerts()" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="alerts-list" class="space-y-3">
|
||||
<div class="text-center py-8">
|
||||
<p class="text-gray-500">正在加载告警数据...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统设置 -->
|
||||
<div id="settings-section" class="section hidden">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">系统设置</h3>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h4 class="text-md font-medium mb-3">监控配置</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">采样率</label>
|
||||
<input type="number" id="sample-rate" class="w-full px-3 py-2 border rounded-lg" value="1.0" step="0.1" min="0" max="1">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">数据保留时间(秒)</label>
|
||||
<input type="number" id="retention" class="w-full px-3 py-2 border rounded-lg" value="3600" step="60">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-md font-medium mb-3">告警阈值</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">错误率阈值</label>
|
||||
<input type="number" id="error-threshold" class="w-full px-3 py-2 border rounded-lg" value="0.05" step="0.01" min="0" max="1">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">内存使用阈值</label>
|
||||
<input type="number" id="memory-threshold" class="w-full px-3 py-2 border rounded-lg" value="0.8" step="0.1" min="0" max="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button onclick="saveSettings()" class="px-6 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600">
|
||||
保存设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.35.0/dist/apexcharts.min.js"></script>
|
||||
<script src="dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user