Merge branch 'dev-v2' into pr@dev-v2@feat_report_task_retry

This commit is contained in:
fit2cloud-chenyw 2024-07-09 12:21:24 +08:00
commit 0eb05103ed
43 changed files with 1678 additions and 38 deletions

View File

@ -0,0 +1,104 @@
package io.dataease.copilot.api;
import io.dataease.api.copilot.dto.ReceiveDTO;
import io.dataease.api.copilot.dto.SendDTO;
import io.dataease.copilot.dao.auto.entity.CoreCopilotConfig;
import io.dataease.copilot.dao.auto.mapper.CoreCopilotConfigMapper;
import io.dataease.exception.DEException;
import io.dataease.utils.HttpClientConfig;
import io.dataease.utils.HttpClientUtil;
import io.dataease.utils.JsonUtil;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.json.simple.JSONObject;
import org.springframework.stereotype.Component;
import java.util.Base64;
import java.util.Map;
/**
* @Author Junjun
*/
@Component
public class CopilotAPI {
public static final String TOKEN = "/auth/token/license";
public static final String FREE_TOKEN = "/auth/token/free";
public static final String API = "/copilot/v1";
public static final String CHART = "/generate-chart";
public static final String RATE_LIMIT = "/rate-limit";
@Resource
private CoreCopilotConfigMapper coreCopilotConfigMapper;
public String basicAuth(String userName, String password) {
String auth = userName + ":" + password;
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
return "Basic " + encodedAuth;
}
public String bearerAuth(String token) {
return "Bearer " + token;
}
public CoreCopilotConfig getConfig() {
CoreCopilotConfig coreCopilotConfig = coreCopilotConfigMapper.selectById(1);
coreCopilotConfig.setPwd(new String(Base64.getDecoder().decode(coreCopilotConfig.getPwd())));
return coreCopilotConfig;
}
public String getToken(String license) throws Exception {
String url = getConfig().getCopilotUrl() + TOKEN;
JSONObject json = new JSONObject();
json.put("licenseText", license);
HttpClientConfig httpClientConfig = new HttpClientConfig();
httpClientConfig.addHeader("Authorization", basicAuth(getConfig().getUsername(), getConfig().getPwd()));
String tokenJson = HttpClientUtil.post(url, json.toString(), httpClientConfig);
return (String) JsonUtil.parse(tokenJson, Map.class).get("accessToken");
}
public String getFreeToken() throws Exception {
String url = getConfig().getCopilotUrl() + FREE_TOKEN;
HttpClientConfig httpClientConfig = new HttpClientConfig();
httpClientConfig.addHeader("Authorization", basicAuth(getConfig().getUsername(), getConfig().getPwd()));
String tokenJson = HttpClientUtil.post(url, "", httpClientConfig);
return (String) JsonUtil.parse(tokenJson, Map.class).get("accessToken");
}
public ReceiveDTO generateChart(String token, SendDTO sendDTO) {
String url = getConfig().getCopilotUrl() + API + CHART;
String request = (String) JsonUtil.toJSONString(sendDTO);
HttpClientConfig httpClientConfig = new HttpClientConfig();
httpClientConfig.addHeader("Authorization", bearerAuth(token));
String result = HttpClientUtil.post(url, request, httpClientConfig);
return JsonUtil.parseObject(result, ReceiveDTO.class);
}
public void checkRateLimit(String token) {
String url = getConfig().getCopilotUrl() + API + RATE_LIMIT;
HttpClientConfig httpClientConfig = new HttpClientConfig();
httpClientConfig.addHeader("Authorization", bearerAuth(token));
HttpResponse httpResponse = HttpClientUtil.postWithHeaders(url, null, httpClientConfig);
Header[] allHeaders = httpResponse.getAllHeaders();
String limit = "";
String seconds = "";
for (Header header : allHeaders) {
if (StringUtils.equalsIgnoreCase(header.getName(), "x-rate-limit-remaining")) {
limit = header.getValue();
}
if (StringUtils.equalsIgnoreCase(header.getName(), "x-rate-limit-retry-after-seconds")) {
seconds = header.getValue();
}
}
if (Long.parseLong(limit) <= 0) {
DEException.throwException(String.format("当前请求频率已达上限,请在%s秒后重试", seconds));
}
}
}

View File

@ -0,0 +1,71 @@
package io.dataease.copilot.dao.auto.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
/**
* <p>
*
* </p>
*
* @author fit2cloud
* @since 2024-07-08
*/
@TableName("core_copilot_config")
public class CoreCopilotConfig implements Serializable {
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
private String copilotUrl;
private String username;
private String pwd;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCopilotUrl() {
return copilotUrl;
}
public void setCopilotUrl(String copilotUrl) {
this.copilotUrl = copilotUrl;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
@Override
public String toString() {
return "CoreCopilotConfig{" +
"id = " + id +
", copilotUrl = " + copilotUrl +
", username = " + username +
", pwd = " + pwd +
"}";
}
}

View File

@ -0,0 +1,276 @@
package io.dataease.copilot.dao.auto.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
/**
* <p>
*
* </p>
*
* @author fit2cloud
* @since 2024-07-04
*/
@TableName("core_copilot_msg")
public class CoreCopilotMsg implements Serializable {
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 数据集ID
*/
private Long datasetGroupId;
/**
* user or api
*/
private String msgType;
/**
* mysql oracle ...
*/
private String engineType;
/**
* create sql
*/
private String schemaSql;
/**
* 用户提问
*/
private String question;
/**
* 历史信息
*/
private String history;
/**
* copilot 返回 sql
*/
private String copilotSql;
/**
* copilot 返回信息
*/
private String apiMsg;
/**
* sql 状态
*/
private Integer sqlOk;
/**
* chart 状态
*/
private Integer chartOk;
/**
* chart 内容
*/
private String chart;
/**
* 视图数据
*/
private String chartData;
/**
* 执行请求的SQL
*/
private String execSql;
/**
* msg状态0失败 1成功
*/
private Integer msgStatus;
/**
* de错误信息
*/
private String errMsg;
/**
* 创建时间
*/
private Long createTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public Long getDatasetGroupId() {
return datasetGroupId;
}
public void setDatasetGroupId(Long datasetGroupId) {
this.datasetGroupId = datasetGroupId;
}
public String getMsgType() {
return msgType;
}
public void setMsgType(String msgType) {
this.msgType = msgType;
}
public String getEngineType() {
return engineType;
}
public void setEngineType(String engineType) {
this.engineType = engineType;
}
public String getSchemaSql() {
return schemaSql;
}
public void setSchemaSql(String schemaSql) {
this.schemaSql = schemaSql;
}
public String getQuestion() {
return question;
}
public void setQuestion(String question) {
this.question = question;
}
public String getHistory() {
return history;
}
public void setHistory(String history) {
this.history = history;
}
public String getCopilotSql() {
return copilotSql;
}
public void setCopilotSql(String copilotSql) {
this.copilotSql = copilotSql;
}
public String getApiMsg() {
return apiMsg;
}
public void setApiMsg(String apiMsg) {
this.apiMsg = apiMsg;
}
public Integer getSqlOk() {
return sqlOk;
}
public void setSqlOk(Integer sqlOk) {
this.sqlOk = sqlOk;
}
public Integer getChartOk() {
return chartOk;
}
public void setChartOk(Integer chartOk) {
this.chartOk = chartOk;
}
public String getChart() {
return chart;
}
public void setChart(String chart) {
this.chart = chart;
}
public String getChartData() {
return chartData;
}
public void setChartData(String chartData) {
this.chartData = chartData;
}
public String getExecSql() {
return execSql;
}
public void setExecSql(String execSql) {
this.execSql = execSql;
}
public Integer getMsgStatus() {
return msgStatus;
}
public void setMsgStatus(Integer msgStatus) {
this.msgStatus = msgStatus;
}
public String getErrMsg() {
return errMsg;
}
public void setErrMsg(String errMsg) {
this.errMsg = errMsg;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "CoreCopilotMsg{" +
"id = " + id +
", userId = " + userId +
", datasetGroupId = " + datasetGroupId +
", msgType = " + msgType +
", engineType = " + engineType +
", schemaSql = " + schemaSql +
", question = " + question +
", history = " + history +
", copilotSql = " + copilotSql +
", apiMsg = " + apiMsg +
", sqlOk = " + sqlOk +
", chartOk = " + chartOk +
", chart = " + chart +
", chartData = " + chartData +
", execSql = " + execSql +
", msgStatus = " + msgStatus +
", errMsg = " + errMsg +
", createTime = " + createTime +
"}";
}
}

View File

@ -0,0 +1,74 @@
package io.dataease.copilot.dao.auto.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
/**
* <p>
*
* </p>
*
* @author fit2cloud
* @since 2024-07-08
*/
@TableName("core_copilot_token")
public class CoreCopilotToken implements Serializable {
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* free or license
*/
private String type;
private String token;
private Long updateTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Long getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Long updateTime) {
this.updateTime = updateTime;
}
@Override
public String toString() {
return "CoreCopilotToken{" +
"id = " + id +
", type = " + type +
", token = " + token +
", updateTime = " + updateTime +
"}";
}
}

