Browse Source

refactor: 抽取 useCommandProgress 指令进度逻辑

main
xiaosi 3 weeks ago
parent
commit
ba26e0a406
  1. 183
      src/composables/useCommandProgress.js
  2. 181
      src/views/MonitorView/MonitorView.vue

183
src/composables/useCommandProgress.js

@ -0,0 +1,183 @@
import { computed, reactive } from 'vue'
import request from '@/utils/http'
import * as urls from '@/config/urls'
const COMMAND_STATUS = {
sent: { name: '已下发', step: 2, result: '等待应答', terminal: false },
acked: { name: '已确认', step: 3, result: '执行成功', terminal: true, ok: true },
timeout: { name: '已超时', step: 3, result: '已超时', terminal: true, ok: false },
terminal: { name: '执行失败', step: 3, result: '执行失败', terminal: true, ok: false },
}
export function useCommandProgress({ getRecord, ui, devices, reload }) {
const commandProgress = reactive({
open: false,
title: '',
deviceName: '',
commandId: '',
status: '', // sent/acked/timeout/terminal
result: '',
step: 1, // 1 确认下发 2 已下发 3 终态
error: '',
buttonTitle: '',
})
let commandPollTimer = null
let commandAutoCloseTimer = null
let commandPollFails = 0
let commandPollInFlight = false
let commandProgressEpoch = 0
function buttonStatusText(title) {
if (commandProgress.buttonTitle !== title || !commandProgress.status) return '状态:待执行'
const meta = COMMAND_STATUS[commandProgress.status]
return meta ? `状态:${meta.name}` : '状态:待执行'
}
const commandProgressTerminalLabel = computed(() => {
if (commandProgress.status === 'acked') return '已确认'
return COMMAND_STATUS[commandProgress.status]?.name || '执行结果'
})
function clearCommandTimers() {
if (commandPollTimer) {
clearInterval(commandPollTimer)
commandPollTimer = null
}
if (commandAutoCloseTimer) {
clearTimeout(commandAutoCloseTimer)
commandAutoCloseTimer = null
}
}
function closeCommandProgress() {
clearCommandTimers()
commandProgressEpoch += 1
commandProgress.open = false
}
function applyCommandStatus(cmd) {
const status = cmd?.status || 'sent'
const meta = COMMAND_STATUS[status] || COMMAND_STATUS.sent
commandProgress.status = status
commandProgress.step = meta.step
commandProgress.result = cmd?.ackResultCode || meta.result
commandProgress.error = ''
return meta
}
function scheduleAutoClose() {
clearTimeout(commandAutoCloseTimer)
commandAutoCloseTimer = setTimeout(() => {
closeCommandProgress()
}, 5000)
}
async function pollCommandOnce() {
if (!commandProgress.commandId || commandPollInFlight) return
const epoch = commandProgressEpoch
commandPollInFlight = true
try {
const cmd = await request.get(urls.COMMAND(commandProgress.commandId))
if (epoch !== commandProgressEpoch) return
commandPollFails = 0
const meta = applyCommandStatus(cmd)
if (meta.terminal) {
clearInterval(commandPollTimer)
commandPollTimer = null
scheduleAutoClose()
}
} catch (e) {
if (epoch !== commandProgressEpoch) return
commandPollFails += 1
const status = e?.status ?? e?.response?.status
if (status === 403 || status === 404 || commandPollFails >= 3) {
clearInterval(commandPollTimer)
commandPollTimer = null
commandProgress.error =
status === 403
? '无权限查看进度,请稍后在操作记录确认'
: status === 404
? '未找到指令记录'
: (e.message || '进度查询失败')
scheduleAutoClose()
}
} finally {
commandPollInFlight = false
}
}
function startCommandProgress(cmd, title, deviceName) {
clearCommandTimers()
commandPollInFlight = false
commandProgressEpoch += 1
commandPollFails = 0
commandProgress.open = true
commandProgress.title = title
commandProgress.deviceName = deviceName || '--'
commandProgress.commandId = String(cmd?.id || '')
commandProgress.buttonTitle = title
commandProgress.step = 1
commandProgress.error = ''
applyCommandStatus(cmd || { status: 'sent' })
if (!commandProgress.commandId) {
commandProgress.error = '未返回指令 ID,无法跟踪进度'
scheduleAutoClose()
return
}
const ttl = Number(cmd?.ttlMs) > 0 ? Number(cmd.ttlMs) : 30000
const deadline = Date.now() + Math.max(ttl, 30000) + 5000
commandPollTimer = setInterval(async () => {
if (Date.now() > deadline) {
clearInterval(commandPollTimer)
commandPollTimer = null
commandProgressEpoch += 1
if (!COMMAND_STATUS[commandProgress.status]?.terminal) {
commandProgress.error = '等待超时,请稍后在操作记录查看'
commandProgress.step = 3
}
scheduleAutoClose()
return
}
await pollCommandOnce()
}, 1500)
// 立即查一次,避免干等 1.5s
pollCommandOnce()
}
async function runCommand(title, desc) {
const rec = getRecord()
if (!rec) return
if (!rec.online) {
ui.toast('设备当前离线,无法下发指令')
return
}
if (rec.kind === 'drone' && rec.bindingState !== 'bound') {
ui.toast('无人机未绑定机巢,无法下发指令')
return
}
const ok = await ui.confirm(title, desc)
if (!ok) return
try {
const cmd = await devices.sendCommand(rec, title)
ui.toast(`${title}指令已下发`)
startCommandProgress(cmd, title, rec.name)
await reload()
} catch (e) {
ui.toast(e.message || '指令下发失败')
}
}
return {
commandProgress,
commandProgressTerminalLabel,
buttonStatusText,
startCommandProgress,
closeCommandProgress,
runCommand,
}
}

