vxe-table-select-area
Version:
一个基于 vxe-table 的可区域选中复制、粘贴的组件
1,309 lines (1,242 loc) • 54.7 kB
JavaScript
import XEUtils from 'xe-utils'
import GlobalConfig from '../../v-x-e-table/src/conf'
import VXETable from '../../v-x-e-table'
import UtilTools from '../../tools/utils'
import { mergeBodyMethod, isColumnInfo } from '../../table/src/util'
import { browse } from '../../tools/dom'
import { warnLog, errLog, getLog } from '../../tools/log'
const { formatText } = UtilTools
// 默认导出或打印的 HTML 样式
const defaultHtmlStyle = 'body{margin:0;color:#333333;font-size:14px;font-family:"Microsoft YaHei",微软雅黑,"MicrosoftJhengHei",华文细黑,STHeiti,MingLiu}body *{-webkit-box-sizing:border-box;box-sizing:border-box}.vxe-table{border-collapse:collapse;text-align:left;border-spacing:0}.vxe-table:not(.is--print){table-layout:fixed}.vxe-table,.vxe-table th,.vxe-table td,.vxe-table td{border-color:#D0D0D0;border-style:solid;border-width:0}.vxe-table.is--print{width:100%}.border--default,.border--full,.border--outer{border-top-width:1px}.border--default,.border--full,.border--outer{border-left-width:1px}.border--outer,.border--default th,.border--default td,.border--full th,.border--full td,.border--outer th,.border--inner th,.border--inner td{border-bottom-width:1px}.border--default,.border--outer,.border--full th,.border--full td{border-right-width:1px}.border--default th,.border--full th,.border--outer th{background-color:#f8f8f9}.vxe-table td>div,.vxe-table th>div{padding:.5em .4em}.col--center{text-align:center}.col--right{text-align:right}.vxe-table:not(.is--print) .col--ellipsis>div{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-break:break-all}.vxe-table--tree-node{text-align:left}.vxe-table--tree-node-wrapper{position:relative}.vxe-table--tree-icon-wrapper{position:absolute;top:50%;width:1em;height:1em;text-align:center;-webkit-transform:translateY(-50%);transform:translateY(-50%);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer}.vxe-table--tree-unfold-icon,.vxe-table--tree-fold-icon{position:absolute;width:0;height:0;border-style:solid;border-width:.5em;border-right-color:transparent;border-bottom-color:transparent}.vxe-table--tree-unfold-icon{left:.3em;top:0;border-left-color:#939599;border-top-color:transparent}.vxe-table--tree-fold-icon{left:0;top:.3em;border-left-color:transparent;border-top-color:#939599}.vxe-table--tree-cell{display:block;padding-left:1.5em}.vxe-table input[type="checkbox"]{margin:0}.vxe-table input[type="checkbox"],.vxe-table input[type="radio"],.vxe-table input[type="checkbox"]+span,.vxe-table input[type="radio"]+span{vertical-align:middle;padding-left:0.4em}'
let htmlCellElem
// 导入
let fileForm
let fileInput
// 打印
let printFrame
const csvBOM = '\ufeff'
const enterSymbol = '\r\n'
function createFrame () {
const frame = document.createElement('iframe')
frame.className = 'vxe-table--print-frame'
return frame
}
function getExportBlobByContent (content, options) {
if (window.Blob) {
return new Blob([content], { type: `text/${options.type};charset=utf-8;` })
}
return null
}
function hasTreeChildren ($xetable, row) {
const treeOpts = $xetable.treeOpts
return row[treeOpts.children] && row[treeOpts.children].length > 0
}
function getSeq ($xetable, row, $rowIndex, column, $columnIndex) {
const seqOpts = $xetable.seqOpts
const seqMethod = seqOpts.seqMethod || column.seqMethod
if (seqMethod) {
return seqMethod({
row,
rowIndex: $xetable.getRowIndex(row),
$rowIndex,
column,
columnIndex: $xetable.getColumnIndex(column),
$columnIndex
})
}
return $xetable.getRowSeq(row)
}
function defaultFilterExportColumn (column) {
return column.property || ['seq', 'checkbox', 'radio'].indexOf(column.type) > -1
}
function toTableBorder (border) {
if (border === true) {
return 'full'
}
if (border) {
return border
}
return 'default'
}
function toBooleanValue (cellValue) {
return XEUtils.isBoolean(cellValue) ? (cellValue ? 'TRUE' : 'FALSE') : cellValue
}
function getLabelData ($xetable, opts, columns, datas) {
const { isAllExpand, mode } = opts
const { treeConfig, treeOpts, radioOpts, checkboxOpts } = $xetable
if (!htmlCellElem) {
htmlCellElem = document.createElement('div')
}
if (treeConfig) {
// 如果是树表格只允许导出数据源
const rest = []
const expandMaps = new Map()
XEUtils.eachTree(datas, (item, $rowIndex, items, path, parent, nodes) => {
const row = item._row || item
const parentRow = parent && parent._row ? parent._row : parent
if ((isAllExpand || !parentRow || (expandMaps.has(parentRow) && $xetable.isTreeExpandByRow(parentRow)))) {
const hasRowChild = hasTreeChildren($xetable, row)
const item = {
_row: row,
_level: nodes.length - 1,
_hasChild: hasRowChild,
_expand: hasRowChild && $xetable.isTreeExpandByRow(row)
}
columns.forEach((column, $columnIndex) => {
let cellValue = ''
const renderOpts = column.editRender || column.cellRender
let exportLabelMethod = column.exportMethod
if (!exportLabelMethod && renderOpts && renderOpts.name) {
const compConf = VXETable.renderer.get(renderOpts.name)
if (compConf) {
exportLabelMethod = compConf.exportMethod || compConf.cellExportMethod
}
}
if (exportLabelMethod) {
cellValue = exportLabelMethod({ $table: $xetable, row, column, options: opts })
} else {
switch (column.type) {
case 'seq':
cellValue = mode === 'all' ? path.map((num, i) => i % 2 === 0 ? (Number(num) + 1) : '.').join('') : getSeq($xetable, row, $rowIndex, column, $columnIndex)
break
case 'checkbox':
cellValue = toBooleanValue($xetable.isCheckedByCheckboxRow(row))
item._checkboxLabel = checkboxOpts.labelField ? XEUtils.get(row, checkboxOpts.labelField) : ''
item._checkboxDisabled = checkboxOpts.checkMethod && !checkboxOpts.checkMethod({ row })
break
case 'radio':
cellValue = toBooleanValue($xetable.isCheckedByRadioRow(row))
item._radioLabel = radioOpts.labelField ? XEUtils.get(row, radioOpts.labelField) : ''
item._radioDisabled = radioOpts.checkMethod && !radioOpts.checkMethod({ row })
break
default:
if (opts.original) {
cellValue = UtilTools.getCellValue(row, column)
} else {
cellValue = $xetable.getCellLabel(row, column)
if (column.type === 'html') {
htmlCellElem.innerHTML = cellValue
cellValue = htmlCellElem.innerText.trim()
} else {
const cell = $xetable.getCell(row, column)
if (cell) {
cellValue = cell.innerText.trim()
}
}
}
}
}
item[column.id] = XEUtils.toValueString(cellValue)
})
expandMaps.set(row, 1)
rest.push(Object.assign(item, row))
}
}, treeOpts)
return rest
}
return datas.map((row, $rowIndex) => {
const item = {
_row: row
}
columns.forEach((column, $columnIndex) => {
let cellValue = ''
const renderOpts = column.editRender || column.cellRender
let exportLabelMethod = column.exportMethod
if (!exportLabelMethod && renderOpts && renderOpts.name) {
const compConf = VXETable.renderer.get(renderOpts.name)
if (compConf) {
exportLabelMethod = compConf.exportMethod || compConf.cellExportMethod
}
}
if (exportLabelMethod) {
cellValue = exportLabelMethod({ $table: $xetable, row, column, options: opts })
} else {
switch (column.type) {
case 'seq':
cellValue = mode === 'all' ? $rowIndex + 1 : getSeq($xetable, row, $rowIndex, column, $columnIndex)
break
case 'checkbox':
cellValue = toBooleanValue($xetable.isCheckedByCheckboxRow(row))
item._checkboxLabel = checkboxOpts.labelField ? XEUtils.get(row, checkboxOpts.labelField) : ''
item._checkboxDisabled = checkboxOpts.checkMethod && !checkboxOpts.checkMethod({ row })
break
case 'radio':
cellValue = toBooleanValue($xetable.isCheckedByRadioRow(row))
item._radioLabel = radioOpts.labelField ? XEUtils.get(row, radioOpts.labelField) : ''
item._radioDisabled = radioOpts.checkMethod && !radioOpts.checkMethod({ row })
break
default :
if (opts.original) {
cellValue = UtilTools.getCellValue(row, column)
} else {
cellValue = $xetable.getCellLabel(row, column)
if (column.type === 'html') {
htmlCellElem.innerHTML = cellValue
cellValue = htmlCellElem.innerText.trim()
} else {
const cell = $xetable.getCell(row, column)
if (cell) {
cellValue = cell.innerText.trim()
}
}
}
}
}
item[column.id] = XEUtils.toValueString(cellValue)
})
return item
})
}
function getExportData ($xetable, opts) {
const { columns, dataFilterMethod } = opts
let datas = opts.data
if (dataFilterMethod) {
datas = datas.filter((row, index) => dataFilterMethod({ row, $rowIndex: index }))
}
return getLabelData($xetable, opts, columns, datas)
}
function getBooleanValue (cellValue) {
return cellValue === 'TRUE' || cellValue === 'true' || cellValue === true
}
function getHeaderTitle (opts, column) {
return (opts.original ? column.property : column.getTitle()) || ''
}
function getFooterCellValue ($xetable, opts, items, column) {
const renderOpts = column.editRender || column.cellRender
let exportLabelMethod = column.footerExportMethod
if (!exportLabelMethod && renderOpts && renderOpts.name) {
const compConf = VXETable.renderer.get(renderOpts.name)
if (compConf) {
exportLabelMethod = compConf.footerExportMethod || compConf.footerCellExportMethod
}
}
const _columnIndex = $xetable.getVTColumnIndex(column)
const cellValue = exportLabelMethod ? exportLabelMethod({ $table: $xetable, items, itemIndex: _columnIndex, _columnIndex, column, options: opts }) : XEUtils.toValueString(items[_columnIndex])
return cellValue
}
function getFooterData (opts, footerTableData) {
const { footerFilterMethod } = opts
return footerFilterMethod ? footerTableData.filter((items, index) => footerFilterMethod({ items, $rowIndex: index })) : footerTableData
}
function getCsvCellTypeLabel (column, cellValue) {
if (cellValue) {
if (column.type === 'seq') {
return `\t${cellValue}`
}
switch (column.cellType) {
case 'string':
if (!isNaN(cellValue)) {
return `\t${cellValue}`
}
break
case 'number':
break
default:
if (cellValue.length >= 12 && !isNaN(cellValue)) {
return `\t${cellValue}`
}
break
}
}
return cellValue
}
function toTxtCellLabel (val) {
if (/[",\s\n]/.test(val)) {
return `"${val.replace(/"/g, '""')}"`
}
return val
}
function toCsv ($xetable, opts, columns, datas) {
let content = csvBOM
if (opts.isHeader) {
content += columns.map(column => toTxtCellLabel(getHeaderTitle(opts, column))).join(',') + enterSymbol
}
datas.forEach(row => {
content += columns.map(column => toTxtCellLabel(getCsvCellTypeLabel(column, row[column.id]))).join(',') + enterSymbol
})
if (opts.isFooter) {
const footerTableData = $xetable.footerTableData
const footers = getFooterData(opts, footerTableData)
footers.forEach(rows => {
content += columns.map(column => toTxtCellLabel(getFooterCellValue($xetable, opts, rows, column))).join(',') + enterSymbol
})
}
return content
}
function toTxt ($xetable, opts, columns, datas) {
let content = ''
if (opts.isHeader) {
content += columns.map(column => toTxtCellLabel(getHeaderTitle(opts, column))).join('\t') + enterSymbol
}
datas.forEach(row => {
content += columns.map(column => toTxtCellLabel(row[column.id])).join('\t') + enterSymbol
})
if (opts.isFooter) {
const footerTableData = $xetable.footerTableData
const footers = getFooterData(opts, footerTableData)
footers.forEach(rows => {
content += columns.map(column => toTxtCellLabel(getFooterCellValue($xetable, opts, rows, column))).join(',') + enterSymbol
})
}
return content
}
function hasEllipsis ($xetable, column, property, allColumnOverflow) {
const columnOverflow = column[property]
const headOverflow = XEUtils.isUndefined(columnOverflow) || XEUtils.isNull(columnOverflow) ? allColumnOverflow : columnOverflow
const showEllipsis = headOverflow === 'ellipsis'
const showTitle = headOverflow === 'title'
const showTooltip = headOverflow === true || headOverflow === 'tooltip'
let isEllipsis = showTitle || showTooltip || showEllipsis
// 虚拟滚动不支持动态高度
if (($xetable.scrollXLoad || $xetable.scrollYLoad) && !isEllipsis) {
isEllipsis = true
}
return isEllipsis
}
function createHtmlPage (opts, content) {
const { style } = opts
return [
'<!DOCTYPE html><html>',
'<head>',
'<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,minimal-ui">',
`<title>${opts.sheetName}</title>`,
`<style>${defaultHtmlStyle}</style>`,
style ? `<style>${style}</style>` : '',
'</head>',
`<body>${content}</body>`,
'</html>'
].join('')
}
function toHtml ($xetable, opts, columns, datas) {
const { id, border, treeConfig, treeOpts, isAllSelected, isIndeterminate, headerAlign: allHeaderAlign, align: allAlign, footerAlign: allFooterAlign, showOverflow: allColumnOverflow, showHeaderOverflow: allColumnHeaderOverflow, mergeList } = $xetable
const { print: isPrint, isHeader, isFooter, isColgroup, isMerge, colgroups, original } = opts
const allCls = 'check-all'
const clss = [
'vxe-table',
`border--${toTableBorder(border)}`,
isPrint ? 'is--print' : '',
isHeader ? 'is--header' : ''
].filter(cls => cls)
const tables = [
`<table class="${clss.join(' ')}" border="0" cellspacing="0" cellpadding="0">`,
`<colgroup>${columns.map(column => `<col style="width:${column.renderWidth}px">`).join('')}</colgroup>`
]
if (isHeader) {
tables.push('<thead>')
if (isColgroup && !original) {
colgroups.forEach(cols => {
tables.push(
`<tr>${cols.map(column => {
const headAlign = column.headerAlign || column.align || allHeaderAlign || allAlign
const classNames = hasEllipsis($xetable, column, 'showHeaderOverflow', allColumnHeaderOverflow) ? ['col--ellipsis'] : []
const cellTitle = getHeaderTitle(opts, column)
let childWidth = 0
let countChild = 0
XEUtils.eachTree([column], item => {
if (!item.childNodes || !column.childNodes.length) {
countChild++
}
childWidth += item.renderWidth
}, { children: 'childNodes' })
const cellWidth = childWidth - countChild
if (headAlign) {
classNames.push(`col--${headAlign}`)
}
if (column.type === 'checkbox') {
return `<th class="${classNames.join(' ')}" colspan="${column._colSpan}" rowspan="${column._rowSpan}"><div ${isPrint ? '' : `style="width: ${cellWidth}px"`}><input type="checkbox" class="${allCls}" ${isAllSelected ? 'checked' : ''}><span>${cellTitle}</span></div></th>`
}
return `<th class="${classNames.join(' ')}" colspan="${column._colSpan}" rowspan="${column._rowSpan}" title="${cellTitle}"><div ${isPrint ? '' : `style="width: ${cellWidth}px"`}><span>${formatText(cellTitle, true)}</span></div></th>`
}).join('')}</tr>`
)
})
} else {
tables.push(
`<tr>${columns.map(column => {
const headAlign = column.headerAlign || column.align || allHeaderAlign || allAlign
const classNames = hasEllipsis($xetable, column, 'showHeaderOverflow', allColumnHeaderOverflow) ? ['col--ellipsis'] : []
const cellTitle = getHeaderTitle(opts, column)
if (headAlign) {
classNames.push(`col--${headAlign}`)
}
if (column.type === 'checkbox') {
return `<th class="${classNames.join(' ')}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}><input type="checkbox" class="${allCls}" ${isAllSelected ? 'checked' : ''}><span>${cellTitle}</span></div></th>`
}
return `<th class="${classNames.join(' ')}" title="${cellTitle}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}><span>${formatText(cellTitle, true)}</span></div></th>`
}).join('')}</tr>`
)
}
tables.push('</thead>')
}
if (datas.length) {
tables.push('<tbody>')
if (treeConfig) {
datas.forEach(item => {
tables.push(
'<tr>' + columns.map(column => {
const cellAlign = column.align || allAlign
const classNames = hasEllipsis($xetable, column, 'showOverflow', allColumnOverflow) ? ['col--ellipsis'] : []
const cellValue = item[column.id]
if (cellAlign) {
classNames.push(`col--${cellAlign}`)
}
if (column.treeNode) {
let treeIcon = ''
if (item._hasChild) {
treeIcon = `<i class="${item._expand ? 'vxe-table--tree-fold-icon' : 'vxe-table--tree-unfold-icon'}"></i>`
}
classNames.push('vxe-table--tree-node')
if (column.type === 'radio') {
return `<td class="${classNames.join(' ')}" title="${cellValue}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}><div class="vxe-table--tree-node-wrapper" style="padding-left: ${item._level * treeOpts.indent}px"><div class="vxe-table--tree-icon-wrapper">${treeIcon}</div><div class="vxe-table--tree-cell"><input type="radio" name="radio_${id}" ${item._radioDisabled ? 'disabled ' : ''}${getBooleanValue(cellValue) ? 'checked' : ''}><span>${item._radioLabel}</span></div></div></div></td>`
} else if (column.type === 'checkbox') {
return `<td class="${classNames.join(' ')}" title="${cellValue}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}><div class="vxe-table--tree-node-wrapper" style="padding-left: ${item._level * treeOpts.indent}px"><div class="vxe-table--tree-icon-wrapper">${treeIcon}</div><div class="vxe-table--tree-cell"><input type="checkbox" ${item._checkboxDisabled ? 'disabled ' : ''}${getBooleanValue(cellValue) ? 'checked' : ''}><span>${item._checkboxLabel}</span></div></div></div></td>`
}
return `<td class="${classNames.join(' ')}" title="${cellValue}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}><div class="vxe-table--tree-node-wrapper" style="padding-left: ${item._level * treeOpts.indent}px"><div class="vxe-table--tree-icon-wrapper">${treeIcon}</div><div class="vxe-table--tree-cell">${cellValue}</div></div></div></td>`
}
if (column.type === 'radio') {
return `<td class="${classNames.join(' ')}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}><input type="radio" name="radio_${id}" ${item._radioDisabled ? 'disabled ' : ''}${getBooleanValue(cellValue) ? 'checked' : ''}><span>${item._radioLabel}</span></div></td>`
} else if (column.type === 'checkbox') {
return `<td class="${classNames.join(' ')}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}><input type="checkbox" ${item._checkboxDisabled ? 'disabled ' : ''}${getBooleanValue(cellValue) ? 'checked' : ''}><span>${item._checkboxLabel}</span></div></td>`
}
return `<td class="${classNames.join(' ')}" title="${cellValue}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}>${formatText(cellValue, true)}</div></td>`
}).join('') + '</tr>'
)
})
} else {
datas.forEach(item => {
tables.push(
'<tr>' + columns.map(column => {
const cellAlign = column.align || allAlign
const classNames = hasEllipsis($xetable, column, 'showOverflow', allColumnOverflow) ? ['col--ellipsis'] : []
const cellValue = item[column.id]
let rowSpan = 1
let colSpan = 1
if (isMerge && mergeList.length) {
const _rowIndex = $xetable.getVTRowIndex(item._row)
const _columnIndex = $xetable.getVTColumnIndex(column)
const spanRest = mergeBodyMethod(mergeList, _rowIndex, _columnIndex)
if (spanRest) {
const { rowspan, colspan } = spanRest
if (!rowspan || !colspan) {
return ''
}
if (rowspan > 1) {
rowSpan = rowspan
}
if (colspan > 1) {
colSpan = colspan
}
}
}
if (cellAlign) {
classNames.push(`col--${cellAlign}`)
}
if (column.type === 'radio') {
return `<td class="${classNames.join(' ')}" rowspan="${rowSpan}" colspan="${colSpan}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}><input type="radio" name="radio_${id}" ${item._radioDisabled ? 'disabled ' : ''}${getBooleanValue(cellValue) ? 'checked' : ''}><span>${item._radioLabel}</span></div></td>`
} else if (column.type === 'checkbox') {
return `<td class="${classNames.join(' ')}" rowspan="${rowSpan}" colspan="${colSpan}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}><input type="checkbox" ${item._checkboxDisabled ? 'disabled ' : ''}${getBooleanValue(cellValue) ? 'checked' : ''}><span>${item._checkboxLabel}</span></div></td>`
}
return `<td class="${classNames.join(' ')}" rowspan="${rowSpan}" colspan="${colSpan}" title="${cellValue}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}>${formatText(cellValue, true)}</div></td>`
}).join('') + '</tr>'
)
})
}
tables.push('</tbody>')
}
if (isFooter) {
const footerTableData = $xetable.footerTableData
const footers = getFooterData(opts, footerTableData)
if (footers.length) {
tables.push('<tfoot>')
footers.forEach(rows => {
tables.push(
`<tr>${columns.map(column => {
const footAlign = column.footerAlign || column.align || allFooterAlign || allAlign
const classNames = hasEllipsis($xetable, column, 'showOverflow', allColumnOverflow) ? ['col--ellipsis'] : []
const cellValue = getFooterCellValue($xetable, opts, rows, column)
if (footAlign) {
classNames.push(`col--${footAlign}`)
}
return `<td class="${classNames.join(' ')}" title="${cellValue}"><div ${isPrint ? '' : `style="width: ${column.renderWidth}px"`}>${formatText(cellValue, true)}</div></td>`
}).join('')}</tr>`
)
})
tables.push('</tfoot>')
}
}
// 是否半选状态
const script = !isAllSelected && isIndeterminate ? `<script>(function(){var a=document.querySelector(".${allCls}");if(a){a.indeterminate=true}})()</script>` : ''
tables.push('</table>', script)
return isPrint ? tables.join('') : createHtmlPage(opts, tables.join(''))
}
function toXML ($xetable, opts, columns, datas) {
let xml = [
'<?xml version="1.0"?>',
'<?mso-application progid="Excel.Sheet"?>',
'<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet" xmlns:html="http://www.w3.org/TR/REC-html40">',
'<DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">',
'<Version>16.00</Version>',
'</DocumentProperties>',
'<ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">',
'<WindowHeight>7920</WindowHeight>',
'<WindowWidth>21570</WindowWidth>',
'<WindowTopX>32767</WindowTopX>',
'<WindowTopY>32767</WindowTopY>',
'<ProtectStructure>False</ProtectStructure>',
'<ProtectWindows>False</ProtectWindows>',
'</ExcelWorkbook>',
`<Worksheet ss:Name="${opts.sheetName}">`,
'<Table>',
columns.map(column => `<Column ss:Width="${column.renderWidth}"/>`).join('')
].join('')
if (opts.isHeader) {
xml += `<Row>${columns.map(column => `<Cell><Data ss:Type="String">${getHeaderTitle(opts, column)}</Data></Cell>`).join('')}</Row>`
}
datas.forEach(row => {
xml += '<Row>' + columns.map(column => `<Cell><Data ss:Type="String">${row[column.id]}</Data></Cell>`).join('') + '</Row>'
})
if (opts.isFooter) {
const footerTableData = $xetable.footerTableData
const footers = getFooterData(opts, footerTableData)
footers.forEach(rows => {
xml += `<Row>${columns.map(column => `<Cell><Data ss:Type="String">${getFooterCellValue($xetable, opts, rows, column)}</Data></Cell>`).join('')}</Row>`
})
}
return `${xml}</Table></Worksheet></Workbook>`
}
function getContent ($xetable, opts, columns, datas) {
if (columns.length) {
switch (opts.type) {
case 'csv':
return toCsv($xetable, opts, columns, datas)
case 'txt':
return toTxt($xetable, opts, columns, datas)
case 'html':
return toHtml($xetable, opts, columns, datas)
case 'xml':
return toXML($xetable, opts, columns, datas)
}
}
return ''
}
/**
* 保存文件到本地
* @param {*} options 参数
*/
export function saveLocalFile (options) {
const { filename, type, content } = options
const name = `${filename}.${type}`
if (window.Blob) {
const blob = content instanceof Blob ? content : getExportBlobByContent(XEUtils.toValueString(content), options)
if (navigator.msSaveBlob) {
navigator.msSaveBlob(blob, name)
} else {
const url = URL.createObjectURL(blob)
const linkElem = document.createElement('a')
linkElem.target = '_blank'
linkElem.download = name
linkElem.href = url
document.body.appendChild(linkElem)
linkElem.click()
document.body.removeChild(linkElem)
requestAnimationFrame(() => {
if (linkElem.parentNode) {
linkElem.parentNode.removeChild(linkElem)
}
URL.revokeObjectURL(url)
})
}
return Promise.resolve()
}
return Promise.reject(new Error(getLog('vxe.error.notExp')))
}
function downloadFile ($xetable, opts, content) {
const { filename, type, download } = opts
if (!download) {
const blob = getExportBlobByContent(content, opts)
return Promise.resolve({ type, content, blob })
}
saveLocalFile({ filename, type, content }).then(() => {
if (opts.message !== false) {
// 检测弹窗模块
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
if (!VXETable.modal) {
errLog('vxe.error.reqModule', ['Modal'])
}
}
VXETable.modal.message({ content: GlobalConfig.i18n('vxe.table.expSuccess'), status: 'success' })
}
})
}
function clearColumnConvert (columns) {
XEUtils.eachTree(columns, column => {
delete column._level
delete column._colSpan
delete column._rowSpan
delete column._children
delete column.childNodes
}, { children: 'children' })
}
function handleExport ($xetable, opts) {
const { remote, columns, colgroups, exportMethod, afterExportMethod } = opts
return new Promise(resolve => {
if (remote) {
const params = { options: opts, $table: $xetable, $grid: $xetable.$xegrid }
resolve(exportMethod ? exportMethod(params) : params)
} else {
const datas = getExportData($xetable, opts)
resolve(
$xetable.preventEvent(null, 'event.export', { options: opts, columns, colgroups, datas }, () => {
return downloadFile($xetable, opts, getContent($xetable, opts, columns, datas))
})
)
}
}).then((params) => {
clearColumnConvert(columns)
if (!opts.print) {
if (afterExportMethod) {
afterExportMethod({ status: true, options: opts, $table: $xetable, $grid: $xetable.$xegrid })
}
}
return Object.assign({ status: true }, params)
}).catch(() => {
clearColumnConvert(columns)
if (!opts.print) {
if (afterExportMethod) {
afterExportMethod({ status: false, options: opts, $table: $xetable, $grid: $xetable.$xegrid })
}
}
const params = { status: false }
return Promise.reject(params)
})
}
function getElementsByTagName (elem, qualifiedName) {
return elem.getElementsByTagName(qualifiedName)
}
function getTxtCellKey (now) {
return `#${now}@${XEUtils.uniqueId()}`
}
function replaceTxtCell (cell, vMaps) {
return cell.replace(/#\d+@\d+/g, (key) => XEUtils.hasOwnProp(vMaps, key) ? vMaps[key] : key)
}
function getTxtCellValue (val, vMaps) {
const rest = replaceTxtCell(val, vMaps)
return rest.replace(/^"+$/g, (qVal) => '"'.repeat(Math.ceil(qVal.length / 2)))
}
function parseCsvAndTxt (columns, content, cellSeparator) {
const list = content.split(enterSymbol)
const rows = []
let fields = []
if (list.length) {
const vMaps = {}
const now = Date.now()
list.forEach((rVal) => {
if (rVal) {
const item = {}
rVal = rVal.replace(/("")|(\n)/g, (text, dVal) => {
const key = getTxtCellKey(now)
vMaps[key] = dVal ? '"' : '\n'
return key
}).replace(/"(.*?)"/g, (text, cVal) => {
const key = getTxtCellKey(now)
vMaps[key] = replaceTxtCell(cVal, vMaps)
return key
})
const cells = rVal.split(cellSeparator)
if (!fields.length) {
fields = cells.map((val) => getTxtCellValue(val.trim(), vMaps))
} else {
cells.forEach((val, colIndex) => {
if (colIndex < fields.length) {
item[fields[colIndex]] = getTxtCellValue(val, vMaps)
}
})
rows.push(item)
}
}
})
}
return { fields, rows }
}
function parseCsv (columns, content) {
return parseCsvAndTxt(columns, content, ',')
}
function parseTxt (columns, content) {
return parseCsvAndTxt(columns, content, '\t')
}
function parseHTML (columns, content) {
const domParser = new DOMParser()
const xmlDoc = domParser.parseFromString(content, 'text/html')
const bodyNodes = getElementsByTagName(xmlDoc, 'body')
const rows = []
const fields = []
if (bodyNodes.length) {
const tableNodes = getElementsByTagName(bodyNodes[0], 'table')
if (tableNodes.length) {
const theadNodes = getElementsByTagName(tableNodes[0], 'thead')
if (theadNodes.length) {
XEUtils.arrayEach(getElementsByTagName(theadNodes[0], 'tr'), rowNode => {
XEUtils.arrayEach(getElementsByTagName(rowNode, 'th'), cellNode => {
fields.push(cellNode.textContent)
})
})
const tbodyNodes = getElementsByTagName(tableNodes[0], 'tbody')
if (tbodyNodes.length) {
XEUtils.arrayEach(getElementsByTagName(tbodyNodes[0], 'tr'), rowNode => {
const item = {}
XEUtils.arrayEach(getElementsByTagName(rowNode, 'td'), (cellNode, colIndex) => {
if (fields[colIndex]) {
item[fields[colIndex]] = cellNode.textContent || ''
}
})
rows.push(item)
})
}
}
}
}
return { fields, rows }
}
function parseXML (columns, content) {
const domParser = new DOMParser()
const xmlDoc = domParser.parseFromString(content, 'application/xml')
const sheetNodes = getElementsByTagName(xmlDoc, 'Worksheet')
const rows = []
const fields = []
if (sheetNodes.length) {
const tableNodes = getElementsByTagName(sheetNodes[0], 'Table')
if (tableNodes.length) {
const rowNodes = getElementsByTagName(tableNodes[0], 'Row')
if (rowNodes.length) {
XEUtils.arrayEach(getElementsByTagName(rowNodes[0], 'Cell'), cellNode => {
fields.push(cellNode.textContent)
})
XEUtils.arrayEach(rowNodes, (rowNode, index) => {
if (index) {
const item = {}
const cellNodes = getElementsByTagName(rowNode, 'Cell')
XEUtils.arrayEach(cellNodes, (cellNode, colIndex) => {
if (fields[colIndex]) {
item[fields[colIndex]] = cellNode.textContent
}
})
rows.push(item)
}
})
}
}
}
return { fields, rows }
}
/**
* 检查导入的列是否完整
* @param {Array} fields 字段名列表
* @param {Array} rows 数据列表
*/
function checkImportData (columns, fields) {
const tableFields = []
columns.forEach((column) => {
const field = column.property
if (field) {
tableFields.push(field)
}
})
return fields.some(field => tableFields.indexOf(field) > -1)
}
function handleImport ($xetable, content, opts) {
const { tableFullColumn, _importResolve, _importReject } = $xetable
let rest = { fields: [], rows: [] }
switch (opts.type) {
case 'csv':
rest = parseCsv(tableFullColumn, content)
break
case 'txt':
rest = parseTxt(tableFullColumn, content)
break
case 'html':
rest = parseHTML(tableFullColumn, content)
break
case 'xml':
rest = parseXML(tableFullColumn, content)
break
}
const { fields, rows } = rest
const status = checkImportData(tableFullColumn, fields)
if (status) {
$xetable.createData(rows)
.then((data) => {
let loadRest
if (opts.mode === 'insert') {
loadRest = $xetable.insert(data)
} else {
loadRest = $xetable.reloadData(data)
}
if (opts.message !== false) {
// 检测弹窗模块
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
if (!VXETable.modal) {
errLog('vxe.error.reqModule', ['Modal'])
}
}
VXETable.modal.message({ content: GlobalConfig.i18n('vxe.table.impSuccess', [rows.length]), status: 'success' })
}
return loadRest.then(() => {
if (_importResolve) {
_importResolve({ status: true })
}
})
})
} else if (opts.message !== false) {
// 检测弹窗模块
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
if (!VXETable.modal) {
errLog('vxe.error.reqModule', ['Modal'])
}
}
VXETable.modal.message({ content: GlobalConfig.i18n('vxe.error.impFields'), status: 'error' })
if (_importReject) {
_importReject({ status: false })
}
}
}
function handleFileImport ($xetable, file, opts) {
const { importMethod, afterImportMethod } = opts
const { type, filename } = UtilTools.parseFile(file)
// 检查类型,如果为自定义导出,则不需要校验类型
if (!importMethod && !XEUtils.includes(VXETable.config.importTypes, type)) {
if (opts.message !== false) {
// 检测弹窗模块
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
if (!VXETable.modal) {
errLog('vxe.error.reqModule', ['Modal'])
}
}
VXETable.modal.message({ content: GlobalConfig.i18n('vxe.error.notType', [type]), status: 'error' })
}
const params = { status: false }
return Promise.reject(params)
}
const rest = new Promise((resolve, reject) => {
const _importResolve = (params) => {
resolve(params)
$xetable._importResolve = null
$xetable._importReject = null
}
const _importReject = (params) => {
reject(params)
$xetable._importResolve = null
$xetable._importReject = null
}
$xetable._importResolve = _importResolve
$xetable._importReject = _importReject
if (window.FileReader) {
const options = Object.assign({ mode: 'insert' }, opts, { type, filename })
if (options.remote) {
if (importMethod) {
Promise.resolve(importMethod({ file, options, $table: $xetable })).then(() => {
_importResolve({ status: true })
}).catch(() => {
_importResolve({ status: true })
})
} else {
_importResolve({ status: true })
}
} else {
$xetable.preventEvent(null, 'event.import', { file, options, columns: $xetable.tableFullColumn }, () => {
const reader = new FileReader()
reader.onerror = () => {
errLog('vxe.error.notType', [type])
_importReject({ status: false })
}
reader.onload = (e) => {
handleImport($xetable, e.target.result, options)
}
reader.readAsText(file, options.encoding || 'UTF-8')
})
}
} else {
// 不支持的浏览器
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
errLog('vxe.error.notExp')
}
_importResolve({ status: true })
}
})
return rest.then(() => {
if (afterImportMethod) {
afterImportMethod({ status: true, options: opts, $table: $xetable })
}
}).catch((e) => {
if (afterImportMethod) {
afterImportMethod({ status: false, options: opts, $table: $xetable })
}
return Promise.reject(e)
})
}
/**
* 读取本地文件
* @param {*} options 参数
*/
export function readLocalFile (options = {}) {
if (!fileForm) {
fileForm = document.createElement('form')
fileInput = document.createElement('input')
fileForm.className = 'vxe-table--file-form'
fileInput.name = 'file'
fileInput.type = 'file'
fileForm.appendChild(fileInput)
document.body.appendChild(fileForm)
}
return new Promise((resolve, reject) => {
const types = options.types || []
const isAllType = !types.length || types.some((type) => type === '*')
fileInput.multiple = !!options.multiple
fileInput.accept = isAllType ? '' : `.${types.join(', .')}`
fileInput.onchange = (evnt) => {
const { files } = evnt.target
const file = files[0]
let errType
// 校验类型
if (!isAllType) {
for (let fIndex = 0; fIndex < files.length; fIndex++) {
const { type } = UtilTools.parseFile(files[fIndex])
if (!XEUtils.includes(types, type)) {
errType = type
break
}
}
}
if (!errType) {
resolve({ status: true, files, file })
} else {
if (options.message !== false) {
// 检测弹窗模块
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
if (!VXETable.modal) {
errLog('vxe.error.reqModule', ['Modal'])
}
}
VXETable.modal.message({ content: GlobalConfig.i18n('vxe.error.notType', [errType]), status: 'error' })
}
const params = { status: false, files, file }
reject(params)
}
}
fileForm.reset()
fileInput.click()
})
}
function removePrintFrame () {
if (printFrame) {
if (printFrame.parentNode) {
try {
printFrame.contentDocument.write('')
} catch (e) { }
printFrame.parentNode.removeChild(printFrame)
}
printFrame = null
}
}
function appendPrintFrame () {
if (!printFrame.parentNode) {
document.body.appendChild(printFrame)
}
}
function afterPrintEvent () {
requestAnimationFrame(removePrintFrame)
}
export function handlePrint ($xetable, opts, content) {
const { beforePrintMethod } = opts
if (beforePrintMethod) {
content = beforePrintMethod({ content, options: opts, $table: $xetable }) || ''
}
content = createHtmlPage(opts, content)
const blob = getExportBlobByContent(content, opts)
if (browse.msie) {
removePrintFrame()
printFrame = createFrame()
appendPrintFrame()
printFrame.contentDocument.write(content)
printFrame.contentDocument.execCommand('print')
} else {
if (!printFrame) {
printFrame = createFrame()
printFrame.onload = (evnt) => {
if (evnt.target.src) {
evnt.target.contentWindow.onafterprint = afterPrintEvent
evnt.target.contentWindow.print()
}
}
}
appendPrintFrame()
printFrame.src = URL.createObjectURL(blob)
}
}
function handleExportAndPrint ($xetable, options, isPrint) {
const { initStore, customOpts, collectColumn, footerTableData, treeConfig, mergeList, isGroup, exportParams } = $xetable
const selectRecords = $xetable.getCheckboxRecords()
const hasFooter = !!footerTableData.length
const hasTree = treeConfig
const hasMerge = !hasTree && mergeList.length
const defOpts = Object.assign({ message: true, isHeader: true }, options)
const types = defOpts.types || VXETable.config.exportTypes
const modes = defOpts.modes
const checkMethod = customOpts.checkMethod
const exportColumns = collectColumn.slice(0)
const { columns } = defOpts
// 处理类型
const typeList = types.map(value => {
return {
value,
label: `vxe.export.types.${value}`
}
})
const modeList = modes.map(value => {
return {
value,
label: `vxe.export.modes.${value}`
}
})
// 默认选中
XEUtils.eachTree(exportColumns, (column, index, items, path, parent) => {
const isColGroup = column.children && column.children.length
if (isColGroup || defaultFilterExportColumn(column)) {
column.checked = columns ? columns.some((item) => {
if (isColumnInfo(item)) {
return column === item
} else if (XEUtils.isString(item)) {
return column.field === item
} else {
const colid = item.id || item.colId
const type = item.type
const field = item.property || item.field
if (colid) {
return column.id === colid
} else if (field && type) {
return column.property === field && column.type === type
} else if (field) {
return column.property === field
} else if (type) {
return column.type === type
}
}
return false
}) : column.visible
column.halfChecked = false
column.disabled = (parent && parent.disabled) || (checkMethod ? !checkMethod({ column }) : false)
}
})
// 更新条件
Object.assign($xetable.exportStore, {
columns: exportColumns,
typeList,
modeList,
hasFooter,
hasMerge,
hasTree,
isPrint,
hasColgroup: isGroup,
visible: true
})
// 默认参数
Object.assign(exportParams, {
mode: selectRecords.length ? 'selected' : 'current'
}, defOpts)
if (modes.indexOf(exportParams.mode) === -1) {
exportParams.mode = modes[0]
}
if (types.indexOf(exportParams.type) === -1) {
exportParams.type = types[0]
}
initStore.export = true
return $xetable.$nextTick()
}
const getConvertColumns = (columns) => {
const result = []
columns.forEach((column) => {
if (column.childNodes && column.childNodes.length) {
result.push(column)
result.push(...getConvertColumns(column.childNodes))
} else {
result.push(column)
}
})
return result
}
const convertToRows = (originColumns) => {
let maxLevel = 1
const traverse = (column, parent) => {
if (parent) {
column._level = parent._level + 1
if (maxLevel < column._level) {
maxLevel = column._level
}
}
if (column.childNodes && column.childNodes.length) {
let colSpan = 0
column.childNodes.forEach((subColumn) => {
traverse(subColumn, column)
colSpan += subColumn._colSpan
})
column._colSpan = colSpan
} else {
column._colSpan = 1
}
}
originColumns.forEach((column) => {
column._level = 1
traverse(column)
})
const rows = []
for (let i = 0; i < maxLevel; i++) {
rows.push([])
}
const allColumns = getConvertColumns(originColumns)
allColumns.forEach((column) => {
if (column.childNodes && column.childNodes.length) {
column._rowSpan = 1
} else {
column._rowSpan = maxLevel - column._level + 1
}
rows[column._level - 1].push(column)
})
return rows
}
export default {
methods: {
/**
* 导出文件,支持 csv/html/xml/txt
* 如果是树表格,则默认是导出所有节点
* 如果是启用了虚拟滚动,则只能导出数据源,可以配合 dataFilterMethod 函数自行转换数据
* @param {Object} options 参数
*/
_exportData (options) {
const { $xegrid, isGroup, tableGroupColumn, tableFullColumn, afterFullData, treeConfig, treeOpts, exportOpts } = this
const opts = Object.assign({
// filename: '',
// sheetName: '',
// original: false,
// message: false,
isHeader: true,
isFooter: true,
isColgroup: true,
isMerge: false,
isAllExpand: false,
download: true,
type: 'csv',
mode: 'current'
// data: null,
// remote: false,
// dataFilterMethod: null,
// footerFilterMethod: null,
// exportMethod: null,
// columnFilterMethod: null,
// beforeExportMethod: null,
// afterExportMethod: null
}, exportOpts, {
print: false
}, options)
const { type, mode, columns, original, beforeExportMethod } = opts
let groups = []
const customCols = columns && columns.length ? columns : null
// 如果设置源数据,则默认导出设置了字段的列
let columnFilterMethod = opts.columnFilterMethod
if (!customCols && !columnFilterMethod) {
columnFilterMethod = original ? ({ column }) => column.property : ({ column }) => defaultFilterExportColumn(column)
}
if (customCols) {
groups = XEUtils.searchTree(
XEUtils.mapTree(customCols, item => {
let targetColumn
if (item) {
if (isColumnInfo(item)) {
targetColumn = item
} else if (XEUtils.isString(item)) {
targetColumn = this.getColumnByField(item)
} else {
const colid = item.id || item.colId
const type = item.type
const field = item.property || item.field
if (colid) {
targetColumn = this.getColumnById(colid)
} else if (field && type) {
targetColumn = tableFullColumn.find((column) => column.property === field && column.type === type)
} else if (field) {
targetColumn = this.getColumnByField(field)
} else if (type) {
targetColumn = tableFullColumn.find((column) => column.type === type)
}
}
return targetColumn || {}
}
}, {
children: 'childNodes',
mapChildren: '_children'
}),
(column, index) => isColumnInfo(column) && (!columnFilterMethod || columnFilterMethod({ column, $columnIndex: index })),
{
children: '_children',
mapChildren: 'childNodes',
original: true
}
)
} else {
groups = XEUtils.searchTree(isGroup ? tableGroupColumn : tableFullColumn, (column, index) => column.visible && (!columnFilterMethod || columnFilterMethod({ column, $columnIndex: index })), { children: 'children', mapChildren: 'childNodes', original: true })
}
// 获取所有列
const cols = []
XEUtils.eachTree(groups, column => {
const isColGroup = column.children && column.children.length
if (!isColGroup) {
cols.push(column)
}
}, { children: 'childNodes' })
// 构建分组层级
opts.columns = cols
opts.colgroups = convertToRows(groups)
if (!opts.filename) {
opts.filename = GlobalConfig.i18n(opts.original ? 'vxe.table.expOriginFilename' : 'vxe.table.expFilename', [XEUtils.toDateString(Date.now(), 'yyyyMMddHHmmss')])
}
if (!opts.sheetName) {
opts.sheetName = document.title
}
// 检查类型,如果为自定义导出,则不需要校验类型
if (!opts.exportMethod && !XEUtils.includes(VXETable.config.exportTypes, type)) {
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
errLog('vxe.error.notType', [type])
}
const params = { status: false }
return Promise.reject(params)
}
if (!opts.print) {
if (beforeExportMethod) {
beforeExportMethod({ options: opts, $table: this, $grid: $xegrid })
}
}
if (!opts.data) {
opts.data = afterFullData
if (mode === 'selected') {
const selectRecords = this.getCheckboxRecords()
if (['html', 'pdf'].indexOf(type) > -1 && treeConfig) {
opts.data = XEUtils.searchTree(this.getTableData().fullData, (item) => selectRecords.indexOf(item) > -1, Object.assign({}, treeOpts, { data: '_row' }))
} else {
opts.data = selectRecords
}
} else if (mode === 'all') {
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
if (!$xegrid) {
warnLog('vxe.error.errProp', ['all', 'mode=current,selected'])
}
}
if ($xegrid && !opts.remote) {
const { beforeQueryAll, afterQueryAll, ajax = {}, props = {} } = $xegrid.proxyOpts
const ajaxMethods = ajax.queryAll
if (process.env.VUE_APP_VXE_TABLE_ENV === 'development') {
if (!ajaxMethods) {
warnLog('vxe.error.notFunc', ['proxy-config.ajax.queryAll'])
}
}
if (ajaxMethods) {
const params = {
$table: this,
$grid: $xegrid,
s