diff --git a/core/backend/src/main/java/io/dataease/auth/config/cas/CasStrategy.java b/core/backend/src/main/java/io/dataease/auth/config/cas/CasStrategy.java index b1b6990dff..29b8f82e17 100644 --- a/core/backend/src/main/java/io/dataease/auth/config/cas/CasStrategy.java +++ b/core/backend/src/main/java/io/dataease/auth/config/cas/CasStrategy.java @@ -86,7 +86,7 @@ public class CasStrategy implements UrlPatternMatcherStrategy { try { DecodedJWT jwt = JWT.decode(token); Claim forShot = jwt.getClaim("forShot"); - return ObjectUtils.isNotEmpty(forShot) && forShot.asBoolean(); + return ObjectUtils.isNotEmpty(forShot) && !forShot.isNull() && forShot.asBoolean(); } catch (Exception e) { LogUtil.error(e.getMessage()); return false; diff --git a/core/backend/src/main/java/io/dataease/job/sechedule/strategy/impl/EmailTaskHandler.java b/core/backend/src/main/java/io/dataease/job/sechedule/strategy/impl/EmailTaskHandler.java index 3f673ce971..5ece55ee56 100644 --- a/core/backend/src/main/java/io/dataease/job/sechedule/strategy/impl/EmailTaskHandler.java +++ b/core/backend/src/main/java/io/dataease/job/sechedule/strategy/impl/EmailTaskHandler.java @@ -9,6 +9,7 @@ import io.dataease.commons.model.AuthURD; import io.dataease.commons.utils.*; import io.dataease.dto.PermissionProxy; import io.dataease.dto.chart.ViewOption; +import io.dataease.dto.panel.PanelGroupDTO; import io.dataease.ext.ExtTaskMapper; import io.dataease.job.sechedule.ScheduleManager; import io.dataease.job.sechedule.strategy.TaskHandler; @@ -31,6 +32,7 @@ import io.dataease.plugins.xpack.wecom.dto.entity.WecomMsgResult; import io.dataease.plugins.xpack.wecom.service.WecomXpackService; import io.dataease.service.chart.ChartViewService; import io.dataease.service.chart.ViewExportExcel; +import io.dataease.service.panel.PanelGroupService; import io.dataease.service.sys.SysUserService; import io.dataease.service.system.EmailService; import org.apache.commons.collections4.CollectionUtils; @@ -269,7 +271,9 @@ public class EmailTaskHandler extends TaskHandler implements Job { emailService.sendWithImageAndFiles(recipients, emailTemplateDTO.getTitle(), contentStr, bytes, files); } else { bytes = emailXpackService.printPdf(url, token, xpackPixelEntity, false, true); - emailService.sendPdfWithFiles(recipients, emailTemplateDTO.getTitle(), contentStr, bytes, files); + PanelGroupDTO panelInfo = CommonBeanFactory.getBean(PanelGroupService.class).findOne(panelId); + String pdfFileName = panelInfo.getName() + "pdf"; + emailService.sendPdfWithFiles(recipients, emailTemplateDTO.getTitle(), contentStr, bytes, files, pdfFileName); } } catch (Exception e) { diff --git a/core/backend/src/main/java/io/dataease/service/chart/ChartViewService.java b/core/backend/src/main/java/io/dataease/service/chart/ChartViewService.java index 2023a4959c..986b3ba41d 100644 --- a/core/backend/src/main/java/io/dataease/service/chart/ChartViewService.java +++ b/core/backend/src/main/java/io/dataease/service/chart/ChartViewService.java @@ -520,7 +520,7 @@ public class ChartViewService { QueryProvider qp = ProviderFactory.getQueryProvider(ds.getType()); if (StringUtils.equalsIgnoreCase(table.getType(), DatasetType.DB.name())) { datasourceRequest.setTable(dataTableInfoDTO.getTable()); - if (StringUtils.equalsIgnoreCase("text", view.getType()) || StringUtils.equalsIgnoreCase("gauge", view.getType()) || StringUtils.equalsIgnoreCase("liquid", view.getType())) { + if (StringUtils.equalsIgnoreCase("gauge", view.getType()) || StringUtils.equalsIgnoreCase("liquid", view.getType())) { datasourceRequest.setQuery(qp.getSQLSummary(dataTableInfoDTO.getTable(), yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, view, ds)); } else if (StringUtils.containsIgnoreCase(view.getType(), "stack")) { datasourceRequest.setQuery(qp.getSQLStack(dataTableInfoDTO.getTable(), xAxis, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, extStack, ds, view)); @@ -539,7 +539,7 @@ public class ChartViewService { } else if (StringUtils.equalsIgnoreCase(table.getType(), DatasetType.SQL.name())) { String sql = dataTableInfoDTO.isBase64Encryption() ? new String(java.util.Base64.getDecoder().decode(dataTableInfoDTO.getSql())) : dataTableInfoDTO.getSql(); sql = handleVariable(sql, requestList, qp, table, ds); - if (StringUtils.equalsIgnoreCase("text", view.getType()) || StringUtils.equalsIgnoreCase("gauge", view.getType()) || StringUtils.equalsIgnoreCase("liquid", view.getType())) { + if (StringUtils.equalsIgnoreCase("gauge", view.getType()) || StringUtils.equalsIgnoreCase("liquid", view.getType())) { datasourceRequest.setQuery(qp.getSQLSummaryAsTmp(sql, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, view)); } else if (StringUtils.containsIgnoreCase(view.getType(), "stack")) { datasourceRequest.setQuery(qp.getSQLAsTmpStack(sql, xAxis, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, extStack, view)); @@ -559,7 +559,7 @@ public class ChartViewService { DataTableInfoDTO dt = gson.fromJson(table.getInfo(), DataTableInfoDTO.class); List list = dataSetTableUnionService.listByTableId(dt.getList().get(0).getTableId()); String sql = dataSetTableService.getCustomSQLDatasource(dt, list, ds); - if (StringUtils.equalsIgnoreCase("text", view.getType()) || StringUtils.equalsIgnoreCase("gauge", view.getType()) || StringUtils.equalsIgnoreCase("liquid", view.getType())) { + if (StringUtils.equalsIgnoreCase("gauge", view.getType()) || StringUtils.equalsIgnoreCase("liquid", view.getType())) { datasourceRequest.setQuery(qp.getSQLSummaryAsTmp(sql, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, view)); } else if (StringUtils.containsIgnoreCase(view.getType(), "stack")) { datasourceRequest.setQuery(qp.getSQLAsTmpStack(sql, xAxis, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, extStack, view)); @@ -580,7 +580,7 @@ public class ChartViewService { Map sqlMap = dataSetTableService.getUnionSQLDatasource(dt, ds); String sql = (String) sqlMap.get("sql"); - if (StringUtils.equalsIgnoreCase("text", view.getType()) || StringUtils.equalsIgnoreCase("gauge", view.getType()) || StringUtils.equalsIgnoreCase("liquid", view.getType())) { + if (StringUtils.equalsIgnoreCase("gauge", view.getType()) || StringUtils.equalsIgnoreCase("liquid", view.getType())) { datasourceRequest.setQuery(qp.getSQLSummaryAsTmp(sql, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, view)); } else if (StringUtils.containsIgnoreCase(view.getType(), "stack")) { datasourceRequest.setQuery(qp.getSQLAsTmpStack(sql, xAxis, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, extStack, view)); @@ -610,7 +610,7 @@ public class ChartViewService { String tableName = "ds_" + table.getId().replaceAll("-", "_"); datasourceRequest.setTable(tableName); QueryProvider qp = ProviderFactory.getQueryProvider(ds.getType()); - if (StringUtils.equalsIgnoreCase("text", view.getType()) || StringUtils.equalsIgnoreCase("gauge", view.getType()) || StringUtils.equalsIgnoreCase("liquid", view.getType())) { + if (StringUtils.equalsIgnoreCase("gauge", view.getType()) || StringUtils.equalsIgnoreCase("liquid", view.getType())) { datasourceRequest.setQuery(qp.getSQLSummary(tableName, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, view, ds)); } else if (StringUtils.containsIgnoreCase(view.getType(), "stack")) { datasourceRequest.setQuery(qp.getSQLStack(tableName, xAxis, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, extStack, ds, view)); @@ -781,6 +781,28 @@ public class ChartViewService { } break; case "text": + xAxis = new ArrayList<>(); + yAxis = yAxis.stream().filter(item -> chartViewFieldNameList.contains(item.getDataeaseName()) || (!desensitizationList.keySet().contains(item.getDataeaseName()) && dataeaseNames.contains(item.getDataeaseName()))).collect(Collectors.toList()); + if (CollectionUtils.isEmpty(yAxis)) { + return emptyChartViewDTO(view); + } + ChartFieldCompareDTO compareCalc = yAxis.get(0).getCompareCalc(); + boolean isYoy = StringUtils.isNotEmpty(compareCalc.getType()) && !StringUtils.equalsIgnoreCase(compareCalc.getType(),"none"); + if(isYoy){ + List xField = fields.stream().filter(item->StringUtils.equalsIgnoreCase(item.getId(),compareCalc.getField())).collect(Collectors.toList()); + if(CollectionUtils.isNotEmpty(xField)){ + ChartViewFieldDTO xFieldChartViewFieldDTO = new ChartViewFieldDTO(); + org.springframework.beans.BeanUtils.copyProperties(xField.get(0), xFieldChartViewFieldDTO); + xAxis.add(xFieldChartViewFieldDTO); + xAxis.get(0).setSort("desc"); + if(Objects.isNull(compareCalc.getCustom())){ + xAxis.get(0).setDateStyle("y_M_d"); + }else{ + xAxis.get(0).setDateStyle(compareCalc.getCustom().getTimeType()); + } + } + } + break; case "gauge": case "liquid": xAxis = new ArrayList<>(); @@ -1229,7 +1251,7 @@ public class ChartViewService { QueryProvider qp = ProviderFactory.getQueryProvider(ds.getType()); if (StringUtils.equalsIgnoreCase(table.getType(), DatasetType.DB.name())) { datasourceRequest.setTable(dataTableInfoDTO.getTable()); - if (StringUtils.equalsAnyIgnoreCase(view.getType(), "text", "gauge", "liquid")) { + if (StringUtils.equalsAnyIgnoreCase(view.getType(), "gauge", "liquid")) { querySql = qp.getSQLSummary(dataTableInfoDTO.getTable(), yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, view, ds); if (isYOY) { yoySql = qp.getSQLSummary(dataTableInfoDTO.getTable(), yAxis, fieldCustomFilter, rowPermissionsTree, yoyFilterList, view, ds); @@ -1269,7 +1291,7 @@ public class ChartViewService { } else if (StringUtils.equalsIgnoreCase(table.getType(), DatasetType.SQL.name())) { String sql = dataTableInfoDTO.isBase64Encryption() ? new String(java.util.Base64.getDecoder().decode(dataTableInfoDTO.getSql())) : dataTableInfoDTO.getSql(); sql = handleVariable(sql, chartExtRequest, qp, table, ds); - if (StringUtils.equalsAnyIgnoreCase(view.getType(), "text", "gauge", "liquid")) { + if (StringUtils.equalsAnyIgnoreCase(view.getType(), "gauge", "liquid")) { querySql = qp.getSQLSummaryAsTmp(sql, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, view); if (isYOY) { yoySql = qp.getSQLSummaryAsTmp(sql, yAxis, fieldCustomFilter, rowPermissionsTree, yoyFilterList, view); @@ -1311,7 +1333,7 @@ public class ChartViewService { DataTableInfoDTO dt = gson.fromJson(table.getInfo(), DataTableInfoDTO.class); List list = dataSetTableUnionService.listByTableId(dt.getList().get(0).getTableId()); String sql = dataSetTableService.getCustomSQLDatasource(dt, list, ds); - if (StringUtils.equalsAnyIgnoreCase(view.getType(), "text", "gauge", "liquid")) { + if (StringUtils.equalsAnyIgnoreCase(view.getType(), "gauge", "liquid")) { querySql = qp.getSQLSummaryAsTmp(sql, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, view); if (isYOY) { yoySql = qp.getSQLSummaryAsTmp(sql, yAxis, fieldCustomFilter, rowPermissionsTree, yoyFilterList, view); @@ -1352,7 +1374,7 @@ public class ChartViewService { DataTableInfoDTO dt = gson.fromJson(table.getInfo(), DataTableInfoDTO.class); Map sqlMap = dataSetTableService.getUnionSQLDatasource(dt, ds); String sql = (String) sqlMap.get("sql"); - if (StringUtils.equalsAnyIgnoreCase(view.getType(), "text", "gauge", "liquid")) { + if (StringUtils.equalsAnyIgnoreCase(view.getType(), "gauge", "liquid")) { querySql = qp.getSQLSummaryAsTmp(sql, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, view); if (isYOY) { yoySql = qp.getSQLSummaryAsTmp(sql, yAxis, fieldCustomFilter, rowPermissionsTree, yoyFilterList, view); @@ -1433,7 +1455,7 @@ public class ChartViewService { String tableName = "ds_" + table.getId().replaceAll("-", "_"); datasourceRequest.setTable(tableName); QueryProvider qp = ProviderFactory.getQueryProvider(ds.getType()); - if (StringUtils.equalsAnyIgnoreCase(view.getType(), "text", "gauge", "liquid")) { + if (StringUtils.equalsAnyIgnoreCase(view.getType(), "gauge", "liquid")) { datasourceRequest.setQuery(qp.getSQLSummary(tableName, yAxis, fieldCustomFilter, rowPermissionsTree, extFilterList, view, ds)); if (isYOY) { yoySql = qp.getSQLSummary(tableName, yAxis, fieldCustomFilter, rowPermissionsTree, yoyFilterList, view, ds); diff --git a/core/backend/src/main/java/io/dataease/service/exportCenter/ExportCenterService.java b/core/backend/src/main/java/io/dataease/service/exportCenter/ExportCenterService.java index 4e456f9c2e..56deaa8a7a 100644 --- a/core/backend/src/main/java/io/dataease/service/exportCenter/ExportCenterService.java +++ b/core/backend/src/main/java/io/dataease/service/exportCenter/ExportCenterService.java @@ -7,6 +7,7 @@ import io.dataease.auth.service.ProxyAuthService; import io.dataease.commons.constants.ParamConstants; import io.dataease.commons.constants.SysLogConstants; import io.dataease.commons.utils.*; +import io.dataease.controller.chart.ChartViewController; import io.dataease.controller.request.chart.ChartExtRequest; import io.dataease.controller.request.dataset.DataSetExportRequest; import io.dataease.controller.request.dataset.DataSetTableRequest; @@ -120,6 +121,8 @@ public class ExportCenterService { @Resource private ChartViewService chartViewService; @Resource + private ChartViewController chartViewController; + @Resource private DataSetGroupService dataSetGroupService; @Resource private PanelGroupService panelGroupService; @@ -332,7 +335,7 @@ public class ExportCenterService { componentFilterInfo.setProxy(request.getProxy()); componentFilterInfo.setUser(request.getUserId()); componentFilterInfo.setDownloadType(request.getDownloadType()); - ChartViewDTO chartViewInfo = chartViewService.getData(request.getViewId(), componentFilterInfo); + ChartViewDTO chartViewInfo = chartViewController.getData(request.getViewId(), null, componentFilterInfo); List tableRow = (List) chartViewInfo.getData().get("sourceData"); request.setDetails(tableRow); if ("dataset".equals(request.getDownloadType())) { diff --git a/core/backend/src/main/java/io/dataease/service/system/EmailService.java b/core/backend/src/main/java/io/dataease/service/system/EmailService.java index dd27d90019..73ba952a5c 100644 --- a/core/backend/src/main/java/io/dataease/service/system/EmailService.java +++ b/core/backend/src/main/java/io/dataease/service/system/EmailService.java @@ -83,7 +83,7 @@ public class EmailService { } - public void sendPdfWithFiles(String to, String title, String content, byte[] bytes, List files) { + public void sendPdfWithFiles(String to, String title, String content, byte[] bytes, List files, String pdfFileName) { if (ArrayUtils.isEmpty(bytes)) { send(to, title, content); return; @@ -98,7 +98,7 @@ public class EmailService { MimeMessage mimeMessage = driver.createMimeMessage(); try { multipart = addContent(multipart, content); - multipart = addPdf(multipart, bytes); + multipart = addPdf(multipart, bytes, pdfFileName); if (CollectionUtils.isNotEmpty(files)) { multipart = addFiles(multipart, files); } @@ -165,11 +165,11 @@ public class EmailService { return multipart; } - private MimeMultipart addPdf(MimeMultipart multipart, byte[] bytes) throws Exception { + private MimeMultipart addPdf(MimeMultipart multipart, byte[] bytes, String pdfFileName) throws Exception { MimeBodyPart attach = new MimeBodyPart(); ByteArrayDataSource fileDataSource = new ByteArrayDataSource(bytes, "application/pdf"); attach.setDataHandler(new DataHandler(fileDataSource)); - attach.setFileName(MimeUtility.encodeText("panel.pdf", "gb2312", null)); + attach.setFileName(MimeUtility.encodeText(pdfFileName, "gb2312", null)); multipart.addBodyPart(attach); multipart.setSubType("related"); return multipart; diff --git a/core/frontend/package.json b/core/frontend/package.json index f7a77248e3..3b692fa82d 100644 --- a/core/frontend/package.json +++ b/core/frontend/package.json @@ -1,6 +1,6 @@ { "name": "dataease", - "version": "1.18.24", + "version": "1.18.25", "description": "dataease front", "private": true, "scripts": { @@ -45,6 +45,7 @@ "echarts": "^5.0.1", "element-resize-detector": "^1.2.3", "element-ui": "2.15.7", + "exceljs": "^4.4.0", "file-saver": "^2.0.5", "fit2cloud-ui": "^1.8.0", "flv.js": "^1.6.2", diff --git a/core/frontend/src/components/canvas/components/editor/EditBar.vue b/core/frontend/src/components/canvas/components/editor/EditBar.vue index 53d50ac9b0..c8285d0485 100644 --- a/core/frontend/src/components/canvas/components/editor/EditBar.vue +++ b/core/frontend/src/components/canvas/components/editor/EditBar.vue @@ -89,6 +89,16 @@ @click.stop="exportExcelDownload()" /> + + + { - cb() - } - } + Button, + { + props: { + type: 'text' }, - this.$t('data_export.export_center') - ), + class: 'btn-text', + on: { + click: () => { + cb() + } + } + }, + this.$t('data_export.export_center') + ), this.$t('data_export.export_info') ]), iconClass, @@ -522,7 +540,7 @@ export default { Button, { props: { - type: 'text', + type: 'text' }, class: 'btn-text', on: { @@ -542,6 +560,13 @@ export default { exportExcelDownload() { exportExcelDownload(this.chart, null, null, null, null, null, this.exportDataCb) }, + exportFormattedExcel() { + const instance = this.$store.state.chart.tableInstance[this.chart.id] + if (!instance) { + return + } + exportPivotExcel(instance, this.chart) + }, auxiliaryMatrixChange() { if (this.curComponent.auxiliaryMatrix) { this.curComponent.auxiliaryMatrix = false diff --git a/core/frontend/src/components/canvas/customComponent/UserView.vue b/core/frontend/src/components/canvas/customComponent/UserView.vue index 603468d770..4828746de0 100644 --- a/core/frontend/src/components/canvas/customComponent/UserView.vue +++ b/core/frontend/src/components/canvas/customComponent/UserView.vue @@ -601,6 +601,7 @@ export default { 'cfilters': { handler: function(val1, val2) { if (isChange(val1, val2) && !this.isFirstLoad) { + this.currentPage.page = 1 this.getData(this.element.propValue.viewId) this.getDataLoading = true } @@ -779,20 +780,20 @@ export default { message: h('p', null, [ this.$t('data_export.exporting'), h( - Button, - { - props: { - type: 'text', - }, - class: 'btn-text', - on: { - click: () => { - cb() - } - } + Button, + { + props: { + type: 'text' }, - this.$t('data_export.export_center') - ), + class: 'btn-text', + on: { + click: () => { + cb() + } + } + }, + this.$t('data_export.export_center') + ), this.$t('data_export.export_info') ]), iconClass, @@ -811,7 +812,7 @@ export default { Button, { props: { - type: 'text', + type: 'text' }, class: 'btn-text', on: { @@ -999,7 +1000,7 @@ export default { }, getData(id, cache = true, dataBroadcast = false) { // Err1001 已删除的不在重复请求 - if (this.requestStatus === 'waiting' || (this.message && this.message.indexOf('Err1001')> -1)) { + if (this.requestStatus === 'waiting' || (this.message && this.message.indexOf('Err1001') > -1)) { return } if (id) { diff --git a/core/frontend/src/components/widget/deWidget/DeSelect.vue b/core/frontend/src/components/widget/deWidget/DeSelect.vue index df97f6665d..d27b1e66ed 100644 --- a/core/frontend/src/components/widget/deWidget/DeSelect.vue +++ b/core/frontend/src/components/widget/deWidget/DeSelect.vue @@ -420,10 +420,14 @@ export default { if (!token && linkToken) { method = linkMultFieldValues } - method({ + const param = { fieldIds: this.element.options.attrs.fieldId.split(this.separator), sort: this.element.options.attrs.sort - }).then(res => { + } + if (this.panelInfo.proxy) { + param.userId = this.panelInfo.proxy + } + method(param).then(res => { this.data = this.optionData(res.data) bus.$emit('valid-values-change', true) cb && cb() diff --git a/core/frontend/src/components/widget/deWidget/DeSelectGrid.vue b/core/frontend/src/components/widget/deWidget/DeSelectGrid.vue index cf43f0521f..c5caad7810 100644 --- a/core/frontend/src/components/widget/deWidget/DeSelectGrid.vue +++ b/core/frontend/src/components/widget/deWidget/DeSelectGrid.vue @@ -364,10 +364,14 @@ export default { if (!token && linkToken) { method = linkMultFieldValues } - method({ + const param = { fieldIds: this.element.options.attrs.fieldId.split(','), sort: this.element.options.attrs.sort, keyword: this.keyWord - }).then(res => { + } + if (this.panelInfo.proxy) { + param.userId = this.panelInfo.proxy + } + method(param).then(res => { this.data = this.optionData(res.data) this.changeInputStyle() if (this.element.options.attrs.multiple) { diff --git a/core/frontend/src/components/widget/deWidget/DeSelectTree.vue b/core/frontend/src/components/widget/deWidget/DeSelectTree.vue index 0a4112e149..2dc7725884 100644 --- a/core/frontend/src/components/widget/deWidget/DeSelectTree.vue +++ b/core/frontend/src/components/widget/deWidget/DeSelectTree.vue @@ -293,10 +293,14 @@ export default { if (!token && linkToken) { method = linkMappingFieldValues } - method({ + const param = { fieldIds: this.element.options.attrs.fieldId.split(','), sort: this.element.options.attrs.sort - }).then(res => { + } + if (this.panelInfo.proxy) { + param.userId = this.panelInfo.proxy + } + method(param).then(res => { this.data = this.optionData(res.data) this.$nextTick(() => { this.$refs.deSelectTree && this.$refs.deSelectTree.treeDataUpdateFun(this.data) diff --git a/core/frontend/src/icons/svg/ds-excel-format.svg b/core/frontend/src/icons/svg/ds-excel-format.svg new file mode 100644 index 0000000000..387f4d7708 --- /dev/null +++ b/core/frontend/src/icons/svg/ds-excel-format.svg @@ -0,0 +1 @@ + diff --git a/core/frontend/src/icons/svg/stock-line.svg b/core/frontend/src/icons/svg/stock-line.svg new file mode 100644 index 0000000000..b241a48968 --- /dev/null +++ b/core/frontend/src/icons/svg/stock-line.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/frontend/src/lang/en.js b/core/frontend/src/lang/en.js index 6764dc5008..296bf0f629 100644 --- a/core/frontend/src/lang/en.js +++ b/core/frontend/src/lang/en.js @@ -1412,6 +1412,7 @@ export default { chart_bar_stack_horizontal: 'Stack Horizontal Bar', chart_percentage_bar_stack_horizontal: 'Horizontal Percentage Stack Bar', chart_bidirectional_bar: 'Bidirectional Bar', + chart_stock_line: 'Stock Line', chart_line: 'Base Line', chart_line_stack: 'Stack Line', chart_pie: 'Pie', @@ -1858,7 +1859,9 @@ export default { polynomial_regression: 'Polynomial regression', show_summary: 'Show summary', summary_label: 'Summary label', - tip: 'Tip' + tip: 'Tip', + pivot_export_empty_fields: 'Can not export without row dimension or quota', + export_formatted_excel: 'Export formatted excel' }, dataset: { scope_edit: 'Effective only when editing', diff --git a/core/frontend/src/lang/tw.js b/core/frontend/src/lang/tw.js index 466cf44667..f05b7ff68e 100644 --- a/core/frontend/src/lang/tw.js +++ b/core/frontend/src/lang/tw.js @@ -1411,6 +1411,7 @@ export default { chart_bar_stack_horizontal: '橫嚮堆疊柱狀圖', chart_percentage_bar_stack_horizontal: '橫嚮百分比柱狀圖', chart_bidirectional_bar: '對稱柱狀圖', + chart_stock_line: 'K 線圖', chart_line: '基礎摺線圖', chart_line_stack: '堆疊摺線圖', chart_pie: '餅圖', @@ -1851,7 +1852,9 @@ export default { polynomial_regression: '多項式擬合', show_summary: '顯示總計', summary_label: '總計標籤', - tip: '提示' + tip: '提示', + pivot_export_empty_fields: '行維度或指標維度為空不可導出', + export_formatted_excel: '導出 Excel (帶格式)' }, dataset: { scope_edit: '僅編輯時生效', diff --git a/core/frontend/src/lang/zh.js b/core/frontend/src/lang/zh.js index 70508fc6fa..e4fd1b5b75 100644 --- a/core/frontend/src/lang/zh.js +++ b/core/frontend/src/lang/zh.js @@ -1408,6 +1408,7 @@ export default { chart_bar_stack_horizontal: '横向堆叠柱状图', chart_percentage_bar_stack_horizontal: '横向百分比柱状图', chart_bidirectional_bar: '对称柱状图', + chart_stock_line: 'K 线图', chart_line: '基础折线图', chart_line_stack: '堆叠折线图', chart_pie: '饼图', @@ -1848,7 +1849,9 @@ export default { polynomial_regression: '多项式拟合', show_summary: '显示总计', summary_label: '总计标签', - tip: '提示' + tip: '提示', + pivot_export_empty_fields: '行维度或指标维度为空不可导出', + export_formatted_excel: '导出 Excel (带格式)' }, dataset: { goto: ', 前往 ', diff --git a/core/frontend/src/store/modules/chart.js b/core/frontend/src/store/modules/chart.js index b68f06f8f0..e3aac1428c 100644 --- a/core/frontend/src/store/modules/chart.js +++ b/core/frontend/src/store/modules/chart.js @@ -5,7 +5,8 @@ const getDefaultState = () => { sceneId: {}, viewId: null, tableId: {}, - chartSceneData: {} + chartSceneData: {}, + tableInstance: {} } } @@ -29,6 +30,9 @@ const mutations = { }, setChartSceneData: (state, chartSceneData) => { state.chartSceneData = chartSceneData + }, + setTableInstance: (state, { viewId, tableInstance }) => { + state.tableInstance[viewId] = tableInstance } } @@ -50,6 +54,9 @@ const actions = { }, setChartSceneData: ({ commit }, chartSceneData) => { commit('setChartSceneData', chartSceneData) + }, + setTableInstance: ({ commit }, { viewId, tableInstance }) => { + commit('setTableInstance', { viewId, tableInstance }) } } diff --git a/core/frontend/src/utils/request.js b/core/frontend/src/utils/request.js index ffb4bb8e59..9cf17c2916 100644 --- a/core/frontend/src/utils/request.js +++ b/core/frontend/src/utils/request.js @@ -114,6 +114,7 @@ service.setTimeOut = time => { // 请根据实际需求修改 service.interceptors.response.use(response => { + checkCasRedirect(response) response.config.loading && tryHideLoading(store.getters.currentPath) checkAuth(response) Vue.prototype.$currentHttpRequestList.delete(response.config.url) @@ -191,4 +192,17 @@ const checkAuth = response => { store.dispatch('user/setLinkToken', linkToken) } } +const checkCasRedirect = (response) => { + if (!response || !response.data) { + return + } + const resData = response.data + const routine = resData.hasOwnProperty('success') + const redirectUrl = response?.request?.responseURL + if (resData && !routine && resData.startsWith(' { + location.reload() + }) + } +} export default service diff --git a/core/frontend/src/views/chart/chart/bar/bar_antv.js b/core/frontend/src/views/chart/chart/bar/bar_antv.js index ec183ee775..809d81e0f4 100644 --- a/core/frontend/src/views/chart/chart/bar/bar_antv.js +++ b/core/frontend/src/views/chart/chart/bar/bar_antv.js @@ -1,4 +1,4 @@ -import { Column, Bar, BidirectionalBar } from '@antv/g2plot' +import {Column, Bar, BidirectionalBar, Mix} from '@antv/g2plot' import { getTheme, getLabel, @@ -17,6 +17,14 @@ import { import { antVCustomColor, getColors, handleEmptyDataStrategy, hexColorToRGBA, handleStackSort } from '@/views/chart/chart/util' import { cloneDeep, find, groupBy, each } from 'lodash-es' import { formatterItem, valueFormatter } from '@/views/chart/chart/formatter' +import { + calculateMinMax, + calculateMovingAverage, + configXAxis, configYAxis, + configBasicStyle, + configTooltip, + registerEvent, customConfigEmptyDataStrategy +} from "@/views/chart/chart/bar/stock_line_util"; export function baseBarOptionAntV(container, chart, action, isGroup, isStack) { // theme @@ -587,3 +595,201 @@ export function baseBidirectionalBarOptionAntV(container, chart, action, isGroup configPlotTooltipEvent(chart, plot) return plot } + +export function stockLineOptionAntV(container, chart, action) { + if (!chart.data?.data?.length) { + return + } + const xAxis = JSON.parse(chart.xaxis) + const yAxis = JSON.parse(chart.yaxis) + if (yAxis.length !== 4) { + return + } + const theme = getTheme(chart) + const legend = getLegend(chart) + const basicStyle = JSON.parse(chart.customAttr).color + const colors = [] + const alpha = basicStyle.alpha + basicStyle.colors.forEach(ele => { + colors.push(hexColorToRGBA(ele, alpha)) + }) + const data = cloneDeep(chart.data?.tableRow ?? []) + + // 时间字段 + const xAxisDataeaseName = xAxis[0].dataeaseName + const averages = [5, 10, 20, 60, 120, 180] + const legendItems = [ + { + name: '日K', + value: 'k', + marker: { + symbol: (x, y, r) => { + const width = r * 1 + const height = r + return [ + // 矩形框 + ['M', x - width - 1 / 2, y - height / 2], + ['L', x + width + 1 / 2, y - height / 2], + ['L', x + width + 1 / 2, y + height / 2], + ['L', x - width - 1 / 2, y + height / 2], + ['Z'], + // 中线 + ['M', x, y + 10 / 2], + ['L', x, y - 10 / 2] + ] + }, + style: { fill: 'red', stroke: 'red', lineWidth: 2 } + } + } + ] + // 计算均线数据 + const averagesLineData = new Map() + averages.forEach(item => { + averagesLineData.set('ma' + item, calculateMovingAverage(data, item, chart)) + }) + + // 将均线数据设置到主数据中 + data.forEach((item) => { + const date = item[xAxisDataeaseName] + for (const [key, value] of averagesLineData) { + item[key] = value.find(m => m[xAxisDataeaseName] === date)?.value + } + }) + + const averageLines = [] + let index = 0 + const start = 0.5 + const end = 1 + const startIndex = Math.floor(start * data.length) + const endIndex = Math.ceil(end * data.length) + const filteredData = data.slice(startIndex, endIndex) + const { maxValue, minValue } = calculateMinMax(filteredData) + for (const key of averagesLineData.keys()) { + index++ + averageLines.push({ + type: 'line', + top: true, + options: { + smooth: false, + xField: xAxisDataeaseName, + yField: key, + color: colors[index - 1], + xAxis: null, + yAxis: { + label: false, + min: minValue, + max: maxValue, + grid: null, + line: null + }, + lineStyle: { + lineWidth: 2 + } + } + }) + legendItems.push({ + name: key.toUpperCase(), + value: key, + marker: { symbol: 'hyphen', style: { stroke: colors[index - 1], lineWidth: 2 } } + }) + } + const axis =JSON.parse(chart.xaxis) ?? [] + let dateFormat + const dateSplit = axis[0]?.datePattern === 'date_split' ? '/' : '-' + switch (axis[0]?.dateStyle) { + case 'y': + dateFormat = 'YYYY' + break + case 'y_M': + dateFormat = 'YYYY' + dateSplit + 'MM' + break + case 'y_M_d': + dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + break + case 'y_M_d_H': + dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH' + break + case 'y_M_d_H_m': + dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH:mm' + break + case 'y_M_d_H_m_s': + dateFormat = 'YYYY' + dateSplit + 'MM' + dateSplit + 'DD' + ' HH:mm:ss' + break + default: + dateFormat = 'YYYY-MM-dd HH:mm:ss' + } + let option = { + data, + theme, + appendPadding: getPadding(chart), + slider: { + start: 0.5, + end: 1 + }, + plots: [ + ...averageLines, + { + type: 'stock', + top: true, + options: { + meta: { + [xAxisDataeaseName]: { + mask: dateFormat + } + }, + stockStyle: { + stroke: 'black', + lineWidth: 0.5 + }, + xField: xAxisDataeaseName, + yField: [ + yAxis[0].dataeaseName, + yAxis[1].dataeaseName, + yAxis[2].dataeaseName, + yAxis[3].dataeaseName + ], + legend: !legend?false:{ + position: 'top', + custom: true, + items: legendItems + }, + fallingFill: hexColorToRGBA('#ef5350', alpha), + risingFill: hexColorToRGBA('#26a69a', alpha), + } + } + ] + } + option = configBasicStyle(chart, option) + option = configXAxis(chart, option) + option = configYAxis(chart, option) + option = configTooltip(chart, option) + option = customConfigEmptyDataStrategy(chart,option) + const plot = new Mix(container, option) + registerEvent(data, plot, averagesLineData) + plot.on('schema:click', evt => { + const selectSchema = evt.data.data[xAxisDataeaseName] + const paramData = cloneDeep(chart.data?.data ?? []) + const selectData = paramData.filter(item => item.field === selectSchema) + const quotaList = [] + selectData.forEach(item => { + quotaList.push({ ...item.quotaList[0], value: item.value }) + }) + if (selectData.length) { + const param = { + x: evt.x, + y: evt.y, + data: { + data: { + ...evt.data.data, + value: quotaList[0].value, + name: selectSchema, + dimensionList: selectData[0].dimensionList, + quotaList: quotaList + } + } + } + action(param) + } + }) + return plot +} diff --git a/core/frontend/src/views/chart/chart/bar/stock_line_util.js b/core/frontend/src/views/chart/chart/bar/stock_line_util.js new file mode 100644 index 0000000000..03be84d0d5 --- /dev/null +++ b/core/frontend/src/views/chart/chart/bar/stock_line_util.js @@ -0,0 +1,423 @@ +import {getXAxis, getYAxis} from '@/views/chart/chart/common/common_antv' +import { valueFormatter } from '@/views/chart/chart/formatter' +import {cloneDeep} from "lodash"; +import {handleEmptyDataStrategy} from "@/views/chart/chart/util"; + +/** + * 计算收盘价平均值 + * @param data + * @param dayCount + * @param chart + */ +export const calculateMovingAverage = (data, dayCount, chart) => { + const xAxis = JSON.parse(chart.xaxis) + const yAxis = JSON.parse(chart.yaxis) + // 时间字段 + const xAxisDataeaseName = xAxis[0].dataeaseName + // 收盘价字段 + const yAxisDataeaseName = yAxis[1].dataeaseName + const result = [] + for (let i = 0; i < data.length; i++) { + if (i < dayCount) { + result.push({ + [xAxisDataeaseName]: data[i][xAxisDataeaseName], + value: null + }) + } else { + const sum = data + .slice(i - dayCount + 1, i + 1) + .reduce((sum, item) => sum + item[yAxisDataeaseName], 0) + result.push({ + [xAxisDataeaseName]: data[i][xAxisDataeaseName], + value: parseFloat((sum / dayCount).toFixed(3)) + }) + } + } + return result +} + +/** + * 获取数据集合中对象属性值的最大最小值 + * @param data + */ +export const calculateMinMax = data => { + return data.reduce( + (acc, current) => { + // 获取 current 对象的所有属性值 + const values = Object.values(current) + // 过滤出数字值 + const numericValues = values.filter(value => typeof value === 'number') ?? [] + // 找到 current 对象的数字属性值中的最大值和最小值 + // 如果存在数字值,则计算当前对象的最大值和最小值 + if (numericValues.length > 0) { + const currentMax = Math.max(...numericValues) + const currentMin = Math.min(...numericValues) + // 更新全局最大值和最小值 + acc.maxValue = Math.max(acc.maxValue, currentMax) + acc.minValue = Math.min(acc.minValue, currentMin) + } + return acc + }, + { maxValue: Number.NEGATIVE_INFINITY, minValue: Number.POSITIVE_INFINITY } + ) +} + +/** + * 注册图表事件 + * @param data + * @param plot + * @param averagesLineData + */ +export const registerEvent = (data, plot, averagesLineData) => { + // 监听图例点击事件,显示隐藏 + let risingVisible = true + plot.on('legend-item:click', evt => { + const { value } = evt.target.get('delegateObject').item + if (value === 'k') { + risingVisible = !risingVisible + plot.chart.geometries.forEach(geom => { + if (geom.type === 'schema') { + geom.changeVisible(risingVisible) + } + }) + } else { + const lines = plot.chart.geometries.filter(item => item.type === 'line') + const points = plot.chart.geometries.filter(item => item.type === 'point') + let lineIndex = 0 + for (const key of averagesLineData.keys()) { + lineIndex++ + if (key === value) { + lines[lineIndex - 1].changeVisible(!lines[lineIndex - 1].visible) + points[lineIndex - 1].changeVisible(!points[lineIndex - 1].visible) + } + } + } + }) + // 监听图表渲染事件 + plot.on('afterrender', e => { + let first = false + if (plot.chart.options.slider.start === 0.5 && plot.chart.options.slider.end === 1) { + first = true + } + if (e.view?.options?.scales) { + const startIndex = Math.floor(0.5 * data.length) + const endIndex = Math.ceil(1 * data.length) + const filteredData = data.slice(startIndex, endIndex) + const { maxValue, minValue } = calculateMinMax( + first ? filteredData : e.view.filteredData + ) + const a = e.view.options.scales + Object.keys(a).forEach(item => { + if (a[item].max) { + a[item].max = maxValue + } + if (a[item].min) { + a[item].min = minValue + } + }) + } + }) + // 监听图例组点击事件,设置缩放 + plot.on('legend-item-group:click', e => { + if (e.view?.options?.scales) { + const { maxValue, minValue } = calculateMinMax(e.view.filteredData) + const a = e.view.options.scales + Object.keys(a).forEach(item => { + if (a[item].max) { + a[item].max = maxValue + } + if (a[item].min) { + a[item].min = minValue + } + }) + } + }) + // 监听滑块事件,设置缩放 + plot.on('slider:valuechanged', e => { + const start = e.gEvent.currentTarget.cfg.component.cfg.start + const end = e.gEvent.currentTarget.cfg.component.cfg.end + plot.chart.options.slider.start = start + plot.chart.options.slider.end = end + const startIndex = Math.floor(start * data.length) + const endIndex = Math.ceil(end * data.length) + const filteredData = data.slice(startIndex, endIndex) + const { maxValue, minValue } = calculateMinMax(filteredData) + const a = e.view.options.scales + Object.keys(a).forEach(item => { + if (a[item].max) { + a[item].max = maxValue + } + if (a[item].min) { + a[item].min = minValue + } + }) + }) +} + +export const configBasicStyle = (chart, options) => { + // size + const customAttr = JSON.parse(chart.customAttr) + const s = JSON.parse(JSON.stringify(customAttr.size)) + const smooth = s.lineSmooth + const point = { + size: s.lineSymbolSize, + shape: s.lineSymbol + } + const lineStyle = { + lineWidth: s.lineWidth + } + const plots = [] + options.plots.forEach(item => { + if (item.type === 'line') { + plots.push({ ...item, options: { ...item.options, smooth, point, lineStyle } }) + } + if (item.type === 'stock') { + plots.push({ ...item }) + } + }) + return { + ...options, + plots + } +} + +export const configTooltip = (chart, options)=> { + const tooltipAttr = JSON.parse(chart.customAttr).tooltip + const newPlots = [] + const linePlotList = options.plots.filter(item => item.type === 'line') + linePlotList.forEach(item => { + newPlots.push(item) + }) + const stockPlot = options.plots.filter(item => item.type === 'stock')[0] + if (!tooltipAttr.show) { + const stockOption = { + ...stockPlot.options, + tooltip: { + showContent: false + } + } + newPlots.push({ ...stockPlot, options: stockOption }) + return { + ...options, + plots: newPlots + } + } + + const showFiled = chart.data.fields + const yAxis = cloneDeep(JSON.parse(chart.yaxis)) + const customTooltipItems = originalItems => { + const formattedItems = originalItems.map(item => { + const fieldObj = showFiled.find(q => q.dataeaseName === item.name) + const displayName = fieldObj?.chartShowName || fieldObj?.name || item.name + const formattedName = displayName.startsWith('ma') ? displayName.toUpperCase() : displayName + if(!yAxis[0].formatterCfg){ + yAxis[0].formatterCfg = { + type: 'value', // auto,value,percent + unit: 1, // 换算单位 + suffix: '', // 单位后缀 + decimalCount: 3, // 小数位数 + thousandSeparator: true// 千分符 + } + } + if(yAxis[0].formatterCfg.type === 'auto'){ + yAxis[0].formatterCfg.type = 'value' + yAxis[0].formatterCfg.decimalCount = 3 + } + const formattedValue = valueFormatter(item.value, yAxis[0].formatterCfg) + return { + ...item, + name: formattedName, + value: formattedValue, + color: item.color + } + }) + + const hasKLine = formattedItems.some(item => !item.name.startsWith('MA')) + const kLines = formattedItems.filter(item => !item.name.startsWith('MA')) + return hasKLine + ? [ + { name: '日K', value: '', marker: true, color: kLines[0]?.color }, + ...kLines, + ...formattedItems.filter(item => item.name.startsWith('MA')) + ] + : formattedItems + } + const formatTooltipItem = (item) => { + const size = item.name.startsWith('MA') || !item.value ? 10 : 5 + const markerMarginRight = item.name.startsWith('MA') || !item.value ? 5 : 9 + const markerMarginLeft = item.name.startsWith('MA') || !item.value ? 0 : 2 + return ` +
  • +
    + +
    +
    + ${item.name} + ${item.name.startsWith('MA') && item.value === '0' ? '-' : item.value} +
    +
  • + ` + } + const generateCustomTooltipContent = (title, items) => { + return ` +
    +
    ${title}
    +
      + ${items.map(formatTooltipItem).join('')} +
    +
    + ` + } + const stockOption = { + ...stockPlot.options, + tooltip: { + showMarkers: true, + showCrosshairs: true, + showNil: true, + crosshairs: { + follow: true, + text: (axisType, value, data) => { + if (axisType === 'y') { + return { content: value ? value.toFixed(0) : value } + } + return { content: data[0].title, position: 'end' } + } + }, + showContent: true, + customItems: customTooltipItems, + customContent: generateCustomTooltipContent + } + } + newPlots.push({ ...stockPlot, options: stockOption }) + return { + ...options, + plots: newPlots + } +} + +export const configXAxis = (chart, options) => { + const xAxisOptions = getXAxis(chart) + if (!xAxisOptions) { + options.plots.forEach(item => { + if(item.type === 'stock'){ + item.options.xAxis = false + } + }) + return options + } + const newPlots = [] + const linePlotList = options.plots.filter(item => item.type === 'line') + + const stockPlot = options.plots.filter(item => item.type === 'stock')[0] + const newStockPlot = { + ...stockPlot, + options: { + ...stockPlot.options, + xAxis: xAxisOptions + ? { + ...stockPlot.options['xAxis'], + ...xAxisOptions + } + : { + label: false, + line: null + } + } + } + newPlots.push(newStockPlot) + linePlotList.forEach(item => { + newPlots.push(item) + }) + return { + ...options, + plots: newPlots + } +} + +export const configYAxis = (chart, options) => { + const yAxisOptions = getYAxis(chart) + if (!yAxisOptions) { + options.plots.forEach(item => { + if(item.type === 'stock'){ + item.options.yAxis = false + } + }) + return options + } + const yAxis = JSON.parse(chart.yaxis) + const newPlots = [] + const linePlotList = options.plots.filter(item => item.type === 'line') + + const stockPlot = options.plots.filter(item => item.type === 'stock')[0] + let label = false + if (yAxisOptions.label) { + label = { + ...yAxisOptions.label + } + } + const newStockPlot = { + ...stockPlot, + options: { + ...stockPlot.options, + yAxis: label + ? { + ...stockPlot.options['yAxis'], + ...yAxisOptions, + label + } + : { + label, + grid: null, + line: null + } + } + } + newPlots.push(newStockPlot) + linePlotList.forEach(item => { + newPlots.push(item) + }) + return { + ...options, + plots: newPlots + } +} + +export const customConfigEmptyDataStrategy = (chart, options) => { + const { data } = options + if (!data?.length) { + return options + } + const strategy = JSON.parse(chart.senior)?.functionCfg?.emptyDataStrategy + if (strategy === 'ignoreData') { + for (let i = data.length - 1; i >= 0; i--) { + const item = data[i] + Object.keys(item).forEach(key => { + if (key.startsWith('C_') && item[key] === null) { + data.splice(i, 1) + } + }) + } + } + const updateValues = (strategy, data) => { + data.forEach(obj => { + Object.keys(obj).forEach(key => { + if (key.startsWith('C_') && obj[key] === null) { + obj[key] = strategy === 'breakLine' ? null : 0 + } + }) + }) + } + if (strategy === 'breakLine' || strategy === 'setZero') { + updateValues(strategy, data) + } + return options +} diff --git a/core/frontend/src/views/chart/chart/common/common_table.js b/core/frontend/src/views/chart/chart/common/common_table.js index 88f4c03cb0..244cdb1aed 100644 --- a/core/frontend/src/views/chart/chart/common/common_table.js +++ b/core/frontend/src/views/chart/chart/common/common_table.js @@ -1,5 +1,9 @@ import { hexColorToRGBA, resetRgbOpacity } from '@/views/chart/chart/util' import { DEFAULT_COLOR_CASE, DEFAULT_SIZE } from '@/views/chart/chart/chart' +import Exceljs from 'exceljs' +import { saveAs } from 'file-saver' +import i18n from '@/lang' +import {Message} from "element-ui"; export function getCustomTheme(chart) { const headerColor = hexColorToRGBA(DEFAULT_COLOR_CASE.tableHeaderBgColor, DEFAULT_COLOR_CASE.alpha) @@ -292,3 +296,168 @@ export function getSize(chart) { return size } + +export async function exportPivotExcel(instance, chart) { + const { meta, fields } = instance.dataCfg + const rowLength = fields?.rows?.length || 0 + const colLength = fields?.columns?.length || 0 + const valueLength = fields?.values?.length || 0 + if (!(rowLength && valueLength)) { + Message.warning({ + message: i18n.t('chart.pivot_export_empty_fields'), + type: 'warning', + showClose: true, + duration: 5000 + }) + return + } + const workbook = new Exceljs.Workbook() + const worksheet = workbook.addWorksheet(chart.title) + const metaMap = meta?.reduce((p, n) => { + if (n.field) { + p[n.field] = n + } + return p + }, {}) + // 角头 + fields.columns?.forEach((column, index) => { + const cell = worksheet.getCell(index + 1, 1) + cell.value = metaMap[column]?.name ?? column + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (rowLength >= 2) { + worksheet.mergeCells(index + 1, 1, index + 1, rowLength) + } + }) + fields?.rows?.forEach((row, index) => { + const cell = worksheet.getCell(colLength + 1, index + 1) + cell.value = metaMap[row]?.name ?? row + cell.alignment = { vertical: 'middle', horizontal: 'center' } + }) + const { layoutResult } = instance.facet + // 行头 + const { rowLeafNodes, rowsHierarchy, rowNodes } = layoutResult + const maxColIndex = rowsHierarchy.maxLevel + 1 + const notLeafNodeHeightMap = {} + rowLeafNodes.forEach(node => { + // 行头的高度由子节点相加决定,也就是行头子节点中包含的叶子节点数量 + let curNode = node.parent + while (curNode) { + const height = notLeafNodeHeightMap[curNode.id] ?? 0 + notLeafNodeHeightMap[curNode.id] = height + 1 + curNode = curNode.parent + } + const { rowIndex } = node + const writeRowIndex = rowIndex + 1 + colLength + 1 + const writeColIndex = node.level + 1 + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + cell.value = node.label + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (writeColIndex < maxColIndex) { + worksheet.mergeCells(writeRowIndex, writeColIndex, writeRowIndex, maxColIndex) + } + }) + + const getNodeStartRowIndex = (node) => { + if (!node.children?.length) { + return node.rowIndex + 1 + } else { + return getNodeStartRowIndex(node.children[0]) + } + } + rowNodes?.forEach(node => { + if (node.isLeaf) { + return + } + const rowIndex = getNodeStartRowIndex(node) + const height = notLeafNodeHeightMap[node.id] + const writeRowIndex = rowIndex + colLength + 1 + const mergeColCount = node.children[0].level - node.level + const value = node.label + const cell = worksheet.getCell(writeRowIndex, node.level + 1) + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (mergeColCount > 1 || height > 1) { + worksheet.mergeCells( + writeRowIndex, + node.level + 1, + writeRowIndex + height - 1, + node.level + mergeColCount + ) + } + }) + + // 列头 + const { colLeafNodes, colNodes, colsHierarchy } = layoutResult + const maxColHeight = colsHierarchy.maxLevel + 1 + const notLeafNodeWidthMap = {} + colLeafNodes.forEach(node => { + // 列头的宽度由子节点相加决定,也就是列头子节点中包含的叶子节点数量 + let curNode = node.parent + while (curNode) { + const width = notLeafNodeWidthMap[curNode.id] ?? 0 + notLeafNodeWidthMap[curNode.id] = width + 1 + curNode = curNode.parent + } + const { colIndex } = node + const writeRowIndex = node.level + 1 + const writeColIndex = colIndex + 1 + rowLength + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + let value = node.label + if (node.field === '$$extra$$' && metaMap[value]?.name) { + value = metaMap[value].name + } + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (writeRowIndex < maxColHeight) { + worksheet.mergeCells(writeRowIndex, writeColIndex, maxColHeight, writeColIndex) + } + }) + const getNodeStartColIndex = (node) => { + if (!node.children?.length) { + return node.colIndex + 1 + } else { + return getNodeStartColIndex(node.children[0]) + } + } + colNodes.forEach(node => { + if (node.isLeaf) { + return + } + const colIndex = getNodeStartColIndex(node) + const width = notLeafNodeWidthMap[node.id] + const writeRowIndex = node.level + 1 + const mergeRowCount = node.children[0].level - node.level + const value = node.label + const writeColIndex = colIndex + rowLength + const cell = worksheet.getCell(writeRowIndex, writeColIndex) + cell.value = value + cell.alignment = { vertical: 'middle', horizontal: 'center' } + if (mergeRowCount > 1 || width > 1) { + worksheet.mergeCells( + writeRowIndex, + writeColIndex, + writeRowIndex + mergeRowCount - 1, + writeColIndex + width - 1 + ) + } + }) + // 单元格数据 + for (let rowIndex = 0; rowIndex < rowLeafNodes.length; rowIndex++) { + for (let colIndex = 0; colIndex < colLeafNodes.length; colIndex++) { + const dataCellMeta = layoutResult.getCellMeta(rowIndex, colIndex) + const { fieldValue } = dataCellMeta + if (fieldValue) { + const meta = metaMap[dataCellMeta.valueField] + const cell = worksheet.getCell(rowIndex + maxColHeight + 1, rowLength + colIndex + 1) + const value = meta?.formatter?.(fieldValue) || fieldValue.toString() + cell.alignment = { vertical: 'middle', horizontal: 'center' } + cell.value = value + } + } + } + const buffer = await workbook.xlsx.writeBuffer() + const dataBlob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8' + }) + saveAs(dataBlob, `${chart.title ?? '透视表'}.xlsx`) +} diff --git a/core/frontend/src/views/chart/chart/util.js b/core/frontend/src/views/chart/chart/util.js index bbaf1b0b2f..f577e96c70 100644 --- a/core/frontend/src/views/chart/chart/util.js +++ b/core/frontend/src/views/chart/chart/util.js @@ -1738,7 +1738,74 @@ export const TYPE_CONFIGS = [ ] } }, - + { + render: 'antv', + category: 'chart.chart_type_compare', + value: 'stock-line', + title: 'chart.chart_stock_line', + icon: 'stock-line', + properties: [ + 'color-selector', + 'size-selector', + 'tooltip-selector-ant-v', + 'x-axis-selector-ant-v', + 'y-axis-selector-ant-v', + 'title-selector-ant-v', + 'legend-selector-ant-v' + ], + propertyInner: { + 'color-selector': [ + 'value', + 'colorPanel', + 'customColor', + 'alpha' + ], + 'size-selector': [ + 'lineWidth', + 'lineSymbol', + 'lineSymbolSize', + 'lineSmooth' + ], + 'tooltip-selector-ant-v': [ + 'show', + 'textStyle' + ], + 'x-axis-selector-ant-v': [ + 'show', + 'position', + 'splitLine', + 'axisForm', + 'axisLabel' + ], + 'y-axis-selector-ant-v': [ + 'show', + 'position', + 'name', + 'nameTextStyle', + 'axisValue', + 'splitLine', + 'axisForm', + 'axisLabel' + ], + 'title-selector-ant-v': [ + 'show', + 'title', + 'fontSize', + 'color', + 'hPosition', + 'isItalic', + 'isBolder', + 'remarkShow', + 'fontFamily', + 'letterSpace', + 'fontShadow' + ], + 'legend-selector-ant-v': [ + 'show', + 'textStyle', + ] + } + }, { render: 'antv', category: 'chart.chart_type_distribute', diff --git a/core/frontend/src/views/chart/components/ChartComponentG2.vue b/core/frontend/src/views/chart/components/ChartComponentG2.vue index 49a66692b0..d571a0b302 100644 --- a/core/frontend/src/views/chart/components/ChartComponentG2.vue +++ b/core/frontend/src/views/chart/components/ChartComponentG2.vue @@ -45,7 +45,13 @@ import { baseLiquid } from '@/views/chart/chart/liquid/liquid' import { uuid } from 'vue-uuid' import ViewTrackBar from '@/components/canvas/components/editor/ViewTrackBar' import { adjustPosition, getRemark, hexColorToRGBA } from '@/views/chart/chart/util' -import { baseBarOptionAntV, hBaseBarOptionAntV, baseBidirectionalBarOptionAntV, timeRangeBarOptionAntV } from '@/views/chart/chart/bar/bar_antv' +import { + baseBarOptionAntV, + hBaseBarOptionAntV, + baseBidirectionalBarOptionAntV, + timeRangeBarOptionAntV, + stockLineOptionAntV +} from '@/views/chart/chart/bar/bar_antv' import { baseAreaOptionAntV, baseLineOptionAntV } from '@/views/chart/chart/line/line_antv' import { basePieOptionAntV, basePieRoseOptionAntV } from '@/views/chart/chart/pie/pie_antv' import { baseScatterOptionAntV } from '@/views/chart/chart/scatter/scatter_antv' @@ -298,6 +304,8 @@ export default { this.myChart = await baseFlowMapOption(this.chartId, chart, this.antVAction) } else if (chart.type === 'bidirectional-bar') { this.myChart = baseBidirectionalBarOptionAntV(this.chartId, chart, this.antVAction) + } else if (chart.type === 'stock-line'){ + this.myChart = stockLineOptionAntV(this.chartId, chart, this.antVAction) } if (this.myChart && !equalsAny(chart.type, 'liquid', 'flow-map') && this.searchCount > 0) { diff --git a/core/frontend/src/views/chart/components/ChartComponentS2.vue b/core/frontend/src/views/chart/components/ChartComponentS2.vue index f872da4437..8f5eed6b92 100644 --- a/core/frontend/src/views/chart/components/ChartComponentS2.vue +++ b/core/frontend/src/views/chart/components/ChartComponentS2.vue @@ -308,6 +308,7 @@ export default { this.myChart = baseTableNormal(this.chartId, chart, this.antVAction, this.tableData, this, this.columnResize) } else if (chart.type === 'table-pivot') { this.myChart = baseTablePivot(this.chartId, chart, this.antVAction, this.tableHeaderClick, this.tableData) + this.$store.dispatch('chart/setTableInstance', { viewId: this.chart.id, tableInstance: this.myChart }) } if (this.myChart && this.searchCount > 0) { diff --git a/core/frontend/src/views/chart/components/compare/CompareEdit.vue b/core/frontend/src/views/chart/components/compare/CompareEdit.vue index 6217e41514..ca8b3bc830 100644 --- a/core/frontend/src/views/chart/components/compare/CompareEdit.vue +++ b/core/frontend/src/views/chart/components/compare/CompareEdit.vue @@ -17,12 +17,30 @@ + + + + + + ele.deType === 1)) + t1.push(...this.quotaData.filter(ele => ele.deType === 1)) + } + this.fieldList = t1 // 如果没有选中字段,则默认选中第一个 if ((!this.compareItem.compareCalc.field || this.compareItem.compareCalc.field === '') && this.fieldList.length > 0) { @@ -147,6 +188,36 @@ export default { if ((!this.compareItem.compareCalc.type || this.compareItem.compareCalc.type === '' || this.compareItem.compareCalc.type === 'none') && this.compareList.length > 0) { this.compareItem.compareCalc.type = this.compareList[0].value } + }, + changeDateFormatter() { + const checkedField = this.fieldList.filter(ele => ele.id === this.compareItem.compareCalc.field) + if (checkedField && checkedField.length > 0) { + checkedField[0].dateStyle = this.dateFormatter + if (!this.compareItem.compareCalc.custom) { + this.compareItem.compareCalc.custom = { timeType: 'y_M_d' } + } + this.compareItem.compareCalc.custom.timeType = this.dateFormatter + } + this.initCompareType() + }, + initDateFormatter() { + const timeType = this.compareItem.compareCalc.custom?.timeType + if (this.isIndicator && timeType) { + this.dateFormatter = timeType==='0'?'y_M_d':timeType + this.changeDateFormatter() + } + }, + fieldFormatter(field) { + if (this.isIndicator) { + return field.name + } else { + return field.name + '(' + this.$t('chart.' + field.dateStyle) + ')' + } + } + }, + computed: { + isIndicator() { + return this.chart.type === 'text' } } } diff --git a/core/frontend/src/views/chart/components/dragItem/QuotaItem.vue b/core/frontend/src/views/chart/components/dragItem/QuotaItem.vue index 8109d24d35..c1d6e38eaa 100644 --- a/core/frontend/src/views/chart/components/dragItem/QuotaItem.vue +++ b/core/frontend/src/views/chart/components/dragItem/QuotaItem.vue @@ -323,6 +323,11 @@ export default { } }, isEnableCompare() { + // 指标卡直接放行同环比配置 + if (this.chart.type === 'text') { + this.disableEditCompare = false + return + } let xAxis = null if (Object.prototype.toString.call(this.chart.xaxis) === '[object Array]') { xAxis = JSON.parse(JSON.stringify(this.chart.xaxis)) diff --git a/core/frontend/src/views/chart/components/senior/FunctionCfg.vue b/core/frontend/src/views/chart/components/senior/FunctionCfg.vue index e5b99b5135..44faea9835 100644 --- a/core/frontend/src/views/chart/components/senior/FunctionCfg.vue +++ b/core/frontend/src/views/chart/components/senior/FunctionCfg.vue @@ -78,7 +78,7 @@ v-model="functionForm.emptyDataStrategy" @change="changeFunctionCfg" > - {{ $t('chart.break_line') }} + {{ $t('chart.break_line') }} {{ $t('chart.set_zero') }} import { DEFAULT_FUNCTION_CFG, COLOR_PANEL } from '../../chart/chart' import { equalsAny, includesAny } from '@/utils/StringUtils' +import {cloneDeep} from "lodash"; export default { name: 'FunctionCfg', @@ -134,7 +135,7 @@ export default { showSlider() { return this.chart.type !== 'bidirectional-bar' && !equalsAny(this.chart.type, 'map') && - !includesAny(this.chart.type, 'table', 'text') + !includesAny(this.chart.type, 'table', 'text','stock-line') }, showEmptyStrategy() { return (this.chart.render === 'antv' && @@ -148,7 +149,10 @@ export default { return this.showEmptyStrategy && includesAny(this.chart.type, 'table') && this.functionForm.emptyDataStrategy !== 'breakLine' - } + }, + showBreakOption() { + return !equalsAny(this.chart.type, 'stock-line') + }, }, watch: { 'chart': { @@ -170,10 +174,14 @@ export default { } else { senior = JSON.parse(chart.senior) } + const defaultFunctionCfg = cloneDeep(DEFAULT_FUNCTION_CFG) + if (equalsAny(this.chart.type, 'stock-line')) { + defaultFunctionCfg.emptyDataStrategy = 'setZero' + } if (senior.functionCfg) { - this.functionForm = { ...DEFAULT_FUNCTION_CFG, ...senior.functionCfg } + this.functionForm = { ...defaultFunctionCfg, ...senior.functionCfg } } else { - this.functionForm = JSON.parse(JSON.stringify(DEFAULT_FUNCTION_CFG)) + this.functionForm = JSON.parse(JSON.stringify(defaultFunctionCfg)) } this.initFieldCtrl() } diff --git a/core/frontend/src/views/chart/view/ChartEdit.vue b/core/frontend/src/views/chart/view/ChartEdit.vue index 629d1dde15..d579a0de56 100644 --- a/core/frontend/src/views/chart/view/ChartEdit.vue +++ b/core/frontend/src/views/chart/view/ChartEdit.vue @@ -1772,6 +1772,8 @@
    diff --git a/core/frontend/src/views/dataFilling/form/create.vue b/core/frontend/src/views/dataFilling/form/create.vue index 8c219c864f..b7e5b94969 100644 --- a/core/frontend/src/views/dataFilling/form/create.vue +++ b/core/frontend/src/views/dataFilling/form/create.vue @@ -980,6 +980,7 @@ export default { :placeholder="item.settings.placeholder" style="width: 100%" size="small" + filterable :multiple="item.settings.multiple" clearable > diff --git a/core/mobile/package.json b/core/mobile/package.json index ea791fcfbb..1a7ac46a68 100644 --- a/core/mobile/package.json +++ b/core/mobile/package.json @@ -1,6 +1,6 @@ { "name": "dataease-mobile", - "version": "1.18.24", + "version": "1.18.25", "private": true, "scripts": { "serve": "npm run dev:h5", diff --git a/pom.xml b/pom.xml index 3917421731..42bbf4e53f 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ pom - 1.18.24 + 1.18.25 4.1.1 3.1.0 9.0.64