excel操作增加了exceljs的实现

master
toesbieya 6 years ago
parent 5a38a70c18
commit aea61edb66
  1. 1
      vue/public/index.html
  2. 9582
      vue/public/static/js/exceljs.js
  3. 1
      vue/public/static/js/exceljs.js.map
  4. 34
      vue/public/static/js/exceljs.min.js
  5. 1
      vue/public/static/js/exceljs.min.js.map
  6. 2
      vue/src/mixins/bizDocumentTableMixin.js
  7. 244
      vue/src/utils/excel.js
  8. 143
      vue/src/utils/excel/common.js
  9. 83
      vue/src/utils/excel/exceljs.js
  10. 121
      vue/src/utils/excel/xlsx.js
  11. 15
      vue/src/utils/file.js
  12. 9
      vue/src/views/example/developingTest/components/ExportExcelTest.vue
  13. 2
      vue/src/views/stock/current/index.vue

@ -29,6 +29,7 @@
<!--excel-->
<script async src="/static/js/xlsx.full.min.js"></script>
<script async src="/static/js/exceljs.min.js"></script>
<!--富文本-->
<script async src="https://cdn.bootcdn.net/ajax/libs/tinymce/5.3.1/tinymce.min.js"></script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -3,7 +3,7 @@ import LinerProgress from '@/components/LinerProgress'
import {isEmpty} from "@/utils"
import {elConfirm, elError, elSuccess} from "@/utils/message"
import {auth} from "@/utils/auth"
import {exportExcel} from "@/utils/excel"
import {exportExcel} from "@/utils/excel/xlsx"
export const commonMethods = {
getStatus(status) {

@ -1,244 +0,0 @@
import {isEmpty} from "@/utils"
import {download} from "@/utils/file"
import request from "@/config/request"
import {elError} from "@/utils/message"
/**
* 搜索表格页导出excel
* 响应头为json时进行前端导出否则直接下载文件
* @param url 搜索的请求地址
* @param searchForm 搜索参数
* @param options 合并选项{column, merge}只有前端导出时才需要
*/
export function exportExcel(url, searchForm, options) {
request({url, method: 'post', responseType: 'blob', data: searchForm})
.then(({headers, data}) => {
let contentType = headers['content-type']
const contentDisposition = headers['content-disposition']
const filename = window.decodeURI(contentDisposition.split('=')[1])
if (contentType.includes('json')) {
const reader = new FileReader()
reader.onload = () => {
const response = JSON.parse(reader.result)
if (response.status !== 200) return elError(response.msg)
let {column, merge} = options
const sheet = json2sheet(column, response.data, merge)
exportExcelByJs(sheet, filename)
}
reader.readAsText(data)
}
else download(data, filename)
})
.catch(e => elError(e))
}
//纯前端导出excel
export function exportExcelByJs(sheet, filename) {
const wb = {
SheetNames: ['Sheet1'],
Sheets: {Sheet1: sheet}
}
const out = window.XLSX.write(wb, {
bookType: 'xlsx',
bookSST: false,
type: 'binary'
})
download(new Blob([s2ab(out)], {type: "application/octet-stream"}), filename)
}
/**
* json数组转sheet
* @param column 列配置
* @param data json数组
* @param mergeOption 合并配置项
* @param headerRows 表头占的行数
* @returns
*/
export function json2sheet(column, data, mergeOption, headerRows = 1) {
const header = []
const propMap = {}
const colWidth = []
const needMerge = []
column.forEach((item, i) => {
if (!isEmpty(item.header)) header[i] = item.header
if (!isEmpty(item.prop)) propMap[item.prop] = i
if (item.merge) needMerge[i] = item.prop
colWidth[i] = {wch: item.width || 20}
})
let mergeResult = []
if (mergeOption && needMerge.length > 0) {
mergeResult = mergeExcel(needMerge, data, mergeOption.primaryKey, mergeOption.orderKey, headerRows)
}
let result = data.map(i => Object.keys(i).reduce((arr, key) => {
arr[propMap[key]] = i[key]
return arr
}, []))
headerRows && result.unshift(header)
const sheet = aoa_to_sheet(result, headerRows)
sheet['!cols'] = colWidth
sheet['!merges'] = mergeResult
return sheet
}
/**
* 合并excel
* @param primaryKey 该行json的唯一标识字段名
* @param indexKey 该行json的序号字段名
* @param prop 表头的属性名称用于映射json
* @param data json数组
* @param ignoreRows 忽略的行数默认为1忽略表头
*/
function mergeExcel(prop, data, primaryKey, indexKey, ignoreRows = 1) {
const result = []
const startRows = ignoreRows + 1
const merge = prop.map(_ => isEmpty(_) ? undefined : [1])
const temp = prop.map(_ => isEmpty(_) ? undefined : 1)
//序号从1开始
let indexAfterMerge = 1
data[0][indexKey] = indexAfterMerge
function compare(currentRow, lastRow, nextRow, colIndex) {
const attr = prop[colIndex]
//若与上一行的primaryKey相同且属性相同,则temp对应项+1
if (currentRow[primaryKey] === lastRow[primaryKey]
&& currentRow[attr] === lastRow[attr]) {
temp[colIndex]++
//最后一行特殊处理
if (!nextRow) {
merge[colIndex].push(temp[colIndex])
}
else {
//若与下一行相同
if (currentRow[primaryKey] === nextRow[primaryKey]
&& currentRow[attr] === nextRow[attr]) {
merge[colIndex].push(1)
}
//否则存入
else merge[colIndex].push(temp[colIndex])
}
}
//否则清零
else {
temp[colIndex] = 1
merge[colIndex].push(1)
}
}
for (let i = 1; i < data.length; i++) {
const currentRow = data[i]
const lastRow = data[i - 1]
const nextRow = data[i + 1]
//重写序号
if (currentRow[primaryKey] === lastRow[primaryKey]) {
currentRow[indexKey] = lastRow[indexKey]
}
else currentRow[indexKey] = ++indexAfterMerge
if (nextRow && nextRow[primaryKey] === currentRow[primaryKey]) {
nextRow[indexKey] = currentRow[indexKey]
}
prop.forEach((_, colIndex) => !isEmpty(_) && compare(currentRow, lastRow, nextRow, colIndex))
}
function number2excelColumnHeader(n) {
let s = ""
while (n >= 0) {
s = String.fromCharCode(n % 26 + 65) + s
n = Math.floor(n / 26) - 1
}
return s
}
function mergeResultConstructor(arr, colIndex) {
const colHeader = number2excelColumnHeader(colIndex)
arr.forEach((colspan, rowIndex) => {
if (colspan > 1) {
const start = colHeader + (startRows + rowIndex - (colspan - 1))
const end = colHeader + (startRows + rowIndex)
const str = `${start}:${end}`
result.push(XLSX.utils.decode_range(str))
}
})
}
merge.forEach((arr, index) => arr && mergeResultConstructor(arr, index))
return result
}
//默认的excel单元格样式
const defaultCellStyle = {
font: {name: '宋体'},
alignment: {
wrapText: 1,// 自动换行
horizontal: "center",
vertical: "center",
indent: 0
}
}
//默认的头部样式
const defaultHeaderStyle = {
...defaultCellStyle,
fill: {fgColor: {rgb: "C0C0C0"}},
font: {name: '宋体', sz: 14, bold: true},
border: {top: {style: 'thin'}, left: {style: 'thin'}, bottom: {style: 'thin'}, right: {style: 'thin'}}
}
function datenum(v, date1904) {
if (date1904) v += 1462
const epoch = Date.parse(v)
return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000)
}
function aoa_to_sheet(data, headerRows) {
const ws = {}
const range = {s: {c: 10000000, r: 10000000}, e: {c: 0, r: 0}}
for (let R = 0; R !== data.length; ++R) {
for (let C = 0; C !== data[R].length; ++C) {
if (range.s.r > R) range.s.r = R
if (range.s.c > C) range.s.c = C
if (range.e.r < R) range.e.r = R
if (range.e.c < C) range.e.c = C
//生成cell的时候,使用默认样式
const cell = {
v: data[R][C],
s: R === 0 && headerRows > R ? defaultHeaderStyle : defaultCellStyle
}
if (cell.v == null) continue
const cell_ref = XLSX.utils.encode_cell({c: C, r: R})
/* TEST: proper cell types and value handling */
if (typeof cell.v === 'number') cell.t = 'n'
else if (typeof cell.v === 'boolean') cell.t = 'b'
else if (cell.v instanceof Date) {
cell.t = 'n'
cell.z = XLSX.SSF._table[14]
cell.v = datenum(cell.v)
}
else cell.t = 's'
ws[cell_ref] = cell
}
}
/* TEST: proper range */
if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range)
return ws
}
function s2ab(s) {
const buf = new ArrayBuffer(s.length)
const view = new Uint8Array(buf)
for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF
return buf
}

