提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
在实现网页可以编辑excel表格时,选用了x-spreadsheet这个插件,结合xlsx实现导入和导出时,发现是没有带样式的,包括行高/列宽/背景颜色等,当表头字段和单元格有多行数据时,页面表格不适应宽高是不合理的,因此研究下样式问题如何优化本示例使用 vue + xlsx + x-spreadsheet
一、X-Spreadsheet
X-Spreadsheet 是一个基于 Web 的 JS 在线表格
中文文档:https://hondrytravis.com/x-spreadsheet-doc/
二、导入和导出
导入:将本地excel数据 转为 x-spreadsheet支持的格式数据
导出:将x-spreadsheet格式数据 转为 excel能识别的格式
1. 导入
import * as xlsx from "xlsx"
// 导入
const handleFileChange: UploadProps['onChange'] = (uploadFile, uploadFiles) => {
const file = uploadFile as UploadRawFile; // 获取用户选择的文件
const reader = new FileReader();// 使用FileReader读取文件内容为ArrayBuffer
reader.onload = async (e) => {
const ab = e.target.result; // 获取ArrayBuffer
重点是这里的处理(stox是x-spreadsheet的导入函数)
(xlsx.read是读取Excel, 其中cellStyles:true一定要设置,才能将样式保存在单元格的s字段)
返回WordBook对象(具体数据格式可以打印出来查看)
const WordBook = stox(xlsx.read(ab,{type:'array',cellStyles:true}));
grid.value.loadData(WordBook); // 加载数据到当前表格
};
reader.readAsArrayBuffer(file.raw); // 读取文件内容
};
其中stox代码(样式处理分别在1234位置):
export function stox(wb) {
var out = [];
wb.SheetNames.forEach(function (name) {
var o = { name: name, rows: {}, styles: [] };
var ws = wb.Sheets[name];
if(!ws || !ws["!ref"]) return;
var range = XLSX.utils.decode_range(ws['!ref']);
// sheet_to_json will lost empty row and col at begin as default
range.s = { r: 0, c: 0 };
var aoa = XLSX.utils.sheet_to_json(ws, {
raw: false,
header: 1,
range: range
});
aoa.forEach(function (r, i) {
var cells = {};
r.forEach(function (c, j) {
cells[j] = { text: c };
var cellRef = XLSX.utils.encode_cell({ r: i, c: j });
if ( ws[cellRef] != null && ws[cellRef].f != null) {
cells[j].text = "=" + ws[cellRef].f;
}
// 1. 提取excel样式信息
if (ws[cellRef] != null && ws[cellRef].s != null) {
var styleIndex = o.styles.indexOf(ws[cellRef].s)
if(styleIndex === -1){
styleIndex = o.styles.push(ws[cellRef].s) - 1
}
cells[j].style = styleIndex;
}
});
o.rows[i] = { cells: cells };
// 2. 提取excel行高信息
if (ws["!rows"] && ws["!rows"][i]) {
o.rows[i].height = ws["!rows"][i].hpt * 1.25;
}
});
o.merges = [];
(ws["!merges"]||[]).forEach(function (merge, i) {
//Needed to support merged cells with empty content
if (o.rows[merge.s.r] == null) {
o.rows[merge.s.r] = { cells: {} };
}
if (o.rows[merge.s.r].cells[merge.s.c] == null) {
o.rows[merge.s.r].cells[merge.s.c] = {};
}
o.rows[merge.s.r].cells[merge.s.c].merge = [
merge.e.r - merge.s.r,
merge.e.c - merge.s.c
];
o.merges[i] = XLSX.utils.encode_range(merge);
});
// 3. 提取excel列宽信息
o.cols = {}
Object.keys(ws["!cols"] || {}).forEach(function(key){
var col = ws["!cols"][key];
o.cols[key] = {
width: col.wpx * 1.5
}
})
out.push(o);
});
// 4. 在工作表处理完毕后,统一转换x-spreadsheet样式字段
out.forEach(function(o) {
o.styles = o.styles.map(converStyleFields);
});
return out;
}
function converStyleFields(style){
if(style.fgColor){
style.bgcolor = "#" + style.fgColor.rgb;
}
return style;
}
2. 导出
import XLSX from 'xlsx-js-style'; // 重点!带样式导出使用xlsx-js-style,而不是xlsx
function handleExport(){
if(grid.value !== null){
const data = grid.value.getData(); // 获取当前表格的数据
const worksheet:any = xtos(Object.values(data)); // 将数据转换为适当的格式
XLSX.writeFile(worksheet, "SheetJS.xlsx"); // 写入文件
}
}
其中xtos代码(样式处理分别在1234位置):
export function xtos(sdata) {
var out = XLSX.utils.book_new();
sdata.forEach(function (xws) {
var ws = {};
var rowobj = xws.rows;
var cols = xws.cols;//列宽
var styles = xws.styles || [];//样式
var styleMap = {};
// 1. 初始化样式映射(只转换了基础样式,其他复杂样式根据需要添加)
styles.forEach((style, index) => {
let xf = {};
if (style.color) {
xf.font = { color: { rgb: style.color.substring(1) } };
}
if (style.bgcolor) {
xf.fill = { fgColor: { rgb: style.bgcolor.substring(1) } };
}
if (style.bold) {
xf.font.bold = style.bold;
}
if (style.size) {
xf.font.sz = style.sz;
}
if(style.align || style.valign){
xf.alignment = { horizontal: style.align, vertical: style.valign, wrapText: true };
}
styleMap[index] = xf;
});
var minCoord = { r: 0, c: 0 }, maxCoord = { r: 0, c: 0 };
for (var ri = 0; ri < rowobj.len; ++ri) {
var row = rowobj[ri];
if (!row) continue;
Object.keys(row.cells).forEach(function (k) {
var idx = +k;
if (isNaN(idx)) return;
var lastRef = XLSX.utils.encode_cell({ r: ri, c: idx });
if (ri > maxCoord.r) maxCoord.r = ri;
if (idx > maxCoord.c) maxCoord.c = idx;
var cellText = row.cells[k].text, type = "s";
if (!cellText) {
cellText = "";
type = "z";
} else if (!isNaN(Number(cellText))) {
cellText = Number(cellText);
type = "n";
} else if (cellText.toLowerCase() === "true" || cellText.toLowerCase() === "false") {
cellText = Boolean(cellText);
type = "b";
}
ws[lastRef] = { v: cellText, t: type };
if (type == "s" && cellText[0] == "=") {
ws[lastRef].f = cellText.slice(1);
}
// 2. 添加样式
var styleIndex = row.cells[k].style;
if (styleIndex in styleMap) {
ws[lastRef].s = styleMap[styleIndex];
}
// 3. 设置行高
if (row.height != null) {
if (ws["!rows"] == null) ws["!rows"] = [];
ws["!rows"][ri] = { hpx: row.height };
}
if (row.cells[k].merge != null) {
if (ws["!merges"] == null) ws["!merges"] = [];
ws["!merges"].push({
s: { r: ri, c: idx },
e: {
r: ri + row.cells[k].merge[0],
c: idx + row.cells[k].merge[1]
}
});
}
});
}
ws["!ref"] = minCoord ? XLSX.utils.encode_range({
s: minCoord,
e: maxCoord
}) : "A1";
// 4. 设置列宽
let columns = []
// 遍历 cols 对象的每一个键值对
Object.keys(cols).forEach(key => {
const colConfig = cols[key];
if (key !== 'len' && 'width' in colConfig) {
columns[key] = { wpx: colConfig.width };
}
});
// 将列配置数组赋值给 ws['!cols']
ws['!cols'] = columns;
XLSX.utils.book_append_sheet(out, ws, xws.name);
});
return out;
}
总结
仅分享,如果有更好的方法,欢迎一起讨论和分享。
另外,在使用Vue 3集成x-spreadsheet时遇到了切换sheet表就报错的问题,欢迎共同探讨解决方案。