View File

@ -0,0 +1,18 @@
package io.dataease.copilot.dao.auto.mapper;
import io.dataease.copilot.dao.auto.entity.CoreCopilotConfig;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author fit2cloud
* @since 2024-07-08
*/
@Mapper
public interface CoreCopilotConfigMapper extends BaseMapper<CoreCopilotConfig> {
}

View File

@ -0,0 +1,18 @@
package io.dataease.copilot.dao.auto.mapper;
import io.dataease.copilot.dao.auto.entity.CoreCopilotMsg;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author fit2cloud
* @since 2024-07-04
*/
@Mapper
public interface CoreCopilotMsgMapper extends BaseMapper<CoreCopilotMsg> {
}

View File

@ -0,0 +1,18 @@
package io.dataease.copilot.dao.auto.mapper;
import io.dataease.copilot.dao.auto.entity.CoreCopilotToken;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author fit2cloud
* @since 2024-07-08
*/
@Mapper
public interface CoreCopilotTokenMapper extends BaseMapper<CoreCopilotToken> {
}

View File

@ -0,0 +1,377 @@
package io.dataease.copilot.manage;
import com.fasterxml.jackson.core.type.TypeReference;
import io.dataease.api.copilot.dto.DESendDTO;
import io.dataease.api.copilot.dto.MsgDTO;
import io.dataease.api.copilot.dto.ReceiveDTO;
import io.dataease.api.copilot.dto.TokenDTO;
import io.dataease.api.dataset.union.DatasetGroupInfoDTO;
import io.dataease.api.dataset.union.UnionDTO;
import io.dataease.chart.utils.ChartDataBuild;
import io.dataease.copilot.api.CopilotAPI;
import io.dataease.dataset.dao.auto.entity.CoreDatasetGroup;
import io.dataease.dataset.dao.auto.mapper.CoreDatasetGroupMapper;
import io.dataease.dataset.manage.DatasetDataManage;
import io.dataease.dataset.manage.DatasetSQLManage;
import io.dataease.dataset.manage.DatasetTableFieldManage;
import io.dataease.dataset.manage.PermissionManage;
import io.dataease.engine.constant.DeTypeConstants;
import io.dataease.engine.utils.Utils;
import io.dataease.exception.DEException;
import io.dataease.extensions.datasource.constant.SqlPlaceholderConstants;
import io.dataease.extensions.datasource.dto.DatasetTableFieldDTO;
import io.dataease.extensions.datasource.dto.DatasourceRequest;
import io.dataease.extensions.datasource.dto.DatasourceSchemaDTO;
import io.dataease.extensions.datasource.dto.TableField;
import io.dataease.extensions.datasource.factory.ProviderFactory;
import io.dataease.extensions.datasource.provider.Provider;
import io.dataease.extensions.view.dto.ColumnPermissionItem;
import io.dataease.i18n.Translator;
import io.dataease.license.dao.po.LicensePO;
import io.dataease.license.manage.F2CLicManage;
import io.dataease.license.utils.LicenseUtil;
import io.dataease.utils.AuthUtils;
import io.dataease.utils.BeanUtils;
import io.dataease.utils.JsonUtil;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Author Junjun
*/
@Component
public class CopilotManage {
@Resource
private DatasetSQLManage datasetSQLManage;
@Resource
private CoreDatasetGroupMapper coreDatasetGroupMapper;
@Resource
private DatasetTableFieldManage datasetTableFieldManage;
@Resource
private DatasetDataManage datasetDataManage;
@Resource
private PermissionManage permissionManage;
@Resource
private MsgManage msgManage;
private static Logger logger = LoggerFactory.getLogger(CopilotManage.class);
@Resource
private TokenManage tokenManage;
@Resource
private CopilotAPI copilotAPI;
@Resource
private F2CLicManage f2CLicManage;
public MsgDTO chat(MsgDTO msgDTO) throws Exception {
CoreDatasetGroup coreDatasetGroup = coreDatasetGroupMapper.selectById(msgDTO.getDatasetGroupId());
if (coreDatasetGroup == null) {
return null;
}
DatasetGroupInfoDTO dto = new DatasetGroupInfoDTO();
BeanUtils.copyBean(dto, coreDatasetGroup);
dto.setUnionSql(null);
List<UnionDTO> unionDTOList = JsonUtil.parseList(coreDatasetGroup.getInfo(), new TypeReference<>() {
});
dto.setUnion(unionDTOList);
// 获取field
List<DatasetTableFieldDTO> dsFields = datasetTableFieldManage.selectByDatasetGroupId(msgDTO.getDatasetGroupId());
List<DatasetTableFieldDTO> allFields = dsFields.stream().filter(ele -> ele.getExtField() == 0)
.map(ele -> {
DatasetTableFieldDTO datasetTableFieldDTO = new DatasetTableFieldDTO();
BeanUtils.copyBean(datasetTableFieldDTO, ele);
datasetTableFieldDTO.setFieldShortName(ele.getDataeaseName());
return datasetTableFieldDTO;
}).collect(Collectors.toList());
Map<String, Object> sqlMap = datasetSQLManage.getUnionSQLForEdit(dto, null);
String sql = (String) sqlMap.get("sql");// 数据集原始SQL
Map<Long, DatasourceSchemaDTO> dsMap = (Map<Long, DatasourceSchemaDTO>) sqlMap.get("dsMap");
boolean crossDs = Utils.isCrossDs(dsMap);
if (crossDs) {
DEException.throwException("跨源数据集不支持该功能");
}
// 调用copilot service 获取SQL和chart struct将返回SQL中表名替换成数据集SQL
// deSendDTO 构建schema和engine
if (ObjectUtils.isEmpty(dsMap)) {
DEException.throwException("No datasource");
}
DatasourceSchemaDTO ds = dsMap.entrySet().iterator().next().getValue();
String type = ds.getType();// 数据库类型如mysqloracle等可能需要映射成copilot需要的类型
datasetDataManage.buildFieldName(sqlMap, allFields);
List<String> strings = transCreateTableFields(allFields);
String createSql = "CREATE TABLE de_tmp_table (" + StringUtils.join(strings, ",") + ")";
logger.info("Copilot Schema SQL: " + createSql);
// PerMsgDTO perMsgDTO = new PerMsgDTO();
msgDTO.setDatasetGroupId(dto.getId());
msgDTO.setMsgType("user");
msgDTO.setEngineType(type);
msgDTO.setSchemaSql(createSql);
msgDTO.setHistory(msgDTO.getHistory());
msgDTO.setMsgStatus(1);
msgManage.save(msgDTO);// 到这里为止提问所需参数构建完毕往数据库插入一条提问记录
DESendDTO deSendDTO = new DESendDTO();
deSendDTO.setDatasetGroupId(dto.getId());
deSendDTO.setQuestion(msgDTO.getQuestion());
deSendDTO.setHistory(msgDTO.getHistory());
deSendDTO.setEngine(type);
deSendDTO.setSchema(createSql);
// do copilot chat
ReceiveDTO receiveDTO = copilotChat(deSendDTO);
// copilot 请求结束继续de获取数据
// 获取数据集相关行列权限最终再套一层SQL
Map<String, ColumnPermissionItem> desensitizationList = new HashMap<>();
allFields = permissionManage.filterColumnPermissions(allFields, desensitizationList, dto.getId(), null);
if (ObjectUtils.isEmpty(allFields)) {
DEException.throwException(Translator.get("i18n_no_column_permission"));
}
List<String> dsList = new ArrayList<>();
for (Map.Entry<Long, DatasourceSchemaDTO> next : dsMap.entrySet()) {
dsList.add(next.getValue().getType());
}
boolean needOrder = Utils.isNeedOrder(dsList);
if (!crossDs) {
sql = Utils.replaceSchemaAlias(sql, dsMap);
}
Provider provider;
if (crossDs) {
provider = ProviderFactory.getDefaultProvider();
} else {
provider = ProviderFactory.getProvider(dsList.getFirst());
}
// List<DataSetRowPermissionsTreeDTO> rowPermissionsTree = new ArrayList<>();
// TokenUserBO user = AuthUtils.getUser();
// if (user != null) {
// rowPermissionsTree = permissionManage.getRowPermissionsTree(dto.getId(), user.getUserId());
// }
// build query sql
// SQLMeta sqlMeta = new SQLMeta();
// Table2SQLObj.table2sqlobj(sqlMeta, null, "(" + sql + ")", crossDs);
// Field2SQLObj.field2sqlObj(sqlMeta, allFields, allFields, crossDs, dsMap);
// WhereTree2Str.transFilterTrees(sqlMeta, rowPermissionsTree, allFields, crossDs, dsMap);
// Order2SQLObj.getOrders(sqlMeta, dto.getSortFields(), allFields, crossDs, dsMap);
// String querySQL = SQLProvider.createQuerySQL(sqlMeta, false, false, needOrder);
// querySQL = provider.rebuildSQL(querySQL, sqlMeta, crossDs, dsMap);
// logger.info("preview sql: " + querySQL);
// todo test
String querySQL = sql;
String copilotSQL = receiveDTO.getSql();
// 通过数据源请求数据
// 调用数据源的calcite获得data
DatasourceRequest datasourceRequest = new DatasourceRequest();
datasourceRequest.setDsList(dsMap);
String s = "";
Map<String, Object> data;
try {
s = copilotSQL
.replaceAll(SqlPlaceholderConstants.KEYWORD_PREFIX_REGEX + "de_tmp_table" + SqlPlaceholderConstants.KEYWORD_SUFFIX_REGEX, "(" + querySQL + ")")
.replaceAll(SqlPlaceholderConstants.KEYWORD_PREFIX_REGEX + "DE_TMP_TABLE" + SqlPlaceholderConstants.KEYWORD_SUFFIX_REGEX, "(" + querySQL + ")");
logger.info("copilot sql: " + s);
datasourceRequest.setQuery(s);
data = provider.fetchResultField(datasourceRequest);
} catch (Exception e) {
try {
s = copilotSQL
.replaceAll(SqlPlaceholderConstants.KEYWORD_PREFIX_REGEX + "de_tmp_table" + SqlPlaceholderConstants.KEYWORD_SUFFIX_REGEX, "(" + querySQL + ") tmp")
.replaceAll(SqlPlaceholderConstants.KEYWORD_PREFIX_REGEX + "DE_TMP_TABLE" + SqlPlaceholderConstants.KEYWORD_SUFFIX_REGEX, "(" + querySQL + ") tmp");
logger.info("copilot sql: " + s);
datasourceRequest.setQuery(s);
data = provider.fetchResultField(datasourceRequest);
} catch (Exception e1) {
// 如果异常则获取最后一条成功的history
MsgDTO lastSuccessMsg = msgManage.getLastSuccessMsg(AuthUtils.getUser().getUserId(), dto.getId());
// 请求数据出错记录错误信息和copilot返回的信息
MsgDTO result = new MsgDTO();
result.setDatasetGroupId(dto.getId());
result.setMsgType("api");
result.setHistory(lastSuccessMsg == null ? new ArrayList<>() : lastSuccessMsg.getHistory());
result.setCopilotSql(receiveDTO.getSql());
result.setApiMsg(receiveDTO.getApiMessage());
result.setSqlOk(receiveDTO.getSqlOk() ? 1 : 0);
result.setChartOk(receiveDTO.getChartOk() ? 1 : 0);
result.setChart(receiveDTO.getChart());
result.setExecSql(s);
result.setMsgStatus(0);
result.setErrMsg(e1.getMessage());
msgManage.save(result);
return result;
}
}
List<TableField> fields = (List<TableField>) data.get("fields");
Map<String, Object> map = new LinkedHashMap<>();
// 重新构造data
Map<String, Object> previewData = buildPreviewData(data, fields, desensitizationList);
map.put("data", previewData);
// map.put("allFields", allFields);// map.data 中包含了fields和data
// if (ObjectUtils.isEmpty(dto.getId())) {
// map.put("allFields", allFields);
// } else {
// List<DatasetTableFieldDTO> fieldList = datasetTableFieldManage.selectByDatasetGroupId(dto.getId());
// map.put("allFields", fieldList);
// }
map.put("sql", Base64.getEncoder().encodeToString(s.getBytes()));
MsgDTO result = new MsgDTO();
result.setDatasetGroupId(dto.getId());
result.setMsgType("api");
result.setHistory(receiveDTO.getHistory());
result.setCopilotSql(receiveDTO.getSql());
result.setApiMsg(receiveDTO.getApiMessage());
result.setSqlOk(receiveDTO.getSqlOk() ? 1 : 0);
result.setChartOk(receiveDTO.getChartOk() ? 1 : 0);
result.setChart(receiveDTO.getChart());
result.setChartData(map);
result.setExecSql(s);
result.setMsgStatus(1);
msgManage.save(result);
return result;
}
public ReceiveDTO copilotChat(DESendDTO deSendDTO) throws Exception {
boolean valid = LicenseUtil.licenseValid();
// call copilot service
TokenDTO tokenDTO = tokenManage.getToken(valid);
ReceiveDTO receiveDTO;
if (StringUtils.isEmpty(tokenDTO.getToken())) {
// get token
String token;
if (valid) {
LicensePO read = f2CLicManage.read();
token = copilotAPI.getToken(read.getLicense());
} else {
token = copilotAPI.getFreeToken();
}
tokenManage.updateToken(token, valid);
receiveDTO = copilotAPI.generateChart(token, deSendDTO);
} else {
try {
receiveDTO = copilotAPI.generateChart(tokenDTO.getToken(), deSendDTO);
} catch (Exception e) {
// error, get token again
String token;
if (valid) {
LicensePO read = f2CLicManage.read();
token = copilotAPI.getToken(read.getLicense());
} else {
token = copilotAPI.getFreeToken();
}
tokenManage.updateToken(token, valid);
receiveDTO = copilotAPI.generateChart(token, deSendDTO);
}
}
if (!receiveDTO.getSqlOk() || !receiveDTO.getChartOk()) {
DEException.throwException((String) JsonUtil.toJSONString(receiveDTO));
}
logger.info("Copilot Service SQL: " + receiveDTO.getSql());
logger.info("Copilot Service Chart: " + JsonUtil.toJSONString(receiveDTO.getChart()));
return receiveDTO;
}
public List<MsgDTO> getList(Long userId) {
MsgDTO lastMsg = msgManage.getLastMsg(userId);
if (lastMsg == null) {
return null;
}
List<MsgDTO> msg = msgManage.getMsg(lastMsg);
msgManage.deleteMsg(lastMsg);
return msg;
}
public void clearAll(Long userId) {
msgManage.clearAllUserMsg(userId);
}
public MsgDTO errorMsg(MsgDTO msgDTO, String errMsg) throws Exception {
// 如果异常则获取最后一条成功的history
MsgDTO lastSuccessMsg = msgManage.getLastSuccessMsg(AuthUtils.getUser().getUserId(), msgDTO.getDatasetGroupId());
MsgDTO dto = new MsgDTO();
dto.setDatasetGroupId(msgDTO.getDatasetGroupId());
dto.setHistory(lastSuccessMsg == null ? new ArrayList<>() : lastSuccessMsg.getHistory());
dto.setMsgStatus(0);
dto.setMsgType("api");
dto.setErrMsg(errMsg);
msgManage.save(dto);
return dto;
}
public Map<String, Object> buildPreviewData(Map<String, Object> data, List<TableField> fields, Map<String, ColumnPermissionItem> desensitizationList) {
Map<String, Object> map = new LinkedHashMap<>();
List<String[]> dataList = (List<String[]>) data.get("data");
List<LinkedHashMap<String, Object>> dataObjectList = new ArrayList<>();
if (ObjectUtils.isNotEmpty(dataList)) {
for (int i = 0; i < dataList.size(); i++) {
String[] row = dataList.get(i);
LinkedHashMap<String, Object> obj = new LinkedHashMap<>();
if (row.length > 0) {
for (int j = 0; j < fields.size(); j++) {
TableField tableField = fields.get(j);
if (desensitizationList.containsKey(tableField.getOriginName())) {
obj.put(tableField.getOriginName(), ChartDataBuild.desensitizationValue(desensitizationList.get(tableField.getOriginName()), String.valueOf(row[j])));
} else {
if (tableField.getDeExtractType() == DeTypeConstants.DE_INT
|| tableField.getDeExtractType() == DeTypeConstants.DE_FLOAT
|| tableField.getDeExtractType() == DeTypeConstants.DE_BOOL) {
try {
obj.put(tableField.getOriginName(), new BigDecimal(row[j]));
} catch (Exception e) {
obj.put(tableField.getOriginName(), row[j]);
}
} else {
obj.put(tableField.getOriginName(), row[j]);
}
}
}
}
dataObjectList.add(obj);
}
}
map.put("fields", fields);
map.put("data", dataObjectList);
return map;
}
public List<String> transCreateTableFields(List<DatasetTableFieldDTO> allFields) {
List<String> list = new ArrayList<>();
for (DatasetTableFieldDTO dto : allFields) {
list.add(" " + dto.getDataeaseName() + " " + transNum2Type(dto.getDeExtractType()) +
" COMMENT '" + dto.getName() + "'");
}
return list;
}
public String transNum2Type(Integer num) {
return switch (num) {
case 0, 1, 5 -> "VARCHAR(50)";
case 2, 3, 4 -> "INT(10)";
default -> "VARCHAR(50)";
};
}
}

