#
Junjie
6 小时以前 fd82105a3dfe347c4c9acb0410c117d8d67c9339
src/main/webapp/views/deviceLogs/deviceLogs.html
@@ -6,192 +6,1346 @@
    <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">
    <!-- CSS -->
    <link rel="stylesheet" href="../../static/vue/element/element.css">
    <link rel="stylesheet" href="../../static/css/common.css">
    <style>
        body { margin: 0; padding: 0; background-color: #f0f2f5; height: 100vh; overflow: hidden; }
        #app { height: 100%; padding: 10px; box-sizing: border-box; display: flex; flex-direction: column; }
        .main-container { flex: 1; display: flex; overflow: hidden; }
        .sidebar { width: 260px; margin-right: 10px; display: flex; flex-direction: column; }
        .content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
        .box-card { height: 100%; display: flex; flex-direction: column; border: none; box-shadow: 0 1px 4px rgba(0,21,41,.08); }
        .box-card .el-card__header { padding: 10px 15px; border-bottom: 1px solid #ebeef5; background: #fff; font-weight: bold; font-size: 15px; }
        .box-card .el-card__body { flex: 1; overflow: auto; padding: 15px; }
        .device-item { margin-bottom: 10px; }
        .device-card { background-color: #fff; border: 1px solid #e6ebf5; border-radius: 4px; transition: all .3s; }
        .device-card:hover { box-shadow: 0 2px 12px 0 rgba(0,0,0,.1); }
        .device-info { display: flex; justify-content: space-between; align-items: center; padding: 15px; }
        .device-info .info-text { font-size: 14px; color: #606266; }
        .device-info .info-text b { color: #303133; margin-right: 5px; }
        .device-info .tag-group { margin-left: 15px; }
        .control-bar { margin-bottom: 15px; padding: 15px; background: #fff; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,21,41,.08); }
        /* Visualization styles */
        .vis-control-panel { margin-bottom: 10px; display: flex; align-items: center; background: #f5f7fa; padding: 10px; border-radius: 4px; }
        .vis-container { border: 1px solid #ebeef5; padding: 10px; border-radius: 4px; min-height: 400px; height: calc(80vh - 100px); overflow-y: auto; }
        :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="main-container">
        <!-- Sidebar: Date Tree -->
        <div class="sidebar">
            <el-card class="box-card" :body-style="{padding: '10px'}">
                <div slot="header">日期选择</div>
                <el-tree
                    ref="dateTree"
                    :data="dateTreeData"
                    :props="defaultProps"
                    node-key="id"
                    :default-expanded-keys="defaultExpandedKeys"
                    @node-click="handleNodeClick"
                    highlight-current
                    accordion>
                    <span class="custom-tree-node" slot-scope="{ node, data }">
                        <i v-if="data.children" class="el-icon-folder"></i>
                        <i v-else class="el-icon-document"></i>
                        <span style="margin-left: 5px;">{{ node.label }}</span>
                    </span>
                </el-tree>
            </el-card>
        </div>
    <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>
        <!-- Main Content -->
        <div class="content">
            <!-- Search Bar -->
            <div class="control-bar">
                <el-form :inline="true" :model="searchForm" size="small" style="margin-bottom: -18px;">
                    <el-form-item label="选中日期">
                        <el-input v-model="searchForm.day" placeholder="yyyyMMdd" readonly style="width: 120px;" disabled></el-input>
                    </el-form-item>
                    <el-form-item label="设备类型">
                        <el-select v-model="searchForm.type" placeholder="全部" clearable style="width: 100px;">
                            <el-option label="Crn" value="Crn"></el-option>
                            <el-option label="Devp" value="Devp"></el-option>
                            <el-option label="Rgv" value="Rgv"></el-option>
                        </el-select>
                    </el-form-item>
                    <el-form-item label="设备编号">
                        <el-input v-model="searchForm.deviceNo" placeholder="请输入编号" style="width: 120px;" clearable></el-input>
                    </el-form-item>
                    <el-form-item label="起始序号">
                        <el-input-number v-model="searchForm.offset" :min="0" controls-position="right" style="width: 100px;"></el-input-number>
                    </el-form-item>
                    <el-form-item label="最大文件">
                        <el-input-number v-model="searchForm.limit" :min="1" :max="1000" controls-position="right" style="width: 100px;"></el-input-number>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" icon="el-icon-download" @click="handleBatchDownload" :disabled="!canDownload">下载</el-button>
                    </el-form-item>
                </el-form>
            </div>
                <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>
            <!-- Device List -->
            <el-card class="box-card">
                <div slot="header" class="clearfix">
                    <span>设备列表</span>
                    <span style="float: right; color: #909399; font-size: 12px;">共 {{ filteredDeviceList.length }} 个设备</span>
                </div>
                <div v-if="loading" style="text-align: center; padding: 20px;">
                    <i class="el-icon-loading" style="font-size: 24px;"></i>
                </div>
                <div v-else-if="filteredDeviceList.length === 0" style="text-align: center; color: #909399; padding: 50px;">
                    <i class="el-icon-info" style="margin-right: 5px;"></i>暂无数据,请先选择日期
                </div>
                <div v-else>
                    <div v-for="(item, index) in filteredDeviceList" :key="index" class="device-item">
                        <div class="device-card">
                            <div class="device-info">
                                <div>
                                    <span class="info-text"><b>设备编号:</b> {{ item.deviceNo }}</span>
                                    <span class="info-text tag-group"><b>类型:</b> {{ item.types.join(', ') }}</span>
                                    <span class="info-text tag-group"><b>文件数:</b> {{ item.fileCount }}</span>
            <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>
                                    <template v-for="t in item.types">
                                        <el-button size="mini" icon="el-icon-download" @click="downloadLog(item.deviceNo, t)">下载({{t}})</el-button>
                                        <el-button size="mini" type="success" icon="el-icon-view" @click="visualizeLog(item.deviceNo, t)">可视化({{t}})</el-button>
                                    </template>
                                <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>
            </el-card>
        </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>
    <!-- Visualization Dialog -->
    <el-dialog
        :title="visualizationTitle"
        :visible.sync="visualizationVisible"
        width="90%"
        top="5vh"
        :close-on-click-modal="false"
        @close="handleVisualizationClose">
        <div class="vis-control-panel">
            <el-button-group>
                <el-button type="primary" icon="el-icon-video-play" @click="play" v-if="!isPlaying" size="small">播放</el-button>
                <el-button type="primary" icon="el-icon-video-pause" @click="pause" v-else size="small">暂停</el-button>
                <el-button type="warning" icon="el-icon-refresh-left" @click="reset" size="small">重置</el-button>
            </el-button-group>
            <div style="margin-left: 20px; flex: 1; padding-right: 20px;">
                <el-slider v-model="sliderValue" :max="maxSliderValue" @change="sliderChange" @input="sliderInput" :format-tooltip="formatTooltip"></el-slider>
            </div>
            <div style="width: 210px; font-size: 14px; font-weight: bold; font-family: monospace; display: flex; align-items: center;">
                {{ currentTimeStr }}
                <el-popover
                    placement="bottom"
                    width="200"
                    trigger="click"
                    v-model="jumpVisible"
                    @show="initJumpTime">
                    <div style="text-align: center;">
                        <el-time-picker
                            v-model="jumpTime"
                            size="small"
                            placeholder="选择时间"
                            style="width: 100%; margin-bottom: 10px;"
                            :picker-options="{ selectableRange: '00:00:00 - 23:59:59' }">
                        </el-time-picker>
                        <el-button type="primary" size="mini" @click="confirmJump" style="width: 100%;">跳转</el-button>
                    </div>
                    <el-button type="text" slot="reference" icon="el-icon-edit" style="margin-left: 5px; padding: 0;" title="跳转时间"></el-button>
                </el-popover>
            </div>
             <div style="margin-left: 10px;">
                <el-select v-model="playbackSpeed" style="width: 100px;" size="small" placeholder="倍速">
                    <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-option :value="500" label="500x"></el-option>
                    <el-option :value="1000" label="1000x"></el-option>
                </el-select>
            </div>
        </div>
        <div class="vis-container">
            <watch-crn-card v-if="visDeviceType === 'Crn'" ref="card" :auto-refresh="false" :read-only="true"></watch-crn-card>
            <watch-rgv-card v-else-if="visDeviceType === 'Rgv'" ref="card" :auto-refresh="false" :read-only="true"></watch-rgv-card>
            <watch-dual-crn-card v-else-if="visDeviceType === 'DualCrn'" ref="card" :auto-refresh="false" :read-only="true"></watch-dual-crn-card>
            <devp-card v-else-if="visDeviceType === 'Devp'" ref="card" :auto-refresh="false" :read-only="true"></devp-card>
            <div v-else style="text-align: center; padding: 50px; color: #909399;">
                未知设备类型: {{ visDeviceType }}
            </div>
        </div>
    </el-dialog>
    <!-- Download Progress Dialog -->
    <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>
@@ -206,11 +1360,11 @@
<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"></script>
<script src="../../components/WatchCrnCard.js"></script>
<script src="../../components/WatchRgvCard.js"></script>
<script src="../../components/WatchDualCrnCard.js"></script>
<script src="../../components/DevpCard.js"></script>
<script type="text/javascript" src="../../static/js/deviceLogs/deviceLogs.js?v=20260309_i18n_pagefix1" charset="utf-8"></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>