on-site-robots-screen/src/views/home/components/control-deck-new.vue

1037 lines
30 KiB
Vue

<template>
<!-- 机器人操控面板 -->
<div class="control-deck child-container">
<!-- 第一行 -->
<n-grid :cols="24">
<n-grid-item :span="9">
<div class="row-1-item">摄像头</div>
</n-grid-item>
<n-grid-item :span="6">
<!-- <div class="row-1-item">升降杆</div> -->
<div
class="row-1-item"
style="display: flex; align-items: center; justify-content: center"
>
<n-icon size="26">
<!-- 绿色 -->
<BatteryFullSharp
color="#16E607"
v-if="robotBaseInfo.dl >= 80 && robotBaseInfo.type != '充电中'"
/>
<BatteryHalfSharp
color="#E69807"
v-else-if="robotBaseInfo.dl >= 50 && robotBaseInfo.type != '充电中'"
/>
<BatteryDeadSharp
color="#F90303"
v-else-if="robotBaseInfo.type != '充电中'"
/>
<BatteryChargingSharp
color="#16E607"
v-if="robotBaseInfo.type == '充电中'"
/>
</n-icon>
<span style="margin-left: 4px; font-size: 13px"> {{ robotBaseInfo.dl }}%</span>
</div>
</n-grid-item>
<n-grid-item :span="9">
<div class="row-1-item">底盘</div>
</n-grid-item>
</n-grid>
<!-- 第二行 -->
<n-grid :cols="24">
<n-grid-item :span="9">
<div class="row-2-item">
<div>速度</div>
<div>30</div>
</div>
</n-grid-item>
<n-grid-item :span="6">
<!-- <div class="row-2-item">
<div>高度</div>
<div>
{{ currentHeight }}
</div>
</div> -->
<div class="row-2-item">
<div></div>
<div style="width: 70px">
{{ robotBaseInfo.type }}
</div>
</div>
</n-grid-item>
<n-grid-item :span="9">
<div class="row-2-item">
<div>速度</div>
<div>30</div>
</div>
</n-grid-item>
</n-grid>
<!-- 第三行 -->
<n-grid :cols="10">
<n-grid-item :span="5">
<div class="row-3-item-1">
<!-- 上下左右控制按钮 -->
<img
class="arrow-top hand-direction"
@mousedown="handleChangeCamera('up')"
@mouseup="handleChangeCamera('stop')"
@mouseleave="handleChangeCamera('stop')"
src="@/assets/home-imgs/control-2-arrow.png"
/>
<img
class="arrow-right hand-direction"
@mousedown="handleChangeCamera('right')"
@mouseup="handleChangeCamera('stop')"
@mouseleave="handleChangeCamera('stop')"
src="@/assets/home-imgs/control-2-arrow.png"
/>
<img
class="arrow-bottom hand-direction"
@mousedown="handleChangeCamera('down')"
@mouseup="handleChangeCamera('stop')"
@mouseleave="handleChangeCamera('stop')"
src="@/assets/home-imgs/control-2-arrow.png"
/>
<img
class="arrow-left hand-direction"
@mousedown="handleChangeCamera('left')"
@mouseup="handleChangeCamera('stop')"
@mouseleave="handleChangeCamera('stop')"
src="@/assets/home-imgs/control-2-arrow.png"
/>
<!-- 中间的按钮 -->
<div class="row-3-item-1-center">
<img class="center-icon" :src="isStopLeft ? stopImg : startImg" />
</div>
<!-- 描述文字 -->
<div class="text-top description-text">正前</div>
<div class="text-bottom description-text">正后</div>
<div class="text-left description-text">正左</div>
<div class="text-right description-text">正右</div>
<!-- 增加减少按钮 -->
<div class="add-reduce-btn">
<span>缩放</span>
<img
style="margin-bottom: 2px"
@click="handleChangeZoomCamera('ZoomIn')"
src="@/assets/home-imgs/control-2-add.png"
/>
<img
@click="handleChangeZoomCamera('ZoomOut')"
src="@/assets/home-imgs/control-2-reduce.png"
/>
</div>
</div>
</n-grid-item>
<n-grid-item :span="3" v-if="false">
<div class="row-3-item-2" ref="container">
<!-- 上下滑动调节 -->
<span
ref="draggable"
class="up-down-box"
@mousedown="startDrag"
:style="{ top: currentTop + 'px' }"
/>
</div>
</n-grid-item>
<n-grid-item :span="5">
<div class="row-3-item-3">
<!-- 上下左右控制按钮 -->
<img
class="arrow-top"
@mousedown="handleChangeRobot('5')"
@mouseup="handleChangeRobot(9)"
@mouseleave="handleChangeRobot(9)"
src="@/assets/home-imgs/control-2-arrow.png"
/>
<img
class="arrow-right"
@mousedown="handleChangeRobot('8')"
@mouseup="handleChangeRobot(9)"
@mouseleave="handleChangeRobot(9)"
src="@/assets/home-imgs/control-2-arrow.png"
/>
<img
class="arrow-bottom"
@mousedown="handleChangeRobot('6')"
@mouseup="handleChangeRobot(9)"
@mouseleave="handleChangeRobot(9)"
src="@/assets/home-imgs/control-2-arrow.png"
/>
<img
class="arrow-left"
@mousedown="handleChangeRobot('7')"
@mouseup="handleChangeRobot(9)"
@mouseleave="handleChangeRobot(9)"
src="@/assets/home-imgs/control-2-arrow.png"
/>
<!-- 描述文字 -->
<div class="text-top description-text">前进</div>
<div class="text-bottom description-text">后退</div>
<div class="text-left description-text">左转</div>
<div class="text-right description-text">右转</div>
<div class="row-3-item-1-center">
<img class="center-icon" :src="isStopRight ? startImg : stopImg" />
</div>
</div>
</n-grid-item>
</n-grid>
<!-- 第四行 -->
<n-grid :cols="12" class="row-4">
<n-grid-item :span="3">
<div>警灯</div>
</n-grid-item>
<n-grid-item :span="3">
<div>
<n-radio
value="关"
name="basic-demo"
@change="handleChange1"
:checked="checkedValue === '关'"
>
</n-radio>
</div>
</n-grid-item>
<n-grid-item :span="3">
<div>
<n-radio
value="闪烁"
name="basic-demo"
@change="handleChange2"
:checked="checkedValue === '闪烁'"
>
闪烁
</n-radio>
</div>
</n-grid-item>
<n-grid-item :span="3">
<div class="row-4-item">
<n-radio
value="运动时闪烁"
name="basic-demo"
@change="handleChange3"
:checked="checkedValue === '运动时闪烁'"
>
运动时闪烁
</n-radio>
</div>
</n-grid-item>
</n-grid>
<!-- 第五行 -->
<n-grid :cols="12" class="row-5">
<n-grid-item :span="3">
<div>录音播放</div>
</n-grid-item>
<n-grid-item :span="3">
<div>
<div>
<!-- <n-select
size="tiny"
v-model:value="selectValue"
:options="selectOptions"
/> -->
<!-- <n-select
size="small"
v-model:value="selectValue"
:options="selectOptions"
/> -->
<!-- <n-select
:options="selectOptions"
size="small"
:dropdown-style="{
borderRadius: '4px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
color: '#fff',
}"
/> -->
<!-- <n-button type="info" size="small">请上传</n-button> -->
<n-upload
:accept="audioMimeTypes"
:show-file-list="false"
:default-upload="false"
@before-upload="beforeUpload"
@change="handleChangeUpload"
v-if="!audioInfo"
>
<n-button type="info" size="small">请上传</n-button>
</n-upload>
<n-popselect
trigger="click"
v-model:value="audioValue"
:options="audioOptions"
@update:value="handleChangeAudio"
v-else
>
<n-button type="info" size="small">
{{ audioInfo?.fileName }}
</n-button>
</n-popselect>
</div>
<!-- <div style="margin-top: 16px">
<n-radio
:checked="checkedValue === '循环播放'"
value="循环播放"
name="basic-demo"
@change="handleChange4"
>
循环播放
</n-radio>
</div> -->
</div>
</n-grid-item>
<n-grid-item :span="6">
<div class="row-5-item">
<div class="row-5-item-1">
<img src="@/assets/home-imgs/control-3-voice.png" alt="" />
<div style="width: 60%">
<n-slider
:step="10"
:default-value="50"
:format-tooltip="formatTooltip"
@update:value="handleChangeVolume"
/>
</div>
</div>
<!-- <div class="row-5-item-1" style="margin-top: 16px">
<img src="@/assets/home-imgs/control-3-video.png" alt="" />
<div style="width: 60%">
<n-slider
:default-value="50"
:step="10"
:format-tooltip="formatTooltip"
/>
</div>
</div> -->
</div>
</n-grid-item>
</n-grid>
<!-- 第六行 -->
<n-grid :cols="10" class="row-6">
<n-grid-item :span="4">
<div class="row-6-item" @click="handleChangeRobot(10)">回桩充电</div>
</n-grid-item>
<n-grid-item :span="3">
<div class="row-6-item-1">
<img src="@/assets/home-imgs/control-3-mk.png" alt="" />
</div>
</n-grid-item>
</n-grid>
</div>
<n-modal v-model:show="selectPlayType">
<n-card
size="huge"
role="dialog"
aria-modal="true"
:bordered="false"
class="modal-container"
>
<n-form
ref="formRef"
size="small"
label-placement="left"
style="margin-top: 10px"
:model="playParams"
>
<n-form-item label="播放类型:">
<n-radio-group v-model:value="playParams.playType" name="radiogroup">
<n-space>
<n-radio :value="1"> 次数播放 </n-radio>
<n-radio :value="0"> 循环播放 </n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="播放次数:" v-if="playParams.playType === 1">
<n-input-number
v-model:value="playParams.playCount"
placeholder="输入播放次数"
:show-button="false"
/>
</n-form-item>
<n-button type="info" @click="handlePlayAudio"> 确定</n-button>
<n-button color="#6E90A9" style="margin-left: 8px" @click="selectPlayType = false">
取消
</n-button>
</n-form>
</n-card>
</n-modal>
</template>
<script setup>
import { ref, onMounted, h } from 'vue'
import {
stopDeviceCameraApi,
changeDeviceCameraApi,
stopDeviceCameraZoomApi,
changeDeviceCameraZoomInApi,
changeDeviceCameraZoomOutApi,
} from '@/utils/initLogin'
import { handleRobotActionApi, uploadAudioApi, getAudioInfoApi, deleteAudioApi } from '@/api/home'
import { useRobotDataStore } from '@/store/robot'
import { getRobotTokenFn, getRobotDeviceListFn } from '@/utils/getRobotInfo.js'
import {
BatteryHalfSharp,
BatteryDeadSharp,
BatteryFullSharp,
BatteryChargingSharp,
} from '@vicons/ionicons5'
import { useMessage, useDialog, NRadio, NRadioGroup, NInputNumber, NSpace } from 'naive-ui'
import { debounce } from 'lodash'
import stopImg from '@/assets/home-imgs/control-2-stop.png'
import startImg from '@/assets/home-imgs/control-2-start.png'
const dialog = useDialog()
const message = useMessage()
const robotData = useRobotDataStore()
const isStopLeft = ref(true)
const isStopRight = ref(false)
const upDownHeight = ref(30)
const container = ref(null)
const draggable = ref(null)
const isDragging = ref(false)
const currentTop = ref(0)
const startY = ref(0)
const startTop = ref(0)
const checkedValue = ref('')
const selectValue = ref('')
const deviceToken = ref('')
const deviceInfo = ref({})
const isZoom = ref(false)
const robotBaseInfo = ref({})
const selectPlayType = ref(false)
const audioValue = ref('')
const audioInfo = ref(null)
const playParams = ref({
playType: 1,
playCount: 1,
})
const audioMimeTypes = ref(
'.pcm,.wav,.aac,audio/wav,audio/x-wav,audio/x-aiff,audio/aac,audio/x-aac',
)
const audioOptions = ref([
{
label: '播放',
value: '播放',
},
{
label: '删除',
value: '删除',
},
{
label: '暂停',
value: '暂停',
},
])
const beforeUpload = ({ file }) => {
const allowedExtensions = ['.pcm', '.wav', '.aac']
const fileName = file.name.toLowerCase()
const isValid = allowedExtensions.some((ext) => fileName.endsWith(ext))
if (!isValid) {
message.error('只能上传 PCM、WAV、AAC 格式的音频文件')
return false
}
return true
}
// 上传音频
const handleChangeUpload = async (e) => {
const file = e.file
const formData = new FormData()
formData.append('file', file.file)
formData.append('puid', deviceInfo.value?.puId)
const { data: res } = await uploadAudioApi(formData)
if (res.code == 200) {
message.success('上传音频文件成功')
getAudioInfo()
} else {
message.error('上传音频文件失败')
}
}
// 获取音频信息
const getAudioInfo = async () => {
const { data: res } = await getAudioInfoApi({
puid: deviceInfo.value?.puId,
})
if (res.code == 200) {
audioInfo.value = res?.data
}
}
const handleChangeAudio = async (e) => {
// console.log(e, '选择音频操作')
if (e === '播放') {
// showPlayTypeDialog()
selectPlayType.value = true
} else if (e === '删除') {
const { data: res } = await deleteAudioApi({
puid: robotData.robotInfo?.puId,
id: audioInfo.value?.id,
})
if (res.code == 200) {
message.success('删除音频成功')
getAudioInfo()
} else {
message.error('删除音频失败')
}
} else {
const { data: res } = await handleRobotActionApi({
puId: robotData.robotInfo?.puId,
type: '17',
enable: '0',
videoId: audioInfo.value?.id,
})
if (res.code == 200) {
message.success('已暂停')
} else {
message.error('暂停音频失败')
}
}
}
// 计算当前高度百分比(0-100)
const currentHeight = computed(() => {
if (!container.value) return 0
const containerHeight = container.value.offsetHeight
const draggableHeight = draggable.value?.offsetHeight || 0
const maxTop = containerHeight - draggableHeight - 20 // 20是底部初始偏移
// 计算百分比 (0-100)
return Math.round(100 - (currentTop.value / maxTop) * 100)
})
const startDrag = (e) => {
isDragging.value = true
startY.value = e.clientY
startTop.value = currentTop.value
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
}
const onDrag = (e) => {
if (!isDragging.value) return
const deltaY = e.clientY - startY.value
let newTop = startTop.value + deltaY
const containerHeight = container.value.offsetHeight
const draggableHeight = draggable.value.offsetHeight
const maxTop = containerHeight - draggableHeight - 20
newTop = Math.max(0, Math.min(newTop, maxTop))
currentTop.value = newTop
}
const stopDrag = () => {
isDragging.value = false
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
// 拖动结束时可以在这里使用currentHeight.value
console.log('当前高度:', currentHeight.value)
}
onMounted(() => {
// const containerHeight = container.value.offsetHeight
// const draggableHeight = draggable.value.offsetHeight
// const maxTop = containerHeight - draggableHeight - 20
// currentTop.value = maxTop * (1 - upDownHeight.value / 100)
})
const handleChange1 = (e) => {
console.log(e)
checkedValue.value = e.target.value
}
const handleChange2 = (e) => {
console.log(e)
checkedValue.value = e.target.value
}
const handleChange3 = (e) => {
checkedValue.value = e.target.value
}
const handleChange4 = (e) => {
checkedValue.value = e.target.value
}
const formatTooltip = (value) => {
return `${value}%`
}
// 停止缩放
const handleStopDeviceCameraZoom = async () => {
isZoom.value = false
const res = await stopDeviceCameraZoomApi({
idx: 0,
token: deviceToken.value,
puid: '201115200268437643',
})
console.log(res, '停止位置---')
}
// 操控机器人上方的摄像头
const handleChangeCamera = debounce(async (motion) => {
if (motion === 'stop' && isZoom.value) {
isStopLeft.value = true
handleStopDeviceCameraZoom()
return
}
if (motion === 'stop') {
isStopLeft.value = true
} else {
isStopLeft.value = false
}
if (!deviceInfo.value?.puId) {
message.error('当前机器人未连接', {
duration: 1000,
})
return
}
const res = await changeDeviceCameraApi({
token: deviceToken.value,
puid: deviceInfo.value?.puId,
idx: 0,
motion,
})
}, 1000)
// 球机缩放
const handleChangeZoomCamera = async (motion) => {
isStopLeft.value = false
isZoom.value = true
if (motion === 'ZoomIn') {
const res = await changeDeviceCameraZoomInApi({
token: deviceToken.value,
puid: deviceInfo.value?.puId,
idx: 0,
})
} else {
const res = await changeDeviceCameraZoomOutApi({
token: deviceToken.value,
puid: deviceInfo.value?.puId,
idx: 0,
})
}
}
// 操控机器人
const handleChangeRobot = debounce(async (type) => {
// type 5 前进 6 后退 7 左转 8 右转
if (!robotData.robotInfo?.puId) {
message.error('当前机器人未连接', {
duration: 1000,
})
return
}
const params = {
puId: robotData.robotInfo?.puId,
type,
}
console.log(params, 'params---')
const res = await handleRobotActionApi(params)
console.log(res, '操控机器人---')
if (res.code == 200 && type != 9) {
isStopRight.value = true
}
if (res.code == 200 && type == 9) {
isStopRight.value = false
}
}, 1000)
// 停止机器人
const handleStopRobot = async () => {
console.log('停止机器人')
}
// 获取机器人的基础信息
const getRobotBaseInfo = async () => {
const { data: res } = await handleRobotActionApi({
puId: deviceInfo.value?.puId,
type: '1',
})
robotBaseInfo.value = res.data
}
// 确认播放
const handlePlayAudio = async () => {
const loop = playParams.value.playType === 0 ? 0 : playParams.value.playCount
const { data: res } = await handleRobotActionApi({
puId: robotData.robotInfo?.puId,
type: '17',
loop,
enable: '1',
videoId: audioInfo.value?.id,
})
if (res.code == 200) {
message.success('播放音频成功')
selectPlayType.value = false
} else {
message.error('播放音频失败')
}
}
// 音量选择
const handleChangeVolume = debounce(async (value) => {
const { data: res } = await handleRobotActionApi({
puId: robotData.robotInfo?.puId,
type: '18',
sound: value,
videoId: audioInfo.value?.id,
})
if (res.code == 200) {
message.success('音量调整成功')
} else {
message.error('音量调整失败')
}
}, 1000)
onMounted(async () => {
const token = await getRobotTokenFn() // 获取设备token
const device = await getRobotDeviceListFn() // 获取设备信息
deviceToken.value = token
deviceInfo.value = device
getRobotBaseInfo()
getAudioInfo()
})
</script>
<style lang="scss" scoped>
.control-deck {
justify-content: space-between;
}
.row-1-item,
.row-2-item {
width: 90px;
margin: 0 auto;
}
.row-1-item {
height: 24px;
background: url('@/assets/home-imgs/control-1-bg.png') no-repeat center center;
background-size: 100% 100%;
line-height: 24px;
text-align: center;
}
.row-2-item {
// margin-top: 6px;
display: flex;
align-items: center;
justify-content: space-around;
& div:last-child {
// width: 32px;
height: 16px;
background-color: #005899;
border-radius: 6px;
font-size: 12px;
text-align: center;
line-height: 16px;
padding: 2px 10px;
}
}
.row-3-item-1,
.row-3-item-3 {
width: 68px;
height: 68px;
margin: 20px auto 0;
position: relative;
background: url('@/assets/home-imgs/control-2-round.png') no-repeat center center;
background-size: 100% 100%;
> img {
width: 16px;
height: 18px;
cursor: pointer;
object-fit: contain;
}
// // 增加一个移入时的放大平滑动画效果
// .hand-direction:hover {
// transform: scale(1.1);
// transition: transform 0.3s ease-in-out;
// }
.arrow-top {
position: absolute;
top: 1px;
left: 50%;
transform: translateX(-50%) rotate(-90deg);
}
.arrow-right {
position: absolute;
top: 50%;
right: 1px;
transform: translateY(-50%);
}
.arrow-bottom {
position: absolute;
bottom: 1px;
left: 50%;
transform: translateX(-50%) rotate(90deg);
}
.arrow-left {
position: absolute;
top: 50%;
left: 1px;
transform: translateY(-50%) rotate(-180deg);
}
.row-3-item-1-center {
width: 16px;
height: 16px;
position: absolute;
top: 50%;
left: 50%;
display: flex;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%) rotate(45deg);
border: 2px solid #7fc0ff;
border-radius: 4px;
background-color: #2f78bf;
cursor: pointer;
> img {
width: 9px;
height: 9px;
object-fit: contain;
transform: rotate(-45deg);
}
}
// 描述文字
.description-text {
position: absolute;
font-size: 12px;
}
.text-top {
left: 50%;
top: -28px;
transform: translateX(-50%);
}
.text-bottom {
left: 50%;
bottom: -28px;
transform: translateX(-50%);
}
.text-top,
.text-bottom {
width: 36px;
height: 22px;
border: 1px solid #7fc0ff;
border-radius: 4px;
text-align: center;
line-height: 22px;
}
.text-left {
left: -28px;
top: 50%;
transform: translateY(-50%);
}
.text-right {
right: -28px;
top: 50%;
transform: translateY(-50%);
}
.text-left,
.text-right {
height: 36px;
width: 22px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #7fc0ff;
border-radius: 4px;
// 使文字竖向展示 并垂直居中
writing-mode: vertical-rl;
text-orientation: upright;
}
.add-reduce-btn {
position: absolute;
right: -58px;
top: -16px;
// height: 100px;
width: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
span {
font-size: 13px;
width: 30px;
text-align: center;
color: #7fc0ff;
}
img {
width: 20px;
height: 50px;
object-fit: contain;
cursor: pointer;
}
}
}
.row-3-item-3 {
width: 68px;
height: 68px;
margin: 20px auto 0 !important;
> img {
width: 16px;
height: 18px;
cursor: pointer;
object-fit: contain;
}
}
.row-3-item-2 {
width: 60px;
height: 120px;
margin: 32px auto 0 !important;
background: url('@/assets/home-imgs/control-2-up-down.png') no-repeat center center;
background-size: 100% 100%;
position: relative;
user-select: none;
.up-down-box {
position: absolute;
left: 50%;
width: 30px;
height: 10px;
background: url('@/assets/home-imgs/control-2-up-down-hand.png') no-repeat center center;
background-size: 100% 100%;
transform: translateX(-50%);
cursor: pointer;
touch-action: none;
}
}
.row-4 {
margin-top: 10px;
.row-4-item {
text-align: center;
}
}
.row-5-item-1 {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
> img {
width: 16px;
height: 16px;
margin-right: 10px;
object-fit: contain;
}
}
.row-6 {
// margin-top: 20px;
display: flex;
align-items: center;
}
.row-6-item {
padding: 2px 8px;
display: inline-block;
background-color: #005db6;
border: 1px solid #7fc0ff;
border-radius: 4px;
cursor: pointer;
}
.row-6-item-1 {
text-align: center;
img {
width: 28px;
height: 40px;
object-fit: contain;
cursor: pointer;
}
}
:deep(.n-radio__label) {
color: #fff !important;
}
:deep(.n-radio__dot) {
background: transparent;
border: 1px solid #7fc0ff;
box-shadow: none;
}
:deep(.n-radio__dot::before) {
background: #7fc0ff;
}
// :deep(.n-base-selection .n-base-selection-label) {
// background-color: #054a92;
// color: #fff;
// }
:deep(.n-slider .n-slider-rail .n-slider-rail__fill) {
background-color: #7fc0ff;
}
:deep(.n-slider .n-slider-handles .n-slider-handle-wrapper .n-slider-handle) {
background-color: #7fc0ff;
}
:deep(.n-slider .n-slider-rail) {
background-color: #2d6098;
}
.modal-container {
height: 40vh;
width: 30%;
display: flex;
flex-direction: column;
background: url('@/assets/home-imgs/modal-bg.png') no-repeat center center;
background-size: 100% 100%;
}
</style>