View File

@ -0,0 +1,100 @@
package io.dataease.copilot.manage;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.dataease.api.copilot.dto.ChartDTO;
import io.dataease.api.copilot.dto.HistoryDTO;
import io.dataease.api.copilot.dto.MsgDTO;
import io.dataease.copilot.dao.auto.entity.CoreCopilotMsg;
import io.dataease.copilot.dao.auto.mapper.CoreCopilotMsgMapper;
import io.dataease.utils.AuthUtils;
import io.dataease.utils.BeanUtils;
import io.dataease.utils.IDUtils;
import io.dataease.utils.JsonUtil;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @Author Junjun
*/
@Component
public class MsgManage {
@Resource
private CoreCopilotMsgMapper coreCopilotMsgMapper;
private ObjectMapper objectMapper = new ObjectMapper();
public void save(MsgDTO msgDTO) throws Exception {
msgDTO.setId(IDUtils.snowID());
msgDTO.setCreateTime(System.currentTimeMillis());
msgDTO.setUserId(AuthUtils.getUser().getUserId());
coreCopilotMsgMapper.insert(transDTO(msgDTO));
}
public List<MsgDTO> getMsg(MsgDTO msgDTO) {
QueryWrapper<CoreCopilotMsg> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", msgDTO.getUserId());
wrapper.eq("dataset_group_id", msgDTO.getDatasetGroupId());
wrapper.orderByAsc("create_time");
List<CoreCopilotMsg> perCopilotMsgs = coreCopilotMsgMapper.selectList(wrapper);
return perCopilotMsgs.stream().map(this::transRecord).toList();
}
public void deleteMsg(MsgDTO msgDTO) {
QueryWrapper<CoreCopilotMsg> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", msgDTO.getUserId());
wrapper.ne("dataset_group_id", msgDTO.getDatasetGroupId());
coreCopilotMsgMapper.delete(wrapper);
}
public void clearAllUserMsg(Long userId) {
QueryWrapper<CoreCopilotMsg> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
coreCopilotMsgMapper.delete(wrapper);
}
public MsgDTO getLastMsg(Long userId) {
QueryWrapper<CoreCopilotMsg> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
wrapper.orderByDesc("create_time");
List<CoreCopilotMsg> perCopilotMsgs = coreCopilotMsgMapper.selectList(wrapper);
return ObjectUtils.isEmpty(perCopilotMsgs) ? null : transRecord(perCopilotMsgs.getFirst());
}
public MsgDTO getLastSuccessMsg(Long userId, Long datasetGroupId) {
QueryWrapper<CoreCopilotMsg> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
wrapper.eq("dataset_group_id", datasetGroupId);
wrapper.eq("msg_status", 1);
wrapper.eq("msg_type", "api");
wrapper.orderByDesc("create_time");
List<CoreCopilotMsg> perCopilotMsgs = coreCopilotMsgMapper.selectList(wrapper);
return ObjectUtils.isEmpty(perCopilotMsgs) ? null : transRecord(perCopilotMsgs.getFirst());
}
private CoreCopilotMsg transDTO(MsgDTO dto) throws Exception {
CoreCopilotMsg record = new CoreCopilotMsg();
BeanUtils.copyBean(record, dto);
record.setHistory(dto.getHistory() == null ? null : objectMapper.writeValueAsString(dto.getHistory()));
record.setChart(dto.getChart() == null ? null : objectMapper.writeValueAsString(dto.getChart()));
record.setChartData(dto.getChartData() == null ? null : objectMapper.writeValueAsString(dto.getChartData()));
return record;
}
private MsgDTO transRecord(CoreCopilotMsg record) {
MsgDTO dto = new MsgDTO();
BeanUtils.copyBean(dto, record);
TypeReference<List<HistoryDTO>> tokenType = new TypeReference<>() {
};
dto.setHistory(record.getHistory() == null ? new ArrayList<>() : JsonUtil.parseList(record.getHistory(), tokenType));
dto.setChart(record.getChart() == null ? null : JsonUtil.parseObject(record.getChart(), ChartDTO.class));
dto.setChartData(record.getChartData() == null ? null : JsonUtil.parse(record.getChartData(), Map.class));
return dto;
}
}

