mirror of
https://github.com/rnvm9wjdtj-bot/myaps_api.git
synced 2026-06-02 05:54:40 +00:00
2706 lines
129 KiB
HTML
2706 lines
129 KiB
HTML
<!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}`;
|
||
}
|
||
|
||
// 高级过滤参数(根据当前queryType获取对应页签的过滤条件)
|
||
let filterModule = '';
|
||
let filterKeyword = '';
|
||
let filterMethod = '';
|
||
let filterClientIp = '';
|
||
let filterStatusMin = '';
|
||
let filterStatusMax = '';
|
||
let filterDurationMin = '';
|
||
let filterDurationMax = '';
|
||
|
||
// 只在查询特定类型时应用该类型的过滤条件
|
||
// 查询全部数据时不应用任何页签的过滤条件
|
||
if (queryType === 'logs') {
|
||
filterModule = document.getElementById('filter-module')?.value?.trim() || '';
|
||
filterKeyword = document.getElementById('filter-keyword')?.value?.trim() || '';
|
||
} else if (queryType === '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 (queryType === '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 = '';
|
||
|
||
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> |