需求:
vue项目中,table表格批量导入一些数据;导入成功后
弹窗组件
下载模版
上传的文件
上传成功后后端返回的文件流自动下载到本地
list.vue
// shop_group_web:src/module/views/prodManaged/list.vue
<template>
<el-button type="primary" @click="add" size="mini">批量上下架</el-button>
<up-loader ref="addUploader" @downLoad="addExcelExport" @upLoad="addExcelImport"></up-loader>
</template>
<script>
import UpLoader from "@/module/components/uploader/uploader.vue"
import eportsheet from '@/module/components/exportsheet/exportsheet'
export default {
components: { UpLoader },
data() {
return {
importAddConfig: {
skuCode: '商品编码(SGU)',
priceStatusStr: '上下架状态',
},
}
}
}
methods: {
add() {
this.$refs.addUploader.open()
},
addExcelExport() {
eportsheet.exportFileByConfig([{}], `批量上下架模板.xlsx`, this.importAddConfig)
},
async addExcelImport(file) {
let parseConfig = {}
for (let key in this.importAddConfig) {
parseConfig[this.importAddConfig[key]] = key
}
await eportsheet.parseToJSON(file, parseConfig).then(result => {
if(!result){
this.$notify.error({ title: '提示', message:"上传文件数据解析为空" })
return false
}
// 循环excel中的每一项
for(let i=0; i<result.length; i++){
let row = result[i]
if(!row.skuCode){
this.$notify.error({ title: '提示', message:"商品编码(SGU)不能为空" })
return false
}
if(!row.priceStatusStr){
this.$notify.error({ title: '提示', message:"上下架状态不能为空" })
return false
}
}
// 将excel的数据转成数组后传给后端,后端返回操作结果的文件流
this.$http.post(`${api.updateStatusByList}ForExcel`, result, {}, 'blob').then(res => {
if (res) {
this.$refs.addUploader.close()
this.$notify.success({title: '操作提示',message: "上传成功"})
this.search()
this.exportLoading = false
var blob = new Blob([res], {type: 'application/ms-excel'});
var elink = document.createElement('a');
elink.download = '批量上下架结果' + '.xlsx';
elink.href = URL.createObjectURL(blob);
document.body.appendChild(elink);
elink.click(); //点击下载
document.body.removeChild(elink); //下载完成移除元素
window.URL.revokeObjectURL(elink); //释放掉blob对象
this.loading = false
return
} else {
this.$notify.error({
title: '提示',
message: res.msg
})
}
})
})
},
}
</script>
uploader.vue
<template>
<!--批量导入弹窗-->
<el-dialog :visible.sync="visible" size="large" title="批量导入" :loading="loadingInfo">
<el-row class="mv10">
<el-col :span="24">
<label :for="`excelFile${tmp}`" class="excelFileLabel">
<span class="file-label-text">请选择文件</span>
<input accept=".xlm,.xls,.xlt,.xlw,.xlsx" type="file" :id="`excelFile${tmp}`" hidden @change="handleFileSelect" :value="fileValue" />
<el-input class="search-input" v-model="file.name" disabled></el-input>
</label>
<el-button style="margin-left: 10px" size="small" type="text" @click="exportTemp">下载模版</el-button>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-button style="margin-top: 20px" :loading="loadingInfo" size="small" type="primary" @click="submitUpload">导入
</el-button>
<el-button @click="exportErrorFile" v-if="upStatus.code">下载错误列表</el-button>
</el-col>
</el-row>
<el-row class="mt20" v-if="upStatus.code">
<el-col :span="24" style="color:red">
<p>{{file.name}} 导入失败,请修改后重新上传</p>
<p>失败原因:{{upStatus.msg}}</p>
</el-col>
</el-row>
</el-dialog>
</template>
<script>
export default {
name: 'upLoader',
props: {
loadingInfo: { type: Boolean, default: false }
},
data () {
return {
tmp: Date.now(),
visible: false,
fileValue: '',
loading: false,
file: { name: '' },
upStatus: {
code: '',
msg: '',
data: []
},
tempConfig: {}
}
},
methods: {
handleFileSelect (e) {
this.file = e.target.files[0] || { name: '' }
// 如果文件改变需要初始化上传状态
this.upStatus = { code: '', msg: '', data: [] }
},
exportTemp () {
try {
this.$emit('downLoad')
} catch (e) {
this.$notify.error({
title: '提示',
message: '模板下载遇到错误'
})
}
},
submitUpload () {
if (!this.file.name || this.file.name.indexOf(".xl") === -1) {
this.$notify.warning({
title: '提示',
message: '请选择excel文件'
})
return
}
this.$emit('upLoad', this.file)
},
open () {
this.visible = true
this.fileValue = ''
this.file = { name: '' }
},
close () {
this.visible = false
}
}
}
</script>
<style scoped>
.file-label-text {
cursor: pointer;
color: #409eff;
}
</style>
exportsheet.js
import XLSX from 'xlsx'
import * as util from '@/utils/utils'
// js导出execl
function s2ab (s) {
if (typeof ArrayBuffer !== 'undefined') {
let buf = new ArrayBuffer(s.length)
let view = new Uint8Array(buf)
for (let i = 0; i !== s.length; ++i) {
view[i] = s.charCodeAt(i) & 0xFF
}
return buf
} else {
let buf = new Array(s.length)
for (let i = 0; i !== s.length; ++i) {
buf[i] = s.charCodeAt(i) & 0xFF
}
return buf
}
}
// 将指定的自然数转换为26进制表示。映射关系:[0-25] -> [A-Z]。
function getCharCol (n) {
let s = ''
let m = 0
while (n > 0) {
m = n % 26 + 1
s = String.fromCharCode(m + 64) + s
n = (n - m) / 26
}
return s
}
function exportFileByConfig (json, fileName, exportConfig, extraData = []) {
if (exportConfig) {
json = json.map((item, index) => {
let newItem = {}
for (let k of Object.keys(exportConfig)) {
let value = item[k]
if (value === true) {
value = '是'
}
if (value === false) {
value = '否'
}
if (value === null || value === undefined) {
value = ''
}
newItem[exportConfig[k]] = value
}
return newItem
})
}
createFile(json, fileName, extraData)
}
// 建议使用exportFileByConfig方法,可以控制顺序
function exportFile (json, fileName, exportConfig, extraData = []) {
if (exportConfig) {
json = json.map((item, index) => {
let newItem = {}
// 设置导出的表头名称
for (let k of Object.keys(item)) {
if (!(k in exportConfig)) {
// newItem[k] = item[k] === null ? '' : (item[k] === true ? '是' : '否') //将本句注释取消可将未配置字段也导出
continue
}
let value = item[k]
if (value === true) {
value = '是'
}
if (value === false) {
value = '否'
}
if (value === null || value === undefined) {
value = ''
}
newItem[exportConfig[k]] = value
}
return newItem
})
}
createFile(json, fileName, extraData)
}
function createFile (json, fileName, extraList) {
let _tmpdata = json[0]
json.unshift({})
var keyMap = [] // 获取keys
for (var k in _tmpdata) {
keyMap.push(k)
json[0][k] = k
}
let tmpdata = [] // 用来保存转换好的json
let blankNum = extraList.length ? 2 : 1
json.map((v, i) => keyMap.map((k, j) => Object.assign({}, {
v: v[k],
// position: (j > 25 ? getCharCol(j) : String.fromCharCode(65 + j)) + (i + 1 + extraList.length) // 开头加个空行
position: (j > 25 ? getCharCol(j) : String.fromCharCode(65 + j)) + (i + blankNum)
}))).reduce((prev, next) => prev.concat(next)).forEach((v, i) => {
tmpdata[v.position] = {
v: v.v
}
})
let temObj = {}
for (let i = 0; i < extraList.length; i++) {
let item = extraList[i]
for (let j = 0; j < item.length; j++) {
let key = String.fromCharCode(65 + i) + (j + 1)
temObj[key] = {v: item[j].label + item[j].value}
}
}
tmpdata = Object.assign(temObj, tmpdata)
var outputPos = Object.keys(tmpdata) // 设置区域,比如表格从A1到D10
var tmpWB = {
SheetNames: ['mySheet'], // 保存的表标题
Sheets: {
'mySheet': Object.assign({},
tmpdata, // 内容
{
'!ref': 'A1' + ':' + outputPos[outputPos.length - 1] // 设置填充区域
})
}
}
let wbout = XLSX.write(tmpWB, {bookType: 'xlsx', type: 'binary'})
/* force a download */
let tmpDown = new Blob([s2ab(wbout)], {type: 'application/octet-stream'})
var href = URL.createObjectURL(tmpDown) // 创建对象超链接
var a = document.createElement('a')
a.href = href // 绑定a标签
a.download = fileName || `导出数据${util.formatDate(new Date())}.xlsx`
document.body.appendChild(a)
a.click() // 模拟点击实现下载
document.body.removeChild(a)
setTimeout(function () { // 延时释放
URL.revokeObjectURL(tmpDown) // 用URL.revokeObjectURL()来释放这个object URL
}, 100)
}
function toJSON (excelFile, config) {
return new Promise((resolve, reject) => {
if (window.FileReader) {
let result = []
let fr = new FileReader()
fr.readAsBinaryString(excelFile)
fr.onload = ev => {
try {
let data = ev.target.result
// 以二进制形式读取excel文件
let workbook = XLSX.read(data, {
type: 'binary'
})
// 遍历每个表
for (let sheet of workbook.SheetNames) {
if (workbook.Sheets[sheet]) {
// 修改field配置
result = config ? result.concat(XLSX.utils.sheet_to_json(workbook.Sheets[sheet], config)) : result.concat(XLSX.utils.sheet_to_json(workbook.Sheets[sheet]))
}
}
resolve(result)
} catch (e) {
return reject(new Error('文件类型不正确'))
}
}
return
}
reject(new Error('该浏览器不支持该功能,请更换或升级浏览器'))
})
}
/*
* @excelFile File
* @config Object 解析字段
* @setter Object XLSX.utils.sheet_to_json的配置
*/
function parseToJSON (excelFile, config, setter = {}) {
return new Promise((resolve, reject) => {
if (window.FileReader) { // 判断浏览器是否支持FileReader对象
let result = []
let fr = new FileReader() // 新建文件读取
fr.readAsBinaryString(excelFile) // 将文件读取二进制码
fr.onload = ev => { // 数据读取成功完成时触发
try {
let data = ev.target.result
// 以二进制流方式读取得到整份excel表格对象
let workbook = XLSX.read(data, {
type: 'binary'
})
// 只遍历第一个表
let name = workbook.SheetNames[0]
let sheet = workbook.Sheets[name]
if (sheet) {
result = XLSX.utils.sheet_to_json(sheet, {})
if (config) {
for (let item of result) {
for (let excelTitleKey in config) {
item[config[excelTitleKey]] = ''
}
}
result.forEach(item => {
for (let key in item) {
if (config[key]) {
item[config[key]] = item[key].toString().trim()
delete item[key]
}
}
})
// 去空行
if (result && result.length) {
let keyList = Object.keys(result[0])
for (let i = result.length - 1; i > 0; i--) {
let value = ''
for (let key of keyList) {
value += result[i][key]
}
if (value.trim() === '') {
result.splice(i, 1)
}
}
}
}
}
resolve(result)
} catch (e) {
reject(new Error('文件类型不正确'))
}
}
return
}
reject(new Error('该浏览器不支持该功能,请更换或升级浏览器'))
})
}
export default {
exportFile,
exportFileByConfig,
toJSON,
parseToJSON
}
utils/utils.js
/**
* formatDate 格式化日期 yyyy-MM-dd ios默认是utc时间比gmt少8个小时 2017-11-30新增 支持解析到秒
* @param str
* @param seconds
* @returns {string}
*/
export function formatDate(str, seconds, isNow = false) {
if (!str || str.toString().trim() === '') {
return ''
}
function parse(n) {
return n < 10 ? ('0' + n) : n
}
let dt = new Date(str)
let y = dt.getFullYear()
let m = dt.getMonth() + 1
let d = dt.getDate()
if (!seconds) {
return [y, parse(m), parse(d)].join('-')
} else if (seconds && isNow) {
let nowDate = new Date()
let h = nowDate.getHours()
let M = nowDate.getMinutes()
let s = nowDate.getSeconds()
return [y, parse(m), parse(d)].join('-') + ' ' + [parse(h), parse(M), parse(s)].join(':')
} else {
let h = dt.getHours()
let M = dt.getMinutes()
let s = dt.getSeconds()
return [y, parse(m), parse(d)].join('-') + ' ' + [parse(h), parse(M), parse(s)].join(':')
}
}
参考文章:
前端导出 excel ,设置字体,列宽,行高,对其方式,合并单元格等效果
vue使用js-xlsx、xlsx、xlsxStyle导出excel,可合并表格、修改格子样式,例如背景颜色、字体大小、列宽等