feat(视图): 透视表平铺模式支持导出格式化内容

This commit is contained in:
wisonic-s 2024-09-18 15:12:58 +08:00
parent 55e2357bbb
commit 48a5669606
9 changed files with 228 additions and 18 deletions

View File

@ -45,6 +45,7 @@
"echarts": "^5.0.1", "echarts": "^5.0.1",
"element-resize-detector": "^1.2.3", "element-resize-detector": "^1.2.3",
"element-ui": "2.15.7", "element-ui": "2.15.7",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"fit2cloud-ui": "^1.8.0", "fit2cloud-ui": "^1.8.0",
"flv.js": "^1.6.2", "flv.js": "^1.6.2",

View File

@ -89,6 +89,16 @@
@click.stop="exportExcelDownload()" @click.stop="exportExcelDownload()"
/> />
</span> </span>
<span
v-if="exportFormattedExcelShow"
:title="$t('chart.export_formatted_excel')"
@click.stop="exportFormattedExcel()"
>
<svg-icon
style="font-size: 14px; color: white; margin-right: 3px"
icon-class="ds-excel-format"
/>
</span>
<setting-menu <setting-menu
v-if="activeModel==='edit'" v-if="activeModel==='edit'"
style="float: right;height: 24px!important;" style="float: right;height: 24px!important;"
@ -207,6 +217,7 @@ import eventBus from '@/components/canvas/utils/eventBus'
import { hasDataPermission } from '@/utils/permission' import { hasDataPermission } from '@/utils/permission'
import { exportExcelDownload } from '@/components/canvas/utils/utils' import { exportExcelDownload } from '@/components/canvas/utils/utils'
import { Button } from 'element-ui' import { Button } from 'element-ui'
import { exportPivotExcel } from '@/views/chart/chart/common/common_table'
export default { export default {
components: { Background, LinkJumpSet, FieldsList, SettingMenu, LinkageField, MapLayerController }, components: { Background, LinkJumpSet, FieldsList, SettingMenu, LinkageField, MapLayerController },
@ -298,6 +309,13 @@ export default {
exportExcelShow() { exportExcelShow() {
return this.detailsShow && hasDataPermission('export', this.$store.state.panel.panelInfo.privileges) && this.chart && this.chart.dataFrom !== 'template' return this.detailsShow && hasDataPermission('export', this.$store.state.panel.panelInfo.privileges) && this.chart && this.chart.dataFrom !== 'template'
}, },
exportFormattedExcelShow() {
return this.detailsShow &&
hasDataPermission('export', this.$store.state.panel.panelInfo.privileges) &&
this.chart &&
this.chart.dataFrom !== 'template' &&
JSON.parse(this.chart.customAttr).size?.tableLayoutMode !== 'tree'
},
enlargeShow() { enlargeShow() {
return this.curComponent.type === 'view' && this.curComponent.propValue.innerType && this.curComponent.propValue.innerType !== 'richTextView' && !this.curComponent.propValue.innerType.includes('table') return this.curComponent.type === 'view' && this.curComponent.propValue.innerType && this.curComponent.propValue.innerType !== 'richTextView' && !this.curComponent.propValue.innerType.includes('table')
}, },
@ -490,20 +508,20 @@ export default {
message: h('p', null, [ message: h('p', null, [
this.$t('data_export.exporting'), this.$t('data_export.exporting'),
h( h(
Button, Button,
{ {
props: { props: {
type: 'text', type: 'text'
},
class: 'btn-text',
on: {
click: () => {
cb()
}
}
}, },
this.$t('data_export.export_center') class: 'btn-text',
), on: {
click: () => {
cb()
}
}
},
this.$t('data_export.export_center')
),
this.$t('data_export.export_info') this.$t('data_export.export_info')
]), ]),
iconClass, iconClass,
@ -522,7 +540,7 @@ export default {
Button, Button,
{ {
props: { props: {
type: 'text', type: 'text'
}, },
class: 'btn-text', class: 'btn-text',
on: { on: {
@ -542,6 +560,13 @@ export default {
exportExcelDownload() { exportExcelDownload() {
exportExcelDownload(this.chart, null, null, null, null, null, this.exportDataCb) 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() { auxiliaryMatrixChange() {
if (this.curComponent.auxiliaryMatrix) { if (this.curComponent.auxiliaryMatrix) {
this.curComponent.auxiliaryMatrix = false this.curComponent.auxiliaryMatrix = false

View File

@ -0,0 +1 @@
<svg t="1726630203120" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9498" width="32" height="32"><path d="M901.347556 805.546667c-16.839111 0-30.72 12.970667-32.085334 29.354666v118.328889H106.268444v-119.921778c0.227556-1.592889 0.455111-3.185778 0.455112-5.006222V468.764444c0-1.592889-0.227556-3.413333-0.455112-5.006222V70.314667h552.049778v171.804444s3.185778 30.037333 31.857778 30.037333H869.262222v172.714667c1.820444 16.156444 15.473778 28.899556 32.085334 28.899556 14.791111 0 27.306667-10.012444 31.175111-23.438223V254.862222l-1.137778-1.137778c-1.137778-5.233778-3.640889-14.336-8.192-17.976888l-225.28-227.100445s-4.096-4.551111-16.839111-6.826667c-0.910222-0.682667-2.048-1.365333-2.958222-1.820444H95.573333C66.218667 0 42.552889 23.665778 42.552889 53.020444v463.985778c0.227556 0.455111 0.227556 1.137778 0.455111 1.592889V829.895111c-0.227556 0.682667-0.455111 1.137778-0.455111 1.820445v139.264c0 29.127111 23.665778 53.020444 53.020444 53.020444h783.928889c29.354667 0 53.020444-23.665778 53.020445-53.020444v-141.994667c-3.868444-13.425778-16.384-23.438222-31.175111-23.438222zM836.494222 218.680889l-130.389333 0.910222-0.910222-132.664889 131.299555 131.754667z" p-id="9499"></path><path d="M963.015111 611.214222l-95.573333-114.460444c-20.935111-14.336-34.133333 2.958222-34.133334 2.958222-13.880889 16.611556-0.682667 41.187556-0.682666 41.187556l56.206222 67.356444H604.842667c-16.611556 0-30.037333 16.156444-30.037334 35.953778 0 19.797333 13.425778 35.953778 30.037334 35.953778h282.851555l-58.254222 70.314666c-12.743111 27.989333 5.688889 43.235556 5.688889 43.235556 13.198222 16.611556 33.450667-1.365333 33.450667-1.365334l12.060444-14.563555 8.192-9.102222 83.057778-100.124445c25.713778-31.175111-8.874667-57.344-8.874667-57.344zM178.858667 276.935111c-16.611556 0-29.809778 14.336-29.809778 31.857778s13.425778 31.857778 29.809778 31.857778h370.460444c16.611556 0 29.809778-14.336 29.809778-31.857778s-13.425778-31.857778-29.809778-31.857778H178.858667zM780.743111 435.313778c0-18.887111-19.569778-34.133333-43.463111-34.133334H198.428444c-24.120889 0-43.463111 15.246222-43.463111 34.133334s19.569778 34.133333 43.463111 34.133333h538.624c24.120889 0 43.690667-15.246222 43.690667-34.133333zM544.768 155.420444H174.307556c-16.611556 0-29.809778 14.336-29.809778 31.857778s13.425778 31.857778 29.809778 31.857778h370.460444c16.611556 0 29.809778-14.336 29.809778-31.857778 0.227556-17.521778-13.198222-31.857778-29.809778-31.857778zM427.804444 531.797333l-84.423111 84.423111-84.423111-84.423111H142.222222l142.677334 142.904889-142.677334 142.904889h116.736l84.423111-84.423111 84.423111 84.423111h116.963556l-142.904889-142.904889 142.904889-142.904889z" p-id="9500"></path></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1859,7 +1859,9 @@ export default {
polynomial_regression: 'Polynomial regression', polynomial_regression: 'Polynomial regression',
show_summary: 'Show summary', show_summary: 'Show summary',
summary_label: 'Summary label', 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: { dataset: {
scope_edit: 'Effective only when editing', scope_edit: 'Effective only when editing',

View File

@ -1852,7 +1852,9 @@ export default {
polynomial_regression: '多項式擬合', polynomial_regression: '多項式擬合',
show_summary: '顯示總計', show_summary: '顯示總計',
summary_label: '總計標籤', summary_label: '總計標籤',
tip: '提示' tip: '提示',
pivot_export_empty_fields: '行維度或指標維度為空不可導出',
export_formatted_excel: '導出 Excel (帶格式)'
}, },
dataset: { dataset: {
scope_edit: '僅編輯時生效', scope_edit: '僅編輯時生效',

View File

@ -1849,7 +1849,9 @@ export default {
polynomial_regression: '多项式拟合', polynomial_regression: '多项式拟合',
show_summary: '显示总计', show_summary: '显示总计',
summary_label: '总计标签', summary_label: '总计标签',
tip: '提示' tip: '提示',
pivot_export_empty_fields: '行维度或指标维度为空不可导出',
export_formatted_excel: '导出 Excel (带格式)'
}, },
dataset: { dataset: {
goto: ', 前往 ', goto: ', 前往 ',

View File

@ -5,7 +5,8 @@ const getDefaultState = () => {
sceneId: {}, sceneId: {},
viewId: null, viewId: null,
tableId: {}, tableId: {},
chartSceneData: {} chartSceneData: {},
tableInstance: {}
} }
} }
@ -29,6 +30,9 @@ const mutations = {
}, },
setChartSceneData: (state, chartSceneData) => { setChartSceneData: (state, chartSceneData) => {
state.chartSceneData = chartSceneData state.chartSceneData = chartSceneData
},
setTableInstance: (state, { viewId, tableInstance }) => {
state.tableInstance[viewId] = tableInstance
} }
} }
@ -50,6 +54,9 @@ const actions = {
}, },
setChartSceneData: ({ commit }, chartSceneData) => { setChartSceneData: ({ commit }, chartSceneData) => {
commit('setChartSceneData', chartSceneData) commit('setChartSceneData', chartSceneData)
},
setTableInstance: ({ commit }, { viewId, tableInstance }) => {
commit('setTableInstance', { viewId, tableInstance })
} }
} }

View File

@ -1,5 +1,9 @@
import { hexColorToRGBA, resetRgbOpacity } from '@/views/chart/chart/util' import { hexColorToRGBA, resetRgbOpacity } from '@/views/chart/chart/util'
import { DEFAULT_COLOR_CASE, DEFAULT_SIZE } from '@/views/chart/chart/chart' 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) { export function getCustomTheme(chart) {
const headerColor = hexColorToRGBA(DEFAULT_COLOR_CASE.tableHeaderBgColor, DEFAULT_COLOR_CASE.alpha) const headerColor = hexColorToRGBA(DEFAULT_COLOR_CASE.tableHeaderBgColor, DEFAULT_COLOR_CASE.alpha)
@ -292,3 +296,168 @@ export function getSize(chart) {
return size 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`)
}

View File

@ -308,6 +308,7 @@ export default {
this.myChart = baseTableNormal(this.chartId, chart, this.antVAction, this.tableData, this, this.columnResize) this.myChart = baseTableNormal(this.chartId, chart, this.antVAction, this.tableData, this, this.columnResize)
} else if (chart.type === 'table-pivot') { } else if (chart.type === 'table-pivot') {
this.myChart = baseTablePivot(this.chartId, chart, this.antVAction, this.tableHeaderClick, this.tableData) 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) { if (this.myChart && this.searchCount > 0) {