Merge pull request #12255 from dataease/pr@dev@feat_pivot_export_formatted_excel
feat(视图): 透视表平铺模式支持导出格式化内容
This commit is contained in:
commit
d2eb09c60c
@ -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",
|
||||
|
||||
@ -89,6 +89,16 @@
|
||||
@click.stop="exportExcelDownload()"
|
||||
/>
|
||||
</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
|
||||
v-if="activeModel==='edit'"
|
||||
style="float: right;height: 24px!important;"
|
||||
@ -207,6 +217,7 @@ import eventBus from '@/components/canvas/utils/eventBus'
|
||||
import { hasDataPermission } from '@/utils/permission'
|
||||
import { exportExcelDownload } from '@/components/canvas/utils/utils'
|
||||
import { Button } from 'element-ui'
|
||||
import { exportPivotExcel } from '@/views/chart/chart/common/common_table'
|
||||
|
||||
export default {
|
||||
components: { Background, LinkJumpSet, FieldsList, SettingMenu, LinkageField, MapLayerController },
|
||||
@ -298,6 +309,13 @@ export default {
|
||||
exportExcelShow() {
|
||||
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() {
|
||||
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, [
|
||||
this.$t('data_export.exporting'),
|
||||
h(
|
||||
Button,
|
||||
{
|
||||
props: {
|
||||
type: 'text',
|
||||
},
|
||||
class: 'btn-text',
|
||||
on: {
|
||||
click: () => {
|
||||
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
|
||||
|
||||
1
core/frontend/src/icons/svg/ds-excel-format.svg
Normal file
1
core/frontend/src/icons/svg/ds-excel-format.svg
Normal 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 |
@ -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',
|
||||
|
||||
@ -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: '僅編輯時生效',
|
||||
|
||||
@ -1849,7 +1849,9 @@ export default {
|
||||
polynomial_regression: '多项式拟合',
|
||||
show_summary: '显示总计',
|
||||
summary_label: '总计标签',
|
||||
tip: '提示'
|
||||
tip: '提示',
|
||||
pivot_export_empty_fields: '行维度或指标维度为空不可导出',
|
||||
export_formatted_excel: '导出 Excel (带格式)'
|
||||
},
|
||||
dataset: {
|
||||
goto: ', 前往 ',
|
||||
|
||||
@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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`)
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user