Compare commits
2 Commits
45ae1670c6
...
fe0d4304f8
| Author | SHA1 | Date | |
|---|---|---|---|
| fe0d4304f8 | |||
| d8c0885c84 |
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
<title>管廊巡检机器人综合管理平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -352,6 +352,18 @@
|
||||
content: "视";
|
||||
}
|
||||
|
||||
.evidence-photo {
|
||||
display: block;
|
||||
width: 88px;
|
||||
height: 52px;
|
||||
object-fit: cover;
|
||||
border: 1px solid rgba(87, 198, 255, 0.18);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(130, 232, 255, 0.08),
|
||||
0 8px 18px rgba(2, 10, 18, 0.24);
|
||||
background: rgba(8, 31, 62, 0.58);
|
||||
}
|
||||
|
||||
.table-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -637,7 +649,6 @@
|
||||
.tree-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
@@ -703,10 +714,45 @@
|
||||
|
||||
.tree-device-list {
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding-left: 10px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.tree-panel-footer {
|
||||
padding: 12px 10px 2px;
|
||||
border-top: 1px solid rgba(77, 188, 248, 0.12);
|
||||
}
|
||||
|
||||
.tree-playback-btn {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: 1px solid rgba(102, 235, 255, 0.28);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(20, 86, 152, 0.94) 0%,
|
||||
rgba(10, 42, 86, 0.98) 100%
|
||||
);
|
||||
color: #eefcff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 0 0 1px rgba(102, 235, 255, 0.08),
|
||||
0 0 18px rgba(29, 133, 222, 0.12);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.tree-playback-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(102, 235, 255, 0.54);
|
||||
box-shadow: inset 0 0 0 1px rgba(102, 235, 255, 0.12),
|
||||
0 0 24px rgba(52, 177, 255, 0.18);
|
||||
}
|
||||
|
||||
.tree-device-btn {
|
||||
@@ -762,7 +808,7 @@
|
||||
|
||||
.grid-switch-btn,
|
||||
.playback-btn,
|
||||
.playback-filter input {
|
||||
.playback-field input {
|
||||
height: 34px;
|
||||
border: 1px solid rgba(87, 198, 255, 0.18);
|
||||
background: rgba(8, 31, 62, 0.72);
|
||||
@@ -872,42 +918,103 @@
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.playback-panel {
|
||||
margin-top: 16px;
|
||||
padding: 14px 16px;
|
||||
background: rgba(8, 34, 68, 0.62);
|
||||
border: 1px solid rgba(77, 188, 248, 0.12);
|
||||
.playback-dialog-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 90;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(1, 8, 18, 0.74);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.playback-header,
|
||||
.playback-controls,
|
||||
.playback-filter {
|
||||
.playback-dialog {
|
||||
width: min(960px, calc(100vw - 80px));
|
||||
max-height: calc(100vh - 90px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
padding: 24px 24px 22px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(8, 30, 58, 0.98) 0%,
|
||||
rgba(4, 18, 36, 0.98) 100%
|
||||
);
|
||||
border: 1px solid rgba(89, 203, 255, 0.24);
|
||||
box-shadow: 0 24px 54px rgba(0, 0, 0, 0.34),
|
||||
inset 0 0 0 1px rgba(64, 180, 244, 0.08);
|
||||
}
|
||||
|
||||
.playback-dialog-header,
|
||||
.playback-dialog-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.playback-header {
|
||||
.playback-dialog-header {
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.playback-title {
|
||||
.playback-dialog-title {
|
||||
color: #f2fbff;
|
||||
font-size: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.playback-filter {
|
||||
.playback-dialog-meta {
|
||||
margin-top: 6px;
|
||||
color: rgba(166, 219, 245, 0.76);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.playback-dialog-filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) 136px;
|
||||
gap: 14px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.playback-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.playback-filter input {
|
||||
padding: 0 10px;
|
||||
.playback-field span {
|
||||
color: rgba(182, 232, 248, 0.78);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.playback-controls {
|
||||
.playback-field input {
|
||||
width: 100%;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.playback-launch-btn {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.playback-preview {
|
||||
position: relative;
|
||||
height: 420px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(82, 191, 255, 0.16);
|
||||
background: radial-gradient(circle at 50% 50%, rgba(34, 116, 190, 0.16), rgba(4, 16, 33, 0.96));
|
||||
}
|
||||
|
||||
.playback-preview video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.playback-dialog-controls {
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.playback-track {
|
||||
@@ -1366,9 +1473,12 @@
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 50% 50%, rgba(32, 112, 183, 0.12), transparent 45%),
|
||||
linear-gradient(180deg, rgba(5, 17, 35, 0.94) 0%, rgba(3, 12, 27, 0.96) 100%);
|
||||
radial-gradient(circle at 50% 58%, rgba(46, 163, 224, 0.12), transparent 34%),
|
||||
linear-gradient(180deg, rgba(6, 18, 31, 0.96) 0%, rgba(3, 10, 22, 0.98) 100%);
|
||||
border: 1px solid rgba(82, 191, 255, 0.16);
|
||||
box-shadow:
|
||||
inset 0 0 38px rgba(24, 92, 140, 0.18),
|
||||
inset 0 -46px 72px rgba(0, 0, 0, 0.34);
|
||||
}
|
||||
|
||||
.nav-map-stage::before {
|
||||
@@ -1376,10 +1486,24 @@
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
opacity: 0.5;
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.015) 0, transparent 36%),
|
||||
linear-gradient(rgba(102, 208, 255, 0.028) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(102, 208, 255, 0.022) 1px, transparent 1px);
|
||||
background-size:
|
||||
100% 100%,
|
||||
46px 46px,
|
||||
46px 46px;
|
||||
opacity: 0.42;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-map-stage::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(2, 8, 18, 0.62), transparent 18%, transparent 82%, rgba(2, 8, 18, 0.62)),
|
||||
linear-gradient(180deg, rgba(2, 8, 18, 0.3), transparent 20%, transparent 80%, rgba(2, 8, 18, 0.54));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -1393,35 +1517,60 @@
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.cloud-layer circle {
|
||||
fill: rgba(140, 230, 255, 0.74);
|
||||
.corridor-lane {
|
||||
fill: rgba(14, 48, 76, 0.46);
|
||||
stroke: rgba(104, 222, 255, 0.08);
|
||||
stroke-width: 1.2;
|
||||
filter: drop-shadow(0 0 16px rgba(54, 163, 235, 0.2));
|
||||
}
|
||||
|
||||
.corridor-edge {
|
||||
fill: none;
|
||||
stroke: rgba(133, 224, 255, 0.36);
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 4 8;
|
||||
stroke-linecap: round;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.wall-layer circle {
|
||||
fill: rgba(153, 241, 255, 0.82);
|
||||
opacity: 0.88;
|
||||
filter: drop-shadow(0 0 4px rgba(82, 198, 255, 0.28));
|
||||
filter: drop-shadow(0 0 8px rgba(86, 214, 255, 0.24));
|
||||
}
|
||||
|
||||
.wall-upper circle {
|
||||
fill: rgba(151, 234, 255, 0.76);
|
||||
}
|
||||
|
||||
.wall-lower circle {
|
||||
fill: rgba(110, 216, 255, 0.64);
|
||||
}
|
||||
|
||||
.route-glow {
|
||||
fill: none;
|
||||
stroke: rgba(88, 219, 255, 0.18);
|
||||
stroke-width: 18;
|
||||
stroke: rgba(88, 219, 255, 0.16);
|
||||
stroke-width: 12;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.route-line {
|
||||
fill: none;
|
||||
stroke: #67e3ff;
|
||||
stroke-width: 5;
|
||||
stroke: #76efff;
|
||||
stroke-width: 4;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-dasharray: 12 12;
|
||||
stroke-dasharray: 10 8;
|
||||
animation: routeFlow 12s linear infinite;
|
||||
filter: drop-shadow(0 0 10px rgba(70, 212, 255, 0.32));
|
||||
filter: drop-shadow(0 0 10px rgba(70, 212, 255, 0.26));
|
||||
}
|
||||
|
||||
.checkpoint circle {
|
||||
fill: rgba(6, 22, 51, 0.95);
|
||||
stroke: #7ae5ff;
|
||||
fill: rgba(5, 18, 42, 0.96);
|
||||
stroke: #8becff;
|
||||
stroke-width: 2;
|
||||
filter: drop-shadow(0 0 12px rgba(97, 220, 255, 0.24));
|
||||
}
|
||||
|
||||
.checkpoint text {
|
||||
@@ -1451,6 +1600,7 @@
|
||||
color: rgba(183, 234, 255, 0.82);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.7px;
|
||||
text-shadow: 0 0 8px rgba(56, 190, 248, 0.22);
|
||||
}
|
||||
|
||||
.nav-map-overlay.top-left {
|
||||
@@ -1808,27 +1958,73 @@
|
||||
rgba(8, 31, 68, 0.92) 0%,
|
||||
rgba(10, 49, 92, 0.58) 100%
|
||||
);
|
||||
border-left: 3px solid rgba(92, 196, 255, 0.76);
|
||||
box-shadow: inset 0 0 0 1px rgba(65, 174, 243, 0.08);
|
||||
border: 1px solid rgba(78, 194, 255, 0.14);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(65, 174, 243, 0.08),
|
||||
0 12px 22px rgba(3, 14, 28, 0.22);
|
||||
}
|
||||
|
||||
.alert-main {
|
||||
.alert-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.alert-thumbnail {
|
||||
width: 124px;
|
||||
height: 78px;
|
||||
flex: 0 0 124px;
|
||||
object-fit: cover;
|
||||
border: 1px solid rgba(116, 219, 255, 0.24);
|
||||
box-shadow: 0 0 0 1px rgba(78, 194, 255, 0.08);
|
||||
background: rgba(6, 22, 39, 0.72);
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.alert-summary {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(92px, auto);
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.alert-field {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.alert-field-right {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.alert-label {
|
||||
color: rgba(146, 213, 238, 0.72);
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.alert-title {
|
||||
color: #f0fbff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.alert-level {
|
||||
min-width: 46px;
|
||||
padding: 3px 8px;
|
||||
min-width: 48px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
@@ -1853,12 +2049,17 @@
|
||||
border: 1px solid rgba(110, 255, 197, 0.2);
|
||||
}
|
||||
|
||||
.alert-meta {
|
||||
.alert-time-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: rgba(171, 223, 246, 0.8);
|
||||
font-size: 11px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.alert-time {
|
||||
color: rgba(190, 234, 249, 0.84);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.alert-empty {
|
||||
@@ -1872,8 +2073,12 @@
|
||||
}
|
||||
|
||||
.video-card {
|
||||
> .card-header .title {
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
.video-panel {
|
||||
margin: 12px 24px 0;
|
||||
margin: 12px 4px 0 20px;
|
||||
height: calc(100% - 56px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1984,8 +2189,12 @@
|
||||
}
|
||||
|
||||
.control-card {
|
||||
> .card-header .title {
|
||||
margin-left: 80px;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
margin: 12px 24px 0;
|
||||
margin: 12px 4px 0 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
@@ -2032,7 +2241,7 @@
|
||||
|
||||
.control-top-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, calc(50% - 10px)) minmax(0, calc(50% + 10px));
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
}
|
||||
@@ -2064,6 +2273,45 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.drive-mode-tabs {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin: 2px 0 10px;
|
||||
}
|
||||
|
||||
.drive-mode-tab {
|
||||
height: 34px;
|
||||
border: 1px solid rgba(87, 198, 255, 0.18);
|
||||
background: rgba(8, 31, 62, 0.72);
|
||||
color: rgba(192, 235, 250, 0.78);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(102, 235, 255, 0.38);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #eefcff;
|
||||
border-color: rgba(102, 235, 255, 0.7);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(24, 102, 180, 0.88),
|
||||
rgba(35, 158, 215, 0.88)
|
||||
);
|
||||
box-shadow: 0 0 14px rgba(52, 177, 255, 0.18);
|
||||
}
|
||||
}
|
||||
|
||||
.vehicle-pad {
|
||||
width: 208px;
|
||||
height: 208px;
|
||||
@@ -2310,6 +2558,63 @@
|
||||
transform: scale(0.88);
|
||||
}
|
||||
|
||||
.gimbal-preset-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 2px 0 10px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.gimbal-preset-group {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.gimbal-preset-select {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(91, 208, 255, 0.22);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(10, 36, 72, 0.9) 0%,
|
||||
rgba(7, 23, 48, 0.82) 100%
|
||||
);
|
||||
color: #eefcff;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 14px rgba(62, 177, 248, 0.08);
|
||||
}
|
||||
|
||||
.gimbal-preset-btn {
|
||||
flex: 0 0 72px;
|
||||
height: 38px;
|
||||
border: 1px solid rgba(91, 208, 255, 0.26);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(16, 74, 132, 0.96) 0%,
|
||||
rgba(8, 38, 78, 0.98) 56%,
|
||||
rgba(7, 26, 52, 0.98) 100%
|
||||
);
|
||||
color: #eefcff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 0 0 1px rgba(57, 180, 246, 0.08),
|
||||
0 0 18px rgba(29, 133, 222, 0.12);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(102, 235, 255, 0.72);
|
||||
box-shadow: inset 0 0 0 1px rgba(57, 180, 246, 0.12),
|
||||
0 0 22px rgba(52, 177, 255, 0.22);
|
||||
}
|
||||
}
|
||||
|
||||
.lift-control-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { getRecentAlertPhoto } from "../shared/alertPhotos";
|
||||
|
||||
interface AlarmRecord {
|
||||
id: string;
|
||||
type: string;
|
||||
robot: string;
|
||||
photo: string;
|
||||
video: string;
|
||||
status: "待研判" | "待处理" | "处置完成" | "误判";
|
||||
status: "待处理";
|
||||
time: string;
|
||||
}
|
||||
|
||||
@@ -16,17 +16,15 @@ const alarms: AlarmRecord[] = [
|
||||
id: "AL-001",
|
||||
type: "线缆破损",
|
||||
robot: "巡检机器人 R1",
|
||||
photo: "破损抓拍_001.jpg",
|
||||
video: "线缆破损_001.mp4",
|
||||
status: "待研判",
|
||||
photo: getRecentAlertPhoto("线缆破损"),
|
||||
status: "待处理",
|
||||
time: "2026-06-23 10:24:18",
|
||||
},
|
||||
{
|
||||
id: "AL-002",
|
||||
type: "局部高温",
|
||||
robot: "巡检机器人 Q1",
|
||||
photo: "高温抓拍_017.jpg",
|
||||
video: "局部高温_017.mp4",
|
||||
photo: getRecentAlertPhoto("局部高温"),
|
||||
status: "待处理",
|
||||
time: "2026-06-23 10:18:42",
|
||||
},
|
||||
@@ -34,26 +32,23 @@ const alarms: AlarmRecord[] = [
|
||||
id: "AL-003",
|
||||
type: "墙体裂缝",
|
||||
robot: "巡检机器人 N3",
|
||||
photo: "裂缝取证_006.jpg",
|
||||
video: "墙体裂缝_006.mp4",
|
||||
status: "处置完成",
|
||||
photo: getRecentAlertPhoto("墙体裂缝"),
|
||||
status: "待处理",
|
||||
time: "2026-06-23 09:56:11",
|
||||
},
|
||||
{
|
||||
id: "AL-004",
|
||||
type: "水渍渗漏",
|
||||
robot: "巡检机器人 R2",
|
||||
photo: "渗漏抓拍_012.jpg",
|
||||
video: "水渍渗漏_012.mp4",
|
||||
status: "误判",
|
||||
photo: getRecentAlertPhoto("局部高温"),
|
||||
status: "待处理",
|
||||
time: "2026-06-23 09:20:36",
|
||||
},
|
||||
{
|
||||
id: "AL-005",
|
||||
type: "应急门开启异常",
|
||||
robot: "巡检机器人 N1",
|
||||
photo: "门禁取证_031.jpg",
|
||||
video: "门禁异常_031.mp4",
|
||||
photo: getRecentAlertPhoto("线缆破损"),
|
||||
status: "待处理",
|
||||
time: "2026-06-23 08:42:57",
|
||||
},
|
||||
@@ -101,10 +96,7 @@ const visiblePages = computed(() => {
|
||||
|
||||
function getAlarmStatusClass(status: AlarmRecord["status"]) {
|
||||
const classMap = {
|
||||
待研判: "alarm-status-pending",
|
||||
待处理: "alarm-status-handling",
|
||||
处置完成: "alarm-status-finished",
|
||||
误判: "alarm-status-false",
|
||||
};
|
||||
|
||||
return classMap[status];
|
||||
@@ -125,7 +117,6 @@ function handleSearch() {
|
||||
<section class="page-card management-table-card">
|
||||
<div class="management-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<button class="page-action-btn primary" type="button">批量通知</button>
|
||||
<button class="page-action-btn ghost" type="button">导出告警</button>
|
||||
<input
|
||||
v-model="keyword"
|
||||
@@ -146,7 +137,6 @@ function handleSearch() {
|
||||
<th>告警类型</th>
|
||||
<th>报警机器人</th>
|
||||
<th>取证照片</th>
|
||||
<th>取证视频</th>
|
||||
<th>案件状态</th>
|
||||
<th>告警时间</th>
|
||||
<th>操作</th>
|
||||
@@ -158,10 +148,7 @@ function handleSearch() {
|
||||
<td>{{ alarm.type }}</td>
|
||||
<td>{{ alarm.robot }}</td>
|
||||
<td>
|
||||
<div class="evidence-chip photo">{{ alarm.photo }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="evidence-chip video">{{ alarm.video }}</div>
|
||||
<img class="evidence-photo" :src="alarm.photo" :alt="`${alarm.type}取证照片`" />
|
||||
</td>
|
||||
<td>
|
||||
<span :class="['table-status', getAlarmStatusClass(alarm.status)]">
|
||||
@@ -171,8 +158,6 @@ function handleSearch() {
|
||||
<td>{{ alarm.time }}</td>
|
||||
<td>
|
||||
<div class="table-actions">
|
||||
<button type="button">研判</button>
|
||||
<button type="button">通知</button>
|
||||
<button class="danger" type="button">删除</button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import monitorVideo from "../assets/循环背景动画.mp4";
|
||||
import { recentAlertPhotoSources } from "../shared/alertPhotos";
|
||||
|
||||
const monitorVideo = "https://wuhuimg.oss-cn-shanghai.aliyuncs.com/video/output.mp4";
|
||||
|
||||
type DriveMode = "阿克曼" | "旋转" | "横移";
|
||||
|
||||
interface DeviceItem {
|
||||
id: string;
|
||||
@@ -26,7 +30,11 @@ interface NavPoint {
|
||||
|
||||
interface NavScenario {
|
||||
routePath: string;
|
||||
cloudPoints: NavPoint[];
|
||||
lanePath: string;
|
||||
upperEdgePath: string;
|
||||
lowerEdgePath: string;
|
||||
upperWallPoints: NavPoint[];
|
||||
lowerWallPoints: NavPoint[];
|
||||
checkPoints: Array<{ x: number; y: number; label: string }>;
|
||||
}
|
||||
|
||||
@@ -34,8 +42,8 @@ interface AlertItem {
|
||||
id: string;
|
||||
title: string;
|
||||
level: "高" | "中" | "低";
|
||||
location: string;
|
||||
time: string;
|
||||
photo: string;
|
||||
}
|
||||
|
||||
const projectDevices: ProjectItem[] = [
|
||||
@@ -92,28 +100,35 @@ const recentAlerts: AlertItem[] = [
|
||||
id: "AL-001",
|
||||
title: "线缆破损",
|
||||
level: "高",
|
||||
location: "西江隧道 K1+240",
|
||||
time: "10:24:18",
|
||||
time: "2026-06-26 10:24:18",
|
||||
photo: recentAlertPhotoSources["线缆破损"],
|
||||
},
|
||||
{
|
||||
id: "AL-002",
|
||||
title: "局部高温",
|
||||
level: "中",
|
||||
location: "西江隧道 泵房区",
|
||||
time: "10:18:42",
|
||||
time: "2026-06-26 10:18:42",
|
||||
photo: recentAlertPhotoSources["局部高温"],
|
||||
},
|
||||
{
|
||||
id: "AL-003",
|
||||
title: "墙体裂缝",
|
||||
level: "低",
|
||||
location: "西江隧道 K1+860",
|
||||
time: "09:56:11",
|
||||
time: "2026-06-26 09:56:11",
|
||||
photo: recentAlertPhotoSources["墙体裂缝"],
|
||||
},
|
||||
];
|
||||
|
||||
const alertTabs = ["全部", "线缆破损", "局部高温", "墙体裂缝"] as const;
|
||||
const activeAlertTab = ref<(typeof alertTabs)[number]>("全部");
|
||||
const activeCameraMode = ref<"white" | "infrared">("white");
|
||||
const driveModeTabs: DriveMode[] = ["阿克曼", "旋转", "横移"];
|
||||
const activeDriveMode = ref<DriveMode>("阿克曼");
|
||||
const presetPointOptions = Array.from(
|
||||
{ length: 10 },
|
||||
(_, index) => `预置点${index + 1}`,
|
||||
);
|
||||
const selectedPresetPoint = ref("预置点1");
|
||||
const controlMode = ref<"人工接管" | "自主巡检">("人工接管");
|
||||
const targetLiftHeight = ref("1200");
|
||||
const liftRealtimeStatus = ref("待命");
|
||||
@@ -121,38 +136,73 @@ const liftRealtimeHeight = ref("1180");
|
||||
|
||||
const navScenario: NavScenario = {
|
||||
routePath:
|
||||
"M 72 310 C 118 282, 164 265, 212 248 S 312 205, 360 216 S 446 270, 504 248 S 612 188, 690 210",
|
||||
cloudPoints: [
|
||||
{ x: 52, y: 120 },
|
||||
{ x: 86, y: 132 },
|
||||
{ x: 128, y: 148 },
|
||||
{ x: 164, y: 138 },
|
||||
{ x: 204, y: 118 },
|
||||
{ x: 252, y: 104 },
|
||||
{ x: 304, y: 120 },
|
||||
{ x: 346, y: 142 },
|
||||
{ x: 392, y: 168 },
|
||||
{ x: 442, y: 162 },
|
||||
{ x: 494, y: 134 },
|
||||
{ x: 548, y: 116 },
|
||||
{ x: 602, y: 132 },
|
||||
{ x: 660, y: 152 },
|
||||
{ x: 94, y: 254 },
|
||||
{ x: 150, y: 236 },
|
||||
{ x: 214, y: 220 },
|
||||
{ x: 278, y: 208 },
|
||||
{ x: 338, y: 214 },
|
||||
{ x: 400, y: 236 },
|
||||
{ x: 468, y: 258 },
|
||||
{ x: 530, y: 238 },
|
||||
{ x: 592, y: 214 },
|
||||
{ x: 656, y: 226 },
|
||||
"M 106 318 L 154 306 L 208 292 L 256 278 L 306 264 L 358 258 L 414 258 L 468 248 L 522 241 L 578 234 L 638 226",
|
||||
lanePath:
|
||||
"M 82 270 L 136 258 L 190 244 L 244 230 L 296 216 L 350 208 L 408 208 L 464 198 L 518 190 L 576 182 L 642 174 L 656 188 L 650 278 L 586 286 L 528 294 L 472 302 L 414 312 L 358 312 L 304 320 L 252 334 L 198 348 L 142 360 L 88 370 L 74 356 Z",
|
||||
upperEdgePath:
|
||||
"M 82 270 L 136 258 L 190 244 L 244 230 L 296 216 L 350 208 L 408 208 L 464 198 L 518 190 L 576 182 L 642 174",
|
||||
lowerEdgePath:
|
||||
"M 88 370 L 142 360 L 198 348 L 252 334 L 304 320 L 358 312 L 414 312 L 472 302 L 528 294 L 586 286 L 650 278",
|
||||
upperWallPoints: [
|
||||
{ x: 64, y: 164 },
|
||||
{ x: 84, y: 150 },
|
||||
{ x: 104, y: 142 },
|
||||
{ x: 126, y: 136 },
|
||||
{ x: 148, y: 132 },
|
||||
{ x: 172, y: 126 },
|
||||
{ x: 196, y: 122 },
|
||||
{ x: 224, y: 118 },
|
||||
{ x: 252, y: 116 },
|
||||
{ x: 280, y: 112 },
|
||||
{ x: 312, y: 110 },
|
||||
{ x: 344, y: 108 },
|
||||
{ x: 378, y: 108 },
|
||||
{ x: 412, y: 110 },
|
||||
{ x: 448, y: 112 },
|
||||
{ x: 484, y: 114 },
|
||||
{ x: 520, y: 116 },
|
||||
{ x: 556, y: 120 },
|
||||
{ x: 590, y: 126 },
|
||||
{ x: 622, y: 134 },
|
||||
{ x: 650, y: 144 },
|
||||
{ x: 676, y: 158 },
|
||||
{ x: 116, y: 170 },
|
||||
{ x: 214, y: 156 },
|
||||
{ x: 326, y: 146 },
|
||||
{ x: 446, y: 148 },
|
||||
{ x: 566, y: 160 },
|
||||
{ x: 638, y: 172 },
|
||||
],
|
||||
lowerWallPoints: [
|
||||
{ x: 72, y: 380 },
|
||||
{ x: 96, y: 392 },
|
||||
{ x: 122, y: 404 },
|
||||
{ x: 150, y: 410 },
|
||||
{ x: 180, y: 416 },
|
||||
{ x: 214, y: 420 },
|
||||
{ x: 248, y: 422 },
|
||||
{ x: 284, y: 424 },
|
||||
{ x: 322, y: 426 },
|
||||
{ x: 360, y: 426 },
|
||||
{ x: 400, y: 424 },
|
||||
{ x: 440, y: 420 },
|
||||
{ x: 480, y: 414 },
|
||||
{ x: 520, y: 408 },
|
||||
{ x: 558, y: 400 },
|
||||
{ x: 594, y: 392 },
|
||||
{ x: 628, y: 384 },
|
||||
{ x: 660, y: 374 },
|
||||
{ x: 700, y: 366 },
|
||||
{ x: 126, y: 376 },
|
||||
{ x: 238, y: 392 },
|
||||
{ x: 354, y: 396 },
|
||||
{ x: 474, y: 388 },
|
||||
{ x: 590, y: 372 },
|
||||
],
|
||||
checkPoints: [
|
||||
{ x: 104, y: 280, label: "起点" },
|
||||
{ x: 268, y: 224, label: "高温检测点" },
|
||||
{ x: 480, y: 256, label: "泵房区" },
|
||||
{ x: 674, y: 214, label: "终点" },
|
||||
{ x: 106, y: 318, label: "起点" },
|
||||
{ x: 358, y: 258, label: "途经点" },
|
||||
{ x: 638, y: 226, label: "终点" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -177,11 +227,19 @@ function switchCameraMode(mode: "white" | "infrared") {
|
||||
activeCameraMode.value = mode;
|
||||
}
|
||||
|
||||
function switchDriveMode(mode: DriveMode) {
|
||||
activeDriveMode.value = mode;
|
||||
}
|
||||
|
||||
function toggleControlMode() {
|
||||
controlMode.value =
|
||||
controlMode.value === "人工接管" ? "自主巡检" : "人工接管";
|
||||
}
|
||||
|
||||
function jumpToPresetPoint() {
|
||||
selectedPresetPoint.value = selectedPresetPoint.value;
|
||||
}
|
||||
|
||||
function runToTargetHeight() {
|
||||
const normalizedHeight = targetLiftHeight.value.replace(/[^\d]/g, "");
|
||||
|
||||
@@ -218,7 +276,6 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
</div>
|
||||
<div class="device-meta">
|
||||
<span>{{ device.id }}</span>
|
||||
<span>{{ device.location }}</span>
|
||||
<span>{{ device.battery }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,14 +299,24 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
</div>
|
||||
<template v-if="filteredRecentAlerts.length">
|
||||
<div v-for="alert in filteredRecentAlerts" :key="alert.id" class="alert-item">
|
||||
<div class="alert-main">
|
||||
<div class="alert-title">{{ alert.title }}</div>
|
||||
<div class="alert-level">{{ alert.level }}级</div>
|
||||
</div>
|
||||
<div class="alert-meta">
|
||||
<span>{{ alert.id }}</span>
|
||||
<span>{{ alert.location }}</span>
|
||||
<span>{{ alert.time }}</span>
|
||||
<div class="alert-content">
|
||||
<img class="alert-thumbnail" :src="alert.photo" :alt="alert.title" />
|
||||
<div class="alert-info">
|
||||
<div class="alert-summary">
|
||||
<div class="alert-field">
|
||||
<div class="alert-label">告警类型</div>
|
||||
<div class="alert-title">{{ alert.title }}</div>
|
||||
</div>
|
||||
<div class="alert-field alert-field-right">
|
||||
<div class="alert-label">告警等级</div>
|
||||
<div :class="['alert-level', `level-${alert.level}`]">{{ alert.level }}级</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-time-row">
|
||||
<span class="alert-label">告警时间</span>
|
||||
<span class="alert-time">{{ alert.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -266,13 +333,25 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
<line v-for="line in 7" :key="`h-${line}`" :x1="40" :y1="line * 52" :x2="720" :y2="line * 52" />
|
||||
<line v-for="line in 12" :key="`v-${line}`" :x1="line * 56" :y1="40" :x2="line * 56" :y2="380" />
|
||||
</g>
|
||||
<g class="cloud-layer">
|
||||
<path class="corridor-lane" :d="currentScenario.lanePath" />
|
||||
<path class="corridor-edge" :d="currentScenario.upperEdgePath" />
|
||||
<path class="corridor-edge" :d="currentScenario.lowerEdgePath" />
|
||||
<g class="wall-layer wall-upper">
|
||||
<circle
|
||||
v-for="(point, index) in currentScenario.cloudPoints"
|
||||
:key="`point-${index}`"
|
||||
v-for="(point, index) in currentScenario.upperWallPoints"
|
||||
:key="`upper-wall-${index}`"
|
||||
:cx="point.x"
|
||||
:cy="point.y"
|
||||
:r="index % 3 === 0 ? 3 : 2"
|
||||
:r="index % 4 === 0 ? 3.4 : 2.4"
|
||||
/>
|
||||
</g>
|
||||
<g class="wall-layer wall-lower">
|
||||
<circle
|
||||
v-for="(point, index) in currentScenario.lowerWallPoints"
|
||||
:key="`lower-wall-${index}`"
|
||||
:cx="point.x"
|
||||
:cy="point.y"
|
||||
:r="index % 5 === 0 ? 3.2 : 2.3"
|
||||
/>
|
||||
</g>
|
||||
<path class="route-glow" :d="currentScenario.routePath" />
|
||||
@@ -300,35 +379,14 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="nav-map-overlay top-left">激光雷达点云</div>
|
||||
<div class="nav-map-overlay top-right">路径跟踪中</div>
|
||||
<div class="nav-map-overlay top-left">隧道点云走廊</div>
|
||||
<div class="nav-map-overlay top-right">窄通道路径跟踪</div>
|
||||
<div class="nav-map-overlay bottom-left">
|
||||
机器人编号:{{ currentRobot.id }} / {{ currentRobot.task }}
|
||||
</div>
|
||||
<div class="nav-map-overlay bottom-right">
|
||||
线速度 {{ currentRobot.linearSpeed }} / 角速度 {{ currentRobot.angularSpeed }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-map-footer">
|
||||
<div class="footer-item">
|
||||
<span class="footer-label">当前位置</span>
|
||||
<span class="footer-value">{{ currentRobot.location }}</span>
|
||||
</div>
|
||||
<div class="footer-item">
|
||||
<span class="footer-label">电量</span>
|
||||
<span class="footer-value">{{ currentRobot.battery }}</span>
|
||||
</div>
|
||||
<div class="footer-item">
|
||||
<span class="footer-label">任务状态</span>
|
||||
<span class="footer-value">{{ currentRobot.task }}</span>
|
||||
</div>
|
||||
<div class="footer-item">
|
||||
<span class="footer-label">实时线速度</span>
|
||||
<span class="footer-value">{{ currentRobot.linearSpeed }}</span>
|
||||
</div>
|
||||
<div class="footer-item">
|
||||
<span class="footer-label">实时角速度</span>
|
||||
<span class="footer-value">{{ currentRobot.angularSpeed }}</span>
|
||||
电量 {{ currentRobot.battery }} / 线速度 {{ currentRobot.linearSpeed }} / 角速度
|
||||
{{ currentRobot.angularSpeed }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -371,7 +429,6 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
{{ activeCameraMode === "white" ? "白光镜头" : "红外镜头" }}
|
||||
</div>
|
||||
<div class="video-overlay bottom-left">{{ currentRobot.id }}</div>
|
||||
<div class="video-overlay bottom-right">{{ currentRobot.location }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -403,6 +460,17 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
<div class="control-top-row">
|
||||
<div class="vehicle-section">
|
||||
<div class="section-title control-block-title">底盘驱动</div>
|
||||
<div class="drive-mode-tabs">
|
||||
<button
|
||||
v-for="mode in driveModeTabs"
|
||||
:key="mode"
|
||||
:class="['drive-mode-tab', { active: activeDriveMode === mode }]"
|
||||
type="button"
|
||||
@click="switchDriveMode(mode)"
|
||||
>
|
||||
{{ mode }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="vehicle-pad">
|
||||
<button class="drive-btn drive-up" type="button">
|
||||
<span class="drive-arrow">↑</span>
|
||||
@@ -425,6 +493,22 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
</div>
|
||||
<div class="control-section gimbal-section">
|
||||
<div class="section-title">云台控制</div>
|
||||
<div class="gimbal-preset-panel">
|
||||
<label class="gimbal-preset-group">
|
||||
<select v-model="selectedPresetPoint" class="gimbal-preset-select">
|
||||
<option
|
||||
v-for="preset in presetPointOptions"
|
||||
:key="preset"
|
||||
:value="preset"
|
||||
>
|
||||
{{ preset }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="gimbal-preset-btn" type="button" @click="jumpToPresetPoint">
|
||||
跳转
|
||||
</button>
|
||||
</div>
|
||||
<div class="vehicle-pad gimbal-wheel">
|
||||
<button class="drive-btn drive-up" type="button">
|
||||
<span class="drive-arrow">↑</span>
|
||||
@@ -482,3 +566,4 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ interface PatrolRecord {
|
||||
robot: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
mileage: string;
|
||||
alarms: PatrolAlarmRecord[];
|
||||
}
|
||||
|
||||
@@ -22,7 +21,6 @@ const patrolRecords: PatrolRecord[] = [
|
||||
robot: "巡检机器人 R1",
|
||||
startTime: "2026-06-23 08:10:22",
|
||||
endTime: "2026-06-23 09:02:15",
|
||||
mileage: "3.8 km",
|
||||
alarms: [
|
||||
{ id: "AL-001", type: "线缆破损", reportTime: "2026-06-23 08:36:12" },
|
||||
{ id: "AL-002", type: "局部高温", reportTime: "2026-06-23 08:42:09" },
|
||||
@@ -33,7 +31,6 @@ const patrolRecords: PatrolRecord[] = [
|
||||
robot: "巡检机器人 Q1",
|
||||
startTime: "2026-06-23 09:20:08",
|
||||
endTime: "2026-06-23 10:01:46",
|
||||
mileage: "2.9 km",
|
||||
alarms: [
|
||||
{ id: "AL-005", type: "应急门开启异常", reportTime: "2026-06-23 09:48:57" },
|
||||
],
|
||||
@@ -43,7 +40,6 @@ const patrolRecords: PatrolRecord[] = [
|
||||
robot: "巡检机器人 N3",
|
||||
startTime: "2026-06-23 10:08:34",
|
||||
endTime: "2026-06-23 11:12:25",
|
||||
mileage: "4.6 km",
|
||||
alarms: [
|
||||
{ id: "AL-003", type: "墙体裂缝", reportTime: "2026-06-23 10:36:41" },
|
||||
{ id: "AL-006", type: "水渍渗漏", reportTime: "2026-06-23 10:54:18" },
|
||||
@@ -55,7 +51,6 @@ const patrolRecords: PatrolRecord[] = [
|
||||
robot: "巡检机器人 R2",
|
||||
startTime: "2026-06-23 13:15:17",
|
||||
endTime: "2026-06-23 14:05:52",
|
||||
mileage: "3.2 km",
|
||||
alarms: [],
|
||||
},
|
||||
];
|
||||
@@ -93,7 +88,6 @@ function closeDialog() {
|
||||
<th>巡检机器人</th>
|
||||
<th>开始巡检时间</th>
|
||||
<th>结束巡检时间</th>
|
||||
<th>巡检里程</th>
|
||||
<th>告警触发次数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -103,7 +97,6 @@ function closeDialog() {
|
||||
<td>{{ record.robot }}</td>
|
||||
<td>{{ record.startTime }}</td>
|
||||
<td>{{ record.endTime }}</td>
|
||||
<td>{{ record.mileage }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="alarm-link-btn"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { computed, nextTick, ref } from "vue";
|
||||
import monitorVideo from "../assets/循环背景动画.mp4";
|
||||
|
||||
type GridMode = 1 | 4 | 9;
|
||||
@@ -38,7 +38,7 @@ const projectData: ProjectNode = {
|
||||
cameras: [
|
||||
{ id: "CAM-001", name: "白光镜头", type: "白光", status: "在线" },
|
||||
{ id: "CAM-002", name: "红外镜头", type: "红外", status: "在线" },
|
||||
{ id: "CAM-003", name: "云台辅镜头", type: "辅助", status: "在线" },
|
||||
{ id: "CAM-003", name: "云台辅助镜头", type: "辅助", status: "在线" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -53,10 +53,13 @@ const projectData: ProjectNode = {
|
||||
};
|
||||
|
||||
const gridMode = ref<GridMode>(4);
|
||||
const playbackProgress = ref(42);
|
||||
const playbackDate = ref("2026-06-23");
|
||||
const playbackTime = ref("14:20:00");
|
||||
const activeCameraId = ref(projectData.devices[0].cameras[0].id);
|
||||
const playbackDialogVisible = ref(false);
|
||||
const playbackStartAt = ref("2026-06-23T14:20");
|
||||
const playbackEndAt = ref("2026-06-23T14:50");
|
||||
const playbackProgress = ref(0);
|
||||
const playbackPaused = ref(true);
|
||||
const playbackPlayerRef = ref<HTMLVideoElement | null>(null);
|
||||
|
||||
const currentProject = computed(() => projectData);
|
||||
|
||||
@@ -71,7 +74,9 @@ const projectCameras = computed<ProjectCameraItem[]>(() =>
|
||||
);
|
||||
|
||||
const activeCamera = computed(
|
||||
() => projectCameras.value.find((camera) => camera.id === activeCameraId.value) ?? projectCameras.value[0],
|
||||
() =>
|
||||
projectCameras.value.find((camera) => camera.id === activeCameraId.value) ??
|
||||
projectCameras.value[0],
|
||||
);
|
||||
|
||||
const orderedCameras = computed(() => {
|
||||
@@ -97,6 +102,12 @@ const visibleCameras = computed(() => {
|
||||
return Array.from({ length: targetCount }, (_, index) => cameras[index] ?? null);
|
||||
});
|
||||
|
||||
const playbackRangeLabel = computed(() => {
|
||||
const startLabel = playbackStartAt.value.replace("T", " ");
|
||||
const endLabel = playbackEndAt.value.replace("T", " ");
|
||||
return `${startLabel} - ${endLabel}`;
|
||||
});
|
||||
|
||||
function selectCamera(cameraId: string) {
|
||||
activeCameraId.value = cameraId;
|
||||
}
|
||||
@@ -104,6 +115,92 @@ function selectCamera(cameraId: string) {
|
||||
function setGridMode(mode: GridMode) {
|
||||
gridMode.value = mode;
|
||||
}
|
||||
|
||||
function syncPlaybackProgress() {
|
||||
const video = playbackPlayerRef.value;
|
||||
|
||||
if (!video) {
|
||||
playbackProgress.value = 0;
|
||||
playbackPaused.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = Number.isFinite(video.duration) ? video.duration : 0;
|
||||
playbackProgress.value = duration > 0 ? (video.currentTime / duration) * 100 : 0;
|
||||
playbackPaused.value = video.paused;
|
||||
}
|
||||
|
||||
function startPlayback() {
|
||||
const video = playbackPlayerRef.value;
|
||||
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
|
||||
video.currentTime = 0;
|
||||
void video.play();
|
||||
playbackPaused.value = false;
|
||||
syncPlaybackProgress();
|
||||
}
|
||||
|
||||
async function openPlaybackDialog() {
|
||||
playbackDialogVisible.value = true;
|
||||
playbackProgress.value = 0;
|
||||
playbackPaused.value = true;
|
||||
|
||||
await nextTick();
|
||||
startPlayback();
|
||||
}
|
||||
|
||||
function closePlaybackDialog() {
|
||||
playbackPlayerRef.value?.pause();
|
||||
playbackDialogVisible.value = false;
|
||||
playbackPaused.value = true;
|
||||
}
|
||||
|
||||
function togglePlaybackPause() {
|
||||
const video = playbackPlayerRef.value;
|
||||
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (video.paused) {
|
||||
void video.play();
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
|
||||
syncPlaybackProgress();
|
||||
}
|
||||
|
||||
function seekPlayback(offsetSeconds: number) {
|
||||
const video = playbackPlayerRef.value;
|
||||
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = Number.isFinite(video.duration) ? video.duration : 0;
|
||||
const targetTime = Math.max(0, Math.min(duration, video.currentTime + offsetSeconds));
|
||||
video.currentTime = targetTime;
|
||||
syncPlaybackProgress();
|
||||
}
|
||||
|
||||
function updatePlaybackProgress() {
|
||||
const video = playbackPlayerRef.value;
|
||||
|
||||
if (!video) {
|
||||
return;
|
||||
}
|
||||
|
||||
const duration = Number.isFinite(video.duration) ? video.duration : 0;
|
||||
if (duration <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
video.currentTime = (playbackProgress.value / 100) * duration;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -128,6 +225,11 @@ function setGridMode(mode: GridMode) {
|
||||
<span class="tree-device-id">{{ camera.deviceName }} / {{ camera.id }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tree-panel-footer">
|
||||
<button class="tree-playback-btn" type="button" @click="openPlaybackDialog">
|
||||
录像回放
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -176,36 +278,74 @@ function setGridMode(mode: GridMode) {
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="playback-panel">
|
||||
<div class="playback-header">
|
||||
<div class="playback-title">录像回放</div>
|
||||
<div class="playback-filter">
|
||||
<input v-model="playbackDate" type="date" />
|
||||
<input v-model="playbackTime" type="time" step="1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="playback-controls">
|
||||
<button class="playback-btn" type="button">回放</button>
|
||||
<button class="playback-btn" type="button">暂停</button>
|
||||
<button class="playback-btn" type="button">快退</button>
|
||||
<button class="playback-btn" type="button">快进</button>
|
||||
<div class="playback-track">
|
||||
<div class="playback-track-line">
|
||||
<span :style="{ width: `${playbackProgress}%` }"></span>
|
||||
</div>
|
||||
<input
|
||||
v-model="playbackProgress"
|
||||
class="playback-range"
|
||||
max="100"
|
||||
min="0"
|
||||
type="range"
|
||||
/>
|
||||
</div>
|
||||
<div class="playback-time">{{ playbackDate }} {{ playbackTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="playbackDialogVisible && activeCamera"
|
||||
class="playback-dialog-mask"
|
||||
@click.self="closePlaybackDialog"
|
||||
>
|
||||
<div class="playback-dialog">
|
||||
<div class="playback-dialog-header">
|
||||
<div>
|
||||
<div class="playback-dialog-title">录像回放</div>
|
||||
<div class="playback-dialog-meta">
|
||||
{{ activeCamera.deviceName }} / {{ activeCamera.name }} / {{ activeCamera.id }}
|
||||
</div>
|
||||
</div>
|
||||
<button class="dialog-close" type="button" @click="closePlaybackDialog">×</button>
|
||||
</div>
|
||||
|
||||
<div class="playback-dialog-filter">
|
||||
<label class="playback-field">
|
||||
<span>开始时间</span>
|
||||
<input v-model="playbackStartAt" type="datetime-local" />
|
||||
</label>
|
||||
<label class="playback-field">
|
||||
<span>结束时间</span>
|
||||
<input v-model="playbackEndAt" type="datetime-local" />
|
||||
</label>
|
||||
<button class="playback-btn playback-launch-btn" type="button" @click="startPlayback">
|
||||
开始回放
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="playback-preview">
|
||||
<video
|
||||
ref="playbackPlayerRef"
|
||||
:src="monitorVideo"
|
||||
playsinline
|
||||
@ended="syncPlaybackProgress"
|
||||
@loadedmetadata="syncPlaybackProgress"
|
||||
@pause="syncPlaybackProgress"
|
||||
@play="syncPlaybackProgress"
|
||||
@timeupdate="syncPlaybackProgress"
|
||||
></video>
|
||||
</div>
|
||||
|
||||
<div class="playback-dialog-controls">
|
||||
<button class="playback-btn" type="button" @click="togglePlaybackPause">
|
||||
{{ playbackPaused ? "播放" : "暂停" }}
|
||||
</button>
|
||||
<button class="playback-btn" type="button" @click="seekPlayback(-10)">快退 10s</button>
|
||||
<button class="playback-btn" type="button" @click="seekPlayback(10)">快进 10s</button>
|
||||
<div class="playback-track">
|
||||
<div class="playback-track-line">
|
||||
<span :style="{ width: `${playbackProgress}%` }"></span>
|
||||
</div>
|
||||
<input
|
||||
v-model="playbackProgress"
|
||||
class="playback-range"
|
||||
max="100"
|
||||
min="0"
|
||||
type="range"
|
||||
@input="updatePlaybackProgress"
|
||||
/>
|
||||
</div>
|
||||
<div class="playback-time">{{ playbackRangeLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
78
src/views/dashboard1/shared/alertPhotos.ts
Normal file
78
src/views/dashboard1/shared/alertPhotos.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
export type AlertPhotoScene = "cable" | "heat" | "crack";
|
||||
|
||||
export function createAlertPhoto(scene: AlertPhotoScene, accent: string) {
|
||||
const sceneMarkup =
|
||||
scene === "cable"
|
||||
? `
|
||||
<path d="M64 112 C140 90, 222 88, 304 104 S470 132, 556 110" stroke="${accent}" stroke-width="6" fill="none" stroke-linecap="round" opacity="0.9" />
|
||||
<path d="M314 106 L340 92 L366 120" stroke="#ffe9a6" stroke-width="5" fill="none" stroke-linecap="round" />
|
||||
<circle cx="340" cy="106" r="32" fill="rgba(255,102,82,0.18)" stroke="#ff8e79" stroke-width="2" stroke-dasharray="6 6" />
|
||||
`
|
||||
: scene === "heat"
|
||||
? `
|
||||
<ellipse cx="340" cy="122" rx="84" ry="46" fill="rgba(255,122,68,0.26)" />
|
||||
<ellipse cx="344" cy="122" rx="58" ry="30" fill="rgba(255,193,82,0.28)" />
|
||||
<path d="M320 146 C330 126, 334 114, 332 96 C344 112, 350 126, 348 148" fill="none" stroke="#ffd36a" stroke-width="4" stroke-linecap="round" />
|
||||
<path d="M358 142 C366 126, 372 114, 370 100 C382 112, 388 126, 384 144" fill="none" stroke="#ff8a5b" stroke-width="4" stroke-linecap="round" />
|
||||
`
|
||||
: `
|
||||
<path d="M206 88 L234 130 L226 166 L268 210 L254 248 L294 282" stroke="#d8ecff" stroke-width="4" fill="none" stroke-linecap="round" />
|
||||
<path d="M248 152 L280 136 L306 164" stroke="#d8ecff" stroke-width="3" fill="none" stroke-linecap="round" />
|
||||
<path d="M266 212 L302 196 L328 220" stroke="#d8ecff" stroke-width="3" fill="none" stroke-linecap="round" />
|
||||
<ellipse cx="258" cy="178" rx="86" ry="118" fill="rgba(132,196,255,0.08)" stroke="${accent}" stroke-width="2" stroke-dasharray="5 7" />
|
||||
`;
|
||||
|
||||
const svg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#041523" />
|
||||
<stop offset="55%" stop-color="#0a2740" />
|
||||
<stop offset="100%" stop-color="#06111d" />
|
||||
</linearGradient>
|
||||
<linearGradient id="tunnel" x1="0" x2="1" y1="0.4" y2="0.6">
|
||||
<stop offset="0%" stop-color="#132b3f" />
|
||||
<stop offset="50%" stop-color="#0a2135" />
|
||||
<stop offset="100%" stop-color="#132b3f" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="640" height="360" fill="url(#bg)" />
|
||||
<rect x="0" y="0" width="640" height="58" fill="rgba(7,18,30,0.52)" />
|
||||
<rect x="0" y="302" width="640" height="58" fill="rgba(3,10,18,0.66)" />
|
||||
<path d="M0 82 C98 58, 196 52, 320 56 C448 60, 548 58, 640 78 L640 0 L0 0 Z" fill="rgba(17,38,56,0.88)" />
|
||||
<path d="M0 314 C112 286, 228 278, 320 282 C456 286, 554 280, 640 256 L640 360 L0 360 Z" fill="rgba(14,24,36,0.96)" />
|
||||
<path d="M44 250 C148 226, 254 214, 352 214 C452 214, 548 204, 606 188" stroke="url(#tunnel)" stroke-width="92" fill="none" stroke-linecap="round" opacity="0.92" />
|
||||
<path d="M54 248 C158 226, 254 214, 352 214 C452 214, 540 204, 596 190" stroke="rgba(113,227,255,0.12)" stroke-width="1.6" fill="none" stroke-dasharray="4 10" />
|
||||
<g opacity="0.78">
|
||||
<circle cx="96" cy="104" r="3" fill="#8ae5ff" />
|
||||
<circle cx="134" cy="118" r="2.2" fill="#8ae5ff" />
|
||||
<circle cx="182" cy="94" r="2.6" fill="#8ae5ff" />
|
||||
<circle cx="444" cy="96" r="2.8" fill="#8ae5ff" />
|
||||
<circle cx="522" cy="116" r="2.4" fill="#8ae5ff" />
|
||||
<circle cx="564" cy="102" r="2.8" fill="#8ae5ff" />
|
||||
</g>
|
||||
<g opacity="0.54">
|
||||
<circle cx="86" cy="286" r="2.2" fill="#74d9ff" />
|
||||
<circle cx="154" cy="300" r="2.8" fill="#74d9ff" />
|
||||
<circle cx="452" cy="292" r="2.2" fill="#74d9ff" />
|
||||
<circle cx="548" cy="284" r="2.6" fill="#74d9ff" />
|
||||
</g>
|
||||
${sceneMarkup}
|
||||
<rect x="20" y="20" width="600" height="320" rx="16" fill="none" stroke="rgba(110,224,255,0.12)" stroke-width="2" />
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
|
||||
}
|
||||
|
||||
export const recentAlertPhotoSources = {
|
||||
"线缆破损": createAlertPhoto("cable", "#ff8f79"),
|
||||
"局部高温": createAlertPhoto("heat", "#ffc86c"),
|
||||
"墙体裂缝": createAlertPhoto("crack", "#89d8ff"),
|
||||
} as const;
|
||||
|
||||
const fallbackAlertPhoto = recentAlertPhotoSources["墙体裂缝"];
|
||||
|
||||
export function getRecentAlertPhoto(type: string) {
|
||||
return recentAlertPhotoSources[type as keyof typeof recentAlertPhotoSources] ?? fallbackAlertPhoto;
|
||||
}
|
||||
Reference in New Issue
Block a user