Jelajahi Sumber

数据导入

Hwf 8 bulan lalu
induk
melakukan
fbff5b2873

+ 294 - 0
src/components/DataImport/index.vue

@@ -0,0 +1,294 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="批量导入" width="780px" append-to-body>
+    <div class="describe-content">
+      <div class="title">操作说明:</div>
+      <div class="step-container">
+        <el-steps direction="vertical">
+          <el-step status="process" title="首先点击“模板下载,下载录入模板”" />
+          <el-step status="process" :title="'在模板空白处中填写' + stepsText" />
+          <el-step status="process" title="通过点击“浏览,选择导入文件,点击“导入”按钮,完成导入" />
+          <el-step
+            status="process"
+            :title="'导入成功,对于系统中没有的' + stepsText + ',进行新增操作对于原本系统已存在的' + stepsText + ',进行信息合并操作。'"
+          />
+        </el-steps>
+      </div>
+    </div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-upload
+          ref="fileUploadRef"
+          multiple
+          :action="uploadFileUrl"
+          :before-upload="handleBeforeUpload"
+          :file-list="fileList"
+          :limit="limit"
+          :on-error="handleUploadError"
+          :on-exceed="handleExceed"
+          :on-success="handleUploadSuccess"
+          :show-file-list="false"
+          :headers="headers"
+          :http-request="uploadFile"
+          class="upload-file-uploader"
+        >
+          <el-button type="primary" :icon="Upload">导入</el-button>
+        </el-upload>
+        <el-button type="primary" :icon="Download" style="margin-left: 16px" @click="handleDownload">模板下载</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup name="DataImport">
+import { Upload, Download } from '@element-plus/icons-vue';
+import { download2, globalHeaders } from '@/utils/request';
+import { propTypes } from '@/utils/propTypes';
+import { v1 as uuidv1 } from 'uuid';
+import axios from 'axios';
+
+const props = defineProps({
+  modelValue: Boolean,
+  // 数量限制
+  limit: propTypes.number.def(20),
+  // 大小限制(MB)
+  fileSize: propTypes.number.def(100),
+  fileType: propTypes.array.def(['xls', 'xlsx']),
+  stepsText: String,
+  url: String,
+  fileName: String
+});
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const emits = defineEmits(['update:modelValue']);
+const dialogVisible = computed({
+  get() {
+    return props.modelValue;
+  },
+  set(newValue) {
+    emits('update:modelValue', newValue);
+  }
+});
+
+const number = ref(0);
+const uploadList = ref<any[]>([]);
+
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const downLoadApi = import.meta.env.VITE_APP_BASE_DOWNLOAD_API;
+const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服务器地址
+const headers = ref(globalHeaders());
+
+const fileList = ref<any[]>([]);
+const fileUploadRef = ref<ElUploadInstance>();
+// 隐藏清空数据
+watch(
+  () => props.modelValue,
+  async (val) => {
+    if (!val) {
+      fileList.value = [];
+    }
+  },
+  { deep: true, immediate: true }
+);
+
+// 进入导入界面
+const handleImport = () => {
+
+};
+// 上传前校检格式和大小
+const handleBeforeUpload = (file: any) => {
+  // 校检文件类型
+  if (props.fileType.length) {
+    const fileName = file.name.split('.');
+    const fileExt = fileName[fileName.length - 1];
+    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
+    if (!isTypeOk) {
+      proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`);
+      return false;
+    }
+  }
+  // 校检文件大小
+  if (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize;
+    if (!isLt) {
+      proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
+      return false;
+    }
+  }
+  proxy?.$modal.loading('正在上传文件,请稍候...');
+  number.value++;
+  return true;
+};
+
+// 文件个数超出
+const handleExceed = () => {
+  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
+};
+
+// 上传失败
+const handleUploadError = () => {
+  proxy?.$modal.msgError('上传文件失败');
+};
+
+const uploadFile = async ({ data, file }) => {
+  // data是上传时附带的额外参数,file是文件
+  let url = baseUrl + '/file/upload/uploadfile'; //上传文件接口
+  try {
+    // 如果文件大于等于5MB,分片上传
+    data.file = file;
+    const res = await uploadByPieces(url, data);
+    // 分片上传后操作
+    return res;
+  } catch (e) {
+    return e;
+  }
+};
+//分片上传
+const uploadByPieces = async (url, { fileName, file }) => {
+  // 上传过程中用到的变量
+  const chunkSize = 5 * 1024 * 1024; // 5MB一片
+  const chunkCount = Math.ceil(file.size / chunkSize); // 总片数
+  // 获取当前chunk数据
+  const identifier = uuidv1();
+  const getChunkInfo = (file, index) => {
+    let start = index * chunkSize;
+    let end = Math.min(file.size, start + chunkSize);
+    let chunknumber = file.slice(start, end);
+    const fileName = file.name;
+    return { chunknumber, fileName };
+  };
+  // 分片上传接口
+  const uploadChunk = (data, params) => {
+    return new Promise((resolve, reject) => {
+      axios({
+        url,
+        method: 'post',
+        data,
+        params,
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        }
+      })
+        .then((res) => {
+          return resolve(res.data);
+        })
+        .catch((err) => {
+          return reject(err);
+        });
+    });
+  };
+  // 针对单个文件进行chunk上传
+  const readChunk = (index) => {
+    const { chunknumber } = getChunkInfo(file, index);
+    let fetchForm = new FormData();
+    fetchForm.append('file', chunknumber);
+    return uploadChunk(fetchForm, {
+      chunknumber: index,
+      identifier: identifier
+    });
+  };
+  // 针对每个文件进行chunk处理
+  const promiseList = [];
+  try {
+    for (let index = 0; index < chunkCount; ++index) {
+      promiseList.push(readChunk(index));
+    }
+    await Promise.all(promiseList);
+    // 文件分片上传完成后,调用合并接口
+    const res = await mergeChunks(identifier, file.name);
+    res.originalName = file.name;
+    return res;
+  } catch (e) {
+    return e;
+  }
+};
+const mergeChunks = async (identifier: string, filename: string) => {
+  try {
+    const response = await axios.post(baseUrl + '/file/upload/mergefile', null, {
+      params: {
+        identifier: identifier,
+        filename: filename,
+        chunkstar: 0 // 假设所有分片的开始序号为0
+      }
+    });
+
+    return response.data;
+  } catch (error) {
+    throw new Error('合并请求失败');
+  }
+};
+// 上传成功回调
+const handleUploadSuccess = (res: any, file: UploadFile) => {
+  if (res.code === 200) {
+    uploadList.value.push({
+      name: res.originalName,
+      url: res.filename
+    });
+    uploadedSuccessfully();
+    proxy.$modal.msgSuccess('导入成功');
+    emits('update:modelValue', false);
+  } else {
+    number.value--;
+    proxy?.$modal.closeLoading();
+    proxy?.$modal.msgError(res.msg);
+    fileUploadRef.value?.handleRemove(file);
+    uploadedSuccessfully();
+    proxy.$modal.msgError('导入失败');
+  }
+};
+
+// 上传结束处理
+const uploadedSuccessfully = () => {
+  if (number.value > 0 && uploadList.value.length === number.value) {
+    fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
+    uploadList.value = [];
+    number.value = 0;
+    proxy?.$modal.closeLoading();
+  }
+};
+
+// 下载模板
+const handleDownload = () => {
+  if (props.url) return false;
+  download2(props.url, props.fileName);
+};
+</script>
+
+<style lang="scss" scoped>
+.title {
+  font-size: 18px;
+  line-height: 36px;
+  margin-bottom: 10px;
+  color: rgba(0, 0, 0, 0.85);
+}
+.step-container {
+  width: 700px;
+  height: 180px;
+  :deep(.el-step) {
+    .el-step__icon.is-text {
+      border-color: #a8abb2;
+      color: rgba(0, 0, 0, 0.85);
+    }
+    .el-step__title.is-process {
+      font-size: 16px;
+      color: rgba(0, 0, 0, 0.85);
+      font-weight: normal;
+    }
+  }
+}
+.upload-file-list .el-upload-list__item {
+  line-height: 22px;
+  margin-bottom: 10px;
+  position: relative;
+  padding: 3px 5px;
+}
+
+.upload-file-list .ele-upload-list__item-content {
+  display: flex;
+  align-items: center;
+  color: inherit;
+}
+
+.ele-upload-list__item-content-action .el-link {
+  margin-right: 10px;
+}
+</style>

+ 3 - 2
src/components/FileUpload/index.vue

@@ -17,12 +17,13 @@
         class="upload-file-uploader"
       >
         <!-- 上传按钮 -->
-        <el-button type="primary">
+        <el-button v-if="!$slots.default()" type="primary">
           <div class="button-flex">
             <i class="upload-icon" />
             {{ buttonText }}
           </div>
         </el-button>
+        <slot></slot>
       </el-upload>
       <!-- 上传提示 -->
       <div v-if="showTip" class="el-upload__tip">
@@ -296,7 +297,7 @@ const listToString = (list: any[], separator?: string) => {
 
 // 下载方法
 const handleDownload = (file: any) => {
-  download2(baseUrl + downLoadApi + file.url, file.name);
+  download2(baseUrl + downLoadApi + file.url, file.name ? file.name : '导入模板');
 };
 </script>
 

+ 1 - 8
src/types/components.d.ts

@@ -17,19 +17,16 @@ declare module 'vue' {
     CompanyMap: typeof import('./../components/Map/company-map.vue')['default']
     Contact: typeof import('./../components/Contact/index.vue')['default']
     ContactSelect: typeof import('./../components/ContactSelect/index.vue')['default']
+    DataImport: typeof import('./../components/DataImport/index.vue')['default']
     Dialog: typeof import('./../components/Dialog/index.vue')['default']
     DictTag: typeof import('./../components/DictTag/index.vue')['default']
     DistributionMap: typeof import('./../components/Map/YztMap/DistributionMap.vue')['default']
     DrawMap: typeof import('./../components/Map/YztMap/DrawMap.vue')['default']
     Editor: typeof import('./../components/Editor/index.vue')['default']
-    ElAnchor: typeof import('element-plus/es')['ElAnchor']
-    ElAnchorLink: typeof import('element-plus/es')['ElAnchorLink']
     ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
     ElBadge: typeof import('element-plus/es')['ElBadge']
     ElButton: typeof import('element-plus/es')['ElButton']
-    ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
-    ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
     ElCol: typeof import('element-plus/es')['ElCol']
     ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
@@ -47,7 +44,6 @@ declare module 'vue' {
     ElImage: typeof import('element-plus/es')['ElImage']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
-    ElLink: typeof import('element-plus/es')['ElLink']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
     ElOption: typeof import('element-plus/es')['ElOption']
@@ -65,8 +61,6 @@ declare module 'vue' {
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
-    ElTabPane: typeof import('element-plus/es')['ElTabPane']
-    ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
     ElText: typeof import('element-plus/es')['ElText']
     ElTimeline: typeof import('element-plus/es')['ElTimeline']
@@ -87,7 +81,6 @@ declare module 'vue' {
     IconSelect: typeof import('./../components/IconSelect/index.vue')['default']
     IEpCaretBottom: typeof import('~icons/ep/caret-bottom')['default']
     IEpCaretTop: typeof import('~icons/ep/caret-top')['default']
-    IEpUploadFilled: typeof import('~icons/ep/upload-filled')['default']
     IFrame: typeof import('./../components/iFrame/index.vue')['default']
     ImagePreview: typeof import('./../components/ImagePreview/index.vue')['default']
     ImageUpload: typeof import('./../components/ImageUpload/index.vue')['default']

+ 1 - 0
src/views/emergencyCommandMap/LeftSection/Communication.vue

@@ -402,6 +402,7 @@ onMounted(() => {
         width: 255px;
         height: 180px;
         background: url('@/assets/images/emergencyCommandMap/communication/treeBg.png') no-repeat;
+        background-size: 100% 100%;
         padding: 15px 8px;
         overflow: hidden;
         :deep(.el-tree) {

+ 8 - 0
src/views/emergencyCommandMap/LeftSection/VideoMonitor.vue

@@ -511,4 +511,12 @@ initData();
 //    min-height: 150px;
 //  }
 //}
+.common-btn-primary {
+  width: 94px;
+  height: 60px;
+}
+.common-btn {
+  width: 66px;
+  height: 32px;
+}
 </style>