mirror of
https://github.com/dataease/dataease.git
synced 2026-05-18 09:48:10 +08:00
feat(图表): 明细表支持字段分组
This commit is contained in:
@@ -1929,6 +1929,16 @@ Scatter chart (bubble) chart: {a} (series name), {b} (data name), {c} (value arr
|
||||
central_point: 'Center point',
|
||||
full_display: 'Full display',
|
||||
show_hover_style: 'Show mouse hover style',
|
||||
table_header_group: 'Header grouping',
|
||||
table_header_group_config: 'Header grouping config',
|
||||
cancel_group: 'Cancel grouping',
|
||||
cancel_all_group: 'Cancel all grouping',
|
||||
group_name: 'Group name',
|
||||
merge_group: 'Merge group',
|
||||
table_header_group_config_tip:
|
||||
'Field additions, deletions, positional changes, and explicit and implicit modifications can cause grouping to become invalid.',
|
||||
group_name_edit_tip: 'Group names are 1-20 characters in length',
|
||||
group_name_error_tip: 'Please input valid group name',
|
||||
merge_cells: 'Merge cells',
|
||||
length_limit: 'Length limit',
|
||||
radar_point: 'Enable auxiliary points',
|
||||
|
||||
@@ -1884,6 +1884,15 @@ export default {
|
||||
central_point: '中心點',
|
||||
full_display: '全量顯示',
|
||||
show_hover_style: '顯示滑鼠懸浮樣式',
|
||||
table_header_group: '表頭分組',
|
||||
table_header_group_config: '表頭分組設置',
|
||||
cancel_group: '取消分組',
|
||||
cancel_all_group: '取消所有分組',
|
||||
group_name: '分組名稱',
|
||||
merge_group: '合併分組',
|
||||
table_header_group_config_tip: '字段的增刪,位置變動,顯隱修改都會導致分組失效',
|
||||
group_name_edit_tip: '分組名稱長度為 1-20 個字符',
|
||||
group_name_error_tip: '請輸入正確的分組名稱',
|
||||
merge_cells: '合併儲存格',
|
||||
length_limit: '長度限制',
|
||||
radar_point: '開啟輔助點',
|
||||
|
||||
@@ -1894,6 +1894,15 @@ export default {
|
||||
central_point: '中心点',
|
||||
full_display: '全量显示',
|
||||
show_hover_style: '显示鼠标悬浮样式',
|
||||
table_header_group: '表头分组',
|
||||
table_header_group_config: '表头分组设置',
|
||||
cancel_group: '取消分组',
|
||||
cancel_all_group: '取消所有分组',
|
||||
group_name: '分组名称',
|
||||
merge_group: '合并分组',
|
||||
table_header_group_config_tip: '字段的增删,位置变动,显隐修改都会导致分组失效',
|
||||
group_name_edit_tip: '分组名称长度为 1-20 个字符',
|
||||
group_name_error_tip: '请输入正确的分组名称',
|
||||
merge_cells: '合并单元格',
|
||||
length_limit: '长度限制',
|
||||
radar_point: '开启辅助点',
|
||||
|
||||
@@ -441,6 +441,32 @@ declare interface ChartTableHeaderAttr {
|
||||
isBolder: boolean
|
||||
isCornerBolder: boolean
|
||||
isColBolder: boolean
|
||||
/**
|
||||
* 表头分组开关
|
||||
*/
|
||||
headerGroup: boolean
|
||||
/**
|
||||
* 表头分组设置
|
||||
*/
|
||||
headerGroupConfig: {
|
||||
/**
|
||||
* 分组结构
|
||||
*/
|
||||
columns: Columns
|
||||
/**
|
||||
* 分组名称
|
||||
*/
|
||||
meta: {
|
||||
/**
|
||||
* 字段id
|
||||
*/
|
||||
field: string
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
name: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 单元格属性
|
||||
@@ -1276,3 +1302,10 @@ declare interface ConversionTagAtt {
|
||||
*/
|
||||
precision: number
|
||||
}
|
||||
|
||||
declare interface ColumnNode {
|
||||
key: string
|
||||
children?: Columns
|
||||
}
|
||||
|
||||
declare type Columns = Array<string | ColumnNode>
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
<template>
|
||||
<div id="table-container"></div>
|
||||
<div class="button-group">
|
||||
<el-button :effect="themes" @click="onCancelConfig">{{ t('chart.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="onConfigChange">{{ t('chart.confirm') }}</el-button>
|
||||
</div>
|
||||
<div id="menu-group" class="group-menu"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
||||
import { formatterItem, valueFormatter } from '@/views/chart/components/js/formatter'
|
||||
import {
|
||||
BaseTooltip,
|
||||
ColumnNode,
|
||||
S2DataConfig,
|
||||
S2Event,
|
||||
S2Options,
|
||||
TableSheet,
|
||||
TooltipShowOptions
|
||||
} from '@antv/s2'
|
||||
import { ElMessageBox } from 'element-plus-secondary'
|
||||
import { cloneDeep, isEqual, isNumber } from 'lodash-es'
|
||||
import { nextTick, onMounted, PropType } from 'vue'
|
||||
import { uuid } from 'vue-uuid'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import {
|
||||
getColumns,
|
||||
getCustomTheme,
|
||||
getLeafNodes
|
||||
} from '@/views/chart/components/js/panel/common/common_table'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const props = defineProps({
|
||||
chart: {
|
||||
type: Object as PropType<ChartObj>,
|
||||
required: true
|
||||
},
|
||||
themes: {
|
||||
type: String as PropType<EditorTheme>,
|
||||
default: 'dark'
|
||||
},
|
||||
propertyInner: {
|
||||
type: Array<string>
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['onConfigChange', 'onCancelConfig'])
|
||||
const onCancelConfig = () => {
|
||||
emits('onCancelConfig')
|
||||
}
|
||||
|
||||
const onConfigChange = () => {
|
||||
const allAxis = props.chart.xAxis
|
||||
?.map(axis => axis.hide !== true && axis.dataeaseName)
|
||||
.filter(i => i)
|
||||
const { fields, meta } = s2.dataCfg
|
||||
const groupMeta = meta.filter(item => !allAxis.includes(item.field))
|
||||
emits('onConfigChange', { columns: fields.columns, meta: groupMeta })
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
const chart = cloneDeep(props.chart)
|
||||
const xAxis = chart.xAxis
|
||||
const { headerGroupConfig } = chart.customAttr.tableHeader
|
||||
const showColumns = []
|
||||
xAxis?.forEach(axis => {
|
||||
axis.hide !== true && showColumns.push({ key: axis.dataeaseName })
|
||||
})
|
||||
if (!showColumns.length) {
|
||||
return
|
||||
}
|
||||
if (headerGroupConfig?.columns?.length) {
|
||||
const allAxis = showColumns.map(item => item.key)
|
||||
const leafNodes = getLeafNodes(headerGroupConfig.columns as Array<ColumnNode>)
|
||||
const leafKeys = leafNodes.map(item => item.key)
|
||||
if (!isEqual(allAxis, leafKeys)) {
|
||||
const { columns, meta } = headerGroupConfig
|
||||
columns.splice(0, columns.length, ...showColumns)
|
||||
meta.splice(0, meta.length)
|
||||
}
|
||||
} else {
|
||||
chart.customAttr.tableHeader.headerGroupConfig = {
|
||||
columns: [...showColumns],
|
||||
meta: []
|
||||
}
|
||||
}
|
||||
nextTick(() => {
|
||||
renderTable(chart)
|
||||
})
|
||||
}
|
||||
let s2: TableSheet
|
||||
const renderTable = (chart: ChartObj) => {
|
||||
const data = dvMainStore.getViewDataDetails(chart.id)
|
||||
const containerDom = document.getElementById('table-container')
|
||||
let realData = []
|
||||
if (data?.tableRow?.length) {
|
||||
realData = data.tableRow.slice(0, 10)
|
||||
}
|
||||
const { headerGroupConfig } = chart.customAttr.tableHeader
|
||||
const meta = [...headerGroupConfig.meta]
|
||||
const columns = headerGroupConfig.columns
|
||||
const axisMap = chart.xAxis.reduce((pre, cur) => {
|
||||
pre[cur.dataeaseName] = cur
|
||||
return pre
|
||||
}, {})
|
||||
if (data?.fields?.length) {
|
||||
data.fields.forEach(ele => {
|
||||
const f = axisMap[ele.dataeaseName]
|
||||
if (f?.hide === true) {
|
||||
return
|
||||
}
|
||||
meta.push({
|
||||
field: ele.dataeaseName,
|
||||
name: ele.chartShowName ?? ele.name,
|
||||
formatter: function (value) {
|
||||
if (!f) {
|
||||
return value
|
||||
}
|
||||
if (value === null || value === undefined) {
|
||||
return value
|
||||
}
|
||||
if (![2, 3].includes(f.deType) || !isNumber(value)) {
|
||||
return value
|
||||
}
|
||||
let formatCfg = f.formatterCfg
|
||||
if (!formatCfg) {
|
||||
formatCfg = formatterItem
|
||||
}
|
||||
return valueFormatter(value, formatCfg)
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
chart.xAxis?.forEach(axis => {
|
||||
if (axis.hide !== true) {
|
||||
meta.push({
|
||||
field: axis.dataeaseName,
|
||||
name: axis.chartShowName ?? axis.name
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
// // data config
|
||||
const s2DataConfig: S2DataConfig = {
|
||||
fields: {
|
||||
columns
|
||||
},
|
||||
meta,
|
||||
data: realData
|
||||
}
|
||||
// options
|
||||
const s2Options: S2Options = {
|
||||
width: containerDom.getBoundingClientRect().width,
|
||||
height: containerDom.offsetHeight,
|
||||
tooltip: {
|
||||
getContainer: () => containerDom,
|
||||
renderTooltip: sheet => new GroupMenu(sheet),
|
||||
style: {
|
||||
position: 'absolute',
|
||||
borderRadius: '4px'
|
||||
}
|
||||
}
|
||||
}
|
||||
s2 = new TableSheet(containerDom, s2DataConfig, s2Options)
|
||||
const theme = getCustomTheme(chart)
|
||||
s2.setTheme(theme)
|
||||
const groupMenuContainer = document.getElementById('menu-group')
|
||||
s2.on(S2Event.COL_CELL_CONTEXT_MENU, e => {
|
||||
e.preventDefault()
|
||||
const curColumns = s2.dataCfg.fields.columns as Array<ColumnNode>
|
||||
const curMeta = s2.dataCfg.meta
|
||||
const activeCells = s2.interaction.getActiveCells()
|
||||
const colKeys = activeCells?.map(cell => cell.getMeta().field)
|
||||
const activeColumns = getColumns(colKeys, curColumns)
|
||||
const curCell = s2.getCell(e.target)
|
||||
groupMenuContainer.innerText = ''
|
||||
// 右键点击的目标单元格不在已选的单元格中,清空已选单元格,隐藏菜单
|
||||
if (activeColumns?.length) {
|
||||
const index = activeColumns.findIndex(cell => cell.key === curCell.getMeta().field)
|
||||
if (index === -1) {
|
||||
s2.interaction.clearState()
|
||||
s2.hideTooltip()
|
||||
return
|
||||
}
|
||||
}
|
||||
//只有一个cell,并且colIndex为-1,那就是组合的,显示取消分组按钮和重命名按钮
|
||||
if (activeColumns?.length === 1 && curCell.getMeta().colIndex === -1) {
|
||||
const cancelBtn = document.createElement('span')
|
||||
groupMenuContainer.appendChild(cancelBtn)
|
||||
cancelBtn.innerText = t('chart.cancel_group')
|
||||
cancelBtn.onclick = () => {
|
||||
s2.hideTooltip()
|
||||
const parent = curCell.getMeta().parent
|
||||
if (parent?.id === 'root') {
|
||||
const startIndex = curColumns.findIndex(cell => cell.key === curCell.getMeta().field)
|
||||
const [curCol] = getColumns([curCell.getMeta().field], curColumns)
|
||||
curColumns.splice(startIndex, 1, ...curCol.children)
|
||||
const index = curMeta.findIndex(meta => meta.field === curCell.getMeta().field)
|
||||
curMeta.splice(index, 1)
|
||||
s2.setDataCfg({
|
||||
fields: {
|
||||
columns: curColumns
|
||||
},
|
||||
meta: curMeta
|
||||
})
|
||||
s2.render(true)
|
||||
} else {
|
||||
const [parentColumn] = getColumns([parent.field], curColumns)
|
||||
if (parentColumn) {
|
||||
const startIndex = parentColumn.children?.findIndex(
|
||||
cell => cell.key === curCell.getMeta().field
|
||||
)
|
||||
const [curCol] = getColumns([curCell.getMeta().field], parentColumn.children)
|
||||
parentColumn.children?.splice(startIndex, 1, ...curCol.children)
|
||||
const index = curMeta.findIndex(meta => meta.field === curCell.getMeta().field)
|
||||
curMeta.splice(index, 1)
|
||||
s2.setDataCfg({
|
||||
fields: {
|
||||
columns: curColumns
|
||||
},
|
||||
meta: curMeta
|
||||
})
|
||||
s2.render(true)
|
||||
}
|
||||
}
|
||||
s2.interaction.clearState()
|
||||
}
|
||||
const cancelAllBtn = document.createElement('span')
|
||||
groupMenuContainer.appendChild(cancelAllBtn)
|
||||
cancelAllBtn.innerText = t('chart.cancel_all_group')
|
||||
cancelAllBtn.onclick = () => {
|
||||
s2.hideTooltip()
|
||||
const parent = curCell.getMeta().parent
|
||||
if (parent?.id === 'root') {
|
||||
const [curCol] = getColumns([curCell.getMeta().field], curColumns)
|
||||
const leafNodes = getLeafNodes(curCol.children)
|
||||
const startIndex = curColumns.findIndex(cell => cell.key === curCell.getMeta().field)
|
||||
curColumns.splice(startIndex, 1, ...leafNodes)
|
||||
const noneLeafNodes = getNonLeafNodes([curCol])
|
||||
const newMeta = curMeta.filter(meta => !noneLeafNodes.includes(meta.field))
|
||||
s2.setDataCfg({
|
||||
fields: {
|
||||
columns: curColumns
|
||||
},
|
||||
meta: newMeta
|
||||
})
|
||||
s2.render(true)
|
||||
} else {
|
||||
const [parentColumn] = getColumns([parent.field], curColumns)
|
||||
if (parentColumn) {
|
||||
const [curCol] = getColumns([curCell.getMeta().field], parentColumn.children)
|
||||
const leafNodes = getLeafNodes(curCol.children)
|
||||
const startIndex = parentColumn.children?.findIndex(
|
||||
cell => cell.key === curCell.getMeta().field
|
||||
)
|
||||
parentColumn.children?.splice(startIndex, 1, ...leafNodes)
|
||||
const noneLeafNodes = getNonLeafNodes([curCol])
|
||||
const newMeta = curMeta.filter(meta => !noneLeafNodes.includes(meta.field))
|
||||
s2.setDataCfg({
|
||||
fields: {
|
||||
columns: curColumns
|
||||
},
|
||||
meta: newMeta
|
||||
})
|
||||
s2.render(true)
|
||||
}
|
||||
}
|
||||
s2.interaction.clearState()
|
||||
}
|
||||
const renameBtn = document.createElement('span')
|
||||
groupMenuContainer.appendChild(renameBtn)
|
||||
renameBtn.innerText = t('chart.rename')
|
||||
renameBtn.onclick = () => {
|
||||
s2.hideTooltip()
|
||||
const cellMeta = curMeta.find(meta => meta.field === curCell.getMeta().field)
|
||||
ElMessageBox.prompt('', t('chart.group_name'), {
|
||||
confirmButtonText: t('chart.confirm'),
|
||||
cancelButtonText: t('chart.cancel'),
|
||||
showClose: false,
|
||||
showInput: true,
|
||||
inputPlaceholder: t('chart.group_name_edit_tip'),
|
||||
inputValue: cellMeta.name,
|
||||
inputErrorMessage: t('chart.group_name_error_tip'),
|
||||
// 正则校验,长度 1-20
|
||||
inputValidator: val => {
|
||||
if (val?.length < 1 || val?.length > 20) {
|
||||
return t('chart.group_name_error_tip')
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
cellMeta.name = res.value
|
||||
s2.setDataCfg({
|
||||
meta: curMeta
|
||||
})
|
||||
s2.render(true)
|
||||
})
|
||||
.catch(() => {
|
||||
// do nothing
|
||||
})
|
||||
}
|
||||
s2.showTooltip({
|
||||
position: {
|
||||
x: e.x,
|
||||
y: e.y
|
||||
},
|
||||
content: groupMenuContainer
|
||||
})
|
||||
return
|
||||
}
|
||||
//如果有多个cell都在同一个层级,并且parent相同,那就是可以进行合并分组操作
|
||||
if (activeColumns?.length > 1) {
|
||||
const sameParent = activeCells.every(
|
||||
cell => cell.getMeta().parent === curCell.getMeta().parent
|
||||
)
|
||||
if (!sameParent) {
|
||||
return
|
||||
}
|
||||
let upDepth = -1
|
||||
let tmpCell = curCell
|
||||
while (tmpCell?.getMeta?.()?.parent || tmpCell?.parent) {
|
||||
upDepth++
|
||||
tmpCell = tmpCell?.getMeta?.()?.parent || tmpCell?.parent
|
||||
}
|
||||
let startIndex = -1
|
||||
let endIndex = -1
|
||||
const parent = curCell.getMeta().parent
|
||||
// 分组的节点
|
||||
if (parent.colIndex !== -1) {
|
||||
activeColumns.forEach(cell => {
|
||||
const index = parent.children.findIndex(item => item.getMeta().field === cell.key)
|
||||
if (index < startIndex || startIndex === -1) {
|
||||
startIndex = index
|
||||
}
|
||||
if (index > endIndex || endIndex === -1) {
|
||||
endIndex = index
|
||||
}
|
||||
})
|
||||
} else {
|
||||
activeColumns.forEach(cell => {
|
||||
const index = parent.children.findIndex(item => item.key === cell.key)
|
||||
if (index < startIndex || startIndex === -1) {
|
||||
startIndex = index
|
||||
}
|
||||
if (index > endIndex || endIndex === -1) {
|
||||
endIndex = index
|
||||
}
|
||||
})
|
||||
}
|
||||
const totalColumns = []
|
||||
if (parent?.id === 'root') {
|
||||
totalColumns.push(...curColumns.slice(startIndex, endIndex + 1))
|
||||
} else {
|
||||
const [parentColumn] = getColumns([parent.field], curColumns)
|
||||
totalColumns.push(...parentColumn.children?.slice(startIndex, endIndex + 1))
|
||||
}
|
||||
const chiildDepth = getTreesMaxDepth(totalColumns)
|
||||
// 最大分组为 3 级
|
||||
if (chiildDepth + upDepth > 1) {
|
||||
return
|
||||
}
|
||||
const mergeBtn = document.createElement('span')
|
||||
groupMenuContainer.appendChild(mergeBtn)
|
||||
mergeBtn.innerText = t('chart.merge_group')
|
||||
mergeBtn.onclick = () => {
|
||||
s2.hideTooltip()
|
||||
ElMessageBox.prompt('', t('chart.group_name'), {
|
||||
confirmButtonText: t('chart.confirm'),
|
||||
cancelButtonText: t('chart.cancel'),
|
||||
showClose: false,
|
||||
showInput: true,
|
||||
inputPlaceholder: t('chart.group_name_edit_tip'),
|
||||
inputErrorMessage: t('chart.group_name_error_tip'),
|
||||
// 正则校验,长度 1-20
|
||||
inputValidator: val => {
|
||||
if (val?.length < 1 || val?.length > 20) {
|
||||
return t('chart.group_name_error_tip')
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if (parent?.id === 'root') {
|
||||
const newKey = uuid.v4()
|
||||
curColumns?.splice(startIndex, endIndex - startIndex + 1, {
|
||||
key: newKey,
|
||||
children: totalColumns
|
||||
})
|
||||
curMeta.push({
|
||||
field: newKey,
|
||||
name: res.value
|
||||
})
|
||||
s2.setDataCfg({
|
||||
fields: {
|
||||
columns: curColumns
|
||||
},
|
||||
meta: curMeta
|
||||
})
|
||||
s2.render(true)
|
||||
} else {
|
||||
const [parentColumn] = getColumns([parent.field], curColumns)
|
||||
const newKey = uuid.v4()
|
||||
parentColumn.children?.splice(startIndex, endIndex - startIndex + 1, {
|
||||
key: newKey,
|
||||
children: totalColumns
|
||||
})
|
||||
curMeta.push({
|
||||
field: newKey,
|
||||
name: res.value
|
||||
})
|
||||
s2.setDataCfg({
|
||||
fields: {
|
||||
columns: curColumns
|
||||
},
|
||||
meta: curMeta
|
||||
})
|
||||
s2.render(true)
|
||||
}
|
||||
s2.interaction.clearState()
|
||||
})
|
||||
.catch(() => {
|
||||
// do nothing
|
||||
})
|
||||
}
|
||||
s2.showTooltip({
|
||||
position: {
|
||||
x: e.x,
|
||||
y: e.y
|
||||
},
|
||||
content: groupMenuContainer
|
||||
})
|
||||
return
|
||||
}
|
||||
})
|
||||
s2.render()
|
||||
}
|
||||
|
||||
const getNonLeafNodes = (tree: Array<ColumnNode>): string[] => {
|
||||
const result: string[] = []
|
||||
|
||||
const inorderTraversal = (node: ColumnNode) => {
|
||||
// 如果有子节点,则为非叶子节点
|
||||
if (node.children?.length > 0) {
|
||||
result.push(node.key)
|
||||
|
||||
// 递归处理子节点
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
inorderTraversal(node.children[i] as ColumnNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历树中所有节点
|
||||
tree.forEach(node => inorderTraversal(node))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const getTreesMaxDepth = (nodes: Array<ColumnNode>): number => {
|
||||
if (!nodes?.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 获取单个节点的最大子树深度
|
||||
const getNodeMaxDepth = (node: ColumnNode): number => {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return 0
|
||||
}
|
||||
const childrenDepths = node.children.map(child => getNodeMaxDepth(child as ColumnNode))
|
||||
return Math.max(...childrenDepths) + 1
|
||||
}
|
||||
|
||||
// 计算所有根节点的最大深度
|
||||
const rootDepths = nodes.map(node => getNodeMaxDepth(node))
|
||||
return Math.max(...rootDepths)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
class GroupMenu extends BaseTooltip {
|
||||
show<T = string | Element>(showOptions: TooltipShowOptions<T>): void {
|
||||
super.show(showOptions)
|
||||
this.container.style.display = 'flex'
|
||||
}
|
||||
hide(): void {
|
||||
if (this.container) {
|
||||
this.container.style.display = 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
#table-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 40vh;
|
||||
}
|
||||
|
||||
.group-menu {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
color: black;
|
||||
font-size: 14px;
|
||||
:deep(span) {
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
&:hover {
|
||||
background-color: var(--ed-fill-color-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@ import icon_italic_outlined from '@/assets/svg/icon_italic_outlined.svg'
|
||||
import icon_leftAlignment_outlined from '@/assets/svg/icon_left-alignment_outlined.svg'
|
||||
import icon_centerAlignment_outlined from '@/assets/svg/icon_center-alignment_outlined.svg'
|
||||
import icon_rightAlignment_outlined from '@/assets/svg/icon_right-alignment_outlined.svg'
|
||||
import icon_edit_outlined from '@/assets/svg/icon_edit_outlined.svg'
|
||||
import { computed, onMounted, PropType, reactive, watch } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { COLOR_PANEL, DEFAULT_TABLE_HEADER } from '@/views/chart/components/editor/util/chart'
|
||||
@@ -12,6 +13,8 @@ import { cloneDeep, defaultsDeep } from 'lodash-es'
|
||||
import { convertToAlphaColor, isAlphaColor } from '@/views/chart/components/js/util'
|
||||
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import TableHeaderGroupConfig from './TableHeaderGroupConfig.vue'
|
||||
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const { mobileInPc } = storeToRefs(dvMainStore)
|
||||
const { t } = useI18n()
|
||||
@@ -58,7 +61,8 @@ const fontSizeList = computed(() => {
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
tableHeaderForm: {} as ChartTableHeaderAttr
|
||||
tableHeaderForm: {} as ChartTableHeaderAttr,
|
||||
showTableHeaderGroupConfig: false
|
||||
})
|
||||
|
||||
const emit = defineEmits(['onTableHeaderChange'])
|
||||
@@ -67,6 +71,13 @@ const changeTableHeader = prop => {
|
||||
emit('onTableHeaderChange', state.tableHeaderForm, prop)
|
||||
}
|
||||
|
||||
const changeHeaderGroupConfig = (headerGroupConfig: ChartTableHeaderAttr['headerGroupConfig']) => {
|
||||
state.tableHeaderForm.headerGroupConfig = headerGroupConfig
|
||||
state.showTableHeaderGroupConfig = false
|
||||
log(headerGroupConfig)
|
||||
changeTableHeader('headerGroupConfig')
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
const tableHeader = props.chart?.customAttr?.tableHeader
|
||||
if (tableHeader) {
|
||||
@@ -723,7 +734,67 @@ onMounted(() => {
|
||||
{{ t('chart.table_header_show_vertical_border') }}
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="showProperty('headerGroup')"
|
||||
class="form-item"
|
||||
:class="'form-item-' + themes"
|
||||
>
|
||||
<el-checkbox
|
||||
size="small"
|
||||
:effect="themes"
|
||||
v-model="state.tableHeaderForm.headerGroup"
|
||||
@change="changeTableHeader('headerGroup')"
|
||||
>
|
||||
{{ t('chart.table_header_group') }}
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="showProperty('headerGroup') && state.tableHeaderForm.headerGroup"
|
||||
class="form-item"
|
||||
:class="'form-item-' + themes"
|
||||
>
|
||||
<div class="header-group-config">
|
||||
<span>{{ t('chart.table_header_group_config') }}</span>
|
||||
<div class="group-icon">
|
||||
<span v-if="state.tableHeaderForm.headerGroupConfig?.columns?.length">
|
||||
{{ t('visualization.already_setting') }}
|
||||
</span>
|
||||
<div
|
||||
class="icon-btn"
|
||||
:class="{
|
||||
dark: themes === 'dark'
|
||||
}"
|
||||
>
|
||||
<el-icon @click="state.showTableHeaderGroupConfig = true">
|
||||
<Icon>
|
||||
<icon_edit_outlined class="svg-icon" />
|
||||
</Icon>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-dialog
|
||||
v-model="state.showTableHeaderGroupConfig"
|
||||
:effect="themes"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
:show-close="false"
|
||||
:class="themes === 'dark' ? 'table-header-group-config-dialog' : ''"
|
||||
>
|
||||
<template #header>
|
||||
{{ t('chart.table_header_group_config') }}
|
||||
<span style="font-size: 12px">({{ t('chart.table_header_group_config_tip') }})</span>
|
||||
</template>
|
||||
<table-header-group-config
|
||||
:chart="chart"
|
||||
:themes="themes"
|
||||
:tableHeaderForm="state.tableHeaderForm"
|
||||
@onConfigChange="changeHeaderGroupConfig"
|
||||
@onCancelConfig="() => (state.showTableHeaderGroupConfig = false)"
|
||||
/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -804,4 +875,28 @@ onMounted(() => {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
.header-group-config {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 22px;
|
||||
font-size: 12px;
|
||||
.group-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.table-header-group-config-dialog {
|
||||
.ed-dialog__header,
|
||||
.ed-dialog__body {
|
||||
color: #a6a6a6;
|
||||
background-color: #1a1a1a;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -448,7 +448,12 @@ export const DEFAULT_TABLE_HEADER: ChartTableHeaderAttr = {
|
||||
isColItalic: false,
|
||||
isBolder: true,
|
||||
isCornerBolder: true,
|
||||
isColBolder: true
|
||||
isColBolder: true,
|
||||
headerGroup: false,
|
||||
headerGroupConfig: {
|
||||
columns: [],
|
||||
meta: []
|
||||
}
|
||||
}
|
||||
export const DEFAULT_TABLE_CELL: ChartTableCellAttr = {
|
||||
tableFontColor: '#000000',
|
||||
@@ -1671,7 +1676,10 @@ export const DEFAULT_BASIC_STYLE: ChartBasicStyle = {
|
||||
maxLines: 3,
|
||||
radarShowPoint: true,
|
||||
radarPointSize: 4,
|
||||
radarAreaColor: true
|
||||
radarAreaColor: true,
|
||||
circleBorderColor: '#fff',
|
||||
circleBorderWidth: 0,
|
||||
circlePadding: 0
|
||||
}
|
||||
|
||||
export const BASE_VIEW_CONFIG = {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util'
|
||||
import { S2ChartView, S2DrawOptions } from '../../types/impl/s2'
|
||||
import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { isNumber, merge } from 'lodash-es'
|
||||
import { isEqual, isNumber, merge } from 'lodash-es'
|
||||
import {
|
||||
copyContent,
|
||||
CustomDataCell,
|
||||
@@ -24,7 +24,10 @@ import {
|
||||
SortTooltip,
|
||||
configSummaryRow,
|
||||
summaryRowStyle,
|
||||
configEmptyDataStyle
|
||||
configEmptyDataStyle,
|
||||
getValidLeafNodes,
|
||||
getLeafNodes,
|
||||
getColumns
|
||||
} from '@/views/chart/components/js/panel/common/common_table'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -63,7 +66,8 @@ export class TableInfo extends S2ChartView<TableSheet> {
|
||||
'table-header-selector': [
|
||||
...TABLE_EDITOR_PROPERTY_INNER['table-header-selector'],
|
||||
'tableHeaderSort',
|
||||
'showTableHeader'
|
||||
'showTableHeader',
|
||||
'headerGroup'
|
||||
],
|
||||
'basic-style-selector': [
|
||||
'tableColumnMode',
|
||||
@@ -103,6 +107,7 @@ export class TableInfo extends S2ChartView<TableSheet> {
|
||||
pre[cur.dataeaseName] = cur
|
||||
return pre
|
||||
}, {})
|
||||
const drillFieldMap = {}
|
||||
if (chart.drill) {
|
||||
// 下钻过滤字段
|
||||
const filterFields = chart.drillFilters.map(i => i.fieldId)
|
||||
@@ -111,13 +116,14 @@ export class TableInfo extends S2ChartView<TableSheet> {
|
||||
const drillFieldIndex = chart.xAxis.findIndex(ele => ele.id === drillFieldId)
|
||||
// 当前下钻字段
|
||||
const curDrillFieldId = chart.drillFields[filterFields.length].id
|
||||
const curDrillField = fields.filter(ele => ele.id === curDrillFieldId)
|
||||
const curDrillField = fields.find(ele => ele.id === curDrillFieldId)
|
||||
filterFields.push(curDrillFieldId)
|
||||
// 移除下钻字段,把当前下钻字段插入到下钻入口位置
|
||||
fields = fields.filter(ele => {
|
||||
return !filterFields.includes(ele.id)
|
||||
})
|
||||
fields.splice(drillFieldIndex, 0, ...curDrillField)
|
||||
drillFieldMap[curDrillField.dataeaseName] = chart.drillFields[0].dataeaseName
|
||||
fields.splice(drillFieldIndex, 0, curDrillField)
|
||||
}
|
||||
fields.forEach(ele => {
|
||||
const f = axisMap[ele.dataeaseName]
|
||||
@@ -146,6 +152,27 @@ export class TableInfo extends S2ChartView<TableSheet> {
|
||||
}
|
||||
})
|
||||
})
|
||||
const { basicStyle, tableCell, tableHeader, tooltip } = parseJson(chart.customAttr)
|
||||
// 表头分组
|
||||
const { headerGroup } = tableHeader
|
||||
if (headerGroup) {
|
||||
const { headerGroupConfig } = tableHeader
|
||||
if (headerGroupConfig?.columns?.length) {
|
||||
const allKeys = columns.map(c => drillFieldMap[c] || c)
|
||||
const leafNodes = getLeafNodes(headerGroupConfig.columns as ColumnNode[])
|
||||
const leafKeys = leafNodes.map(c => c.key)
|
||||
if (isEqual(leafKeys, allKeys)) {
|
||||
if (Object.keys(drillFieldMap).length) {
|
||||
const originField = Object.values(drillFieldMap)[0]
|
||||
const drillField = Object.keys(drillFieldMap)[0]
|
||||
const [drillCol] = getColumns([originField], headerGroupConfig.columns as ColumnNode[])
|
||||
drillCol.key = drillField
|
||||
}
|
||||
columns.splice(0, columns.length, ...headerGroupConfig.columns)
|
||||
meta.push(...headerGroupConfig.meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 空值处理
|
||||
const newData = this.configEmptyDataStrategy(chart)
|
||||
// data config
|
||||
@@ -157,7 +184,6 @@ export class TableInfo extends S2ChartView<TableSheet> {
|
||||
data: newData
|
||||
}
|
||||
|
||||
const { basicStyle, tableCell, tableHeader, tooltip } = parseJson(chart.customAttr)
|
||||
// options
|
||||
const s2Options: S2Options = {
|
||||
width: containerDom.getBoundingClientRect().width,
|
||||
@@ -302,6 +328,13 @@ export class TableInfo extends S2ChartView<TableSheet> {
|
||||
n.x = p
|
||||
return p + n.width
|
||||
}, 0)
|
||||
// 处理分组的单元格,宽度为所有叶子节点之和
|
||||
ev.colNodes.forEach(n => {
|
||||
if (n.colIndex === -1) {
|
||||
n.width = calcTreeWidth(n)
|
||||
n.x = getStartPosition(n)
|
||||
}
|
||||
})
|
||||
ev.colsHierarchy.width = totalWidth
|
||||
newChart.store.set('lastLayoutResult', undefined)
|
||||
return
|
||||
@@ -334,6 +367,13 @@ export class TableInfo extends S2ChartView<TableSheet> {
|
||||
n.x = p
|
||||
return p + n.width
|
||||
}, 0)
|
||||
// 处理分组的单元格,宽度为所有叶子节点之和
|
||||
ev.colNodes.forEach(n => {
|
||||
if (n.colIndex === -1) {
|
||||
n.width = calcTreeWidth(n)
|
||||
n.x = getStartPosition(n)
|
||||
}
|
||||
})
|
||||
if (totalWidth > containerWidth) {
|
||||
ev.colLeafNodes[ev.colLeafNodes.length - 1].width -= totalWidth - containerWidth
|
||||
}
|
||||
@@ -451,3 +491,19 @@ export class TableInfo extends S2ChartView<TableSheet> {
|
||||
super('table-info', [])
|
||||
}
|
||||
}
|
||||
|
||||
function calcTreeWidth(node) {
|
||||
if (!node.children?.length) {
|
||||
return node.width
|
||||
}
|
||||
return node.children.reduce((pre, cur) => {
|
||||
return pre + calcTreeWidth(cur)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function getStartPosition(node) {
|
||||
if (!node.children?.length) {
|
||||
return node.x
|
||||
}
|
||||
return getStartPosition(node.children[0])
|
||||
}
|
||||
|
||||
@@ -1963,3 +1963,35 @@ export const configEmptyDataStyle = (newChart, basicStyle, newData, container) =
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getLeafNodes = (tree: Array<ColumnNode>): ColumnNode[] => {
|
||||
const result: ColumnNode[] = []
|
||||
const inorderTraversal = node => {
|
||||
if (!node.children?.length) {
|
||||
// 叶子节点,添加到结果数组
|
||||
result.push(node)
|
||||
return
|
||||
}
|
||||
// 中序遍历
|
||||
for (let i = 0; i < node.children?.length; i++) {
|
||||
inorderTraversal(node.children[i])
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历树中所有节点
|
||||
tree.forEach(node => inorderTraversal(node))
|
||||
return result
|
||||
}
|
||||
|
||||
export const getColumns = (fields, cols: Array<ColumnNode>) => {
|
||||
const result = []
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
if (fields.includes(cols[i].key)) {
|
||||
result.push(cols[i])
|
||||
}
|
||||
if (cols[i].children?.length) {
|
||||
result.push(...getColumns(fields, cols[i].children as Array<ColumnNode>))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user