Browse Source

refactor: 抽取 MonitorDetailPanel 右侧详情面板

main
xiaosi 3 weeks ago
parent
commit
29be5ef316
  1. 289
      src/components/MonitorDetailPanel.vue
  2. 298
      src/views/MonitorView/MonitorView.vue

289
src/components/MonitorDetailPanel.vue

@ -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>

298
src/views/MonitorView/MonitorView.vue

@ -91,186 +91,28 @@
</div> </div>
</div> </div>
<aside class="detail-panel" :class="{ 'drone-selected': isDrone }">
<template v-if="selectedAsset">
<header class="detail-header">
<div>
<span class="eyebrow">{{ isDrone ? '无人机设备' : '机巢设备' }}</span>
<h2>{{ selectedAsset.name }}</h2>
<p><svg><use href="#i-map-pin" /></svg>{{ selectedAsset.location }}</p>
</div>
<div class="detail-head-actions">
<i class="status" :class="selectedAsset.statusClass">{{ selectedAsset.status }}</i>
<button class="icon-button" title="查看设备详情" @click="goDetail"><svg><use href="#i-maximize" /></svg></button>
<button class="icon-button" title="关闭详情" @click="clearSelection"><svg><use href="#i-x" /></svg></button>
</div>
</header>
<!-- Drone only: bound dock -->
<div class="asset-relation" v-if="isDrone">
<span><svg><use href="#i-home" /></svg>所属机巢</span>
<button v-if="selectedAsset.parent" @click="select(selectedAsset.parent)">{{ selectedDroneParentName }}<svg><use href="#i-chevron" /></svg></button>
<button v-else disabled>{{ selectedDroneParentName }}</button>
</div>
<!-- Dock only: live video -->
<section class="video-section">
<div class="section-title">
<h3>实时画面</h3>
<div>
<span class="live-dot" :class="{ active: !!live.session }" />
<b>{{ livePhaseText }}</b>
<button class="icon-button" type="button" title="全屏播放" :disabled="!canFullscreenLive" @click="fullscreenLive">
<svg><use href="#i-maximize" /></svg>
</button>
</div>
</div>
<LivePlayer
ref="livePlayerRef"
:play-url="live.playUrl"
:phase="live.session?.phase"
:active="!!live.session"
:camera-label="isDrone ? '无人机图传' : '机巢摄像头'"
@toggle="toggleLive"
@error="ui.toast('视频播放失败')"
/>
</section>
<!-- Dock only: status metrics -->
<section v-if="!isDrone" class="status-section">
<div class="section-title">
<h3>运行状态</h3>
<button class="text-button" type="button" @click="openDockStatus">查看详情</button>
</div>
<div class="metric-grid">
<div><svg><use href="#i-battery" /></svg><span>无人机电量</span><strong>{{ selectedDrone?.battery || '--' }}</strong></div>
<div><svg><use href="#i-zap" /></svg><span>充电状态</span><strong :class="{ success: chargeTone }">{{ chargeText(rt('chargingState')) }}</strong></div>
<div><svg><use href="#i-door" /></svg><span>舱门状态</span><strong>{{ doorSummary }}</strong></div>
<div><svg><use href="#i-satellite" /></svg><span>控制模式</span><strong>{{ dockControlMode }}</strong></div>
</div>
</section>
<!-- Drone only: flight status -->
<section v-if="isDrone" class="status-section">
<div class="section-title">
<h3>飞行状态</h3>
<span class="mission-state">{{ droneMissionState }}</span>
</div>
<div class="metric-grid drone-metrics">
<div><svg><use href="#i-battery" /></svg><span>飞行电量</span><strong>{{ record?.battery || '--' }}</strong></div>
<div><svg><use href="#i-plane" /></svg><span>相对高度</span><strong>{{ displayWithUnit(selectedAsset.altitude, 'm') }}</strong></div>
<div><svg><use href="#i-route" /></svg><span>地速</span><strong>{{ displayWithUnit(selectedAsset.speed, 'm/s', 1) }}</strong></div>
<div><svg><use href="#i-satellite" /></svg><span>卫星数量</span><strong>{{ displayWithUnit(selectedAsset.satellites, '颗') }}</strong></div>
</div>
</section>
<section v-if="isDrone" class="mission-progress">
<div class="section-title">
<h3>当前任务</h3>
<button class="text-button" type="button" :disabled="!missionDetail.taskId" @click="goMissionDetail">任务详情</button>
</div>
<template v-if="missionDetail.hasMission">
<div class="mission-name">
<span>
<strong>{{ missionDetail.name }}</strong>
<small>{{ missionDetail.route }}</small>
</span>
<b>{{ missionDetail.progressText }}</b>
</div>
<div class="progress-track"><i :style="{ width: missionDetail.progressWidth }"></i></div>
<div class="progress-meta">
<span>{{ missionDetail.waypointText }}</span>
<span>{{ missionDetail.elapsedText }}</span>
<span>{{ missionDetail.distanceText }}</span>
</div>
</template>
<div v-else class="table-empty">暂无任务</div>
</section>
<!-- Drone only: mission -->
<section class="environment-section">
<div class="section-title">
<h3>机巢环境</h3>
<span>{{ selectedDock?.updated || record?.updated || '刚刚' }}</span>
</div>
<div v-if="hasEnvironment" class="environment-row">
<div><svg><use href="#i-sun" /></svg><span>舱外温度</span><strong>{{ displayWithUnit(selectedDockEnvironment?.outsideTemperature, '℃') }}</strong></div>
<div><svg><use href="#i-wind" /></svg><span>风速</span><strong>{{ displayWithUnit(selectedDockEnvironment?.windSpeed, 'm/s') }}</strong></div>
<div><svg><use href="#i-cloud-rain" /></svg><span>雨量</span><strong>{{ rainfallText(selectedDockEnvironment) }}</strong></div>
</div>
<div v-else class="table-empty">暂无环境数据</div>
</section>
<!-- Quick actions -->
<section v-if="!isAdmin" class="quick-actions" :class="{ 'drone-quick-actions': isDrone, 'all-controls': controlExpanded }">
<template v-if="!isDrone">
<div class="section-title">
<h3>{{ controlExpanded ? '控制' : '快捷控制' }}</h3>
<button class="control-mode-toggle" :class="{ expanded: controlExpanded }" type="button" @click="controlExpanded = !controlExpanded">
{{ controlExpanded ? '简单控制' : '更多控制' }}<svg><use href="#i-chevron" /></svg>
</button>
</div>
<div class="workflow-actions">
<button class="workflow-action takeoff" @click="runCommand('一键起飞', '无人机将从机巢起飞并进入待命状态。')"><svg><use href="#i-plane" /></svg><span><strong>一键起飞</strong><small>{{ buttonStatusText('一键起飞') }}</small></span></button>
<button class="workflow-action landing" @click="runCommand('一键降落', '无人机将返回机巢并自动降落。')"><svg><use href="#i-home" /></svg><span><strong>一键降落</strong><small>{{ buttonStatusText('一键降落') }}</small></span></button>
</div>
<div class="stage-actions">
<button @click="runCommand('起飞准备', '将执行开舱并解除归中。')"><b>01</b><span>起飞准备<small>{{ buttonStatusText('起飞准备') }}</small></span></button>
<button @click="runCommand('起飞准备完成', '确认起飞准备已完成。')"><b>02</b><span>起飞准备完成<small>{{ buttonStatusText('起飞准备完成') }}</small></span></button>
<button @click="runCommand('降落准备', '将开舱并等待无人机返航。')"><b>03</b><span>降落准备<small>{{ buttonStatusText('降落准备') }}</small></span></button>
<button @click="runCommand('降落准备完成', '确认降落准备已完成。')"><b>04</b><span>降落准备完成<small>{{ buttonStatusText('降落准备完成') }}</small></span></button>
</div>
<button class="reset-action" @click="runCommand('整机复位', '机巢将恢复待命状态。')"><svg><use href="#i-settings" /></svg><span><strong>整机复位</strong><small>{{ buttonStatusText('整机复位') }}</small></span></button>
<div v-show="controlExpanded" class="dock-control-extra">
<div class="dock-control-extra-grid">
<button v-for="cmd in dockExtraCommands" :key="cmd.title" @click="runCommand(cmd.title, cmd.desc)">
<svg><use :href="cmd.icon" /></svg>
<span><strong>{{ cmd.title }}</strong><small>{{ commandProgress.buttonTitle === cmd.title && commandProgress.status ? buttonStatusText(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="controlExpanded = !controlExpanded">
{{ controlExpanded ? '简单控制' : '更多控制' }}<svg><use href="#i-chevron" /></svg>
</button>
</div>
<button
v-if="isFlying"
class="primary-action return-action"
@click="runCommand('一键返航', '无人机将返回绑定机巢并自动降落。')"
>
<svg><use href="#i-home" /></svg>
<span><strong>一键返航</strong><small>{{ commandProgress.buttonTitle === '一键返航' && commandProgress.status ? buttonStatusText('一键返航') : '自动回巢 · 降落' }}</small></span>
</button>
<button
v-else
class="primary-action"
@click="runCommand('起飞', '无人机将立即起飞进入待命状态。')"
>
<svg><use href="#i-plane" /></svg>
<span><strong>起飞</strong><small>{{ commandProgress.buttonTitle === '起飞' && commandProgress.status ? buttonStatusText('起飞') : '离巢后进入待命' }}</small></span>
</button>
<div class="secondary-actions">
<button @click="runCommand('暂停任务', '向绑定机巢下发暂停指令。')"><svg><use href="#i-stop" /></svg>暂停任务</button>
<button @click="runCommand('继续任务', '向绑定机巢下发继续指令。')"><svg><use href="#i-play" /></svg>继续任务</button>
<button @click="runCommand('取消任务', '向绑定机巢下发取消指令。')"><svg><use href="#i-x" /></svg>取消任务</button>
</div>
<button class="reset-action" @click="runCommand('紧急停止', '立即中止飞行,无人机原地悬停待命。')"><svg><use href="#i-stop" /></svg><span><strong>紧急停止</strong><small>{{ commandProgress.buttonTitle === '紧急停止' && commandProgress.status ? buttonStatusText('紧急停止') : '中止任务 · 原地悬停' }}</small></span></button>
<div v-show="controlExpanded" class="drone-control-all">
<button v-for="cmd in droneExtraCommands" :key="cmd.title" :class="{ danger: cmd.danger }" @click="runCommand(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>
<MonitorDetailPanel
ref="detailPanelRef"
:selected="!!selectedAsset"
:header="detailHeader"
:relation="detailRelation"
:live-view="detailLiveView"
:dock-status="detailDockStatus"
:flight-status="detailFlightStatus"
:mission="detailMission"
:environment="detailEnvironment"
:actions="detailActions"
v-model:control-expanded="controlExpanded"
@close="clearSelection"
@open-device-detail="goDetail"
@select-parent="select"
@toggle-live="toggleLive"
@fullscreen-live="fullscreenLive"
@live-error="ui.toast('视频播放失败')"
@open-dock-status="openDockStatus"
@open-mission-detail="goMissionDetail"
@command="onDetailCommand"
/>
</section> </section>
@ -375,7 +217,7 @@ import commonRefs from '@/utils/commonRefs'
import mapHelper from '@/core/mapHelper' import mapHelper from '@/core/mapHelper'
import mapConfig, { MAP_STYLES, MAP_OVERVIEW, MAP_VIEW } from '@/config/map' import mapConfig, { MAP_STYLES, MAP_OVERVIEW, MAP_VIEW } from '@/config/map'
import { applyChineseLabels } from '@/utils/mapLabels' import { applyChineseLabels } from '@/utils/mapLabels'
import LivePlayer from '@/components/LivePlayer.vue'
import MonitorDetailPanel from '@/components/MonitorDetailPanel.vue'
import DockDeviceCard from '@/components/DockDeviceCard.vue' import DockDeviceCard from '@/components/DockDeviceCard.vue'
import { getLivePlayURL, heartbeatLive, joinLive, leaveLive } from '@/api/live' import { getLivePlayURL, heartbeatLive, joinLive, leaveLive } from '@/api/live'
import request from '@/utils/http' import request from '@/utils/http'
@ -407,7 +249,7 @@ const isAdmin = computed(() => userStore.isAdmin())
const selectedId = ref(null) const selectedId = ref(null)
const live = reactive({ session: null, dockId: '', playUrl: '', heartbeatTimer: null }) const live = reactive({ session: null, dockId: '', playUrl: '', heartbeatTimer: null })
const livePlayerRef = ref(null)
const detailPanelRef = ref(null)
const filter = ref('all') const filter = ref('all')
const search = ref('') const search = ref('')
@ -989,6 +831,96 @@ const livePhaseText = computed(() => {
}) })
const canFullscreenLive = computed(() => !!live.playUrl && !live.playUrl.startsWith('fake://')) const canFullscreenLive = computed(() => !!live.playUrl && !live.playUrl.startsWith('fake://'))
const detailHeader = computed(() => {
const asset = selectedAsset.value
if (!asset) return null
return {
kindLabel: isDrone.value ? '无人机设备' : '机巢设备',
name: asset.name,
location: asset.location,
status: asset.status,
statusClass: asset.statusClass,
isDrone: isDrone.value,
}
})
const detailRelation = computed(() => {
if (!isDrone.value || !selectedAsset.value) return null
return {
parentId: selectedAsset.value.parent || null,
parentName: selectedDroneParentName.value,
}
})
const detailLiveView = computed(() => ({
playUrl: live.playUrl,
phase: live.session?.phase,
active: !!live.session,
phaseText: livePhaseText.value,
canFullscreen: canFullscreenLive.value,
cameraLabel: isDrone.value ? '无人机图传' : '机巢摄像头',
}))
const detailDockStatus = computed(() => {
if (isDrone.value) return null
return {
droneBattery: selectedDrone.value?.battery || '--',
chargeText: chargeText(rt('chargingState')),
chargeActive: chargeTone.value,
doorSummary: doorSummary.value,
controlMode: dockControlMode.value,
}
})
const detailFlightStatus = computed(() => {
if (!isDrone.value || !selectedAsset.value) return null
return {
missionState: droneMissionState.value,
battery: record.value?.battery || '--',
altitudeText: displayWithUnit(selectedAsset.value.altitude, 'm'),
speedText: displayWithUnit(selectedAsset.value.speed, 'm/s', 1),
satellitesText: displayWithUnit(selectedAsset.value.satellites, '颗'),
}
})
const detailMission = computed(() => {
if (!isDrone.value) return null
const detail = missionDetail.value
return {
hasMission: detail.hasMission,
name: detail.name,
route: detail.route,
progressText: detail.progressText,
progressWidth: detail.progressWidth,
waypointText: detail.waypointText,
elapsedText: detail.elapsedText,
distanceText: detail.distanceText,
canOpenDetail: !!detail.taskId,
}
})
const detailEnvironment = computed(() => ({
updatedText: selectedDock.value?.updated || record.value?.updated || '刚刚',
hasData: hasEnvironment.value,
outsideTemperatureText: displayWithUnit(selectedDockEnvironment.value?.outsideTemperature, '℃'),
windSpeedText: displayWithUnit(selectedDockEnvironment.value?.windSpeed, 'm/s'),
rainfallText: rainfallText(selectedDockEnvironment.value),
}))
const detailActions = computed(() => ({
visible: !isAdmin.value,
isDrone: isDrone.value,
isFlying: isFlying.value,
statusText: buttonStatusText,
activeTitle: commandProgress.status ? commandProgress.buttonTitle : '',
dockExtra: dockExtraCommands,
droneExtra: droneExtraCommands,
}))
function onDetailCommand({ title, desc }) {
runCommand(title, desc)
}
function toggleLive() { function toggleLive() {
if (live.session) closeLive() if (live.session) closeLive()
else openLive() else openLive()
@ -999,7 +931,7 @@ function fullscreenLive() {
ui.toast('当前无可全屏播放的视频流') ui.toast('当前无可全屏播放的视频流')
return return
} }
const ok = livePlayerRef.value?.requestFullscreen?.()
const ok = detailPanelRef.value?.livePlayerRef?.requestFullscreen?.()
if (!ok) ui.toast('当前浏览器不支持全屏') if (!ok) ui.toast('当前浏览器不支持全屏')
} }

Loading…
Cancel
Save