@ -0,0 +1,143 @@
import {isEmpty} from "@/utils"
import request from "@/config/request"
import {elError} from "@/utils/message"
import {download} from "@/utils/file"
/**
* 搜索表格页导出excel
* 响应头为json时进行前端导出否则直接下载文件
*
* @param url 搜索的请求地址
* @param searchForm 搜索参数
* @param options 合并选项{column, merge}只有前端导出时才需要
* @param json2workbook
* @param exportExcelByJs
*/
export function abstractExportExcel(url, searchForm, options, json2workbook, exportExcelByJs) {
request({url, method: 'post', responseType: 'blob', data: searchForm})
.then(({headers, data}) => {
const contentType = headers['content-type']
const contentDisposition = headers['content-disposition']
const filename = window.decodeURI(contentDisposition.split('=')[1])
if (contentType.includes('json')) {
const reader = new FileReader()
reader.onload = () => {
const response = JSON.parse(reader.result)
if (response.status !== 200) return elError(response.msg)
const {column, merge} = options
const workbook = json2workbook(column, response.data, merge)
exportExcelByJs(workbook, filename)
}
reader.readAsText(data)
}
else download(data, filename)
})
.catch(e => elError(e))
}
/**
* 合并excel
* 此方法会修改原始json数据重写序号
*
* @param props 需要合并的字段数组用于映射json
* @param data json数组
* @param primaryKey 该行json的唯一标识字段名
* @param orderKey 该行json的序号字段名
* @param ignoreRows 忽略的行数默认为1忽略表头
* @return 包含合并信息的数组形如['A1:A10',...]
*/
export function mergeExcel(props, data, primaryKey, orderKey, ignoreRows = 1) {
const result = [], merge = [], temp = []
props.forEach((prop, index) => {
merge[index] = prop ? [1] : undefined
temp[index] = prop ? 1 : undefined
})
//序号从1开始
let indexAfterMerge = data[0][orderKey] = 1
function compare(currentRow, lastRow, nextRow, colIndex) {
const attr = props[colIndex]
//若与上一行的primaryKey相同且属性相同,则temp对应项+1
if (currentRow[primaryKey] === lastRow[primaryKey]
&& currentRow[attr] === lastRow[attr]) {
temp[colIndex]++
//最后一行特殊处理
if (!nextRow) return merge[colIndex].push(temp[colIndex])
//若与下一行相同
if (currentRow[primaryKey] === nextRow[primaryKey]
&& currentRow[attr] === nextRow[attr]) {
merge[colIndex].push(1)
}
//否则存入
else merge[colIndex].push(temp[colIndex])
}
//否则清零
else {
temp[colIndex] = 1
merge[colIndex].push(1)
}
}
for (let i = 1; i < data.length; i++) {
const currentRow = data[i]
const lastRow = data[i - 1]
const nextRow = data[i + 1]
//重写序号
if (currentRow[primaryKey] === lastRow[primaryKey]) {
currentRow[orderKey] = lastRow[orderKey]
}
else currentRow[orderKey] = ++indexAfterMerge
if (nextRow && nextRow[primaryKey] === currentRow[primaryKey]) {
nextRow[orderKey] = currentRow[orderKey]
}
props.forEach((_, colIndex) => !isEmpty(_) && compare(currentRow, lastRow, nextRow, colIndex))
}
function mergeResultConstructor(arr, colIndex) {
const colHeader = number2excelColumnHeader(colIndex)
const startRows = ignoreRows + 1
arr.forEach((rowspan, rowIndex) => {
if (rowspan > 1) {
const start = colHeader + (startRows + rowIndex - (rowspan - 1))
const end = colHeader + (startRows + rowIndex)
result.push(`${start}:${end}`)
}
})
}
merge.forEach((arr, index) => arr && mergeResultConstructor(arr, index))
return result
}
/**
* 生成二维数组例如json为[{id:1,name:'x'}]结果为[[1,'x']]
*
* @param data json数组
* @param propMap 表头和字段名的映射表
*/
export function jsonArray2rowArray(data, propMap) {
return data.map(i => Object.keys(i)
.reduce((arr, key) => {
arr[propMap[key]] = i[key]
return arr
}, []))
}
//数字转excel列名
export function number2excelColumnHeader(n) {
let s = ""
while (n >= 0) {
s = String.fromCharCode(n % 26 + 65) + s
n = Math.floor(n / 26) - 1
}
return s
}

