Bladeren bron

Merge branch 'feature/camera-batch-export-delete' into 'all'

feat: 相机-批量导出/修改+批量删除

See merge request skyeye/skyeye_frontend/skyeye-admin!180
楼航飞 1 jaar geleden
bovenliggende
commit
f04ea7e578

+ 9 - 0
src/api/camera/camera-overview.ts

@@ -171,6 +171,15 @@ export const deleteCameraItem = (params: { cameraId: number }) => {
   });
 };
 
+// 批量删除相机
+export const deleteCameraItems = (data: number[]) => {
+  return http.request({
+    url: '/addCameraList/deleteCameraList',
+    method: 'delete',
+    data,
+  });
+};
+
 /** 获取相机状态 */
 export const getCameraState = (data: { cameraCodeList: string[] }) => {
   return http.request<{ cameraCode: string; status: 0 | 1 }[]>({

+ 14 - 2
src/components/Table/src/Table.vue

@@ -87,6 +87,7 @@
         v-bind="getBindValues"
         v-loading="tableLoading"
         @sort-change="sortChange"
+        @selection-change="selectionChange"
         :show-header="props.showHeader"
         :expand-row-keys="$props.expendRow"
       >
@@ -162,7 +163,7 @@
     FullscreenOutlined,
   } from '@vicons/antd';
   import { useFullscreen } from '@vueuse/core';
-  import { ElIcon } from 'element-plus';
+  import { ElIcon, ElTable } from 'element-plus';
   import { PaginationProps } from './types/pagination';
 
   const props = defineProps({
@@ -181,6 +182,7 @@
     'edit-change',
     'columns-change',
     'order-change',
+    'selection-change',
     'page-num-change',
     'page-size-change',
   ]);
@@ -189,7 +191,8 @@
   const isShowTable = ref(true);
   const deviceHeight = ref<Number | String>('auto');
   const sTableRef = ref<HTMLElement | null>(null);
-  const tableElRef = ref<HTMLElement | null>(null);
+  // const tableElRef = ref<HTMLElement | null>(null);
+  const tableElRef = ref<InstanceType<typeof ElTable>>();
   const basicTableRef = ref<HTMLElement | null>(null);
   const wrapRef = ref(null);
   const checkedRowKeys = ref<any>([]);
@@ -434,6 +437,14 @@
     emit('order-change', sortInfo);
   }
 
+  function selectionChange(selection: any[]) {
+    emit('selection-change', selection);
+  }
+
+  function clearAll() {
+    tableElRef.value!.clearSelection();
+  }
+
   onMounted(() => {
     nextTick(() => {
       computeTableHeight();
@@ -455,6 +466,7 @@
     updateTableDataRecord,
     deleteTableDataRecord,
     redoHeight,
+    clearAll,
   });
 </script>
 <style lang="scss" scoped>

+ 476 - 240
src/views/cameras/overview/CamerasOverview.vue

@@ -2,26 +2,69 @@
   <div class="camera-page">
     <ConditionQuery />
     <div class="camera-list">
-      <BasicTable :columns="columns" :data-source="cameraItems" :row-key="(row) => row.code"
+      <div v-if="showActionBar" class="action-bar">
+        <span class="num-text">已选{{ chooseNum }}项</span>
+        <el-button :class="isActiveExport ? 'btn-active' : 'btn-normal'" @click="handleBatchExport"
+          >导出</el-button
+        >
+        <el-button :class="isActiveDelete ? 'btn-active' : 'btn-normal'" @click="handleBatchDelete"
+          >删除</el-button
+        >
+        <span class="close-btn" @click="handleSelectNone"></span>
+      </div>
+      <BasicTable
+        :columns="columns"
+        :data-source="cameraItems"
+        :row-key="(row) => row.code"
         :action-column="actionColumn"
-        :pagination="{ total: total, currentPage: page, pageSize: size, hideOnSinglePage: !cameraItems.length }"
-        :loading="loading" :tableSetting="{
+        :pagination="{
+          total: total,
+          currentPage: page,
+          pageSize: size,
+          hideOnSinglePage: !cameraItems.length,
+        }"
+        :loading="loading"
+        :tableSetting="{
           size: false,
           redo: false,
           fullscreen: false,
           striped: false,
           setting: false,
-        }" :striped="true" ref="tableRef" @order-change="orderByItem" @page-num-change="handlePageNumChange"
-        @page-size-change="handlePageSizeChange">
+        }"
+        :striped="true"
+        ref="tableRef"
+        @order-change="orderByItem"
+        @selection-change="handleSelectionChange"
+        @page-num-change="handlePageNumChange"
+        @page-size-change="handlePageSizeChange"
+      >
         <template #tableTitle>
           <el-button type="primary" :icon="Plus" @click="showAddPopover = true">添加</el-button>
