forked from github/dataease
feat(图表): 透视表支持自定义汇总 #10997
This commit is contained in:
parent
898a016f44
commit
9fd8c36229
@ -1,11 +1,383 @@
|
||||
package io.dataease.chart.charts.impl.table;
|
||||
|
||||
import io.dataease.chart.charts.impl.GroupChartHandler;
|
||||
import io.dataease.engine.constant.DeTypeConstants;
|
||||
import io.dataease.engine.constant.ExtFieldConstant;
|
||||
import io.dataease.engine.sql.SQLProvider;
|
||||
import io.dataease.engine.trans.Dimension2SQLObj;
|
||||
import io.dataease.engine.trans.Quota2SQLObj;
|
||||
import io.dataease.engine.utils.Utils;
|
||||
import io.dataease.extensions.datasource.dto.DatasourceRequest;
|
||||
import io.dataease.extensions.datasource.dto.DatasourceSchemaDTO;
|
||||
import io.dataease.extensions.datasource.model.SQLMeta;
|
||||
import io.dataease.extensions.datasource.provider.Provider;
|
||||
import io.dataease.extensions.view.dto.*;
|
||||
import io.dataease.extensions.view.util.FieldUtil;
|
||||
import io.dataease.utils.BeanUtils;
|
||||
import io.dataease.utils.IDUtils;
|
||||
import io.dataease.utils.JsonUtil;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Component
|
||||
public class TablePivotHandler extends GroupChartHandler {
|
||||
@Getter
|
||||
private String type = "table-pivot";
|
||||
|
||||
@Override
|
||||
public <T extends ChartCalcDataResult> T calcChartResult(ChartViewDTO view, AxisFormatResult formatResult, CustomFilterResult filterResult, Map<String, Object> sqlMap, SQLMeta sqlMeta, Provider provider) {
|
||||
T result = super.calcChartResult(view, formatResult, filterResult, sqlMap, sqlMeta, provider);
|
||||
Map<String, Object> customCalc = calcCustomExpr(view, filterResult, sqlMap, sqlMeta, provider);
|
||||
result.getData().put("customCalc", customCalc);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> calcCustomExpr(ChartViewDTO view, CustomFilterResult filterResult, Map<String, Object> sqlMap, SQLMeta sqlMeta, Provider provider) {
|
||||
Object totalStr = JsonUtil.toJSONString(view.getCustomAttr().get("tableTotal"));
|
||||
TableTotal tableTotal = JsonUtil.parseObject((String) totalStr, TableTotal.class);
|
||||
var dsMap = (Map<Long, DatasourceSchemaDTO>) sqlMap.get("dsMap");
|
||||
List<String> dsList = new ArrayList<>();
|
||||
for (Map.Entry<Long, DatasourceSchemaDTO> next : dsMap.entrySet()) {
|
||||
dsList.add(next.getValue().getType());
|
||||
}
|
||||
boolean needOrder = Utils.isNeedOrder(dsList);
|
||||
boolean crossDs = Utils.isCrossDs(dsMap);
|
||||
DatasourceRequest datasourceRequest = new DatasourceRequest();
|
||||
datasourceRequest.setDsList(dsMap);
|
||||
var allFields = (List<ChartViewFieldDTO>) filterResult.getContext().get("allFields");
|
||||
var rowAxis = view.getXAxis();
|
||||
var colAxis = view.getXAxisExt();
|
||||
var dataMap = new HashMap<String, Object>();
|
||||
|
||||
// 行总计,列维度聚合加上自定义字段
|
||||
var row = tableTotal.getRow();
|
||||
if (row.isShowGrandTotals()) {
|
||||
var yAxis = new ArrayList<ChartViewFieldDTO>();
|
||||
for (TableCalcTotalCfg totalCfg : row.getCalcTotals().getCfg()) {
|
||||
if (StringUtils.equalsIgnoreCase(totalCfg.getAggregation(), "CUSTOM")){
|
||||
var field = new ChartViewFieldDTO();
|
||||
field.setDeType(DeTypeConstants.DE_FLOAT);
|
||||
BeanUtils.copyBean(field, totalCfg);
|
||||
field.setId(IDUtils.snowID());
|
||||
field.setExtField(ExtFieldConstant.EXT_CALC);
|
||||
yAxis.add(field);
|
||||
}
|
||||
}
|
||||
if (!yAxis.isEmpty()) {
|
||||
Dimension2SQLObj.dimension2sqlObj(sqlMeta, colAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
String querySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view);
|
||||
querySql = provider.rebuildSQL(querySql, sqlMeta, crossDs, dsMap);
|
||||
datasourceRequest.setQuery(querySql);
|
||||
logger.debug("calcite chart sql: " + querySql);
|
||||
List<String[]> data = (List<String[]>) provider.fetchResultField(datasourceRequest).get("data");
|
||||
nullToBlank(data);
|
||||
var tmp = new HashMap<String, Object>();
|
||||
dataMap.put("rowTotal", tmp);
|
||||
tmp.put("data", buildCustomCalcResult(data, colAxis, yAxis));
|
||||
tmp.put("sql", Base64.getEncoder().encodeToString(querySql.getBytes()));
|
||||
}
|
||||
}
|
||||
// 行小计,列维度聚合,自定义指标数 * (行维度的数量 - 1)
|
||||
if (row.isShowSubTotals()) {
|
||||
var yAxis = new ArrayList<ChartViewFieldDTO>();
|
||||
for (TableCalcTotalCfg totalCfg : row.getCalcSubTotals().getCfg()) {
|
||||
if (StringUtils.equalsIgnoreCase(totalCfg.getAggregation(), "CUSTOM")){
|
||||
var field = new ChartViewFieldDTO();
|
||||
field.setDeType(DeTypeConstants.DE_FLOAT);
|
||||
BeanUtils.copyBean(field, totalCfg);
|
||||
field.setId(IDUtils.snowID());
|
||||
field.setExtField(ExtFieldConstant.EXT_CALC);
|
||||
yAxis.add(field);
|
||||
}
|
||||
}
|
||||
if (!yAxis.isEmpty()) {
|
||||
var tmpData = new ArrayList<Map<String, Object>>();
|
||||
dataMap.put("rowSubTotal", tmpData);
|
||||
for (int i = 0; i < rowAxis.size(); i++) {
|
||||
if ( i == rowAxis.size() - 1) {
|
||||
break;
|
||||
}
|
||||
var xAxis = new ArrayList<>(colAxis);
|
||||
var subRowAxis = rowAxis.subList(0, i + 1);
|
||||
xAxis.addAll(subRowAxis);
|
||||
if (!yAxis.isEmpty()) {
|
||||
Dimension2SQLObj.dimension2sqlObj(sqlMeta, xAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
String querySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view);
|
||||
querySql = provider.rebuildSQL(querySql, sqlMeta, crossDs, dsMap);
|
||||
datasourceRequest.setQuery(querySql);
|
||||
logger.debug("calcite chart sql: " + querySql);
|
||||
List<String[]> data = (List<String[]>) provider.fetchResultField(datasourceRequest).get("data");
|
||||
nullToBlank(data);
|
||||
var tmp = new HashMap<String, Object>();
|
||||
tmp.put("data", buildCustomCalcResult(data, xAxis, yAxis));
|
||||
tmp.put("sql", Base64.getEncoder().encodeToString(querySql.getBytes()));
|
||||
tmpData.add(tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 列总计,行维度聚合加上自定义字段
|
||||
var col = tableTotal.getCol();
|
||||
if (col.isShowGrandTotals()) {
|
||||
var yAxis = new ArrayList<ChartViewFieldDTO>();
|
||||
for (TableCalcTotalCfg totalCfg : col.getCalcTotals().getCfg()) {
|
||||
if (StringUtils.equalsIgnoreCase(totalCfg.getAggregation(), "CUSTOM")){
|
||||
var field = new ChartViewFieldDTO();
|
||||
field.setDeType(DeTypeConstants.DE_FLOAT);
|
||||
BeanUtils.copyBean(field, totalCfg);
|
||||
field.setId(IDUtils.snowID());
|
||||
field.setExtField(ExtFieldConstant.EXT_CALC);
|
||||
yAxis.add(field);
|
||||
}
|
||||
}
|
||||
if (!yAxis.isEmpty()) {
|
||||
Dimension2SQLObj.dimension2sqlObj(sqlMeta, rowAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
String querySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view);
|
||||
querySql = provider.rebuildSQL(querySql, sqlMeta, crossDs, dsMap);
|
||||
datasourceRequest.setQuery(querySql);
|
||||
logger.debug("calcite chart sql: " + querySql);
|
||||
List<String[]> data = (List<String[]>) provider.fetchResultField(datasourceRequest).get("data");
|
||||
nullToBlank(data);
|
||||
var tmp = new HashMap<String, Object>();
|
||||
dataMap.put("colTotal", tmp);
|
||||
tmp.put("data", buildCustomCalcResult(data, rowAxis, yAxis));
|
||||
tmp.put("sql", Base64.getEncoder().encodeToString(querySql.getBytes()));
|
||||
}
|
||||
}
|
||||
// 列小计,行维度聚合,自定义指标数 * (列维度的数量 - 1)
|
||||
if (col.isShowSubTotals()) {
|
||||
var yAxis = new ArrayList<ChartViewFieldDTO>();
|
||||
for (TableCalcTotalCfg totalCfg : col.getCalcSubTotals().getCfg()) {
|
||||
if (StringUtils.equalsIgnoreCase(totalCfg.getAggregation(), "CUSTOM")){
|
||||
var field = new ChartViewFieldDTO();
|
||||
field.setDeType(DeTypeConstants.DE_FLOAT);
|
||||
BeanUtils.copyBean(field, totalCfg);
|
||||
field.setId(IDUtils.snowID());
|
||||
field.setExtField(ExtFieldConstant.EXT_CALC);
|
||||
yAxis.add(field);
|
||||
}
|
||||
}
|
||||
if (!yAxis.isEmpty()) {
|
||||
var tmpData = new ArrayList<Map<String, Object>>();
|
||||
dataMap.put("colSubTotal", tmpData);
|
||||
for (int i = 0; i < colAxis.size(); i++) {
|
||||
if ( i == colAxis.size() - 1) {
|
||||
break;
|
||||
}
|
||||
var xAxis = new ArrayList<>(rowAxis);
|
||||
var subColAxis = colAxis.subList(0, i + 1);
|
||||
xAxis.addAll(subColAxis);
|
||||
if (!yAxis.isEmpty()) {
|
||||
Dimension2SQLObj.dimension2sqlObj(sqlMeta, xAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
String querySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view);
|
||||
querySql = provider.rebuildSQL(querySql, sqlMeta, crossDs, dsMap);
|
||||
datasourceRequest.setQuery(querySql);
|
||||
logger.debug("calcite chart sql: " + querySql);
|
||||
List<String[]> data = (List<String[]>) provider.fetchResultField(datasourceRequest).get("data");
|
||||
nullToBlank(data);
|
||||
var tmp = new HashMap<String, Object>();
|
||||
tmp.put("data", buildCustomCalcResult(data, xAxis, yAxis));
|
||||
tmp.put("sql", Base64.getEncoder().encodeToString(querySql.getBytes()));
|
||||
tmpData.add(tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 行列交叉部分总计,无聚合,直接算,用列总计公式
|
||||
if (row.isShowGrandTotals() && col.isShowGrandTotals()) {
|
||||
var yAxis = new ArrayList<ChartViewFieldDTO>();
|
||||
for (TableCalcTotalCfg totalCfg : col.getCalcTotals().getCfg()) {
|
||||
if (StringUtils.equalsIgnoreCase(totalCfg.getAggregation(), "CUSTOM")){
|
||||
var field = new ChartViewFieldDTO();
|
||||
BeanUtils.copyBean(field, totalCfg);
|
||||
field.setExtField(ExtFieldConstant.EXT_CALC);
|
||||
field.setDeType(DeTypeConstants.DE_FLOAT);
|
||||
field.setId(IDUtils.snowID());
|
||||
yAxis.add(field);
|
||||
}
|
||||
}
|
||||
if (!yAxis.isEmpty()) {
|
||||
// 清掉聚合轴
|
||||
Dimension2SQLObj.dimension2sqlObj(sqlMeta, Collections.emptyList(), FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
String querySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view);
|
||||
querySql = provider.rebuildSQL(querySql, sqlMeta, crossDs, dsMap);
|
||||
datasourceRequest.setQuery(querySql);
|
||||
logger.debug("calcite chart sql: " + querySql);
|
||||
List<String[]> data = (List<String[]>) provider.fetchResultField(datasourceRequest).get("data");
|
||||
nullToBlank(data);
|
||||
var tmp = new HashMap<String, Object>();
|
||||
dataMap.put("rowColTotal", tmp);
|
||||
var tmpData = new HashMap<String, String>();
|
||||
for (int i = 0; i < yAxis.size(); i++) {
|
||||
var a = yAxis.get(i);
|
||||
tmpData.put(a.getDataeaseName(), data.getFirst()[i]);
|
||||
}
|
||||
tmp.put("data", tmpData);
|
||||
tmp.put("sql", Base64.getEncoder().encodeToString(querySql.getBytes()));
|
||||
}
|
||||
}
|
||||
// 行总计里面的列小计
|
||||
if (row.isShowGrandTotals() && col.isShowSubTotals()) {
|
||||
var yAxis = new ArrayList<ChartViewFieldDTO>();
|
||||
for (TableCalcTotalCfg totalCfg : col.getCalcTotals().getCfg()) {
|
||||
if (StringUtils.equalsIgnoreCase(totalCfg.getAggregation(), "CUSTOM")){
|
||||
var field = new ChartViewFieldDTO();
|
||||
BeanUtils.copyBean(field, totalCfg);
|
||||
field.setExtField(ExtFieldConstant.EXT_CALC);
|
||||
field.setDeType(DeTypeConstants.DE_FLOAT);
|
||||
field.setId(IDUtils.snowID());
|
||||
yAxis.add(field);
|
||||
}
|
||||
}
|
||||
if (!yAxis.isEmpty()) {
|
||||
var tmpData = new ArrayList<Map<String, Object>>();
|
||||
dataMap.put("colSubInRowTotal", tmpData);
|
||||
for (int i = 0; i < colAxis.size(); i++) {
|
||||
if ( i == colAxis.size() - 1) {
|
||||
break;
|
||||
}
|
||||
var xAxis = colAxis.subList(0, i + 1);
|
||||
Dimension2SQLObj.dimension2sqlObj(sqlMeta, xAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
String querySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view);
|
||||
querySql = provider.rebuildSQL(querySql, sqlMeta, crossDs, dsMap);
|
||||
datasourceRequest.setQuery(querySql);
|
||||
logger.debug("calcite chart sql: " + querySql);
|
||||
List<String[]> data = (List<String[]>) provider.fetchResultField(datasourceRequest).get("data");
|
||||
nullToBlank(data);
|
||||
var tmp = new HashMap<String, Object>();
|
||||
tmp.put("data", buildCustomCalcResult(data, xAxis, yAxis));
|
||||
tmp.put("sql", Base64.getEncoder().encodeToString(querySql.getBytes()));
|
||||
tmpData.add(tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 列总计里面的行小计
|
||||
if (col.isShowGrandTotals() && row.isShowGrandTotals()) {
|
||||
var yAxis = new ArrayList<ChartViewFieldDTO>();
|
||||
for (TableCalcTotalCfg totalCfg : row.getCalcTotals().getCfg()) {
|
||||
if (StringUtils.equalsIgnoreCase(totalCfg.getAggregation(), "CUSTOM")){
|
||||
var field = new ChartViewFieldDTO();
|
||||
BeanUtils.copyBean(field, totalCfg);
|
||||
field.setExtField(ExtFieldConstant.EXT_CALC);
|
||||
field.setDeType(DeTypeConstants.DE_FLOAT);
|
||||
field.setId(IDUtils.snowID());
|
||||
yAxis.add(field);
|
||||
}
|
||||
}
|
||||
if (!yAxis.isEmpty()) {
|
||||
var tmpData = new ArrayList<Map<String, Object>>();
|
||||
dataMap.put("rowSubInColTotal", tmpData);
|
||||
for (int i = 0; i < rowAxis.size(); i++) {
|
||||
if ( i == rowAxis.size() - 1) {
|
||||
break;
|
||||
}
|
||||
var xAxis = rowAxis.subList(0, i + 1);
|
||||
Dimension2SQLObj.dimension2sqlObj(sqlMeta, xAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
String querySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view);
|
||||
querySql = provider.rebuildSQL(querySql, sqlMeta, crossDs, dsMap);
|
||||
datasourceRequest.setQuery(querySql);
|
||||
logger.debug("calcite chart sql: " + querySql);
|
||||
List<String[]> data = (List<String[]>) provider.fetchResultField(datasourceRequest).get("data");
|
||||
nullToBlank(data);
|
||||
var tmp = new HashMap<String, Object>();
|
||||
tmp.put("data", buildCustomCalcResult(data, xAxis, yAxis));
|
||||
tmp.put("sql", Base64.getEncoder().encodeToString(querySql.getBytes()));
|
||||
tmpData.add(tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 行小计和列小计相交部分
|
||||
if (row.isShowSubTotals() && col.isShowSubTotals()) {
|
||||
var yAxis = new ArrayList<ChartViewFieldDTO>();
|
||||
for (TableCalcTotalCfg totalCfg : col.getCalcTotals().getCfg()) {
|
||||
if (StringUtils.equalsIgnoreCase(totalCfg.getAggregation(), "CUSTOM")){
|
||||
var field = new ChartViewFieldDTO();
|
||||
BeanUtils.copyBean(field, totalCfg);
|
||||
field.setExtField(ExtFieldConstant.EXT_CALC);
|
||||
field.setDeType(DeTypeConstants.DE_FLOAT);
|
||||
field.setId(IDUtils.snowID());
|
||||
yAxis.add(field);
|
||||
}
|
||||
}
|
||||
if (!yAxis.isEmpty()) {
|
||||
var tmpData = new ArrayList<List<Map<String, Object>>>();
|
||||
dataMap.put("rowSubInColSub", tmpData);
|
||||
for (int i = 0; i < rowAxis.size(); i++) {
|
||||
if ( i == rowAxis.size() - 1) {
|
||||
break;
|
||||
}
|
||||
var tmpList = new ArrayList<Map<String, Object>>();
|
||||
tmpData.add(tmpList);
|
||||
var subRow = rowAxis.subList(0, i + 1);
|
||||
var xAxis = new ArrayList<>(subRow);
|
||||
for (int j = 0; j < colAxis.size(); j++) {
|
||||
if ( j == colAxis.size() - 1) {
|
||||
break;
|
||||
}
|
||||
var subCol = colAxis.subList(0, j + 1);
|
||||
xAxis.addAll(subCol);
|
||||
Dimension2SQLObj.dimension2sqlObj(sqlMeta, xAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
Quota2SQLObj.quota2sqlObj(sqlMeta, yAxis, FieldUtil.transFields(allFields), crossDs, dsMap, Utils.getParams(FieldUtil.transFields(allFields)), view.getCalParams(), pluginManage);
|
||||
String querySql = SQLProvider.createQuerySQL(sqlMeta, true, needOrder, view);
|
||||
querySql = provider.rebuildSQL(querySql, sqlMeta, crossDs, dsMap);
|
||||
datasourceRequest.setQuery(querySql);
|
||||
logger.debug("calcite chart sql: " + querySql);
|
||||
List<String[]> data = (List<String[]>) provider.fetchResultField(datasourceRequest).get("data");
|
||||
nullToBlank(data);
|
||||
var tmp = new HashMap<String, Object>();
|
||||
tmp.put("data", buildCustomCalcResult(data, xAxis, yAxis));
|
||||
tmp.put("sql", Base64.getEncoder().encodeToString(querySql.getBytes()));
|
||||
tmpList.add(tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return dataMap;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildCustomCalcResult(List<String[]> data, List<ChartViewFieldDTO> dimAxis, List<ChartViewFieldDTO> quotaAxis) {
|
||||
var rootResult = new HashMap<String, Object>();
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
var rowData = data.get(i);
|
||||
Map<String, Object> curSubMap = rootResult;
|
||||
for (int j = 0; j < dimAxis.size(); j++) {
|
||||
var tmpMap = curSubMap.get(rowData[j]);
|
||||
if (tmpMap == null) {
|
||||
tmpMap = new HashMap<String, Object>();
|
||||
curSubMap.put(rowData[j], tmpMap);
|
||||
curSubMap = (Map<String, Object>) tmpMap;
|
||||
} else {
|
||||
curSubMap = (Map<String, Object>) tmpMap;
|
||||
}
|
||||
if (j == dimAxis.size() - 1) {
|
||||
for (int k = 0; k < quotaAxis.size(); k++) {
|
||||
var qAxis = quotaAxis.get(k);
|
||||
curSubMap.put(qAxis.getDataeaseName(), rowData[j + k + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return rootResult;
|
||||
}
|
||||
|
||||
private void nullToBlank(List<String[]> data) {
|
||||
data.forEach(r -> {
|
||||
for (int i = 0; i < r.length; i++) {
|
||||
if (r[i] == null) {
|
||||
r[i] = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -100,6 +100,9 @@ public class Quota2SQLObj {
|
||||
String cast = String.format(SQLConstants.CAST, originField, Objects.equals(y.getDeType(), DeTypeConstants.DE_INT) ? SQLConstants.DEFAULT_INT_FORMAT : SQLConstants.DEFAULT_FLOAT_FORMAT);
|
||||
if (StringUtils.equalsIgnoreCase(y.getSummary(), "count_distinct")) {
|
||||
fieldName = String.format(SQLConstants.AGG_FIELD, "COUNT", "DISTINCT " + cast);
|
||||
} else if (y.getSummary() == null){
|
||||
// 透视表自定义汇总不用聚合
|
||||
fieldName = cast;
|
||||
} else {
|
||||
fieldName = String.format(SQLConstants.AGG_FIELD, y.getSummary(), cast);
|
||||
}
|
||||
|
@ -478,9 +478,13 @@ declare interface CalcTotals {
|
||||
/**
|
||||
* 汇总聚合配置
|
||||
*/
|
||||
declare interface CalcTotalCfg {
|
||||
declare interface CalcTotalCfg extends Axis {
|
||||
dataeaseName: string
|
||||
aggregation: 'MIN' | 'MAX' | 'AVG' | 'SUM' | ''
|
||||
aggregation: 'MIN' | 'MAX' | 'AVG' | 'SUM' | 'CUSTOM' | ''
|
||||
/**
|
||||
* 自定义汇总表达式
|
||||
*/
|
||||
originName: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,6 +32,7 @@ declare interface Chart {
|
||||
fields: ChartViewField[]
|
||||
tableRow: []
|
||||
}
|
||||
customCalc: any
|
||||
}
|
||||
xAxis?: Axis[]
|
||||
xAxisExt?: Axis[]
|
||||
|
@ -0,0 +1,628 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount, watch, unref, computed, nextTick } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import CodeMirror from '@/views/visualized/data/dataset/form/CodeMirror.vue'
|
||||
import { getFunction } from '@/api/dataset'
|
||||
import { fieldType } from '@/utils/attr'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { guid } from '@/views/visualized/data/dataset/form/util'
|
||||
|
||||
export interface CalcFieldType {
|
||||
id?: string
|
||||
datasourceId?: string // 数据源id
|
||||
datasetTableId?: string // union node id
|
||||
datasetGroupId?: string // 有就传,没有null
|
||||
originName: string // 物理字段名
|
||||
name: string // 字段显示名
|
||||
dataeaseName?: string // 字段别名
|
||||
groupType: 'd' | 'q' // d=维度,q=指标
|
||||
type: string
|
||||
params?: Array<{ id: string; name: string; value: number }>
|
||||
checked: boolean
|
||||
deType: number // 字段类型
|
||||
deExtractType?: number // 字段原始类型
|
||||
extField?: number // 0=原始字段,2=复制或计算字段
|
||||
fieldShortName?: string // 字段别名
|
||||
}
|
||||
const { t } = useI18n()
|
||||
|
||||
const myCm = ref()
|
||||
const searchField = ref('')
|
||||
const searchFunction = ref('')
|
||||
|
||||
const mirror = ref()
|
||||
|
||||
const props = defineProps({
|
||||
crossDs: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
}
|
||||
})
|
||||
|
||||
const defaultForm = {
|
||||
originName: '', // 物理字段名
|
||||
name: '', // 字段显示名
|
||||
groupType: 'd', // d=维度,q=指标
|
||||
type: 'VARCHAR',
|
||||
deType: 0, // 字段类型
|
||||
extField: 2,
|
||||
id: '',
|
||||
params: [],
|
||||
checked: true
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
functionData: [],
|
||||
quotaData: []
|
||||
})
|
||||
const formQuota = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
value: null
|
||||
})
|
||||
const dialogFormVisible = ref(false)
|
||||
|
||||
const fieldForm = reactive<CalcFieldType>({ ...(defaultForm as CalcFieldType) })
|
||||
|
||||
const setFieldForm = () => {
|
||||
const str = mirror.value.state.doc.toString()
|
||||
const name2Auto = []
|
||||
fieldForm.originName = setNameIdTrans('name', 'id', str, name2Auto)
|
||||
}
|
||||
|
||||
const setNameIdTrans = (from, to, originName, name2Auto?: string[]) => {
|
||||
let name2Id = originName
|
||||
const nameIdMap = state.quotaData.reduce((pre, next) => {
|
||||
pre[next[from]] = next[to]
|
||||
return pre
|
||||
}, {})
|
||||
const on = originName.match(/\[(.+?)\]/g)
|
||||
if (on) {
|
||||
on.forEach(itm => {
|
||||
const ele = itm.slice(1, -1)
|
||||
if (name2Auto) {
|
||||
name2Auto.push(nameIdMap[ele])
|
||||
}
|
||||
name2Id = name2Id.replace(`[${ele}]`, `[${nameIdMap[ele]}]`)
|
||||
})
|
||||
}
|
||||
return name2Id
|
||||
}
|
||||
|
||||
let quotaDataList = []
|
||||
const initEdit = (obj, quotaData) => {
|
||||
formQuota.id = null
|
||||
Object.assign(fieldForm, { ...defaultForm, ...obj })
|
||||
state.quotaData = quotaData.concat(fieldForm.params || [])
|
||||
quotaDataList = cloneDeep(quotaData.concat(fieldForm.params || []))
|
||||
if (!obj.originName) {
|
||||
mirror.value.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: mirror.value.viewState.state.doc.length,
|
||||
insert: ''
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
nextTick(() => {
|
||||
mirror.value.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: mirror.value.viewState.state.doc.length,
|
||||
insert: setNameIdTrans('id', 'name', obj.originName)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const insertFieldToCodeMirror = (value: string) => {
|
||||
mirror.value.dispatch({
|
||||
changes: { from: mirror.value.viewState.state.selection.ranges[0].from, insert: value },
|
||||
selection: { anchor: mirror.value.viewState.state.selection.ranges[0].from }
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
mirror.value = myCm.value.codeComInit()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mirror.value.destroy?.()
|
||||
})
|
||||
|
||||
const insertParamToCodeMirror = (value: string) => {
|
||||
mirror.value.dispatch({
|
||||
changes: { from: mirror.value.viewState.state.selection.ranges[0].from, insert: value },
|
||||
selection: { anchor: mirror.value.viewState.state.selection.ranges[0].from }
|
||||
})
|
||||
}
|
||||
let functions = []
|
||||
const initFunction = () => {
|
||||
getFunction().then(res => {
|
||||
functions = cloneDeep(res)
|
||||
state.functionData = cloneDeep(res)
|
||||
})
|
||||
}
|
||||
watch(
|
||||
() => searchField.value,
|
||||
val => {
|
||||
if (val && val !== '') {
|
||||
state.quotaData = JSON.parse(
|
||||
JSON.stringify(
|
||||
quotaDataList.filter(
|
||||
ele =>
|
||||
ele.name.toLocaleLowerCase().includes(val.toLocaleLowerCase()) && ele.extField === 0
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
state.quotaData = JSON.parse(JSON.stringify(quotaDataList)).filter(ele => ele.extField === 0)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => searchFunction.value,
|
||||
val => {
|
||||
if (val && val !== '') {
|
||||
state.functionData = JSON.parse(
|
||||
JSON.stringify(
|
||||
functions.filter(ele => {
|
||||
return ele.func.toLocaleLowerCase().includes(val.toLocaleLowerCase())
|
||||
})
|
||||
)
|
||||
)
|
||||
} else {
|
||||
state.functionData = cloneDeep(functions)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({
|
||||
initEdit,
|
||||
setFieldForm,
|
||||
fieldForm
|
||||
})
|
||||
const parmasTitle = ref('')
|
||||
|
||||
const updateParmasToQuota = () => {
|
||||
const [o] = fieldForm.params
|
||||
parmasTitle.value = '编辑计算参数'
|
||||
Object.assign(formQuota, o || {})
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
const delParmasToQuota = () => {
|
||||
const [o] = fieldForm.params
|
||||
fieldForm.params = []
|
||||
const str = mirror.value.state.doc.toString()
|
||||
const name2Auto = []
|
||||
fieldForm.originName = setNameIdTrans('name', 'id', str, name2Auto).replaceAll(`[${o.id}]`, '')
|
||||
state.quotaData = state.quotaData.filter(ele => ele.id !== o.id)
|
||||
mirror.value.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: mirror.value.viewState.state.doc.length,
|
||||
insert: setNameIdTrans('id', 'name', fieldForm.originName)
|
||||
}
|
||||
})
|
||||
}
|
||||
initFunction()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @keydown.stop @keyup.stop class="calcu-field">
|
||||
<div class="calcu-cont">
|
||||
<div style="flex: 1">
|
||||
<div style="max-width: 488px">
|
||||
<div class="mb8 field-exp">
|
||||
<span>{{ t('dataset.field_exp') }}</span>
|
||||
<span>*</span>
|
||||
<el-tooltip class="item" effect="dark" placement="top">
|
||||
<template #content>
|
||||
<div v-if="props.crossDs">{{ t('dataset.calc_tips.tip1') }}</div>
|
||||
<div v-else>{{ t('dataset.calc_tips.tip1_1') }}</div>
|
||||
<div>{{ t('dataset.calc_tips.tip2') }}</div>
|
||||
</template>
|
||||
<el-icon size="16px">
|
||||
<Icon name="icon_info_outlined"></Icon>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<code-mirror
|
||||
:quotaMap="state.quotaData.map(ele => ele.name)"
|
||||
:dimensionMap="[]"
|
||||
ref="myCm"
|
||||
height="500px"
|
||||
dom-id="calcField"
|
||||
></code-mirror>
|
||||
</div>
|
||||
</div>
|
||||
<div class="padding-lr">
|
||||
<span class="mb8">
|
||||
{{ t('dataset.click_ref_field') }}
|
||||
<el-tooltip class="item" effect="dark" placement="bottom">
|
||||
<template #content>
|
||||
{{ t('dataset.calc_tips.tip3') }}
|
||||
<br />
|
||||
{{ t('dataset.calc_tips.tip4') }}
|
||||
<br />
|
||||
{{ t('dataset.calc_tips.tip5') }}
|
||||
</template>
|
||||
<el-icon size="16px">
|
||||
<Icon name="icon_info_outlined"></Icon>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<div class="padding-lr-content">
|
||||
<el-input v-model="searchField" :placeholder="t('dataset.edit_search')" clearable>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Icon name="icon_search-outline_outlined"></Icon>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="quota-btn_de">
|
||||
<span>{{ t('chart.quota') }}</span>
|
||||
</div>
|
||||
<div class="field-height">
|
||||
<div v-if="state.quotaData.length" class="field-list">
|
||||
<span
|
||||
v-for="item in state.quotaData"
|
||||
:key="item.id"
|
||||
class="item-quota flex-align-center ellipsis"
|
||||
:title="item.name"
|
||||
@click="insertFieldToCodeMirror('[' + item.name + ']')"
|
||||
>
|
||||
<el-icon v-if="!item.groupType">
|
||||
<Icon name="icon_adjustment_outlined"></Icon>
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<Icon
|
||||
:name="`field_${fieldType[item.deType]}`"
|
||||
:className="`field-icon-${fieldType[item.deType]}`"
|
||||
></Icon>
|
||||
</el-icon>
|
||||
{{ item.name }}
|
||||
<div v-if="!item.groupType" class="icon-right">
|
||||
<el-icon @click.stop="updateParmasToQuota" class="hover-icon">
|
||||
<Icon name="icon_edit_outlined"></Icon>
|
||||
</el-icon>
|
||||
<el-icon @click.stop="delParmasToQuota" class="hover-icon">
|
||||
<Icon name="icon_delete-trash_outlined"></Icon>
|
||||
</el-icon>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="class-na">{{ t('dataset.na') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="padding-lr">
|
||||
<span class="mb8">
|
||||
{{ t('dataset.click_ref_function') }}
|
||||
<el-tooltip class="item" effect="dark" placement="bottom">
|
||||
<template #content>
|
||||
<div v-if="props.crossDs">
|
||||
{{ t('dataset.calc_tips.tip6') }}
|
||||
<br />
|
||||
{{ t('dataset.calc_tips.tip8') }}
|
||||
<br />
|
||||
https://calcite.apache.org/docs/reference.html
|
||||
</div>
|
||||
<div v-else>{{ t('dataset.calc_tips.tip7') }}</div>
|
||||
</template>
|
||||
<el-icon size="16px">
|
||||
<Icon name="icon_info_outlined"></Icon>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<div class="padding-lr-content">
|
||||
<el-input
|
||||
v-model="searchFunction"
|
||||
style="margin-bottom: 8px"
|
||||
:placeholder="t('dataset.edit_search')"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Icon name="icon_search-outline_outlined"></Icon>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-row class="function-height">
|
||||
<div v-if="state.functionData.length" style="width: 100%">
|
||||
<el-popover
|
||||
v-for="(item, index) in state.functionData"
|
||||
:key="index"
|
||||
class="function-pop"
|
||||
placement="right"
|
||||
width="200"
|
||||
trigger="hover"
|
||||
:open-delay="500"
|
||||
>
|
||||
<template #reference>
|
||||
<span
|
||||
class="function-style flex-align-center"
|
||||
@click="insertParamToCodeMirror(item.func)"
|
||||
>{{ item.func }}</span
|
||||
>
|
||||
</template>
|
||||
<p class="pop-title">{{ item.name }}</p>
|
||||
<p class="pop-info">{{ item.func }}</p>
|
||||
<p class="pop-info">{{ item.desc }}</p>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div v-else class="class-na">{{ t('chart.no_function') }}</div>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.calcu-field {
|
||||
.calcu-cont {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mr12 {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.mr0 {
|
||||
margin-right: 0;
|
||||
|
||||
:deep(.ed-select__prefix--light) {
|
||||
padding: 0;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-select {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid #bbbfc4;
|
||||
border-radius: 4px;
|
||||
|
||||
.is-active {
|
||||
background: var(--ed-color-primary-1a, rgba(51, 112, 255, 0.1));
|
||||
}
|
||||
|
||||
.ed-button:not(.is-active) {
|
||||
color: #1f2329;
|
||||
}
|
||||
.ed-button.is-text {
|
||||
height: 24px;
|
||||
width: 44px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.ed-button + .ed-button {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.mb8 {
|
||||
margin-bottom: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #1f2329;
|
||||
|
||||
&.field-exp {
|
||||
& > :nth-child(2) {
|
||||
margin: 0 -0.67px 0 2px;
|
||||
color: #f54a45;
|
||||
font-family: '阿里巴巴普惠体 3.0 55 Regular L3';
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.ed-icon {
|
||||
color: #646a73;
|
||||
margin-left: 4.67px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding-lr {
|
||||
margin-left: 12px;
|
||||
width: 214px;
|
||||
overflow-y: hidden;
|
||||
.padding-lr-content {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--deCardStrokeColor, #dee0e3);
|
||||
box-sizing: border-box;
|
||||
height: 500px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
.hover-icon_quota {
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
|
||||
&[aria-expanded='true'] {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(31, 35, 41, 0.1);
|
||||
border-radius: 4px;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(31, 35, 41, 0.1);
|
||||
border-radius: 4px;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(31, 35, 41, 0.2);
|
||||
border-radius: 4px;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quota-btn_de {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 12px;
|
||||
color: #1f2329;
|
||||
}
|
||||
.field-height {
|
||||
height: calc(50% - 41px);
|
||||
margin-top: 12px;
|
||||
overflow-y: auto;
|
||||
& > :nth-child(1) {
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.not-allow {
|
||||
cursor: not-allowed;
|
||||
color: #bbbfc4 !important;
|
||||
}
|
||||
}
|
||||
.item-dimension,
|
||||
.item-quota {
|
||||
padding: 1px 8px;
|
||||
border: solid 1px #dee0e3;
|
||||
background-color: white;
|
||||
color: #1f2329;
|
||||
|
||||
.ed-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
height: 28px;
|
||||
margin-top: 4px;
|
||||
word-break: break-all;
|
||||
border-radius: 4px;
|
||||
|
||||
.icon-right {
|
||||
display: none;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
.ed-icon {
|
||||
margin: 0 0 0 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-dimension:hover {
|
||||
border-color: var(--ed-color-primary, #3370ff);
|
||||
background: var(--ed-color-primary-1a, rgba(51, 112, 255, 0.1));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-quota {
|
||||
.ed-icon {
|
||||
color: #04b49c;
|
||||
}
|
||||
}
|
||||
|
||||
.item-quota:hover {
|
||||
background: rgba(4, 180, 156, 0.1);
|
||||
border-color: #04b49c;
|
||||
cursor: pointer;
|
||||
.icon-right {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.function-style {
|
||||
min-height: 28px;
|
||||
padding: 0px 8px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
color: #1f2329;
|
||||
&:hover {
|
||||
background: rgba(31, 35, 41, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.function-style:hover {
|
||||
border-color: var(--ed-color-primary, #3370ff);
|
||||
cursor: pointer;
|
||||
}
|
||||
.function-height {
|
||||
height: calc(100% - 29px);
|
||||
overflow: auto;
|
||||
width: calc(100% + 16px);
|
||||
margin-left: -8px;
|
||||
}
|
||||
.function-pop :deep(.ed-popover) {
|
||||
padding: 6px !important;
|
||||
}
|
||||
.pop-title {
|
||||
margin: 6px 0 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.pop-info {
|
||||
margin: 6px 0 0 0;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.class-na {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--deTextDisable);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.calcu-field {
|
||||
.cm-scroller {
|
||||
height: 320px;
|
||||
border: 1px solid #bbbfc4;
|
||||
border-radius: 4px;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cm-focused {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
.ed-select-dropdown__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
@ -1,8 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, PropType, reactive, watch } from 'vue'
|
||||
import { onMounted, PropType, reactive, watch, ref, inject, nextTick } from 'vue'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { DEFAULT_TABLE_TOTAL } from '@/views/chart/components/editor/util/chart'
|
||||
import { cloneDeep, defaultsDeep } from 'lodash-es'
|
||||
import CustomAggrEdit from './CustomAggrEdit.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@ -31,26 +32,18 @@ const aggregations = [
|
||||
{ name: t('chart.sum'), value: 'SUM' },
|
||||
{ name: t('chart.avg'), value: 'AVG' },
|
||||
{ name: t('chart.max'), value: 'MAX' },
|
||||
{ name: t('chart.min'), value: 'MIN' }
|
||||
{ name: t('chart.min'), value: 'MIN' },
|
||||
{ name: t('commons.custom'), value: 'CUSTOM' }
|
||||
]
|
||||
const state = reactive({
|
||||
tableTotalForm: cloneDeep(DEFAULT_TABLE_TOTAL) as ChartTableTotalAttr,
|
||||
rowSubTotalItem: {
|
||||
dataeaseName: '',
|
||||
aggregation: ''
|
||||
} as CalcTotalCfg,
|
||||
rowTotalItem: {
|
||||
dataeaseName: '',
|
||||
aggregation: ''
|
||||
} as CalcTotalCfg,
|
||||
colSubTotalItem: {
|
||||
dataeaseName: '',
|
||||
aggregation: ''
|
||||
} as CalcTotalCfg,
|
||||
colTotalItem: {
|
||||
dataeaseName: '',
|
||||
aggregation: ''
|
||||
} as CalcTotalCfg
|
||||
rowSubTotalItem: {} as DeepPartial<CalcTotalCfg>,
|
||||
rowTotalItem: {} as DeepPartial<CalcTotalCfg>,
|
||||
colSubTotalItem: {} as DeepPartial<CalcTotalCfg>,
|
||||
colTotalItem: {} as DeepPartial<CalcTotalCfg>,
|
||||
totalCfg: [] as CalcTotalCfg[],
|
||||
totalCfgAttr: '',
|
||||
totalItem: {} as DeepPartial<CalcTotalCfg>
|
||||
})
|
||||
|
||||
const emit = defineEmits(['onTableTotalChange'])
|
||||
@ -86,7 +79,7 @@ const init = () => {
|
||||
totals.forEach(total => {
|
||||
setupTotalCfg(total.cfg, yAxis)
|
||||
})
|
||||
const totalTupleArr: [CalcTotalCfg, CalcTotalCfg[]][] = [
|
||||
const totalTupleArr: [DeepPartial<CalcTotalCfg>, CalcTotalCfg[]][] = [
|
||||
[state.rowTotalItem, state.tableTotalForm.row.calcTotals.cfg],
|
||||
[state.rowSubTotalItem, state.tableTotalForm.row.calcSubTotals.cfg],
|
||||
[state.colTotalItem, state.tableTotalForm.col.calcTotals.cfg],
|
||||
@ -97,6 +90,7 @@ const init = () => {
|
||||
if (!totalCfg.length) {
|
||||
total.dataeaseName = ''
|
||||
total.aggregation = ''
|
||||
total.originName = ''
|
||||
return
|
||||
}
|
||||
const totalIndex = totalCfg.findIndex(i => i.dataeaseName === total.dataeaseName)
|
||||
@ -105,6 +99,7 @@ const init = () => {
|
||||
} else {
|
||||
total.dataeaseName = totalCfg[0].dataeaseName
|
||||
total.aggregation = totalCfg[0].aggregation
|
||||
total.originName = totalCfg[0].originName
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -114,6 +109,7 @@ const changeTotal = (totalItem, totals) => {
|
||||
const item = totals[i]
|
||||
if (item.dataeaseName === totalItem.dataeaseName) {
|
||||
totalItem.aggregation = item.aggregation
|
||||
totalItem.originName = item.originName
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -126,6 +122,9 @@ const changeTotalAggr = (totalItem, totals, colOrNum) => {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (totalItem.aggregation == 'CUSTOM' && !totalItem.originName) {
|
||||
return
|
||||
}
|
||||
changeTableTotal(colOrNum)
|
||||
}
|
||||
const setupTotalCfg = (totalCfg, axis) => {
|
||||
@ -150,10 +149,51 @@ const setupTotalCfg = (totalCfg, axis) => {
|
||||
axis.forEach(i => {
|
||||
totalCfg.push({
|
||||
dataeaseName: i.dataeaseName,
|
||||
aggregation: cfgMap[i.dataeaseName] ? cfgMap[i.dataeaseName].aggregation : 'SUM'
|
||||
aggregation: cfgMap[i.dataeaseName] ? cfgMap[i.dataeaseName].aggregation : 'SUM',
|
||||
originName: cfgMap[i.dataeaseName] ? cfgMap[i.dataeaseName].originName : ''
|
||||
})
|
||||
})
|
||||
}
|
||||
const quota = inject('quota', () => [])
|
||||
const dimension = inject('dimension', () => [])
|
||||
const calcEdit = ref()
|
||||
const editCalcField = ref(false)
|
||||
const editField = (totalItem, totalCfg, attr) => {
|
||||
editCalcField.value = true
|
||||
state.totalCfg = totalCfg
|
||||
state.totalCfgAttr = attr
|
||||
state.totalItem = totalItem
|
||||
nextTick(() => {
|
||||
calcEdit.value.initEdit(
|
||||
totalItem,
|
||||
quota().filter(ele => ele.id !== '-1')
|
||||
)
|
||||
})
|
||||
}
|
||||
const closeEditCalc = () => {
|
||||
editCalcField.value = false
|
||||
}
|
||||
const confirmEditCalc = () => {
|
||||
calcEdit.value.setFieldForm()
|
||||
const obj = cloneDeep(calcEdit.value.fieldForm)
|
||||
state.totalCfg?.forEach(item => {
|
||||
if (item.dataeaseName === obj.dataeaseName) {
|
||||
item.originName = obj.originName
|
||||
setFieldDefaultValue(item)
|
||||
state.totalItem.originName = item.originName
|
||||
}
|
||||
})
|
||||
closeEditCalc()
|
||||
changeTableTotal(state.totalCfgAttr)
|
||||
}
|
||||
const setFieldDefaultValue = field => {
|
||||
field.extField = 2
|
||||
field.chartId = props.chart.id
|
||||
field.datasetGroupId = props.chart.tableId
|
||||
field.lastSyncTime = null
|
||||
field.columnIndex = dimension().length + quota().length
|
||||
field.deExtractType = field.deType
|
||||
}
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
@ -232,7 +272,7 @@ onMounted(() => {
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="11" :offset="2">
|
||||
<el-col :span="state.rowTotalItem.aggregation === 'CUSTOM' ? 8 : 11" :offset="2">
|
||||
<el-select
|
||||
:effect="themes"
|
||||
v-model="state.rowTotalItem.aggregation"
|
||||
@ -253,6 +293,19 @@ onMounted(() => {
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col v-if="state.rowTotalItem.aggregation === 'CUSTOM'" :span="2" :offset="1">
|
||||
<el-icon>
|
||||
<Setting
|
||||
@click="
|
||||
editField(
|
||||
state.rowTotalItem,
|
||||
state.tableTotalForm.row.calcTotals.cfg,
|
||||
'row.calcTotals.cfg'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-icon>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="chart.type === 'table-pivot'"
|
||||
@ -360,7 +413,7 @@ onMounted(() => {
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="11" :offset="2">
|
||||
<el-col :span="state.rowSubTotalItem.aggregation === 'CUSTOM' ? 8 : 11" :offset="2">
|
||||
<el-select
|
||||
:effect="themes"
|
||||
v-model="state.rowSubTotalItem.aggregation"
|
||||
@ -382,6 +435,19 @@ onMounted(() => {
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col v-if="state.rowSubTotalItem.aggregation === 'CUSTOM'" :span="2" :offset="1">
|
||||
<el-icon>
|
||||
<Setting
|
||||
@click="
|
||||
editField(
|
||||
state.rowSubTotalItem,
|
||||
state.tableTotalForm.row.calcSubTotals.cfg,
|
||||
'row.calcSubTotals.cfg'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-icon>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
@ -455,7 +521,7 @@ onMounted(() => {
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="11" :offset="2">
|
||||
<el-col :span="state.colTotalItem.aggregation === 'CUSTOM' ? 8 : 11" :offset="2">
|
||||
<el-select
|
||||
:effect="themes"
|
||||
v-model="state.colTotalItem.aggregation"
|
||||
@ -476,6 +542,19 @@ onMounted(() => {
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col v-if="state.colTotalItem.aggregation === 'CUSTOM'" :span="2" :offset="1">
|
||||
<el-icon>
|
||||
<Setting
|
||||
@click="
|
||||
editField(
|
||||
state.colTotalItem,
|
||||
state.tableTotalForm.col.calcTotals.cfg,
|
||||
'col.calcTotals.cfg'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-icon>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="chart.type === 'table-pivot'"
|
||||
@ -582,7 +661,7 @@ onMounted(() => {
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="11" :offset="2">
|
||||
<el-col :span="state.colSubTotalItem.aggregation === 'CUSTOM' ? 8 : 11" :offset="2">
|
||||
<el-select
|
||||
:effect="themes"
|
||||
v-model="state.colSubTotalItem.aggregation"
|
||||
@ -604,9 +683,35 @@ onMounted(() => {
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col v-if="state.colSubTotalItem.aggregation === 'CUSTOM'" :span="2" :offset="1">
|
||||
<el-icon>
|
||||
<Setting
|
||||
@click="
|
||||
editField(
|
||||
state.colSubTotalItem,
|
||||
state.tableTotalForm.col.calcSubTotals.cfg,
|
||||
'col.calcSubTotals.cfg'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-icon>
|
||||
</el-col>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<!--图表计算字段-->
|
||||
<el-dialog
|
||||
v-model="editCalcField"
|
||||
width="1000px"
|
||||
title="自定义聚合公式"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<custom-aggr-edit ref="calcEdit" />
|
||||
<template #footer>
|
||||
<el-button secondary @click="closeEditCalc()">{{ t('dataset.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="confirmEditCalc()">{{ t('dataset.confirm') }} </el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
@ -210,6 +210,7 @@ const filedList = computed(() => {
|
||||
})
|
||||
|
||||
provide('filedList', () => filedList.value)
|
||||
provide('quota', () => state.quota)
|
||||
watch(
|
||||
[() => view.value['tableId']],
|
||||
() => {
|
||||
|
@ -1,14 +1,64 @@
|
||||
import { EXTRA_FIELD, PivotSheet, S2Event, S2Options, TOTAL_VALUE, S2Theme, Totals } from '@antv/s2'
|
||||
import {
|
||||
EXTRA_FIELD,
|
||||
PivotSheet,
|
||||
S2Event,
|
||||
S2Options,
|
||||
TOTAL_VALUE,
|
||||
S2Theme,
|
||||
Totals,
|
||||
PivotDataSet,
|
||||
Query,
|
||||
VALUE_FIELD,
|
||||
QueryDataType,
|
||||
TotalStatus,
|
||||
Aggregation
|
||||
} from '@antv/s2'
|
||||
import { formatterItem, valueFormatter } from '../../../formatter'
|
||||
import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util'
|
||||
import { S2ChartView, S2DrawOptions } from '../../types/impl/s2'
|
||||
import { TABLE_EDITOR_PROPERTY_INNER } from './common'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { isNumber, maxBy, merge, minBy } from 'lodash-es'
|
||||
import { isNumber, keys, maxBy, merge, minBy, some, isEmpty, get } from 'lodash-es'
|
||||
import { copyContent } from '../../common/common_table'
|
||||
import Decimal from 'decimal.js'
|
||||
|
||||
type DataItem = Record<string, any>
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
class CustomPivotDataset extends PivotDataSet {
|
||||
getTotalValue(query: Query, totalStatus?: TotalStatus) {
|
||||
const { options } = this.spreadsheet
|
||||
const effectiveStatus = some(totalStatus)
|
||||
const status = effectiveStatus ? totalStatus : this.getTotalStatus(query)
|
||||
const { aggregation, calcFunc } =
|
||||
getAggregationAndCalcFuncByQuery(status, options?.totals) || {}
|
||||
|
||||
// 聚合方式从用户配置的 s2Options.totals 取, 在触发前端兜底计算汇总逻辑时, 如果没有汇总的配置, 默认按 [求和] 计算,避免排序失效.
|
||||
const defaultAggregation =
|
||||
isEmpty(options?.totals) && !this.spreadsheet.isHierarchyTreeType() ? Aggregation.SUM : ''
|
||||
const calcAction = calcActionByType[aggregation || defaultAggregation]
|
||||
|
||||
// 前端计算汇总值
|
||||
if (calcAction || calcFunc) {
|
||||
const data = this.getMultiData(query, {
|
||||
queryType: QueryDataType.DetailOnly
|
||||
})
|
||||
let totalValue: number
|
||||
if (calcFunc) {
|
||||
totalValue = calcFunc(query, data, this.spreadsheet, status)
|
||||
} else if (calcAction) {
|
||||
totalValue = calcAction(data, VALUE_FIELD)
|
||||
}
|
||||
|
||||
return {
|
||||
...query,
|
||||
[VALUE_FIELD]: totalValue,
|
||||
[query[EXTRA_FIELD]]: totalValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 透视表
|
||||
*/
|
||||
@ -75,7 +125,7 @@ export class TablePivot extends S2ChartView<PivotSheet> {
|
||||
)
|
||||
|
||||
// fields
|
||||
const fields = chart.data.fields
|
||||
const { fields, customCalc } = chart.data
|
||||
if (!fields || fields.length === 0) {
|
||||
if (chartObj) {
|
||||
chartObj.destroy()
|
||||
@ -168,6 +218,11 @@ export class TablePivot extends S2ChartView<PivotSheet> {
|
||||
tableTotal.col.calcTotals,
|
||||
tableTotal.col.calcSubTotals
|
||||
]
|
||||
const axisMap = {
|
||||
row: chart.xAxis,
|
||||
col: chart.xAxisExt,
|
||||
quota: chart.yAxis
|
||||
}
|
||||
totals.forEach(total => {
|
||||
if (total.cfg?.length) {
|
||||
delete total.aggregation
|
||||
@ -175,8 +230,8 @@ export class TablePivot extends S2ChartView<PivotSheet> {
|
||||
p[n.dataeaseName] = n
|
||||
return p
|
||||
}, {})
|
||||
total.calcFunc = (query, data) => {
|
||||
return customCalcFunc(query, data, totalCfgMap)
|
||||
total.calcFunc = (query, data, _, status) => {
|
||||
return customCalcFunc(query, data, status, totalCfgMap, axisMap, customCalc)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -205,7 +260,8 @@ export class TablePivot extends S2ChartView<PivotSheet> {
|
||||
tooltip: {
|
||||
getContainer: () => containerDom
|
||||
},
|
||||
hierarchyType: basicStyle.tableLayoutMode ?? 'grid'
|
||||
hierarchyType: basicStyle.tableLayoutMode ?? 'grid',
|
||||
dataSet: spreadSheet => new CustomPivotDataset(spreadSheet)
|
||||
}
|
||||
|
||||
// tooltip
|
||||
@ -383,7 +439,7 @@ export class TablePivot extends S2ChartView<PivotSheet> {
|
||||
super('table-pivot', [])
|
||||
}
|
||||
}
|
||||
function customCalcFunc(query, data, totalCfgMap) {
|
||||
function customCalcFunc(query, data, status, totalCfgMap, axisMap, customCalc) {
|
||||
if (!data?.length || !query[EXTRA_FIELD]) {
|
||||
return 0
|
||||
}
|
||||
@ -412,6 +468,13 @@ function customCalcFunc(query, data, totalCfgMap) {
|
||||
})
|
||||
return result?.[query[EXTRA_FIELD]]
|
||||
}
|
||||
case 'CUSTOM': {
|
||||
const val = getCustomCalcResult(query, axisMap, status, customCalc || {})
|
||||
if (val === '') {
|
||||
return val
|
||||
}
|
||||
return parseFloat(val)
|
||||
}
|
||||
default: {
|
||||
return data.reduce((p, n) => {
|
||||
return p + parseFloat(n[query[EXTRA_FIELD]] ?? 0)
|
||||
@ -419,3 +482,233 @@ function customCalcFunc(query, data, totalCfgMap) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCustomCalcResult(query, axisMap, status: TotalStatus, customCalc) {
|
||||
const quotaField = query[EXTRA_FIELD]
|
||||
const { row, col } = axisMap
|
||||
// 行列交叉总计
|
||||
if (status.isRowTotal && status.isColTotal) {
|
||||
return customCalc.rowColTotal?.data?.[quotaField]
|
||||
}
|
||||
// 列总计
|
||||
if (status.isColTotal && !status.isRowSubTotal) {
|
||||
const { colTotal } = customCalc
|
||||
const path = getTreePath(query, row)
|
||||
let val
|
||||
if (path.length && colTotal) {
|
||||
path.push(quotaField)
|
||||
val = get(colTotal.data, path)
|
||||
}
|
||||
return val
|
||||
}
|
||||
// 列小计
|
||||
if (status.isColSubTotal && !status.isRowTotal && !status.isRowSubTotal) {
|
||||
const { colSubTotal } = customCalc
|
||||
const subLevel = getSubLevel(query, col)
|
||||
const rowPath = getTreePath(query, row)
|
||||
const colPath = getTreePath(query, col)
|
||||
const path = [...rowPath, ...colPath]
|
||||
const { data } = colSubTotal[subLevel]
|
||||
let val
|
||||
if (path.length && data) {
|
||||
path.push(quotaField)
|
||||
val = get(data, path)
|
||||
}
|
||||
return val
|
||||
}
|
||||
// 行总计
|
||||
if (status.isRowTotal && !status.isColSubTotal) {
|
||||
const { rowTotal } = customCalc
|
||||
const path = getTreePath(query, col)
|
||||
let val
|
||||
if (path.length && rowTotal) {
|
||||
path.push(quotaField)
|
||||
val = get(rowTotal.data, path)
|
||||
}
|
||||
return val
|
||||
}
|
||||
// 行小计
|
||||
if (status.isRowSubTotal && !status.isColTotal && !status.isColSubTotal) {
|
||||
const { rowSubTotal } = customCalc
|
||||
const rowLevel = getSubLevel(query, row)
|
||||
const colPath = getTreePath(query, col)
|
||||
const rowPath = getTreePath(query, row)
|
||||
const path = [...colPath, ...rowPath]
|
||||
const { data } = rowSubTotal[rowLevel]
|
||||
let val
|
||||
if (path.length && rowSubTotal) {
|
||||
path.push(quotaField)
|
||||
val = get(data, path)
|
||||
}
|
||||
return val
|
||||
}
|
||||
// 行总计里面的列小计
|
||||
if (status.isRowTotal && status.isColSubTotal) {
|
||||
const { colSubInRowTotal } = customCalc
|
||||
const colLevel = getSubLevel(query, col)
|
||||
const { data } = colSubInRowTotal[colLevel]
|
||||
const colPath = getTreePath(query, col)
|
||||
let val
|
||||
if (colPath.length && colSubInRowTotal) {
|
||||
colPath.push(quotaField)
|
||||
val = get(data, colPath)
|
||||
}
|
||||
return val
|
||||
}
|
||||
// 列总计里面的行小计
|
||||
if (status.isColTotal && status.isRowSubTotal) {
|
||||
const { rowSubInColTotal } = customCalc
|
||||
const rowSubLevel = getSubLevel(query, row)
|
||||
const data = rowSubInColTotal[rowSubLevel]?.data
|
||||
const path = getTreePath(query, row)
|
||||
let val
|
||||
if (path.length && rowSubInColTotal) {
|
||||
path.push(quotaField)
|
||||
val = get(data, path)
|
||||
}
|
||||
return val
|
||||
}
|
||||
// 列小计里面的行小计
|
||||
if (status.isColSubTotal && status.isRowSubTotal) {
|
||||
const { rowSubInColSub } = customCalc
|
||||
const rowSubLevel = getSubLevel(query, row)
|
||||
const colSubLevel = getSubLevel(query, col)
|
||||
const { data } = rowSubInColSub[rowSubLevel][colSubLevel]
|
||||
const rowPath = getTreePath(query, row)
|
||||
const colPath = getTreePath(query, col)
|
||||
const path = [...rowPath, ...colPath]
|
||||
let val
|
||||
if (path.length && rowSubInColSub) {
|
||||
path.push(quotaField)
|
||||
val = get(data, path)
|
||||
}
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
function getSubLevel(query, axis) {
|
||||
const fields: [] = axis.map(a => a.dataeaseName)
|
||||
let subLevel = -1
|
||||
const queryFields = keys(query)
|
||||
for (let i = fields.length - 1; i >= 0; i--) {
|
||||
const field = fields[i]
|
||||
const index = queryFields.findIndex(f => f === field)
|
||||
if (index !== -1) {
|
||||
subLevel++
|
||||
}
|
||||
}
|
||||
return subLevel
|
||||
}
|
||||
|
||||
function getTreePath(query, axis) {
|
||||
const path = []
|
||||
const fields = keys(query)
|
||||
axis.forEach(a => {
|
||||
const index = fields.findIndex(f => f === a.dataeaseName)
|
||||
if (index !== -1) {
|
||||
path.push(query[a.dataeaseName])
|
||||
}
|
||||
})
|
||||
return path
|
||||
}
|
||||
|
||||
function getAggregationAndCalcFuncByQuery(totalsStatus, totalsOptions) {
|
||||
const { isRowTotal, isRowSubTotal, isColTotal, isColSubTotal } = totalsStatus
|
||||
const { row, col } = totalsOptions || {}
|
||||
const { calcTotals: rowCalcTotals = {}, calcSubTotals: rowCalcSubTotals = {} } = row || {}
|
||||
const { calcTotals: colCalcTotals = {}, calcSubTotals: colCalcSubTotals = {} } = col || {}
|
||||
|
||||
const getCalcTotals = (dimensionTotals: CalcTotals, isTotal: boolean) => {
|
||||
if ((dimensionTotals.aggregation || dimensionTotals.calcFunc) && isTotal) {
|
||||
return {
|
||||
aggregation: dimensionTotals.aggregation,
|
||||
calcFunc: dimensionTotals.calcFunc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 优先级: 列总计/小计 > 行总计/小计
|
||||
return (
|
||||
getCalcTotals(colCalcTotals, isColTotal) ||
|
||||
getCalcTotals(colCalcSubTotals, isColSubTotal) ||
|
||||
getCalcTotals(rowCalcTotals, isRowTotal) ||
|
||||
getCalcTotals(rowCalcSubTotals, isRowSubTotal)
|
||||
)
|
||||
}
|
||||
|
||||
export const isNotNumber = (value: unknown) => {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isNaN(value)
|
||||
}
|
||||
if (!value) {
|
||||
return true
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return Number.isNaN(Number(value))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const processFieldValues = (data: DataItem[], field: string, filterIllegalValue = false) => {
|
||||
if (!data?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return data.reduce<Array<Decimal>>((resultArr, item) => {
|
||||
const fieldValue = get(item, field)
|
||||
const notNumber = isNotNumber(fieldValue)
|
||||
|
||||
if (filterIllegalValue && notNumber) {
|
||||
// 过滤非法值
|
||||
return resultArr
|
||||
}
|
||||
|
||||
const val = notNumber ? 0 : fieldValue
|
||||
resultArr.push(new Decimal(val))
|
||||
|
||||
return resultArr
|
||||
}, [])
|
||||
}
|
||||
|
||||
export const getDataSumByField = (data: DataItem[], field: string): number => {
|
||||
const fieldValues = processFieldValues(data, field)
|
||||
if (!fieldValues.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Decimal.sum(...fieldValues).toNumber()
|
||||
}
|
||||
|
||||
export const getDataExtremumByField = (
|
||||
method: 'min' | 'max',
|
||||
data: DataItem[],
|
||||
field: string
|
||||
): number => {
|
||||
// 防止预处理时默认值 0 影响极值结果,处理时需过滤非法值
|
||||
const fieldValues = processFieldValues(data, field, true)
|
||||
if (!fieldValues?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
return Decimal[method](...fieldValues).toNumber()
|
||||
}
|
||||
|
||||
export const getDataAvgByField = (data: DataItem[], field: string): number => {
|
||||
const fieldValues = processFieldValues(data, field)
|
||||
if (!fieldValues?.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return Decimal.sum(...fieldValues)
|
||||
.dividedBy(fieldValues.length)
|
||||
.toNumber()
|
||||
}
|
||||
|
||||
const calcActionByType: {
|
||||
[type in Aggregation]: (data: DataItem[], field: string) => number
|
||||
} = {
|
||||
[Aggregation.SUM]: getDataSumByField,
|
||||
[Aggregation.MIN]: (data, field) => getDataExtremumByField('min', data, field),
|
||||
[Aggregation.MAX]: (data, field) => getDataExtremumByField('max', data, field),
|
||||
[Aggregation.AVG]: getDataAvgByField
|
||||
}
|
||||
|
@ -173,8 +173,8 @@ const renderChart = (viewInfo: Chart, resetPageInfo: boolean) => {
|
||||
recursionTransObj(customStyleTrans, actualChart.customStyle, scale.value, terminal.value)
|
||||
|
||||
setupPage(actualChart, resetPageInfo)
|
||||
myChart?.facet.timer?.stop()
|
||||
myChart?.facet.cancelScrollFrame()
|
||||
myChart?.facet?.timer?.stop()
|
||||
myChart?.facet?.cancelScrollFrame()
|
||||
myChart?.destroy()
|
||||
myChart = null
|
||||
const chartView = chartViewManager.getChartView(
|
||||
@ -220,7 +220,7 @@ const setupPage = (chart: ChartObj, resetPageInfo?: boolean) => {
|
||||
}
|
||||
|
||||
const mouseMove = () => {
|
||||
myChart?.facet.timer?.stop()
|
||||
myChart?.facet?.timer?.stop()
|
||||
}
|
||||
|
||||
const mouseLeave = () => {
|
||||
@ -673,7 +673,7 @@ const tablePageClass = computed(() => {
|
||||
</div>
|
||||
<el-dialog v-model="state.imgEnlarge" append-to-body>
|
||||
<div class="enlarge-image">
|
||||
<img :src="state.imgSrc" />
|
||||
<img :src="state.imgSrc" style="width: 100%; height: 100%; object-fit: contain" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@ -728,7 +728,7 @@ const tablePageClass = computed(() => {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
package io.dataease.extensions.view.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class TableCalcTotal {
|
||||
private List<TableCalcTotalCfg> cfg;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package io.dataease.extensions.view.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TableCalcTotalCfg {
|
||||
private String dataeaseName;
|
||||
private String aggregation;
|
||||
private String originName;
|
||||
private int extField;
|
||||
private long chartId;
|
||||
private long datasetGroupId;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package io.dataease.extensions.view.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TableTotal {
|
||||
private TableTotalCfg row;
|
||||
private TableTotalCfg col;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package io.dataease.extensions.view.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TableTotalCfg {
|
||||
private boolean showGrandTotals;
|
||||
private boolean showSubTotals;
|
||||
private TableCalcTotal calcTotals;
|
||||
private TableCalcTotal calcSubTotals;
|
||||
}
|
Loading…
Reference in New Issue
Block a user