|
|
@@ -0,0 +1,276 @@
|
|
|
+<template>
|
|
|
+ <div class="table-has-download">
|
|
|
+ <!-- <a-table :columns="columns" :data="tableData" bordered /> -->
|
|
|
+ <!-- <table></table> -->
|
|
|
+ <div v-html="table_html"></div>
|
|
|
+ <a-tooltip content="导出">
|
|
|
+ <icon-arrow-down class="download-icon" @click="toDownLoad" />
|
|
|
+ </a-tooltip>
|
|
|
+ <!-- <a-button @click="toDownLoad" style="margin-top: 16px">下载(列宽自适应)</a-button> -->
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { reactive, ref } from 'vue';
|
|
|
+import marked from '../../3rd-libs/marked';
|
|
|
+// import * as XLSX from 'xlsx';
|
|
|
+
|
|
|
+// 确保所有叶子列都有 dataIndex(修复核心)
|
|
|
+const columns = [
|
|
|
+ {
|
|
|
+ title: 'Name',
|
|
|
+ dataIndex: 'name'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'Salary',
|
|
|
+ dataIndex: 'salary'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'Address',
|
|
|
+ dataIndex: 'address'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'Email',
|
|
|
+ dataIndex: 'email'
|
|
|
+ }
|
|
|
+];
|
|
|
+const tableData = reactive([
|
|
|
+ {
|
|
|
+ key: '1',
|
|
|
+ name: 'Jane Doe',
|
|
|
+ salary: 23000,
|
|
|
+ address: '32 Park Road, London',
|
|
|
+ email: 'jane.doe@example.com'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '2',
|
|
|
+ name: 'Alisa Ross',
|
|
|
+ salary: 25000,
|
|
|
+ address: '35 Park Road, London',
|
|
|
+ email: 'alisa.ross@example.com'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '3',
|
|
|
+ name: 'Kevin Sandra',
|
|
|
+ salary: 22000,
|
|
|
+ address: '31 Park Road, London',
|
|
|
+ email: 'kevin.sandra@example.com'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '4',
|
|
|
+ name: 'Ed Hellen',
|
|
|
+ salary: 17000,
|
|
|
+ address: '42 Park Road, London',
|
|
|
+ email: 'ed.hellen@example.com'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '5',
|
|
|
+ name: 'William Smith',
|
|
|
+ salary: 27000,
|
|
|
+ address: '62 Park Road, London',
|
|
|
+ email: 'william.smith@example.com'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '5',
|
|
|
+ name: 'William Smith',
|
|
|
+ salary: 27000,
|
|
|
+ address: '62 Park Road, London',
|
|
|
+ email: 'william.smith@example.com'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '5',
|
|
|
+ name: 'William Smith',
|
|
|
+ salary: 27000,
|
|
|
+ address: '62 Park Road, London',
|
|
|
+ email: 'william.smith@example.com'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '5',
|
|
|
+ name: 'William Smith',
|
|
|
+ salary: 27000,
|
|
|
+ address: '62 Park Road, London',
|
|
|
+ email: 'william.smith@example.com'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '5',
|
|
|
+ name: 'William Smith',
|
|
|
+ salary: 27000,
|
|
|
+ address: '62 Park Road, London',
|
|
|
+ email: 'william.smith@example.com'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: '5',
|
|
|
+ name: 'William Smith',
|
|
|
+ salary: 27000,
|
|
|
+ address: '62 Park Road, London',
|
|
|
+ email: 'william.smith@example.com'
|
|
|
+ }
|
|
|
+]);
|
|
|
+
|
|
|
+const md_table = ref(`| 学科 | 满分 | 及格线 | 难度系数 |
|
|
|
+| :----- | ---: | :----: | :------- |
|
|
|
+| 数学 | 150 | 90 | 高 |
|
|
|
+| 语文 | 150 | 90 | 中 |
|
|
|
+| 英语 | 120 | 72 | 中 |
|
|
|
+| 物理 | 100 | 60 | 高 |`);
|
|
|
+
|
|
|
+const table_html = marked.parse(md_table.value);
|
|
|
+const toDownLoad = () => {
|
|
|
+ const { headerMatrix, headerMerges, maxLevel, flatCols } = parseMultiLevelHeaders(columns);
|
|
|
+ const { dataMatrix } = parseData(tableData, flatCols);
|
|
|
+ const dataMerges = parseDataMerges(tableData, maxLevel);
|
|
|
+
|
|
|
+ const allData = [...headerMatrix, ...dataMatrix];
|
|
|
+ const worksheet = XLSX.utils.aoa_to_sheet(allData);
|
|
|
+ worksheet['!merges'] = [...headerMerges, ...dataMerges];
|
|
|
+
|
|
|
+ adjustColumnWidths(worksheet, allData);
|
|
|
+
|
|
|
+ const workbook = XLSX.utils.book_new();
|
|
|
+ XLSX.utils.book_append_sheet(workbook, worksheet, '表格数据');
|
|
|
+ const fileName = `列宽自适应表格_${new Date().toLocaleDateString().replace(/\//g, '-')}.xlsx`;
|
|
|
+ XLSX.writeFile(workbook, fileName);
|
|
|
+};
|
|
|
+
|
|
|
+// 自动调整列宽
|
|
|
+function adjustColumnWidths(worksheet, allData) {
|
|
|
+ if (!allData.length) return;
|
|
|
+
|
|
|
+ const colCount = allData[0].length;
|
|
|
+ const maxLengths = new Array(colCount).fill(0);
|
|
|
+
|
|
|
+ allData.forEach((row) => {
|
|
|
+ row.forEach((cell, colIndex) => {
|
|
|
+ if (cell === undefined || cell === null) cell = '';
|
|
|
+ const cellStr = String(cell);
|
|
|
+ const length = cellStr.split('').reduce((len, char) => {
|
|
|
+ return len + (char.charCodeAt(0) > 127 ? 2 : 1);
|
|
|
+ }, 0);
|
|
|
+ if (length > maxLengths[colIndex]) {
|
|
|
+ maxLengths[colIndex] = length;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ worksheet['!cols'] = maxLengths.map((length) => ({
|
|
|
+ wch: Math.max(length + 2, 8)
|
|
|
+ }));
|
|
|
+}
|
|
|
+
|
|
|
+// 解析多级表头
|
|
|
+function parseMultiLevelHeaders(columns) {
|
|
|
+ const flatCols = [];
|
|
|
+ let maxLevel = 1;
|
|
|
+
|
|
|
+ function traverse(cols, currentLevel, parentPath = []) {
|
|
|
+ cols.forEach((col) => {
|
|
|
+ const path = [...parentPath, col];
|
|
|
+ if (col.children && col.children.length > 0) {
|
|
|
+ const nextLevel = currentLevel + 1;
|
|
|
+ if (nextLevel > maxLevel) maxLevel = nextLevel;
|
|
|
+ traverse(col.children, nextLevel, path);
|
|
|
+ } else {
|
|
|
+ // 过滤掉没有 dataIndex 的列(避免后续报错)
|
|
|
+ if (col.dataIndex) {
|
|
|
+ flatCols.push({ path, depth: path.length, dataIndex: col.dataIndex });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ traverse(columns, 1);
|
|
|
+
|
|
|
+ const headerMatrix = Array.from({ length: maxLevel }, () => []);
|
|
|
+ flatCols.forEach(({ path }) => {
|
|
|
+ path.forEach((col, levelIndex) => {
|
|
|
+ headerMatrix[levelIndex].push(col.title || '');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ const headerMerges = [];
|
|
|
+ for (let level = 0; level < maxLevel; level++) {
|
|
|
+ let currentCol = 0;
|
|
|
+ while (currentCol < flatCols.length) {
|
|
|
+ const currentPath = flatCols[currentCol].path;
|
|
|
+ const currentTitle = currentPath[level]?.title;
|
|
|
+ if (!currentTitle) {
|
|
|
+ currentCol++;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ let mergeCount = 1;
|
|
|
+ while (currentCol + mergeCount < flatCols.length) {
|
|
|
+ const nextPath = flatCols[currentCol + mergeCount].path;
|
|
|
+ const nextTitle = nextPath[level]?.title;
|
|
|
+ if (nextTitle === currentTitle) mergeCount++;
|
|
|
+ else break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (mergeCount > 1) {
|
|
|
+ headerMerges.push({
|
|
|
+ s: { r: level, c: currentCol },
|
|
|
+ e: { r: level, c: currentCol + mergeCount - 1 }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ currentCol += mergeCount;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { headerMatrix, headerMerges, maxLevel, flatCols };
|
|
|
+}
|
|
|
+
|
|
|
+// 处理数据行
|
|
|
+function parseData(tableData, flatCols) {
|
|
|
+ const dataMatrix = tableData.map((row) => {
|
|
|
+ return flatCols.map((col) => {
|
|
|
+ // 传入明确的 dataIndex(来自 flatCols 过滤后的数据)
|
|
|
+ return getValueByDataIndex(row, col.dataIndex);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ return { dataMatrix };
|
|
|
+}
|
|
|
+
|
|
|
+// 处理数据合并
|
|
|
+function parseDataMerges(tableData, headerRowCount) {
|
|
|
+ const merges = [];
|
|
|
+ tableData.forEach((row, rowIndex) => {
|
|
|
+ const excelRow = headerRowCount + rowIndex;
|
|
|
+ if (!row.merges) return;
|
|
|
+
|
|
|
+ row.merges.forEach((merge) => {
|
|
|
+ merges.push({
|
|
|
+ s: { r: excelRow, c: merge.col },
|
|
|
+ e: {
|
|
|
+ r: excelRow + (merge.rowspan || 1) - 1,
|
|
|
+ c: merge.col + (merge.colspan || 1) - 1
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+ return merges;
|
|
|
+}
|
|
|
+
|
|
|
+// 修复:增加 dataIndex 校验,避免 undefined
|
|
|
+const getValueByDataIndex = (row, dataIndex) => {
|
|
|
+ // 如果 dataIndex 不存在,直接返回空
|
|
|
+ if (!dataIndex) return '';
|
|
|
+ return dataIndex.split('.').reduce((obj, key) => {
|
|
|
+ return obj ? obj[key] : '';
|
|
|
+ }, row);
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.table-has-download {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+.table-has-download {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+.download-icon {
|
|
|
+ position: relative;
|
|
|
+ top: -28px;
|
|
|
+ left: 10px;
|
|
|
+ font-size: 24px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+</style>
|