Przeglądaj źródła

feat: 完成相机批量导入

bxy 1 rok temu
rodzic
commit
9164383dcb

+ 1 - 0
.env.development

@@ -23,6 +23,7 @@ VITE_DROP_CONSOLE = true
 VITE_PROXY=[["/skyeye-admin-api","http://192.168.13.68/skyeye-admin-api"],[],["/eye_api_bak","http://192.168.13.68/eye_api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"],["/skyeye-file-upload","http://192.168.13.68/skyeye-file-upload"]]
 # VITE_PROXY=[["/skyeye-admin-api","http://192.168.22.163:8800/api"],[],["/eye_api_bak","http://192.168.22.163:8800/api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"]]
 #VITE_PROXY=[["/skyeye-admin-api","http://192.168.22.121:8800/api"],["/eye_api_bak","http://192.168.22.121:8800/api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"],["/skyeye-file-upload","http://192.168.13.68/skyeye-file-upload"]]
+# VITE_PROXY=[["/skyeye-admin-api","http://192.168.22.163:8800/api"],[],["/eye_api_bak","http://192.168.22.163:8800/api"],["/push_stream_host","http://192.168.13.68/push_stream_host"],["/skyeye-login","http://192.168.13.68/skyeye-login"],["/ws_api_bak","ws://192.168.13.68/ws_api_bak"],["/skyeye-file-upload","http://192.168.13.68/skyeye-file-upload"]]
 
 
 # API 接口地址

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

@@ -181,3 +181,22 @@ export const addNVRCameraItem = (data: CameraNVRItem) => {
     data,
   });
 };
+
+
+// 批量添加相机 - 下载场景code
+export function downloadSpaceCode() {
+  return http.request({
+    url: '/addCameraList/downloadWorkspaceCodeForm',
+    method: 'get',
+    responseType: 'blob',
+  });
+};
+
+// 批量添加相机 - 下载模板
+export function downloadBatchTemplate() {
+  return http.request({
+    url: '/skyeye/CAMERALIST_TEMPLATE/camera-upload-template.xlsx',
+    method: 'get',
+    responseType: 'blob',
+  });
+};

+ 209 - 202
src/views/cameras/overview/CamerasOverview.vue

@@ -2,28 +2,27 @@
   <div class="camera-page">
     <ConditionQuery />
     <div class="camera-list">
-      <BasicTable
-        :columns="columns"
-        :data-source="cameraItems"
-        :row-key="(row) => row.code"
+      <BasicTable :columns="columns" :data-source="cameraItems" :row-key="(row) => row.code"
         :action-column="actionColumn"
-        :pagination="{ total: total, pageSize: size, hideOnSinglePage: !cameraItems.length }"
-        :loading="loading"
+        :pagination="{ total: total, 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" @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>
+            <template #icon>
+              <el-icon>
+                <DocumentAdd />
+              </el-icon>
+            </template>
+            批量导入
+          </el-button>
           <el-badge :value="totalRow" :hidden="totalRow < 1" class="item">
             <el-button color="#1890FF" @click="showSharedPopover = true" plain>共享相机</el-button>
           </el-badge>
@@ -37,227 +36,235 @@
       </BasicTable>
     </div>
     <AddCamera class="add-popover" v-model="showAddPopover" />
+    <BatchImportCamera class="batch-import" v-if="showBatchImportPopover" v-model="showBatchImportPopover" />
     <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 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 } 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';
+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;
+const useShare = useCameraShare();
+const { totalRow, queryToTenantId, isAddState, conditionSearch } = useShare;
 
-  onMounted(() => {
-    isAddState.value = false;
-    console.log('isAddState', isAddState.value);
-    queryToTenantId.value = -10;
-    conditionSearch();
-  });
+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 cameraOverview = useCameraOverview();
+const { cameraItems, loading, total, page, size } = storeToRefs(cameraOverview);
+const { getCameraItems, openInterval, closeInterval, reset } = cameraOverview;
 
