diff --git a/src/composables/useCommandProgress.js b/src/composables/useCommandProgress.js new file mode 100644 index 0000000..a2a7c1b --- /dev/null +++ b/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, + } +} diff --git a/src/views/MonitorView/MonitorView.vue b/src/views/MonitorView/MonitorView.vue index 8cd489b..595e608 100644 --- a/src/views/MonitorView/MonitorView.vue +++ b/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 || '指令下发失败') - } -}