Browse Source

feat: 设备列表接入服务端分页

main
xiaosi 3 weeks ago
parent
commit
dc6864dc55
  1. 309
      src/views/DevicesView/DevicesView.vue

309
src/views/DevicesView/DevicesView.vue

@ -20,14 +20,20 @@
</div>
<div class="asset-workspace">
<t-tabs v-model="tab" class="device-asset-tabs" @change="onTabChange">
<t-tabs v-model="tab" class="device-asset-tabs">
<t-tab-panel value="docks" :label="docksTabLabel" :destroy-on-hide="false" />
<t-tab-panel value="drones" :label="dronesTabLabel" :destroy-on-hide="false" />
</t-tabs>
<div class="asset-toolbar">
<div class="asset-search">
<t-input v-model="search" clearable placeholder="搜索设备名称、编号或 SN">
<t-input
v-model="search"
clearable
placeholder="搜索设备名称、编号或 SN"
@keyup.enter="onSearchCommit"
@clear="onSearchCommit"
>
<template #prefix-icon><t-icon name="search" /></template>
</t-input>
</div>
@ -43,7 +49,7 @@
row-key="id"
:data="tableData"
:columns="tableColumns"
empty="暂无符合条件的设备"
:empty="tableEmpty"
hover
@row-click="handleRowClick"
>
@ -85,7 +91,12 @@
</div>
<div class="table-footer">
<span> {{ tableData.length }} </span>
<t-pagination
v-model:current="pageNum"
:total="total"
:page-size="pageSize"
@current-change="loadTable"
/>
</div>
</div>
@ -220,7 +231,7 @@
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useDeviceStore } from '@/stores/modules/devicesStore'
import { useUiStore } from '@/stores/modules/uiStore'
import { useRouter } from 'vue-router'
@ -236,19 +247,18 @@ const userStore = useUserStore()
const isAdmin = computed(() => userStore.isAdmin())
const tab = ref('docks')
onMounted(async () => {
try {
await devices.load()
loadedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
} catch (e) {
ui.toast(e.message || '加载设备失败')
}
if (!isAdmin.value) loadFirmwares()
})
const pageNum = ref(1)
const pageSize = 20
const total = ref(0)
const tableRecords = ref([])
const docksTotal = ref(0)
const dronesTotal = ref(0)
const search = ref('')
const searchKeyword = ref('')
const statusFilter = ref('')
const bindingFilter = ref('')
let searchTimer = null
const statusOptions = [
{ label: '全部状态', value: '' },
{ label: '在线', value: 'online' },
@ -275,12 +285,17 @@ const upgradeFirmwareId = ref(null)
const upgrading = ref(false)
const firmwareVisible = ref(false)
const firmwareError = ref('')
const firmwareRecord = computed(() => (drawerId.value ? devices.record(drawerId.value) : null))
function findLocalRecord(id) {
const key = String(id)
return tableRecords.value.find((d) => d.id === key) || devices.record(key)
}
const firmwareRecord = computed(() => (drawerId.value ? findLocalRecord(drawerId.value) : null))
const upgradeFirmwareOptions = computed(() =>
firmwareList.value.map((f) => ({ label: firmwareOptionLabel(f), value: f.id }))
)
const drawerRecord = computed(() => (drawerId.value ? devices.record(drawerId.value) : null))
const drawerRecord = computed(() => (drawerId.value ? findLocalRecord(drawerId.value) : null))
const overview = computed(() => {
const all = [...devices.docks, ...devices.drones]
@ -317,16 +332,180 @@ function displayCoord(value) {
return value == null || value === '' ? '--' : value
}
function matchesFilters(d) {
const q = search.value.trim().toLowerCase()
function fmtTime(t) {
if (!t) return '--'
const d = new Date(t)
if (Number.isNaN(d.getTime())) return '--'
return d.toLocaleString('zh-CN', { hour12: false })
}
function parseJson(value, fallback = null) {
if (!value) return fallback
if (typeof value === 'object') return value
try { return JSON.parse(value) } catch { return fallback }
}
function alarmCodes(realtime) {
const codes = parseJson(realtime?.alarmCodes, [])
return Array.isArray(codes) ? codes : []
}
function modeText(m) {
const key = String(m || '').toUpperCase()
switch (key) {
case 'STANDBY': return '待命'
case 'AUTO': return 'AUTO'
case 'GUIDED': return 'GUIDED'
case 'LOITER': return '悬停'
case 'RTL': return '返航'
default: return m || '--'
}
}
function controlModeText(m) {
const key = String(m || '').toLowerCase()
if (key === 'auto' || key === 'automatic') return '自动'
if (key === 'manual') return '手动'
return m || '--'
}
function displayValue(value) {
return value == null || value === '' ? '--' : String(value)
}
function coordinate(value, min, max) {
const number = Number(value)
return Number.isFinite(number) && number >= min && number <= max ? number : null
}
function gpsText(q) {
const key = String(q || '').toUpperCase().replace(/-/g, '_')
switch (key) {
case 'RTK_FIX':
case 'RTK_FIXED': return 'RTK 固定解'
case 'RTK_FLOAT': return 'RTK 浮点解'
case '3D_FIX': return '3D 定位'
case '2D_FIX': return '2D 定位'
default: return q ? String(q) : '无定位'
}
}
function mapDockRow(d, detail = {}) {
const id = String(d.id)
const rt = detail.realtime || {}
const online = Boolean(detail.online)
const registerStatus = d.registerStatus || 'registered'
const pending = registerStatus === 'pending' && !online
const st = pending
? { status: '待连接', statusClass: 'pending' }
: online
? alarmCodes(rt).length ? { status: '告警', statusClass: 'alarm' } : { status: '在线', statusClass: 'online' }
: { status: '离线', statusClass: 'offline' }
const cachedDrone = devices.drones.find((dr) => dr.dockId === d.dockId)
return {
id,
kind: 'dock',
dockId: d.dockId,
name: d.name || d.dockId,
code: d.code || d.sn || d.dockId,
sn: d.sn || d.dockId || '--',
iccid: d.iccid || '--',
longitude: coordinate(d.longitude, -180, 180),
latitude: coordinate(d.latitude, -90, 90),
location: d.location || '--',
status: st.status,
statusClass: st.statusClass,
binding: cachedDrone?.name || '--',
bindingState: cachedDrone ? 'bound' : 'unbound',
mode: controlModeText(rt.controlMode),
version: d.softwareVer ? `v${d.softwareVer}` : '--',
updated: fmtTime(d.updatedAt),
protocol: d.protocolVer ? `MQTT v${d.protocolVer}` : '--',
registerStatus,
realtime: rt,
online
}
}
function mapDroneRow(d, detail = {}) {
const id = String(d.id)
const rt = detail.realtime || {}
const armed = ['1', 'true', 1, true].includes(rt.armed)
const altitude = coordinate(rt.altitude, -1000, 100000)
const flying = armed || (altitude != null && altitude > 0)
const online = Boolean(detail.online)
const st = !online
? { status: '离线', statusClass: 'offline' }
: flying
? { status: '飞行中', statusClass: 'mission' }
: { status: '在巢', statusClass: 'online' }
const dock = devices.docks.find((dk) => dk.dockId === d.dockId)
const battery = rt.batteryPercent ?? rt.batteryPct ?? d.battery
return {
id,
kind: 'drone',
droneSn: d.droneSn,
dockId: d.dockId,
name: d.name || d.droneSn,
code: d.code || d.droneSn,
status: st.status,
statusClass: st.statusClass,
binding: dock?.name || '--',
bindingState: dock ? 'bound' : 'unbound',
battery: battery != null && battery !== '' ? `${battery}%` : '--',
mode: modeText(rt.flightMode),
mission: displayValue(rt.missionName || rt.mission),
updated: fmtTime(d.updatedAt),
sysid: displayValue(rt.currentSysId),
gps: gpsText(rt.gpsQuality),
sn: d.droneSn || d.code || '--',
model: d.model || '--',
firmwareVer: d.firmwareVer || '--',
realtime: rt,
online
}
}
async function enrichRecords(records, kind) {
if (!records.length) return []
const details = await Promise.all(
records.map((row) =>
request
.get(kind === 'docks' ? urls.DOCK(row.id) : urls.DRONE(row.id))
.then((r) => r || {})
.catch(() => ({}))
)
)
return records.map((row, i) => (kind === 'docks' ? mapDockRow(row, details[i]) : mapDroneRow(row, details[i])))
}
async function loadTable() {
const kind = tab.value
const params = { pageNum: pageNum.value, pageSize }
const keyword = searchKeyword.value.trim()
if (keyword) params.keyword = keyword
try {
const page = await request.get(kind === 'docks' ? urls.DOCKS : urls.DRONES, { params })
const records = page?.records || []
total.value = Number(page?.total) || 0
if (kind === 'docks') docksTotal.value = total.value
else dronesTotal.value = total.value
tableRecords.value = await enrichRecords(records, kind)
} catch (e) {
tableRecords.value = []
total.value = 0
ui.toast(e.message || '加载设备列表失败')
}
}
function matchesClientFilters(d) {
if (statusFilter.value && d.statusClass !== statusFilter.value) return false
if (bindingFilter.value && d.bindingState !== bindingFilter.value) return false
if (!q) return true
const haystack = [d.name, d.code, d.sn, d.location, d.binding].filter(Boolean).join(' ').toLowerCase()
return haystack.includes(q)
return true
}
const docksTabLabel = computed(() => `机巢设备 ${devices.docks.length}`)
const dronesTabLabel = computed(() => `无人机 ${devices.drones.length}`)
const docksTabLabel = computed(() => `机巢设备 ${docksTotal.value || devices.docks.length}`)
const dronesTabLabel = computed(() => `无人机 ${dronesTotal.value || devices.drones.length}`)
const dockColumns = [
{ colKey: 'name', title: '机巢设备', width: '18%' },
@ -351,26 +530,71 @@ const droneColumns = [
]
const tableColumns = computed(() => (tab.value === 'docks' ? dockColumns : droneColumns))
const tableData = computed(() => (tab.value === 'docks' ? filteredDocks.value : filteredDrones.value))
const filteredDocks = computed(() => devices.docks.filter(matchesFilters))
const tableData = computed(() => tableRecords.value.filter(matchesClientFilters))
const tableEmpty = computed(() =>
statusFilter.value || bindingFilter.value
? '当前页无符合条件的设备(状态/绑定仅筛选当前页)'
: '暂无符合条件的设备'
)
const filteredDrones = computed(() => devices.drones.filter(matchesFilters))
function onSearchCommit() {
if (searchTimer) {
clearTimeout(searchTimer)
searchTimer = null
}
const next = search.value.trim()
if (searchKeyword.value === next && pageNum.value === 1) {
loadTable()
return
}
searchKeyword.value = next
pageNum.value = 1
loadTable()
}
watch(search, (value) => {
if (searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
searchTimer = null
const next = String(value || '').trim()
if (searchKeyword.value === next) return
searchKeyword.value = next
pageNum.value = 1
loadTable()
}, 300)
})
function onTabChange() {
resetFilters()
watch(tab, () => {
if (searchTimer) {
clearTimeout(searchTimer)
searchTimer = null
}
search.value = ''
searchKeyword.value = ''
statusFilter.value = ''
bindingFilter.value = ''
drawerOpen.value = false
}
pageNum.value = 1
loadTable()
})
function handleRowClick({ row }) {
openDrawer(row.id)
}
function resetFilters() {
function resetFilters(reload = true) {
if (searchTimer) {
clearTimeout(searchTimer)
searchTimer = null
}
search.value = ''
searchKeyword.value = ''
statusFilter.value = ''
bindingFilter.value = ''
if (reload) {
pageNum.value = 1
loadTable()
}
}
function openDrawer(id) {
@ -384,7 +608,7 @@ function goControl(id) {
async function reload() {
try {
await devices.load()
await Promise.all([loadTable(), devices.load()])
loadedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
ui.toast('设备列表已刷新')
} catch (e) {
@ -416,6 +640,7 @@ function openEditDock(dock) {
editDockRecord.value = dock
editDockVisible.value = true
}
async function deleteDock(dock) {
const ok = await ui.confirm(`确认硬删除机巢「${dock.name}」?`, '删除后机巢及其关联设备数据将永久移除,且无法恢复。')
if (!ok) return
@ -423,16 +648,17 @@ async function deleteDock(dock) {
await request.delete(urls.DOCK(dock.id))
drawerOpen.value = false
ui.toast('机巢已永久删除')
await loadTable()
await devices.load()
} catch (e) {
ui.toast(e.message || '删除机巢失败')
}
}
async function handleDockSaved() {
editDockVisible.value = false
try {
await loadTable()
await devices.load()
ui.toast('机巢信息已更新')
} catch (e) {
@ -484,6 +710,7 @@ async function submitAddDock() {
})
addDockVisible.value = false
ui.toast('机巢已成功接入')
await loadTable()
await devices.load()
} catch (e) {
addDockError.value = e.message || '接入失败'
@ -536,6 +763,16 @@ async function doUpgrade() {
upgrading.value = false
}
}
onMounted(async () => {
try {
await Promise.all([devices.load(), loadTable()])
loadedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false })
} catch (e) {
ui.toast(e.message || '加载设备失败')
}
if (!isAdmin.value) loadFirmwares()
})
</script>
<style scoped>

Loading…
Cancel
Save