-  // 添加弹窗相关
-  const showAddPopover = ref(false);
+// 添加弹窗相关
+const showAddPopover = ref(false);
+// 批量导入弹窗
+const showBatchImportPopover = ref(false);
+const showSharedPopover = ref(false);
+const addSharedPopover = 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 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 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 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 orderByItem = () => {};
+const handlePageNumChange = (pageNum) => {
+  page.value = pageNum;
+  getCameraItems();
+};
 
-  const handlePageNumChange = (pageNum) => {
-    page.value = pageNum;
-    getCameraItems();
-  };
+const handlePageSizeChange = (pageSize) => {
+  size.value = pageSize;
+  getCameraItems();
+};
 
-  const handlePageSizeChange = (pageSize) => {
-    size.value = pageSize;
-    getCameraItems();
-  };
+const handleShare = (row) => {
+  addSharedPopover.value = true;
+  shareCameraData.value = row;
+};
 
-  const handleShare = (row) => {
-    addSharedPopover.value = true;
-    shareCameraData.value = row;
-  };
+const handlePreview = (_row) => {
+  router.push(`/cameras/preview?cameraId=${_row.id}`);
+};
 
-  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('删除成功');
 
-  const handleDelete = (row) => {
-    ElMessageBox.confirm(`您想删除相机${row.code}`, '提示', {
-      confirmButtonText: '确定',
-      cancelButtonText: '取消',
-      type: 'warning',
-      draggable: true,
+        getCameraItems();
+      });
     })