181
src/views/MonitorView/MonitorView.vue

@ -91,8 +91,7 @@ import DockStatusDialog from '@/components/DockStatusDialog.vue'
import CommandProgressFloat from '@/components/CommandProgressFloat.vue'
import MonitorMapChrome from '@/components/MonitorMapChrome.vue'
import { getLivePlayURL, heartbeatLive, joinLive, leaveLive } from '@/api/live'
import request from '@/utils/http'
import * as urls from '@/config/urls'
import { useCommandProgress } from '@/composables/useCommandProgress'
const DEVICE_SOURCE = 'monitor-devices'
@ -1193,172 +1192,16 @@ function zoomOut() {
mapInstance.zoomTo(Math.max(mapInstance.getMinZoom(), mapInstance.getZoom() - 1), { duration: 200 })
}
const COMMAND_STATUS = {
sent: { name: '已下发', step: 2, result: '等待应答', terminal: false },
acked: { name: '已确认', step: 3, result: '执行成功', terminal: true, ok: true },
timeout: { name: '已超时', step: 3, result: '已超时', terminal: true, ok: false },
terminal: { name: '执行失败', step: 3, result: '执行失败', terminal: true, ok: false },
}
const commandProgress = reactive({
open: false,
title: '',
deviceName: '',
commandId: '',
status: '', // sent/acked/timeout/terminal
result: '',
step: 1, // 1 2 3
error: '',
buttonTitle: '',
const {
commandProgress,
commandProgressTerminalLabel,
buttonStatusText,
closeCommandProgress,
runCommand,
} = useCommandProgress({
getRecord: () => record.value,
ui,
devices,
reload,
})
let commandPollTimer = null
let commandAutoCloseTimer = null
let commandPollFails = 0
let commandPollInFlight = false
let commandProgressEpoch = 0
function buttonStatusText(title) {
if (commandProgress.buttonTitle !== title || !commandProgress.status) return '状态:待执行'
const meta = COMMAND_STATUS[commandProgress.status]
return meta ? `状态:${meta.name}` : '状态:待执行'
}
const commandProgressTerminalLabel = computed(() => {
if (commandProgress.status === 'acked') return '已确认'
return COMMAND_STATUS[commandProgress.status]?.name || '执行结果'
})
function clearCommandTimers() {
if (commandPollTimer) {
clearInterval(commandPollTimer)
commandPollTimer = null
}
if (commandAutoCloseTimer) {
clearTimeout(commandAutoCloseTimer)
commandAutoCloseTimer = null
}
}
function closeCommandProgress() {
clearCommandTimers()
commandProgressEpoch += 1
commandProgress.open = false
}
function applyCommandStatus(cmd) {
const status = cmd?.status || 'sent'
const meta = COMMAND_STATUS[status] || COMMAND_STATUS.sent
commandProgress.status = status
commandProgress.step = meta.step
commandProgress.result = cmd?.ackResultCode || meta.result
commandProgress.error = ''
return meta
}
function scheduleAutoClose() {
if (commandAutoCloseTimer) clearTimeout(commandAutoCloseTimer)
commandAutoCloseTimer = setTimeout(() => {
closeCommandProgress()
}, 5000)
}
async function pollCommandOnce() {
if (!commandProgress.commandId || commandPollInFlight) return
const epoch = commandProgressEpoch
commandPollInFlight = true
try {
const cmd = await request.get(urls.COMMAND(commandProgress.commandId))
if (epoch !== commandProgressEpoch) return
commandPollFails = 0
const meta = applyCommandStatus(cmd)
if (meta.terminal) {
clearInterval(commandPollTimer)
commandPollTimer = null
scheduleAutoClose()
}
} catch (e) {
if (epoch !== commandProgressEpoch) return
commandPollFails += 1
const status = e?.status ?? e?.response?.status
if (status === 403 || status === 404 || commandPollFails >= 3) {
clearInterval(commandPollTimer)
commandPollTimer = null
commandProgress.error =
status === 403
? '无权限查看进度,请稍后在操作记录确认'
: status === 404
? '未找到指令记录'
: (e.message || '进度查询失败')
scheduleAutoClose()
}
} finally {
commandPollInFlight = false
}
}
function startCommandProgress(cmd, title, deviceName) {
clearCommandTimers()
commandPollInFlight = false
commandProgressEpoch += 1
commandPollFails = 0
commandProgress.open = true
commandProgress.title = title
commandProgress.deviceName = deviceName || '--'
commandProgress.commandId = String(cmd?.id || '')
commandProgress.buttonTitle = title
commandProgress.step = 1
commandProgress.error = ''
applyCommandStatus(cmd || { status: 'sent' })
if (!commandProgress.commandId) {
commandProgress.error = '未返回指令 ID,无法跟踪进度'
scheduleAutoClose()
return
}
const ttl = Number(cmd?.ttlMs) > 0 ? Number(cmd.ttlMs) : 30000
const deadline = Date.now() + Math.max(ttl, 30000) + 5000
commandPollTimer = setInterval(async () => {
if (Date.now() > deadline) {
clearInterval(commandPollTimer)
commandPollTimer = null
commandProgressEpoch += 1
if (!COMMAND_STATUS[commandProgress.status]?.terminal) {
commandProgress.error = '等待超时,请稍后在操作记录查看'
commandProgress.step = 3
}
scheduleAutoClose()
return
}
await pollCommandOnce()
}, 1500)
// 1.5s
pollCommandOnce()
}
async function runCommand(title, desc) {
const rec = record.value
if (!rec) return
if (!rec.online) {
ui.toast('设备当前离线,无法下发指令')
return
}
if (rec.kind === 'drone' && rec.bindingState !== 'bound') {
ui.toast('无人机未绑定机巢,无法下发指令')
return
}
const ok = await ui.confirm(title, desc)
if (!ok) return
try {
const cmd = await devices.sendCommand(rec, title)
ui.toast(`${title}指令已下发`)
startCommandProgress(cmd, title, rec.name)
await reload()
} catch (e) {
ui.toast(e.message || '指令下发失败')
}
}
</script>

Loading…
Cancel
Save