<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="utf-8">
|
<title>设备日志</title>
|
<meta name="renderer" content="webkit">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<link rel="stylesheet" href="../../static/vue/element/element.css">
|
<link rel="stylesheet" href="../../static/css/common.css">
|
<style>
|
:root {
|
--dl-bg: linear-gradient(180deg, #edf3f7 0%, #e7edf4 100%);
|
--dl-panel-bg: rgba(248, 251, 253, 0.94);
|
--dl-panel-border: rgba(223, 232, 240, 0.96);
|
--dl-panel-shadow: 0 12px 26px rgba(114, 136, 164, 0.08);
|
--dl-text-main: #22384f;
|
--dl-text-sub: #708396;
|
--dl-accent: #6f95bd;
|
--dl-accent-strong: #557ca7;
|
--dl-success: #52b17e;
|
--dl-warning: #c78a3f;
|
--dl-danger: #c96660;
|
}
|
|
html, body {
|
width: 100%;
|
height: 100%;
|
margin: 0;
|
overflow: hidden;
|
}
|
|
body {
|
background: var(--dl-bg);
|
color: var(--dl-text-main);
|
}
|
|
#app {
|
width: 100%;
|
height: 100%;
|
}
|
|
.dl-shell {
|
width: 100%;
|
height: 100%;
|
padding: 16px;
|
box-sizing: border-box;
|
display: flex;
|
gap: 14px;
|
}
|
|
.dl-sidebar {
|
width: 320px;
|
min-width: 320px;
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
min-height: 0;
|
}
|
|
.dl-workbench {
|
flex: 1;
|
min-width: 0;
|
min-height: 0;
|
display: grid;
|
grid-template-columns: minmax(520px, 1.28fr) minmax(340px, 0.92fr);
|
gap: 14px;
|
}
|
|
.dl-center,
|
.dl-visual {
|
min-width: 0;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
}
|
|
.dl-center {
|
order: 2;
|
}
|
|
.dl-visual {
|
order: 1;
|
}
|
|
.dl-panel {
|
border-radius: 20px;
|
border: 1px solid var(--dl-panel-border);
|
background: var(--dl-panel-bg);
|
box-shadow: var(--dl-panel-shadow);
|
overflow: hidden;
|
min-height: 0;
|
}
|
|
.dl-panel-head {
|
padding: 14px 16px 10px;
|
border-bottom: 1px solid rgba(228, 236, 243, 0.92);
|
background: rgba(255, 255, 255, 0.26);
|
}
|
|
.dl-panel-title {
|
font-size: 15px;
|
font-weight: 700;
|
color: var(--dl-text-main);
|
line-height: 1.25;
|
}
|
|
.dl-panel-desc {
|
margin-top: 5px;
|
font-size: 12px;
|
color: var(--dl-text-sub);
|
line-height: 1.5;
|
}
|
|
.dl-panel-body {
|
padding: 14px 16px 16px;
|
box-sizing: border-box;
|
min-height: 0;
|
height: 100%;
|
}
|
|
.dl-hero {
|
padding: 16px;
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.88) 0%, rgba(240, 246, 251, 0.82) 100%);
|
}
|
|
.dl-hero-title {
|
font-size: 19px;
|
font-weight: 700;
|
color: var(--dl-text-main);
|
line-height: 1.2;
|
}
|
|
.dl-hero-desc {
|
margin-top: 8px;
|
font-size: 12px;
|
line-height: 1.6;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-hero-stats {
|
margin-top: 14px;
|
display: grid;
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
gap: 10px;
|
}
|
|
.dl-stat {
|
padding: 10px 12px;
|
border-radius: 14px;
|
border: 1px solid rgba(225, 233, 240, 0.94);
|
background: rgba(255, 255, 255, 0.7);
|
}
|
|
.dl-stat-value {
|
font-size: 18px;
|
font-weight: 700;
|
color: var(--dl-accent-strong);
|
}
|
|
.dl-stat-label {
|
margin-top: 4px;
|
font-size: 11px;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-date-panel,
|
.dl-device-panel,
|
.dl-log-panel,
|
.dl-raw-panel,
|
.dl-visual-card,
|
.dl-device-summary,
|
.dl-timeline-panel {
|
display: flex;
|
flex-direction: column;
|
min-height: 0;
|
}
|
|
.dl-date-panel {
|
flex: 0 0 260px;
|
}
|
|
.dl-device-panel {
|
flex: 1;
|
}
|
|
.dl-quick-days {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
margin-bottom: 14px;
|
}
|
|
.dl-day-chip {
|
padding: 0 10px;
|
height: 30px;
|
border-radius: 999px;
|
border: 1px solid rgba(219, 228, 236, 0.96);
|
background: rgba(255, 255, 255, 0.84);
|
color: #47627d;
|
cursor: pointer;
|
transition: all .16s ease;
|
}
|
|
.dl-day-chip.is-active {
|
border-color: rgba(111, 149, 189, 0.44);
|
background: rgba(111, 149, 189, 0.14);
|
color: var(--dl-accent-strong);
|
box-shadow: 0 8px 18px rgba(111, 149, 189, 0.16);
|
}
|
|
.dl-tree-wrap {
|
flex: 1;
|
min-height: 0;
|
overflow: auto;
|
padding-right: 4px;
|
scrollbar-gutter: stable;
|
}
|
|
.dl-type-tabs {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 8px;
|
margin-bottom: 12px;
|
}
|
|
.dl-type-tab {
|
padding: 10px 12px;
|
border-radius: 14px;
|
border: 1px solid rgba(222, 231, 239, 0.96);
|
background: rgba(255, 255, 255, 0.72);
|
cursor: pointer;
|
text-align: left;
|
transition: all .16s ease;
|
}
|
|
.dl-type-tab.is-active {
|
border-color: rgba(111, 149, 189, 0.44);
|
background: rgba(111, 149, 189, 0.14);
|
box-shadow: 0 10px 20px rgba(111, 149, 189, 0.12);
|
}
|
|
.dl-type-tab-label {
|
font-size: 13px;
|
font-weight: 700;
|
color: var(--dl-text-main);
|
}
|
|
.dl-type-tab-meta {
|
margin-top: 4px;
|
font-size: 11px;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-device-search {
|
margin-bottom: 12px;
|
}
|
|
.dl-device-list {
|
flex: 1;
|
min-height: 0;
|
overflow: auto;
|
padding-right: 4px;
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
scrollbar-gutter: stable;
|
}
|
|
.dl-device-item {
|
padding: 12px 13px;
|
border-radius: 16px;
|
border: 1px solid rgba(223, 232, 240, 0.94);
|
background: rgba(255, 255, 255, 0.72);
|
cursor: pointer;
|
transition: all .16s ease;
|
}
|
|
.dl-device-item.is-active {
|
border-color: rgba(111, 149, 189, 0.48);
|
background: rgba(111, 149, 189, 0.12);
|
box-shadow: 0 12px 24px rgba(111, 149, 189, 0.14);
|
}
|
|
.dl-device-item:hover {
|
transform: translateY(-1px);
|
}
|
|
.dl-device-name {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 8px;
|
font-size: 14px;
|
font-weight: 700;
|
color: var(--dl-text-main);
|
}
|
|
.dl-device-badge {
|
padding: 3px 8px;
|
border-radius: 999px;
|
background: rgba(111, 149, 189, 0.12);
|
color: var(--dl-accent-strong);
|
font-size: 10px;
|
font-weight: 700;
|
}
|
|
.dl-device-meta {
|
margin-top: 8px;
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
font-size: 11px;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-date-panel .dl-panel-body,
|
.dl-device-panel .dl-panel-body {
|
display: flex;
|
flex-direction: column;
|
flex: 1;
|
min-height: 0;
|
height: auto;
|
}
|
|
.dl-device-summary .dl-panel-head {
|
padding: 12px 16px 8px;
|
}
|
|
.dl-device-summary .dl-panel-desc {
|
margin-top: 3px;
|
}
|
|
.dl-device-summary .dl-panel-body,
|
.dl-timeline-panel .dl-panel-body {
|
padding: 12px 16px 14px;
|
}
|
|
.dl-timeline-panel .dl-panel-head {
|
padding: 8px 14px 4px;
|
}
|
|
.dl-timeline-panel .dl-panel-desc {
|
display: none;
|
}
|
|
.dl-timeline-panel .dl-panel-body {
|
padding: 8px 14px 10px;
|
}
|
|
.dl-timeline-panel .dl-btn {
|
height: 30px;
|
padding: 0 12px;
|
border-radius: 10px;
|
}
|
|
.dl-raw-panel .dl-panel-body {
|
display: flex;
|
flex-direction: column;
|
flex: 1;
|
min-height: 0;
|
height: auto;
|
}
|
|
.dl-summary-toolbar {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 10px;
|
margin-bottom: 10px;
|
}
|
|
.dl-device-summary .dl-btn {
|
height: 30px;
|
padding: 0 12px;
|
border-radius: 10px;
|
}
|
|
.dl-summary-grid {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 6px;
|
}
|
|
.dl-summary-cell {
|
padding: 9px 10px;
|
border-radius: 12px;
|
border: 1px solid rgba(224, 232, 239, 0.96);
|
background: rgba(255, 255, 255, 0.78);
|
}
|
|
.dl-summary-cell-label {
|
font-size: 10px;
|
line-height: 1.2;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-summary-cell-value {
|
margin-top: 4px;
|
font-size: 12px;
|
font-weight: 700;
|
color: var(--dl-text-main);
|
line-height: 1.35;
|
word-break: normal;
|
overflow-wrap: anywhere;
|
}
|
|
.dl-summary-cell-sub {
|
margin-top: 2px;
|
font-size: 11px;
|
line-height: 1.3;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-summary-cell.is-range,
|
.dl-summary-cell.is-current {
|
grid-column: 1 / -1;
|
}
|
|
.dl-summary-cell.is-range .dl-summary-cell-value,
|
.dl-summary-cell.is-range .dl-summary-cell-sub {
|
font-family: monospace;
|
}
|
|
.dl-inline-actions {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
|
.dl-btn {
|
height: 34px;
|
padding: 0 14px;
|
border-radius: 12px;
|
border: none;
|
background: var(--dl-accent);
|
color: #fff;
|
font-size: 12px;
|
font-weight: 700;
|
cursor: pointer;
|
transition: transform .16s ease, box-shadow .16s ease;
|
box-shadow: 0 10px 18px rgba(111, 149, 189, 0.16);
|
}
|
|
.dl-btn:hover {
|
transform: translateY(-1px);
|
}
|
|
.dl-btn.is-ghost {
|
border: 1px solid rgba(219, 228, 236, 0.96);
|
background: rgba(255, 255, 255, 0.84);
|
color: #4b667f;
|
box-shadow: none;
|
}
|
|
.dl-btn[disabled] {
|
opacity: .5;
|
cursor: not-allowed;
|
transform: none;
|
box-shadow: none;
|
}
|
|
.dl-log-workspace {
|
flex: 1;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
}
|
|
.dl-detail-panel {
|
flex: 1;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.dl-detail-toolbar {
|
padding: 14px 16px 10px;
|
border-bottom: 1px solid rgba(228, 236, 243, 0.92);
|
display: flex;
|
align-items: flex-start;
|
justify-content: space-between;
|
gap: 10px;
|
}
|
|
.dl-detail-tabs {
|
display: inline-flex;
|
gap: 8px;
|
flex-wrap: wrap;
|
justify-content: flex-end;
|
}
|
|
.dl-detail-body {
|
flex: 1;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.dl-log-status {
|
display: inline-flex;
|
align-items: center;
|
height: 24px;
|
padding: 0 9px;
|
border-radius: 999px;
|
font-size: 11px;
|
font-weight: 700;
|
margin-right: 8px;
|
}
|
|
.dl-tone-success { background: rgba(82, 177, 126, 0.12); color: #2d7650; }
|
.dl-tone-working { background: rgba(111, 149, 189, 0.12); color: #3f6286; }
|
.dl-tone-warning { background: rgba(214, 162, 94, 0.14); color: #9b6a24; }
|
.dl-tone-danger { background: rgba(201, 102, 96, 0.14); color: #a74d47; }
|
.dl-tone-muted { background: rgba(148, 163, 184, 0.14); color: #748397; }
|
|
.dl-log-list {
|
flex: 1;
|
min-height: 0;
|
overflow: auto;
|
padding: 12px 16px 16px;
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
scrollbar-gutter: stable;
|
}
|
|
.dl-raw-content {
|
flex: 1;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
padding: 12px 16px 16px;
|
box-sizing: border-box;
|
}
|
|
.dl-log-row {
|
padding: 10px 12px;
|
border-radius: 14px;
|
border: 1px solid rgba(223, 232, 240, 0.94);
|
background: rgba(255, 255, 255, 0.72);
|
cursor: pointer;
|
transition: all .16s ease;
|
}
|
|
.dl-log-row.is-active {
|
border-color: rgba(111, 149, 189, 0.5);
|
background: rgba(111, 149, 189, 0.12);
|
box-shadow: 0 12px 22px rgba(111, 149, 189, 0.12);
|
}
|
|
.dl-log-row-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 8px;
|
}
|
|
.dl-log-time {
|
font-size: 12px;
|
font-weight: 700;
|
color: var(--dl-accent-strong);
|
font-family: monospace;
|
}
|
|
.dl-log-title {
|
margin-top: 6px;
|
font-size: 12px;
|
font-weight: 700;
|
color: var(--dl-text-main);
|
line-height: 1.35;
|
}
|
|
.dl-log-meta-line {
|
margin-top: 4px;
|
font-size: 11px;
|
line-height: 1.4;
|
color: var(--dl-text-sub);
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
}
|
|
.dl-log-hint {
|
margin-top: 5px;
|
display: inline-flex;
|
align-items: center;
|
font-size: 11px;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-picker-main {
|
flex: 1;
|
min-width: 0;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.dl-picker-panel {
|
flex: 1;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.dl-picker-panel .dl-panel-body {
|
display: flex;
|
flex-direction: column;
|
min-height: 0;
|
height: auto;
|
}
|
|
.dl-picker-toolbar {
|
display: grid;
|
grid-template-columns: minmax(0, 1fr) 280px;
|
gap: 12px;
|
align-items: start;
|
margin-bottom: 12px;
|
}
|
|
.dl-picker-side-tools {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
|
.dl-picker-current-day {
|
padding: 10px 12px;
|
border-radius: 14px;
|
border: 1px solid rgba(223, 232, 240, 0.94);
|
background: rgba(255, 255, 255, 0.72);
|
font-size: 12px;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-picker-current-day strong {
|
display: block;
|
margin-top: 3px;
|
font-size: 13px;
|
color: var(--dl-text-main);
|
}
|
|
.dl-picker-device-grid {
|
flex: 1;
|
min-height: 0;
|
overflow: auto;
|
display: grid;
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
gap: 12px;
|
padding-right: 4px;
|
scrollbar-gutter: stable;
|
}
|
|
.dl-picker-device-grid .dl-device-item {
|
min-height: 112px;
|
text-align: left;
|
}
|
|
.dl-picker-span-all {
|
grid-column: 1 / -1;
|
}
|
|
.dl-viewer-shell {
|
flex: 1;
|
min-width: 0;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
}
|
|
.dl-viewer-header .dl-panel-body {
|
height: auto;
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
padding: 12px 16px;
|
}
|
|
.dl-viewer-header-main {
|
min-width: 0;
|
display: flex;
|
align-items: flex-start;
|
gap: 12px;
|
}
|
|
.dl-viewer-copy {
|
min-width: 0;
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
}
|
|
.dl-viewer-meta {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
}
|
|
.dl-viewer-meta-item {
|
display: inline-flex;
|
align-items: center;
|
gap: 6px;
|
padding: 5px 10px;
|
border-radius: 999px;
|
border: 1px solid rgba(223, 232, 240, 0.94);
|
background: rgba(255, 255, 255, 0.72);
|
font-size: 11px;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-viewer-meta-item strong {
|
color: var(--dl-text-main);
|
font-weight: 700;
|
}
|
|
.dl-viewer-grid {
|
flex: 1;
|
min-height: 0;
|
display: grid;
|
grid-template-columns: minmax(620px, 1.28fr) minmax(360px, 0.92fr);
|
gap: 12px;
|
}
|
|
.dl-viewer-main,
|
.dl-viewer-side {
|
min-width: 0;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
|
.dl-viewer-side .dl-timeline-panel {
|
flex: 0 0 auto;
|
}
|
|
.dl-raw-toolbar {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
margin-bottom: 12px;
|
}
|
|
.dl-raw-meta {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px 12px;
|
font-size: 12px;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-raw-tabs {
|
display: inline-flex;
|
gap: 8px;
|
}
|
|
.dl-tab-btn {
|
height: 30px;
|
padding: 0 12px;
|
border-radius: 999px;
|
border: 1px solid rgba(219, 228, 236, 0.96);
|
background: rgba(255, 255, 255, 0.84);
|
color: #4b667f;
|
font-size: 12px;
|
font-weight: 700;
|
cursor: pointer;
|
transition: all .16s ease;
|
}
|
|
.dl-tab-btn.is-active {
|
border-color: rgba(111, 149, 189, 0.44);
|
background: rgba(111, 149, 189, 0.14);
|
color: var(--dl-accent-strong);
|
}
|
|
.dl-json-box {
|
flex: 1;
|
min-height: 0;
|
overflow: auto;
|
padding: 14px;
|
border-radius: 16px;
|
border: 1px solid rgba(223, 232, 240, 0.94);
|
background: rgba(244, 248, 252, 0.88);
|
scrollbar-gutter: stable;
|
}
|
|
.dl-json-pre {
|
margin: 0;
|
font-size: 12px;
|
line-height: 1.55;
|
color: #2b425a;
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
white-space: pre-wrap;
|
word-break: break-word;
|
}
|
|
.dl-json-note {
|
margin-top: 8px;
|
font-size: 11px;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-timeline-toolbar {
|
display: grid;
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
align-items: center;
|
gap: 8px 10px;
|
margin-bottom: 6px;
|
}
|
|
.dl-timeline-range {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
min-width: 0;
|
}
|
|
.dl-timeline-range .el-slider {
|
flex: 1 1 220px;
|
width: 100%;
|
min-width: 180px;
|
}
|
|
.dl-time-readout {
|
min-width: 196px;
|
font-size: 13px;
|
font-weight: 700;
|
color: var(--dl-text-main);
|
font-family: monospace;
|
text-align: right;
|
flex-shrink: 0;
|
}
|
|
.dl-timeline-side {
|
display: inline-flex;
|
align-items: center;
|
justify-content: flex-end;
|
gap: 8px;
|
min-width: 0;
|
}
|
|
.dl-range-meta {
|
margin-top: 4px;
|
display: flex;
|
flex-wrap: wrap;
|
gap: 4px 10px;
|
font-size: 11px;
|
color: var(--dl-text-sub);
|
}
|
|
.dl-visual-card {
|
flex: 1;
|
min-height: 0;
|
}
|
|
.dl-visual-card .dl-panel-head {
|
padding: 12px 16px 8px;
|
}
|
|
.dl-visual-card .dl-panel-body {
|
height: 100%;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
overflow: hidden;
|
padding: 10px 12px 12px;
|
}
|
|
.dl-visual-card .mc-toolbar {
|
margin-bottom: 6px;
|
}
|
|
.dl-visual-card .mc-title {
|
font-size: 13px;
|
}
|
|
.dl-visual-card .mc-head {
|
padding: 8px 10px;
|
}
|
|
.dl-visual-card .mc-head-title {
|
font-size: 12px;
|
}
|
|
.dl-visual-card .mc-head-subtitle {
|
display: none;
|
}
|
|
.dl-visual-card .mc-body {
|
padding: 0 10px 8px;
|
}
|
|
.dl-visual-card .mc-detail-grid {
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
gap: 5px;
|
margin-top: 6px;
|
}
|
|
.dl-visual-card .mc-detail-cell {
|
padding: 7px 8px;
|
border-radius: 9px;
|
}
|
|
.dl-visual-card .mc-detail-label {
|
font-size: 10px;
|
}
|
|
.dl-visual-card .mc-detail-value {
|
margin-top: 2px;
|
font-size: 10px;
|
line-height: 1.25;
|
}
|
|
.dl-visual-card .mc-footer {
|
margin-top: 6px;
|
}
|
|
.dl-tree-wrap,
|
.dl-device-list,
|
.dl-log-list,
|
.dl-json-box,
|
.dl-visual-card .mc-collapse {
|
scrollbar-width: auto;
|
scrollbar-color: rgba(111, 149, 189, 0.9) rgba(225, 233, 241, 0.72);
|
}
|
|
.dl-visual-card .mc-collapse {
|
padding-right: 6px;
|
scrollbar-gutter: stable;
|
}
|
|
.dl-tree-wrap::-webkit-scrollbar,
|
.dl-device-list::-webkit-scrollbar,
|
.dl-log-list::-webkit-scrollbar,
|
.dl-json-box::-webkit-scrollbar,
|
.dl-visual-card .mc-collapse::-webkit-scrollbar {
|
width: 12px;
|
height: 12px;
|
}
|
|
.dl-tree-wrap::-webkit-scrollbar-track,
|
.dl-device-list::-webkit-scrollbar-track,
|
.dl-log-list::-webkit-scrollbar-track,
|
.dl-json-box::-webkit-scrollbar-track,
|
.dl-visual-card .mc-collapse::-webkit-scrollbar-track {
|
background: rgba(225, 233, 241, 0.78);
|
border-radius: 999px;
|
border: 2px solid rgba(248, 251, 253, 0.96);
|
}
|
|
.dl-tree-wrap::-webkit-scrollbar-thumb,
|
.dl-device-list::-webkit-scrollbar-thumb,
|
.dl-log-list::-webkit-scrollbar-thumb,
|
.dl-json-box::-webkit-scrollbar-thumb,
|
.dl-visual-card .mc-collapse::-webkit-scrollbar-thumb {
|
background: linear-gradient(180deg, rgba(111, 149, 189, 0.96) 0%, rgba(85, 124, 167, 0.96) 100%);
|
border-radius: 999px;
|
border: 2px solid rgba(248, 251, 253, 0.96);
|
}
|
|
.dl-tree-wrap::-webkit-scrollbar-thumb:hover,
|
.dl-device-list::-webkit-scrollbar-thumb:hover,
|
.dl-log-list::-webkit-scrollbar-thumb:hover,
|
.dl-json-box::-webkit-scrollbar-thumb:hover,
|
.dl-visual-card .mc-collapse::-webkit-scrollbar-thumb:hover {
|
background: linear-gradient(180deg, rgba(98, 136, 177, 0.98) 0%, rgba(74, 112, 154, 0.98) 100%);
|
}
|
|
.dl-empty,
|
.dl-loading {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
height: 100%;
|
min-height: 160px;
|
color: var(--dl-text-sub);
|
font-size: 13px;
|
text-align: center;
|
padding: 0 20px;
|
box-sizing: border-box;
|
}
|
|
.dl-loading i {
|
font-size: 22px;
|
margin-right: 8px;
|
}
|
|
@media (max-width: 1440px) {
|
.dl-workbench {
|
grid-template-columns: minmax(460px, 1.18fr) minmax(320px, 0.92fr);
|
}
|
|
.dl-picker-toolbar {
|
grid-template-columns: 1fr;
|
}
|
|
.dl-viewer-grid {
|
grid-template-columns: minmax(520px, 1.15fr) minmax(320px, 0.92fr);
|
}
|
}
|
|
@media (max-width: 1180px) {
|
.dl-workbench {
|
grid-template-columns: 1fr;
|
}
|
|
.dl-center,
|
.dl-visual {
|
order: initial;
|
}
|
|
.dl-summary-grid,
|
.dl-visual-card .mc-detail-grid {
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
}
|
|
.dl-viewer-grid {
|
grid-template-columns: 1fr;
|
}
|
}
|
|
@media (max-width: 1560px) {
|
.dl-timeline-toolbar {
|
grid-template-columns: auto auto;
|
align-items: center;
|
justify-content: space-between;
|
}
|
|
.dl-timeline-range {
|
grid-column: 1 / -1;
|
width: 100%;
|
}
|
|
.dl-timeline-side {
|
justify-content: flex-end;
|
}
|
}
|
|
@media (max-width: 1320px) {
|
.dl-timeline-range {
|
flex-wrap: wrap;
|
}
|
|
.dl-time-readout {
|
min-width: 0;
|
width: 100%;
|
text-align: left;
|
}
|
}
|
</style>
|
</head>
|
<body>
|
<div id="app" v-cloak>
|
<div class="dl-shell">
|
<template v-if="viewMode === 'picker'">
|
<aside class="dl-sidebar">
|
<section class="dl-panel dl-hero">
|
<div class="dl-hero-title">设备日志工作台</div>
|
<div class="dl-hero-desc">先选择日志日期和设备,再进入独立的数据查看页。筛选入口和查看态拆开,避免设备列表被数据看板挤压。</div>
|
<div class="dl-hero-stats">
|
<div class="dl-stat">
|
<div class="dl-stat-value">{{ summaryStats.totalDevices || 0 }}</div>
|
<div class="dl-stat-label">设备数</div>
|
</div>
|
<div class="dl-stat">
|
<div class="dl-stat-value">{{ summaryStats.totalFiles || 0 }}</div>
|
<div class="dl-stat-label">日志文件</div>
|
</div>
|
<div class="dl-stat">
|
<div class="dl-stat-value">{{ recentDays.length }}</div>
|
<div class="dl-stat-label">最近日期</div>
|
</div>
|
</div>
|
</section>
|
|
<section class="dl-panel dl-date-panel">
|
<div class="dl-panel-head">
|
<div class="dl-panel-title">日期导航</div>
|
<div class="dl-panel-desc">先选日志日期,再进入设备选择页。</div>
|
</div>
|
<div class="dl-panel-body">
|
<div class="dl-quick-days">
|
<button
|
v-for="day in recentDays"
|
:key="day.day"
|
type="button"
|
class="dl-day-chip"
|
:class="{ 'is-active': selectedDay === day.day }"
|
@click="handleRecentDayClick(day.day)"
|
>{{ day.label }}</button>
|
</div>
|
<div class="dl-tree-wrap">
|
<el-tree
|
ref="dateTree"
|
:data="dateTreeData"
|
:props="defaultProps"
|
node-key="id"
|
:default-expanded-keys="defaultExpandedKeys"
|
highlight-current
|
accordion
|
@node-click="handleNodeClick">
|
<span slot-scope="{ node, data }">
|
<i v-if="data.children" class="el-icon-folder"></i>
|
<i v-else class="el-icon-date"></i>
|
<span style="margin-left: 6px;">{{ node.label }}</span>
|
</span>
|
</el-tree>
|
</div>
|
</div>
|
</section>
|
</aside>
|
|
<section class="dl-picker-main">
|
<section class="dl-panel dl-picker-panel">
|
<div class="dl-panel-head">
|
<div class="dl-panel-title">设备选择</div>
|
<div class="dl-panel-desc">{{ selectedDay ? ('日志日期 ' + formatDayText(selectedDay) + ',先筛选设备类型,再点击一台设备进入状态查看页。') : '先从左侧选择一个日志日期。' }}</div>
|
</div>
|
<div class="dl-panel-body">
|
<div class="dl-picker-toolbar">
|
<div class="dl-type-tabs">
|
<button
|
v-for="group in deviceGroups"
|
:key="group.type"
|
type="button"
|
class="dl-type-tab"
|
:class="{ 'is-active': activeType === group.type }"
|
@click="selectTypeGroup(group.type)">
|
<div class="dl-type-tab-label">{{ group.typeLabel }}</div>
|
<div class="dl-type-tab-meta">{{ group.deviceCount }} 台 / {{ group.totalFiles }} 文件</div>
|
</button>
|
</div>
|
<div class="dl-picker-side-tools">
|
<div class="dl-picker-current-day">
|
当前日期
|
<strong>{{ selectedDay ? formatDayText(selectedDay) : '未选择' }}</strong>
|
</div>
|
<div class="dl-device-search" style="margin-bottom: 0;">
|
<el-input
|
v-model.trim="searchDeviceNo"
|
size="small"
|
clearable
|
placeholder="按设备编号筛选">
|
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
</el-input>
|
</div>
|
</div>
|
</div>
|
<div class="dl-picker-device-grid">
|
<div v-if="summaryLoading" class="dl-loading dl-picker-span-all">
|
<i class="el-icon-loading"></i><span>正在加载设备摘要...</span>
|
</div>
|
<div v-else-if="!selectedDay" class="dl-empty dl-picker-span-all">先从左侧选择一个日期。</div>
|
<div v-else-if="filteredDevices.length === 0" class="dl-empty dl-picker-span-all">当前分组下没有匹配的设备。</div>
|
<button
|
v-else
|
v-for="device in filteredDevices"
|
:key="buildDeviceKey(device.type, device.deviceNo)"
|
type="button"
|
class="dl-device-item"
|
@click="selectDevice(device)">
|
<div class="dl-device-name">
|
<span>{{ device.deviceNo }} 号{{ device.typeLabel }}</span>
|
<span class="dl-device-badge">{{ device.fileCount }} 文件</span>
|
</div>
|
<div class="dl-device-meta">
|
<span>首条: {{ formatTimestamp(device.firstTime, false) }}</span>
|
<span>末条: {{ formatTimestamp(device.lastTime, false) }}</span>
|
</div>
|
</button>
|
</div>
|
</div>
|
</section>
|
</section>
|
</template>
|
|
<section v-else class="dl-viewer-shell">
|
<section class="dl-panel dl-viewer-header">
|
<div class="dl-panel-body">
|
<div class="dl-viewer-header-main">
|
<button type="button" class="dl-btn is-ghost" @click="returnToSelector">返回筛选</button>
|
<div class="dl-viewer-copy">
|
<div class="dl-panel-title">{{ selectedDeviceSummary ? (selectedDeviceSummary.typeLabel + ' ' + selectedDeviceSummary.deviceNo + '号') : '设备状态查看' }}</div>
|
<div class="dl-panel-desc">{{ selectedDay ? ('日志日期 ' + formatDayText(selectedDay)) : '请选择日期和设备' }}</div>
|
<div v-if="selectedDeviceSummary" class="dl-viewer-meta">
|
<span class="dl-viewer-meta-item">类型 <strong>{{ selectedDeviceSummary.typeLabel }}</strong></span>
|
<span class="dl-viewer-meta-item">文件 <strong>{{ selectedDeviceSummary.fileCount }}</strong></span>
|
<span class="dl-viewer-meta-item">已载 <strong>{{ loadedSegmentCount }}</strong></span>
|
<span class="dl-viewer-meta-item">范围 <strong>{{ timelineRangeText }}</strong></span>
|
</div>
|
</div>
|
</div>
|
<div class="dl-inline-actions">
|
<span class="dl-log-status" :class="'dl-tone-' + currentStatusTone">{{ currentStatusLabel }}</span>
|
<button type="button" class="dl-btn is-ghost" @click="loadPreviousSegment" :disabled="!canLoadPreviousSegment">加载更早片段</button>
|
<button type="button" class="dl-btn is-ghost" @click="loadNextSegment" :disabled="!canLoadNextSegment">加载更新片段</button>
|
<button type="button" class="dl-btn" @click="handleCurrentDeviceDownload" :disabled="!canDownload">下载当日日志</button>
|
</div>
|
</div>
|
</section>
|
|
<div class="dl-viewer-grid">
|
<section class="dl-viewer-main">
|
<section class="dl-panel dl-visual-card">
|
<div class="dl-panel-head">
|
<div class="dl-panel-title">状态可视化</div>
|
<div class="dl-panel-desc">{{ selectedLogRow ? selectedLogRow._summary.detail : '拖动时间轴或点击日志后,在这里查看该时刻的设备状态。' }}</div>
|
</div>
|
<div class="dl-panel-body">
|
<component
|
v-if="visualComponentName"
|
:is="visualComponentName"
|
:key="activeDeviceKey"
|
:items="visualItems"
|
:param="visualParam"
|
:auto-refresh="false"
|
:read-only="true"></component>
|
<div v-else class="dl-empty">先选择一个设备,并加载至少一条日志记录。</div>
|
</div>
|
</section>
|
</section>
|
|
<section class="dl-viewer-side">
|
<section class="dl-panel dl-timeline-panel">
|
<div class="dl-panel-head">
|
<div class="dl-panel-title">时间轴与回放</div>
|
<div class="dl-panel-desc">拖动时间轴或跳转时间点查看该设备的状态变化。</div>
|
</div>
|
<div class="dl-panel-body">
|
<div class="dl-timeline-toolbar">
|
<div class="dl-inline-actions">
|
<button type="button" class="dl-btn" v-if="!isPlaying" @click="play" :disabled="!canPlay">播放</button>
|
<button type="button" class="dl-btn" v-else @click="pause">暂停</button>
|
<button type="button" class="dl-btn is-ghost" @click="resetPlayback" :disabled="!selectedDeviceSummary">重置</button>
|
</div>
|
<div class="dl-timeline-range">
|
<el-slider
|
:value="sliderValue"
|
:max="sliderMax"
|
:disabled="!selectedDeviceSummary || sliderMax <= 0"
|
@input="handleSliderInput"
|
@change="handleSliderChange"
|
:format-tooltip="formatTooltip"></el-slider>
|
<div class="dl-time-readout">{{ currentTimeStr }}</div>
|
</div>
|
<div class="dl-timeline-side">
|
<el-popover
|
placement="bottom"
|
width="220"
|
trigger="click"
|
v-model="jumpVisible"
|
@show="initJumpTime">
|
<div style="display: flex; flex-direction: column; gap: 10px;">
|
<el-time-picker
|
v-model="jumpTime"
|
size="small"
|
placeholder="选择时间"
|
style="width: 100%;"
|
:picker-options="{ selectableRange: '00:00:00 - 23:59:59' }">
|
</el-time-picker>
|
<button type="button" class="dl-btn" style="width: 100%;" @click="confirmJump">跳转</button>
|
</div>
|
<button slot="reference" type="button" class="dl-btn is-ghost" :disabled="!selectedDeviceSummary">跳转</button>
|
</el-popover>
|
<el-select v-model="playbackSpeed" size="small" style="width: 92px;" :disabled="!selectedDeviceSummary">
|
<el-option :value="1" label="1x"></el-option>
|
<el-option :value="5" label="5x"></el-option>
|
<el-option :value="10" label="10x"></el-option>
|
<el-option :value="50" label="50x"></el-option>
|
<el-option :value="100" label="100x"></el-option>
|
<el-option :value="200" label="200x"></el-option>
|
</el-select>
|
</div>
|
</div>
|
<div class="dl-range-meta">
|
<span>完整范围: {{ timelineRangeText }}</span>
|
<span>已加载片段: {{ loadedSegmentCount }} / {{ timelineMeta.totalFiles || 0 }}</span>
|
<span v-if="selectedLogRow">当前记录: {{ selectedLogRow._summary.title }}</span>
|
</div>
|
</div>
|
</section>
|
|
<section class="dl-panel dl-detail-panel">
|
<div class="dl-detail-toolbar">
|
<div>
|
<div class="dl-panel-title">{{ detailTab === 'raw' ? '原始数据' : '日志列表' }}</div>
|
<div v-if="detailTab === 'raw'" class="dl-panel-desc">{{ selectedLogRow ? '当前记录的 wcsData JSON 已格式化展示,必要时可切换查看原始文本。' : '先选择一条日志,再查看原始数据。' }}</div>
|
</div>
|
<div class="dl-detail-tabs">
|
<button type="button" class="dl-tab-btn" :class="{ 'is-active': detailTab === 'logs' }" @click="detailTab = 'logs'">日志列表</button>
|
<button type="button" class="dl-tab-btn" :class="{ 'is-active': detailTab === 'raw' }" @click="detailTab = 'raw'" :disabled="!selectedLogRow">原始数据</button>
|
</div>
|
</div>
|
<div class="dl-detail-body">
|
<div v-show="detailTab === 'logs'" class="dl-log-list">
|
<div v-if="timelineLoading || logLoading" class="dl-loading">
|
<i class="el-icon-loading"></i><span>{{ timelineLoading ? '正在构建设备时间轴...' : '正在加载日志片段...' }}</span>
|
</div>
|
<div v-else-if="!selectedDeviceSummary" class="dl-empty">先返回筛选页选择设备。</div>
|
<div v-else-if="logRows.length === 0" class="dl-empty">{{ logLoadError || '当前设备暂无可展示日志。' }}</div>
|
<div
|
v-else
|
v-for="row in logRows"
|
:key="row._key"
|
:id="'log-row-' + row._key"
|
class="dl-log-row"
|
:class="{ 'is-active': row._key === selectedLogKey }"
|
@click="handleLogRowClick(row)">
|
<div class="dl-log-row-head">
|
<span class="dl-log-time">{{ formatTimestamp(row._ts, true) }}</span>
|
<span class="dl-log-status" :class="'dl-tone-' + row._summary.tone">{{ row._summary.statusLabel }}</span>
|
</div>
|
<div class="dl-log-title">{{ row._summary.title }}</div>
|
<div class="dl-log-meta-line">{{ buildLogMetaLine(row._summary) }}</div>
|
</div>
|
</div>
|
<div v-show="detailTab === 'raw'" class="dl-raw-content">
|
<template v-if="selectedLogRow">
|
<div class="dl-raw-toolbar">
|
<div class="dl-raw-meta">
|
<span>{{ formatTimestamp(selectedLogRow._ts, true) }}</span>
|
<span>{{ currentLogTitle }}</span>
|
</div>
|
<div class="dl-raw-tabs">
|
<button type="button" class="dl-tab-btn" :class="{ 'is-active': rawTab === 'wcs' }" @click="rawTab = 'wcs'">WCS JSON</button>
|
<button type="button" class="dl-tab-btn" :class="{ 'is-active': rawTab === 'origin' }" @click="rawTab = 'origin'">originData</button>
|
</div>
|
</div>
|
<div class="dl-json-box">
|
<pre class="dl-json-pre">{{ activeRawText }}</pre>
|
</div>
|
<div class="dl-json-note">{{ activeRawHint }}</div>
|
</template>
|
<div v-else class="dl-empty">先在日志列表点击一条记录,再查看原始数据。</div>
|
</div>
|
</div>
|
</section>
|
</section>
|
</div>
|
</section>
|
</div>
|
|
<el-dialog :title="downloadDialogTitle" :visible.sync="downloadDialogVisible" width="400px" :close-on-click-modal="false" :show-close="false">
|
<div style="padding: 10px;">
|
<div style="margin-bottom: 5px; font-size: 14px;">压缩生成进度</div>
|
<el-progress :percentage="buildProgress" :text-inside="true" :stroke-width="18"></el-progress>
|
<div style="margin: 20px 0 5px; font-size: 14px;">下载接收进度</div>
|
<el-progress :percentage="receiveProgress" :text-inside="true" :stroke-width="18" status="success"></el-progress>
|
</div>
|
</el-dialog>
|
</div>
|
|
<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
|
<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
|
<script src="../../static/vue/js/vue.min.js"></script>
|
<script src="../../static/vue/element/element.js"></script>
|
<script src="../../components/MonitorCardKit.js?v=20260318_monitor_v2"></script>
|
<script src="../../components/WatchCrnCard.js?v=20260318_monitor_v2"></script>
|
<script src="../../components/WatchRgvCard.js?v=20260318_monitor_v2"></script>
|
<script src="../../components/WatchDualCrnCard.js?v=20260318_monitor_v2"></script>
|
<script src="../../components/DevpCard.js?v=20260318_monitor_v2"></script>
|
<script type="text/javascript" src="../../static/js/deviceLogs/deviceLogs.js?v=20260318_workbench_v9" charset="utf-8"></script>
|
</body>
|
</html>
|