From f25d23d4df23d1d1c8febdf68cadf5adaca0b6af Mon Sep 17 00:00:00 2001 From: wisonic-s Date: Tue, 2 Jul 2024 15:15:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=9B=BE=E8=A1=A8):=20=E9=80=8F=E8=A7=86?= =?UTF-8?q?=E8=A1=A8=E6=94=AF=E6=8C=81=E5=AF=BC=E5=87=BA=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E8=A1=A8=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/core-frontend/package.json | 1 + .../visualization/ComponentEditBar.vue | 20 ++- .../visualization/UserViewEnlarge.vue | 21 +++ .../modules/data-visualization/dvMain.ts | 8 + .../js/panel/common/common_table.ts | 168 +++++++++++++++++- .../views/components/ChartComponentS2.vue | 1 + 6 files changed, 217 insertions(+), 2 deletions(-) diff --git a/core/core-frontend/package.json b/core/core-frontend/package.json index aa153cf001..2056df8307 100644 --- a/core/core-frontend/package.json +++ b/core/core-frontend/package.json @@ -61,6 +61,7 @@ "vue3-ace-editor": "^2.2.2", "vuedraggable": "^4.1.0", "web-storage-cache": "^1.1.1", + "exceljs": "^4.4.0", "xss": "^1.0.14" }, "devDependencies": { diff --git a/core/core-frontend/src/components/visualization/ComponentEditBar.vue b/core/core-frontend/src/components/visualization/ComponentEditBar.vue index 0b35d6dbff..9302dcc6c1 100644 --- a/core/core-frontend/src/components/visualization/ComponentEditBar.vue +++ b/core/core-frontend/src/components/visualization/ComponentEditBar.vue @@ -115,6 +115,11 @@ @@ -140,6 +145,11 @@ @@ -169,6 +179,7 @@ import FieldsList from '@/custom-component/rich-text/FieldsList.vue' import { RefreshLeft } from '@element-plus/icons-vue' import { ElMessage, ElTooltip, ElButton } from 'element-plus-secondary' import CustomTabsSort from '@/custom-component/de-tabs/CustomTabsSort.vue' +import { exportPivotExcel } from '@/views/chart/components/js/panel/common/common_table' const dvMainStore = dvMainStoreWithOut() const snapshotStore = snapshotStoreWithOut() const copyStore = copyStoreWithOut() @@ -367,7 +378,14 @@ const openMessageLoading = cb => { const callbackExport = () => { useEmitt().emitter.emit('data-export-center', { activeName: 'IN_PROGRESS' }) } - +const exportAsFormattedExcel = () => { + const s2Instance = dvMainStore.getViewInstanceInfo(element.value.id) + if (!s2Instance) { + return + } + const chart = dvMainStore.getViewDetails(element.value.id) + exportPivotExcel(s2Instance, chart) +} const exportAsExcel = () => { const viewDataInfo = dvMainStore.getViewDataDetails(element.value.id) const chartExtRequest = dvMainStore.getLastViewRequestInfo(element.value.id) diff --git a/core/core-frontend/src/components/visualization/UserViewEnlarge.vue b/core/core-frontend/src/components/visualization/UserViewEnlarge.vue index c3320099f6..dca999451f 100644 --- a/core/core-frontend/src/components/visualization/UserViewEnlarge.vue +++ b/core/core-frontend/src/components/visualization/UserViewEnlarge.vue @@ -45,6 +45,17 @@ > 导出Excel + + 导出Excel(带格式) +
{ exportLoading.value = false } +const exportAsFormattedExcel = () => { + const s2Instance = dvMainStore.getViewInstanceInfo(viewInfo.value.id) + if (!s2Instance) { + return + } + const chart = dvMainStore.getViewDetails(viewInfo.value.id) + exportPivotExcel(s2Instance, chart) +} + const exportData = () => { useEmitt().emitter.emit('data-export-center', { activeName: 'IN_PROGRESS' }) } diff --git a/core/core-frontend/src/store/modules/data-visualization/dvMain.ts b/core/core-frontend/src/store/modules/data-visualization/dvMain.ts index a994231fee..e2c48289c9 100644 --- a/core/core-frontend/src/store/modules/data-visualization/dvMain.ts +++ b/core/core-frontend/src/store/modules/data-visualization/dvMain.ts @@ -78,6 +78,8 @@ export const dvMainStore = defineStore('dataVisualization', { canvasViewInfo: {}, // 图表展示数据信息 canvasViewDataInfo: {}, + // 图表实例信息 + canvasViewInstanceInfo: {}, // 图表最新请求信息 lastViewRequestInfo: {}, // 仪表板基础矩阵信息 @@ -1149,6 +1151,12 @@ export const dvMainStore = defineStore('dataVisualization', { getViewDataDetails(viewId) { return this.canvasViewDataInfo[viewId] }, + setViewInstanceInfo(viewId, instance) { + this.canvasViewInstanceInfo[viewId] = instance + }, + getViewInstanceInfo(viewId) { + return this.canvasViewInstanceInfo[viewId] + }, setLastViewRequestInfo(viewId, viewRequestInfo) { this.lastViewRequestInfo[viewId] = viewRequestInfo }, diff --git a/core/core-frontend/src/views/chart/components/js/panel/common/common_table.ts b/core/core-frontend/src/views/chart/components/js/panel/common/common_table.ts index f2abee3199..73f692adb7 100644 --- a/core/core-frontend/src/views/chart/components/js/panel/common/common_table.ts +++ b/core/core-frontend/src/views/chart/components/js/panel/common/common_table.ts @@ -12,11 +12,17 @@ import { setTooltipContainerStyle, Style, S2Options, - SERIES_NUMBER_FIELD + SERIES_NUMBER_FIELD, + type PivotSheet, + type Node, + type Meta } from '@antv/s2' import { keys, intersection, filter, cloneDeep, merge, find } from 'lodash-es' import { createVNode, render } from 'vue' import TableTooltip from '@/views/chart/components/editor/common/TableTooltip.vue' +import Exceljs from 'exceljs' +import { saveAs } from 'file-saver' +import { ElMessage } from 'element-plus-secondary' export function getCustomTheme(chart: Chart): S2Theme { const headerColor = hexColorToRGBA( @@ -835,3 +841,163 @@ function getTooltipPosition(event) { } return result } + +export async function exportPivotExcel(instancce: PivotSheet, chart: ChartObj) { + const { meta, fields } = instancce.dataCfg + const rowLength = fields?.rows?.length || 0 + const colLength = fields?.columns?.length || 0 + const valueLength = fields?.values?.length || 0 + if (!(rowLength && valueLength)) { + ElMessage.warning('行维度或指标维度为空不可导出!') + return + } + const workbook = new Exceljs.Workbook() + const worksheet = workbook.addWorksheet(chart.title) + const metaMap: Record = meta?.reduce((p, n) => { + if (n.field) { + p[n.field] = n + } + return p + }, {}) + // 角头 + fields.columns?.forEach((column, index) => { + const cell = worksheet.getCell(index + 1, 1) + cell.value = metaMap[column]?.name ?? column + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (rowLength >= 2) { + worksheet.mergeCells(index + 1, 1, index + 1, rowLength) + } + }) + fields?.rows?.forEach((row, index) => { + const cell = worksheet.getCell(colLength + 1, index + 1) + cell.value = metaMap[row]?.name ?? row + cell.alignment = { vertical: 'middle', horizontal: 'center' } + }) + const { layoutResult } = instancce.facet + // 行头 + const { rowLeafNodes, rowsHierarchy, rowNodes } = layoutResult + const maxColIndex = rowsHierarchy.maxLevel + 1 + const notLeafNodeHeightMap: Record = {} + rowLeafNodes.forEach(node => { + // 行头的高度由子节点相加决定,也就是行头子节点中包含的叶子节点数量 + let curNode = node.parent + while (curNode) { + const height = notLeafNodeHeightMap[curNode.id] ?? 0 + notLeafNodeHeightMap[curNode.id] = height + 1 + curNode = curNode.parent + } + const { rowIndex } = node + const writeRowIndex = rowIndex + 1 + colLength + 1 + const writeColIndex = node.level + 1 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + cell.value = node.label + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (writeColIndex < maxColIndex) { + worksheet.mergeCells(writeRowIndex, writeColIndex, writeRowIndex, maxColIndex) + } + }) + + const getNodeStartRowIndex = (node: Node) => { + if (!node.children?.length) { + return node.rowIndex + 1 + } else { + return getNodeStartRowIndex(node.children[0]) + } + } + rowNodes?.forEach(node => { + if (node.isLeaf) { + return + } + const rowIndex = getNodeStartRowIndex(node) + const height = notLeafNodeHeightMap[node.id] + const writeRowIndex = rowIndex + colLength + 1 + const mergeColCount = node.children[0].level - node.level + const value = node.label + const cell = worksheet.getCell(writeRowIndex, node.level + 1) + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (mergeColCount > 1 || height > 1) { + worksheet.mergeCells( + writeRowIndex, + node.level + 1, + writeRowIndex + height - 1, + node.level + mergeColCount + ) + } + }) + + // 列头 + const { colLeafNodes, colNodes, colsHierarchy } = layoutResult + const maxColHeight = colsHierarchy.maxLevel + 1 + const notLeafNodeWidthMap: Record = {} + colLeafNodes.forEach(node => { + // 列头的宽度由子节点相加决定,也就是列头子节点中包含的叶子节点数量 + let curNode = node.parent + while (curNode) { + const width = notLeafNodeWidthMap[curNode.id] ?? 0 + notLeafNodeWidthMap[curNode.id] = width + 1 + curNode = curNode.parent + } + const { colIndex } = node + const writeRowIndex = node.level + 1 + const writeColIndex = colIndex + 1 + rowLength + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + let value = node.label + if (node.field === '$$extra$$' && metaMap[value]?.name) { + value = metaMap[value].name + } + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (writeRowIndex < maxColHeight) { + worksheet.mergeCells(writeRowIndex, writeColIndex, maxColHeight, writeColIndex) + } + }) + const getNodeStartColIndex = (node: Node) => { + if (!node.children?.length) { + return node.colIndex + 1 + } else { + return getNodeStartColIndex(node.children[0]) + } + } + colNodes.forEach(node => { + if (node.isLeaf) { + return + } + const colIndex = getNodeStartColIndex(node) + const width = notLeafNodeWidthMap[node.id] + const writeRowIndex = node.level + 1 + const mergeRowCount = node.children[0].level - node.level + const value = node.label + const writeColIndex = colIndex + rowLength + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (mergeRowCount > 1 || width > 1) { + worksheet.mergeCells( + writeRowIndex, + writeColIndex, + writeRowIndex + mergeRowCount - 1, + writeColIndex + width - 1 + ) + } + }) + // 单元格数据 + for (let rowIndex = 0; rowIndex < rowLeafNodes.length; rowIndex++) { + for (let colIndex = 0; colIndex < colLeafNodes.length; colIndex++) { + const dataCellMeta = layoutResult.getCellMeta(rowIndex, colIndex) + const { fieldValue } = dataCellMeta + if (fieldValue) { + const meta = metaMap[dataCellMeta.valueField] + const cell = worksheet.getCell(rowIndex + maxColHeight + 1, rowLength + colIndex + 1) + const value = meta?.formatter?.(fieldValue) || fieldValue.toString() + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.value = value + } + } + } + const buffer = await workbook.xlsx.writeBuffer() + const dataBlob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8' + }) + saveAs(dataBlob, `${chart.title ?? '透视表'}.xlsx`) +} diff --git a/core/core-frontend/src/views/chart/components/views/components/ChartComponentS2.vue b/core/core-frontend/src/views/chart/components/views/components/ChartComponentS2.vue index 680f00bb97..234193682c 100644 --- a/core/core-frontend/src/views/chart/components/views/components/ChartComponentS2.vue +++ b/core/core-frontend/src/views/chart/components/views/components/ChartComponentS2.vue @@ -178,6 +178,7 @@ const renderChart = (viewInfo: Chart, resetPageInfo: boolean) => { resizeAction }) myChart?.render() + dvMainStore.setViewInstanceInfo(viewInfo.id, myChart) initScroll() }