Browse Source

设备列表重构

李富豪 1 year ago
parent
commit
5112e060df

+ 1 - 2
Web/src/api/manage.ts

@@ -30,7 +30,6 @@ export interface HmsQueryBody {
 }
 
 export const login = async function (body: LoginBody): Promise<IWorkspaceResponse<any>> {
-  console.log("LoginBody", body)
   const url = `${HTTP_PREFIX}/login`
   const result = await request.post(url, body)
   return result.data
@@ -128,7 +127,7 @@ export const getDeviceBySn = async function (workspace_id: string, device_sn: st
  * @param domain
  * @returns
  */
-export const getBindingDevices = async function (workspace_id: string, body: IPage): Promise<IListWorkspaceResponse<Device>> {
+export const getBindingDevices = async function (workspace_id: string, body: { page: number, page_size: number }): Promise<IListWorkspaceResponse<Device>> {
   const url = `${HTTP_PREFIX}/devices/${workspace_id}/devices/bound?&page=${body.page}&page_size=${body.page_size}`
   const result = await request.get(url)
   return result.data

+ 1 - 1
Web/src/api/media.ts

@@ -3,7 +3,7 @@ import request, { IPage, IWorkspaceResponse } from '/@/api/http/request'
 const HTTP_PREFIX = '/media/api/v1'
 
 // Get Media Files
-export const getMediaFiles = async function (wid: string, pagination: IPage): Promise<IWorkspaceResponse<any>> {
+export const getMediaFiles = async function (wid: string, pagination: { page: number, page_size: number }): Promise<IWorkspaceResponse<any>> {
   const url = `${HTTP_PREFIX}/files/${wid}/files?page=${pagination.page}&page_size=${pagination.page_size}`
   const result = await request.get(url)
   return result.data

+ 1 - 0
Web/src/assets/media/file.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1719905139221" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1537" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M848.8576 199.1936H415.7568c0-26.5728-21.5424-48.128-48.128-48.128H175.1424c-26.5728 0-48.128 21.5424-48.128 48.128V343.5648c0 26.5984 21.5424 48.1408 48.128 48.1408h673.728c26.5728 0 48.128-21.5424 48.128-48.1408v-96.2432c-0.0128-26.5856-21.5552-48.128-48.1408-48.128z" fill="#CCA352" p-id="1538"></path><path d="M800.7424 247.3088H223.2576c-26.5728 0-48.128 21.5424-48.128 48.128v48.128c0 26.5984 21.5424 48.1408 48.128 48.1408h577.472c26.5728 0 48.128-21.5424 48.128-48.1408v-48.128c0-26.5728-21.5424-48.128-48.1152-48.128z" fill="#FFFFFF" p-id="1539"></path><path d="M848.8576 295.4368H175.1424c-26.5728 0-48.128 21.5424-48.128 48.128v481.2544c0 26.5472 21.5424 48.128 48.128 48.128h673.728c26.5728 0 48.128-21.568 48.128-48.128V343.552c-0.0128-26.5728-21.5552-48.1152-48.1408-48.1152z" fill="#FFCC66" p-id="1540"></path></svg>

+ 1 - 0
Web/src/assets/media/picture.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1719905467401" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10546" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M930.936471 0C992.978824 0 1024 31.021176 1024 93.063529v837.872942c0 62.042353-31.021176 93.063529-93.063529 93.063529H93.063529C31.021176 1024 0 992.978824 0 930.936471V93.063529C0 31.021176 31.021176 0 93.063529 0h837.872942zM601.630118 384.722824a15.811765 15.811765 0 0 0-13.70353 7.318588l-147.57647 231.303529-67.282824-65.867294a15.811765 15.811765 0 0 0-24.18447 2.379294l-101.797648 149.624471a15.811765 15.811765 0 0 0 13.101177 24.69647h524.468706a15.811765 15.811765 0 0 0 13.733647-23.732706l-183.416471-317.801411a15.811765 15.811765 0 0 0-13.342117-7.920941z m-244.254118-93.816471a79.811765 79.811765 0 1 0 0 159.593412 79.811765 79.811765 0 0 0 0-159.593412z" p-id="10547" fill="#c0c6d1"></path></svg>

+ 1 - 0
Web/src/assets/media/video.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1719905321017" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5358" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M450.048 213.504L305.152 42.496H140.8l144.896 170.496h164.352z m426.496 0L731.648 42.496h-164.352l144.896 170.496h164.352z m-212.992 0L518.656 42.496H354.304l144.896 170.496h164.352z m274.944-171.008h-157.696l144.896 170.496h76.8V106.496c0-36.352-27.648-64-64-64z m-846.848 0h-6.144c-36.352 0-64 27.648-64 64v106.496h215.552L91.648 42.496zM21.504 917.504c0 36.352 27.648 64 64 64h853.504c36.352 0 64-27.648 64-64V256H21.504v661.504z m340.992-469.504c0-36.352 29.696-53.248 64-53.248 10.752 0 23.552 2.048 34.304 8.704l243.2 140.8a60.416 60.416 0 0 1 0 106.496L460.8 791.552c-10.752 6.656-21.504 8.704-34.304 8.704-34.304 0-64-16.896-64-53.248V448z" fill="#c0c6d1" p-id="5359"></path></svg>

+ 1 - 1
Web/src/components/devices/changeRecord/components/CustomCell.vue

@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<Props>(), {
     justify-content: center;
 
     &-item {
-        height: 30px;
+        height: 40px;
         display: flex;
         align-items: center;
     }

+ 9 - 8
Web/src/components/devices/device-hms/DeviceHmsDrawer.vue

@@ -1,7 +1,7 @@
 <template>
   <a-drawer title="设备告警信息" placement="right" v-model:visible="sVisible" @update:visible="onVisibleChange"
     :destroyOnClose="true" :width="800">
-    <div class="flex-row flex-align-center">
+    <div class="flex-row flex-align-center" style="margin-bottom: 20px;">
       <div style="width: 240px;">
         <a-range-picker v-model:value="time" format="YYYY-MM-DD" @change="onTimeChange" />
       </div>
@@ -134,13 +134,14 @@ function getPaginationBody() {
 function showHms() {
   const dock = props.device
   if (!dock) return
-  if (dock.domain === EDeviceTypeName.Dock) {
-    getDeviceHmsBySn(dock.device_sn, dock.children?.[0].device_sn ?? '')
-  }
-  if (dock.domain === EDeviceTypeName.Aircraft) {
-    param.domain = EDeviceTypeName.Aircraft
-    getDeviceHmsBySn('', dock.device_sn)
-  }
+  // 先注释掉,这里机场与飞机合并在一起了
+  // if (dock.domain === EDeviceTypeName.Dock) {
+  //   getDeviceHmsBySn(dock.device_sn, dock.children?.[0].device_sn ?? '')
+  // }
+  // if (dock.domain === EDeviceTypeName.Aircraft) {
+  //   param.domain = EDeviceTypeName.Aircraft
+  //   getDeviceHmsBySn('', dock.device_sn)
+  // }
 }
 
 function refreshHmsData(page: Pagination) {

+ 0 - 1
Web/src/components/devices/device-log/DeviceLogUploadRecordDrawer.vue

@@ -65,7 +65,6 @@
   <!-- 设备日志上传弹框 -->
   <DeviceLogUploadModal v-model:visible="deviceLogUploadModalVisible" :device="props.device"
     @upload-log-ok="onUploadLogOk"></DeviceLogUploadModal>
-
   <!-- 设备日志上传详情弹框 -->
   <DeviceLogDetailModal v-model:visible="deviceLogDetailModalVisible" :deviceLog="currentDeviceLog">
   </DeviceLogDetailModal>

+ 3 - 2
Web/src/components/devices/device-upgrade/DeviceFirmwareUpgrade.vue

@@ -1,7 +1,5 @@
 <template>
   <div class="firmware_upgrade_wrap">
-    <!-- 版本 -->
-    <span class="version"> {{ device.firmware_version }}</span>
     <!-- tag -->
     <span v-if="getTagStatus(device)" class="status-tag pointer">
       <a-tag class="pointer" :color="getFirmwareTag(device.firmware_status).color" @click="deviceUpgrade(device)">
@@ -12,6 +10,9 @@
     <span v-if="device.firmware_status === DeviceFirmwareStatusEnum.DuringUpgrade">
       {{ `${device.firmware_progress}` }}
     </span>
+    <span v-else>
+      不支持
+    </span>
   </div>
 </template>
 

+ 54 - 0
Web/src/components/devices/deviceList/components/CustomCell.vue

@@ -0,0 +1,54 @@
+<template>
+    <div class="customCell">
+        <template v-if="isEdit">
+            <div class="customCell-item">
+                <a-input v-model:value="record[fieldName]" />
+            </div>
+            <div class="customCell-item">
+                <a-input v-model:value="record.children[fieldName]" />
+            </div>
+        </template>
+        <template v-else>
+            <div class="customCell-item">
+                {{ record[fieldName] || '--' }}
+            </div>
+            <div class="customCell-item" v-if="record.children">
+                <div class="mt-5 ml0"
+                    style=" width:16px;height:16px;border-left:2px solid rgb(200,200,200);border-bottom:2px solid rgb(200,200,200);float:left;margin-right:5px;"
+                    v-if="showIcon">
+                </div>
+                <div>
+                    {{ record.children[fieldName] || '--' }}
+                </div>
+            </div>
+        </template>
+    </div>
+</template>
+
+<script lang="ts" setup>
+interface Props {
+    record: any,
+    fieldName: string,
+    showIcon?: boolean,
+    isEdit?: boolean,
+};
+
+const props = withDefaults(defineProps<Props>(), {
+    showIcon: false,
+    isEdit: false,
+});
+</script>
+
+<style lang="scss" scoped>
+.customCell {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+
+    &-item {
+        height: 40px;
+        display: flex;
+        align-items: center;
+    }
+}
+</style>

+ 3 - 1
Web/src/components/devices/deviceList/components/Search.vue

@@ -26,7 +26,7 @@
           如何绑定机场?
         </a>
       </a-popover>
-      <a-button>
+      <a-button type="primary" danger v-if="selectedRowKeys.length" @click="onClickDelete">
         删除
       </a-button>
     </a-col>
@@ -111,6 +111,8 @@ import { ref, reactive } from 'vue';
 import { ReadOutlined, MenuOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
 
 interface Props {
+  selectedRowKeys: string[],
+  onClickDelete: () => Promise<any>,
   onClickSearch: (query: any) => Promise<any>,
   onClickReset: (query: any) => Promise<any>,
 };

+ 137 - 223
Web/src/components/devices/deviceList/index.vue

@@ -1,75 +1,64 @@
 <template>
   <div class="deviceList">
-    <Search :onClickSearch="async () => { }" :onClickReset="async () => { }" />
+    <Search :selectedRowKeys="state.selectedRowKeys" :onClickDelete="onClickBatchDelete"
+      :onClickSearch="async () => { }" :onClickReset="async () => { }" />
     <div class="deviceList-table">
-      <a-table :columns="columns" :data-source="data.device" :pagination="paginationProp" @change="refreshData"
-        row-key="device_sn" :expandedRowKeys="expandRows" :row-selection="rowSelection" :rowClassName="rowClassName"
-        :scroll="{ x: '100%', y: 500 }" :expandIcon="expandIcon" :loading="loading">
-        <template v-for="col in ['nickname']" #[col]="{ text, record }" :key="col">
-          <div>
-            <a-input v-if="editableData[record.device_sn]" v-model:value="editableData[record.device_sn][col]" />
-            <template v-else>
-              <a-tooltip :title="text">
-                {{ text }}
-              </a-tooltip>
-            </template>
-          </div>
+      <a-table :scroll="{ x: '100%', y: 500 }" :childrenColumnName="null" rowKey="device_sn"
+        :loading="state.listLoading" :columns="columns" :dataSource="state.list" @change="refreshData"
+        :rowClassName="rowClassName" :pagination="paginationConfig" :rowSelection="rowSelection">
+        <!-- 设备型号 -->
+        <template #device_name="{ record }">
+          <CustomCell :record="record" fieldName="device_name" :showIcon="true" />
+        </template>
+        <!-- 设备SN -->
+        <template #device_sn="{ record }">
+          <CustomCell :record="record" fieldName="device_sn" />
         </template>
-        <template v-for="col in ['sn', 'workspace']" #[col]="{ text }" :key="col">
-          <a-tooltip :title="text">
-            <span>{{ text }}</span>
-          </a-tooltip>
+        <!-- 设备名称 -->
+        <template #nickname="{ record }">
+          <CustomCell :record="record" fieldName="nickname" :isEdit="!!state.editableData[record.device_sn]" />
         </template>
         <!-- 固件版本 -->
         <template #firmware_version="{ record }">
-          <span v-if="record.domin === 3">
-            <DeviceFirmwareUpgrade :device="record" class="table-flex-col" @device-upgrade="onDeviceUpgrade" />
-          </span>
-          <span v-else>
-            {{ record.firmware_version }}
-          </span>
+          <CustomCell :record="record" fieldName="firmware_version" />
         </template>
         <!-- 固件升级 -->
-        <template #firmware_status="{ text }">
-          <div v-if="text === -1">
-            不支持
-          </div>
-          <div v-else>
-            {{ DeviceFirmwareStatus[text] }}
-          </div>
+        <template #firmware_status="{ record }">
+          <DeviceFirmwareUpgrade :device="record" />
         </template>
-        <!-- 状态 -->
-        <template #status="{ text }">
-          <span v-if="text" class="flex-row flex-align-center">
-            <span class="mr5" style="width: 12px; height: 12px; border-radius: 50%; background-color: green;" />
-            <span>在线</span>
-          </span>
-          <span class="flex-row flex-align-center" v-else>
-            <span class="mr5" style="width: 12px; height: 12px; border-radius: 50%; background-color: red;" />
-            <span>离线</span>
-          </span>
+        <!-- 当前状态 -->
+        <template #status="{ record }">
+          <CustomCell :record="record" fieldName="status" />
+        </template>
+        <!-- 加入项目时间 -->
+        <template #bound_time="{ record }">
+          <CustomCell :record="record" fieldName="bound_time" />
+        </template>
+        <!-- 最后在线时间 -->
+        <template #login_time="{ record }">
+          <CustomCell :record="record" fieldName="login_time" />
         </template>
         <!-- 操作 -->
         <template #action="{ record }">
           <!-- 编辑态操作 -->
-          <div v-if="editableData[record.device_sn]">
+          <div v-if="state.editableData[record.device_sn]">
             <a-tooltip title="确定">
-              <CheckOutlined style="color: #28d445;margin-right: 10px;" @click="save(record)" />
+              <CheckOutlined style="color: #28d445;margin-right: 10px;" @click="onClickSave(record)" />
             </a-tooltip>
             <a-tooltip title="取消">
-              <CloseOutlined style="color: #e70102;" @click="() => delete editableData[record.device_sn]" />
+              <CloseOutlined style="color: #e70102;" @click="() => delete state.editableData[record.device_sn]" />
             </a-tooltip>
           </div>
           <!-- 非编辑态操作 -->
           <div v-else class="flex-align-center flex-row" style="color: #2d8cf0">
             <a-tooltip title="设备日志">
-              <CloudServerOutlined style="margin-right: 10px;" @click="showDeviceLogUploadRecord(record)" />
+              <CloudServerOutlined style="margin-right: 10px;" @click="onClickDeviceLog(record)" />
             </a-tooltip>
             <a-tooltip title="告警信息">
-              <FileSearchOutlined style="margin-right: 10px;" @click="showHms(record)" />
+              <FileSearchOutlined style="margin-right: 10px;" @click="onClickDeviceHms(record)" />
             </a-tooltip>
             <a-tooltip title="编辑">
-              <EditOutlined style="margin-right: 10px;" @click="edit(record)" />
+              <EditOutlined style="margin-right: 10px;" @click="onClickEdit(record)" />
             </a-tooltip>
             <a-tooltip title="删除">
               <DeleteOutlined @click="onClickDelete(record)" />
@@ -77,35 +66,76 @@
           </div>
         </template>
       </a-table>
-      <!-- 设备升级 -->
-      <DeviceFirmwareUpgradeModal title="设备升级" v-model:visible="deviceFirmwareUpgradeModalVisible"
-        :device="selectedDevice" @ok="onUpgradeDeviceOk" />
-      <!-- 设备日志上传记录 -->
-      <DeviceLogUploadRecordDrawer v-model:visible="deviceLogUploadRecordVisible" :device="currentDevice" />
-      <!-- hms 信息 -->
-      <DeviceHmsDrawer v-model:visible="hmsVisible" :device="currentDevice" />
     </div>
   </div>
+  <!-- 设备日志 -->
+  <DeviceLogUploadRecordDrawer v-model:visible="state.deviceLogDrawerVisible" :device="state.currentDevice" />
+  <!-- 告警信息 -->
+  <DeviceHmsDrawer v-model:visible="state.deviceHmsDrawerVisible" :device="state.currentDevice" />
 </template>
 
 <script lang="ts" setup>
-import { h, onMounted, reactive, ref, UnwrapRef } from 'vue'
-import { Modal, notification } from 'ant-design-vue'
-import { EditOutlined, CheckOutlined, CloseOutlined, DeleteOutlined, FileSearchOutlined, CloudServerOutlined } from '@ant-design/icons-vue'
-import Search from './components/Search.vue'
-import { getBindingDevices, unbindDevice, updateDevice } from '/@/api/manage'
-import { Device, DeviceFirmwareStatus, DeviceFirmwareStatusEnum } from '/@/types/device'
-import DeviceFirmwareUpgrade from '/@/components/devices/device-upgrade/DeviceFirmwareUpgrade.vue'
-import DeviceFirmwareUpgradeModal from '/@/components/devices/device-upgrade/DeviceFirmwareUpgradeModal.vue'
-import { useDeviceFirmwareUpgrade } from '/@/components/devices/device-upgrade/use-device-upgrade'
-import { useDeviceUpgradeEvent } from '/@/components/devices/device-upgrade/use-device-upgrade-event'
-import { DeviceCmdExecuteInfo, DeviceCmdExecuteStatus } from '/@/types/device-cmd'
-import DeviceLogUploadRecordDrawer from '/@/components/devices/device-log/DeviceLogUploadRecordDrawer.vue'
-import DeviceHmsDrawer from '/@/components/devices/device-hms/DeviceHmsDrawer.vue'
-import { IPage } from '/@/api/http/type'
-import { EDeviceTypeName, ELocalStorageKey } from '/@/types'
+import { reactive, onMounted } from 'vue';
+import { Modal } from 'ant-design-vue';
+import { EditOutlined, CheckOutlined, CloseOutlined, DeleteOutlined, FileSearchOutlined, CloudServerOutlined } from '@ant-design/icons-vue';
+import Search from './components/Search.vue';
+import CustomCell from './components/CustomCell.vue';
+import DeviceFirmwareUpgrade from '/@/components/devices/device-upgrade/DeviceFirmwareUpgrade.vue';
+import DeviceLogUploadRecordDrawer from '/@/components/devices/device-log/DeviceLogUploadRecordDrawer.vue';
+import DeviceHmsDrawer from '/@/components/devices/device-hms/DeviceHmsDrawer.vue';
+import { getBindingDevices, updateDevice, unbindDevice } from '/@/api/manage';
+
+interface State {
+  workspaceId: string,
+  listLoading: boolean,
+  list: any[],
+  selectedRowKeys: string[],
+  currentDevice: any,
+  editableData: {
+    [key: string]: string,
+  },
+  deviceLogDrawerVisible: boolean,
+  deviceHmsDrawerVisible: boolean,
+};
+
+const state: State = reactive({
+  workspaceId: localStorage.getItem('workspace_id') || '',
+  listLoading: false,
+  list: [],
+  selectedRowKeys: [],
+  currentDevice: {},
+  editableData: {},
+  deviceLogDrawerVisible: false,
+  deviceHmsDrawerVisible: false,
+})
+
+const paginationConfig = reactive({
+  pageSizeOptions: ['20', '50', '100'],
+  showQuickJumper: true,
+  showSizeChanger: true,
+  pageSize: 20,
+  current: 1,
+  total: 0
+})
+
+const fetchList = async () => {
+  state.listLoading = true;
+  try {
+    const res = await getBindingDevices(state.workspaceId, { page: paginationConfig.current, page_size: paginationConfig.pageSize });
+    state.list = res.data.list;
+    paginationConfig.total = res.data.pagination.total
+    paginationConfig.current = res.data.pagination.page
+    paginationConfig.pageSize = res.data.pagination.page_size
+  } catch (e) {
+    console.error(e);
+  } finally {
+    state.listLoading = false;
+  }
+}
 
-const loading = ref(true)
+onMounted(async () => {
+  await fetchList();
+})
 
 const columns = [
   {
@@ -114,13 +144,14 @@ const columns = [
     width: 150,
     ellipsis: true,
     sorter: (a: any, b: any) => a.device_name.localeCompare(b.device_name),
+    slots: { customRender: 'device_name' }
   },
   {
     title: '设备SN',
     dataIndex: 'device_sn',
-    width: 200,
+    width: 250,
     ellipsis: true,
-    slots: { customRender: 'sn' }
+    slots: { customRender: 'device_sn' }
   },
   {
     title: '设备名称',
@@ -156,12 +187,14 @@ const columns = [
     dataIndex: 'bound_time',
     width: 200,
     sorter: (a: any, b: any) => a.bound_time.localeCompare(b.bound_time),
+    slots: { customRender: 'bound_time' },
   },
   {
     title: '最后在线时间',
     dataIndex: 'login_time',
     width: 200,
     sorter: (a: any, b: any) => a.login_time.localeCompare(b.login_time),
+    slots: { customRender: 'login_time' },
   },
   {
     title: '操作',
@@ -172,16 +205,6 @@ const columns = [
   },
 ]
 
-const expandIcon = (props: any) => {
-  if (!props.expanded) {
-    return h('div',
-      {
-        style: 'border-left: 2px solid rgb(200,200,200); border-bottom: 2px solid rgb(200,200,200); height: 16px; width: 16px; float: left;',
-        class: 'mt-5 ml0',
-      })
-  }
-}
-
 const rowClassName = (record: any, index: number) => {
   const className = []
   if ((index & 1) === 0) {
@@ -190,172 +213,63 @@ const rowClassName = (record: any, index: number) => {
   return className.toString().replaceAll(',', ' ')
 }
 
-const expandRows = ref<string[]>([])
-
-const data: {
-  device: Device[]
-} = reactive({
-  device: []
-})
-
-const paginationProp = reactive({
-  pageSizeOptions: ['20', '50', '100'],
-  showQuickJumper: true,
-  showSizeChanger: true,
-  pageSize: 20,
-  current: 1,
-  total: 0
-})
-
-// 获取分页信息
-function getPaginationBody() {
-  return {
-    page: paginationProp.current,
-    page_size: paginationProp.pageSize
-  } as IPage
+const refreshData = async (page: any) => {
+  paginationConfig.current = page?.current!
+  paginationConfig.pageSize = page?.pageSize!
+  await fetchList();
 }
 
 const rowSelection = {
-  onChange: (selectedRowKeys: (string | number)[], selectedRows: []) => {
-    console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows)
-  },
-  onSelect: (record: any, selected: boolean, selectedRows: []) => {
-    console.log(record, selected, selectedRows)
+  onChange: (selectedRowKeys: string[]) => {
+    state.selectedRowKeys = selectedRowKeys;
   },
-  onSelectAll: (selected: boolean, selectedRows: [], changeRows: []) => {
-    console.log(selected, selectedRows, changeRows)
-  },
-  getCheckboxProps: (record: any) => ({
-    disabled: record.domain !== EDeviceTypeName.Dock,
-  }),
 }
 
-const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || ''
-
-const editableData: UnwrapRef<Record<string, Device>> = reactive({})
-
-// 设备升级
-const {
-  deviceFirmwareUpgradeModalVisible,
-  selectedDevice,
-  onDeviceUpgrade,
-  onUpgradeDeviceOk
-} = useDeviceFirmwareUpgrade(workspaceId)
-
-function onDeviceUpgradeWs(payload: DeviceCmdExecuteInfo) {
-  updateDevicesByWs(data.device, payload)
+// 点击批量删除
+const onClickBatchDelete = async () => {
+  console.log(state.selectedRowKeys, '点击批量删除');
 }
 
-function updateDevicesByWs(devices: Device[], payload: DeviceCmdExecuteInfo) {
-  if (!devices || devices.length <= 0) {
-    return
-  }
-  for (let i = 0; i < devices.length; i++) {
-    if (devices[i].device_sn === payload.sn) {
-      if (!payload.output) return
-      const { status, progress, ext } = payload.output
-      if (status === DeviceCmdExecuteStatus.Sent || status === DeviceCmdExecuteStatus.InProgress) { // 升级中
-        const rate = ext?.rate ? (ext.rate / 1024).toFixed(2) + 'kb/s' : ''
-        devices[i].firmware_status = DeviceFirmwareStatusEnum.DuringUpgrade
-        devices[i].firmware_progress = (progress?.percent || 0) + '% ' + rate
-      } else { // 终态:成功,失败,超时
-        if (status === DeviceCmdExecuteStatus.Failed || status === DeviceCmdExecuteStatus.Timeout) {
-          notification.error({
-            message: `(${payload.sn}) Upgrade failed`,
-            description: `Error Code: ${payload.result}`,
-            duration: null
-          })
-        }
-        // 拉取列表
-        getDevices(true)
-      }
-      return
-    }
-    if (devices[i].children) {
-      updateDevicesByWs(devices[i].children || [], payload)
-    }
-  }
+// 点击设备日志
+const onClickDeviceLog = (record: any) => {
+  state.deviceLogDrawerVisible = true;
+  state.currentDevice = record;
 }
 
-useDeviceUpgradeEvent(onDeviceUpgradeWs)
-
-// 获取设备列表信息
-function getDevices(closeLoading?: boolean) {
-  if (!closeLoading) {
-    loading.value = true
-  }
-  getBindingDevices(workspaceId, getPaginationBody()).then(res => {
-    if (res.code !== 0) {
-      return
-    }
-    const resData: Device[] = res.data.list
-    expandRows.value = []
-    resData.forEach((val: any) => {
-      if (val.children) {
-        val.children = [val.children]
-        expandRows.value.push(val.device_sn);
-      }
-    })
-    data.device = resData;
-    paginationProp.total = res.data.pagination.total
-    paginationProp.current = res.data.pagination.page
-    paginationProp.pageSize = res.data.pagination.page_size
-    loading.value = false
-  })
+// 点击告警信息
+const onClickDeviceHms = (record: any) => {
+  state.deviceHmsDrawerVisible = true;
+  state.currentDevice = record;
 }
 
-function refreshData(page: any) {
-  paginationProp.current = page?.current!
-  paginationProp.pageSize = page?.pageSize!
-  getDevices()
+// 点击编辑
+const onClickEdit = (record: any) => {
+  state.editableData[record.device_sn] = record;
 }
 
-// 编辑
-function edit(record: Device) {
-  editableData[record.device_sn] = record
-}
-
-// 保存
-function save(record: Device) {
-  delete editableData[record.device_sn]
-  updateDevice({ nickname: record.nickname }, workspaceId, record.device_sn)
+// 点击保存
+const onClickSave = async (record: any) => {
+  delete state.editableData[record.device_sn];
+  await updateDevice({ nickname: record.nickname }, state.workspaceId, record.device_sn)
+  if (record.children) {
+    await updateDevice({ nickname: record.children.nickname }, state.workspaceId, record.children.device_sn)
+  }
 }
 
 // 点击删除
-const onClickDelete = (record: Device) => {
+const onClickDelete = (record: any) => {
   Modal.confirm({
     title: '提示',
     content: `确定删除${record.device_name}吗?`,
     onOk: async () => {
-      unbindDevice(record.device_sn).then(res => {
-        if (res.code !== 0) {
-          return
-        }
-        getDevices()
-      })
+      const res = await unbindDevice(record.device_sn);
+      if (res.code !== 0) {
+        return
+      }
+      await fetchList();
     },
   });
 }
-
-const currentDevice = ref({} as Device)
-// 设备日志
-const deviceLogUploadRecordVisible = ref(false)
-function showDeviceLogUploadRecord(dock: Device) {
-  deviceLogUploadRecordVisible.value = true
-  currentDevice.value = dock
-}
-
-// 健康状态
-const hmsVisible = ref<boolean>(false)
-
-function showHms(dock: Device) {
-  hmsVisible.value = true
-  currentDevice.value = dock
-}
-
-onMounted(() => {
-  getDevices()
-})
 </script>
 
 <style lang="scss">

+ 1 - 1
Web/src/components/devices/feedbackRecord/components/CustomCell.vue

@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<Props>(), {
     justify-content: center;
 
     &-item {
-        height: 30px;
+        height: 40px;
         display: flex;
         align-items: center;
     }

+ 1 - 1
Web/src/components/devices/feedbackRecord/components/Search.vue

@@ -60,7 +60,7 @@
 
 <script lang="ts" setup>
 import { ref, reactive } from 'vue';
-import { MenuOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
+import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
 
 interface Props {
   onClickSearch: (query: any) => Promise<any>,

+ 11 - 0
Web/src/pages/page-web/projects/media/detail/index.vue

@@ -0,0 +1,11 @@
+<template>
+  <div>
+    这是媒体详情
+  </div>
+</template>
+
+<script lang="ts" setup>
+
+</script>
+
+<style lang="scss" scoped></style>

+ 0 - 11
Web/src/pages/page-web/projects/media/index.vue

@@ -1,11 +0,0 @@
-<template>
-  <div class="project-media-wrapper">
-    <MediaPanel />
-  </div>
-</template>
-
-<script lang="ts" setup>
-import MediaPanel from '/@/components/MediaPanel.vue'
-</script>
-
-<style lang="scss" scoped></style>

+ 143 - 0
Web/src/pages/page-web/projects/media/index/components/Search.vue

@@ -0,0 +1,143 @@
+<template>
+  <a-row style="margin-bottom: 20px;" justify="space-between">
+    <a-col>
+      <a-button type="primary" :disabled="!selectedRowKeys.length" @click="onClickDownload">
+        压缩下载
+      </a-button>
+    </a-col>
+    <a-col>
+      <a-form ref="formRef" layout="inline" :model="formModel" :colon="false">
+        <a-form-item name="date">
+          <a-range-picker style="width: 250px;" v-model:value="formModel.date" />
+        </a-form-item>
+        <a-form-item name="status">
+          <a-select style="width: 200px;margin-right: 10px;" placeholder="请选择任务类型" v-model:value="formModel.status">
+            <a-select-option value="1">
+              1
+            </a-select-option>
+            <a-select-option value="2">
+              2
+            </a-select-option>
+            <a-select-option value="3">
+              3
+            </a-select-option>
+            <a-select-option value="4">
+              4
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item name="device_name">
+          <a-select style="width: 200px;margin-right: 10px;" placeholder="请选择拍摄负载"
+            v-model:value="formModel.device_name">
+            <a-select-option value="1">
+              1
+            </a-select-option>
+            <a-select-option value="2">
+              2
+            </a-select-option>
+            <a-select-option value="3">
+              3
+            </a-select-option>
+            <a-select-option value="4">
+              4
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item name="keyword">
+          <a-input style="width: 200px;margin-right: 10px;" placeholder="文件夹名称" v-model:value="formModel.keyword" />
+        </a-form-item>
+        <a-form-item>
+          <a-button style="margin-right: 10px;" @click="handleClicSekarch">
+            <template #icon>
+              <SearchOutlined />
+            </template>
+          </a-button>
+          <a-button @click="handleClickReset">
+            <template #icon>
+              <ReloadOutlined />
+            </template>
+          </a-button>
+        </a-form-item>
+      </a-form>
+    </a-col>
+  </a-row>
+  <a-modal v-model:visible="state.visible" title="设备绑定码" :footer="null">
+    <div>
+      请在 Pilot2 机场部署流程-云服务配置中,以组织ID和设备绑定码来绑定机场设备。
+    </div>
+    <div class="modal-item">
+      <div class="modal-item-title">
+        项目名称
+      </div>
+      <div>
+        上海展域航空技术有限公司
+      </div>
+    </div>
+    <div class="modal-item">
+      <div class="modal-item-title">
+        设备绑定码
+      </div>
+      <div>
+        PB97VR
+      </div>
+    </div>
+  </a-modal>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive } from 'vue';
+import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
+
+interface Props {
+  selectedRowKeys: string[],
+  onClickDownload: () => Promise<any>,
+  onClickSearch: (query: any) => Promise<any>,
+  onClickReset: (query: any) => Promise<any>,
+};
+
+const props = withDefaults(defineProps<Props>(), {
+
+});
+
+const formRef = ref();
+
+const formModel = reactive({
+  date: [],
+  status: undefined,
+  device_name: undefined,
+  keyword: '',
+})
+
+const state = reactive({
+  visible: false,
+})
+
+// 点击查询
+const handleClicSekarch = async () => {
+  const values = formRef.value?.getFieldsValue();
+  await props.onClickSearch(values);
+}
+
+// 点击重置
+const handleClickReset = async () => {
+  formRef.value?.resetFields();
+  const values = formRef.value?.getFieldsValue();
+  await props.onClickReset(values);
+}
+</script>
+
+<style lang="scss" scoped>
+.popover {
+  width: 200px;
+}
+
+.modal-item {
+  display: flex;
+  margin-top: 20px;
+
+  &-title {
+    width: 100px;
+    font-weight: bold;
+  }
+}
+</style>

+ 302 - 0
Web/src/pages/page-web/projects/media/index/index.vue

@@ -0,0 +1,302 @@
+<template>
+  <div class="mediaList">
+    <Search :selectedRowKeys="state.selectedRowKeys" :onClickDownload="onClickBatchDownload"
+      :onClickSearch="async () => { }" :onClickReset="async () => { }" />
+    <div style="background: #FFFFFF;padding: 20px;">
+      <div class="mediaList-info">
+        <div>
+          <a-button type="primary" v-if="state.mode === 'table'">
+            全选
+          </a-button>
+        </div>
+        <div class="mediaList-info-right">
+          <div class="mediaList-info-right-text">
+            <div>
+              已选/全部:
+            </div>
+            <div>
+              0/175
+            </div>
+          </div>
+          <a-button style="padding:0 5px;" :type="state.mode === 'table' ? 'primary' : 'default'"
+            :ghost="state.mode === 'table'" size="small" @click="state.mode = 'table'">
+            <MenuOutlined />
+          </a-button>
+          <a-button style="padding:0 5px;" :type="state.mode === 'list' ? 'primary' : 'default'"
+            :ghost="state.mode === 'list'" size="small" @click="state.mode = 'list'">
+            <AppstoreOutlined />
+          </a-button>
+        </div>
+      </div>
+      <div class="mediaList-table" v-if="state.mode === 'table'">
+        <a-table :scroll="{ x: '100%', y: 500 }" :childrenColumnName="null" rowKey="file_id"
+          :loading="state.listLoading" :columns="columns" :dataSource="state.list" :rowClassName="rowClassName"
+          :pagination="paginationConfig" :rowSelection="rowSelection">
+          <!-- 文件夹名称 -->
+          <template #file_name="{ record }">
+            <a-tooltip :title="record.file_name">
+              <div class="fileName" @click="onClickFile(record)">
+                <img :src="fileSrc">
+                <div>
+                  {{ record.file_name }}
+                </div>
+              </div>
+            </a-tooltip>
+          </template>
+          <!-- 操作 -->
+          <template #action="{ record }">
+            <div class="flex-align-center flex-row" style="color: #2d8cf0">
+              <a-tooltip title="压缩下载">
+                <DownloadOutlined style="margin-right: 10px;" @click="onClickDownload(record)" />
+              </a-tooltip>
+              <a-tooltip title="轨迹回放">
+                <EnvironmentOutlined @click="onClickTrajectory" />
+              </a-tooltip>
+            </div>
+          </template>
+        </a-table>
+      </div>
+      <div class="mediaList-list" v-else>
+        <a-list :grid="{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 4, xl: 8, xxl: 12 }" :loading="state.listLoading"
+          :dataSource="state.list" :pagination="paginationConfig">
+          <template #renderItem="{ item }">
+            <a-list-item>
+              <a-card hoverable @click="onClickFile(item)">
+                <template #cover>
+                  <div style="display: flex;justify-content:center;align-items: center;">
+                    <img style="width: 70%;" :src="fileSrc" />
+                  </div>
+                </template>
+                <a-card-meta>
+                  <template #description>
+                    <div>
+                      {{ item.file_name }}
+                    </div>
+                  </template>
+                </a-card-meta>
+              </a-card>
+            </a-list-item>
+          </template>
+        </a-list>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { reactive, onMounted } from 'vue';
+import { MenuOutlined, AppstoreOutlined, DownloadOutlined, EnvironmentOutlined } from '@ant-design/icons-vue';
+import Search from './components/Search.vue';
+import fileSrc from '/@/assets/media/file.svg';
+import { getMediaFiles, downloadMediaFile } from '/@/api/media';
+import { downloadFile } from '/@/utils/common';
+import router from '/@/router/index';
+
+interface State {
+  workspaceId: string,
+  listLoading: boolean,
+  list: any[],
+  selectedRowKeys: string[],
+  mode: 'table' | 'list',
+};
+
+const state: State = reactive({
+  workspaceId: localStorage.getItem('workspace_id') || '',
+  listLoading: false,
+  list: [],
+  selectedRowKeys: [],
+  mode: 'table',
+})
+
+const paginationConfig = reactive({
+  pageSizeOptions: ['20', '50', '100'],
+  showQuickJumper: true,
+  showSizeChanger: true,
+  pageSize: 20,
+  current: 1,
+  total: 0,
+  onChange: async (current: number) => {
+    paginationConfig.current = current;
+    await fetchList();
+  },
+  onShowSizeChange: async (current: number, pageSize: number) => {
+    paginationConfig.pageSize = pageSize;
+    await fetchList();
+  }
+})
+
+const fetchList = async () => {
+  state.listLoading = true;
+  try {
+    const res = await getMediaFiles(state.workspaceId, { page: paginationConfig.current, page_size: paginationConfig.pageSize });
+    state.list = res.data.list;
+    paginationConfig.current = res.data.pagination.page;
+    paginationConfig.pageSize = res.data.pagination.page_size;
+    paginationConfig.total = res.data.pagination.total;
+  } catch (e) {
+    console.error(e);
+  } finally {
+    state.listLoading = false;
+  }
+}
+
+onMounted(async () => {
+  await fetchList();
+})
+
+const columns = [
+  {
+    title: '文件夹名称',
+    dataIndex: 'file_name',
+    width: 250,
+    ellipsis: true,
+    sorter: (a: any, b: any) => a.file_name.localeCompare(b.file_name),
+    slots: { customRender: 'file_name' }
+  },
+  {
+    title: '设备型号',
+    dataIndex: 'drone',
+    width: 150,
+    ellipsis: true,
+  },
+  {
+    title: '拍摄负载',
+    dataIndex: 'payload',
+    width: 150,
+    ellipsis: true,
+  },
+  {
+    title: '容量大小',
+    dataIndex: 'is_original',
+    width: 150,
+    ellipsis: true,
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'create_time',
+    width: 200,
+    sorter: (a: any, b: any) => a.create_time.localeCompare(b.create_time),
+  },
+  {
+    title: '航线名称',
+    dataIndex: 'create_time',
+    width: 150,
+  },
+  {
+    title: '任务类型',
+    dataIndex: 'create_time',
+    width: 150,
+  },
+  {
+    title: '创建人',
+    dataIndex: 'create_time',
+    width: 150,
+  },
+  {
+    title: '操作',
+    dataIndex: 'actions',
+    fixed: 'right',
+    width: 80,
+    slots: { customRender: 'action' },
+  },
+]
+
+const rowClassName = (record: any, index: number) => {
+  const className = []
+  if ((index & 1) === 0) {
+    className.push('table-striped')
+  }
+  return className.toString().replaceAll(',', ' ')
+}
+
+const rowSelection = {
+  onChange: (selectedRowKeys: string[]) => {
+    state.selectedRowKeys = selectedRowKeys;
+  },
+}
+
+// 点击批量下载
+const onClickBatchDownload = async () => {
+  console.log(state.selectedRowKeys, '点击批量下载');
+}
+
+// 点击文件夹
+const onClickFile = (record: any) => {
+  router.push({ path: `/media/${record.file_id}` })
+}
+
+// 点击下载
+const onClickDownload = async (record: any) => {
+  state.listLoading = true;
+  try {
+    const res = await downloadMediaFile(state.workspaceId, record.file_id);
+    if (!res) {
+      return
+    }
+    const data = new Blob([res]);
+    downloadFile(data, record.file_name);
+  } catch (e) {
+    console.error(e);
+  } finally {
+    state.listLoading = false;
+  }
+}
+
+// 点击轨迹回放
+const onClickTrajectory = () => {
+  router.push({ path: '/trajectory' })
+}
+</script>
+
+<style lang="scss">
+.mediaList {
+  padding: 20px;
+
+  &-info {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+
+    &-right {
+      display: flex;
+      align-items: center;
+
+      &-text {
+        display: flex;
+        align-items: center;
+        margin-right: 20px;
+      }
+    }
+  }
+
+  .fileName {
+    display: flex;
+    align-items: center;
+    cursor: pointer;
+
+    img {
+      width: 36px;
+      height: 36px;
+      margin-right: 5px;
+    }
+  }
+}
+
+.ant-table {
+  border-top: 1px solid rgb(0, 0, 0, 0.06);
+  border-bottom: 1px solid rgb(0, 0, 0, 0.06);
+}
+
+.ant-table-tbody tr td {
+  border: 0;
+}
+
+.table-striped {
+  background-color: #f7f9fa;
+}
+
+.ant-card-body {
+  padding: 0 10px 10px;
+}
+</style>

+ 6 - 1
Web/src/router/index.ts

@@ -38,7 +38,12 @@ const routes: Array<RouteRecordRaw> = [
       {
         path: '/' + ERouterName.MEDIA,
         name: ERouterName.MEDIA,
-        component: () => import('/@/pages/page-web/projects/media/index.vue')
+        component: () => import('/@/pages/page-web/projects/media/index/index.vue')
+      },
+      {
+        path: '/' + ERouterName.MEDIA + '/:id',
+        name: ERouterName.MEDIA + '/:id',
+        component: () => import('/@/pages/page-web/projects/media/detail/index.vue')
       },
       {
         path: '/' + ERouterName.TRAJECTORY,