Переглянути джерело

feat: 设备详情新增固件升级组件;

liujia 3 тижнів тому
батько
коміт
5ec59e343e

+ 1 - 1
src/api/device/types.ts

@@ -267,7 +267,7 @@ export interface UpdateDeviceParams {
 export interface OtaItem {
   fileId: ID // 文件ID
   fileName: string // 文件名称
-  ossUrl: ID // 文件地址
+  ossUrl: string // 文件地址
   createTime: string // 创建时间
 }
 

+ 67 - 0
src/utils/ota.ts

@@ -0,0 +1,67 @@
+export interface OtaItem {
+  fileName: string
+  ossUrl: string
+}
+
+export interface OtaOption {
+  label: string
+  value: string
+  version: string | null
+  rawFile: string
+}
+
+// 比较两个版本号大小(升序用)
+export function compareVersions(a: string, b: string): number {
+  const pa = a.split('.').map((n) => parseInt(n, 10))
+  const pb = b.split('.').map((n) => parseInt(n, 10))
+  const len = Math.max(pa.length, pb.length)
+  for (let i = 0; i < len; i++) {
+    const na = pa[i] || 0
+    const nb = pb[i] || 0
+    if (na > nb) return 1
+    if (na < nb) return -1
+  }
+  return 0
+}
+
+// 判断字符串是否是合法版本号
+export function isValidVersion(v: string): boolean {
+  return /^\d+(\.\d+)*$/.test(v)
+}
+
+/**
+ * 过滤并排序 OTA 列表
+ * @param data OTA 列表数据
+ * @param currentVersion 当前版本号
+ * @returns 过滤后的 OTA 选项列表
+ * @example
+ * const currentVersion = '2.1.1'
+ * const filteredList = filterAndSortOtaList(otaList, currentVersion);
+ */
+export function filterAndSortOtaList(data: OtaItem[], currentVersion?: string): OtaOption[] {
+  return (
+    data
+      .map((item) => {
+        // 提取版本(例: LNRadar500M-V2.1.4-RPM.bin → 2.1.4-RPM)
+        const match = item.fileName.match(/V([\d.]+(?:-[A-Za-z0-9]+)?)(?:\.bin|\.hex|\.fw)$/)
+        const version = match ? match[1] : null
+
+        return {
+          rawFile: item.fileName,
+          label: version ?? item.fileName,
+          value: item.ossUrl,
+          version,
+          ...item,
+        }
+      })
+      // 只保留有解析到版本号的
+      .filter((item) => item.version)
+      // 如果 currentVersion 有效,则只保留比它高的
+      .filter((item) => {
+        if (!currentVersion || !isValidVersion(currentVersion)) return true
+        return compareVersions(item.version!.split('-')[0], currentVersion) > 0
+      })
+      // 升序排序
+      .sort((a, b) => compareVersions(a.version!.split('-')[0], b.version!.split('-')[0]))
+  )
+}

+ 99 - 0
src/views/device/detail/components/DeviceUpgrade/index.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="deviceUpgrade">
+    <div class="version">{{ currentVersion }}</div>
+    <div class="action">
+      <a-select
+        v-model:value="selectValue"
+        placeholder="请选择"
+        style="width: 100px"
+        size="small"
+        :options="otaList"
+      />
+
+      <a-button type="link" size="small" :disabled="!selectValue" @click="upgradeHandler"
+        >升级</a-button
+      >
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import * as deviceApi from '@/api/device'
+import { filterAndSortOtaList } from '@/utils/ota'
+
+defineOptions({
+  name: 'DeviceUpgrade',
+})
+
+const emit = defineEmits(['success'])
+const props = withDefaults(
+  defineProps<{
+    version: string
+  }>(),
+  {
+    version: '',
+  }
+)
+
+const selectValue = ref<string | undefined>(undefined)
+const currentVersion = ref<string>(props.version)
+const otaList = ref<{ label: string; value: string }[]>([])
+
+const fetchOTAList = async (): Promise<void> => {
+  try {
+    const resp = await deviceApi.getOtaList()
+    if (Array.isArray(resp?.data)) {
+      otaList.value = filterAndSortOtaList(resp.data, currentVersion.value).map((item) => ({
+        label: item.label,
+        value: item.value,
+      }))
+    } else {
+      otaList.value = []
+    }
+  } catch (err) {
+    console.error('获取 OTA 列表失败', err)
+    otaList.value = []
+  }
+}
+fetchOTAList()
+
+watch(
+  () => props.version,
+  (newVersion) => {
+    currentVersion.value = newVersion
+    fetchOTAList()
+  },
+  {
+    immediate: true,
+  }
+)
+
+const upgradeHandler = async (): Promise<void> => {
+  if (!selectValue.value) return
+  try {
+    const payload = { clientIds: [currentVersion.value], ossUrl: selectValue.value }
+    await deviceApi.updateOta(payload)
+    emit('success')
+  } catch (err) {
+    console.error('升级失败', err)
+  }
+}
+</script>
+
+<style scoped lang="less">
+.deviceUpgrade {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  .version {
+    font-size: 14px;
+  }
+  .action {
+    display: flex;
+    align-items: center;
+    gap: 5px;
+  }
+}
+</style>

+ 9 - 1
src/views/device/detail/index.vue

@@ -74,7 +74,9 @@
           <info-item label="设备ID">{{ detailState.clientId }}</info-item>
           <info-item label="设备名称">{{ detailState.devName }}</info-item>
           <info-item label="设备类型">{{ detailState.devType }}</info-item>
-          <info-item label="固件版本号">{{ detailState.hardware }}</info-item>
+          <info-item label="固件版本号">
+            <DeviceUpgrade :version="detailState.hardware" @success="updateSuccess"></DeviceUpgrade>
+          </info-item>
           <info-item label="激活日期">{{ detailState.activeTime }}</info-item>
           <info-item label="在离线状态">
             <template v-if="detailState.clientId">
@@ -282,6 +284,7 @@ import { Empty } from 'ant-design-vue'
 const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
 import { getOriginPosition } from '@/utils'
 import { useDict } from '@/hooks/useDict'
+import DeviceUpgrade from './components/DeviceUpgrade/index.vue'
 
 defineOptions({
   name: 'DeviceDetail',
@@ -763,6 +766,11 @@ const swtichAlarmItem = async (id: number, swtich: boolean, item: AlarmPlanItem)
     message.error('变更失败')
   }
 }
+
+// 设备固件升级成功
+const updateSuccess = () => {
+  message.success('升级指令已发送,请耐心等待设备升级')
+}
 </script>
 
 <style scoped lang="less">