556 lines
17 KiB
Vue
556 lines
17 KiB
Vue
|
|
<template>
|
||
|
|
<div class="chassis-control">
|
||
|
|
<div class="main-wrap">
|
||
|
|
<section class="map-area">
|
||
|
|
<div class="area-bar">
|
||
|
|
<span class="area-bar__title">
|
||
|
|
<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.4" class="area-bar__icon">
|
||
|
|
<polygon points="1 4.5 1 15 6 12.5 12 15 17 12.5 17 1.5 12 4 6 1.5" />
|
||
|
|
<line x1="6" y1="1.5" x2="6" y2="12.5" />
|
||
|
|
<line x1="12" y1="4" x2="12" y2="15" />
|
||
|
|
</svg>
|
||
|
|
MAP VIEW
|
||
|
|
</span>
|
||
|
|
<div class="map-controller">
|
||
|
|
<div class="btn-container">
|
||
|
|
<el-icon :size="16" color="#00D4FF" @click="zoomCanvas(true)"><zoom-out /></el-icon>
|
||
|
|
</div>
|
||
|
|
<div class="btn-container">
|
||
|
|
<el-icon :size="16" color="#00D4FF" @click="zoomCanvas(false)"><zoom-in /></el-icon>
|
||
|
|
</div>
|
||
|
|
<div class="btn-container">
|
||
|
|
<!-- @click="centerCanvasView" -->
|
||
|
|
<el-icon :size="16" color="#00D4FF" ><Location /></el-icon>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="map-body">
|
||
|
|
<!-- <canvas ref="mapCanvas" /> -->
|
||
|
|
<MapCanvas :mapName="mapName" :isDragging="!isDragging" />
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<aside class="panel">
|
||
|
|
<!-- Direction -->
|
||
|
|
<div class="card">
|
||
|
|
<div class="card__head">
|
||
|
|
<h3>方向控制</h3>
|
||
|
|
<el-tag size="small" effect="dark" type="info">长按持续移动</el-tag>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="dpad">
|
||
|
|
<button class="dpad__btn dpad__btn--up"
|
||
|
|
@mousedown="onPress('forward')" @mouseup="onRelease" @mouseleave="onRelease"
|
||
|
|
@touchstart.prevent="onPress('forward')" @touchend.prevent="onRelease">
|
||
|
|
<svg viewBox="0 0 14 14"><path d="M7 1L13 11H1Z" fill="currentColor" /></svg>
|
||
|
|
<small>前进</small>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<button class="dpad__btn dpad__btn--lt"
|
||
|
|
@mousedown="onPress('left')" @mouseup="onRelease"
|
||
|
|
@touchstart.prevent="onPress('left')" @touchend.prevent="onRelease">
|
||
|
|
<svg viewBox="0 0 14 14"><path d="M1 7L11 1V13Z" fill="currentColor" /></svg>
|
||
|
|
<small>左转</small>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<div class="dpad__core"><span /></div>
|
||
|
|
|
||
|
|
<button class="dpad__btn dpad__btn--rt"
|
||
|
|
@mousedown="onPress('right')" @mouseup="onRelease"
|
||
|
|
@touchstart.prevent="onPress('right')" @touchend.prevent="onRelease">
|
||
|
|
<svg viewBox="0 0 14 14"><path d="M13 7L3 1V13Z" fill="currentColor" /></svg>
|
||
|
|
<small>右转</small>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
<button class="dpad__btn dpad__btn--dn"
|
||
|
|
@mousedown="onPress('backward')" @mouseup="onRelease"
|
||
|
|
@touchstart.prevent="onPress('backward')" @touchend.prevent="onRelease">
|
||
|
|
<svg viewBox="0 0 14 14"><path d="M7 13L1 3H13Z" fill="currentColor" /></svg>
|
||
|
|
<small>后退</small>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Parameters -->
|
||
|
|
<div class="card">
|
||
|
|
<div class="card__head"><h3>参数设置</h3></div>
|
||
|
|
|
||
|
|
<div class="param" v-for="p in paramList" :key="p.key">
|
||
|
|
<div class="param__head">
|
||
|
|
<span>{{ p.label }}</span>
|
||
|
|
<b class="param__val">{{ formatVal(p) }}</b>
|
||
|
|
</div>
|
||
|
|
<el-slider
|
||
|
|
v-model="p.model"
|
||
|
|
:min="p.min" :max="p.max" :step="p.step"
|
||
|
|
:show-tooltip="false"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="card">
|
||
|
|
<el-switch
|
||
|
|
v-model="isDragging"
|
||
|
|
size="large"
|
||
|
|
inline-prompt
|
||
|
|
active-text="开启地图选点"
|
||
|
|
inactive-text="关闭地图选点"
|
||
|
|
/>
|
||
|
|
<div v-if="isDragging">
|
||
|
|
<el-row :gutter="20">
|
||
|
|
<el-col :span="6">X:</el-col>
|
||
|
|
<el-col :span="18"><el-input /></el-col>
|
||
|
|
</el-row>
|
||
|
|
<el-row :gutter="20">
|
||
|
|
<el-col :span="6">Y:</el-col>
|
||
|
|
<el-col :span="18"><el-input /></el-col>
|
||
|
|
</el-row>
|
||
|
|
<el-row :gutter="20">
|
||
|
|
<el-col :span="6">Angle:</el-col>
|
||
|
|
<el-col :span="18"><el-input /></el-col>
|
||
|
|
</el-row>
|
||
|
|
<el-button type="danger" round size="large" class="estop" plain>
|
||
|
|
⬣ 紧急停止
|
||
|
|
</el-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</aside>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<footer class="status-bar">
|
||
|
|
<div class="chip" v-for="s in statusItems" :key="s.label">
|
||
|
|
<span class="chip__label">{{ s.label }}</span>
|
||
|
|
<span class="chip__val" :class="{ 'chip__val--ok': s.ok }">{{ s.value }}</span>
|
||
|
|
</div>
|
||
|
|
</footer>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
||
|
|
import { ZoomIn, ZoomOut, Location } from '@element-plus/icons-vue'
|
||
|
|
import { useRobotStore } from '@/stores/robot'
|
||
|
|
import { getCurrentMapName, moveRobot, stopRobot } from '@/api/agv.js'
|
||
|
|
import MapCanvas from './MapCanvas.vue'
|
||
|
|
|
||
|
|
const VITE_DEVICE_ID = import.meta.env.VITE_DEVICE_ID
|
||
|
|
|
||
|
|
const robotStore = useRobotStore()
|
||
|
|
|
||
|
|
const maxSpeed = ref(0.6)
|
||
|
|
const rotationAngle = ref(0)
|
||
|
|
|
||
|
|
const paramList = reactive([
|
||
|
|
{ key: 'speed', label: '速度', model: maxSpeed, min: 0, max: 1, step: 0.1, unit: 'm/s', digits: 1 },
|
||
|
|
{ key: 'rot', label: '旋转角度', model: rotationAngle, min: -1, max: 1, step: 0.1, unit: 'm/s', digits: 0 },
|
||
|
|
])
|
||
|
|
|
||
|
|
const statusItems = [
|
||
|
|
{ label: '位置', value: `${robotStore.position.x},${robotStore.position.y}` },
|
||
|
|
{ label: '航向', value: robotStore.position.angle },
|
||
|
|
{ label: '电量', value: `${robotStore.battery * 100} %`, ok: true },
|
||
|
|
]
|
||
|
|
|
||
|
|
function formatVal(p) {
|
||
|
|
const v = typeof p.model === 'object' ? p.model.value : p.model
|
||
|
|
return (typeof v === 'number' ? v.toFixed(p.digits) : v) + ' ' + p.unit
|
||
|
|
}
|
||
|
|
|
||
|
|
const isDragging = ref(false)
|
||
|
|
const moveRobotData = ref({
|
||
|
|
vx: 0,
|
||
|
|
vy: 0,
|
||
|
|
w: 0,
|
||
|
|
duration: -1
|
||
|
|
})
|
||
|
|
|
||
|
|
/* long-press stubs */
|
||
|
|
const onPress = async (_dir) => {
|
||
|
|
if (_dir === 'forward') {
|
||
|
|
moveRobotData.value.vx = maxSpeed.value
|
||
|
|
} else if (_dir === 'backward') {
|
||
|
|
moveRobotData.value.vx = -maxSpeed.value
|
||
|
|
} else if (_dir === 'left') {
|
||
|
|
moveRobotData.value.vy = maxSpeed.value
|
||
|
|
} else {
|
||
|
|
moveRobotData.value.vy = -maxSpeed.value
|
||
|
|
}
|
||
|
|
moveRobotData.value.w = rotationAngle.value
|
||
|
|
const res = await moveRobot({
|
||
|
|
ip: robotStore.ip,
|
||
|
|
deviceId: VITE_DEVICE_ID,
|
||
|
|
...moveRobotData.value
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
function onRelease() {
|
||
|
|
moveRobotData.value.vx = 0;
|
||
|
|
moveRobotData.value.vy = 0;
|
||
|
|
stopRobot({
|
||
|
|
ip: robotStore.ip,
|
||
|
|
deviceId: VITE_DEVICE_ID
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const mapName = ref('')
|
||
|
|
|
||
|
|
const getRobotCurrentMapName = async () => {
|
||
|
|
const result = await getCurrentMapName({
|
||
|
|
ip: robotStore.ip,
|
||
|
|
deviceId: VITE_DEVICE_ID,
|
||
|
|
})
|
||
|
|
if (result.code === 200) {
|
||
|
|
mapName.value = result.data.current_map
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => {
|
||
|
|
getRobotCurrentMapName()
|
||
|
|
})
|
||
|
|
onBeforeUnmount(() => { })
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style lang="scss" scoped>
|
||
|
|
.chassis-control {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
height: 100%;
|
||
|
|
background: $bg-deep;
|
||
|
|
color: $text;
|
||
|
|
font-family: $font-b;
|
||
|
|
overflow: hidden;
|
||
|
|
user-select: none;
|
||
|
|
|
||
|
|
.main-wrap {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
min-height: 0;
|
||
|
|
gap: 0;
|
||
|
|
|
||
|
|
.map-area {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
min-width: 0;
|
||
|
|
border-right: 1px solid $border;
|
||
|
|
|
||
|
|
.area-bar {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
height: 40px;
|
||
|
|
padding: 0 16px;
|
||
|
|
background: $bg-card;
|
||
|
|
border-bottom: 1px solid $border;
|
||
|
|
flex-shrink: 0;
|
||
|
|
|
||
|
|
&__title {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
font-family: $font-h;
|
||
|
|
font-size: 13px;
|
||
|
|
font-weight: 700;
|
||
|
|
letter-spacing: 1.5px;
|
||
|
|
color: $text-dim;
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
&__icon {
|
||
|
|
width: 16px;
|
||
|
|
height: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
&__actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.map-controller {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
|
||
|
|
.btn-container {
|
||
|
|
width: 32px;
|
||
|
|
height: 32px;
|
||
|
|
border-radius: 8px;
|
||
|
|
background: #1E3A5F;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
border: 1px solid transparent;
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
border-color: #00D4FF;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.el-button) {
|
||
|
|
font-size: 12px;
|
||
|
|
color: $text-dim;
|
||
|
|
border-color: $border;
|
||
|
|
background: transparent;
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
color: $accent;
|
||
|
|
border-color: $border-hi;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.map-body {
|
||
|
|
flex: 1;
|
||
|
|
position: relative;
|
||
|
|
overflow: hidden;
|
||
|
|
background: $bg-deep;
|
||
|
|
|
||
|
|
// canvas {
|
||
|
|
// position: absolute;
|
||
|
|
// inset: 0;
|
||
|
|
// }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel {
|
||
|
|
width: 360px;
|
||
|
|
flex-shrink: 0;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 0;
|
||
|
|
overflow-y: auto;
|
||
|
|
background: $bg-body;
|
||
|
|
|
||
|
|
.card {
|
||
|
|
padding: 18px 20px;
|
||
|
|
border-bottom: 1px solid $border;
|
||
|
|
|
||
|
|
&__head {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
|
||
|
|
h3 {
|
||
|
|
font-family: $font-h;
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 700;
|
||
|
|
letter-spacing: 1.2px;
|
||
|
|
color: $text-dim;
|
||
|
|
margin: 0;
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.el-tag) {
|
||
|
|
font-size: 10px;
|
||
|
|
height: 20px;
|
||
|
|
background: rgba(0,195,255,0.08);
|
||
|
|
border-color: $border;
|
||
|
|
color: $accent;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dpad {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(3, 76px);
|
||
|
|
grid-template-rows: repeat(3, 64px);
|
||
|
|
gap: 6px;
|
||
|
|
justify-content: center;
|
||
|
|
margin-bottom: 18px;
|
||
|
|
|
||
|
|
&__btn {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
gap: 4px;
|
||
|
|
border: 1px solid $border;
|
||
|
|
border-radius: 8px;
|
||
|
|
background: $bg-card;
|
||
|
|
color: $text;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: all .15s ease;
|
||
|
|
|
||
|
|
svg {
|
||
|
|
width: 14px;
|
||
|
|
height: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
small {
|
||
|
|
font-size: 10px;
|
||
|
|
letter-spacing: .5px;
|
||
|
|
color: $text-dim;
|
||
|
|
}
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
border-color: $border-hi;
|
||
|
|
background: $bg-card-hi;
|
||
|
|
color: $accent;
|
||
|
|
}
|
||
|
|
|
||
|
|
&:active {
|
||
|
|
background: $accent;
|
||
|
|
color: $bg-deep;
|
||
|
|
border-color: $accent;
|
||
|
|
box-shadow: 0 0 24px $accent-dim;
|
||
|
|
transform: scale(.94);
|
||
|
|
|
||
|
|
small {
|
||
|
|
color: $bg-deep;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
&--up {
|
||
|
|
grid-column: 2;
|
||
|
|
grid-row: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
&--lt {
|
||
|
|
grid-column: 1;
|
||
|
|
grid-row: 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
&--rt {
|
||
|
|
grid-column: 3;
|
||
|
|
grid-row: 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
&--dn {
|
||
|
|
grid-column: 2;
|
||
|
|
grid-row: 3;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
&__core {
|
||
|
|
grid-column: 2;
|
||
|
|
grid-row: 2;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
border: 1px solid $border;
|
||
|
|
border-radius: 8px;
|
||
|
|
background: rgba(0,195,255,0.03);
|
||
|
|
|
||
|
|
span {
|
||
|
|
width: 10px;
|
||
|
|
height: 10px;
|
||
|
|
border-radius: 50%;
|
||
|
|
background: $accent;
|
||
|
|
opacity: .3;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.estop {
|
||
|
|
width: 100%;
|
||
|
|
height: 42px !important;
|
||
|
|
font-size: 14px;
|
||
|
|
font-weight: 600;
|
||
|
|
letter-spacing: 1px;
|
||
|
|
border-color: $danger !important;
|
||
|
|
color: $danger !important;
|
||
|
|
background: rgba(255,45,85,0.06) !important;
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
background: rgba(255,45,85,0.14) !important;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.param {
|
||
|
|
margin-bottom: 18px;
|
||
|
|
|
||
|
|
&:last-child {
|
||
|
|
margin-bottom: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
&__head {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: baseline;
|
||
|
|
margin-bottom: 8px;
|
||
|
|
font-size: 13px;
|
||
|
|
color: $text-dim;
|
||
|
|
}
|
||
|
|
|
||
|
|
&__val {
|
||
|
|
font-family: $font-m;
|
||
|
|
font-size: 13px;
|
||
|
|
font-weight: 500;
|
||
|
|
color: $accent;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.el-slider) {
|
||
|
|
.el-slider__runway {
|
||
|
|
height: 4px;
|
||
|
|
background: rgba(255,255,255,0.05);
|
||
|
|
border-radius: 2px;
|
||
|
|
}
|
||
|
|
.el-slider__bar {
|
||
|
|
height: 4px;
|
||
|
|
background: $accent;
|
||
|
|
border-radius: 2px;
|
||
|
|
}
|
||
|
|
.el-slider__button-wrapper {
|
||
|
|
top: -16px;
|
||
|
|
}
|
||
|
|
.el-slider__button {
|
||
|
|
width: 14px;
|
||
|
|
height: 14px;
|
||
|
|
border: 2px solid $accent;
|
||
|
|
background: $bg-deep;
|
||
|
|
box-shadow: 0 0 8px $accent-dim;
|
||
|
|
transition: box-shadow .2s;
|
||
|
|
|
||
|
|
&:hover {
|
||
|
|
box-shadow: 0 0 14px rgba(0,195,255,0.35);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-bar {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 16px;
|
||
|
|
height: 38px;
|
||
|
|
padding: 0 24px;
|
||
|
|
background: $bg-card;
|
||
|
|
border-top: 1px solid $border;
|
||
|
|
flex-shrink: 0;
|
||
|
|
|
||
|
|
.chip {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 3px 12px;
|
||
|
|
border-radius: 4px;
|
||
|
|
background: rgba(255,255,255,0.02);
|
||
|
|
border: 1px solid $border;
|
||
|
|
|
||
|
|
&__label {
|
||
|
|
font-size: 10px;
|
||
|
|
color: $text-dim;
|
||
|
|
letter-spacing: .5px;
|
||
|
|
}
|
||
|
|
|
||
|
|
&__val {
|
||
|
|
font-family: $font-m;
|
||
|
|
font-size: 11px;
|
||
|
|
color: $text-bright;
|
||
|
|
font-weight: 500;
|
||
|
|
|
||
|
|
&--ok {
|
||
|
|
color: $success;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
</style>
|