| | |
| | | <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> |
| | |
| | | <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> |