Przeglądaj źródła

设备列表添加批量导入功能

hjs 1 rok temu
rodzic
commit
24baedcd6b

+ 2 - 2
.env.development

@@ -3,5 +3,5 @@ ENV = 'development'
 
 # base api
 # VUE_APP_BASE_API = '/dev-api'
-# VUE_APP_BASE_API = 'http://192.168.11.11:8081/hanghui-server-platform'
-VUE_APP_BASE_API = 'https://tx.hz-hanghui.com:8088/hanghui-server-platform'
+VUE_APP_BASE_API = 'http://192.168.11.11:8081/hanghui-server-platform'
+# VUE_APP_BASE_API = 'https://tx.hz-hanghui.com:8088/hanghui-server-platform'

+ 2 - 1
package.json

@@ -23,7 +23,8 @@
     "path-to-regexp": "2.4.0",
     "vue": "2.6.10",
     "vue-router": "3.0.6",
-    "vuex": "3.1.0"
+    "vuex": "3.1.0",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "4.4.4",

+ 173 - 0
src/components/BatchUpload/index.vue

@@ -0,0 +1,173 @@
+<template>
+  <a href="javascript:;" class="file">
+    {{ file && file.name ? file.name : '选择文件' }}
+    <input
+      id="fileSelect"
+      type="file"
+      accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel"
+      runat="server"
+      @change="upload"
+    >
+  </a>
+</template>
+
+<script>
+import * as XLSX from 'xlsx'
+export default {
+  name: 'BatchUpload',
+  data() {
+    return {
+      file: null
+    }
+  },
+  methods: {
+    upload(e) {
+      // 若出现files赋值未知错误值或照片长度不对则return
+      if (!e.target.files || e.target.files.length < 1) {
+        return
+        // return this.$message.error('请上传excel文件')
+      }
+      // 如果是批量上传图片走这里
+      // if (this.file) {
+      //   if (!this.needFileNameList) {
+      //     return this.$message.error('请选择命名参数')
+      //   }
+      //   if (this.needFileNameList.indexOf('name') === -1) {
+      //     return this.$message.error('姓名为必填项')
+      //   }
+      //   if (this.needFileNameList.indexOf('idNumber') > -1 || this.needFileNameList.indexOf('cardIdEx') > -1 || this.needFileNameList.indexOf('phone') > -1) {
+      //     // 找出所有图片
+      //     this.getPhotoList(Array.from(files))
+      //   } else {
+      //     return this.$message.error('手机号、身份证、卡号任选其一必选')
+      //   }
+      // }
+
+      // 处理excel
+      this.file = e.target.files[0]
+      this.getJsonList(this.file)
+    },
+    // getPhotoList(files) {
+    //   const photoList = {
+    //     correct: [],
+    //     incorrect: []
+    //   }
+    //   files.forEach((item) => {
+    //     const arr = item.name.split('.')
+    //     const formatStyle = arr[arr.length - 1]
+    //     // 若文件类型不在指定类型中,则不处理
+    //     if (!this.formatStyles.includes(formatStyle)) {
+    //       photoList.incorrect.push({
+    //         reason: '文件格式要求为' + this.formatStyles.join(',') + ',上传的类型为' + formatStyle,
+    //         file: item
+    //       })
+    //       return
+    //     } else if (item.size > 1024 * 1024 * 2) {
+    //       // 要求文件大小不大于指定值 单位:字节
+    //       photoList.incorrect.push({
+    //         reason: '单张图片大小要求为2M内,上传的图片大小为' + item.size / 1024 / 1024 + 'M',
+    //         file: item
+    //       })
+    //       return
+    //     }
+
+    //     const obj = this.matchNeedFileNameList(item, arr[0])
+    //     photoList[obj.status ? 'correct' : 'incorrect'].push(obj.file)
+    //   })
+    //   this.doUpload(photoList)
+    // },
+    // matchNeedFileNameList(file, fileName) {
+    //   const arrContent = fileName.split('-')
+    //   const obj = {
+    //     status: true,
+    //     file: {}
+    //   }
+    //   if (arrContent.length < this.needFileNameList.length ||
+    //     arrContent.some((name) => {
+    //       return !name
+    //     })
+    //   ) {
+    //     return {
+    //       status: false,
+    //       file: {
+    //         reason: '文件没有按指定要求命名,需要' + this.needFileNameList.join(','),
+    //         file: file
+    //       }
+    //     }
+    //   }
+    //   this.needFileNameList.forEach((item, index) => {
+    //     obj.file[item] = arrContent[index].trim() || ''
+    //   })
+    //   obj.file['avatar'] = file
+    //   return obj
+    // },
+    doUpload(photoList) {
+      this.$emit('getFileList', photoList)
+    },
+    // 处理excel文件的内容
+    getJsonList(file) {
+      const reader = new FileReader()
+      const photoList = {
+        correct: [],
+        incorrect: []
+      }
+      reader.readAsBinaryString(file)
+      reader.onload = (e) => {
+        // 获取传递的表格
+        const binaryString = e.target.result
+        // 以二进制流方式读取到整份的excel 表格对象
+        const workbook = XLSX.read(binaryString, {
+          type: 'binary',
+          sheetRows: 0,
+          codepage: 936
+        })
+        let data = []
+        // 遍历每张表读取
+        for (const sheet in workbook.Sheets) {
+          data = data.concat(XLSX.utils.sheet_to_json(workbook.Sheets[sheet]))
+        }
+        photoList['correct'] = data
+        this.doUpload(photoList)
+      }
+    },
+    clearFile() {
+      this.file = null
+    }
+  }
+}
+</script>
+
+<style scoped>
+/*批量导入按钮*/
+.file {
+  position: relative;
+  background: #d0eeff;
+  border: 1px solid #99d3f5;
+  border-radius: 4px;
+  padding: 10px 20px;
+  box-sizing: border-box;
+  overflow: hidden;
+  color: #1e88c7;
+  text-decoration: none;
+  text-indent: 0;
+}
+.file input {
+  position: absolute;
+  font-size: 14px;
+  width: 100%;
+  height: 40px;
+  left: 0;
+  top: 0;
+  opacity: 0;
+  cursor: pointer;
+}
+.file:hover {
+  background: #aadffd;
+  border-color: #78c3f3;
+  color: #004974;
+  text-decoration: none;
+}
+.el-button {
+  margin-left: 20px;
+}
+</style>

