mirror of
https://github.com/dataease/dataease.git
synced 2025-02-24 19:42:56 +08:00
feat(图表): 透视表支持自定义汇总 #10997
This commit is contained in:
parent
898a016f44
commit
9fd8c36229
@ -1,11 +1,383 @@
|
|||||||
package io.dataease.chart.charts.impl.table;
|
package io.dataease.chart.charts.impl.table;
|
||||||
|
|
||||||
import io.dataease.chart.charts.impl.GroupChartHandler;
|
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 lombok.Getter;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class TablePivotHandler extends GroupChartHandler {
|
public class TablePivotHandler extends GroupChartHandler {
|
||||||
@Getter
|
@Getter
|
||||||
private String type = "table-pivot";
|
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);
|
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")) {
|
if (StringUtils.equalsIgnoreCase(y.getSummary(), "count_distinct")) {
|
||||||
fieldName = String.format(SQLConstants.AGG_FIELD, "COUNT", "DISTINCT " + cast);
|
fieldName = String.format(SQLConstants.AGG_FIELD, "COUNT", "DISTINCT " + cast);
|
||||||
|
} else if (y.getSummary() == null){
|
||||||
|
// 透视表自定义汇总不用聚合
|
||||||
|
fieldName = cast;
|
||||||
} else {
|
} else {
|
||||||
fieldName = String.format(SQLConstants.AGG_FIELD, y.getSummary(), cast);
|
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
|
dataeaseName: string
|
||||||
aggregation: 'MIN' | 'MAX' | 'AVG' | 'SUM' | ''
|
aggregation: 'MIN' | 'MAX' | 'AVG' | 'SUM' | 'CUSTOM' | ''
|
||||||
|
/**
|
||||||
|
* 自定义汇总表达式
|
||||||
|
*/
|
||||||
|
originName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,6 +32,7 @@ declare interface Chart {
|
|||||||
fields: ChartViewField[]
|
fields: ChartViewField[]
|
||||||
tableRow: []
|
tableRow: []
|
||||||
}
|
}
|
||||||
|
customCalc: any
|
||||||
}
|
}
|
||||||
xAxis?: Axis[]
|
xAxis?: Axis[]
|
||||||
xAxisExt?: 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>
|
<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 { useI18n } from '@/hooks/web/useI18n'
|
||||||
import { DEFAULT_TABLE_TOTAL } from '@/views/chart/components/editor/util/chart'
|
import { DEFAULT_TABLE_TOTAL } from '@/views/chart/components/editor/util/chart'
|
||||||
import { cloneDeep, defaultsDeep } from 'lodash-es'
|
import { cloneDeep, defaultsDeep } from 'lodash-es'
|
||||||
|
import CustomAggrEdit from './CustomAggrEdit.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@ -31,26 +32,18 @@ const aggregations = [
|
|||||||
{ name: t('chart.sum'), value: 'SUM' },
|
{ name: t('chart.sum'), value: 'SUM' },
|
||||||
{ name: t('chart.avg'), value: 'AVG' },
|
{ name: t('chart.avg'), value: 'AVG' },
|
||||||
{ name: t('chart.max'), value: 'MAX' },
|
{ 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({
|
const state = reactive({
|
||||||
tableTotalForm: cloneDeep(DEFAULT_TABLE_TOTAL) as ChartTableTotalAttr,
|
tableTotalForm: cloneDeep(DEFAULT_TABLE_TOTAL) as ChartTableTotalAttr,
|
||||||
rowSubTotalItem: {
|
rowSubTotalItem: {} as DeepPartial<CalcTotalCfg>,
|
||||||
dataeaseName: '',
|
rowTotalItem: {} as DeepPartial<CalcTotalCfg>,
|
||||||
aggregation: ''
|
colSubTotalItem: {} as DeepPartial<CalcTotalCfg>,
|
||||||
} as CalcTotalCfg,
|
colTotalItem: {} as DeepPartial<CalcTotalCfg>,
|
||||||
rowTotalItem: {
|
totalCfg: [] as CalcTotalCfg[],
|
||||||
dataeaseName: '',
|
totalCfgAttr: '',
|
||||||
aggregation: ''
|
totalItem: {} as DeepPartial<CalcTotalCfg>
|
||||||
} as CalcTotalCfg,
|
|
||||||
colSubTotalItem: {
|
|
||||||
dataeaseName: '',
|
|
||||||
aggregation: ''
|
|
||||||
} as CalcTotalCfg,
|
|
||||||
colTotalItem: {
|
|
||||||
dataeaseName: '',
|
|
||||||
aggregation: ''
|
|
||||||
} as CalcTotalCfg
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['onTableTotalChange'])
|
const emit = defineEmits(['onTableTotalChange'])
|
||||||
@ -86,7 +79,7 @@ const init = () => {
|
|||||||
totals.forEach(total => {
|
totals.forEach(total => {
|
||||||
setupTotalCfg(total.cfg, yAxis)
|
setupTotalCfg(total.cfg, yAxis)
|
||||||
})
|
})
|
||||||
const totalTupleArr: [CalcTotalCfg, CalcTotalCfg[]][] = [
|
const totalTupleArr: [DeepPartial<CalcTotalCfg>, CalcTotalCfg[]][] = [
|
||||||
[state.rowTotalItem, state.tableTotalForm.row.calcTotals.cfg],
|
[state.rowTotalItem, state.tableTotalForm.row.calcTotals.cfg],
|
||||||
[state.rowSubTotalItem, state.tableTotalForm.row.calcSubTotals.cfg],
|
[state.rowSubTotalItem, state.tableTotalForm.row.calcSubTotals.cfg],
|
||||||
[state.colTotalItem, state.tableTotalForm.col.calcTotals.cfg],
|
[state.colTotalItem, state.tableTotalForm.col.calcTotals.cfg],
|
||||||
@ -97,6 +90,7 @@ const init = () => {
|
|||||||
if (!totalCfg.length) {
|
if (!totalCfg.length) {
|
||||||
total.dataeaseName = ''
|
total.dataeaseName = ''
|
||||||
total.aggregation = ''
|
total.aggregation = ''
|
||||||
|
total.originName = ''
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const totalIndex = totalCfg.findIndex(i => i.dataeaseName === total.dataeaseName)
|
const totalIndex = totalCfg.findIndex(i => i.dataeaseName === total.dataeaseName)
|
||||||
@ -105,6 +99,7 @@ const init = () => {
|
|||||||
} else {
|
} else {
|
||||||
total.dataeaseName = totalCfg[0].dataeaseName
|
total.dataeaseName = totalCfg[0].dataeaseName
|
||||||
total.aggregation = totalCfg[0].aggregation
|
total.aggregation = totalCfg[0].aggregation
|
||||||
|
total.originName = totalCfg[0].originName
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -114,6 +109,7 @@ const changeTotal = (totalItem, totals) => {
|
|||||||
const item = totals[i]
|
const item = totals[i]
|
||||||
if (item.dataeaseName === totalItem.dataeaseName) {
|
if (item.dataeaseName === totalItem.dataeaseName) {
|
||||||
totalItem.aggregation = item.aggregation
|
totalItem.aggregation = item.aggregation
|
||||||
|
totalItem.originName = item.originName
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,6 +122,9 @@ const changeTotalAggr = (totalItem, totals, colOrNum) => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (totalItem.aggregation == 'CUSTOM' && !totalItem.originName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
changeTableTotal(colOrNum)
|
changeTableTotal(colOrNum)
|
||||||
}
|
}
|
||||||
const setupTotalCfg = (totalCfg, axis) => {
|
const setupTotalCfg = (totalCfg, axis) => {
|
||||||
@ -150,10 +149,51 @@ const setupTotalCfg = (totalCfg, axis) => {
|
|||||||
axis.forEach(i => {
|
axis.forEach(i => {
|
||||||
totalCfg.push({
|
totalCfg.push({
|
||||||
dataeaseName: i.dataeaseName,
|
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(() => {
|
onMounted(() => {
|
||||||
init()
|
init()
|
||||||
})
|
})
|
||||||
@ -232,7 +272,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="11" :offset="2">
|
<el-col :span="state.rowTotalItem.aggregation === 'CUSTOM' ? 8 : 11" :offset="2">
|
||||||
<el-select
|
<el-select
|
||||||
:effect="themes"
|
:effect="themes"
|
||||||
v-model="state.rowTotalItem.aggregation"
|
v-model="state.rowTotalItem.aggregation"
|
||||||
@ -253,6 +293,19 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-col>
|
</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>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
v-if="chart.type === 'table-pivot'"
|
v-if="chart.type === 'table-pivot'"
|
||||||
@ -360,7 +413,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="11" :offset="2">
|
<el-col :span="state.rowSubTotalItem.aggregation === 'CUSTOM' ? 8 : 11" :offset="2">
|
||||||
<el-select
|
<el-select
|
||||||
:effect="themes"
|
:effect="themes"
|
||||||
v-model="state.rowSubTotalItem.aggregation"
|
v-model="state.rowSubTotalItem.aggregation"
|
||||||
@ -382,6 +435,19 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-col>
|
</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>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -455,7 +521,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="11" :offset="2">
|
<el-col :span="state.colTotalItem.aggregation === 'CUSTOM' ? 8 : 11" :offset="2">
|
||||||
<el-select
|
<el-select
|
||||||
:effect="themes"
|
:effect="themes"
|
||||||
v-model="state.colTotalItem.aggregation"
|
v-model="state.colTotalItem.aggregation"
|
||||||
@ -476,6 +542,19 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-col>
|
</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>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
v-if="chart.type === 'table-pivot'"
|
v-if="chart.type === 'table-pivot'"
|
||||||
@ -582,7 +661,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="11" :offset="2">
|
<el-col :span="state.colSubTotalItem.aggregation === 'CUSTOM' ? 8 : 11" :offset="2">
|
||||||
<el-select
|
<el-select
|
||||||
:effect="themes"
|
:effect="themes"
|
||||||
v-model="state.colSubTotalItem.aggregation"
|
v-model="state.colSubTotalItem.aggregation"
|
||||||
@ -604,9 +683,35 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-col>
|
</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>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
@ -210,6 +210,7 @@ const filedList = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
provide('filedList', () => filedList.value)
|
provide('filedList', () => filedList.value)
|
||||||
|
provide('quota', () => state.quota)
|
||||||
watch(
|
watch(
|
||||||
[() => view.value['tableId']],
|
[() => 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 { formatterItem, valueFormatter } from '../../../formatter'
|
||||||
import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util'
|
import { hexColorToRGBA, isAlphaColor, parseJson } from '../../../util'
|
||||||
import { S2ChartView, S2DrawOptions } from '../../types/impl/s2'
|
import { S2ChartView, S2DrawOptions } from '../../types/impl/s2'
|
||||||
import { TABLE_EDITOR_PROPERTY_INNER } from './common'
|
import { TABLE_EDITOR_PROPERTY_INNER } from './common'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
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 { copyContent } from '../../common/common_table'
|
||||||
|
import Decimal from 'decimal.js'
|
||||||
|
|
||||||
|
type DataItem = Record<string, any>
|
||||||
|
|
||||||
const { t } = useI18n()
|
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
|
// fields
|
||||||
const fields = chart.data.fields
|
const { fields, customCalc } = chart.data
|
||||||
if (!fields || fields.length === 0) {
|
if (!fields || fields.length === 0) {
|
||||||
if (chartObj) {
|
if (chartObj) {
|
||||||
chartObj.destroy()
|
chartObj.destroy()
|
||||||
@ -168,6 +218,11 @@ export class TablePivot extends S2ChartView<PivotSheet> {
|
|||||||
tableTotal.col.calcTotals,
|
tableTotal.col.calcTotals,
|
||||||
tableTotal.col.calcSubTotals
|
tableTotal.col.calcSubTotals
|
||||||
]
|
]
|
||||||
|
const axisMap = {
|
||||||
|
row: chart.xAxis,
|
||||||
|
col: chart.xAxisExt,
|
||||||
|
quota: chart.yAxis
|
||||||
|
}
|
||||||
totals.forEach(total => {
|
totals.forEach(total => {
|
||||||
if (total.cfg?.length) {
|
if (total.cfg?.length) {
|
||||||
delete total.aggregation
|
delete total.aggregation
|
||||||
@ -175,8 +230,8 @@ export class TablePivot extends S2ChartView<PivotSheet> {
|
|||||||
p[n.dataeaseName] = n
|
p[n.dataeaseName] = n
|
||||||
return p
|
return p
|
||||||
}, {})
|
}, {})
|
||||||
total.calcFunc = (query, data) => {
|
total.calcFunc = (query, data, _, status) => {
|
||||||
return customCalcFunc(query, data, totalCfgMap)
|
return customCalcFunc(query, data, status, totalCfgMap, axisMap, customCalc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -205,7 +260,8 @@ export class TablePivot extends S2ChartView<PivotSheet> {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
getContainer: () => containerDom
|
getContainer: () => containerDom
|
||||||
},
|
},
|
||||||
hierarchyType: basicStyle.tableLayoutMode ?? 'grid'
|
hierarchyType: basicStyle.tableLayoutMode ?? 'grid',
|
||||||
|
dataSet: spreadSheet => new CustomPivotDataset(spreadSheet)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tooltip
|
// tooltip
|
||||||
@ -383,7 +439,7 @@ export class TablePivot extends S2ChartView<PivotSheet> {
|
|||||||
super('table-pivot', [])
|
super('table-pivot', [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function customCalcFunc(query, data, totalCfgMap) {
|
function customCalcFunc(query, data, status, totalCfgMap, axisMap, customCalc) {
|
||||||
if (!data?.length || !query[EXTRA_FIELD]) {
|
if (!data?.length || !query[EXTRA_FIELD]) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -412,6 +468,13 @@ function customCalcFunc(query, data, totalCfgMap) {
|
|||||||
})
|
})
|
||||||
return result?.[query[EXTRA_FIELD]]
|
return result?.[query[EXTRA_FIELD]]
|
||||||
}
|
}
|
||||||
|
case 'CUSTOM': {
|
||||||
|
const val = getCustomCalcResult(query, axisMap, status, customCalc || {})
|
||||||
|
if (val === '') {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return parseFloat(val)
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return data.reduce((p, n) => {
|
return data.reduce((p, n) => {
|
||||||
return p + parseFloat(n[query[EXTRA_FIELD]] ?? 0)
|
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)
|
recursionTransObj(customStyleTrans, actualChart.customStyle, scale.value, terminal.value)
|
||||||
|
|
||||||
setupPage(actualChart, resetPageInfo)
|
setupPage(actualChart, resetPageInfo)
|
||||||
myChart?.facet.timer?.stop()
|
myChart?.facet?.timer?.stop()
|
||||||
myChart?.facet.cancelScrollFrame()
|
myChart?.facet?.cancelScrollFrame()
|
||||||
myChart?.destroy()
|
myChart?.destroy()
|
||||||
myChart = null
|
myChart = null
|
||||||
const chartView = chartViewManager.getChartView(
|
const chartView = chartViewManager.getChartView(
|
||||||
@ -220,7 +220,7 @@ const setupPage = (chart: ChartObj, resetPageInfo?: boolean) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mouseMove = () => {
|
const mouseMove = () => {
|
||||||
myChart?.facet.timer?.stop()
|
myChart?.facet?.timer?.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
const mouseLeave = () => {
|
const mouseLeave = () => {
|
||||||
@ -673,7 +673,7 @@ const tablePageClass = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<el-dialog v-model="state.imgEnlarge" append-to-body>
|
<el-dialog v-model="state.imgEnlarge" append-to-body>
|
||||||
<div class="enlarge-image">
|
<div class="enlarge-image">
|
||||||
<img :src="state.imgSrc" />
|
<img :src="state.imgSrc" style="width: 100%; height: 100%; object-fit: contain" />
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@ -728,7 +728,7 @@ const tablePageClass = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
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