-          <el-button color="#1890FF" @click="showBatchImportPopover = true" style="margin-left: 18px" plain>
+          <el-button
+            color="#1890FF"
+            @click="showBatchImportPopover = true"
+            style="margin-left: 18px"
+            plain
+          >
             <template #icon>
               <el-icon>
                 <DocumentAdd />
               </el-icon>
             </template>
-            批量导入
+            批量添加
+          </el-button>
+          <el-button
+            color="#1890FF"
+            @click="showBatchEditPopover = true"
+            style="margin-left: 18px"
+            plain
+          >
+            <template #icon>
+              <el-icon>
+                <Edit />
+              </el-icon>
+            </template>
+            批量修改
           </el-button>
           <el-badge :value="totalRow" :hidden="totalRow < 1" class="item">
             <el-button color="#1890FF" @click="showSharedPopover = true" plain>共享相机</el-button>
@@ -36,249 +79,442 @@
       </BasicTable>
     </div>
     <AddCamera class="add-popover" v-model="showAddPopover" />
-    <BatchImportCamera class="batch-import" v-if="showBatchImportPopover" @update="handleUpdateBatchImport"
-      @close="handleCloseBatchImport" />
+    <BatchImportCamera
+      class="batch-import"
+      v-if="showBatchImportPopover"
+      @update="handleUpdateBatchImport"
+      @close="handleCloseBatchImport"
+    />
+    <BatchEditCamera
+      class="batch-import"
+      v-if="showBatchEditPopover"
+      @update="handleUpdateBatchEdit"
+      @close="handleCloseBatchEdit"
+    />
     <EditCamera class="add-popover" v-model="showEditPopover" :edit-data="editCameraData" />
     <EditSRSCamera class="add-popover" v-model="showEditSRSPopover" :edit-data="editCameraData!" />
-    <EditNVRCamera class="add-popover" v-model="showEditNVRPopover"
-      :edit-data="editCameraData! as any as CameraNVRItem" />
+    <EditNVRCamera
+      class="add-popover"
+      v-model="showEditNVRPopover"
+      :edit-data="editCameraData! as any as CameraNVRItem"
+    />
     <ShareCamera class="add-popover" v-model="addSharedPopover" :share-data="shareCameraData" />
-    <EditSharedCamera class="add-popover" v-model="showSharedPopover" @update-unadd="updateUnaddAmount" />
+    <EditSharedCamera
+      class="add-popover"
+      v-model="showSharedPopover"
+      @update-unadd="updateUnaddAmount"
+    />
   </div>
 </template>
 
 <script setup lang="ts">
