Files
myaps_api/static/monitor/history-logs.html
T
chaoge c0b0707c24 fix(monitor logs): 修复历史日志筛选逻辑并完善重置功能
将筛选条件绑定从queryType改为当前活动标签页,确保选中全部数据时也能应用当前页签的筛选规则,同时补充了发送请求页签的过滤条件重置逻辑
2026-05-26 15:07:25 +08:00

2716 lines
129 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n-page-title="page.history_logs">日志历史查询 - 系统监控</title>
<link rel="icon" href="/static/swagger/favicon.png" type="image/png">
<link rel="stylesheet" href="/static/monitor/css/monitor.css">
<script src="/static/lib/i18n/i18n.js"></script>
<link rel="stylesheet" href="/static/mds/css/bootstrap-icons.css">
<style>
body {
font-family: Consolas, Monaco, 'Courier New', Menlo, 'DejaVu Sans Mono', monospace;
background: #f5f7fa;
margin: 0;
padding: 20px;
}
.history-logs-container {
max-width: 100%;
margin: 0 auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
overflow: hidden;
}
.history-logs-header {
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
color: white;
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.history-logs-header h1 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.history-logs-header-left {
display: flex;
align-items: center;
gap: 20px;
}
.history-logs-body {
padding: 20px;
}
.time-range-selector label {
font-size: 14px;
color: var(--text-secondary);
}
/* 高级过滤面板样式 */
.advanced-filters-panel {
margin-bottom: 16px;
border: 1px solid var(--border-color);
border-radius: 6px;
overflow: hidden;
}
.filters-header {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
background: #fafafa;
border-bottom: 1px solid var(--border-color);
flex-wrap: wrap;
}
.filters-title {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
}
.filters-header-controls {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
flex: 1;
}
.filters-header-controls .filter-item {
display: flex;
align-items: center;
gap: 6px;
}
.filters-header-controls .filter-label {
font-size: 13px;
color: var(--text-secondary);
white-space: nowrap;
}
.filters-header-controls .filter-input,
.filters-header-controls
.filters-header-controls .range-input-group {
display: flex;
align-items: center;
gap: 4px;
}
.filters-header-controls .filter-input-small {
padding: 4px 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 13px;
font-family: var(--mono-font);
}
.filters-header-controls .range-separator {
color: var(--text-secondary);
font-size: 13px;
}
.filter-action-btn {
padding: 4px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 13px;
cursor: pointer;
background: white;
color: var(--text-primary);
}
.filter-action-btn:hover {
background: #f5f5f5;
}
.btn-link {
background: none;
border: none;
color: var(--primary-color);
cursor: pointer;
padding: 4px 8px;
font-size: 13px;
}
.btn-link:hover {
text-decoration: underline;
}
.clear-filters-btn {
margin-left: auto;
background: #ff4d4f;
color: white;
padding: 4px 12px;
border-radius: 4px;
font-size: 13px;
}
.clear-filters-btn:hover {
background: #ff7875;
text-decoration: none;
}
.filters-body {
padding: 16px;
}
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 12px;
}
.filter-row:last-child {
margin-bottom: 0;
}
.filter-item {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 13px;
color: var(--text-secondary);
white-space: nowrap;
}
.filter-input {
padding: 6px 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 13px;
font-family: var(--mono-font);
min-width: 180px;
}
.filter-input:focus {
outline: none;
border-color: var(--primary-color);
}
.filter-input-small {
padding: 6px 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 13px;
font-family: var(--mono-font);
width: 80px;
}
.filter-input-small:focus {
outline: none;
border-color: var(--primary-color);
}
.range-input-group {
display: flex;
align-items: center;
gap: 4px;
}
.range-separator {
color: var(--text-secondary);
}
.filter-input::placeholder,
.filter-input-small::placeholder {
color: #bfbfbf;
}
.filter-active {
border-color: var(--primary-color);
background: #e6f7ff;
}
/* 分页组件样式 */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #fafafa;
border-top: 1px solid var(--border-color);
margin-top: 8px;
}
.pagination-info {
font-size: 13px;
color: var(--text-secondary);
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
}
.page-size-select {
padding: 6px 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 13px;
background: white;
cursor: pointer;
}
.btn-page {
padding: 6px 12px;
border: 1px solid var(--border-color);
background: white;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.btn-page:hover:not(:disabled) {
border-color: var(--primary-color);
color: var(--primary-color);
}
.btn-page:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-indicator {
font-size: 13px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 4px;
}
.page-input {
width: 50px;
padding: 4px 6px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 13px;
text-align: center;
}
/* 导出下拉菜单 */
.export-dropdown {
position: relative;
display: inline-block;
}
.export-dropdown .dropdown-toggle {
padding: 6px 14px;
border: 2px solid var(--border-color);
border-radius: 6px;
font-size: 13px;
font-family: Consolas, Monaco, 'Courier New', monospace;
background: white;
color: var(--text-primary);
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.export-dropdown .dropdown-toggle:hover {
border-color: var(--primary-color);
color: var(--primary-color);
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
}
.export-menu {
position: absolute;
top: 100%;
right: 0;
background: white;
border: 2px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
min-width: 200px;
z-index: 100;
margin-top: 6px;
overflow: hidden;
}
.export-menu a {
display: block;
padding: 10px 14px;
font-size: 13px;
font-family: Consolas, Monaco, 'Courier New', monospace;
color: var(--text-primary);
text-decoration: none;
transition: all 0.2s;
border-bottom: 1px solid #f0f0f0;
}
.export-menu a:last-child {
border-bottom: none;
}
.export-menu a:hover {
background: #e6f7ff;
color: var(--primary-color);
}
/* 时间线样式 */
.timeline-container {
padding: 16px;
min-height: 400px;
}
.timeline-empty {
text-align: center;
color: var(--text-secondary);
padding: 40px;
}
.timeline-event {
display: flex;
align-items: flex-start;
padding: 12px 16px;
border-left: 3px solid var(--border-color);
margin-bottom: 8px;
background: #fafafa;
border-radius: 4px;
transition: all 0.2s;
}
.timeline-event:hover {
background: #f0f0f0;
}
.timeline-event.error {
border-left-color: #ef4444;
background: #fef2f2;
}
.timeline-event.warning {
border-left-color: #f59e0b;
background: #fffbeb;
}
.timeline-icon {
font-size: 20px;
margin-right: 12px;
flex-shrink: 0;
}
.timeline-content {
flex: 1;
}
.timeline-time {
font-size: 12px;
color: var(--text-secondary);
font-family: var(--mono-font);
margin-bottom: 4px;
}
.timeline-summary {
font-size: 14px;
color: var(--text-primary);
word-break: break-all;
}
.timeline-type {
font-size: 11px;
padding: 2px 6px;
border-radius: 3px;
background: #e5e5e5;
margin-left: 8px;
}
/* 图表样式 */
.chart-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.chart-card {
flex: 1;
background: #fff;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
}
.chart-card h4 {
margin: 0 0 12px 0;
font-size: 14px;
color: var(--text-primary);
}
.chart-card canvas {
max-height: 250px;
}
.result-tabs-left {
display: flex;
gap: 8px;
}
.result-tabs-left .tab-btn {
padding: 10px 20px;
border: 2px solid transparent;
border-radius: 6px;
background: #f5f5f5;
color: #666;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.result-tabs-left .tab-btn i {
font-size: 16px;
}
.result-tabs-left .tab-btn:hover {
background: #e8e8e8;
color: #333;
}
.result-tabs-left .tab-btn.active {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);
}
.result-tabs-right {
display: flex;
align-items: center;
gap: 16px;
margin-left: auto;
}
.pagination-container-inline {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
font-family: Consolas, Monaco, 'Courier New', monospace;
}
.pagination-text {
color: var(--text-secondary);
}
.btn-page-small {
padding: 6px 12px;
border: 2px solid var(--border-color);
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 13px;
font-family: Consolas, Monaco, 'Courier New', monospace;
line-height: 1;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.btn-page-small:hover:not(:disabled) {
border-color: var(--primary-color);
color: var(--primary-color);
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
transform: translateY(-1px);
}
.btn-page-small:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.page-size-select-small {
padding: 6px 10px;
border: 2px solid var(--border-color);
border-radius: 6px;
font-size: 13px;
font-family: Consolas, Monaco, 'Courier New', monospace;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.page-size-select-small:hover {
border-color: var(--primary-color);
}
.page-size-select-small:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
}
.page-size-select-small option {
font-family: Consolas, Monaco, 'Courier New', monospace;
}
.page-input-small {
width: 50px;
padding: 6px 8px;
border: 2px solid var(--border-color);
border-radius: 6px;
font-size: 13px;
font-family: Consolas, Monaco, 'Courier New', monospace;
text-align: center;
transition: all 0.2s;
}
.page-input-small:hover {
border-color: var(--primary-color);
}
.page-input-small:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
}
.tab-filters {
display: flex;
flex-wrap: wrap;
gap: 16px;
padding: 12px 0;
margin-bottom: 12px;
border-bottom: 1px solid var(--border-color);
align-items: center;
position: sticky;
top: 0;
z-index: 5;
background: white;
}
.result-summary-inline {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.result-summary-inline
.result-summary-inline
.result-summary-inline .summary-separator {
color: var(--border-color);
margin: 0 4px;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
font-family: var(--mono-font);
}
.badge-info {
background: #e6f7ff;
color: #1890ff;
}
.badge-debug {
background: #f0f0f0;
color: #8c8c8c;
}
.badge-warning {
background: #fff7e6;
color: #fa8c16;
}
.badge-error {
background: #fff1f0;
color: #f5222d;
}
.badge-critical {
background: #fff1f0;
color: #a8071a;
font-weight: 600;
}
.history-table th,
.history-table td {
padding: 6px 8px;
text-align: left;
border-bottom: 1px solid var(--border-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-table th {
background: #fafafa;
font-weight: 600;
color: var(--text-secondary);
padding: 8px 8px;
position: sticky;
top: 0;
z-index: 10;
}
.history-table tr:hover {
background: #fafafa;
}
/* 系统日志表格列宽 - 6列:序号、时间、级别、模块、消息、操作 */
#logs-results-table th:nth-child(1),
#logs-results-table td:nth-child(1) {
width: 45px;
text-align: center;
}
#logs-results-table th:nth-child(2),
#logs-results-table td:nth-child(2) {
width: 170px;
}
#logs-results-table th:nth-child(3),
#logs-results-table td:nth-child(3) {
width: 80px;
text-align: center;
}
#logs-results-table th:nth-child(4),
#logs-results-table td:nth-child(4) {
width: 300px;
}
#logs-results-table th:nth-child(5),
#logs-results-table td:nth-child(5) {
width: auto;
white-space: normal;
word-break: break-all;
min-width: 200px;
}
#logs-results-table th:nth-child(6),
#logs-results-table td:nth-child(6) {
width: 180px;
text-align: center;
}
/* 接收请求表格列宽 - 8列:序号、时间、方法、端点、状态码、响应时间、客户端IP、操作 */
#http-results-table th:nth-child(1),
#http-results-table td:nth-child(1) {
width: 45px;
text-align: center;
}
#http-results-table th:nth-child(2),
#http-results-table td:nth-child(2) {
width: 80px;
}
#http-results-table th:nth-child(3),
#http-results-table td:nth-child(3) {
width: 60px;
text-align: center;
}
#http-results-table th:nth-child(4),
#http-results-table td:nth-child(4) {
width: 250px;
}
#http-results-table th:nth-child(5),
#http-results-table td:nth-child(5) {
width: 60px;
text-align: center;
border-bottom: 1px solid #e8e8e8 !important;
font-family: var(--font-mono);
}
#http-results-table th:nth-child(6),
#http-results-table td:nth-child(6) {
width: 80px;
text-align: right;
font-family: var(--font-mono);
}
#http-results-table th:nth-child(7),
#http-results-table td:nth-child(7) {
width: 110px;
font-family: var(--font-mono);
}
#http-results-table th:nth-child(8),
#http-results-table td:nth-child(8) {
width: 90px;
text-align: center;
}
/* 发送请求表格列宽 - 8列:序号、时间、方法、URL、状态码、响应时间、模块、操作 */
#outbound-results-table th:nth-child(1),
#outbound-results-table td:nth-child(1) {
width: 45px;
text-align: center;
}
#outbound-results-table th:nth-child(2),
#outbound-results-table td:nth-child(2) {
width: 80px;
}
#outbound-results-table th:nth-child(3),
#outbound-results-table td:nth-child(3) {
width: 70px;
text-align: center;
}
#outbound-results-table th:nth-child(4),
#outbound-results-table td:nth-child(4) {
width: 300px;
}
#outbound-results-table th:nth-child(5),
#outbound-results-table td:nth-child(5) {
width: 60px;
text-align: center;
border-bottom: 1px solid #e8e8e8 !important;
font-family: var(--font-mono);
}
#outbound-results-table th:nth-child(6),
#outbound-results-table td:nth-child(6) {
width: 60px;
text-align: center;
font-family: var(--font-mono);
}
#outbound-results-table th:nth-child(7),
#outbound-results-table td:nth-child(7) {
width: 120px;
font-family: var(--font-mono);
}
#outbound-results-table th:nth-child(8),
#outbound-results-table td:nth-child(8) {
width: 90px;
text-align: center;
}
.no-data {
text-align: center;
color: var(--text-secondary);
padding: 40px 0;
}
.action-btn {
padding: 4px 12px;
border: 1px solid var(--primary-color);
background: transparent;
color: var(--primary-color);
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s;
}
.action-btn:hover {
background: var(--primary-color);
color: white;
}
.history-logs-header-right {
display: flex;
align-items: center;
gap: 16px;
}
.header-query-controls {
display: flex;
align-items: center;
gap: 12px;
}
.header-query-controls .time-range-selector {
display: flex;
align-items: center;
gap: 8px;
}
.header-query-controls label {
font-size: 13px;
font-family: Consolas, Monaco, 'Courier New', monospace;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
}
.header-query-controls .filter-select {
padding: 6px 12px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
font-size: 13px;
font-family: Consolas, Monaco, 'Courier New', monospace;
background: rgba(255, 255, 255, 0.95);
color: #333;
cursor: pointer;
transition: all 0.2s;
}
.header-query-controls .filter-select:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.6);
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
}
.header-btn {
padding: 6px 16px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
font-family: Consolas, Monaco, 'Courier New', monospace;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.2s;
}
.header-btn.btn-primary {
background: #52c41a;
color: white;
box-shadow: 0 2px 4px rgba(82, 196, 26, 0.3);
}
.header-btn.btn-primary:hover {
background: #73d13d;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(82, 196, 26, 0.4);
}
.header-btn.btn-secondary {
background: rgba(255, 255, 255, 0.95);
color: #333;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.header-btn.btn-secondary:hover {
background: white;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header-btn.header-btn-reset {
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
color: white;
border: none;
box-shadow: 0 2px 4px rgba(255, 77, 79, 0.3);
}
.header-btn.header-btn-reset:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(255, 77, 79, 0.4);
}
.precise-mode-btn {
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
border: none;
cursor: pointer;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
color: white;
transition: all 0.3s;
}
.precise-mode-btn:hover {
opacity: 0.9;
transform: scale(1.02);
}
.highlight-row {
animation: highlightPulse 1s ease-in-out 3;
background: linear-gradient(90deg, transparent, rgba(24, 144, 255, 0.2), transparent);
}
@keyframes highlightPulse {
0%, 100% {
background: linear-gradient(90deg, transparent, rgba(24, 144, 255, 0.1), transparent);
}
50% {
background: linear-gradient(90deg, transparent, rgba(24, 144, 255, 0.3), transparent);
}
}
</style>
</head>
<body>
<div class="history-logs-container">
<div class="history-logs-header">
<div class="history-logs-header-left">
<h1 data-i18n="monitor.page.history_logs">📊 日志历史查询</h1>
<span class="badge" id="history-query-badge" data-i18n="monitor.other.linked_query">联动查询</span>
</div>
<div class="history-logs-header-right">
<select id="lang-selector" class="lang-selector" onchange="i18n.switchLanguage(this.value)">
<option value="zh-CN">🇨🇳 中文</option>
<option value="en-US">🇺🇸 English</option>
<option value="de-DE">🇩🇪 Deutsch</option>
</select>
<div class="header-query-controls">
<div class="time-range-selector">
<input type="datetime-local" id="history-start-time" class="datetime-input" step="1">
<input type="datetime-local" id="history-end-time" class="datetime-input" step="1">
</div>
<div class="query-filters">
<select id="history-log-level" class="filter-select">
<option value="" data-i18n="monitor.filter.level">全部级别</option>
<option value="DEBUG">≥ DEBUG</option>
<option value="INFO">≥ INFO</option>
<option value="WARNING">≥ WARNING</option>
<option value="ERROR">≥ ERROR</option>
<option value="CRITICAL">≥ CRITICAL</option>
</select>
<select id="history-query-type" class="filter-select">
<option value="all" data-i18n="monitor.filter.type">全部数据</option>
<option value="http" data-i18n="monitor.tab.http">接收请求</option>
<option value="outbound" data-i18n="monitor.tab.outbound">发送请求</option>
<option value="logs" data-i18n="monitor.tab.logs">系统日志</option>
</select>
</div>
<button class="btn btn-secondary header-btn" onclick="quickTimeRange('10m')" data-i18n="monitor.time.last_10m">10分钟</button>
<button class="btn btn-secondary header-btn" onclick="quickTimeRange('30m')" data-i18n="monitor.time.last_30m">30分钟</button>
<button class="btn btn-secondary header-btn" onclick="quickTimeRange('1h')" data-i18n="monitor.time.last_1h">1小时</button>
<button class="btn btn-secondary header-btn" onclick="quickTimeRange('6h')" data-i18n="monitor.time.last_6h">6小时</button>
<button class="btn btn-secondary header-btn" onclick="quickTimeRange('24h')" data-i18n="monitor.time.last_24h">24小时</button>
<button class="btn btn-secondary header-btn header-btn-reset" onclick="resetHistoryQuery()" data-i18n="monitor.btn.reset">重置</button>
<button class="btn btn-primary header-btn" id="query-btn" onclick="executeHistoryQuery()" data-i18n="monitor.btn.query">查询</button>
</div>
<div id="precise-mode-container"></div>
</div>
</div>
<div class="history-logs-body">
<div class="result-tabs" id="result-tabs" style="display: none;">
<div class="result-tabs-left">
<button class="tab-btn active" data-tab="logs" onclick="switchResultTab('logs')"><i class="bi bi-journal-text"></i> <span data-i18n="monitor.tab.logs">系统日志</span></button>
<button class="tab-btn" data-tab="http" onclick="switchResultTab('http')"><i class="bi bi-box-arrow-in-right"></i> <span data-i18n="monitor.tab.http">接收请求</span></button>
<button class="tab-btn" data-tab="outbound" onclick="switchResultTab('outbound')"><i class="bi bi-box-arrow-right"></i> <span data-i18n="monitor.tab.outbound">发送请求</span></button>
<button class="tab-btn" data-tab="timeline" onclick="switchResultTab('timeline')"><i class="bi bi-clock-history"></i> <span data-i18n="monitor.tab.timeline">时间线</span></button>
<button class="tab-btn" data-tab="chart" onclick="switchResultTab('chart')"><i class="bi bi-bar-chart-line"></i> <span data-i18n="monitor.tab.chart">图表分析</span></button>
</div>
<div class="result-tabs-right">
<div class="result-summary-inline">
<span class="summary-label" data-i18n="monitor.metric.http_requests">接收:</span>
<span class="badge badge-info" id="http-count">0</span>
<span class="summary-label" data-i18n="monitor.metric.outbound_requests">发送:</span>
<span class="badge badge-info" id="outbound-count">0</span>
<span class="summary-label" data-i18n="monitor.tab.logs">日志:</span>
<span class="badge badge-info" id="logs-count">0</span>
<span class="summary-separator" id="logs-levels-separator" style="display:none;">|</span>
<span class="summary-label" id="logs-levels-label" style="display:none;" data-i18n="monitor.col.level">级别:</span>
<span class="summary-label debug" id="logs-debug-label" style="display:none;" data-i18n="monitor.level.debug">DEBUG:</span>
<span class="badge badge-debug" id="logs-debug-count" style="display:none;">0</span>
<span class="summary-label info" id="logs-info-label" style="display:none;" data-i18n="monitor.level.info">INFO:</span>
<span class="badge badge-info" id="logs-info-count" style="display:none;">0</span>
<span class="summary-label warning" id="logs-warning-label" style="display:none;" data-i18n="monitor.level.warning">WARNING:</span>
<span class="badge badge-warning" id="logs-warning-count" style="display:none;">0</span>
<span class="summary-label error" id="logs-error-label" style="display:none;" data-i18n="monitor.level.error">ERROR:</span>
<span class="badge badge-error" id="logs-error-count" style="display:none;">0</span>
<span class="summary-label critical" id="logs-critical-label" style="display:none;" data-i18n="monitor.level.critical">CRITICAL:</span>
<span class="badge badge-critical" id="logs-critical-count" style="display:none;">0</span>
</div>
<div class="export-dropdown">
<button class="btn btn-secondary dropdown-toggle" onclick="toggleExportMenu()">
<i class="bi bi-download"></i> <span data-i18n="monitor.btn.export">导出</span>
</button>
<div class="export-menu" id="export-menu" style="display:none;">
<a href="javascript:void(0)" onclick="exportData('logs', 'csv')">导出系统日志 CSV</a>
<a href="javascript:void(0)" onclick="exportData('logs', 'json')">导出系统日志 JSON</a>
<a href="javascript:void(0)" onclick="exportData('http', 'csv')">导出接收请求 CSV</a>
<a href="javascript:void(0)" onclick="exportData('http', 'json')">导出接收请求 JSON</a>
<a href="javascript:void(0)" onclick="exportData('outbound', 'csv')">导出发送请求 CSV</a>
<a href="javascript:void(0)" onclick="exportData('outbound', 'json')">导出发送请求 JSON</a>
</div>
</div>
<div class="pagination-container-inline" id="http-pagination" style="display:none;">
<select id="http-page-size" class="page-size-select-small" onchange="changePageSize('http')">
<option value="50">50</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="500">500</option>
</select>
<span class="pagination-text"><span data-i18n="monitor.pagination.page">Page</span> <input type="number" id="http-page-input" class="page-input-small" min="1" onchange="goToPage('http', this.value)"> / <span id="http-total-pages">1</span></span>
<button class="btn-page-small" onclick="goToPrevPage('http')" id="http-prev-btn"></button>
<button class="btn-page-small" onclick="goToNextPage('http')" id="http-next-btn"></button>
</div>
<div class="pagination-container-inline" id="outbound-pagination" style="display:none;">
<select id="outbound-page-size" class="page-size-select-small" onchange="changePageSize('outbound')">
<option value="50">50</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="500">500</option>
</select>
<span class="pagination-text"><span data-i18n="monitor.pagination.page">Page</span> <input type="number" id="outbound-page-input" class="page-input-small" min="1" onchange="goToPage('outbound', this.value)"> / <span id="outbound-total-pages">1</span></span>
<button class="btn-page-small" onclick="goToPrevPage('outbound')" id="outbound-prev-btn"></button>
<button class="btn-page-small" onclick="goToNextPage('outbound')" id="outbound-next-btn"></button>
</div>
<div class="pagination-container-inline" id="logs-pagination" style="display:none;">
<select id="logs-page-size" class="page-size-select-small" onchange="changePageSize('logs')">
<option value="50">50</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="500">500</option>
</select>
<span class="pagination-text"><span data-i18n="monitor.pagination.page">Page</span> <input type="number" id="logs-page-input" class="page-input-small" min="1" onchange="goToPage('logs', this.value)"> / <span id="logs-total-pages">1</span></span>
<button class="btn-page-small" onclick="goToPrevPage('logs')" id="logs-prev-btn"></button>
<button class="btn-page-small" onclick="goToNextPage('logs')" id="logs-next-btn"></button>
</div>
</div>
</div>
<div class="result-tab-content" id="tab-http">
<div class="tab-filters">
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.filter.keyword">关键词:</label>
<input type="text" id="filter-keyword-http" class="filter-input" placeholder="搜索路径/请求体/响应体" style="width:300px;">
</div>
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.filter.method">方法:</label>
<select id="filter-method" class="filter-select" style="width:90px;">
<option value="">全部</option>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.filter.client_ip">IP:</label>
<input type="text" id="filter-client-ip" class="filter-input" placeholder="如:192.168.1.1" style="min-width:100px;">
</div>
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.col.status_code">状态码:</label>
<div class="range-input-group">
<input type="number" id="filter-status-min" class="filter-input-small" placeholder="最小" min="100" max="999" style="width:90px;">
<span class="range-separator">-</span>
<input type="number" id="filter-status-max" class="filter-input-small" placeholder="最大" min="100" max="999" style="width:90px;">
</div>
</div>
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.col.duration">耗时(ms):</label>
<div class="range-input-group">
<input type="number" id="filter-duration-min" class="filter-input-small" placeholder="最小" min="0" style="width:90px;">
<span class="range-separator">-</span>
<input type="number" id="filter-duration-max" class="filter-input-small" placeholder="最大" min="0" style="width:90px;">
</div>
</div>
<div class="filter-item">
<button class="btn btn-secondary filter-action-btn" onclick="saveQueryTemplate()">保存</button>
</div>
<div class="filter-item">
<select id="saved-templates" class="filter-select" onchange="loadQueryTemplate()" style="max-width:120px;">
<option value="" data-i18n="monitor.template.saved_queries">已保存查询...</option>
</select>
</div>
</div>
<table class="history-table" id="http-results-table" data-result-table>
<thead>
<tr>
<th data-i18n="monitor.col.index">序号</th>
<th data-i18n="monitor.col.time">时间</th>
<th data-i18n="monitor.col.method">方法</th>
<th data-i18n="monitor.col.path">端点</th>
<th data-i18n="monitor.col.status_code">状态码</th>
<th data-i18n="monitor.col.duration">响应时间</th>
<th data-i18n="monitor.col.client_ip">客户端IP</th>
<th data-i18n="monitor.col.operation">操作</th>
</tr>
</thead>
<tbody id="http-results-body">
<tr><td colspan="8" class="no-data" data-i18n="monitor.status.no_data">暂无数据</td></tr>
</tbody>
</table>
</div>
<div class="result-tab-content" id="tab-outbound">
<div class="tab-filters">
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.filter.keyword">关键词:</label>
<input type="text" id="filter-keyword-outbound" class="filter-input" placeholder="搜索URL/请求体/响应体" style="width:300px;">
</div>
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.filter.method">方法:</label>
<select id="filter-method-outbound" class="filter-select" style="width:90px;">
<option value="">全部</option>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
</div>
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.col.status_code">状态码:</label>
<div class="range-input-group">
<input type="number" id="filter-status-min-outbound" class="filter-input-small" placeholder="最小" min="100" max="999" style="width:90px;">
<span class="range-separator">-</span>
<input type="number" id="filter-status-max-outbound" class="filter-input-small" placeholder="最大" min="100" max="999" style="width:90px;">
</div>
</div>
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.col.duration">耗时(ms):</label>
<div class="range-input-group">
<input type="number" id="filter-duration-min-outbound" class="filter-input-small" placeholder="最小" min="0" style="width:90px;">
<span class="range-separator">-</span>
<input type="number" id="filter-duration-max-outbound" class="filter-input-small" placeholder="最大" min="0" style="width:90px;">
</div>
</div>
</div>
<table class="history-table" id="outbound-results-table" data-result-table>
<thead>
<tr>
<th data-i18n="monitor.col.index">序号</th>
<th data-i18n="monitor.col.time">时间</th>
<th data-i18n="monitor.col.method">方法</th>
<th data-i18n="monitor.col.URL">URL</th>
<th data-i18n="monitor.col.status_code">状态码</th>
<th data-i18n="monitor.col.duration">响应时间</th>
<th data-i18n="monitor.col.模块">模块</th>
<th data-i18n="monitor.col.operation">操作</th>
</tr>
</thead>
<tbody id="outbound-results-body">
<tr><td colspan="8" class="no-data" data-i18n="monitor.status.no_data">暂无数据</td></tr>
</tbody>
</table>
</div>
<div class="result-tab-content active" id="tab-logs">
<div class="tab-filters">
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.filter.keyword">关键词:</label>
<input type="text" id="filter-keyword" class="filter-input" data-i18n-placeholder="monitor.filter.keyword_placeholder" placeholder="搜索消息内容" style="width:300px;">
</div>
<div class="filter-item">
<label class="filter-label" data-i18n="monitor.filter.module">模块:</label>
<input type="text" id="filter-module" class="filter-input" data-i18n-placeholder="monitor.filter.module_placeholder" placeholder="多个用逗号分隔" style="min-width:300px;">
</div>
</div>
<table class="history-table" id="logs-results-table" data-result-table>
<thead>
<tr>
<th data-i18n="monitor.col.index">序号</th>
<th data-i18n="monitor.col.time">时间</th>
<th data-i18n="monitor.col.level">级别</th>
<th data-i18n="monitor.col.module">模块</th>
<th data-i18n="monitor.col.message">消息</th>
<th data-i18n="monitor.col.operation">操作</th>
</tr>
</thead>
<tbody id="logs-results-body">
<tr><td colspan="6" class="no-data" data-i18n="monitor.status.no_data">暂无数据</td></tr>
</tbody>
</table>
</div>
<!-- 时间线标签页 -->
<div class="result-tab-content" id="tab-timeline">
<div class="timeline-container" id="timeline-container">
<div class="timeline-empty" data-i18n="monitor.timeline.no_data">暂无数据,请先执行查询</div>
</div>
</div>
<!-- 图表分析标签页 -->
<div class="result-tab-content" id="tab-chart">
<div class="chart-container">
<div class="chart-row">
<div class="chart-card">
<h4 data-i18n="monitor.chart.request_trend">📊 请求量趋势</h4>
<canvas id="chart-trend"></canvas>
</div>
<div class="chart-card">
<h4 data-i18n="monitor.chart.level_distribution">📊 日志级别分布</h4>
<canvas id="chart-levels"></canvas>
</div>
</div>
<div class="chart-row">
<div class="chart-card">
<h4 data-i18n="monitor.chart.status_distribution">📈 状态码分布</h4>
<canvas id="chart-status"></canvas>
</div>
<div class="chart-card">
<h4 data-i18n="monitor.chart.slow_requests">⏱️ 慢请求TOP10</h4>
<canvas id="chart-slow"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const API_BASE = '/monitor/api';
let historyQueryData = {
http: [],
outbound: [],
logs: []
};
let queryStats = {};
let currentTab = 'logs';
function formatDateTime(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
async function executeHistoryQuery() {
const startTime = document.getElementById('history-start-time').value;
const endTime = document.getElementById('history-end-time').value;
const logLevel = document.getElementById('history-log-level').value;
const queryType = document.getElementById('history-query-type').value;
// 如果选择时间范围,验证时间逻辑
if (startTime && endTime) {
const startDate = new Date(startTime);
const endDate = new Date(endTime);
if (startDate >= endDate) {
alert(i18n.t('monitor.error.time_range_invalid'));
return;
}
}
let timezoneOffset = -new Date().getTimezoneOffset();
if (isNaN(timezoneOffset)) {
timezoneOffset = 480; // 默认 UTC+8
}
try {
const badge = document.getElementById('history-query-badge');
if (badge) {
badge.textContent = i18n.t('monitor.status.querying');
badge.className = 'badge warning';
}
const queryBtn = document.getElementById('query-btn');
if (queryBtn) {
queryBtn.disabled = true;
queryBtn.textContent = i18n.t('monitor.status.querying');
}
let url = `${API_BASE}/history/query?timezone_offset=${timezoneOffset}`;
if (startTime) {
url += `&start_time=${encodeURIComponent(startTime)}`;
}
if (endTime) {
url += `&end_time=${encodeURIComponent(endTime)}`;
}
if (logLevel) {
url += `&level=${logLevel}`;
}
if (queryType && queryType !== 'all') {
url += `&type=${queryType}`;
}
// 高级过滤参数(根据当前活动标签页获取对应页签的过滤条件)
let filterModule = '';
let filterKeyword = '';
let filterMethod = '';
let filterClientIp = '';
let filterStatusMin = '';
let filterStatusMax = '';
let filterDurationMin = '';
let filterDurationMax = '';
// 根据当前活动标签页应用对应的筛选条件
// 这样即使用户选择"全部数据"查询类型,当前标签页的筛选条件也会生效
if (currentTab === 'logs') {
filterModule = document.getElementById('filter-module')?.value?.trim() || '';
filterKeyword = document.getElementById('filter-keyword')?.value?.trim() || '';
} else if (currentTab === 'http') {
filterMethod = document.getElementById('filter-method')?.value || '';
filterClientIp = document.getElementById('filter-client-ip')?.value?.trim() || '';
filterStatusMin = document.getElementById('filter-status-min')?.value || '';
filterStatusMax = document.getElementById('filter-status-max')?.value || '';
filterDurationMin = document.getElementById('filter-duration-min')?.value || '';
filterDurationMax = document.getElementById('filter-duration-max')?.value || '';
filterKeyword = document.getElementById('filter-keyword-http')?.value?.trim() || '';
} else if (currentTab === 'outbound') {
filterMethod = document.getElementById('filter-method-outbound')?.value || '';
filterStatusMin = document.getElementById('filter-status-min-outbound')?.value || '';
filterStatusMax = document.getElementById('filter-status-max-outbound')?.value || '';
filterDurationMin = document.getElementById('filter-duration-min-outbound')?.value || '';
filterDurationMax = document.getElementById('filter-duration-max-outbound')?.value || '';
filterKeyword = document.getElementById('filter-keyword-outbound')?.value?.trim() || '';
}
if (filterModule) url += `&module=${encodeURIComponent(filterModule)}`;
if (filterKeyword) url += `&keyword=${encodeURIComponent(filterKeyword)}`;
if (filterMethod) url += `&method=${filterMethod}`;
if (filterClientIp) url += `&client_ip=${encodeURIComponent(filterClientIp)}`;
if (filterStatusMin) url += `&status_code_min=${filterStatusMin}`;
if (filterStatusMax) url += `&status_code_max=${filterStatusMax}`;
if (filterDurationMin) url += `&duration_min=${filterDurationMin}`;
if (filterDurationMax) url += `&duration_max=${filterDurationMax}`;
// 分页参数
const currentPage = paginationState[currentTab]?.page || 1;
const currentPageSize = paginationState[currentTab]?.pageSize || 50;
url += `&page=${currentPage}&page_size=${currentPageSize}`;
const response = await fetch(url);
const data = await response.json();
historyQueryData = {
http: data.http_requests || [],
outbound: data.outbound_requests || [],
logs: data.logs || []
};
// 保存统计数据
queryStats = data.stats || {};
// 更新分页元数据
if (data.pagination) {
const p = data.pagination;
paginationState.http.totalCount = p.total_count || 0;
paginationState.http.totalPages = p.total_pages || 0;
paginationState.outbound.totalCount = p.total_count || 0;
paginationState.outbound.totalPages = p.total_pages || 0;
paginationState.logs.totalCount = p.total_count || 0;
paginationState.logs.totalPages = p.total_pages || 0;
} else {
// 无分页数据时根据当前数据计算
['http', 'outbound', 'logs'].forEach(t => {
paginationState[t].totalCount = historyQueryData[t].length;
paginationState[t].totalPages = Math.ceil(historyQueryData[t].length / currentPageSize) || 1;
});
}
updateQuerySummary(startTime, endTime);
updatePaginationUI(currentTab);
document.getElementById('result-tabs').style.display = 'flex';
// 保持当前标签页,而不是强制跳转到logs
switchResultTab(currentTab);
if (badge) {
badge.textContent = i18n.t('monitor.success.query_complete');
badge.className = 'badge healthy';
}
if (queryBtn) {
queryBtn.disabled = false;
queryBtn.textContent = i18n.t('monitor.btn.query');
}
} catch (error) {
console.error('执行历史查询失败:', error);
alert(i18n.t('monitor.error.query_failed'));
const badge = document.getElementById('history-query-badge');
if (badge) {
badge.textContent = i18n.t('monitor.error.query_failed');
badge.className = 'badge error';
}
const queryBtn = document.getElementById('query-btn');
if (queryBtn) {
queryBtn.disabled = false;
queryBtn.textContent = i18n.t('monitor.btn.query');
}
}
}
function resetHistoryQuery() {
document.getElementById('history-start-time').value = '';
document.getElementById('history-end-time').value = '';
document.getElementById('history-log-level').value = '';
document.getElementById('history-query-type').value = 'all';
// 重置高级过滤条件(系统日志)
document.getElementById('filter-module').value = '';
document.getElementById('filter-keyword').value = '';
// 重置高级过滤条件(接收请求)
document.getElementById('filter-method').value = '';
document.getElementById('filter-client-ip').value = '';
document.getElementById('filter-status-min').value = '';
document.getElementById('filter-status-max').value = '';
document.getElementById('filter-duration-min').value = '';
document.getElementById('filter-duration-max').value = '';
// 重置高级过滤条件(发送请求)
document.getElementById('filter-keyword-outbound').value = '';
document.getElementById('filter-method-outbound').value = '';
document.getElementById('filter-status-min-outbound').value = '';
document.getElementById('filter-status-max-outbound').value = '';
document.getElementById('filter-duration-min-outbound').value = '';
document.getElementById('filter-duration-max-outbound').value = '';
updateFilterActiveState();
document.getElementById('result-tabs').style.display = 'none';
document.getElementById('http-results-body').innerHTML = '<tr><td colspan="8" class="no-data">' + i18n.t('monitor.status.no_data') + '</td></tr>';
document.getElementById('outbound-results-body').innerHTML = '<tr><td colspan="8" class="no-data">' + i18n.t('monitor.status.no_data') + '</td></tr>';
document.getElementById('logs-results-body').innerHTML = '<tr><td colspan="6" class="no-data">' + i18n.t('monitor.status.no_data') + '</td></tr>';
historyQueryData = { http: [], outbound: [], logs: [] };
// 清理缓存的排序数据
sortedHttpData = [];
sortedOutboundData = [];
sortedLogsData = [];
const badge = document.getElementById('history-query-badge');
if (badge) {
badge.textContent = i18n.t('monitor.other.linked_query');
badge.className = '';
}
}
function quickTimeRange(duration) {
const end = new Date();
const start = new Date();
// 解析时间单位(支持分钟 m 和小时 h)
const unit = duration.slice(-1);
const value = parseInt(duration.slice(0, -1));
if (unit === 'm') {
// 分钟
start.setMinutes(start.getMinutes() - value);
} else {
// 小时(默认)
start.setHours(start.getHours() - value);
}
const formatDateTime = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
};
document.getElementById('history-start-time').value = formatDateTime(start);
document.getElementById('history-end-time').value = formatDateTime(end);
// 自动触发查询
executeHistoryQuery();
}
// 高级过滤面板控制
let filtersExpanded = true;
function toggleAdvancedFilters() {
filtersExpanded = !filtersExpanded;
const body = document.getElementById('filters-body');
const icon = document.getElementById('toggle-filters-icon');
if (filtersExpanded) {
body.style.display = 'block';
icon.textContent = '▼';
} else {
body.style.display = 'none';
icon.textContent = '▶';
}
}
function clearAdvancedFilters() {
document.getElementById('filter-module').value = '';
document.getElementById('filter-keyword').value = '';
document.getElementById('filter-method').value = '';
document.getElementById('filter-client-ip').value = '';
document.getElementById('filter-status-min').value = '';
document.getElementById('filter-status-max').value = '';
document.getElementById('filter-duration-min').value = '';
document.getElementById('filter-duration-max').value = '';
updateFilterActiveState();
}
function updateFilterActiveState() {
const hasFilter =
document.getElementById('filter-module').value.trim() ||
document.getElementById('filter-keyword').value.trim() ||
document.getElementById('filter-method').value ||
document.getElementById('filter-client-ip').value.trim() ||
document.getElementById('filter-status-min').value ||
document.getElementById('filter-status-max').value ||
document.getElementById('filter-duration-min').value ||
document.getElementById('filter-duration-max').value;
const clearBtn = document.querySelector('.clear-filters-btn');
if (clearBtn) {
clearBtn.style.display = hasFilter ? 'block' : 'none';
}
// 更新输入框的激活状态
const inputs = document.querySelectorAll('#filters-body input, #filters-body select');
inputs.forEach(input => {
if (input.value && input.value.trim()) {
input.classList.add('filter-active');
} else {
input.classList.remove('filter-active');
}
});
}
// 绑定过滤输入事件
document.addEventListener('DOMContentLoaded', function() {
const filterInputs = document.querySelectorAll('#filters-body input, #filters-body select');
filterInputs.forEach(input => {
input.addEventListener('input', updateFilterActiveState);
input.addEventListener('change', updateFilterActiveState);
});
});
// 分页状态管理
let paginationState = {
logs: { page: 1, pageSize: 50, totalCount: 0, totalPages: 0 },
http: { page: 1, pageSize: 50, totalCount: 0, totalPages: 0 },
outbound: { page: 1, pageSize: 50, totalCount: 0, totalPages: 0 }
};
function updatePaginationUI(tab) {
const state = paginationState[tab];
const paginationEl = document.getElementById(`${tab}-pagination`);
if (!paginationEl) return;
if (state.totalCount === 0) {
paginationEl.style.display = 'none';
return;
}
paginationEl.style.display = 'flex';
const pageInputEl = document.getElementById(`${tab}-page-input`);
const totalPagesEl = document.getElementById(`${tab}-total-pages`);
const prevBtn = document.getElementById(`${tab}-prev-btn`);
const nextBtn = document.getElementById(`${tab}-next-btn`);
const pageSizeEl = document.getElementById(`${tab}-page-size`);
if (pageInputEl) pageInputEl.value = state.page;
if (totalPagesEl) totalPagesEl.textContent = state.totalPages;
if (prevBtn) prevBtn.disabled = state.page === 1;
if (nextBtn) nextBtn.disabled = state.page >= state.totalPages;
if (pageSizeEl) pageSizeEl.value = state.pageSize;
}
function goToPage(tab, page) {
const state = paginationState[tab];
if (page === -1) page = state.totalPages;
if (page < 1) page = 1;
if (page > state.totalPages) page = state.totalPages;
state.page = page;
executeHistoryQuery();
}
function goToPrevPage(tab) {
if (paginationState[tab].page > 1) {
paginationState[tab].page--;
executeHistoryQuery();
}
}
function goToNextPage(tab) {
if (paginationState[tab].page < paginationState[tab].totalPages) {
paginationState[tab].page++;
executeHistoryQuery();
}
}
function changePageSize(tab) {
const pageSize = parseInt(document.getElementById(`${tab}-page-size`).value);
paginationState[tab].pageSize = pageSize;
paginationState[tab].page = 1;
executeHistoryQuery();
}
// 导出功能
let exportMenuOpen = false;
function toggleExportMenu() {
exportMenuOpen = !exportMenuOpen;
document.getElementById('export-menu').style.display = exportMenuOpen ? 'block' : 'none';
}
document.addEventListener('click', function(e) {
if (!e.target.closest('.export-dropdown')) {
exportMenuOpen = false;
document.getElementById('export-menu').style.display = 'none';
}
});
function exportData(type, format) {
const startTime = document.getElementById('history-start-time').value;
const endTime = document.getElementById('history-end-time').value;
if (!startTime || !endTime) {
alert(i18n.t('monitor.error.time_range_required'));
return;
}
let url = `${API_BASE}/history/export?start_time=${encodeURIComponent(startTime)}&end_time=${encodeURIComponent(endTime)}&type=${type}&format=${format}`;
const logLevel = document.getElementById('history-log-level').value;
if (logLevel) url += `&level=${logLevel}`;
const filterModule = document.getElementById('filter-module')?.value?.trim();
const filterKeyword = document.getElementById('filter-keyword')?.value?.trim();
const filterMethod = document.getElementById('filter-method')?.value;
const filterClientIp = document.getElementById('filter-client-ip')?.value?.trim();
const filterStatusMin = document.getElementById('filter-status-min')?.value;
const filterStatusMax = document.getElementById('filter-status-max')?.value;
const filterDurationMin = document.getElementById('filter-duration-min')?.value;
const filterDurationMax = document.getElementById('filter-duration-max')?.value;
if (filterModule) url += `&module=${encodeURIComponent(filterModule)}`;
if (filterKeyword) url += `&keyword=${encodeURIComponent(filterKeyword)}`;
if (filterMethod) url += `&method=${filterMethod}`;
if (filterClientIp) url += `&client_ip=${encodeURIComponent(filterClientIp)}`;
if (filterStatusMin) url += `&status_code_min=${filterStatusMin}`;
if (filterStatusMax) url += `&status_code_max=${filterStatusMax}`;
if (filterDurationMin) url += `&duration_min=${filterDurationMin}`;
if (filterDurationMax) url += `&duration_max=${filterDurationMax}`;
window.open(url, '_blank');
toggleExportMenu();
}
function switchResultTab(tab) {
currentTab = tab;
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.tab === tab) {
btn.classList.add('active');
}
});
document.querySelectorAll('.result-tab-content').forEach(content => {
content.classList.remove('active');
});
const tabContent = document.getElementById('tab-' + tab);
if (tabContent) {
tabContent.classList.add('active');
}
if (tab === 'http') {
renderHttpResults();
} else if (tab === 'outbound') {
renderOutboundResults();
} else if (tab === 'logs') {
renderLogsResults();
} else if (tab === 'timeline') {
loadTimeline();
} else if (tab === 'chart') {
renderCharts();
}
// 更新分页UI
updatePaginationUI(tab);
// 显示/隐藏对应的分页组件
['http', 'outbound', 'logs'].forEach(t => {
const paginationEl = document.getElementById(`${t}-pagination`);
if (paginationEl) {
paginationEl.style.display = (t === tab && paginationState[t].totalCount > 0) ? 'flex' : 'none';
}
});
}
// 时间线渲染
let timelineData = [];
function loadTimeline() {
// 复用已查询的数据,避免重复请求
if (!historyQueryData || (!historyQueryData.http?.length && !historyQueryData.outbound?.length && !historyQueryData.logs?.length)) {
timelineData = [];
renderTimeline();
return;
}
// 转换为统一事件格式(与后端逻辑一致)
const events = [];
// HTTP请求事件
for (const req of historyQueryData.http || []) {
events.push({
id: `http_${req.id}`,
timestamp: req.timestamp,
type: 'http',
icon: '📥',
level: req.status_code < 400 ? 'INFO' : 'ERROR',
summary: `${req.method} ${req.path}`,
detail: {
method: req.method,
path: req.path,
status_code: req.status_code,
duration: req.response_time,
client_ip: req.client_ip,
is_error: req.is_error,
is_slow: req.is_slow
}
});
}
// 发送请求事件
for (const req of historyQueryData.outbound || []) {
events.push({
id: `outbound_${req.id}`,
timestamp: req.timestamp,
type: 'outbound',
icon: '📤',
level: req.status_code < 400 ? 'INFO' : 'ERROR',
summary: `${req.method} ${req.url?.substring(0, 80) || ''}`,
detail: {
method: req.method,
url: req.url,
status_code: req.status_code,
duration: req.duration * 1000,
module: req.module,
is_error: req.is_error,
is_slow: req.is_slow
}
});
}
// 系统日志事件
const iconMap = {
'DEBUG': '🔍',
'INFO': '📝',
'WARNING': '⚠️',
'ERROR': '❌',
'CRITICAL': '🔥'
};
for (const log of historyQueryData.logs || []) {
events.push({
id: `log_${log.id}`,
timestamp: log.timestamp,
type: 'log',
icon: iconMap[log.level] || '📝',
level: log.level,
summary: log.message?.substring(0, 100) || '',
detail: {
level: log.level,
module: log.module,
function: log.function,
line_number: log.line_number,
message: log.message,
stack_trace: log.stack_trace
}
});
}
// 按时间戳排序(降序)
events.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
// 应用限制(最多200条)
timelineData = events.slice(0, 200);
renderTimeline();
}
function renderTimeline() {
const container = document.getElementById('timeline-container');
if (!container) return;
if (timelineData.length === 0) {
container.innerHTML = '<div class="timeline-empty">' + i18n.t('monitor.status.no_data') + '</div>';
return;
}
// 异常检测
const anomalies = detectAnomalies(timelineData);
// 统计摘要(一次遍历完成所有统计)
const stats = { total: timelineData.length, http: 0, outbound: 0, log: 0, errors: 0, warnings: 0, slow: 0 };
for (const e of timelineData) {
if (e.type === 'http') stats.http++;
else if (e.type === 'outbound') stats.outbound++;
else if (e.type === 'log') stats.log++;
if (e.level === 'ERROR' || e.level === 'CRITICAL') stats.errors++;
else if (e.level === 'WARNING') stats.warnings++;
if (e.detail?.is_slow) stats.slow++;
}
// 统计摘要HTML
const summaryTitle = typeof i18n !== 'undefined' ? i18n.t('monitor.timeline.summary_title') : '📊 时间线统计';
const totalLabel = typeof i18n !== 'undefined' ? i18n.t('monitor.timeline.total_events') : '总事件';
const httpLabel = typeof i18n !== 'undefined' ? i18n.t('monitor.timeline.http_requests') : 'HTTP请求';
const outboundLabel = typeof i18n !== 'undefined' ? i18n.t('monitor.timeline.outbound_requests') : '发送请求';
const logLabel = typeof i18n !== 'undefined' ? i18n.t('monitor.timeline.system_logs') : '系统日志';
const errorLabel = typeof i18n !== 'undefined' ? i18n.t('monitor.timeline.errors') : '错误';
const warningLabel = typeof i18n !== 'undefined' ? i18n.t('monitor.timeline.warnings') : '警告';
const slowLabel = typeof i18n !== 'undefined' ? i18n.t('monitor.timeline.slow_requests') : '慢请求';
let summaryHtml = `<div style="background:#f0f9ff;border:1px solid #bae6fd;padding:12px;margin-bottom:16px;border-radius:6px;">
<strong style="color:#0369a1;">${summaryTitle}</strong>
<div style="display:flex;gap:16px;margin-top:8px;font-size:12px;color:#0c4a6e;flex-wrap:wrap;">
<span><strong>${stats.total}</strong> ${totalLabel}</span>
<span>📥 <strong>${stats.http}</strong> ${httpLabel}</span>
<span>📤 <strong>${stats.outbound}</strong> ${outboundLabel}</span>
<span>📝 <strong>${stats.log}</strong> ${logLabel}</span>
${stats.errors > 0 ? `<span style="color:#dc2626;">❌ <strong>${stats.errors}</strong> ${errorLabel}</span>` : ''}
${stats.warnings > 0 ? `<span style="color:#f59e0b;">⚠️ <strong>${stats.warnings}</strong> ${warningLabel}</span>` : ''}
${stats.slow > 0 ? `<span style="color:#ef4444;">⏱️ <strong>${stats.slow}</strong> ${slowLabel}</span>` : ''}
</div>
</div>`;
// 异常告警HTML(如果有异常)
if (anomalies.length > 0) {
const anomalyText = typeof i18n !== 'undefined' ?
i18n.t('monitor.timeline.anomaly_detected', {count: anomalies.length}) :
`发现 ${anomalies.length} 处异常`;
summaryHtml += `<div style="background:#fef2f2;border:1px solid #fecaca;padding:12px;margin-bottom:16px;border-radius:6px;">
<strong style="color:#dc2626;">⚠️ ${anomalyText}</strong>
<div style="font-size:12px;color:#991b1b;margin-top:4px;">
${anomalies.map(a => a.description).join('、')}
</div>
</div>`;
}
const renderedHtml = timelineData.map(event => {
const isError = event.level === 'ERROR' || event.level === 'CRITICAL';
const isWarning = event.level === 'WARNING';
const isAnomaly = anomalies.some(a => a.eventId === event.id);
const errorClass = isError ? 'error' : (isWarning ? 'warning' : '');
const anomalyIcon = isAnomaly ? ' ⚠️' : '';
const time = formatDateTime(event.timestamp);
return `
<div class="timeline-event ${errorClass}" onclick="showTimelineDetail('${event.id}')">
<span class="timeline-icon">${event.icon}${anomalyIcon}</span>
<div class="timeline-content">
<div class="timeline-time">${time} <span class="timeline-type">${event.type}</span></div>
<div class="timeline-summary">${event.summary}</div>
</div>
</div>
`;
}).join('');
container.innerHTML = summaryHtml + renderedHtml;
}
// 异常检测算法
function detectAnomalies(events) {
const anomalies = [];
// 1. 检测连续ERROR(≥5条)
let consecutiveErrors = 0;
let errorStart = null;
for (let i = 0; i < events.length; i++) {
if (events[i].level === 'ERROR' || events[i].level === 'CRITICAL') {
if (consecutiveErrors === 0) errorStart = i;
consecutiveErrors++;
if (consecutiveErrors >= 5) {
anomalies.push({
eventId: events[i].id,
type: 'error_burst',
description: `连续${consecutiveErrors}条ERROR日志`
});
}
} else {
consecutiveErrors = 0;
}
}
// 2. 检测异常慢请求(响应时间>平均值3倍)
const durations = events
.filter(e => e.type === 'http' && e.detail?.duration)
.map(e => e.detail.duration);
if (durations.length > 0) {
const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
const threshold = avg * 3;
events.forEach(e => {
if (e.type === 'http' && e.detail?.duration > threshold) {
const slowReqText = typeof i18n !== 'undefined' ?
i18n.t('monitor.timeline.slow_anomaly', {duration: e.detail.duration.toFixed(0), avg: avg.toFixed(0)}) :
`异常慢请求(${e.detail.duration.toFixed(0)}ms > 平均${avg.toFixed(0)}ms×3)`;
anomalies.push({
eventId: e.id,
type: 'slow_anomaly',
description: slowReqText
});
}
});
}
// 3. 检测重复错误(相同消息≥3次)
const errorMessages = {};
events.forEach(e => {
if (e.level === 'ERROR' || e.level === 'CRITICAL') {
const msg = e.summary || '';
errorMessages[msg] = (errorMessages[msg] || []);
errorMessages[msg].push(e.id);
}
});
Object.entries(errorMessages).forEach(([msg, ids]) => {
if (ids.length >= 3) {
const dupErrorText = typeof i18n !== 'undefined' ?
i18n.t('monitor.timeline.duplicate_error', {msg: msg.substring(0, 20), count: ids.length}) :
`重复错误"${msg.substring(0, 20)}..."出现${ids.length}`;
anomalies.push({
eventId: ids[ids.length - 1],
type: 'duplicate_error',
description: dupErrorText
});
}
});
return anomalies;
}
function showTimelineDetail(eventId) {
const event = timelineData.find(e => e.id === eventId);
if (!event) return;
alert(JSON.stringify(event.detail, null, 2));
}
// 图表渲染(HTML/CSS可视化)
async function renderCharts() {
const startTime = document.getElementById('history-start-time').value;
const endTime = document.getElementById('history-end-time').value;
if (!startTime || !endTime) {
document.getElementById('tab-chart').innerHTML = '<div style="padding:40px;text-align:center;color:#999;">' + i18n.t('monitor.chart.query_required') + '</div>';
return;
}
try {
const url = `${API_BASE}/history/stats?start_time=${encodeURIComponent(startTime)}&end_time=${encodeURIComponent(endTime)}`;
const response = await fetch(url);
const data = await response.json();
// 渲染级别分布(柱状图)
const levelCanvas = document.getElementById('chart-levels');
if (levelCanvas) {
const levels = data.level_distribution || {};
const total = Object.values(levels).reduce((a, b) => a + b, 0) || 1;
const colors = {DEBUG: '#3b82f6', INFO: '#22c55e', WARNING: '#f59e0b', ERROR: '#ef4444', CRITICAL: '#dc2626'};
const levelHtml = Object.entries(levels)
.map(([k, v]) => {
const pct = (v / total * 100).toFixed(1);
const color = colors[k] || '#6b7280';
return `<div style="margin:8px 0;">
<div style="display:flex;justify-content:space-between;margin-bottom:4px;">
<span style="font-weight:600;color:${color};">${k}</span>
<span>${v}条 (${pct}%)</span>
</div>
<div style="background:#e5e7eb;border-radius:4px;height:20px;overflow:hidden;">
<div style="background:${color};width:${pct}%;height:100%;transition:width 0.3s;"></div>
</div>
</div>`;
}).join('') || `<div style="color:#999;text-align:center;">${typeof i18n !== 'undefined' ? i18n.t('monitor.chart.no_data') : '无数据'}</div>`;
const levelTitle = typeof i18n !== 'undefined' ? i18n.t('monitor.chart.level_distribution') : '📊 日志级别分布';
levelCanvas.parentElement.innerHTML = `<h4>${levelTitle}</h4><div style="padding:8px 0;">` + levelHtml + '</div>';
}
// 渲染状态码分布(饼图样式)
const statusCanvas = document.getElementById('chart-status');
if (statusCanvas) {
const statuses = data.status_distribution || {};
const total = Object.values(statuses).reduce((a, b) => a + b, 0) || 1;
const colors = {'2xx': '#22c55e', '3xx': '#3b82f6', '4xx': '#f59e0b', '5xx': '#ef4444'};
const statusHtml = Object.entries(statuses)
.map(([k, v]) => {
const pct = (v / total * 100).toFixed(1);
const color = colors[k] || '#6b7280';
return `<div style="display:inline-block;margin:8px;text-align:center;">
<div style="width:60px;height:60px;border-radius:50%;background:${color};display:flex;align-items:center;justify-content:center;color:white;font-weight:bold;font-size:14px;">${pct}%</div>
<div style="margin-top:4px;font-size:12px;">${k} (${v})</div>
</div>`;
}).join('') || `<div style="color:#999;text-align:center;">${typeof i18n !== 'undefined' ? i18n.t('monitor.chart.no_data') : '无数据'}</div>`;
const statusTitle = typeof i18n !== 'undefined' ? i18n.t('monitor.chart.status_distribution') : '📈 状态码分布';
statusCanvas.parentElement.innerHTML = `<h4>${statusTitle}</h4><div style="text-align:center;padding:8px 0;">` + statusHtml + '</div>';
}
// 渲染慢请求TOP10(表格)
const slowCanvas = document.getElementById('chart-slow');
if (slowCanvas) {
const slowList = data.slow_requests || [];
const slowHtml = slowList.length > 0
? `<table style="width:100%;font-size:12px;border-collapse:collapse;">
<thead><tr style="background:#f3f4f6;">
<th style="padding:6px;text-align:left;">排名</th>
<th style="padding:6px;text-align:left;">接口</th>
<th style="padding:6px;text-align:right;">耗时</th>
</tr></thead>
<tbody>
${slowList.slice(0, 10).map((r, i) => `<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:6px;">${i+1}</td>
<td style="padding:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;">${r.method} ${r.path}</td>
<td style="padding:6px;text-align:right;color:#ef4444;font-weight:bold;">${r.duration.toFixed(0)}ms</td>
</tr>`).join('')}
</tbody>
</table>`
: `<div style="color:#22c55e;text-align:center;padding:20px;">✅ ${typeof i18n !== 'undefined' ? i18n.t('monitor.chart.no_slow') : '无慢请求'}</div>`;
const slowTitle = typeof i18n !== 'undefined' ? i18n.t('monitor.chart.slow_requests') : '⏱️ 慢请求TOP10';
slowCanvas.parentElement.innerHTML = `<h4>${slowTitle}</h4>` + slowHtml;
}
// 渲染趋势摘要
const trendCanvas = document.getElementById('chart-trend');
if (trendCanvas) {
const trend = data.trend || [];
const summary = data.summary || {};
const trendHtml = trend.length > 0
? `<div style="padding:12px;">
<div style="display:flex;justify-content:space-around;text-align:center;">
<div><div style="font-size:28px;font-weight:bold;color:#3b82f6;">${summary.total_requests || 0}</div><div style="font-size:12px;color:#6b7280;">${typeof i18n !== 'undefined' ? i18n.t('monitor.chart.total_requests') : '总请求'}</div></div>
<div><div style="font-size:28px;font-weight:bold;color:#ef4444;">${summary.error_count || 0}</div><div style="font-size:12px;color:#6b7280;">${typeof i18n !== 'undefined' ? i18n.t('monitor.chart.error_count') : '错误'}</div></div>
<div><div style="font-size:28px;font-weight:bold;color:#f59e0b;">${summary.slow_count || 0}</div><div style="font-size:12px;color:#6b7280;">${typeof i18n !== 'undefined' ? i18n.t('monitor.chart.slow_count') : '慢请求'}</div></div>
</div>
<div style="margin-top:12px;padding-top:12px;border-top:1px solid #e5e7eb;font-size:12px;color:#6b7280;">
${typeof i18n !== 'undefined' ? i18n.t('monitor.stats.time_range') : '时间范围'}: ${trend.length}${typeof i18n !== 'undefined' ? i18n.t('monitor.other.data_points') : '个时间点'} | ${typeof i18n !== 'undefined' ? i18n.t('monitor.chart.log_count') : '日志总数'}: ${summary.total_logs || 0}${typeof i18n !== 'undefined' ? i18n.t('monitor.pagination.items') : '条'}
</div>
</div>`
: `<div style="color:#999;text-align:center;padding:20px;">${typeof i18n !== 'undefined' ? i18n.t('monitor.chart.no_trend') : '无趋势数据'}</div>`;
const trendTitle = typeof i18n !== 'undefined' ? i18n.t('monitor.chart.request_trend') : '📊 请求量趋势';
trendCanvas.parentElement.innerHTML = `<h4>${trendTitle}</h4>` + trendHtml;
}
} catch (error) {
console.error('渲染图表失败:', error);
const chartContainer = document.querySelector('.chart-container');
if (chartContainer) {
chartContainer.innerHTML = '<div style="padding:40px;text-align:center;color:#ef4444;">' + i18n.t('monitor.chart.load_failed') + '</div>';
}
}
}
// 实时追踪
let realtimeTracking = false;
let realtimeInterval = null;
let lastTimestamp = null;
let trackingDuration = 0;
function toggleRealtimeTracking() {
realtimeTracking = !realtimeTracking;
const btn = document.getElementById('realtime-btn');
if (realtimeTracking) {
btn.textContent = i18n.t('monitor.btn.realtime_on');
btn.className = 'btn btn-primary';
startRealtimeTracking();
} else {
btn.textContent = i18n.t('monitor.btn.realtime_off');
btn.className = 'btn btn-secondary';
stopRealtimeTracking();
}
}
function startRealtimeTracking() {
trackingDuration = 0;
lastTimestamp = new Date().toISOString();
realtimeInterval = setInterval(async () => {
trackingDuration += 5;
// 10分钟自动暂停
if (trackingDuration >= 600) {
stopRealtimeTracking();
alert(i18n.t('monitor.error.auto_pause'));
return;
}
try {
const url = `${API_BASE}/history/recent?since_timestamp=${encodeURIComponent(lastTimestamp)}&data_type=logs&limit=50`;
const response = await fetch(url);
const data = await response.json();
if (data.events && data.events.length > 0) {
lastTimestamp = data.latest_timestamp;
// 追加到数据顶部并高亮
const newLogs = data.events.map(e => ({
id: e.id,
timestamp: e.timestamp,
level: e.level,
module: e.module,
message: e.message,
_isNew: true
}));
historyQueryData.logs = [...newLogs, ...historyQueryData.logs].slice(0, 1000);
renderLogsResults();
}
} catch (error) {
console.error('实时追踪失败:', error);
}
}, 5000);
}
function stopRealtimeTracking() {
if (realtimeInterval) {
clearInterval(realtimeInterval);
realtimeInterval = null;
}
realtimeTracking = false;
const btn = document.getElementById('realtime-btn');
btn.textContent = i18n.t('monitor.btn.realtime_off');
btn.className = 'btn btn-secondary';
}
// ========== 查询模板管理 ==========
const TEMPLATE_STORAGE_KEY = 'logQueryTemplates';
function getTemplates() {
try {
return JSON.parse(localStorage.getItem(TEMPLATE_STORAGE_KEY)) || [];
} catch {
return [];
}
}
function saveTemplates(templates) {
localStorage.setItem(TEMPLATE_STORAGE_KEY, JSON.stringify(templates));
updateTemplateSelect();
}
function updateTemplateSelect() {
const select = document.getElementById('saved-templates');
if (!select) return;
const templates = getTemplates();
select.innerHTML = '<option value="">' + i18n.t('monitor.template.saved_queries') + '</option>' +
templates.map((t, i) => `<option value="${i}">${t.name}</option>`).join('');
}
function saveQueryTemplate() {
const templates = getTemplates();
if (templates.length >= 10) {
alert(i18n.t('monitor.error.max_templates'));
return;
}
const name = prompt('请输入模板名称:');
if (!name || !name.trim()) return;
const template = {
name: name.trim(),
created_at: new Date().toISOString(),
conditions: {
log_level: document.getElementById('history-log-level').value,
query_type: document.getElementById('history-query-type').value,
module: document.getElementById('filter-module').value,
keyword: document.getElementById('filter-keyword').value,
method: document.getElementById('filter-method').value,
client_ip: document.getElementById('filter-client-ip').value,
status_min: document.getElementById('filter-status-min').value,
status_max: document.getElementById('filter-status-max').value,
duration_min: document.getElementById('filter-duration-min').value,
duration_max: document.getElementById('filter-duration-max').value
}
};
templates.push(template);
saveTemplates(templates);
alert(i18n.t('monitor.success.template_saved', {name: name}));
}
function loadQueryTemplate() {
const select = document.getElementById('saved-templates');
const index = select.value;
if (!index) return;
const templates = getTemplates();
const template = templates[parseInt(index)];
if (!template) return;
const c = template.conditions;
document.getElementById('history-log-level').value = c.log_level || '';
document.getElementById('history-query-type').value = c.query_type || 'all';
document.getElementById('filter-module').value = c.module || '';
document.getElementById('filter-keyword').value = c.keyword || '';
document.getElementById('filter-method').value = c.method || '';
document.getElementById('filter-client-ip').value = c.client_ip || '';
document.getElementById('filter-status-min').value = c.status_min || '';
document.getElementById('filter-status-max').value = c.status_max || '';
document.getElementById('filter-duration-min').value = c.duration_min || '';
document.getElementById('filter-duration-max').value = c.duration_max || '';
updateFilterActiveState();
}
// 初始化模板列表
document.addEventListener('DOMContentLoaded', function() {
updateTemplateSelect();
});
function updateQuerySummary(startTime, endTime) {
const timeRange = document.getElementById('query-time-range');
const httpCount = document.getElementById('http-count');
const outboundCount = document.getElementById('outbound-count');
const logsCount = document.getElementById('logs-count');
const logsLevelsSeparator = document.getElementById('logs-levels-separator');
const logsLevelsLabel = document.getElementById('logs-levels-label');
const debugLabel = document.getElementById('logs-debug-label');
const debugCount = document.getElementById('logs-debug-count');
const infoLabel = document.getElementById('logs-info-label');
const infoCount = document.getElementById('logs-info-count');
const warningLabel = document.getElementById('logs-warning-label');
const warningCount = document.getElementById('logs-warning-count');
const errorLabel = document.getElementById('logs-error-label');
const errorCount = document.getElementById('logs-error-count');
const criticalLabel = document.getElementById('logs-critical-label');
const criticalCount = document.getElementById('logs-critical-count');
if (timeRange) {
timeRange.textContent = startTime && endTime ? `${startTime}${endTime}` : '全部时间';
}
// 使用后端返回的统计数据
const stats = queryStats || {};
if (httpCount) {
httpCount.textContent = stats.http_count || 0;
}
if (outboundCount) {
outboundCount.textContent = stats.outbound_count || 0;
}
if (logsCount) {
logsCount.textContent = stats.logs_count || 0;
}
// 使用后端返回的级别统计
const levelStats = stats.level_distribution || {};
const hasLogs = stats.logs_count > 0;
// 显示/隐藏级别分布标签
if (logsLevelsSeparator) {
logsLevelsSeparator.style.display = hasLogs && Object.keys(levelStats).length > 0 ? 'inline' : 'none';
}
if (logsLevelsLabel) {
logsLevelsLabel.style.display = hasLogs && Object.keys(levelStats).length > 0 ? 'inline' : 'none';
}
// 只显示有数据的级别
if (debugLabel) {
debugLabel.style.display = levelStats.DEBUG ? 'inline' : 'none';
}
if (debugCount) {
debugCount.style.display = levelStats.DEBUG ? 'inline' : 'none';
debugCount.textContent = levelStats.DEBUG || 0;
}
if (infoLabel) {
infoLabel.style.display = levelStats.INFO ? 'inline' : 'none';
}
if (infoCount) {
infoCount.style.display = levelStats.INFO ? 'inline' : 'none';
infoCount.textContent = levelStats.INFO || 0;
}
if (warningLabel) {
warningLabel.style.display = levelStats.WARNING ? 'inline' : 'none';
}
if (warningCount) {
warningCount.style.display = levelStats.WARNING ? 'inline' : 'none';
warningCount.textContent = levelStats.WARNING || 0;
}
if (errorLabel) {
errorLabel.style.display = levelStats.ERROR ? 'inline' : 'none';
}
if (errorCount) {
errorCount.style.display = levelStats.ERROR ? 'inline' : 'none';
errorCount.textContent = levelStats.ERROR || 0;
}
if (criticalLabel) {
criticalLabel.style.display = levelStats.CRITICAL ? 'inline' : 'none';
}
if (criticalCount) {
criticalCount.style.display = levelStats.CRITICAL ? 'inline' : 'none';
criticalCount.textContent = levelStats.CRITICAL || 0;
}
}
// 缓存排序后的数据,避免重复排序
let sortedHttpData = [];
let sortedOutboundData = [];
let sortedLogsData = [];
// 精确定位状态
let preciseMode = false;
let preciseTimeStart = 0;
let preciseTimeEnd = 0;
function renderHttpResults() {
const tbody = document.getElementById('http-results-body');
if (!tbody) return;
if (historyQueryData.http.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="no-data">' + i18n.t('monitor.status.no_data') + '</td></tr>';
sortedHttpData = [];
return;
}
sortedHttpData = [...historyQueryData.http].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
tbody.innerHTML = sortedHttpData.map((req, index) => {
const methodClass = req.method.toLowerCase();
const statusClass = req.status_code >= 400 ? 'error' : 'success';
const time = formatDateTime(req.timestamp);
const duration = req.duration ? req.duration.toFixed(2) + 'ms' : '-';
const timestamp = new Date(req.timestamp).getTime();
return `
<tr data-timestamp="${timestamp}">
<td>${index + 1}</td>
<td class="font-mono">${time}</td>
<td><span class="api-method ${methodClass}">${req.method}</span></td>
<td class="font-mono" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${req.path || '-'}</td>
<td class="font-mono"><span class="api-status ${statusClass}">${req.status_code || '-'}</span></td>
<td class="font-mono">${duration}</td>
<td class="font-mono">${req.client_ip || '-'}</td>
<td>
<button class="action-btn" onclick="showHistoryHttpDetail(${index})">详情</button>
<button class="action-btn precise-btn" onclick="preciseLocate(${index}, 'http')">± 60s</button>
</td>
</tr>
`;
}).join('');
}
function renderOutboundResults() {
const tbody = document.getElementById('outbound-results-body');
if (!tbody) return;
if (historyQueryData.outbound.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="no-data">' + i18n.t('monitor.status.no_data') + '</td></tr>';
sortedOutboundData = [];
return;
}
sortedOutboundData = [...historyQueryData.outbound].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
tbody.innerHTML = sortedOutboundData.map((req, index) => {
const methodClass = req.method.toLowerCase();
const statusClass = req.status_code >= 400 ? 'error' : 'success';
const time = formatDateTime(req.timestamp);
const duration = req.duration ? req.duration.toFixed(2) + 'ms' : '-';
const timestamp = new Date(req.timestamp).getTime();
return `
<tr data-timestamp="${timestamp}">
<td>${index + 1}</td>
<td class="font-mono">${time}</td>
<td><span class="api-method ${methodClass}">${req.method}</span></td>
<td class="font-mono" style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${req.url || '-'}</td>
<td class="font-mono"><span class="api-status ${statusClass}">${req.status_code || '-'}</span></td>
<td class="font-mono">${duration}</td>
<td class="font-mono">${req.module || '-'}</td>
<td>
<button class="action-btn" onclick="showHistoryOutboundDetail(${index})">详情</button>
<button class="action-btn precise-btn" onclick="preciseLocate(${index}, 'outbound')">± 60s</button>
</td>
</tr>
`;
}).join('');
}
function renderLogsResults() {
const tbody = document.getElementById('logs-results-body');
if (!tbody) return;
if (historyQueryData.logs.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="no-data">' + i18n.t('monitor.status.no_data') + '</td></tr>';
sortedLogsData = [];
return;
}
sortedLogsData = [...historyQueryData.logs].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
tbody.innerHTML = sortedLogsData.map((log, index) => {
const levelClass = log.level ? log.level.toLowerCase() : 'info';
const time = formatDateTime(log.timestamp);
const timestamp = new Date(log.timestamp).getTime();
return `
<tr data-timestamp="${timestamp}">
<td>${index + 1}</td>
<td class="font-mono">${time}</td>
<td class="font-mono"><span class="log-level-badge ${levelClass}">${log.level || '-'}</span></td>
<td class="font-mono">${log.module || '-'}</td>
<td class="log-message-cell" style="white-space: normal; word-break: break-all;">${log.message || '-'}</td>
<td>
<button class="action-btn" onclick="showHistoryLogDetail(${index})">详情</button>
<button class="action-btn precise-btn" onclick="preciseLocate(${index}, 'logs')">± 60s</button>
</td>
</tr>
`;
}).join('');
}
function formatQueryParams(params) {
if (!params) return '-';
let parsedParams = params;
// 如果是字符串,尝试解析为 JSON
if (typeof params === 'string') {
try {
parsedParams = JSON.parse(params);
} catch (e) {
// 如果解析失败,直接返回原字符串
return params;
}
}
if (typeof parsedParams !== 'object' || Array.isArray(parsedParams)) return '-';
const pairs = [];
for (const [key, value] of Object.entries(parsedParams)) {
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
}
return pairs.length > 0 ? '?' + pairs.join('&') : '-';
}
async function showHistoryHttpDetail(index) {
const req = sortedHttpData[index];
if (!req) return;
// 懒加载:如果数据不包含request_body,则从API加载完整数据
let detailData = req;
if (!req.hasOwnProperty('request_body')) {
try {
const response = await fetch(`${API_BASE}/history/http/${req.id}`);
detailData = await response.json();
} catch (error) {
console.error('加载详情失败:', error);
alert(i18n.t('monitor.error.detail_load_failed') || '加载详情失败');
return;
}
}
const detail = `
<div style="max-height: 60vh; overflow-y: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 120px; color: #8c8c8c;">请求ID</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.id || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">时间戳</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.timestamp || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">请求方法</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;"><span class="api-method ${(detailData.method || '').toLowerCase()}">${detailData.method || '-'}</span></td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">请求路径</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.path || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">查询参数</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${formatQueryParams(detailData.query_params)}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">状态码</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono api-status ${(detailData.status_code || 0) >= 400 ? 'error' : 'success'}">${detailData.status_code || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">响应时间</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.duration ? detailData.duration.toFixed(2) + 'ms' : '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">客户端IP</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.client_ip || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">User Agent</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.user_agent || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.is_slow') : '是否慢请求'}</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${detailData.is_slow ? (typeof i18n !== 'undefined' ? i18n.t('monitor.other.yes') : '是') : (typeof i18n !== 'undefined' ? i18n.t('monitor.other.no') : '否')}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.is_error') : '是否错误'}</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${detailData.is_error ? (typeof i18n !== 'undefined' ? i18n.t('monitor.other.yes') : '是') : (typeof i18n !== 'undefined' ? i18n.t('monitor.other.no') : '否')}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.error_message') : '错误信息'}</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #f5222d;" class="font-mono">${detailData.error_message || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">请求体</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono"><pre style="margin:0;white-space:pre-wrap;word-break:break-all;">${detailData.request_body || '-'}</pre></td></tr>
<tr><td style="padding: 8px; color: #8c8c8c;">响应体</td><td style="padding: 8px;" class="font-mono"><pre style="margin:0;white-space:pre-wrap;word-break:break-all;">${detailData.response_body || '-'}</pre></td></tr>
</table>
</div>
`;
showDetailModal('接收请求详情', detail);
}
async function showHistoryOutboundDetail(index) {
const req = sortedOutboundData[index];
if (!req) return;
// 懒加载:如果数据不包含request_body,则从API加载完整数据
let detailData = req;
if (!req.hasOwnProperty('request_body')) {
try {
const response = await fetch(`${API_BASE}/history/outbound/${req.id}`);
detailData = await response.json();
} catch (error) {
console.error('加载详情失败:', error);
alert(i18n.t('monitor.error.detail_load_failed') || '加载详情失败');
return;
}
}
const detail = `
<div style="max-height: 60vh; overflow-y: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 120px; color: #8c8c8c;">请求ID</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.id || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">时间戳</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.timestamp || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">请求方法</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;"><span class="api-method ${(detailData.method || '').toLowerCase()}">${detailData.method || '-'}</span></td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">请求URL</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.url || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">状态码</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono api-status ${(detailData.status_code || 0) >= 400 ? 'error' : 'success'}">${detailData.status_code || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">响应时间</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.duration ? detailData.duration.toFixed(2) + 'ms' : '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">模块</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.module || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.is_slow') : '是否慢请求'}</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${detailData.is_slow ? (typeof i18n !== 'undefined' ? i18n.t('monitor.other.yes') : '是') : (typeof i18n !== 'undefined' ? i18n.t('monitor.other.no') : '否')}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.is_error') : '是否错误'}</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;">${detailData.is_error ? (typeof i18n !== 'undefined' ? i18n.t('monitor.other.yes') : '是') : (typeof i18n !== 'undefined' ? i18n.t('monitor.other.no') : '否')}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.error_message') : '错误信息'}</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #f5222d;" class="font-mono">${detailData.error_message || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">请求体</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono"><pre style="margin:0;white-space:pre-wrap;word-break:break-all;">${detailData.request_body || '-'}</pre></td></tr>
<tr><td style="padding: 8px; color: #8c8c8c;">响应体</td><td style="padding: 8px;" class="font-mono"><pre style="margin:0;white-space:pre-wrap;word-break:break-all;">${detailData.response_body || '-'}</pre></td></tr>
</table>
</div>
`;
showDetailModal('发送请求详情', detail);
}
async function showHistoryLogDetail(index) {
const log = sortedLogsData[index];
if (!log) return;
// 懒加载:如果数据不包含stack_trace,则从API加载完整数据
let detailData = log;
if (!log.hasOwnProperty('stack_trace')) {
try {
const response = await fetch(`${API_BASE}/history/log/${log.id}`);
detailData = await response.json();
} catch (error) {
console.error('加载详情失败:', error);
alert(i18n.t('monitor.error.detail_load_failed') || '加载详情失败');
return;
}
}
const detail = `
<div style="max-height: 60vh; overflow-y: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; width: 120px; color: #8c8c8c;">日志ID</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.id || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">时间戳</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.timestamp || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">日志级别</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono"><span class="log-level-badge ${(detailData.level || '').toLowerCase()}">${detailData.level || '-'}</span></td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">模块</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.module || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">函数</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.function || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">行号</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono">${detailData.line_number || '-'}</td></tr>
<tr><td style="padding: 8px; border-bottom: 1px solid #e8e8e8; color: #8c8c8c;">消息</td><td style="padding: 8px; border-bottom: 1px solid #e8e8e8;" class="font-mono"><pre style="margin:0;white-space:pre-wrap;word-break:break-all;">${detailData.message || '-'}</pre></td></tr>
${detailData.stack_trace ? `<tr><td style="padding: 8px; color: #8c8c8c;">堆栈</td><td style="padding: 8px;" class="font-mono"><pre style="margin:0;white-space:pre-wrap;word-break:break-all;color:#f5222d;">${detailData.stack_trace}</pre></td></tr>` : ''}
</table>
</div>
`;
showDetailModal('系统日志详情', detail);
}
function showDetailModal(title, content) {
const existingModal = document.getElementById('detail-modal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.id = 'detail-modal';
modal.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; display: flex; justify-content: center; align-items: center;';
const overlay = document.createElement('div');
overlay.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(3px);';
overlay.onclick = () => modal.remove();
const modalContent = document.createElement('div');
modalContent.style.cssText = 'position: relative; background: white; border-radius: 8px; padding: 0; max-width: 700px; width: 90%; max-height: 80vh; overflow: hidden; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);';
const header = document.createElement('div');
header.style.cssText = 'background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%); color: white; padding: 15px 20px; display: flex; justify-content: space-between; align-items: center;';
header.innerHTML = `<h3 style="margin: 0; font-size: 16px; font-weight: 500;">${title}</h3><button onclick="document.getElementById('detail-modal').remove()" style="background: none; border: none; color: white; font-size: 24px; cursor: pointer; line-height: 1;">×</button>`;
const body = document.createElement('div');
body.style.cssText = 'padding: 20px;';
body.innerHTML = content;
modalContent.appendChild(header);
modalContent.appendChild(body);
modal.appendChild(overlay);
modal.appendChild(modalContent);
document.body.appendChild(modal);
}
// 精确定位功能
function preciseLocate(index, type) {
let timestamp;
let rowElement;
let tableId;
// 获取选中记录的时间戳和对应的行元素
switch (type) {
case 'http':
timestamp = new Date(sortedHttpData[index].timestamp).getTime();
tableId = 'http-results-body';
break;
case 'outbound':
timestamp = new Date(sortedOutboundData[index].timestamp).getTime();
tableId = 'outbound-results-body';
break;
case 'logs':
timestamp = new Date(sortedLogsData[index].timestamp).getTime();
tableId = 'logs-results-body';
break;
default:
return;
}
// 设置时间范围(前后60秒)
preciseTimeStart = timestamp - 60000;
preciseTimeEnd = timestamp + 60000;
preciseMode = true;
// 更新UI状态提示
updatePreciseModeUI(true);
// 隐藏不在时间范围内的记录(带动画)
filterByTimeRangeWithAnimation();
// 延迟执行滚动和高亮,等待过滤动画完成
setTimeout(() => {
// 找到对应的行元素
rowElement = document.querySelector(`#${tableId} tr[data-timestamp="${timestamp}"]`);
if (rowElement) {
// 平滑滚动到选中的记录
rowElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// 添加高亮动画
highlightRow(rowElement);
}
}, 300);
}
// 高亮行动画
function highlightRow(row) {
// 移除之前的高亮
document.querySelectorAll('.highlight-row').forEach(r => r.classList.remove('highlight-row'));
// 添加高亮类
row.classList.add('highlight-row');
// 3秒后移除高亮
setTimeout(() => {
row.classList.remove('highlight-row');
}, 3000);
}
// 根据时间范围过滤记录(无动画)
function filterByTimeRange() {
if (!preciseMode) {
// 取消精确定位,显示所有记录
document.querySelectorAll('table[data-result-table] tr').forEach(row => {
row.style.opacity = '1';
row.style.display = '';
});
return;
}
// 隐藏不在时间范围内的记录
document.querySelectorAll('table[data-result-table] tr').forEach(row => {
const rowTimestamp = parseInt(row.getAttribute('data-timestamp'));
if (isNaN(rowTimestamp)) {
// 标题行或空数据行
row.style.display = '';
} else if (rowTimestamp >= preciseTimeStart && rowTimestamp <= preciseTimeEnd) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
// 根据时间范围过滤记录(带动画)
function filterByTimeRangeWithAnimation() {
if (!preciseMode) {
// 取消精确定位,显示所有记录
document.querySelectorAll('table[data-result-table] tr').forEach(row => {
row.style.transition = 'all 0.3s ease';
row.style.opacity = '1';
row.style.display = '';
});
return;
}
// 先隐藏所有数据行(带动画)
document.querySelectorAll('table[data-result-table] tr').forEach(row => {
const rowTimestamp = parseInt(row.getAttribute('data-timestamp'));
if (!isNaN(rowTimestamp)) {
row.style.transition = 'all 0.3s ease';
row.style.opacity = '0';
}
});
// 延迟后显示符合条件的记录
setTimeout(() => {
document.querySelectorAll('table[data-result-table] tr').forEach(row => {
const rowTimestamp = parseInt(row.getAttribute('data-timestamp'));
if (isNaN(rowTimestamp)) {
// 标题行或空数据行
row.style.display = '';
} else if (rowTimestamp >= preciseTimeStart && rowTimestamp <= preciseTimeEnd) {
row.style.display = '';
row.style.opacity = '1';
} else {
row.style.display = 'none';
row.style.opacity = '1';
}
});
}, 300);
}
// 更新精确定位模式UI
function updatePreciseModeUI(active) {
const container = document.getElementById('precise-mode-container');
if (!container) return;
let badge = document.getElementById('precise-mode-badge');
if (!badge) {
badge = document.createElement('button');
badge.id = 'precise-mode-badge';
badge.className = 'precise-mode-btn';
badge.onclick = cancelPreciseLocate;
}
if (active) {
badge.textContent = '± 60s - 点击取消';
container.appendChild(badge);
} else {
if (badge.parentNode === container) {
container.removeChild(badge);
}
}
}
// 取消精确定位
function cancelPreciseLocate() {
// 先隐藏当前显示的记录(带动画)
document.querySelectorAll('table[data-result-table] tr').forEach(row => {
const rowTimestamp = parseInt(row.getAttribute('data-timestamp'));
if (!isNaN(rowTimestamp)) {
row.style.transition = 'all 0.3s ease';
row.style.opacity = '0';
}
});
// 延迟后更新状态并显示所有记录
setTimeout(() => {
preciseMode = false;
preciseTimeStart = 0;
preciseTimeEnd = 0;
updatePreciseModeUI(false);
// 显示所有记录并淡入
document.querySelectorAll('table[data-result-table] tr').forEach(row => {
row.style.transition = 'all 0.3s ease';
row.style.display = '';
row.style.opacity = '1';
});
}, 300);
}
window.onload = function() {
quickTimeRange('1h');
};
</script>
</body>
</html>