Преглед изворни кода

feat: 设备列表,新增未激活设备搜索与展示;

liujia пре 4 недеља
родитељ
комит
04737820b6

+ 1 - 0
components.d.ts

@@ -49,6 +49,7 @@ declare module 'vue' {
     ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
     ASpace: typeof import('ant-design-vue/es')['Space']
     ASpin: typeof import('ant-design-vue/es')['Spin']
+    ASteps: typeof import('ant-design-vue/es')['Steps']
     ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
     ASwitch: typeof import('ant-design-vue/es')['Switch']
     ATable: typeof import('ant-design-vue/es')['Table']

+ 11 - 1
src/api/device/index.ts

@@ -67,5 +67,15 @@ export const getDeviceDetailByDevId = (params: {
 export const updateDevice = (
   params: TYPE.UpdateDeviceParams
 ): Promise<ResponseData<TYPE.Device[]>> => {
-  return request.post('device/updateDevice', params)
+  return request.post('/device/updateDevice', params)
+}
+
+// OTA固件列表查询
+export const getOtaList = (): Promise<ResponseData<TYPE.OtaItem[]>> => {
+  return request.get('/device/OTA/query')
+}
+
+// OTA固件更新
+export const updateOta = (params: TYPE.UpdateOtaParams) => {
+  return request.post('/device/OTA/update', params)
 }

+ 15 - 0
src/api/device/types.ts

@@ -260,3 +260,18 @@ export interface UpdateDeviceParams {
   age?: number | null // 监护对象年龄
   guardianshipType?: string | null // 监护对象类型
 }
+
+/**
+ * OTA文件列表,返回数据
+ */
+export interface OtaItem {
+  fileId: ID // 文件ID
+  fileName: string // 文件名称
+  ossUrl: ID // 文件地址
+  createTime: string // 创建时间
+}
+
+export interface UpdateOtaParams {
+  clientIds: string[] // 设备ID合集
+  ossUrl: string // ota文件地址
+}

+ 304 - 0
src/views/device/list/components/OTADevice/index.vue

@@ -0,0 +1,304 @@
+<template>
+  <div ref="mod">
+    <a-modal
+      :get-container="() => $refs.mod"
+      :open="open"
+      :mask-closable="false"
+      :width="600"
+      @cancel="cancel"
+      :footer="null"
+    >
+      <template #title>
+        <div class="header">
+          <div class="title">{{ title }}</div>
+        </div>
+      </template>
+      <a-upload-dragger
+        v-model:fileList="fileList"
+        name="file"
+        :multiple="false"
+        :action="action"
+        :accept="acceptType"
+        :headers="headers"
+        :customRequest="customRequest"
+        :before-upload="beforeUpload"
+        @change="handleChange"
+        @drop="handleDrop"
+      >
+        <p class="ant-upload-drag-icon">
+          <inbox-outlined></inbox-outlined>
+        </p>
+        <p class="ant-upload-text">单击或拖动文件到此区域进行上传</p>
+        <p class="ant-upload-hint">支持 {{ allowedExt.join(', ') }} 格式文件,最大 10MB </p>
+      </a-upload-dragger>
+
+      <div class="notes">
+        <div class="title">固件列表 ({{ otaList && otaList.length }})</div>
+        <div v-for="(ota, index) in otaList" :key="ota.fileId as string" class="item">
+          <div class="item-index" :title="ota.fileName">{{ index + 1 }}</div>
+          <div class="item-name" :title="ota.fileName">{{ ota.fileName }}</div>
+          <div class="item-time">{{ ota.createTime }}</div>
+        </div>
+      </div>
+
+      <div class="footer"> </div>
+    </a-modal>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import { message, type UploadChangeParam, Upload } from 'ant-design-vue'
+import { InboxOutlined } from '@ant-design/icons-vue'
+import { useUserStore } from '@/stores/user'
+import axios, { type AxiosProgressEvent, type AxiosResponse } from 'axios'
+import type { UploadRequestOption } from 'ant-design-vue/es/vc-upload/interface'
+import * as deviceApi from '@/api/device'
+import type { OtaItem } from '@/api/device/types'
+
+defineOptions({
+  name: 'OTADevice',
+})
+
+const userStore = useUserStore()
+
+type Props = {
+  open: boolean
+  title?: string
+}
+const emit = defineEmits<{
+  (e: 'update:open', value: boolean): void
+  (e: 'success', value: void): void
+}>()
+
+const props = withDefaults(defineProps<Props>(), {
+  open: false,
+  title: 'OTA升级',
+})
+
+const fileList = ref([])
+const action = `${import.meta.env.VITE_API_BASE_URL}/system/OTAUpload`
+const acceptType = '.bin,.hex,.fw'
+
+const handleChange = (info: UploadChangeParam) => {
+  console.log('handleChange', info)
+  const status = info.file.status
+  if (status !== 'uploading') {
+    console.log(info.file, info.fileList)
+  }
+  if (status === 'done') {
+    console.log(`${info.file.name} file uploaded successfully.`)
+  } else if (status === 'error') {
+    console.log(`${info.file.name} file upload failed.`)
+  }
+}
+
+function handleDrop(e: DragEvent) {
+  console.log(e)
+  // 获取拖拽的文件
+  if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) {
+    const file = e.dataTransfer.files[0]
+    // 调用beforeUpload进行验证
+    const isValid = beforeUpload(file)
+    if (isValid !== true) {
+      // 如果验证失败,清空文件列表
+      fileList.value = []
+    }
+  }
+}
+
+const headers = {
+  'content-type': 'multipart/form-data',
+  Authorization: userStore.userInfo.tokenValue,
+}
+
+// 自定义上传文件类型(扩展原生 File 类型)
+interface CustomUploadFile extends File {
+  uid: string
+  readonly lastModifiedDate: Date
+}
+
+// 进度回调函数类型
+type UploadProgressHandler = (progressEvent: {
+  percent: number
+  loaded: number
+  total: number
+  event: ProgressEvent
+}) => void
+
+// 导入设备ID重复列表
+const repeateIds = ref<string[]>([])
+// 成功回调函数类型
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type UploadSuccessHandler = <T = any>(response: T, event?: ProgressEvent) => void
+
+const customRequest = (options: UploadRequestOption) => {
+  const { file, onProgress, onSuccess, onError } = options
+  const customFile = file as CustomUploadFile
+
+  // 创建 FormData
+  const formData = new FormData()
+  formData.append('file', customFile)
+
+  // 处理进度事件
+  const handleProgress: UploadProgressHandler = (progressEvent) => {
+    // 确保进度在 0-100 范围内
+    const percent = Math.min(100, Math.max(0, progressEvent.percent))
+    onProgress?.({ percent })
+  }
+
+  // 处理成功响应
+  const handleSuccess: UploadSuccessHandler = (response) => {
+    onSuccess?.(response)
+  }
+
+  repeateIds.value = []
+  // 发送请求
+  axios
+    .post(action, formData, {
+      headers: { 'Content-Type': 'multipart/form-data' },
+      onUploadProgress: (progressEvent: AxiosProgressEvent) => {
+        if (!progressEvent.total) return
+
+        const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
+
+        handleProgress({
+          percent,
+          loaded: progressEvent.loaded,
+          total: progressEvent.total,
+          event: progressEvent.event,
+        })
+      },
+    })
+    .then((response: AxiosResponse) => {
+      if (['200', 200].includes(response.data.code)) {
+        handleSuccess(response.data)
+        console.log('✅上传成功', response.data)
+        message.success('上传成功')
+        fetchOTAList()
+      } else {
+        const errorMsg = response.data.message
+        message.error(errorMsg)
+        onError?.(response.data)
+      }
+    })
+    .catch((error) => {
+      console.log('❌上传失败', error)
+      const errorMsg = error.code === 'ERR_NETWORK' ? '网络异常,请重试' : error.message
+      error.message = errorMsg
+      message.error(errorMsg)
+      onError?.(error)
+    })
+
+  return {
+    abort() {
+      console.log('上传取消', customFile.name)
+    },
+  }
+}
+
+const allowedExt = ['.bin', '.hex', '.fw']
+const beforeUpload = (file: File) => {
+  const lowerName = file.name.toLowerCase()
+  const hasAllowedExt = allowedExt.some((ext) => lowerName.endsWith(ext))
+
+  if (!hasAllowedExt) {
+    message.error(`只能上传 ${allowedExt.join(', ')} 格式的文件!`)
+    return Upload.LIST_IGNORE
+  }
+
+  const isLt10M = file.size / 1024 / 1024 <= 10
+  if (!isLt10M) {
+    message.error('文件大小不能超过10MB!')
+    return Upload.LIST_IGNORE
+  }
+
+  return true
+}
+
+const cancel = () => {
+  fileList.value = []
+  repeateIds.value = []
+  emit('update:open', false)
+}
+
+const otaList = ref<OtaItem[]>([])
+const fetchOTAList = async () => {
+  try {
+    const response = await deviceApi.getOtaList()
+    otaList.value = response.data
+    console.log('获取OTA列表 成功 ✅', response)
+  } catch (error) {
+    console.log('获取OTA列表 失败 ❌', error)
+  }
+}
+
+watch(
+  () => props.open,
+  (newVal) => {
+    if (newVal) {
+      fetchOTAList()
+    }
+  }
+)
+</script>
+
+<style scoped lang="less">
+:deep(.ant-modal) {
+  .header {
+    display: flex;
+    align-items: center;
+  }
+
+  .notes {
+    background-color: #f7f7f7;
+    padding: 16px;
+    border-radius: 8px;
+    margin-top: 24px;
+    margin-top: 24px;
+    height: 400px;
+    overflow-y: auto;
+
+    .title {
+      font-size: 16px;
+      font-weight: 600;
+    }
+
+    .item {
+      display: flex;
+      color: #333;
+      opacity: 0.6;
+      line-height: 2;
+      &:hover {
+        opacity: 1;
+      }
+      &-index {
+        font-size: 14px;
+        font-weight: 600;
+        margin-right: 10px;
+        flex-shrink: 0;
+        color: #1677ff;
+      }
+      &-name {
+        font-size: 14px;
+        font-weight: 600;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        flex-shrink: 1;
+        margin-right: 10px;
+      }
+      &-time {
+        font-size: 12px;
+        margin-left: auto;
+        flex-shrink: 0;
+      }
+    }
+  }
+
+  .footer {
+    margin-top: 24px;
+    text-align: right;
+  }
+}
+</style>

+ 4 - 0
src/views/device/list/const.ts

@@ -66,4 +66,8 @@ export const deviceStatusOptions: SelectProps['options'] = [
     label: '离线',
     value: 0,
   },
+  {
+    label: '未激活',
+    value: 9,
+  },
 ]

+ 10 - 16
src/views/device/list/index.vue

@@ -58,8 +58,9 @@
         </div>
         <div class="tableCard-header-extra">
           <a-space>
-            <a-button @click="addDeviceHandler">添加设备</a-button>
-            <a-button type="primary" @click="uploadDeviceHandler">批量上传设备</a-button>
+            <!-- <a-button @click="otaDeviceOpen = true">OTA升级</a-button> -->
+            <a-button @click="addDeviceOpen = true">添加设备</a-button>
+            <a-button type="primary" @click="uploadDeviceOpen = true">批量上传设备</a-button>
           </a-space>
         </div>
       </div>
@@ -72,7 +73,7 @@
       >
         <template #bodyCell="{ column, record }">
           <template v-if="column.key === 'online'">
-            <a-tag v-if="record.online === -1" :bordered="false" color="gray">未激活</a-tag>
+            <a-tag v-if="record.online === 9" :bordered="false" color="#ccc">未激活</a-tag>
             <a-tag v-if="record.online === 0" :bordered="false" color="red">离线</a-tag>
             <a-tag v-if="record.online === 1" :bordered="false" color="green">在线</a-tag>
           </template>
@@ -114,6 +115,8 @@
       @success="searchHandler"
     ></upload-device-modal>
 
+    <OTADeviceModal v-model:open="otaDeviceOpen" title="设备OTA升级"></OTADeviceModal>
+
     <baseModal v-model:open="unbindOpen" title="解绑设备">
       <a-descriptions title="" bordered :column="1" size="middle">
         <a-descriptions-item label="设备ID">{{ unbindDeviceData.devId }}</a-descriptions-item>
@@ -154,6 +157,7 @@ import type { Device } from '@/api/device/types'
 import { columns, deviceStatusOptions } from './const'
 import addDeviceModal from './components/addDevice/index.vue'
 import uploadDeviceModal from './components/uploadDevice/index.vue'
+import OTADeviceModal from './components/OTADevice/index.vue'
 import { useSearch } from '@/hooks/useSearch'
 import { useRouter } from 'vue-router'
 import * as tenantAPI from '@/api/tenant'
@@ -200,7 +204,7 @@ const paginationSizeChange = (current: number, pageSize: number) => {
   console.log('showSizeChange', current, pageSize)
 }
 
-const allDeviceTotal = ref(0) // 所设备数量
+const allDeviceTotal = ref(0) // 所设备数量
 const onlineDeviceTotal = ref(0) // 在线设备数量
 const offlineDeviceTotal = ref(0) // 离线设备数量
 
@@ -236,7 +240,7 @@ const fetchList = async () => {
       createTimeEnd: searchState.createTimeEnd,
       online: 1,
     })
-    allDeviceTotal.value = Number(allDeviceRes.data.total) || 0 // 所设备数量
+    allDeviceTotal.value = Number(allDeviceRes.data.total) || 0 // 所设备数量
     onlineDeviceTotal.value = Number(onlineDeviceRes.data.total) || 0 // 在线设备数量
     offlineDeviceTotal.value = allDeviceTotal.value - onlineDeviceTotal.value // 离线设备数量
     console.log('✅获取到设备信息', res, {
@@ -389,18 +393,8 @@ const fetchDeviceDetail = async (devId: number) => {
 }
 
 const addDeviceOpen = ref(false)
-// 添加设备
-const addDeviceHandler = () => {
-  console.log('添加设备')
-  addDeviceOpen.value = true
-}
-
 const uploadDeviceOpen = ref(false)
-// 批量上传设备
-const uploadDeviceHandler = () => {
-  console.log('批量上传设备')
-  uploadDeviceOpen.value = true
-}
+const otaDeviceOpen = ref(false)
 
 // 是否为今天
 function isToday(dateStr: string): boolean {