@ -0,0 +1,83 @@
import {isEmpty} from "@/utils"
import {download} from "@/utils/file"
import {abstractExportExcel, jsonArray2rowArray, mergeExcel, number2excelColumnHeader} from "./common"
//默认的excel单元格样式
const defaultCellStyle = {
font: {name: '宋体'},
alignment: {
wrapText: true,// 自动换行
horizontal: "center",
vertical: "middle",
indent: 0
}
}
//默认的头部样式
const defaultHeaderStyle = {
...defaultCellStyle,
fill: {type: 'pattern', pattern: 'solid', fgColor: {argb: "FFC0C0C0"}},
font: {name: '宋体', size: 14, bold: true},
border: {top: {style: 'thin'}, left: {style: 'thin'}, bottom: {style: 'thin'}, right: {style: 'thin'}}
}
export function exportExcel(url, searchForm, options) {
abstractExportExcel(url, searchForm, options, json2workbook, exportExcelByJs)
}
//纯前端导出excel
export function exportExcelByJs(workbook, filename) {
return workbook.xlsx.writeBuffer()
.then(buffer => download(new Blob([buffer], {type: "application/octet-stream"}), filename))
}
/**
* @param column 列配置
* @param data json数组
* @param mergeOption 合并配置项
* @param headerRows 表头占的行数为0时表头不写入表格
* @returns
*/
export function json2workbook(column, data, mergeOption, headerRows = 1) {
const workbook = new window.ExcelJS.Workbook()
const sheet = workbook.addWorksheet('Sheet1')
const colWidth = []
const header = [] //表格头部
const propMap = {} //表头和字段名的映射表,propMap[字段名]=对应的表头的数组下标
const needMerge = [] //合并行配置,[i]=字段名
column.forEach((item, i) => {
if (!isEmpty(item.header)) header[i] = item.header
if (!isEmpty(item.prop)) propMap[item.prop] = i
if (item.merge) needMerge[i] = item.prop
colWidth[i] = {width: item.width || 20, style: defaultCellStyle}
})
sheet.columns = colWidth
let merge = []
if (mergeOption && needMerge.length > 0) {
const {primaryKey, orderKey} = mergeOption
merge = mergeExcel(needMerge, data, primaryKey, orderKey, headerRows)
}
const result = jsonArray2rowArray(data, propMap)
if (headerRows > 0) {
sheet.addRow(header)
header.forEach((_, index) => {
const cell = sheet.getCell(number2excelColumnHeader(index) + '1')
Object.keys(defaultHeaderStyle).forEach(key => {
cell[key] = defaultHeaderStyle[key]
})
})
}
sheet.addRows(result)
//exceljs的合并行必须在数据添加后进行
merge.forEach(i => sheet.mergeCells(i))
return workbook
}

