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 @@
Excel
+ Excel(带格式)
图片
@@ -140,6 +145,11 @@
Excel
+ Excel(带格式)
图片
@@ -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()
}