View File

@ -0,0 +1,36 @@
package io.dataease.copilot.manage;
import io.dataease.api.copilot.dto.TokenDTO;
import io.dataease.copilot.dao.auto.entity.CoreCopilotToken;
import io.dataease.copilot.dao.auto.mapper.CoreCopilotTokenMapper;
import io.dataease.utils.BeanUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
/**
* @Author Junjun
*/
@Component
public class TokenManage {
@Resource
private CoreCopilotTokenMapper coreCopilotTokenMapper;
public TokenDTO getToken(boolean valid) {
CoreCopilotToken perCopilotToken = coreCopilotTokenMapper.selectById(valid ? 2 : 1);
return transRecord(perCopilotToken);
}
public void updateToken(String token, boolean valid) {
CoreCopilotToken record = new CoreCopilotToken();
record.setId(valid ? 2L : 1L);
record.setToken(token);
record.setUpdateTime(System.currentTimeMillis());
coreCopilotTokenMapper.updateById(record);
}
private TokenDTO transRecord(CoreCopilotToken perCopilotToken) {
TokenDTO dto = new TokenDTO();
BeanUtils.copyBean(dto, perCopilotToken);
return dto;
}
}

View File

@ -0,0 +1,40 @@
package io.dataease.copilot.service;
import io.dataease.api.copilot.CopilotApi;
import io.dataease.api.copilot.dto.MsgDTO;
import io.dataease.copilot.manage.CopilotManage;
import io.dataease.utils.AuthUtils;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @Author Junjun
*/
@RestController
@RequestMapping("copilot")
public class CopilotService implements CopilotApi {
@Resource
private CopilotManage copilotManage;
@Override
public MsgDTO chat(MsgDTO msgDTO) throws Exception {
try {
return copilotManage.chat(msgDTO);
} catch (Exception e) {
return copilotManage.errorMsg(msgDTO, e.getMessage());
}
}
@Override
public List<MsgDTO> getList() throws Exception {
return copilotManage.getList(AuthUtils.getUser().getUserId());
}
@Override
public void clearAll() throws Exception {
copilotManage.clearAll(AuthUtils.getUser().getUserId());
}
}

