first commit

This commit is contained in:
2026-06-26 16:34:27 +08:00
commit 45ae1670c6
135 changed files with 10502 additions and 0 deletions

View File

@@ -0,0 +1,484 @@
<script lang="ts" setup>
import { computed, ref } from "vue";
import monitorVideo from "../assets/循环背景动画.mp4";
interface DeviceItem {
id: string;
name: string;
status: "空闲" | "离线" | "充电中";
location: string;
battery: string;
task: string;
linearSpeed: string;
angularSpeed: string;
}
interface ProjectItem {
id: number;
name: string;
devices: DeviceItem[];
}
interface NavPoint {
x: number;
y: number;
}
interface NavScenario {
routePath: string;
cloudPoints: NavPoint[];
checkPoints: Array<{ x: number; y: number; label: string }>;
}
interface AlertItem {
id: string;
title: string;
level: "高" | "中" | "低";
location: string;
time: string;
}
const projectDevices: ProjectItem[] = [
{
id: 1,
name: "西江隧道",
devices: [
{
id: "ROBOT-001",
name: "巡检机器人 R1",
status: "空闲",
location: "东洞口至 K1+240",
battery: "82%",
task: "日常巡检",
linearSpeed: "0.42m/s",
angularSpeed: "0.08rad/s",
},
{
id: "ROBOT-002",
name: "巡检机器人 R2",
status: "空闲",
location: "K1+240 至 K2+120",
battery: "76%",
task: "设备复核",
linearSpeed: "0.38m/s",
angularSpeed: "0.06rad/s",
},
{
id: "ROBOT-003",
name: "巡检机器人 R3",
status: "充电中",
location: "中控区",
battery: "48%",
task: "回库维护",
linearSpeed: "0.00m/s",
angularSpeed: "0.00rad/s",
},
{
id: "ROBOT-004",
name: "巡检机器人 R4",
status: "空闲",
location: "K2+120 至西洞口",
battery: "91%",
task: "夜间待命",
linearSpeed: "0.31m/s",
angularSpeed: "0.04rad/s",
},
],
},
];
const recentAlerts: AlertItem[] = [
{
id: "AL-001",
title: "线缆破损",
level: "高",
location: "西江隧道 K1+240",
time: "10:24:18",
},
{
id: "AL-002",
title: "局部高温",
level: "中",
location: "西江隧道 泵房区",
time: "10:18:42",
},
{
id: "AL-003",
title: "墙体裂缝",
level: "低",
location: "西江隧道 K1+860",
time: "09:56:11",
},
];
const alertTabs = ["全部", "线缆破损", "局部高温", "墙体裂缝"] as const;
const activeAlertTab = ref<(typeof alertTabs)[number]>("全部");
const activeCameraMode = ref<"white" | "infrared">("white");
const controlMode = ref<"人工接管" | "自主巡检">("人工接管");
const targetLiftHeight = ref("1200");
const liftRealtimeStatus = ref("待命");
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 },
],
checkPoints: [
{ x: 104, y: 280, label: "起点" },
{ x: 268, y: 224, label: "高温检测点" },
{ x: 480, y: 256, label: "泵房区" },
{ x: 674, y: 214, label: "终点" },
],
};
const currentProject = computed(() => projectDevices[0]);
const currentDevices = computed(() => currentProject.value.devices);
const currentRobot = computed(() => currentDevices.value[0]);
const currentScenario = computed(() => navScenario);
const filteredRecentAlerts = computed(() => {
if (activeAlertTab.value === "全部") {
return recentAlerts;
}
return recentAlerts.filter((alert) => alert.title === activeAlertTab.value);
});
function switchAlertTab(tab: (typeof alertTabs)[number]) {
activeAlertTab.value = tab;
}
function switchCameraMode(mode: "white" | "infrared") {
activeCameraMode.value = mode;
}
function toggleControlMode() {
controlMode.value =
controlMode.value === "人工接管" ? "自主巡检" : "人工接管";
}
function runToTargetHeight() {
const normalizedHeight = targetLiftHeight.value.replace(/[^\d]/g, "");
targetLiftHeight.value = normalizedHeight || "0";
liftRealtimeStatus.value = "运行中";
liftRealtimeHeight.value = targetLiftHeight.value;
}
function getStatusClass(status: DeviceItem["status"]) {
const classMap: Record<DeviceItem["status"], string> = {
空闲: "status-idle",
离线: "status-offline",
充电中: "status-charging",
};
return classMap[status];
}
</script>
<template>
<div class="center">
<div class="left">
<div class="card-1 device-card">
<div class="title">设备列表</div>
<div class="device-panel">
<div class="project-name">{{ currentProject.name }}</div>
<div class="device-list">
<div v-for="device in currentDevices" :key="device.id" class="device-item">
<div class="device-main">
<div class="device-name">{{ device.name }}</div>
<div :class="['device-status', getStatusClass(device.status)]">
{{ device.status }}
</div>
</div>
<div class="device-meta">
<span>{{ device.id }}</span>
<span>{{ device.location }}</span>
<span>{{ device.battery }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="card-2 alert-card">
<div class="title">近期告警</div>
<div class="alert-panel">
<div class="alert-tabs">
<button
v-for="tab in alertTabs"
:key="tab"
:class="['alert-tab', { active: activeAlertTab === tab }]"
type="button"
@click="switchAlertTab(tab)"
>
{{ tab }}
</button>
</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>
</div>
</template>
<div v-else class="alert-empty">当前筛选下暂无告警</div>
</div>
</div>
</div>
<div class="middle">
<div class="nav-map-panel">
<div class="nav-map-stage">
<svg class="nav-map-svg" viewBox="0 0 760 420" preserveAspectRatio="xMidYMid meet">
<g class="grid-layer">
<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">
<circle
v-for="(point, index) in currentScenario.cloudPoints"
:key="`point-${index}`"
:cx="point.x"
:cy="point.y"
:r="index % 3 === 0 ? 3 : 2"
/>
</g>
<path class="route-glow" :d="currentScenario.routePath" />
<path class="route-line" :d="currentScenario.routePath" />
<g class="checkpoint-layer">
<g
v-for="point in currentScenario.checkPoints"
:key="point.label"
class="checkpoint"
:transform="`translate(${point.x}, ${point.y})`"
>
<circle r="7"></circle>
<text x="12" y="4">{{ point.label }}</text>
</g>
</g>
<g class="robot-layer">
<circle class="robot-shadow" r="18">
<animateMotion dur="18s" repeatCount="indefinite" :path="currentScenario.routePath" />
</circle>
<circle class="robot-body" r="10">
<animateMotion dur="18s" repeatCount="indefinite" rotate="auto" :path="currentScenario.routePath" />
</circle>
<path class="robot-head" d="M -3 -9 L 8 0 L -3 9 Z">
<animateMotion dur="18s" repeatCount="indefinite" rotate="auto" :path="currentScenario.routePath" />
</path>
</g>
</svg>
<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>
</div>
</div>
</div>
</div>
<div class="right">
<div class="card-1 video-card">
<div class="card-header">
<div class="title">视频监控</div>
<div class="camera-switch header-switch">
<button
:class="['camera-btn', { active: activeCameraMode === 'white' }]"
type="button"
@click="switchCameraMode('white')"
>
白光
</button>
<button
:class="['camera-btn', { active: activeCameraMode === 'infrared' }]"
type="button"
@click="switchCameraMode('infrared')"
>
红外
</button>
</div>
</div>
<div class="video-panel">
<div class="video-screen">
<video
:class="['monitor-video', { infrared: activeCameraMode === 'infrared' }]"
autoplay
loop
muted
playsinline
:src="monitorVideo"
></video>
<div class="scan-line"></div>
<div class="video-overlay top-left">实时视频流</div>
<div class="video-overlay top-right">
{{ activeCameraMode === "white" ? "白光镜头" : "红外镜头" }}
</div>
<div class="video-overlay bottom-left">{{ currentRobot.id }}</div>
<div class="video-overlay bottom-right">{{ currentRobot.location }}</div>
</div>
</div>
</div>
<div class="card-2 control-card">
<div class="card-header">
<div class="title">远程控制</div>
<button class="mode-toggle-btn" type="button" @click="toggleControlMode">
<span :class="['mode-toggle-chip', { active: controlMode === '人工接管' }]">
人工接管
</span>
<span :class="['mode-toggle-chip', { active: controlMode === '自主巡检' }]">
自主巡检
</span>
</button>
</div>
<div class="control-panel">
<div class="control-status">
<div class="status-item">
<span class="status-label">控制对象</span>
<span class="status-value">{{ currentRobot.id }}</span>
</div>
<div class="status-item">
<span class="status-label">当前模式</span>
<span class="status-value">{{ controlMode }}</span>
</div>
</div>
<div class="control-main">
<div class="control-top-row">
<div class="vehicle-section">
<div class="section-title control-block-title">底盘驱动</div>
<div class="vehicle-pad">
<button class="drive-btn drive-up" type="button">
<span class="drive-arrow"></span>
<span class="drive-label">前进</span>
</button>
<button class="drive-btn drive-left" type="button">
<span class="drive-arrow"></span>
<span class="drive-label">左转</span>
</button>
<div class="drive-core" aria-hidden="true"></div>
<button class="drive-btn drive-right" type="button">
<span class="drive-arrow"></span>
<span class="drive-label">右转</span>
</button>
<button class="drive-btn drive-down" type="button">
<span class="drive-arrow"></span>
<span class="drive-label">后退</span>
</button>
</div>
</div>
<div class="control-section gimbal-section">
<div class="section-title">云台控制</div>
<div class="vehicle-pad gimbal-wheel">
<button class="drive-btn drive-up" type="button">
<span class="drive-arrow"></span>
<span class="drive-label"></span>
</button>
<button class="drive-btn drive-left" type="button">
<span class="drive-arrow"></span>
<span class="drive-label"></span>
</button>
<div class="drive-core" aria-hidden="true"></div>
<button class="drive-btn drive-right" type="button">
<span class="drive-arrow"></span>
<span class="drive-label"></span>
</button>
<button class="drive-btn drive-down" type="button">
<span class="drive-arrow"></span>
<span class="drive-label"></span>
</button>
</div>
</div>
</div>
<div class="control-section lift-section">
<div class="section-title">升降柱控制</div>
<div class="lift-control-panel">
<label class="lift-input-group">
<span class="lift-input-label">目标高度</span>
<div class="lift-input-shell">
<input
v-model="targetLiftHeight"
class="lift-input"
type="text"
inputmode="numeric"
/>
<span class="lift-unit">mm</span>
</div>
</label>
<button class="lift-submit-btn" type="button" @click="runToTargetHeight">
运行到指定高度
</button>
<div class="lift-status-panel">
<div class="lift-status-item">
<span class="lift-status-label">实时状态</span>
<span class="lift-status-value">{{ liftRealtimeStatus }}</span>
</div>
<div class="lift-status-item">
<span class="lift-status-label">当前高度</span>
<span class="lift-status-value">{{ liftRealtimeHeight }} mm</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>