@ -0,0 +1,121 @@
import {isEmpty} from "@/utils"
import {download} from "@/utils/file"
import {abstractExportExcel, jsonArray2rowArray, mergeExcel} from "./common"
export function exportExcel(url, searchForm, options) {
abstractExportExcel(url, searchForm, options, json2workbook, exportExcelByJs)
}
//纯前端导出excel
export function exportExcelByJs(workbook, filename) {
const out = window.XLSX.write(workbook, {
bookType: 'xlsx',
bookSST: false,
type: 'binary'
})
download(new Blob([s2ab(out)], {type: "application/octet-stream"}), filename)
}
/**
* @param column 列配置
* @param data json数组
* @param mergeOption 合并配置项
* @param headerRows 表头占的行数为0时表头不写入表格
* @returns
*/
export function json2workbook(column, data, mergeOption, headerRows = 1) {
const header = [] //表格头部
const propMap = {} //表头和字段名的映射表,propMap[字段名]=对应的表头的数组下标
const colWidth = [] //列宽配置,默认20
const needMerge = [] //合并行配置,[i]=字段名
column.forEach((item, i) => {
if (!isEmpty(item.header)) header[i] = item.header
if (!isEmpty(item.prop)) propMap[item.prop] = i
if (item.merge) needMerge[i] = item.prop
colWidth[i] = {wch: item.width || 20}
})
let mergeResult = []
if (mergeOption && needMerge.length > 0) {
const {primaryKey, orderKey} = mergeOption
mergeResult = mergeExcel(needMerge, data, primaryKey, orderKey, headerRows)
}
const result = jsonArray2rowArray(data, propMap)
headerRows && result.unshift(header)
const sheet = aoa_to_sheet(result, headerRows)
sheet['!cols'] = colWidth
sheet['!merges'] = mergeResult.map(i => window.XLSX.utils.decode_range(i))
return {SheetNames: ['Sheet1'], Sheets: {Sheet1: sheet}}
}
//默认的excel单元格样式
const defaultCellStyle = {
font: {name: '宋体'},
alignment: {
wrapText: 1,// 自动换行
horizontal: "center",
vertical: "center",
indent: 0
}
}
//默认的头部样式
const defaultHeaderStyle = {
...defaultCellStyle,
fill: {fgColor: {rgb: "C0C0C0"}},
font: {name: '宋体', sz: 14, bold: true},
border: {top: {style: 'thin'}, left: {style: 'thin'}, bottom: {style: 'thin'}, right: {style: 'thin'}}
}
function datenum(v, date1904) {
if (date1904) v += 1462
const epoch = Date.parse(v)
return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000)
}
function aoa_to_sheet(data, headerRows) {
const ws = {}
const range = {s: {c: 10000000, r: 10000000}, e: {c: 0, r: 0}}
for (let R = 0; R !== data.length; ++R) {
for (let C = 0; C !== data[R].length; ++C) {
if (range.s.r > R) range.s.r = R
if (range.s.c > C) range.s.c = C
if (range.e.r < R) range.e.r = R
if (range.e.c < C) range.e.c = C
//生成cell的时候,使用默认样式
const cell = {
v: data[R][C],
s: R === 0 && headerRows > R ? defaultHeaderStyle : defaultCellStyle
}
if (cell.v == null) continue
const cell_ref = window.XLSX.utils.encode_cell({c: C, r: R})
/* TEST: proper cell types and value handling */
if (typeof cell.v === 'number') cell.t = 'n'
else if (typeof cell.v === 'boolean') cell.t = 'b'
else if (cell.v instanceof Date) {
cell.t = 'n'
cell.z = window.XLSX.SSF._table[14]
cell.v = datenum(cell.v)
}
else cell.t = 's'
ws[cell_ref] = cell
}
}
/* TEST: proper range */
if (range.s.c < 10000000) ws['!ref'] = window.XLSX.utils.encode_range(range)
return ws
}
function s2ab(s) {
const buf = new ArrayBuffer(s.length)
const view = new Uint8Array(buf)
for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF
return buf
}