-      .then(() => {
-        deleteCameraItem({ cameraId: row.id }).then(() => {
-          ElMessage.success('删除成功');
+    .catch(() => { });
+};
 
-          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 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;
+};
 
-  onMounted(() => {
-    getCameraItems();
-    openInterval();
-  });
+onMounted(() => {
+  getCameraItems();
+  openInterval();
+});
 
-  onBeforeUnmount(() => {
-    closeInterval();
-    reset();
-  });
+onBeforeUnmount(() => {
+  closeInterval();
+  reset();
+});
 </script>
 
 <style scoped>
-  .camera-page {
-    position: relative;
-    height: calc(100vh - 64px - 12px);
-    background-color: #ffffff;
-  }
+.camera-page {
+  position: relative;
+  height: calc(100vh - 64px - 12px);
+  background-color: #ffffff;
+}
 
-  .camera-list {
-    padding: 0 21px;
-  }
+.camera-list {
+  padding: 0 21px;
+}
 
-  .empty-content {
-    margin: auto;
-    padding: 125px 0;
-  }
+.empty-content {
+  margin: auto;
+  padding: 125px 0;
+}
 
-  .empty-img {
-    width: 396px;
-  }
+.empty-img {
+  width: 396px;
+}
 
-  .empty-text {
-    font-size: 22px;
-    color: #8e8e8e;
-    line-height: 30px;
-    text-align: center;
-  }
+.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;
-  }
+.add-tip {
+  position: absolute;
+  left: 187px;
+  top: 64px;
+  font-size: 16px;
+  color: red;
+}
 
-  .item {
-    margin: 0px 40px 0px 15px;
-  }
+.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>

+ 265 - 0
src/views/cameras/overview/components/BatchImportCamera.vue

@@ -0,0 +1,265 @@
+<template>
+  <div v-if="props.modelValue">
+    <el-card v-if="cardVisible" class="pop-card">
+      <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="updateValue(false)" style="cursor: pointer;">
+            <Close />
+          </el-icon>
+        </div>
+      </template>
+      <div class="upload-content">
+        <el-upload ref="upload" class="upload-demo" :multiple="false" :limit="1" drag
+          action="/skyeye-admin-api/addCameraList/uploadForm" :headers="headers" :with-credentials="true"
+          :auto-upload="false" :on-exceed="handleExceed" :before-upload="beforeUpload" :on-success="handleUploadSuccess"
+          style="width: 384px; height: 192px; border-radius: 8px">
+          <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: 128px; display: flex">
+          <el-icon :size="18" style="margin-top: 7px;">
+            <Download />
+          </el-icon>
+          <el-tooltip content="点击下载场景字段对应的code信息" placement="top" effect="light">
+            <span style="color:#409efc; margin-top: 6px; margin-right: 12px; cursor: pointer;"
+              @click="handleDownloadSceneCode">场景code信息查询</span>
+          </el-tooltip>
+          <el-button @click="handleDownloadTemplate">下载模板</el-button>
+          <el-button type="primary" @click="handleImport">导入</el-button>
+        </div>
+      </div>
+    </el-card>
+
+    <el-dialog v-model="DialogVisibleErr" title="Warning" width="50%" align-center
+      @close="() => { emits('update:modelValue', false); }">
+      <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">
+        成功上传 <span class="succ-sum">{{ sucCount }}</span> 条,
+        失败 <span class="err-sum">{{ errCount }}</span> 条
+      </div>
+      <div class="err-info">
+        <div v-if="errCount === 0">未检测到相机数据</div>
+        <div v-else>
+          <ul v-for="(item, index) in errDetail" :key="index">
+            <li v-html="item"></li>
+          </ul>
+        </div>
+      </div>
+      <template #footer>
+        <el-button type="primary" @click="handleErrComfirm"> 确定 </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { useUserStore } from '@/store/modules/user';
+import { genFileId, ElMessage } from 'element-plus';
+import type { UploadInstance, UploadProps, UploadRawFile } from 'element-plus';
+import { Close, Document, WarnTriangleFilled, Download } from '@element-plus/icons-vue';
+import { downloadSpaceCode, downloadBatchTemplate } from '@/api/camera/camera-overview'
+
+onMounted(() => {
+  cardVisible.value = props.modelValue;
+});
+
+const userStore = useUserStore();
+const headers = {
+  Satoken: userStore.getToken,
+  Tenantid: userStore.getTenantId,
+};
+
+const upload = ref<UploadInstance>();
+const cardVisible = ref<boolean>(true);
+const DialogVisibleErr = ref<boolean>(false);
+const sucCount = ref<number>(0);
+const errCount = ref<number>(0);
+const errDetail = ref<string[]>([]);
+
+const props = defineProps<{ modelValue: boolean }>();
+const emits = defineEmits(['update:modelValue', 'change']);
+
+const updateValue = (value) => {
+  emits('update:modelValue', value);
+};
+
+// 下载场景code信息查询表
+const handleDownloadSceneCode = async () => {
+  try {
+    const response = await downloadSpaceCode();
+    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 = '场景code信息查询.xlsx';
+    downloadLink.click();
+    // 移除下载链接
+    window.URL.revokeObjectURL(url);
+    downloadLink = null;
+  } catch (error) {
+    console.error('Error downloading file:', error);
+  }
+};
+
+// 下载模板
+const handleDownloadTemplate = async () => {
+  try {
+    const response = await downloadBatchTemplate();
+    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 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.errorList;
+
+  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>')
+    }
+  })
+
+  // 1.全部添加成功 —— failCount === 0
+  if (errCount.value === 0 && sucCount.value != 0) {
+    ElMessage({
+      message: '添加成功',
+      type: 'success',
+    });
+  } else {
+    // 2.有错误 —— 显示错误dialog
+    DialogVisibleErr.value = true;
+  };
+
+  cardVisible.value = false;
+};
+
+const handleErrComfirm = () => {
+  DialogVisibleErr.value = false;
+  emits('update:modelValue', false);
+  emits('change');
+};
+
+// 当超出只能上传一个文件的限制时,自动替换上一个文件
+const handleExceed: UploadProps['onExceed'] = (files) => {
+  upload.value!.clearFiles();
+  const file = files[0] as UploadRawFile;
+  file.uid = genFileId();
+  upload.value!.handleStart(file);
+};
+</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;
+
+      .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>