View File

@ -195,15 +195,22 @@ public class PermissionManage {
DatasetRowPermissionsTreeObj tree = JsonUtil.parseObject(record.getExpressionTree(), DatasetRowPermissionsTreeObj.class);
List<DatasetRowPermissionsTreeItem> items = new ArrayList<>();
for (DatasetRowPermissionsTreeItem datasetRowPermissionsTreeItem : tree.getItems()) {
if (StringUtils.isNotEmpty(userEntity.getAccount()) && datasetRowPermissionsTreeItem.getValue().equalsIgnoreCase("\\$\\{sysParams\\.userId}")) {
if (StringUtils.isNotEmpty(userEntity.getAccount()) && datasetRowPermissionsTreeItem.getValue().equalsIgnoreCase("${sysParams.userId}")) {
datasetRowPermissionsTreeItem.setValue(userEntity.getAccount());
items.add(datasetRowPermissionsTreeItem);
continue;
}
if (StringUtils.isNotEmpty(userEntity.getEmail()) && datasetRowPermissionsTreeItem.getValue().equalsIgnoreCase("\\$\\{sysParams\\.userEmail}")) {
if (StringUtils.isNotEmpty(userEntity.getEmail()) && datasetRowPermissionsTreeItem.getValue().equalsIgnoreCase("${sysParams.userEmail}")) {
datasetRowPermissionsTreeItem.setValue(userEntity.getEmail());
items.add(datasetRowPermissionsTreeItem);
continue;
}
if (StringUtils.isNotEmpty(userEntity.getName()) && datasetRowPermissionsTreeItem.getValue().equalsIgnoreCase("\\$\\{sysParams\\.userName}")) {
if (StringUtils.isNotEmpty(userEntity.getName()) && datasetRowPermissionsTreeItem.getValue().equalsIgnoreCase("${sysParams.userName}")) {
datasetRowPermissionsTreeItem.setValue(userEntity.getName());
items.add(datasetRowPermissionsTreeItem);
continue;
}
String value = handleSysVariable(userEntity, datasetRowPermissionsTreeItem.getValue());
if (value == null) {
continue;

View File

@ -0,0 +1,48 @@
DROP TABLE IF EXISTS `core_copilot_msg`;
CREATE TABLE `core_copilot_msg` (
`id` bigint NOT NULL COMMENT 'ID',
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
`dataset_group_id` bigint DEFAULT NULL COMMENT '数据集ID',
`msg_type` varchar(255) DEFAULT NULL COMMENT 'user or api',
`engine_type` varchar(255) DEFAULT NULL COMMENT 'mysql oracle ...',
`schema_sql` longtext COMMENT 'create sql',
`question` longtext COMMENT '用户提问',
`history` longtext COMMENT '历史信息',
`copilot_sql` longtext COMMENT 'copilot 返回 sql',
`api_msg` longtext COMMENT 'copilot 返回信息',
`sql_ok` int DEFAULT NULL COMMENT 'sql 状态',
`chart_ok` int DEFAULT NULL COMMENT 'chart 状态',
`chart` longtext COMMENT 'chart 内容',
`chart_data` longtext COMMENT '视图数据',
`exec_sql` longtext COMMENT '执行请求的SQL',
`msg_status` int DEFAULT NULL COMMENT 'msg状态0失败 1成功',
`err_msg` longtext COMMENT 'de错误信息',
`create_time` bigint DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
);
DROP TABLE IF EXISTS `core_copilot_token`;
CREATE TABLE `core_copilot_token` (
`id` bigint NOT NULL COMMENT 'ID',
`type` varchar(255) DEFAULT NULL COMMENT 'free or license',
`token` longtext,
`update_time` bigint DEFAULT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `core_copilot_token` VALUES (1, 'free', null, null);
INSERT INTO `core_copilot_token` VALUES (2, 'license', null, null);
DROP TABLE IF EXISTS `core_copilot_config`;
CREATE TABLE `core_copilot_config` (
`id` bigint NOT NULL COMMENT 'ID',
`copilot_url` varchar(255) DEFAULT NULL,
`username` varchar(255) DEFAULT NULL,
`pwd` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `core_copilot_config` VALUES (1, 'https://copilot-demo.test.fit2cloud.dev:5000', 'xlab', 'Q2Fsb25nQDIwMTU=');

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
</template>
<style scoped lang="less">
</style>

View File

@ -113,11 +113,12 @@ onMounted(() => {
})
const onClick = () => {
const events = config.value.events
Object.keys(events).forEach(event => {
currentInstance.ctx[event](events[event])
})
eventBus.emit('v-click', config.value.id)
// do event click
// const events = config.value.events
// Object.keys(events).forEach(event => {
// currentInstance.ctx[event](events[event])
// })
// eventBus.emit('v-click', config.value.id)
}
const getComponentStyleDefault = style => {

View File

@ -80,7 +80,12 @@ const show = () => {
layerStore.showComponent()
menuOpt('show')
}
const categoryChange = type => {
if (curComponent.value) {
snapshotStore.recordSnapshotCache()
curComponent.value['category'] = type
}
}
const rename = () => {
emit('rename')
menuOpt('rename')
@ -222,6 +227,12 @@ const editQueryCriteria = () => {
<el-divider class="custom-divider" />
<li @click="hide" v-show="curComponent['isShow']">隐藏</li>
<li @click="show" v-show="!curComponent['isShow']">取消隐藏</li>
<li @click="categoryChange('hidden')" v-show="curComponent['category'] === 'base'">
转为隐藏组件
</li>
<li @click="categoryChange('base')" v-show="curComponent['category'] === 'hidden'">
转为基础组件
</li>
<li @click="lock">锁定</li>
<el-divider class="custom-divider" />
<li v-if="activePosition === 'aside'" @click="rename">重命名</li>

View File

@ -9,6 +9,7 @@ import { useI18n } from '@/hooks/web/useI18n'
import elementResizeDetectorMaker from 'element-resize-detector'
import { snapshotStoreWithOut } from '@/store/modules/data-visualization/snapshot'
import CommonStyleSet from '@/custom-component/common/CommonStyleSet.vue'
import CommonEvent from '@/custom-component/common/CommonEvent.vue'
const snapshotStore = snapshotStoreWithOut()
const { t } = useI18n()
@ -87,6 +88,10 @@ const colorPickerWidth = computed(() => {
}
})
const eventsShow = computed(() => {
return !dashboardActive.value && ['Picture'].includes(element.value.component)
})
const backgroundCustomShow = computed(() => {
return (
dashboardActive.value ||
@ -149,6 +154,15 @@ const stopEvent = e => {
:element="element"
></common-style-set>
</el-collapse-item>
<el-collapse-item
v-if="element && element.events && eventsShow"
:effect="themes"
title="事件"
name="style"
class="common-style-area"
>
<common-event :themes="themes" :element="element"></common-event>
</el-collapse-item>
</el-collapse>
</div>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { computed, toRefs } from 'vue'
import { ElFormItem } from 'element-plus-secondary'
import { snapshotStoreWithOut } from '@/store/modules/data-visualization/snapshot'
const snapshotStore = snapshotStoreWithOut()
const props = withDefaults(
defineProps<{
themes?: EditorTheme
element: any
}>(),
{
themes: 'dark'
}
)
const { themes, element } = toRefs(props)
const eventsInfo = computed(() => {
return element.value.events
})
const onEventChange = () => {
snapshotStore.recordSnapshotCache('renderChart')
}
</script>
<template>
<el-row class="custom-row">
<el-form label-position="top">
<el-form-item class="form-item" :class="'form-item-' + themes">
<el-checkbox
:effect="themes"
size="small"
v-model="eventsInfo.checked"
@change="onEventChange"
>开启事件绑定</el-checkbox
>
</el-form-item>
<el-form-item class="form-item" :class="'form-item-' + themes" style="margin-bottom: 8px">
<el-radio-group
:effect="themes"
v-model="eventsInfo.type"
class="radio-span"
@change="onEventChange"
>
<el-radio label="displayChange" :effect="themes"> 开启隐藏组件 </el-radio>
<el-radio label="jump" :effect="themes"> 跳转 </el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-row>
</template>
<style scoped lang="less"></style>

View File

@ -8,6 +8,18 @@ export const commonStyle = {
opacity: 1
}
export const BASE_EVENTS = {
checked: false,
type: 'displayChange', // openHidden jump
jump: {
value: null
},
displayChange: {
value: true, // 事件当前值 false
target: 'all'
}
}
// 流媒体视频信息配置
export const STREAMMEDIALINKS = {
videoType: 'flv',
@ -159,12 +171,13 @@ export const COMMON_COMPONENT_BACKGROUND_MAP = {
export const commonAttr = {
animations: [],
canvasId: 'canvas-main',
events: {},
events: BASE_EVENTS,
groupStyle: {}, // 当一个组件成为 Group 的子组件时使用
isLock: false, // 是否锁定组件
maintainRadio: false, // 布局时保持宽高比例
aspectRatio: 1, // 锁定时的宽高比例
isShow: true, // 是否显示组件
category: 'base', //组件类型 base 基础组件 hidden隐藏组件
// 当前组件动作
dragging: false,
resizing: false,

View File

@ -1,5 +1,5 @@
<template>
<div class="pic-main">
<div class="pic-main" @click="onPictureClick">
<img
draggable="false"
v-if="propValue['url']"
@ -20,6 +20,8 @@
import { CSSProperties, computed, nextTick, toRefs } from 'vue'
import { imgUrlTrans } from '@/utils/imgUtils'
import eventBus from '@/utils/eventBus'
import { eventStoreWithOut } from '@/store/modules/data-visualization/event'
const eventStore = eventStoreWithOut()
const props = defineProps({
propValue: {
type: String,
@ -52,7 +54,14 @@ const imageAdapter = computed(() => {
}
return style as CSSProperties
})
const onPictureClick = e => {
if (element.value.events && element.value.events.checked) {
if (element.value.events.type === 'displayChange') {
//
eventStore.displayEventChange(element.value)
}
}
}
const uploadImg = () => {
nextTick(() => {
eventBus.emit('uploadImg')

View File

@ -733,6 +733,8 @@ export default {
table_show_col_tooltip: '开启列头提示',
table_show_cell_tooltip: '开启单元格提示',
table_show_header_tooltip: '开启表头提示',
table_show_summary: '显示总计',
table_summary_label: '总计标签',
stripe: '斑马纹',
start_angle: '起始角度',
end_angle: '结束角度',

View File

@ -256,6 +256,14 @@ declare interface ChartBasicStyle {
* 对称柱状图方向
*/
layout?: 'horizontal' | 'vertical'
/**
* 汇总表显示总计
*/
showSummary: boolean
/**
* 汇总表总计标签
*/
summaryLabel: string
}
/**
* 表头属性

View File

@ -3,7 +3,7 @@ import { store } from '../../index'
import { dvMainStoreWithOut } from './dvMain'
const dvMainStore = dvMainStoreWithOut()
const { curComponent } = storeToRefs(dvMainStore)
const { curComponent, componentData } = storeToRefs(dvMainStore)
export const eventStore = defineStore('event', {
actions: {
@ -13,6 +13,15 @@ export const eventStore = defineStore('event', {
removeEvent(event) {
delete curComponent.value.events[event]
},
displayEventChange(component) {
component.events.displayChange.value = !component.events.displayChange.value
componentData.value.forEach(item => {
if (item.category === 'hidden') {
item.isShow = component.events.displayChange.value
}
})
}
}
})

View File

@ -1,6 +1,7 @@
import { cloneDeep } from 'lodash-es'
import componentList, {
ACTION_SELECTION,
BASE_EVENTS,
COMMON_COMPONENT_BACKGROUND_DARK,
COMMON_COMPONENT_BACKGROUND_LIGHT
} from '@/custom-component/component-list'
@ -153,6 +154,12 @@ export function historyAdaptor(
if ((!canvasVersion || canvasVersion === 2) && canvasInfo.type === 'dashboard') {
matrixAdaptor(componentItem)
}
// 组件事件适配
componentItem.events =
componentItem.events && componentItem.events.checked !== undefined
? componentItem.events
: deepCopy(BASE_EVENTS)
componentItem['category'] = componentItem['category'] || 'base'
})
}

View File

@ -2,8 +2,9 @@
import { SpreadSheet, Node } from '@antv/s2'
import { PropType } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { S2Event } from '@antv/s2'
import { S2Event, SortFuncParam } from '@antv/s2'
import { SortUp, SortDown, Sort } from '@element-plus/icons-vue'
import { cloneDeep } from 'lodash-es'
const { t } = useI18n()
const props = defineProps({
@ -12,15 +13,45 @@ const props = defineProps({
required: true
},
meta: {
type: Object as PropType<Node>
type: Object as PropType<Node>,
required: true
}
})
const sort = type => {
const sortFunc = (sortParams: SortFuncParam) => {
if (!sortParams.sortMethod) {
return sortParams.data
}
const data = cloneDeep(sortParams.data)
return data.sort((a, b) => {
//
if (a['SUMMARY']) {
return 1
}
const field = sortParams.sortFieldId
const aValue = a[field]
const bValue = b[field]
if (aValue === bValue) {
return 0
}
if (sortParams.sortMethod === 'asc') {
if (typeof aValue === 'number') {
return aValue - bValue
}
return aValue < bValue ? 1 : -1
}
if (typeof aValue === 'number') {
return bValue - aValue
}
return aValue > bValue ? 1 : -1
})
}
const sort = (type?) => {
props.table.updateSortMethodMap(props.meta.field, type, true)
props.table.emit(S2Event.RANGE_SORT, [
{
sortFieldId: props.meta.field,
sortMethod: type
sortMethod: type,
sortFunc
}
])
}

View File

@ -957,6 +957,33 @@ onMounted(() => {
<template #append>%</template>
</el-input>
</el-form-item>
<el-form-item
v-if="showProperty('showSummary')"
class="form-item"
:class="'form-item-' + themes"
>
<el-checkbox
size="small"
:effect="themes"
v-model="state.basicStyleForm.showSummary"
@change="changeBasicStyle('showSummary')"
>
{{ t('chart.table_show_summary') }}
</el-checkbox>
</el-form-item>
<el-form-item
v-if="showProperty('summaryLabel') && state.basicStyleForm.showSummary"
:label="$t('chart.table_summary_label')"
:class="'form-item-' + themes"
class="form-item"
>
<el-input
v-model="state.basicStyleForm.summaryLabel"
type="text"
:max-length="10"
@blur="changeBasicStyle('summaryLabel')"
/>
</el-form-item>
<!--table2 end-->
<!--gauge start-->
<el-form-item

View File

@ -1483,7 +1483,9 @@ export const DEFAULT_BASIC_STYLE: ChartBasicStyle = {
topN: 5,
topNLabel: '其他',
gaugeAxisLine: true,
gaugePercentLabel: true
gaugePercentLabel: true,
showSummary: false,
summaryLabel: '总计'
}
export const BASE_VIEW_CONFIG = {

View File

@ -1,15 +1,11 @@
import { S2ChartView, S2DrawOptions } from '@/views/chart/components/js/panel/types/impl/s2'
import { S2Event, S2Options, TableSheet, TableColCell, ViewMeta, TableDataCell } from '@antv/s2'
import { parseJson } from '@/views/chart/components/js/util'
import { formatterItem, valueFormatter } from '@/views/chart/components/js/formatter'
import {
copyContent,
getCurrentField,
SortTooltip
} from '@/views/chart/components/js/panel/common/common_table'
import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common'
import { useI18n } from '@/hooks/web/useI18n'
import { formatterItem, valueFormatter } from '@/views/chart/components/js/formatter'
import { copyContent, SortTooltip } from '@/views/chart/components/js/panel/common/common_table'
import { S2ChartView, S2DrawOptions } from '@/views/chart/components/js/panel/types/impl/s2'
import { parseJson } from '@/views/chart/components/js/util'
import { S2Event, S2Options, TableColCell, TableDataCell, TableSheet, ViewMeta } from '@antv/s2'
import { isNumber } from 'lodash-es'
import { TABLE_EDITOR_PROPERTY, TABLE_EDITOR_PROPERTY_INNER } from './common'
const { t } = useI18n()
/**
@ -17,12 +13,17 @@ const { t } = useI18n()
*/
export class TableNormal extends S2ChartView<TableSheet> {
properties = TABLE_EDITOR_PROPERTY
propertyInner = {
propertyInner: EditorPropertyInner = {
...TABLE_EDITOR_PROPERTY_INNER,
'table-header-selector': [
...TABLE_EDITOR_PROPERTY_INNER['table-header-selector'],
'tableHeaderSort',
'showTableHeader'
],
'basic-style-selector': [
...TABLE_EDITOR_PROPERTY_INNER['basic-style-selector'],
'showSummary',
'summaryLabel'
]
}
axis: AxisType[] = ['xAxis', 'yAxis', 'drill', 'filter']
@ -160,6 +161,43 @@ export class TableNormal extends S2ChartView<TableSheet> {
// header interaction
this.configHeaderInteraction(chart, s2Options)
}
// 总计
if (customAttr.basicStyle.showSummary) {
// 设置汇总行高度和表头一致
const heightByField = {}
heightByField[newData.length] = customAttr.tableHeader.tableTitleHeight
s2Options.style.rowCfg = { heightByField }
// 计算汇总加入到数据里冻结最后一行
s2Options.frozenTrailingRowCount = 1
const yAxis = chart.yAxis
const xAxis = chart.xAxis
const summaryObj = newData.reduce(
(p, n) => {
yAxis.forEach(axis => {
p[axis.dataeaseName] = (n[axis.dataeaseName] || 0) + (p[axis.dataeaseName] || 0)
})
return p
},
{ SUMMARY: true }
)
newData.push(summaryObj)
s2Options.dataCell = viewMeta => {
if (viewMeta.rowIndex !== newData.length - 1) {
return new TableDataCell(viewMeta, viewMeta.spreadsheet)
}
if (viewMeta.colIndex === 0) {
if (customAttr.tableHeader.showIndex) {
viewMeta.fieldValue = customAttr.basicStyle.summaryLabel ?? '总计'
} else {
if (xAxis.length) {
viewMeta.fieldValue = customAttr.basicStyle.summaryLabel ?? '总计'
}
}
}
return new SummaryCell(viewMeta, viewMeta.spreadsheet)
}
}
// 开始渲染
const newChart = new TableSheet(containerDom, s2DataConfig, s2Options)
@ -211,3 +249,13 @@ export class TableNormal extends S2ChartView<TableSheet> {
super('table-normal', [])
}
}
class SummaryCell extends TableDataCell {
getTextStyle() {
return this.theme.colCell.bolderText
}
getBackgroundColor() {
const { backgroundColor, backgroundColorOpacity } = this.theme.colCell.cell
return { backgroundColor, backgroundColorOpacity }
}
}

View File

@ -344,30 +344,30 @@ function customCalcFunc(query, data, totalCfgMap) {
switch (aggregation) {
case 'SUM': {
return data.reduce((p, n) => {
return p + n[query[EXTRA_FIELD]]
return p + parseFloat(n[query[EXTRA_FIELD]])
}, 0)
}
case 'AVG': {
const sum = data.reduce((p, n) => {
return p + n[query[EXTRA_FIELD]]
return p + parseFloat(n[query[EXTRA_FIELD]])
}, 0)
return sum / data.length
}
case 'MIN': {
const result = minBy(data, n => {
return n[query[EXTRA_FIELD]]
return parseFloat(n[query[EXTRA_FIELD]])
})
return result?.[query[EXTRA_FIELD]]
}
case 'MAX': {
const result = maxBy(data, n => {
return n[query[EXTRA_FIELD]]
return parseFloat(n[query[EXTRA_FIELD]])
})
return result?.[query[EXTRA_FIELD]]
}
default: {
return data.reduce((p, n) => {
return p + n[query[EXTRA_FIELD]]
return p + parseFloat(n[query[EXTRA_FIELD]])
}, 0)
}
}

View File

@ -598,7 +598,7 @@ export function mappingColor(value, defaultColor, field, type) {
}
export function handleTableEmptyStrategy(chart: Chart) {
let newData = chart.data?.tableRow as Record<string, any>[]
let newData = (chart.data?.tableRow || []) as Record<string, any>[]
let intersectionArr = []
const senior = parseJson(chart.senior)
let emptyDataStrategy = senior?.functionCfg?.emptyDataStrategy

View File

@ -69,6 +69,10 @@ export abstract class S2ChartView<P extends SpreadSheet> extends AntVAbstractCha
let field
switch (cell.cellType) {
case 'dataCell':
if (meta.valueField === SERIES_NUMBER_FIELD) {
content = meta.fieldValue
break
}
field = find(metaConfig, item => item.field === meta.valueField)
if (meta.fieldValue === 0) {
content = '0'

View File

@ -4,7 +4,7 @@ import { equalsAny, includesAny } from '../editor/util/StringUtils'
import { FeatureCollection } from '@antv/l7plot/dist/esm/plots/choropleth/types'
import { useMapStoreWithOut } from '@/store/modules/map'
import { getGeoJson } from '@/api/map'
import { toRaw } from 'vue'
import { computed, toRaw } from 'vue'
import { Options } from '@antv/g2plot/esm'
import { PickOptions } from '@antv/g2plot/esm/core/plot'
import { innerExportDetails } from '@/api/chart'
@ -12,6 +12,8 @@ import { ElMessage } from 'element-plus-secondary'
import { useI18n } from '@/hooks/web/useI18n'
import { useLinkStoreWithOut } from '@/store/modules/link'
const isDataEaseBi = computed(() => appStore.getIsDataEaseBi)
const { t } = useI18n()
// 同时支持将hex和rgb转换成rgba
export function hexColorToRGBA(hex, alpha) {
@ -511,7 +513,7 @@ export const exportExcelDownload = (chart, callBack?) => {
innerExportDetails(request)
.then(res => {
if (linkStore.getLinkToken) {
if (linkStore.getLinkToken || isDataEaseBi.value) {
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'

View File

@ -116,7 +116,6 @@ const calcData = (view: Chart, callback, resetPageInfo = true) => {
} else {
delete view.chartExtRequest.pageSize
}
console.log(view)
if (view.tableId || view['dataFrom'] === 'template') {
isError.value = false
const v = JSON.parse(JSON.stringify(view))

View File

@ -0,0 +1,30 @@
package io.dataease.api.copilot;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.dataease.api.copilot.dto.MsgDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
/**
* @Author Junjun
*/
@Tag(name = "Copilot")
@ApiSupport(order = 999)
public interface CopilotApi {
@Operation(summary = "发起一次对话")
@PostMapping("chat")
MsgDTO chat(@RequestBody MsgDTO msgDTO) throws Exception;
@Operation(summary = "获取对话记录")
@PostMapping("getList")
List<MsgDTO> getList() throws Exception;
@Operation(summary = "清空对话")
@PostMapping("clearAll")
void clearAll() throws Exception;
}

View File

@ -0,0 +1,12 @@
package io.dataease.api.copilot.dto;
import lombok.Data;
/**
* @Author Junjun
*/
@Data
public class AxisDTO {
private String x;
private String y;
}

View File

@ -0,0 +1,13 @@
package io.dataease.api.copilot.dto;
import lombok.Data;
/**
* @Author Junjun
*/
@Data
public class ChartDTO {
private String type;
private AxisDTO axis;
private String title;
}

View File

@ -0,0 +1,13 @@
package io.dataease.api.copilot.dto;
import lombok.Data;
import java.util.Map;
/**
* @Author Junjun
*/
@Data
public class DEReceiveDTO extends ReceiveDTO {
private Map<String, Object> chartData;
}

View File

@ -0,0 +1,16 @@
package io.dataease.api.copilot.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
/**
* @Author Junjun
*/
@Data
public class DESendDTO extends SendDTO {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
@JsonSerialize(using = ToStringSerializer.class)
private Long datasetGroupId;
}

View File

@ -0,0 +1,12 @@
package io.dataease.api.copilot.dto;
import lombok.Data;
/**
* @Author Junjun
*/
@Data
public class HistoryDTO {
private String type;
private String message;
}

View File

@ -0,0 +1,53 @@
package io.dataease.api.copilot.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* @Author Junjun
*/
@Data
public class MsgDTO {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
@JsonSerialize(using = ToStringSerializer.class)
private Long datasetGroupId;
private String msgType;
private String engineType;
private String schemaSql;
private String question;
private List<HistoryDTO> history;
private String copilotSql;
private String apiMsg;
private Integer sqlOk;
private Integer chartOk;
private ChartDTO chart;
private Long createTime;
private Map<String, Object> chartData;
private String execSql;
private Integer msgStatus;
private String errMsg;
}

View File

@ -0,0 +1,18 @@
package io.dataease.api.copilot.dto;
import lombok.Data;
import java.util.List;
/**
* @Author Junjun
*/
@Data
public class ReceiveDTO {
private String sql;
private List<HistoryDTO> history;
private String apiMessage;
private Boolean sqlOk;
private Boolean chartOk;
private ChartDTO chart;
}

View File

@ -0,0 +1,16 @@
package io.dataease.api.copilot.dto;
import lombok.Data;
import java.util.List;
/**
* @Author Junjun
*/
@Data
public class SendDTO {
private String engine;
private String schema;
private String question;
private List<HistoryDTO> history;
}

View File

@ -0,0 +1,16 @@
package io.dataease.api.copilot.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
/**
* @Author Junjun
*/
@Data
public class TokenDTO {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String token;
private Long updateTime;
}