@ -7,8 +7,7 @@ const baseUrl = '/file'
const defaultOptions = {
headers: {"Content-Type": "multipart/form-data"},
generateKey(filename) {
const now = new Date()
return timeFormat('yyyy/MM/dd/') + now.getTime() + '/' + filename
return timeFormat('yyyy/MM/dd/') + Date.now() + '/' + filename
},
onUploadProgress(e) {
@ -22,31 +21,33 @@ export function getToken() {
//下载文件
export function download(url, name) {
let href = typeof url === 'object' ? window.URL.createObjectURL(url) : url
let anchor = document.createElement('a')
const href = typeof url === 'object' ? window.URL.createObjectURL(url) : url
const anchor = document.createElement('a')
anchor.style.visibility = 'hidden'
anchor.href = href
anchor.download = name
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
if (typeof url === 'object') window.URL.revokeObjectURL(url)
}
//删除已上传的文件
export function deleteUpload(url) {
return request.get(baseUrl + '/delete?url=' + encodeURIComponent(url))
}
//七牛云直传
export function upload(blob, filename = '快照.png', options = defaultOptions) {
export function upload(blob, filename, options = {}) {
return getToken()
.then(token => {
const param = new FormData()
options = {...defaultOptions, ...options}
param.append('token', token)
param.append('key', options.generateKey(filename))
param.append('file', blob, filename)
return request.post(attachmentUploadUrl, param, options)
return request.post(attachmentUploadUrl, param, {...defaultOptions, ...options})
})
.then(({data}) => data)
}

@ -12,7 +12,7 @@
</template>
<script>
import {exportExcelByJs, json2sheet} from "@/utils/excel"
import {exportExcelByJs, json2workbook} from "@/utils/excel/exceljs"
export default {
name: "ExportExcelTest",
@ -41,8 +41,11 @@
this.loading = true
const arr = new Array(this.rows).fill(this.row)
const sheet = json2sheet(this.column, arr, this.merge ? {primaryKey: 'name', orderKey: 'no'} : null)
exportExcelByJs(sheet, '测试导出.xlsx')
const workbook = json2workbook(this.column, arr, this.merge ? {
primaryKey: 'name',
orderKey: 'no'
} : null)
exportExcelByJs(workbook, '测试导出.xlsx')
this.loading = false
const end = Date.now()

@ -72,7 +72,7 @@
import {search} from "@/api/stock/current"
import {isEmpty, debounce} from "@/utils"
import {plus} from "@/utils/math"
import {exportExcel} from "@/utils/excel"
import {exportExcel} from "@/utils/excel/xlsx"
import {getNodeId} from "@/utils/tree"
import tableMixin from '@/mixins/tablePageMixin'

Loading…
Cancel
Save