UNPKG

vxe-table-select-area

Version:

一个基于 vxe-table 的可区域选中复制、粘贴的组件

1,309 lines (1,242 loc) 54.7 kB
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