diff --git a/core/frontend/package.json b/core/frontend/package.json
index f7a77248e3..ccca20d742 100644
--- a/core/frontend/package.json
+++ b/core/frontend/package.json
@@ -45,6 +45,7 @@
"echarts": "^5.0.1",
"element-resize-detector": "^1.2.3",
"element-ui": "2.15.7",
+ "exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"fit2cloud-ui": "^1.8.0",
"flv.js": "^1.6.2",
diff --git a/core/frontend/src/components/canvas/components/editor/EditBar.vue b/core/frontend/src/components/canvas/components/editor/EditBar.vue
index 53d50ac9b0..c8285d0485 100644
--- a/core/frontend/src/components/canvas/components/editor/EditBar.vue
+++ b/core/frontend/src/components/canvas/components/editor/EditBar.vue
@@ -89,6 +89,16 @@
@click.stop="exportExcelDownload()"
/>
+
+
+
{
- cb()
- }
- }
+ Button,
+ {
+ props: {
+ type: 'text'
},
- this.$t('data_export.export_center')
- ),
+ class: 'btn-text',
+ on: {
+ click: () => {
+ cb()
+ }
+ }
+ },
+ this.$t('data_export.export_center')
+ ),
this.$t('data_export.export_info')
]),
iconClass,
@@ -522,7 +540,7 @@ export default {
Button,
{
props: {
- type: 'text',
+ type: 'text'
},
class: 'btn-text',
on: {
@@ -542,6 +560,13 @@ export default {
exportExcelDownload() {
exportExcelDownload(this.chart, null, null, null, null, null, this.exportDataCb)
},
+ exportFormattedExcel() {
+ const instance = this.$store.state.chart.tableInstance[this.chart.id]
+ if (!instance) {
+ return
+ }
+ exportPivotExcel(instance, this.chart)
+ },
auxiliaryMatrixChange() {
if (this.curComponent.auxiliaryMatrix) {
this.curComponent.auxiliaryMatrix = false
diff --git a/core/frontend/src/icons/svg/ds-excel-format.svg b/core/frontend/src/icons/svg/ds-excel-format.svg
new file mode 100644
index 0000000000..387f4d7708
--- /dev/null
+++ b/core/frontend/src/icons/svg/ds-excel-format.svg
@@ -0,0 +1 @@
+
diff --git a/core/frontend/src/lang/en.js b/core/frontend/src/lang/en.js
index baa7c3ac88..296bf0f629 100644
--- a/core/frontend/src/lang/en.js
+++ b/core/frontend/src/lang/en.js
@@ -1859,7 +1859,9 @@ export default {
polynomial_regression: 'Polynomial regression',
show_summary: 'Show summary',
summary_label: 'Summary label',
- tip: 'Tip'
+ tip: 'Tip',
+ pivot_export_empty_fields: 'Can not export without row dimension or quota',
+ export_formatted_excel: 'Export formatted excel'
},
dataset: {
scope_edit: 'Effective only when editing',
diff --git a/core/frontend/src/lang/tw.js b/core/frontend/src/lang/tw.js
index 2914c48904..f05b7ff68e 100644
--- a/core/frontend/src/lang/tw.js
+++ b/core/frontend/src/lang/tw.js
@@ -1852,7 +1852,9 @@ export default {
polynomial_regression: '多項式擬合',
show_summary: '顯示總計',
summary_label: '總計標籤',
- tip: '提示'
+ tip: '提示',
+ pivot_export_empty_fields: '行維度或指標維度為空不可導出',
+ export_formatted_excel: '導出 Excel (帶格式)'
},
dataset: {
scope_edit: '僅編輯時生效',
diff --git a/core/frontend/src/lang/zh.js b/core/frontend/src/lang/zh.js
index 7a3ac36706..e4fd1b5b75 100644
--- a/core/frontend/src/lang/zh.js
+++ b/core/frontend/src/lang/zh.js
@@ -1849,7 +1849,9 @@ export default {
polynomial_regression: '多项式拟合',
show_summary: '显示总计',
summary_label: '总计标签',
- tip: '提示'
+ tip: '提示',
+ pivot_export_empty_fields: '行维度或指标维度为空不可导出',
+ export_formatted_excel: '导出 Excel (带格式)'
},
dataset: {
goto: ', 前往 ',
diff --git a/core/frontend/src/store/modules/chart.js b/core/frontend/src/store/modules/chart.js
index b68f06f8f0..e3aac1428c 100644
--- a/core/frontend/src/store/modules/chart.js
+++ b/core/frontend/src/store/modules/chart.js
@@ -5,7 +5,8 @@ const getDefaultState = () => {
sceneId: {},
viewId: null,
tableId: {},
- chartSceneData: {}
+ chartSceneData: {},
+ tableInstance: {}
}
}
@@ -29,6 +30,9 @@ const mutations = {
},
setChartSceneData: (state, chartSceneData) => {
state.chartSceneData = chartSceneData
+ },
+ setTableInstance: (state, { viewId, tableInstance }) => {
+ state.tableInstance[viewId] = tableInstance
}
}
@@ -50,6 +54,9 @@ const actions = {
},
setChartSceneData: ({ commit }, chartSceneData) => {
commit('setChartSceneData', chartSceneData)
+ },
+ setTableInstance: ({ commit }, { viewId, tableInstance }) => {
+ commit('setTableInstance', { viewId, tableInstance })
}
}
diff --git a/core/frontend/src/views/chart/chart/common/common_table.js b/core/frontend/src/views/chart/chart/common/common_table.js
index 88f4c03cb0..244cdb1aed 100644
--- a/core/frontend/src/views/chart/chart/common/common_table.js
+++ b/core/frontend/src/views/chart/chart/common/common_table.js
@@ -1,5 +1,9 @@
import { hexColorToRGBA, resetRgbOpacity } from '@/views/chart/chart/util'
import { DEFAULT_COLOR_CASE, DEFAULT_SIZE } from '@/views/chart/chart/chart'
+import Exceljs from 'exceljs'
+import { saveAs } from 'file-saver'
+import i18n from '@/lang'
+import {Message} from "element-ui";
export function getCustomTheme(chart) {
const headerColor = hexColorToRGBA(DEFAULT_COLOR_CASE.tableHeaderBgColor, DEFAULT_COLOR_CASE.alpha)
@@ -292,3 +296,168 @@ export function getSize(chart) {
return size
}
+
+export async function exportPivotExcel(instance, chart) {
+ const { meta, fields } = instance.dataCfg
+ const rowLength = fields?.rows?.length || 0
+ const colLength = fields?.columns?.length || 0
+ const valueLength = fields?.values?.length || 0
+ if (!(rowLength && valueLength)) {
+ Message.warning({
+ message: i18n.t('chart.pivot_export_empty_fields'),
+ type: 'warning',
+ showClose: true,
+ duration: 5000
+ })
+ return
+ }
+ const workbook = new Exceljs.Workbook()
+ const worksheet = workbook.addWorksheet(chart.title)
+ const metaMap = 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 } = instance.facet
+ // 行头
+ const { rowLeafNodes, rowsHierarchy, rowNodes } = layoutResult
+ const maxColIndex = rowsHierarchy.maxLevel + 1
+ const notLeafNodeHeightMap = {}
+ 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) => {
+ 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 = {}
+ 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) => {
+ 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/frontend/src/views/chart/components/ChartComponentS2.vue b/core/frontend/src/views/chart/components/ChartComponentS2.vue
index f872da4437..8f5eed6b92 100644
--- a/core/frontend/src/views/chart/components/ChartComponentS2.vue
+++ b/core/frontend/src/views/chart/components/ChartComponentS2.vue
@@ -308,6 +308,7 @@ export default {
this.myChart = baseTableNormal(this.chartId, chart, this.antVAction, this.tableData, this, this.columnResize)
} else if (chart.type === 'table-pivot') {
this.myChart = baseTablePivot(this.chartId, chart, this.antVAction, this.tableHeaderClick, this.tableData)
+ this.$store.dispatch('chart/setTableInstance', { viewId: this.chart.id, tableInstance: this.myChart })
}
if (this.myChart && this.searchCount > 0) {