2 changed files with 404 additions and 183 deletions
@ -0,0 +1,289 @@ |
|||||
|
<template> |
||||
|
<aside class="detail-panel" :class="{ 'drone-selected': selected && header?.isDrone }"> |
||||
|
<template v-if="selected && header"> |
||||
|
<header class="detail-header"> |
||||
|
<div> |
||||
|
<span class="eyebrow">{{ header.kindLabel }}</span> |
||||
|
<h2>{{ header.name }}</h2> |
||||
|
<p><svg><use href="#i-map-pin" /></svg>{{ header.location }}</p> |
||||
|
</div> |
||||
|
<div class="detail-head-actions"> |
||||
|
<i class="status" :class="header.statusClass">{{ header.status }}</i> |
||||
|
<button class="icon-button" title="查看设备详情" @click="$emit('open-device-detail')"><svg><use href="#i-maximize" /></svg></button> |
||||
|
<button class="icon-button" title="关闭详情" @click="$emit('close')"><svg><use href="#i-x" /></svg></button> |
||||
|
</div> |
||||
|
</header> |
||||
|
|
||||
|
<div class="asset-relation" v-if="header.isDrone"> |
||||
|
<span><svg><use href="#i-home" /></svg>所属机巢</span> |
||||
|
<button v-if="relation?.parentId" @click="$emit('select-parent', relation.parentId)">{{ relation.parentName }}<svg><use href="#i-chevron" /></svg></button> |
||||
|
<button v-else disabled>{{ relation?.parentName || '未绑定' }}</button> |
||||
|
</div> |
||||
|
|
||||
|
<section class="video-section"> |
||||
|
<div class="section-title"> |
||||
|
<h3>实时画面</h3> |
||||
|
<div> |
||||
|
<span class="live-dot" :class="{ active: liveView.active }" /> |
||||
|
<b>{{ liveView.phaseText }}</b> |
||||
|
<button class="icon-button" type="button" title="全屏播放" :disabled="!liveView.canFullscreen" @click="$emit('fullscreen-live')"> |
||||
|
<svg><use href="#i-maximize" /></svg> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<LivePlayer |
||||
|
ref="livePlayerRef" |
||||
|
:play-url="liveView.playUrl" |
||||
|
:phase="liveView.phase" |
||||
|
:active="liveView.active" |
||||
|
:camera-label="liveView.cameraLabel" |
||||
|
@toggle="$emit('toggle-live')" |
||||
|
@error="$emit('live-error')" |
||||
|
/> |
||||
|
</section> |
||||
|
|
||||
|
<section v-if="!header.isDrone && dockStatus" class="status-section"> |
||||
|
<div class="section-title"> |
||||
|
<h3>运行状态</h3> |
||||
|
<button class="text-button" type="button" @click="$emit('open-dock-status')">查看详情</button> |
||||
|
</div> |
||||
|
<div class="metric-grid"> |
||||
|
<div><svg><use href="#i-battery" /></svg><span>无人机电量</span><strong>{{ dockStatus.droneBattery }}</strong></div> |
||||
|
<div><svg><use href="#i-zap" /></svg><span>充电状态</span><strong :class="{ success: dockStatus.chargeActive }">{{ dockStatus.chargeText }}</strong></div> |
||||
|
<div><svg><use href="#i-door" /></svg><span>舱门状态</span><strong>{{ dockStatus.doorSummary }}</strong></div> |
||||
|
<div><svg><use href="#i-satellite" /></svg><span>控制模式</span><strong>{{ dockStatus.controlMode }}</strong></div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section v-if="header.isDrone && flightStatus" class="status-section"> |
||||
|
<div class="section-title"> |
||||
|
<h3>飞行状态</h3> |
||||
|
<span class="mission-state">{{ flightStatus.missionState }}</span> |
||||
|
</div> |
||||
|
<div class="metric-grid drone-metrics"> |
||||
|
<div><svg><use href="#i-battery" /></svg><span>飞行电量</span><strong>{{ flightStatus.battery }}</strong></div> |
||||
|
<div><svg><use href="#i-plane" /></svg><span>相对高度</span><strong>{{ flightStatus.altitudeText }}</strong></div> |
||||
|
<div><svg><use href="#i-route" /></svg><span>地速</span><strong>{{ flightStatus.speedText }}</strong></div> |
||||
|
<div><svg><use href="#i-satellite" /></svg><span>卫星数量</span><strong>{{ flightStatus.satellitesText }}</strong></div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section v-if="header.isDrone && mission" class="mission-progress"> |
||||
|
<div class="section-title"> |
||||
|
<h3>当前任务</h3> |
||||
|
<button class="text-button" type="button" :disabled="!mission.canOpenDetail" @click="$emit('open-mission-detail')">任务详情</button> |
||||
|
</div> |
||||
|
<template v-if="mission.hasMission"> |
||||
|
<div class="mission-name"> |
||||
|
<span> |
||||
|
<strong>{{ mission.name }}</strong> |
||||
|
<small>{{ mission.route }}</small> |
||||
|
</span> |
||||
|
<b>{{ mission.progressText }}</b> |
||||
|
</div> |
||||
|
<div class="progress-track"><i :style="{ width: mission.progressWidth }"></i></div> |
||||
|
<div class="progress-meta"> |
||||
|
<span>{{ mission.waypointText }}</span> |
||||
|
<span>{{ mission.elapsedText }}</span> |
||||
|
<span>{{ mission.distanceText }}</span> |
||||
|
</div> |
||||
|
</template> |
||||
|
<div v-else class="table-empty">暂无任务</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="environment-section"> |
||||
|
<div class="section-title"> |
||||
|
<h3>机巢环境</h3> |
||||
|
<span>{{ environment.updatedText }}</span> |
||||
|
</div> |
||||
|
<div v-if="environment.hasData" class="environment-row"> |
||||
|
<div><svg><use href="#i-sun" /></svg><span>舱外温度</span><strong>{{ environment.outsideTemperatureText }}</strong></div> |
||||
|
<div><svg><use href="#i-wind" /></svg><span>风速</span><strong>{{ environment.windSpeedText }}</strong></div> |
||||
|
<div><svg><use href="#i-cloud-rain" /></svg><span>雨量</span><strong>{{ environment.rainfallText }}</strong></div> |
||||
|
</div> |
||||
|
<div v-else class="table-empty">暂无环境数据</div> |
||||
|
</section> |
||||
|
|
||||
|
<section |
||||
|
v-if="actions.visible" |
||||
|
class="quick-actions" |
||||
|
:class="{ 'drone-quick-actions': header.isDrone, 'all-controls': controlExpanded }" |
||||
|
> |
||||
|
<template v-if="!header.isDrone"> |
||||
|
<div class="section-title"> |
||||
|
<h3>{{ controlExpanded ? '控制' : '快捷控制' }}</h3> |
||||
|
<button |
||||
|
class="control-mode-toggle" |
||||
|
:class="{ expanded: controlExpanded }" |
||||
|
type="button" |
||||
|
@click="emit('update:controlExpanded', !controlExpanded)" |
||||
|
> |
||||
|
{{ controlExpanded ? '简单控制' : '更多控制' }}<svg><use href="#i-chevron" /></svg> |
||||
|
</button> |
||||
|
</div> |
||||
|
<div class="workflow-actions"> |
||||
|
<button class="workflow-action takeoff" @click="emitCommand('一键起飞', '无人机将从机巢起飞并进入待命状态。')"> |
||||
|
<svg><use href="#i-plane" /></svg> |
||||
|
<span><strong>一键起飞</strong><small>{{ actions.statusText('一键起飞') }}</small></span> |
||||
|
</button> |
||||
|
<button class="workflow-action landing" @click="emitCommand('一键降落', '无人机将返回机巢并自动降落。')"> |
||||
|
<svg><use href="#i-home" /></svg> |
||||
|
<span><strong>一键降落</strong><small>{{ actions.statusText('一键降落') }}</small></span> |
||||
|
</button> |
||||
|
</div> |
||||
|
<div class="stage-actions"> |
||||
|
<button @click="emitCommand('起飞准备', '将执行开舱并解除归中。')"> |
||||
|
<b>01</b><span>起飞准备<small>{{ actions.statusText('起飞准备') }}</small></span> |
||||
|
</button> |
||||
|
<button @click="emitCommand('起飞准备完成', '确认起飞准备已完成。')"> |
||||
|
<b>02</b><span>起飞准备完成<small>{{ actions.statusText('起飞准备完成') }}</small></span> |
||||
|
</button> |
||||
|
<button @click="emitCommand('降落准备', '将开舱并等待无人机返航。')"> |
||||
|
<b>03</b><span>降落准备<small>{{ actions.statusText('降落准备') }}</small></span> |
||||
|
</button> |
||||
|
<button @click="emitCommand('降落准备完成', '确认降落准备已完成。')"> |
||||
|
<b>04</b><span>降落准备完成<small>{{ actions.statusText('降落准备完成') }}</small></span> |
||||
|
</button> |
||||
|
</div> |
||||
|
<button class="reset-action" @click="emitCommand('整机复位', '机巢将恢复待命状态。')"> |
||||
|
<svg><use href="#i-settings" /></svg> |
||||
|
<span><strong>整机复位</strong><small>{{ actions.statusText('整机复位') }}</small></span> |
||||
|
</button> |
||||
|
<div v-show="controlExpanded" class="dock-control-extra"> |
||||
|
<div class="dock-control-extra-grid"> |
||||
|
<button v-for="cmd in actions.dockExtra" :key="cmd.title" @click="emitCommand(cmd.title, cmd.desc)"> |
||||
|
<svg><use :href="cmd.icon" /></svg> |
||||
|
<span><strong>{{ cmd.title }}</strong><small>{{ actionHint(cmd.title, cmd.hint) }}</small></span> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
<template v-else> |
||||
|
<div class="section-title"> |
||||
|
<h3>{{ controlExpanded ? '控制' : '快捷控制' }}</h3> |
||||
|
<button |
||||
|
class="control-mode-toggle" |
||||
|
:class="{ expanded: controlExpanded }" |
||||
|
type="button" |
||||
|
@click="emit('update:controlExpanded', !controlExpanded)" |
||||
|
> |
||||
|
{{ controlExpanded ? '简单控制' : '更多控制' }}<svg><use href="#i-chevron" /></svg> |
||||
|
</button> |
||||
|
</div> |
||||
|
<button |
||||
|
v-if="actions.isFlying" |
||||
|
class="primary-action return-action" |
||||
|
@click="emitCommand('一键返航', '无人机将返回绑定机巢并自动降落。')" |
||||
|
> |
||||
|
<svg><use href="#i-home" /></svg> |
||||
|
<span><strong>一键返航</strong><small>{{ actionHint('一键返航', '自动回巢 · 降落') }}</small></span> |
||||
|
</button> |
||||
|
<button |
||||
|
v-else |
||||
|
class="primary-action" |
||||
|
@click="emitCommand('起飞', '无人机将立即起飞进入待命状态。')" |
||||
|
> |
||||
|
<svg><use href="#i-plane" /></svg> |
||||
|
<span><strong>起飞</strong><small>{{ actionHint('起飞', '离巢后进入待命') }}</small></span> |
||||
|
</button> |
||||
|
<div class="secondary-actions"> |
||||
|
<button @click="emitCommand('暂停任务', '向绑定机巢下发暂停指令。')"><svg><use href="#i-stop" /></svg>暂停任务</button> |
||||
|
<button @click="emitCommand('继续任务', '向绑定机巢下发继续指令。')"><svg><use href="#i-play" /></svg>继续任务</button> |
||||
|
<button @click="emitCommand('取消任务', '向绑定机巢下发取消指令。')"><svg><use href="#i-x" /></svg>取消任务</button> |
||||
|
</div> |
||||
|
<button class="reset-action" @click="emitCommand('紧急停止', '立即中止飞行,无人机原地悬停待命。')"> |
||||
|
<svg><use href="#i-stop" /></svg> |
||||
|
<span><strong>紧急停止</strong><small>{{ actionHint('紧急停止', '中止任务 · 原地悬停') }}</small></span> |
||||
|
</button> |
||||
|
<div v-show="controlExpanded" class="drone-control-all"> |
||||
|
<button |
||||
|
v-for="cmd in actions.droneExtra" |
||||
|
:key="cmd.title" |
||||
|
:class="{ danger: cmd.danger }" |
||||
|
@click="emitCommand(cmd.title, cmd.desc)" |
||||
|
> |
||||
|
<svg><use :href="cmd.icon" /></svg> |
||||
|
<span><strong>{{ cmd.title }}</strong><small>{{ cmd.hint }}</small></span> |
||||
|
</button> |
||||
|
</div> |
||||
|
</template> |
||||
|
</section> |
||||
|
</template> |
||||
|
|
||||
|
<div v-else class="table-empty" style="height:100%;display:grid;place-items:center;color:var(--muted);font-size:11px">请在左侧选择设备</div> |
||||
|
</aside> |
||||
|
</template> |
||||
|
|
||||
|
<script setup> |
||||
|
import { ref } from 'vue' |
||||
|
import LivePlayer from '@/components/LivePlayer.vue' |
||||
|
|
||||
|
const props = defineProps({ |
||||
|
selected: { type: Boolean, default: false }, |
||||
|
header: { type: Object, default: null }, |
||||
|
relation: { type: Object, default: null }, |
||||
|
liveView: { |
||||
|
type: Object, |
||||
|
default: () => ({ |
||||
|
playUrl: '', |
||||
|
phase: '', |
||||
|
active: false, |
||||
|
phaseText: '待机', |
||||
|
canFullscreen: false, |
||||
|
cameraLabel: '机巢摄像头', |
||||
|
}), |
||||
|
}, |
||||
|
dockStatus: { type: Object, default: null }, |
||||
|
flightStatus: { type: Object, default: null }, |
||||
|
mission: { type: Object, default: null }, |
||||
|
environment: { |
||||
|
type: Object, |
||||
|
default: () => ({ |
||||
|
updatedText: '刚刚', |
||||
|
hasData: false, |
||||
|
outsideTemperatureText: '--', |
||||
|
windSpeedText: '--', |
||||
|
rainfallText: '--', |
||||
|
}), |
||||
|
}, |
||||
|
actions: { |
||||
|
type: Object, |
||||
|
default: () => ({ |
||||
|
visible: false, |
||||
|
isDrone: false, |
||||
|
isFlying: false, |
||||
|
statusText: () => '状态:待执行', |
||||
|
activeTitle: '', |
||||
|
dockExtra: [], |
||||
|
droneExtra: [], |
||||
|
}), |
||||
|
}, |
||||
|
controlExpanded: { type: Boolean, default: false }, |
||||
|
}) |
||||
|
|
||||
|
const emit = defineEmits([ |
||||
|
'close', |
||||
|
'open-device-detail', |
||||
|
'select-parent', |
||||
|
'toggle-live', |
||||
|
'fullscreen-live', |
||||
|
'live-error', |
||||
|
'open-dock-status', |
||||
|
'open-mission-detail', |
||||
|
'update:controlExpanded', |
||||
|
'command', |
||||
|
]) |
||||
|
|
||||
|
const livePlayerRef = ref(null) |
||||
|
|
||||
|
function emitCommand(title, desc) { |
||||
|
emit('command', { title, desc }) |
||||
|
} |
||||
|
|
||||
|
function actionHint(title, hint) { |
||||
|
if (props.actions.activeTitle === title) return props.actions.statusText(title) |
||||
|
return hint |
||||
|
} |
||||
|
|
||||
|
defineExpose({ livePlayerRef }) |
||||
|
</script> |
||||
Loading…
Reference in new issue