You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
152 lines
8.0 KiB
152 lines
8.0 KiB
<template>
|
|
<section class="control-page">
|
|
<header class="control-page-header">
|
|
<h1>远程控制</h1>
|
|
<span>对机巢与无人机下发远程指令</span>
|
|
</header>
|
|
|
|
<div class="control-workspace">
|
|
<aside class="control-device-list">
|
|
<div class="control-list-heading">
|
|
<div><h2>设备列表</h2><span>选择要控制的设备</span></div>
|
|
<button class="icon-button" @click="ui.toast('刷新设备')"><svg><use href="#i-refresh" /></svg></button>
|
|
</div>
|
|
<div class="control-device-items">
|
|
<button
|
|
v-for="d in controlDevices"
|
|
:key="d.id"
|
|
class="control-device-item"
|
|
:class="{ active: selectedId === d.id }"
|
|
@click="selectedId = d.id"
|
|
>
|
|
<span class="control-device-icon"><svg><use :href="d.kind === 'drone' ? '#i-plane' : '#i-home'" /></svg></span>
|
|
<div><strong>{{ d.name }}</strong><small>{{ d.code }} · {{ d.location || d.binding }}</small></div>
|
|
<em class="status" :class="d.statusClass">{{ d.status }}</em>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="control-main">
|
|
<template v-if="selected">
|
|
<header class="control-selected-header">
|
|
<div>
|
|
<h2>{{ selected.name }}</h2>
|
|
<p>{{ selected.kind === 'drone' ? '巡检无人机' : '机巢设备' }} · {{ selected.code }}</p>
|
|
</div>
|
|
<em class="status" :class="selected.statusClass">{{ selected.status }}</em>
|
|
</header>
|
|
|
|
<div class="control-status-band">
|
|
<template v-if="selected.kind === 'drone'">
|
|
<div><span>剩余电量</span><strong>{{ num(selected.battery) }}%</strong></div>
|
|
<div><span>飞行模式</span><strong>{{ selected.mode }}</strong></div>
|
|
<div><span>定位状态</span><strong>{{ selected.gps }}</strong></div>
|
|
<div><span>当前任务</span><strong>{{ selected.mission }}</strong></div>
|
|
<div><span>更新时间</span><strong>{{ selected.updated }}</strong></div>
|
|
</template>
|
|
<template v-else>
|
|
<div><span>绑定状态</span><strong>{{ selected.bindingState === 'bound' ? '已绑定' : '未绑定' }}</strong></div>
|
|
<div><span>绑定设备</span><strong>{{ selected.binding }}</strong></div>
|
|
<div><span>工作模式</span><strong>{{ selected.mode }}</strong></div>
|
|
<div><span>软件版本</span><strong>{{ selected.version }}</strong></div>
|
|
<div><span>通信协议</span><strong>{{ selected.protocol }}</strong></div>
|
|
</template>
|
|
</div>
|
|
|
|
<section class="control-alert-section">
|
|
<div class="control-alert-content" :class="selected.statusClass">
|
|
<span class="control-alert-icon" :class="{ ok: selected.statusClass === 'online' }"><svg><use href="#i-alert" /></svg></span>
|
|
<div>
|
|
<strong>{{ alertTitle }}</strong>
|
|
<small>{{ alertDesc }}</small>
|
|
</div>
|
|
<button class="plain-command" @click="ui.toast('查看详情')">详情</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="control-section">
|
|
<div class="data-section-title"><h3 style="margin:0;font-size:12px">控制指令</h3><span>下发前将进行安全检查</span></div>
|
|
<div class="control-command-grid" v-if="selected.kind === 'drone'">
|
|
<button class="control-command primary" @click="run('起飞', '无人机将立即起飞进入待命状态。')"><svg><use href="#i-plane" /></svg><span><strong>起飞</strong><small>开始任务</small></span></button>
|
|
<button class="control-command landing" @click="run('降落', '无人机将就地降落并停止任务。')"><svg><use href="#i-plane" /></svg><span><strong>降落</strong><small>结束任务</small></span></button>
|
|
<button class="control-command" @click="run('一键返航', '无人机将返回绑定机巢。')"><b>R</b><span><strong>一键返航</strong><small>自动回巢</small></span></button>
|
|
<button class="control-command" @click="run('悬停', '无人机将在当前位置悬停。')"><b>H</b><span><strong>悬停</strong><small>保持位置</small></span></button>
|
|
<button class="control-command danger" @click="run('紧急停止', '立即中止飞行,无人机原地悬停待命。')"><b>E</b><span><strong>紧急停止</strong><small>中止任务</small></span></button>
|
|
</div>
|
|
<div class="control-command-grid" v-else>
|
|
<button class="control-command primary" @click="run('一键起飞', '无人机将从机巢起飞并进入待命状态。')"><svg><use href="#i-plane" /></svg><span><strong>一键起飞</strong><small>自动开舱</small></span></button>
|
|
<button class="control-command" @click="run('打开舱门', '将打开机巢舱门。')"><b>01</b><span><strong>打开舱门</strong><small>开合机构</small></span></button>
|
|
<button class="control-command" @click="run('关闭舱门', '将关闭机巢舱门。')"><b>02</b><span><strong>关闭舱门</strong><small>开合机构</small></span></button>
|
|
<button class="control-command" @click="run('机巢复位', '机巢将执行复位自检。')"><b>03</b><span><strong>机巢复位</strong><small>状态复位</small></span></button>
|
|
<button class="control-command" @click="run('设备自检', '对机巢各子系统执行自检。')"><b>04</b><span><strong>设备自检</strong><small>健康诊断</small></span></button>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="control-section control-note">
|
|
<svg><use href="#i-shield" /></svg>
|
|
<span><strong>安全提示</strong><small>所有远程指令均会经过设备在线校验与指令互斥检查,确认通过后才会下发执行。</small></span>
|
|
</section>
|
|
</template>
|
|
|
|
<div v-else class="table-empty" style="height:100%;display:grid;place-items:center;color:var(--muted);font-size:12px">请在左侧选择设备</div>
|
|
</main>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { useDeviceStore } from '../store/devices'
|
|
import { useUiStore } from '../store/ui'
|
|
|
|
const route = useRoute()
|
|
const devices = useDeviceStore()
|
|
const ui = useUiStore()
|
|
|
|
const controlDevices = computed(() => [...devices.drones, ...devices.docks])
|
|
const selectedId = ref(route.params.id || null)
|
|
const selected = computed(() => (selectedId.value ? devices.record(selectedId.value) : null))
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
await devices.load()
|
|
if (!selectedId.value && devices.docks.length) selectedId.value = devices.docks[0].id
|
|
} catch (e) {
|
|
ui.toast(e.message || '加载设备失败')
|
|
}
|
|
})
|
|
|
|
const alertTitle = computed(() => {
|
|
if (!selected.value) return ''
|
|
if (selected.value.statusClass === 'offline') return '设备离线,无法下发指令'
|
|
if (selected.value.statusClass === 'alarm') return '设备存在未处理告警'
|
|
if (selected.value.statusClass === 'mission') return '设备正在执行任务'
|
|
return '设备状态正常,可下发指令'
|
|
})
|
|
const alertDesc = computed(() => {
|
|
if (!selected.value) return ''
|
|
if (selected.value.statusClass === 'offline') return '请检查通信链路与供电状态'
|
|
if (selected.value.statusClass === 'alarm') return '建议先处理告警再执行控制操作'
|
|
if (selected.value.statusClass === 'mission') return '部分指令将在当前任务结束后生效'
|
|
return '所有子系统运行正常'
|
|
})
|
|
|
|
function num(v) {
|
|
if (v == null) return '--'
|
|
return String(v).replace('%', '')
|
|
}
|
|
|
|
async function run(title, desc) {
|
|
const rec = selected.value
|
|
if (!rec) return
|
|
const ok = await ui.confirm(title, desc)
|
|
if (!ok) return
|
|
try {
|
|
await devices.sendCommand(rec, title)
|
|
ui.toast(`${title}指令已下发`)
|
|
} catch (e) {
|
|
ui.toast(e.message || '指令下发失败')
|
|
}
|
|
}
|
|
</script>
|
|
|