-import { h, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
-import { BasicTable, TableActionIcons } from '@/components/Table';
-import { BasicColumn } from '@/components/Table';
-import { columns } from './overviewColumns';
-import ConditionQuery from './components/ConditionQuery.vue';
-import AddCamera from './components/CameraAddPopover.vue';
-import BatchImportCamera from './components/BatchImportCamera.vue'
-import ShareCamera from './components/CameraSharePopover.vue';
-import EditCamera from './components/CameraEditPopover.vue';
-import EditSRSCamera from './components/CameraEditSRSPopover.vue';
-import EditNVRCamera from './components/CameraEditNVRPopover.vue';
-import EditSharedCamera from './components/CameraSharedEdit.vue';
-import emptyImg from '@/assets/images/table/table-empty.png';
-import { Plus, DocumentAdd } from '@element-plus/icons-vue';
-import shareIcon from '@/assets/images/table/table-share.png';
-import previewIcon from '@/assets/images/table/table-preview.png';
-import editIcon from '@/assets/images/table/table-edit.png';
-import deleteIcon from '@/assets/images/table/table-delete.png';
-import useCameraOverview from './stores/useCameraOverview';
-import { storeToRefs } from 'pinia';
-import { CameraIPItem, CameraNVRItem, CameraShowItem } from './type';
-import { deleteCameraItem } from '@/api/camera/camera-overview';
-import { ElMessage, ElMessageBox } from 'element-plus';
-import useCameraShare from './stores/useCameraShare';
-import router from '@/router';
-import { AddType } from './constant';
-
-const useShare = useCameraShare();
-const { totalRow, queryToTenantId, isAddState, conditionSearch } = useShare;
-
-onMounted(() => {
-  isAddState.value = false;
-  console.log('isAddState', isAddState.value);
-  queryToTenantId.value = -10;
-  conditionSearch();
-});
-
-const cameraOverview = useCameraOverview();
-const { cameraItems, loading, total, page, size } = storeToRefs(cameraOverview);
-const { getCameraItems, openInterval, closeInterval, reset } = cameraOverview;
-
-// 添加弹窗相关
-const showAddPopover = ref(false);
-// 批量导入弹窗
-const showBatchImportPopover = ref(false);
-const showSharedPopover = ref(false);
-const addSharedPopover = ref(false);
-
-const showEditPopover = ref(false);
-const showEditSRSPopover = ref(false);
-const showEditNVRPopover = ref(false);
-const editCameraData = ref<CameraIPItem | null>();
-const shareCameraData = ref<CameraShowItem | null>();
-
-const updateUnaddAmount = () => {
-  isAddState.value = false;
-  console.log('isAddState', isAddState.value);
-  queryToTenantId.value = -10;
-  conditionSearch();
-};
-
-//操作列
-const actionColumn: BasicColumn = reactive({
-  width: 200,
-  title: '操作',
-  prop: 'action',
-  key: 'action',
-  fixed: 'right',
-  render(record) {
-    return h(TableActionIcons as any, {
-      space: 20,
-      color: '#629bf9',
-      style: 'img',
-      size: 16,
-      actionIcons: [
-        {
-          label: '分享',
-          icon: shareIcon,
-          onClick: handleShare.bind(null, record.row),
-        },
-        {
-          label: '预览',
-          icon: previewIcon,
-          onClick: handlePreview.bind(null, record.row),
-        },
-        {
-          label: '编辑',
-          icon: editIcon,
-          onClick: handleEdit.bind(null, record.row),
-        },
-        {
-          label: '删除',
-          icon: deleteIcon,
-          onClick: handleDelete.bind(null, record.row),
-        },
-      ],
+  import { h, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
+  import { BasicTable, TableActionIcons } from '@/components/Table';
+  import { BasicColumn } from '@/components/Table';
+  import { columns } from './overviewColumns';
+  import ConditionQuery from './components/ConditionQuery.vue';
+  import AddCamera from './components/CameraAddPopover.vue';
+  import BatchImportCamera from './components/BatchImportCamera.vue';
+  import BatchEditCamera from './components/BatchEditCamera.vue';
+  import ShareCamera from './components/CameraSharePopover.vue';
+  import EditCamera from './components/CameraEditPopover.vue';
+  import EditSRSCamera from './components/CameraEditSRSPopover.vue';
+  import EditNVRCamera from './components/CameraEditNVRPopover.vue';
+  import EditSharedCamera from './components/CameraSharedEdit.vue';
+  import emptyImg from '@/assets/images/table/table-empty.png';
+  import { Plus, DocumentAdd, Edit } from '@element-plus/icons-vue';
+  import shareIcon from '@/assets/images/table/table-share.png';
+  import previewIcon from '@/assets/images/table/table-preview.png';
+  import editIcon from '@/assets/images/table/table-edit.png';
+  import deleteIcon from '@/assets/images/table/table-delete.png';
+  import useCameraOverview from './stores/useCameraOverview';
+  import { storeToRefs } from 'pinia';
+  import { CameraIPItem, CameraNVRItem, CameraShowItem } from './type';
+  import { deleteCameraItem, deleteCameraItems } from '@/api/camera/camera-overview';
+  import { ElMessage, ElMessageBox } from 'element-plus';
+  import useCameraShare from './stores/useCameraShare';
+  import router from '@/router';
+  import { AddType } from './constant';
+  import axios, { AxiosRequestConfig } from 'axios';
+  import { getHeaders } from '@/utils/http/axios';
+  import { useGlobSetting } from '@/hooks/setting';
+
+  const { urlPrefix } = useGlobSetting();
+
+  const useShare = useCameraShare();
+  const { totalRow, queryToTenantId, isAddState, conditionSearch } = useShare;
+
+  onMounted(() => {
+    isAddState.value = false;
+    console.log('isAddState', isAddState.value);
+    queryToTenantId.value = -10;
+    conditionSearch();
+  });
+
+  const cameraOverview = useCameraOverview();
+  const { cameraItems, loading, total, page, size } = storeToRefs(cameraOverview);
+  const { getCameraItems, openInterval, closeInterval, reset } = cameraOverview;
+
+  const tableRef = ref();
+  // 添加弹窗相关
+  const showAddPopover = ref(false);
+  // 批量添加弹窗
+  const showBatchImportPopover = ref(false);
+  // 批量删除弹窗
+  const showBatchEditPopover = ref(false);
+  const showSharedPopover = ref(false);
+  const addSharedPopover = ref(false);
+  const showEditPopover = ref(false);
+  const showEditSRSPopover = ref(false);
+  const showEditNVRPopover = ref(false);
+  const editCameraData = ref<CameraIPItem | null>();
+  const shareCameraData = ref<CameraShowItem | null>();
+  // 多选操作
+  const showActionBar = ref(false);
+  const chooseNum = ref(0);
+  const chooseId = ref<number[]>([]);
+  const isActiveExport = ref(false);
+  const isActiveDelete = ref(false);
+
+  const updateUnaddAmount = () => {
+    isAddState.value = false;
+    console.log('isAddState', isAddState.value);
+    queryToTenantId.value = -10;
+    conditionSearch();
+  };
+
+  //操作列
+  const actionColumn: BasicColumn = reactive({
+    width: 200,
+    title: '操作',
+    prop: 'action',
+    key: 'action',
+    fixed: 'right',
+    render(record) {
+      return h(TableActionIcons as any, {
+        space: 20,
+        color: '#629bf9',
+        style: 'img',
+        size: 16,
+        actionIcons: [
+          {
+            label: '分享',
+            icon: shareIcon,
+            onClick: handleShare.bind(null, record.row),
+          },
+          {
+            label: '预览',
+            icon: previewIcon,
+            onClick: handlePreview.bind(null, record.row),
+          },
+          {
+            label: '编辑',
+            icon: editIcon,
+            onClick: handleEdit.bind(null, record.row),
+          },
+          {
+            label: '删除',
+            icon: deleteIcon,
+            onClick: handleDelete.bind(null, record.row),
+          },
+        ],
+      });
+    },
+  });
+
+  // 列排序操作
+  const orderByItem = () => {};
+
+  // 多选操作
+  const handleSelectionChange = (selection) => {
+    chooseNum.value = selection.length;
+    showActionBar.value = chooseNum.value > 0 ? true : false;
+    chooseId.value = [];
+    selection.forEach((item) => {
+      if (chooseId.value.indexOf(item.id) === -1) chooseId.value.push(item.id);
     });
-  },
-});
-
-// 列排序操作
-const orderByItem = () => { };
-
-const handlePageNumChange = (pageNum) => {
-  page.value = pageNum;
-  getCameraItems();
-};
-
-const handlePageSizeChange = (pageSize) => {
-  page.value = 1;
-  size.value = pageSize;
-  getCameraItems();
-};
-
-const handleShare = (row) => {
-  addSharedPopover.value = true;
-  shareCameraData.value = row;
-};
-
-const handlePreview = (_row) => {
-  router.push(`/cameras/preview?cameraId=${_row.id}`);
-};
-
-const handleDelete = (row) => {
-  ElMessageBox.confirm(`您想删除相机${row.code}`, '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-    type: 'warning',
-    draggable: true,
-  })
-    .then(() => {
-      deleteCameraItem({ cameraId: row.id }).then(() => {
-        ElMessage.success('删除成功');
-
-        getCameraItems();
+  };
+
+  const handleBatchExport = async () => {
+    try {
+      const requestBody = {
+        cameraIdList: chooseId.value,
+      };
+
+      const config: AxiosRequestConfig = {
+        headers: getHeaders(),
+        responseType: 'blob',
+      };
+      const response = await axios.post(
+        urlPrefix + '/addCameraList/downloadCameraList',
+        requestBody,
+        config,
+      );
+      const blob = new Blob([response.data], {
+        type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
       });
+      // 创建下载链接
+      let downloadLink: HTMLAnchorElement | null = document.createElement('a');
+      const url = window.URL.createObjectURL(blob);
+      downloadLink.href = url;
+      downloadLink.download = '相机导出信息.xlsx';
+      downloadLink.click();
+      // 移除下载链接
+      window.URL.revokeObjectURL(url);
+      downloadLink = null;
+    } catch (error) {
+      console.error('Error downloading file:', error);
+    }
+  };
+
+  const handleBatchDelete = () => {
+    if (showActionBar.value) isActiveDelete.value = !isActiveDelete.value;
+    ElMessageBox.confirm('删除后,相机数据无法恢复', '请确认是否删除相机数据', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+      customClass: 'deleteMessage',
+      center: true,
     })
-    .catch(() => { });
-};
-
-const handleEdit = (row) => {
-  if (row.sourceType === AddType.srs) {
-    showEditSRSPopover.value = true;
-  } else if (row.sourceType === AddType.ip) {
-    showEditPopover.value = true;
-  } else {
-    showEditNVRPopover.value = true;
-  }
-  editCameraData.value = row;
-};
-
-// 批量导入相关事件
-const handleUpdateBatchImport = () => {
-  showBatchImportPopover.value = false;
-  page.value = 1;
-  size.value = 10;
-  getCameraItems();
-};
-
-const handleCloseBatchImport = () => {
-  showBatchImportPopover.value = false;
-};
-
-onMounted(() => {
-  getCameraItems();
-  openInterval();
-});
-
-onBeforeUnmount(() => {
-  closeInterval();
-  reset();
-});
+      .then(() => {
+        deleteCameraItems(chooseId.value).then(() => {
+          ElMessage({
+            type: 'success',
+            message: '删除成功',
+          });
+          page.value = 1;
+          getCameraItems();
+          handleSelectNone();
+          isActiveDelete.value = !isActiveDelete.value;
+        });
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '取消删除',
+        });
+        isActiveDelete.value = !isActiveDelete.value;
+      });
+  };
+
+  // 清除多选
+  const handleSelectNone = () => {
+    chooseId.value = [];
+    chooseNum.value = 0;
+    tableRef.value?.clearAll();
+    showActionBar.value = false;
+  };
+
+  const handlePageNumChange = (pageNum) => {
+    page.value = pageNum;
+    getCameraItems();
+  };
+
+  const handlePageSizeChange = (pageSize) => {
+    page.value = 1;
+    size.value = pageSize;
+    getCameraItems();
+  };
+
+  const handleShare = (row) => {
+    addSharedPopover.value = true;
+    shareCameraData.value = row;
+  };
+
+  const handlePreview = (_row) => {
+    router.push(`/cameras/preview?cameraId=${_row.id}`);
+  };
+
+  const handleDelete = (row) => {
+    ElMessageBox.confirm(`您想删除相机${row.code}`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning',
+      draggable: true,
+    })
+      .then(() => {
+        deleteCameraItem({ cameraId: row.id }).then(() => {
+          ElMessage.success('删除成功');
+
+          getCameraItems();
+        });
+      })
+      .catch(() => {});
+  };
+
+  const handleEdit = (row) => {
+    if (row.sourceType === AddType.srs) {
+      showEditSRSPopover.value = true;
+    } else if (row.sourceType === AddType.ip) {
+      showEditPopover.value = true;
+    } else {
+      showEditNVRPopover.value = true;
+    }
+    editCameraData.value = row;
+  };
+
+  // 批量导入相关事件
+  const handleUpdateBatchImport = () => {
+    showBatchImportPopover.value = false;
+    page.value = 1;
+    size.value = 10;
+    getCameraItems();
+  };
+  const handleCloseBatchImport = () => {
+    showBatchImportPopover.value = false;
+  };
+
+  // 批量修改相关事件
+  const handleUpdateBatchEdit = () => {
+    showBatchEditPopover.value = false;
+    page.value = 1;
+    size.value = 10;
+    getCameraItems();
+  };
+  const handleCloseBatchEdit = () => {
+    showBatchEditPopover.value = false;
+  };
+
+  onMounted(() => {
+    getCameraItems();
+    openInterval();
+  });
+
+  onBeforeUnmount(() => {
+    closeInterval();
+    reset();
+  });
 </script>
 
-<style scoped>
-.camera-page {
-  position: relative;
-  height: calc(100vh - 64px - 12px);
-  background-color: #ffffff;
-}
-
-.camera-list {
-  padding: 0 21px;
-}
-
-.empty-content {
-  margin: auto;
-  padding: 125px 0;
-}
-
-.empty-img {
-  width: 396px;
-}
-
-.empty-text {
-  font-size: 22px;
-  color: #8e8e8e;
-  line-height: 30px;
-  text-align: center;
-}
-
-.add-tip {
-  position: absolute;
-  left: 187px;
-  top: 64px;
-  font-size: 16px;
-  color: red;
-}
-
-.add-popover {
-  position: absolute;
-  width: calc(100% - 21px);
-  height: 622px;
-  top: 0;
-  bottom: 0;
-  margin: auto;
-  z-index: 99;
-}
-
-.batch-import {
-  position: absolute;
-  width: 593px;
-  height: 435px;
-  left: 50%;
-  top: 50%;
-  margin-top: -218px;
-  margin-left: -297px;
-  z-index: 99;
-}
-
-.item {
-  margin: 0px 40px 0px 15px;
-}
+<style scoped lang="less">
+  .action-bar {
+    display: flex;
+    align-items: center;
+    position: absolute;
+    top: 48px;
+    min-width: calc(100vw - 266px);
+    height: 50px;
+    border-radius: 4px 4px 0px 0px;
+    background-color: #ddefff;
+    z-index: 10;
+
+    .num-text {
+      margin: 0 34px 0 25px;
+      color: rgba(0, 0, 0, 0.85);
+      font-weight: 500;
+    }
+
+    .btn-normal {
+      color: #1890ff;
+      background: transparent;
+      border: 1px solid #1890ff;
+      border-radius: 2px;
+    }
+
+    .btn-active {
+      color: #ffffff;
+      background-color: #1890ff;
+    }
+
+    .close-btn {
+      margin-left: auto;
+      margin-right: 20px;
+    }
+
+    .close-btn:before {
+      content: '\2716';
+      color: #000;
+      cursor: pointer;
+    }
+  }
+
+  .camera-page {
+    position: relative;
+    height: calc(100vh - 64px - 12px);
+    background-color: #ffffff;
+  }
+
+  .camera-list {
+    padding: 0 21px;
+    position: relative;
+  }
+
+  .empty-content {
+    margin: auto;
+    padding: 125px 0;
+  }
+
+  .empty-img {
+    width: 396px;
+  }
+
+  .empty-text {
+    font-size: 22px;
+    color: #8e8e8e;
+    line-height: 30px;
+    text-align: center;
+  }
+
+  .add-tip {
+    position: absolute;
+    left: 187px;
+    top: 64px;
+    font-size: 16px;
+    color: red;
+  }
+
+  .add-popover {
+    position: absolute;
+    width: calc(100% - 21px);
+    height: 622px;
+    top: 0;
+    bottom: 0;
+    margin: auto;
+    z-index: 99;
+  }
+
+  .batch-import {
+    position: absolute;
+    width: 593px;
+    height: 435px;
+    left: 50%;
+    top: 50%;
+    margin-top: -218px;
+    margin-left: -297px;
+    z-index: 99;
+  }
+
+  .item {
+    margin: 0px 40px 0px 15px;
+  }
+</style>
+<style lang="less">
+  .deleteMessage {
+    padding: 20px 24px;
+    box-shadow: 0px 12px 48px 16px rgba(0, 0, 0, 0.03), 0px 9px 28px 0px rgba(0, 0, 0, 0.05),
+      0px 6px 16px -8px rgba(0, 0, 0, 0.08);
+    border-radius: 8px;
+
+    .el-message-box__headerbtn {
+      margin-top: 12px;
+      margin-right: 12px;
+    }
+
+    .el-message-box__title {
+      justify-content: start;
+      color: rgba(0, 0, 0, 0.88);
+      font-size: 16px;
+      font-weight: 500;
+    }
+
+    .el-message-box__container {
+      justify-content: start;
+      margin-left: 23px;
+    }
+
+    .el-message-box__btns {
+      display: block;
+      float: right;
+    }
+  }
 </style>

+ 267 - 0
src/views/cameras/overview/components/BatchEditCamera.vue

@@ -0,0 +1,267 @@
+<template>
+  <div>
+    <el-card v-if="cardVisible">
+      <template #header>
+        <div class="flex justify-between items-center pop-head">
+          <div style="font-size: 16px; font-weight: 600">批量修改</div>
+          <el-icon
+            :size="18"
+            class="mr-3"
+            @click="
+              () => {
+                emits('close');
+              }
+            "
+            style="cursor: pointer"
+          >
+            <Close />
+          </el-icon>
+        </div>
+      </template>
+      <div class="upload-content">
+        <el-upload
+          ref="upload"
+          style="width: 384px; height: 192px; border-radius: 8px"
+          :headers="getHeaders()"
+          :multiple="false"
+          :limit="1"
+          drag
+          :action="actionUrl"
+          :with-credentials="true"
+          :auto-upload="false"
+          :before-upload="beforeUpload"
+          :on-success="handleUploadSuccess"
+          :on-exceed="handleExceed"
+          :on-change="handleChange"
+          :on-remove="handleRemove"
+        >
+          <el-icon class="el-icon--upload" style="width: 33px; height: 42px; color: #409efc">
+            <Document />
+          </el-icon>
+          <div class="el-upload__text">
+            <div style="font-size: 12px; color: red; margin-bottom: 5px"
+              >请先导出需要修改的相机信息,并在原模版上修改后在此导入文件</div
+            >
+            <div style="font-size: 16px">点击或将文件拖拽到这里上传</div>
+            <div style="font-size: 12px; color: rgba(0, 0, 0, 0.45); margin-top: 5px"
+              >文件支持.xlsx .xls格式,仅支持上传一个文件</div
+            >
+          </div>
+        </el-upload>
+        <div style="margin-top: 72px; margin-left: 380px; display: flex">
+          <el-button type="primary" @click="handleImport" :disabled="isImportEnable"
+            >导入</el-button
+          >
+        </div>
+      </div>
+    </el-card>
+
+    <el-dialog
+      v-model="DialogVisibleErr"
+      title="Warning"
+      width="50%"
+      align-center
+      @close="
+        () => {
+          emits('update');
+        }
+      "
+    >
+      <template #header>
+        <el-icon :size="24" color="#f2b20a" style="margin: 0 5px 2px">
+          <WarnTriangleFilled />
+        </el-icon>
+        <div class="header-text">批量修改</div>
+      </template>
+      <div class="sum-count">
+        <div
+          >修改成功:<span class="succ-sum">{{ sucCount }}</span> 条</div
+        >
+        <div
+          >修改失败:<span class="err-sum">{{ errCount }}</span> 条</div
+        >
+      </div>
+      <div class="err-info">
+        <ul v-for="(item, index) in errDetail" :key="index">
+          <li v-html="item"></li>
+        </ul>
+      </div>
+      <template #footer>
+        <el-button type="primary" @click="handleErrComfirm"> 确定 </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, ref } from 'vue';
+  import { genFileId, ElMessage } from 'element-plus';
+  import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
+  import { Close, Document, WarnTriangleFilled } from '@element-plus/icons-vue';
+  import { useGlobSetting } from '@/hooks/setting';
+  import urlJoin from 'url-join';
+  import { getHeaders } from '@/utils/http/axios';
+
+  const emits = defineEmits(['close', 'update']);
+
+  const upload = ref<UploadInstance>();
+  const cardVisible = ref<boolean>(true);
+  const isImportEnable = ref<boolean>(true);
+  const DialogVisibleErr = ref<boolean>(false);
+  const sucCount = ref<number>(0);
+  const errCount = ref<number>(0);
+  const errDetail = ref<string[]>([]);
+
+  const { urlPrefix } = useGlobSetting();
+
+  const actionUrl = computed(() => {
+    return urlJoin(urlPrefix, `/addCameraList/updateCameraList`);
+  });
+
+  // 导入
+  const handleImport = async () => {
+    upload.value!.submit();
+  };
+
+  // 上传文件之前的钩子,参数为上传的文件。即上传之前验证文件类型/后缀
+  const beforeUpload = (file) => {
+    const isExcel = /\.(xlsx|xls)$/.test(file.name.toLowerCase());
+    if (!isExcel) {
+      // 提示用户选择正确的文件类型
+      ElMessage({
+        message: '仅支持上传.xlsx .xls格式文件',
+        type: 'error',
+      });
+      return false; // 阻止上传
+    }
+    return true; // 允许上传
+  };
+
+  const handleUploadSuccess = (response, _file, _fileList) => {
+    console.log(response);
+    sucCount.value = response.data.successCount;
+    errCount.value = response.data.failCount;
+    errDetail.value = response.data.resultList;
+
+    try {
+      if (errDetail.value.length > 0) {
+        errDetail.value.forEach((item, index) => {
+          if (item.indexOf('【修改失败】') >= 0) {
+            errDetail.value[index] = item.replace(
+              '【修改失败】',
+              '<span style="color: #ff4d4f">【修改失败】</span>',
+            );
+          } else if (item.indexOf('【修改成功】') >= 0) {
+            errDetail.value[index] = item.replace(
+              '【修改成功】',
+              '<span style="color: #52c41a">【修改成功】</span>',
+            );
+          }
+        });
+      }
+
+      if (sucCount.value != 0 && errCount.value === 0 && errDetail.value.length === 0) {
+        ElMessage({
+          message: '批量修改成功', // 1.全部修改成功 —— failCount === 0
+          type: 'success',
+        });
+        emits('update');
+      } else {
+        DialogVisibleErr.value = true; // 2.有错误 —— 显示错误dialog
+      }
+      cardVisible.value = false;
+    } catch (error) {
+      ElMessage({
+        message: '系统错误',
+        type: 'error',
+      });
+      emits('update');
+    }
+  };
+
+  const handleErrComfirm = () => {
+    DialogVisibleErr.value = false;
+    emits('update');
+  };
+
+  // 当超出只能上传一个文件的限制时,自动替换上一个文件
+  const handleExceed: UploadProps['onExceed'] = (files) => {
+    upload.value!.clearFiles();
+    const file = files[0] as UploadRawFile;
+    file.uid = genFileId();
+    upload.value!.handleStart(file);
+  };
+
+  const handleChange = () => {
+    isImportEnable.value = false;
+  };
+
+  const handleRemove = () => {
+    isImportEnable.value = true;
+  };
+</script>
+
+<style scoped>
+  .upload-content {
+    margin-left: 90px;
+    margin-top: 36px;
+  }
+
+  :deep(.el-dialog) {
+    padding: 0px;
+    border-radius: 5px;
+
+    .el-dialog__header {
+      display: flex;
+      align-items: flex-end;
+      height: 70px;
+      padding: 0px 0px 10px 10px;
+      border-bottom: 1px solid #e7e7e7;
+
+      .header-text {
+        font-size: 20px;
+      }
+    }
+
+    .el-dialog__headerbtn {
+      top: 22px;
+
+      .el-dialog__close {
+        color: black;
+      }
+    }
+
+    .el-dialog__body {
+      padding: 20px;
+
+      .sum-count {
+        margin: 10px 0 20px 20px;
+        font-size: 20px;
+        font-weight: 600;
+
+        .succ-sum {
+          color: #52c41a;
+        }
+
+        .err-sum {
+          color: #ff4d4f;
+        }
+      }
+
+      .err-info {
+        height: 200px;
+        margin-left: 20px;
+        overflow: auto;
+      }
+    }
+
+    .el-dialog__footer {
+      margin: 0 20px 20px 0;
+    }
+  }
+
+  li {
+    font-size: 14px;
+    margin-bottom: 2px;
+  }
+</style>

+ 5 - 0
src/views/cameras/overview/overviewColumns.ts

@@ -11,6 +11,11 @@ const cameraOverview = useCameraOverview();
 const { getState, getCameraItems } = cameraOverview;
 
 export const columns: BasicColumn[] = [
+  {
+    minWidth: 30,
+    type: 'selection',
+    fixed: 'left',
+  },
   {
     label: '序号',
     minWidth: 60,