+ 12 - 1
src/views/device/index.vue

@@ -1,6 +1,7 @@
 <template>
   <div class="app-container">
     <addModal ref="addModel" @success="modalSuccess" />
+    <uploadModal ref="uploadModal" @finish="modalSuccess(false)" />
     <div style="margin-bottom: 20px;">
       <el-input v-model="form.sn" placeholder="设备sn" style="width: 256px;" class="filter-item" />
       <el-input v-model="form.company" placeholder="公司名" style="width: 256px;margin-left: 20px;" class="filter-item" />
@@ -10,6 +11,8 @@
       <el-button class="filter-item" style="margin-left: 20px;" type="primary" icon="el-icon-plus" @click="handleCreate">
         添加
       </el-button>
+      <el-button type="primary" icon="el-icon-download" style="margin-left: 20px;" @click="download">下载模板</el-button>
+      <el-button type="primary" style="margin-left: 20px;" plain @click="addOrUpdate">批量导入</el-button>
     </div>
     <el-table
       v-loading="tableLoading"
@@ -99,12 +102,13 @@
 
 <script>
 import AddModal from './modal/AddModal.vue'
+import UploadModal from './modal/UploadModal.vue'
 import tableMixins from '@/mixins/tableMixins'
 import { getList, deleteById, autoById, autoCancelById } from '@/api/device'
 import Pagination from '@/components/Pagination'
 
 export default {
-  components: { Pagination, AddModal },
+  components: { Pagination, AddModal, UploadModal },
   filters: {
     statusFilter(status) {
       return status ? 'success' : 'info'
@@ -147,6 +151,13 @@ export default {
         company: this.form.company || null
       }
     },
+    // 下载模板
+    download() {
+      window.location.href = process.env.VUE_APP_BASE_API + '/static/deviceListExcel.xlsx'
+    },
+    addOrUpdate() {
+      this.$refs.uploadModal.open()
+    },
     modalSuccess(isEdit) {
       if (!isEdit) {
         this.clearPageParams()

+ 293 - 0
src/views/device/modal/UploadModal.vue

@@ -0,0 +1,293 @@
+// 上传设备
+<template>
+  <div>
+    <el-dialog
+      title="批量导入"
+      width="600px"
+      :visible.sync="dialogVisible"
+      :destroy-on-close="false"
+      :close-on-click-modal="false"
+      @close="dialogClose"
+    >
+      <el-form ref="form" :rules="rules" :model="form" label-position="left" label-width="120px">
+        <el-form-item label="服务商平台名称" prop="tenantId" required>
+          <el-select v-model="form.tenantId" class="filter-item" placeholder="服务商平台名称">
+            <el-option v-for="(item, index) in merchantLists" :key="index" :label="item.name" :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="场景" prop="sceneId" :required="false">
+          <el-select v-model="form.sceneId" class="filter-item" placeholder="请选择场景" clearable>
+            <el-option v-for="(item, index) in sceneLists" :key="index" :label="item.sceneName" :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="⽤户库" prop="userLibId" :required="false">
+          <el-select v-model="form.userLibId" class="filter-item" placeholder="请选择⽤户库" clearable>
+            <el-option v-for="(item, index) in userlibLists" :key="index" :label="item.userLibName" :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="上传文件" prop="correctList">
+          <batch-upload ref="batchUploadRef" @getFileList="batchUploadResult" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogVisible = false">
+          取消
+        </el-button>
+        <el-button type="primary" @click="submit">
+          确定
+        </el-button>
+      </div>
+    </el-dialog>
+    <!-- 批量注册人员-抽屉 -->
+    <el-drawer
+      :title="hasCompleted ? '导入已完成' : '正在处理中..'"
+      :visible.sync="drawerVisible"
+      :wrapper-closable="false"
+      size="50%"
+      direction="rtl"
+    >
+      <div style="padding: 50px">
+        <div>
+          已处理条数;{{ hasDealNumber }}; 待处理条数:{{ waitDealNumber }};
+          总共条数:{{ allNumber }}
+        </div>
+        <el-progress
+          :percentage="makePercent"
+          :format="formatProgress"
+          style="margin-top: 10px"
+        />
+        <el-table
+          ref="batch_upload_result"
+          v-loading="false"
+          :data="batchUploadResultList"
+          class="table"
+          element-loading-text="Loading"
+          border
+          fit
+          highlight-current-row
+          height="500"
+        >
+          <el-table-column label="错误序号" align="center" width="100px">
+            <template slot-scope="scope">
+              {{ scope.$index + 1 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="设备sn" align="center">
+            <template slot-scope="scope">
+              {{ scope.row.sn }}
+            </template>
+          </el-table-column>
+          <el-table-column label="公司名称" align="center">
+            <template slot-scope="scope">
+              {{ scope.row.company }}
+            </template>
+          </el-table-column>
+          <el-table-column label="错误描述" align="center">
+            <template slot-scope="scope">
+              {{ scope.row.err }}
+            </template>
+          </el-table-column>
+        </el-table>
+        <div style="text-align: center; margin: 20px 0">
+          <el-button type="primary" @click="drawerVisible = false">
+            我已知晓
+          </el-button>
+          <el-button type="warning" @click="downloadBatchUploadErrorList">
+            下载错误列表文件
+          </el-button>
+        </div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { add } from '@/api/device'
+import BatchUpload from '@/components/BatchUpload'
+import { getAllList } from '@/api/merchant'
+import { getList as getSceneList } from '@/api/scene'
+import { getList as getUserlibList } from '@/api/userlib'
+
+export default {
+  components: { BatchUpload },
+  data() {
+    return {
+      merchantLists: [],
+      sceneLists: [],
+      userlibLists: [],
+      form: {
+        tenantId: null,
+        sceneId: null,
+        userLibId: null,
+        correctList: {
+          correct: [],
+          incorrect: []
+        }
+      },
+      rules: {
+        tenantId: [
+          { required: true, type: 'number', whitespace: true, message: '不能为空!', trigger: ['change', 'blur'] }
+        ],
+        correctList: [
+          { required: true, type: 'object', message: '模板文件不能为空!', trigger: ['change', 'blur'], validator: (rule, value, callback) => {
+            if (Object.keys(value).length > 0 && ((value.correct && value.correct.length > 0) || (value.incorrect && value.incorrect.length > 0))) {
+              return callback()
+            }
+            return callback('数据不能为空!')
+          } }
+        ]
+      },
+      dialogVisible: false,
+      // 批量注册-处理提示值
+      hasDealNumber: 0,
+      waitDealNumber: 0,
+      allNumber: 0,
+      hasCompleted: false,
+      // 批量注册-抽屉控制值
+      drawerVisible: false,
+      batchUploadResultList: []
+    }
+  },
+  computed: {
+    // 批量注册-返回向下取整,减少渲染次数
+    makePercent() {
+      return Math.floor((this.hasDealNumber / this.allNumber) * 100)
+    }
+  },
+  created() {
+    this.getMerchantList()
+    this.getSceneList()
+    this.getUserlibList()
+  },
+  methods: {
+    open() {
+      this.dialogVisible = true
+      this.$nextTick(() => {
+        this.$refs.form.resetFields()
+      })
+    },
+    getMerchantList() {
+      getAllList().then(res => {
+        this.merchantLists = res.data
+      })
+    },
+    getSceneList() {
+      getSceneList({ pageNumber: 1, pageSize: 1000 }).then(res => {
+        this.sceneLists = res.data.records
+      })
+    },
+    getUserlibList() {
+      getUserlibList({ pageNumber: 1, pageSize: 1000 }).then(res => {
+        this.userlibLists = res.data.records
+      })
+    },
+    dialogClose() {
+      this.$refs.batchUploadRef.clearFile()
+    },
+    batchUploadResult(correctList) {
+      this.form.correctList = correctList
+    },
+    // 批量注册人员-操作
+    getFileList(correctList) {
+      this.hasCompleted = false
+      this.drawerVisible = true
+      this.hasDealNumber = correctList.incorrect.length
+      this.waitDealNumber = correctList.correct.length
+      this.allNumber = correctList.incorrect.length + correctList.correct.length
+      this.batchUploadResultList = correctList.incorrect.map((item) => {
+        return { name: item.file.name, err: item.reason }
+      })
+      this.makeScrollDown()
+      this.doSubmitUrl(correctList, 0)
+    },
+    // 批量注册人员-并提交至添加到接口
+    async doSubmitUrl(correctList, index) {
+      if (index <= correctList.correct.length - 1) {
+        const item = correctList.correct[index]
+        if (item['设备sn'] && item['公司名称']) {
+          // 组装数据上传
+          const data = JSON.parse(JSON.stringify(this.form))
+          data.sn = item['设备sn']
+          data.company = item['公司名称']
+          try {
+            await add(data)
+          } catch (err) {
+            // console.log(err)
+            this.batchUploadResultList.push({
+              sn: data.sn,
+              company: data.company,
+              err: err
+            })
+          }
+        } else {
+          this.batchUploadResultList.push({
+            sn: item['设备sn'] || '',
+            company: item['公司名称'] || '',
+            err: '不包含 "设备sn" 或 "公司名称" 字段'
+          })
+        }
+        this.hasDealNumber += 1
+        this.waitDealNumber -= 1
+        this.makeScrollDown()
+        // 继续调用
+        this.doSubmitUrl(correctList, ++index)
+      } else {
+        this.hasCompleted = true
+        this.$emit('finish')
+      }
+    },
+    // 批量注册人员-保持滚动条在最底部
+    makeScrollDown() {
+      this.$nextTick(() => {
+        this.$refs.batch_upload_result.$refs.bodyWrapper.scrollTop = this.$refs.batch_upload_result.$refs.bodyWrapper.scrollHeight
+      })
+    },
+    // 批量注册人员-返回进度条尾部显示值
+    formatProgress(percent) {
+      return percent === 100 ? '已完成' : ''
+    },
+    // 批量注册人员-导出错误名单
+    downloadBatchUploadErrorList() {
+      const list = this.batchUploadResultList
+      let string = '序号\t文件名\t错误原因\n'
+      for (let i = 0; i < this.batchUploadResultList.length; i++) {
+        string +=
+          (i + 1).toString() +
+          '\t' +
+          list[i].name +
+          '\t' +
+          list[i].err +
+          '\t' +
+          '\n'
+      }
+      const blob = new Blob([string], { type: 'text/plain;charset=utf-8' })
+      const excel_url = window.URL.createObjectURL(blob)
+      const link = document.createElement('a')
+      link.href = excel_url
+      link.download = '批量注册结果错误名单.xls'
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+    },
+    submit(e) {
+      e.preventDefault()
+      this.$refs.form.validate((valid) => {
+        if (!valid) {
+          return false
+        }
+        this.dialogVisible = false
+        this.getFileList(this.form.correctList)
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+::v-deep .el-select {
+  width: 100%;
+}
+.table {
+  margin-top: 15px;
+}
+</style>