Merge pull request #9891 from dataease/pr@dev-v2_export_data

Pr@dev v2 export data
This commit is contained in:
taojinlong 2024-05-27 19:13:37 +08:00 committed by GitHub
commit 6e6df50914
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 2761 additions and 32 deletions

View File

@ -1,6 +1,6 @@
FROM registry.cn-qingdao.aliyuncs.com/dataease/alpine-openjdk21-jre FROM registry.cn-qingdao.aliyuncs.com/dataease/alpine-openjdk21-jre
RUN mkdir -p /opt/apps/config /opt/dataease2.0/drivers/ /opt/dataease2.0/cache/ /opt/dataease2.0/data/map /opt/dataease2.0/data/static-resource/ /opt/dataease2.0/data/appearance/ RUN mkdir -p /opt/apps/config /opt/dataease2.0/drivers/ /opt/dataease2.0/cache/ /opt/dataease2.0/data/map /opt/dataease2.0/data/static-resource/ /opt/dataease2.0/data/appearance/ /opt/dataease2.0/data/exportData/
ADD drivers/* /opt/dataease2.0/drivers/ ADD drivers/* /opt/dataease2.0/drivers/
ADD mapFiles/ /opt/dataease2.0/data/map/ ADD mapFiles/ /opt/dataease2.0/data/map/

View File

@ -83,6 +83,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.fit2cloud</groupId> <groupId>com.fit2cloud</groupId>
<artifactId>quartz-spring-boot-starter</artifactId> <artifactId>quartz-spring-boot-starter</artifactId>

View File

@ -5,13 +5,16 @@ import io.dataease.api.chart.dto.ChartViewDTO;
import io.dataease.api.chart.dto.ViewDetailField; import io.dataease.api.chart.dto.ViewDetailField;
import io.dataease.api.chart.request.ChartExcelRequest; import io.dataease.api.chart.request.ChartExcelRequest;
import io.dataease.chart.manage.ChartDataManage; import io.dataease.chart.manage.ChartDataManage;
import io.dataease.constant.AuthConstant;
import io.dataease.constant.CommonConstants; import io.dataease.constant.CommonConstants;
import io.dataease.engine.constant.DeTypeConstants; import io.dataease.engine.constant.DeTypeConstants;
import io.dataease.exception.DEException; import io.dataease.exception.DEException;
import io.dataease.exportCenter.manage.ExportCenterManage;
import io.dataease.result.ResultCode; import io.dataease.result.ResultCode;
import io.dataease.utils.LogUtil; import io.dataease.utils.LogUtil;
import io.dataease.visualization.manage.VisualizationTemplateExtendDataManage; import io.dataease.visualization.manage.VisualizationTemplateExtendDataManage;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
@ -22,12 +25,13 @@ import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -38,10 +42,11 @@ import java.util.stream.Collectors;
public class ChartDataServer implements ChartDataApi { public class ChartDataServer implements ChartDataApi {
@Resource @Resource
private ChartDataManage chartDataManage; private ChartDataManage chartDataManage;
@Resource
private ExportCenterManage exportCenterManage;
@Resource @Resource
private VisualizationTemplateExtendDataManage extendDataManage; private VisualizationTemplateExtendDataManage extendDataManage;
@Value("${export.views.limit:500000}") @Value("${export.views.limit:500000}")
private Integer limit; private Integer limit;
@ -76,6 +81,12 @@ public class ChartDataServer implements ChartDataApi {
@Override @Override
public void innerExportDetails(ChartExcelRequest request, HttpServletResponse response) throws Exception { public void innerExportDetails(ChartExcelRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String linkToken = httpServletRequest.getHeader(AuthConstant.LINK_TOKEN_KEY);
if (StringUtils.isEmpty(linkToken)) {
exportCenterManage.addTask(request.getViewId(), "chart", request);
return;
}
OutputStream outputStream = response.getOutputStream(); OutputStream outputStream = response.getOutputStream();
try { try {
findExcelData(request); findExcelData(request);

Binary file not shown.

View File

@ -0,0 +1,159 @@
package io.dataease.exportCenter.dao.auto.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
/**
* <p>
* 导出任务表
* </p>
*
* @author fit2cloud
* @since 2024-05-23
*/
@TableName("core_export_task")
public class CoreExportTask implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
private Long userId;
private String fileName;
private Double fileSize;
private String fileSizeUnit;
private String exportFrom;
private String exportStatus;
private String exportFromType;
private Long exportTime;
private String exportProgress;
private String exportMachineName;
/**
* 过滤参数
*/
private String params;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public Double getFileSize() {
return fileSize;
}
public void setFileSize(Double fileSize) {
this.fileSize = fileSize;
}
public String getFileSizeUnit() {
return fileSizeUnit;
}
public void setFileSizeUnit(String fileSizeUnit) {
this.fileSizeUnit = fileSizeUnit;
}
public String getExportFrom() {
return exportFrom;
}
public void setExportFrom(String exportFrom) {
this.exportFrom = exportFrom;
}
public String getExportStatus() {
return exportStatus;
}
public void setExportStatus(String exportStatus) {
this.exportStatus = exportStatus;
}
public String getExportFromType() {
return exportFromType;
}
public void setExportFromType(String exportFromType) {
this.exportFromType = exportFromType;
}
public Long getExportTime() {
return exportTime;
}
public void setExportTime(Long exportTime) {
this.exportTime = exportTime;
}
public String getExportProgress() {
return exportProgress;
}
public void setExportProgress(String exportProgress) {
this.exportProgress = exportProgress;
}
public String getExportMachineName() {
return exportMachineName;
}
public void setExportMachineName(String exportMachineName) {
this.exportMachineName = exportMachineName;
}
public String getParams() {
return params;
}
public void setParams(String params) {
this.params = params;
}
@Override
public String toString() {
return "CoreExportTask{" +
"id = " + id +
", userId = " + userId +
", fileName = " + fileName +
", fileSize = " + fileSize +
", fileSizeUnit = " + fileSizeUnit +
", exportFrom = " + exportFrom +
", exportStatus = " + exportStatus +
", exportFromType = " + exportFromType +
", exportTime = " + exportTime +
", exportProgress = " + exportProgress +
", exportMachineName = " + exportMachineName +
", params = " + params +
"}";
}
}

View File

@ -0,0 +1,18 @@
package io.dataease.exportCenter.dao.auto.mapper;
import io.dataease.exportCenter.dao.auto.entity.CoreExportTask;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* 导出任务表 Mapper 接口
* </p>
*
* @author fit2cloud
* @since 2024-05-23
*/
@Mapper
public interface CoreExportTaskMapper extends BaseMapper<CoreExportTask> {
}

View File

@ -0,0 +1,50 @@
package io.dataease.exportCenter.server;
import io.dataease.api.exportCenter.ExportCenterApi;
import io.dataease.api.exportCenter.vo.ExportTaskDTO;
import io.dataease.exportCenter.manage.ExportCenterManage;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/exportCenter")
@Transactional(rollbackFor = Exception.class)
public class ExportCenterServer implements ExportCenterApi {
@Resource
private ExportCenterManage exportCenterManage;
@Override
public List<ExportTaskDTO> exportTasks(String status) {
return exportCenterManage.exportTasks(status);
}
@Override
public void delete(String id) {
exportCenterManage.delete(id);
}
@Override
public void delete(List<String> ids) {
exportCenterManage.delete(ids);
}
@Override
public void deleteAll(String type) {
exportCenterManage.deleteAll(type);
}
@Override
public void download(String id, HttpServletResponse response) throws Exception {
exportCenterManage.download(id, response);
}
@Override
public void retry(String id) {
exportCenterManage.retry(id);
}
}

View File

@ -13,6 +13,7 @@ import io.dataease.api.visualization.vo.DataVisualizationVO;
import io.dataease.api.visualization.vo.VisualizationResourceVO; import io.dataease.api.visualization.vo.VisualizationResourceVO;
import io.dataease.api.visualization.vo.VisualizationWatermarkVO; import io.dataease.api.visualization.vo.VisualizationWatermarkVO;
import io.dataease.chart.dao.auto.entity.CoreChartView; import io.dataease.chart.dao.auto.entity.CoreChartView;
import io.dataease.chart.dao.auto.mapper.CoreChartViewMapper;
import io.dataease.chart.manage.ChartDataManage; import io.dataease.chart.manage.ChartDataManage;
import io.dataease.chart.manage.ChartViewManege; import io.dataease.chart.manage.ChartViewManege;
import io.dataease.commons.constants.DataVisualizationConstants; import io.dataease.commons.constants.DataVisualizationConstants;
@ -41,6 +42,7 @@ import io.dataease.visualization.dao.auto.mapper.VisualizationWatermarkMapper;
import io.dataease.visualization.dao.ext.mapper.ExtDataVisualizationMapper; import io.dataease.visualization.dao.ext.mapper.ExtDataVisualizationMapper;
import io.dataease.visualization.manage.CoreVisualizationManage; import io.dataease.visualization.manage.CoreVisualizationManage;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@ -48,10 +50,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -64,6 +63,8 @@ public class DataVisualizationServer implements DataVisualizationApi {
@Resource @Resource
private ChartViewManege chartViewManege; private ChartViewManege chartViewManege;
@Resource
private CoreChartViewMapper coreChartViewMapper;
@Resource @Resource
private ExtDataVisualizationMapper extDataVisualizationMapper; private ExtDataVisualizationMapper extDataVisualizationMapper;
@ -391,4 +392,42 @@ public class DataVisualizationServer implements DataVisualizationApi {
} }
} }
public String getAbsPath(String id) {
CoreChartView coreChartView = coreChartViewMapper.selectById(id);
if (coreChartView == null) {
return null;
}
if (coreChartView.getSceneId() == null) {
return coreChartView.getTitle();
}
List<DataVisualizationInfo> parents = getParents(coreChartView.getSceneId());
StringBuilder stringBuilder = new StringBuilder();
parents.forEach(ele -> {
if (ObjectUtils.isNotEmpty(ele)) {
stringBuilder.append(ele.getName()).append("/");
}
});
stringBuilder.append(coreChartView.getTitle());
return stringBuilder.toString();
}
public List<DataVisualizationInfo> getParents(Long id) {
List<DataVisualizationInfo> list = new ArrayList<>();
DataVisualizationInfo dataVisualizationInfo = visualizationInfoMapper.selectById(id);
list.add(dataVisualizationInfo);
getParent(list, dataVisualizationInfo);
Collections.reverse(list);
return list;
}
public void getParent(List<DataVisualizationInfo> list, DataVisualizationInfo dataVisualizationInfo) {
if (ObjectUtils.isNotEmpty(dataVisualizationInfo)) {
if (dataVisualizationInfo.getPid() != null) {
DataVisualizationInfo d = visualizationInfoMapper.selectById(dataVisualizationInfo.getPid());
list.add(d);
getParent(list, d);
}
}
}
} }

Binary file not shown.

View File

@ -0,0 +1,33 @@
package io.dataease.websocket.aop;
import io.dataease.websocket.entity.WsMessage;
import io.dataease.websocket.service.WsService;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Aspect
@Component
public class WSTrigger {
@Autowired
private WsService wsService;
@AfterReturning(value = "execution(* io.dataease.service.message.service.strategy.SendStation.sendMsg(..))")
public void after(JoinPoint point) {
Object[] args = point.getArgs();
Optional.ofNullable(args).ifPresent(objs -> {
if (ArrayUtils.isEmpty(objs)) return;
Object arg = args[0];
Long userId = (Long) arg;
WsMessage message = new WsMessage(userId, "/web-msg-topic", "refresh");
wsService.releaseMessage(message);
});
}
}

View File

@ -0,0 +1,38 @@
package io.dataease.websocket.config;
import io.dataease.websocket.factory.DeWsHandlerFactory;
import io.dataease.websocket.handler.PrincipalHandshakeHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
@Configuration
@EnableWebSocketMessageBroker
public class WsConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket")
.setAllowedOriginPatterns("*")
.setHandshakeHandler(new PrincipalHandshakeHandler())
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/user");
registry.setUserDestinationPrefix("/user");
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
registry.addDecoratorFactory(new DeWsHandlerFactory());
registry.setMessageSizeLimit(8192) //设置消息字节数大小
.setSendBufferSizeLimit(8192)//设置消息缓存大小
.setSendTimeLimit(10000); //设置消息发送时间限制毫秒
}
}

View File

@ -0,0 +1,17 @@
package io.dataease.websocket.entity;
import java.security.Principal;
public class DePrincipal implements Principal {
public DePrincipal(String name) {
this.name = name;
}
private String name;
@Override
public String getName() {
return name;
}
}

View File

@ -0,0 +1,21 @@
package io.dataease.websocket.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WsMessage<T> implements Serializable {
private Long userId;
private String topic;
private T data;
}

View File

@ -0,0 +1,40 @@
package io.dataease.websocket.factory;
import io.dataease.websocket.util.WsUtil;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
import java.util.Optional;
public class DeWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
public DeWebSocketHandlerDecorator(WebSocketHandler delegate) {
super(delegate);
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Optional.ofNullable(session.getPrincipal()).ifPresent(principal -> {
String name = principal.getName();
Long userId = Long.parseLong(name);
WsUtil.onLine(userId);
});
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
Optional.ofNullable(session.getPrincipal()).ifPresent(principal -> {
String name = principal.getName();
Long userId = Long.parseLong(name);
WsUtil.offLine(userId);
});
super.afterConnectionClosed(session, closeStatus);
}
}

View File

@ -0,0 +1,13 @@
package io.dataease.websocket.factory;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;
public class DeWsHandlerFactory implements WebSocketHandlerDecoratorFactory {
@Override
public WebSocketHandler decorate(WebSocketHandler webSocketHandler) {
return new DeWebSocketHandlerDecorator(webSocketHandler);
}
}

View File

@ -0,0 +1,29 @@
package io.dataease.websocket.handler;
import io.dataease.websocket.entity.DePrincipal;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import java.security.Principal;
import java.util.Map;
public class PrincipalHandshakeHandler extends DefaultHandshakeHandler {
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;
HttpServletRequest httpRequest = servletServerHttpRequest.getServletRequest();
final String userId = httpRequest.getParameter("userId");
if (StringUtils.isEmpty(userId)) {
return null;
}
return new DePrincipal(userId);
}
return null;
}
}

View File

@ -0,0 +1,11 @@
package io.dataease.websocket.service;
import io.dataease.websocket.entity.WsMessage;
public interface WsService {
void releaseMessage(WsMessage wsMessage);
}

View File

@ -0,0 +1,21 @@
package io.dataease.websocket.service.impl;
import io.dataease.websocket.entity.WsMessage;
import io.dataease.websocket.service.WsService;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
@Service
public class StandaloneWsService implements WsService {
@Resource
private SimpMessagingTemplate messagingTemplate;
public void releaseMessage(WsMessage wsMessage){
if(ObjectUtils.isEmpty(wsMessage) || ObjectUtils.isEmpty(wsMessage.getUserId()) || ObjectUtils.isEmpty(wsMessage.getTopic())) return;
messagingTemplate.convertAndSendToUser(String.valueOf(wsMessage.getUserId()), wsMessage.getTopic(),wsMessage.getData());
}
}

View File

@ -0,0 +1,41 @@
package io.dataease.websocket.util;
import io.dataease.auth.bo.TokenUserBO;
import io.dataease.utils.AuthUtils;
import org.apache.commons.lang3.ObjectUtils;
import java.util.concurrent.CopyOnWriteArraySet;
public class WsUtil {
private static final CopyOnWriteArraySet<Long> ONLINE_USERS = new CopyOnWriteArraySet();
public static boolean onLine() {
TokenUserBO user = AuthUtils.getUser();
if (ObjectUtils.isNotEmpty(user) && ObjectUtils.isNotEmpty(user.getUserId()))
return onLine(user.getUserId());
return false;
}
public static boolean onLine(Long userId) {
return ONLINE_USERS.add(userId);
}
public static boolean offLine() {
TokenUserBO user = AuthUtils.getUser();
if (ObjectUtils.isNotEmpty(user) && ObjectUtils.isNotEmpty(user.getUserId()))
return offLine(user.getUserId());
return false;
}
public static boolean offLine(Long userId) {
return ONLINE_USERS.remove(userId);
}
public static boolean isOnLine(Long userId) {
return ONLINE_USERS.contains(userId);
}
}

View File

@ -24,6 +24,24 @@ ALTER TABLE `xpack_setting_authentication`
ADD COLUMN `valid` tinyint(1) NOT NULL DEFAULT 0 COMMENT '有效' AFTER `synced`; ADD COLUMN `valid` tinyint(1) NOT NULL DEFAULT 0 COMMENT '有效' AFTER `synced`;
DROP TABLE IF EXISTS `core_export_task`;
CREATE TABLE `core_export_task`
(
`id` varchar(255) NOT NULL,
`user_id` bigint(20) NOT NULL,
`file_name` varchar(2048) DEFAULT NULL,
`file_size` DOUBLE DEFAULT NULL,
`file_size_unit` varchar(255) DEFAULT NULL,
`export_from` varchar(255) DEFAULT NULL,
`export_status` varchar(255) DEFAULT NULL,
`export_from_type` varchar(255) DEFAULT NULL,
`export_time` bigint(20) DEFAULT NULL,
`export_progress` varchar(255) DEFAULT NULL,
`export_machine_name` varchar(512) DEFAULT NULL,
`params` longtext NOT NULL COMMENT '过滤参数',
PRIMARY KEY (`id`)
) COMMENT='导出任务表';
DROP TABLE IF EXISTS `xpack_platform_token`; DROP TABLE IF EXISTS `xpack_platform_token`;
CREATE TABLE `xpack_platform_token` CREATE TABLE `xpack_platform_token`
( (
@ -33,3 +51,4 @@ CREATE TABLE `xpack_platform_token`
`exp_time` bigint NOT NULL, `exp_time` bigint NOT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
); );

View File

@ -38,6 +38,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mathjs": "^11.6.0", "mathjs": "^11.6.0",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"net": "^1.0.2",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.32", "pinia": "^2.0.32",
@ -63,6 +64,8 @@
"@types/element-resize-detector": "^1.1.3", "@types/element-resize-detector": "^1.1.3",
"@types/jquery": "^3.5.16", "@types/jquery": "^3.5.16",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.6",
"@types/sockjs-client": "^1.5.4",
"@types/stompjs": "^2.3.9",
"@typescript-eslint/eslint-plugin": "^5.53.0", "@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0", "@typescript-eslint/parser": "^5.53.0",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
@ -80,6 +83,8 @@
"postcss-scss": "^4.0.6", "postcss-scss": "^4.0.6",
"prettier": "^2.8.4", "prettier": "^2.8.4",
"rimraf": "^4.1.2", "rimraf": "^4.1.2",
"sockjs-client": "^1.6.1",
"stompjs": "^2.3.3",
"stylelint": "^15.2.0", "stylelint": "^15.2.0",
"stylelint-config-html": "^1.1.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-recommended": "^10.0.1", "stylelint-config-recommended": "^10.0.1",
@ -96,6 +101,7 @@
"vite-plugin-style-import-secondary": "^2.0.0", "vite-plugin-style-import-secondary": "^2.0.0",
"vite-plugin-stylelint": "^4.2.0", "vite-plugin-stylelint": "^4.2.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^1.0.24" "vue-tsc": "^1.0.24",
"xss": "^1.0.14"
} }
} }

View File

@ -250,3 +250,39 @@ export const getFunction = async (): Promise<DatasetDetail[]> => {
return res?.data return res?.data
}) })
} }
export const exportTasks = async (type): Promise<IResponse> => {
return request.post({ url: '/exportCenter/exportTasks/' + type, data: {} }).then(res => {
return res
})
}
export const exportRetry = async (id): Promise<IResponse> => {
return request.post({ url: '/exportCenter/retry/' + id, data: {} }).then(res => {
return res?.data
})
}
export const downloadFile = async (id): Promise<Blob> => {
return request.get({ url: 'exportCenter/download/' + id, responseType: 'blob' }).then(res => {
return res?.data
})
}
export const exportDelete = async (id): Promise<IResponse> => {
return request.get({ url: '/exportCenter/delete/' + id }).then(res => {
return res?.data
})
}
export const exportDeleteAll = async (type, data): Promise<IResponse> => {
return request.post({ url: '/exportCenter/deleteAll/' + type, data }).then(res => {
return res?.data
})
}
export const exportDeletePost = async (data): Promise<IResponse> => {
return request.post({ url: '/exportCenter/delete', data }).then(res => {
return res?.data
})
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1716540076970" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7068" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M912 480c-17.7 0-32 14.3-32 32 0 25-2.5 50-7.5 74.2-4.8 23.6-12 46.8-21.4 69-9.2 21.8-20.6 42.8-33.9 62.5-13.2 19.5-28.3 37.8-45 54.5s-35 31.8-54.5 45c-19.7 13.3-40.7 24.7-62.5 33.9-22.2 9.4-45.4 16.6-69 21.4-48.5 9.9-99.9 9.9-148.4 0-23.6-4.8-46.8-12-69-21.4-21.8-9.2-42.8-20.6-62.5-33.9-19.5-13.2-37.8-28.3-54.5-45s-31.8-35-45-54.5c-13.3-19.7-24.7-40.7-33.9-62.5-9.4-22.2-16.6-45.4-21.4-69-5-24.2-7.5-49.2-7.5-74.2s2.5-50 7.5-74.2c4.8-23.6 12-46.8 21.4-69 9.2-21.8 20.6-42.8 33.9-62.5 13.2-19.5 28.3-37.8 45-54.5s35-31.8 54.5-45c19.7-13.3 40.7-24.7 62.5-33.9 22.2-9.4 45.4-16.6 69-21.4 48.5-9.9 99.9-9.9 148.4 0 23.6 4.8 46.8 12 69 21.4 21.8 9.2 42.8 20.6 62.5 33.9 19.5 13.2 37.8 28.3 54.5 45 1.4 1.4 2.8 2.8 4.1 4.2H688c-17.7 0-32 14.3-32 32s14.3 32 32 32h160c17.7 0 32-14.3 32-32V128c0-17.7-14.3-32-32-32s-32 14.3-32 32v77.1c-19.2-19-40.1-36.2-62.4-51.3-23.1-15.6-47.8-29-73.4-39.8-26.1-11-53.4-19.5-81.1-25.2-56.9-11.6-117.1-11.6-174.1 0-27.8 5.7-55.1 14.2-81.1 25.2-25.6 10.8-50.3 24.2-73.4 39.8-22.9 15.4-44.4 33.2-63.9 52.7s-37.3 41-52.7 63.9c-15.6 23.1-29 47.8-39.8 73.4-11 26.1-19.5 53.4-25.2 81.1C83 453.4 80 482.7 80 512s3 58.6 8.8 87c5.7 27.8 14.2 55 25.2 81.1 10.8 25.6 24.2 50.3 39.8 73.4 15.4 22.9 33.2 44.4 52.7 63.9s41 37.3 63.9 52.7c23.1 15.6 47.8 29 73.4 39.8 26.1 11 53.4 19.5 81.1 25.2 28.5 5.8 57.7 8.8 87 8.8s58.6-3 87-8.8c27.8-5.7 55-14.2 81.1-25.2 25.6-10.8 50.3-24.2 73.4-39.8 22.9-15.5 44.4-33.2 63.9-52.7s37.3-41 52.7-63.9c15.6-23.1 29-47.8 39.8-73.4 11-26.1 19.5-53.4 25.2-81.1 5.8-28.5 8.8-57.7 8.8-87 0.2-17.7-14.1-32-31.8-32z" fill="#1875F0" p-id="7069"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -146,7 +146,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onBeforeUnmount, onMounted, reactive, toRefs, watch } from 'vue' import { computed, h, onBeforeUnmount, onMounted, reactive, toRefs, watch } from 'vue'
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain' import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
@ -156,7 +156,8 @@ import { useEmitt } from '@/hooks/web/useEmitt'
import { copyStoreWithOut } from '@/store/modules/data-visualization/copy' import { copyStoreWithOut } from '@/store/modules/data-visualization/copy'
import { exportExcelDownload } from '@/views/chart/components/js/util' import { exportExcelDownload } from '@/views/chart/components/js/util'
import FieldsList from '@/custom-component/rich-text/FieldsList.vue' import FieldsList from '@/custom-component/rich-text/FieldsList.vue'
import { ElTooltip } from 'element-plus-secondary' import { ElMessage, ElTooltip } from 'element-plus-secondary'
import { Button } from 'vant'
const dvMainStore = dvMainStoreWithOut() const dvMainStore = dvMainStoreWithOut()
const snapshotStore = snapshotStoreWithOut() const snapshotStore = snapshotStoreWithOut()
const copyStore = copyStoreWithOut() const copyStore = copyStoreWithOut()
@ -312,12 +313,48 @@ const showBarTooltipPosition = computed(() => {
} }
}) })
const openMessageLoading = cb => {
const iconClass = `el-icon-loading`
const customClass = `de-message-loading de-message-export`
ElMessage({
message: h('p', null, [
'后台导出中,可前往',
h(
Button,
{
props: {
type: 'text',
size: 'mini'
},
class: 'btn-text',
on: {
click: () => {
cb()
}
}
},
'数据导出中心'
),
'查看进度,进行下载'
]),
iconClass,
showClose: true,
customClass
})
}
const callbackExport = () => {
useEmitt().emitter.emit('data-export-center')
}
const exportAsExcel = () => { const exportAsExcel = () => {
const viewDataInfo = dvMainStore.getViewDataDetails(element.value.id) const viewDataInfo = dvMainStore.getViewDataDetails(element.value.id)
const chartExtRequest = dvMainStore.getLastViewRequestInfo(element.value.id) const chartExtRequest = dvMainStore.getLastViewRequestInfo(element.value.id)
const viewInfo = dvMainStore.getViewDetails(element.value.id) const viewInfo = dvMainStore.getViewDetails(element.value.id)
const chart = { ...viewInfo, chartExtRequest, data: viewDataInfo } const chart = { ...viewInfo, chartExtRequest, data: viewDataInfo }
exportExcelDownload(chart) exportExcelDownload(chart, () => {
openMessageLoading(callbackExport)
})
} }
const exportAsImage = () => { const exportAsImage = () => {
// do export // do export

View File

@ -71,7 +71,7 @@
<script setup lang="ts"> <script setup lang="ts">
import ComponentWrapper from '@/components/data-visualization/canvas/ComponentWrapper.vue' import ComponentWrapper from '@/components/data-visualization/canvas/ComponentWrapper.vue'
import { computed, nextTick, ref } from 'vue' import { computed, h, nextTick, ref } from 'vue'
import { toPng } from 'html-to-image' import { toPng } from 'html-to-image'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { deepCopy } from '@/utils/utils' import { deepCopy } from '@/utils/utils'
@ -81,6 +81,8 @@ import { exportExcelDownload } from '@/views/chart/components/js/util'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { assign } from 'lodash-es' import { assign } from 'lodash-es'
import { useEmitt } from '@/hooks/web/useEmitt' import { useEmitt } from '@/hooks/web/useEmitt'
import { Button } from 'vant'
import { ElMessage } from 'element-plus-secondary'
const downLoading = ref(false) const downLoading = ref(false)
const dvMainStore = dvMainStoreWithOut() const dvMainStore = dvMainStoreWithOut()
const dialogShow = ref(false) const dialogShow = ref(false)
@ -201,10 +203,45 @@ const downloadViewDetails = () => {
} }
exportLoading.value = true exportLoading.value = true
exportExcelDownload(chart, () => { exportExcelDownload(chart, () => {
console.log('aa')
openMessageLoading(exportData)
exportLoading.value = false exportLoading.value = false
}) })
} }
const exportData = () => {
// bus.$emit('data-export-center')
}
const openMessageLoading = cb => {
const iconClass = `el-icon-loading`
const customClass = `de-message-loading de-message-export`
ElMessage({
message: h('p', null, [
'后台导出中,可前往',
h(
Button,
{
props: {
type: 'text',
size: 'mini'
},
class: 'btn-text',
on: {
click: () => {
cb()
}
}
},
'数据导出中心'
),
'查看进度,进行下载'
]),
iconClass,
showClose: true,
customClass
})
}
const htmlToImage = () => { const htmlToImage = () => {
downLoading.value = true downLoading.value = true
useEmitt().emitter.emit('renderChart-' + viewInfo.value.id) useEmitt().emitter.emit('renderChart-' + viewInfo.value.id)

View File

@ -4,6 +4,7 @@ import { usePermissionStore } from '@/store/modules/permission'
import { isExternal } from '@/utils/validate' import { isExternal } from '@/utils/validate'
import { formatRoute } from '@/router/establish' import { formatRoute } from '@/router/establish'
import HeaderMenuItem from './HeaderMenuItem.vue' import HeaderMenuItem from './HeaderMenuItem.vue'
import { useEmitt } from '@/hooks/web/useEmitt'
import { Icon } from '@/components/icon-custom' import { Icon } from '@/components/icon-custom'
import { ElHeader, ElMenu } from 'element-plus-secondary' import { ElHeader, ElMenu } from 'element-plus-secondary'
import SystemCfg from './SystemCfg.vue' import SystemCfg from './SystemCfg.vue'
@ -15,8 +16,8 @@ import { isDesktop } from '@/utils/ModelUtil'
import { XpackComponent } from '@/components/plugin' import { XpackComponent } from '@/components/plugin'
import { useAppearanceStoreWithOut } from '@/store/modules/appearance' import { useAppearanceStoreWithOut } from '@/store/modules/appearance'
import AiComponent from '@/layout/components/AiComponent.vue' import AiComponent from '@/layout/components/AiComponent.vue'
import { useEmitt } from '@/hooks/web/useEmitt'
import { findBaseParams } from '@/api/aiComponent' import { findBaseParams } from '@/api/aiComponent'
import ExportExcel from '@/views/visualized/data/dataset/ExportExcel.vue'
import AiTips from '@/layout/components/AiTips.vue' import AiTips from '@/layout/components/AiTips.vue'
const appearanceStore = useAppearanceStoreWithOut() const appearanceStore = useAppearanceStoreWithOut()
@ -42,7 +43,10 @@ const activeIndex = computed(() => {
}) })
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
const ExportExcelRef = ref()
const downloadClick = () => {
ExportExcelRef.value.init()
}
const routers: any[] = formatRoute(permissionStore.getRoutersNotHidden as AppCustomRouteRecordRaw[]) const routers: any[] = formatRoute(permissionStore.getRoutersNotHidden as AppCustomRouteRecordRaw[])
const showSystem = ref(false) const showSystem = ref(false)
const showToolbox = ref(false) const showToolbox = ref(false)
@ -87,6 +91,10 @@ onMounted(() => {
initShowSystem() initShowSystem()
initShowToolbox() initShowToolbox()
initAiBase() initAiBase()
useEmitt({
name: 'data-export-center',
callback: downloadClick
})
}) })
</script> </script>
@ -118,6 +126,9 @@ onMounted(() => {
> >
<Icon name="dv-ai" @click="handleAiClick" /> <Icon name="dv-ai" @click="handleAiClick" />
</el-icon> </el-icon>
<el-icon style="margin: 0 10px">
<Icon name="dv-preview-download" @click="downloadClick" />
</el-icon>
<ai-tips <ai-tips
@confirm="aiTipsConfirm" @confirm="aiTipsConfirm"
v-if="showOverlay && appearanceStore.getShowAi" v-if="showOverlay && appearanceStore.getShowAi"
@ -134,6 +145,7 @@ onMounted(() => {
<div v-if="showOverlay && appearanceStore.getShowAi" class="overlay"></div> <div v-if="showOverlay && appearanceStore.getShowAi" class="overlay"></div>
</div> </div>
</el-header> </el-header>
<ExportExcel ref="ExportExcelRef"></ExportExcel>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -50,6 +50,37 @@ export default {
filter_condition: '筛选条件', filter_condition: '筛选条件',
no_auth_tips: '缺少菜单权限请联系管理员' no_auth_tips: '缺少菜单权限请联系管理员'
}, },
data_export: {
export_center: '数据导出中心',
export_info: '查看进度进行下载',
exporting: '后台导出中,可前往',
del_all: '全部删除',
export_failed: '导出失败',
export_from: '导出来源',
export_obj: '导出对象',
export_time: '导出时间',
sure_del_all: '确定删除全部导出记录吗',
sure_del: '确定删除该导出记录吗',
no_failed_file: '暂无失败文件',
no_file: '暂无文件',
no_task: '暂无任务',
download_all: '下载全部',
download: '下载'
},
driver: {
driver: '驱动',
please_choose_driver: '请选择驱动',
mgm: '驱动管理',
exit_mgm: '退出驱动管理',
add: '添加驱动',
modify: '修改',
show_info: '驱动信息',
file_name: '文件名',
version: '版本',
please_set_driverClass: '请指定驱动类',
please_set_surpportVersions: '请输入支持的数据库大版本',
surpportVersions: '支持版本'
},
login: { login: {
welcome: '欢迎使用', welcome: '欢迎使用',
btn: '登录', btn: '登录',

View File

@ -14,6 +14,7 @@ import { setupCustomComponent } from '@/custom-component'
import { installDirective } from '@/directive' import { installDirective } from '@/directive'
import '@/utils/DateUtil' import '@/utils/DateUtil'
import '@/permission' import '@/permission'
import WebSocketPlugin from '../../websocket'
const setupAll = async () => { const setupAll = async () => {
const app = createApp(App) const app = createApp(App)
installDirective(app) installDirective(app)
@ -23,6 +24,7 @@ const setupAll = async () => {
setupElementPlus(app) setupElementPlus(app)
setupCustomComponent(app) setupCustomComponent(app)
setupElementPlusIcons(app) setupElementPlusIcons(app)
app.use(WebSocketPlugin)
app.mount('#app') app.mount('#app')
} }

View File

@ -392,7 +392,7 @@ em {
} }
strong { strong {
font-synthesis: style weight!important; font-synthesis: style weight !important;
} }
.ed-date-editor .ed-range__icon { .ed-date-editor .ed-range__icon {
@ -400,12 +400,94 @@ strong {
} }
.ed-picker__popper { .ed-picker__popper {
--ed-datepicker-border-color: #DEE0E3 !important; --ed-datepicker-border-color: #dee0e3 !important;
} }
.ed-dialog__headerbtn { .ed-dialog__headerbtn {
top: 21px !important; top: 21px !important;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center justify-content: center;
}
.de-message-export {
min-width: 20px !important;
padding: 16px 20px !important;
display: flex;
align-items: center;
box-shadow: 0px 4px 8px 0px #1f23291a;
& > p {
font-family: AlibabaPuHuiTi;
font-size: 14px;
font-weight: 500;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
color: #1f2329;
display: flex;
align-items: center;
padding-right: 20px;
}
.btn-text {
padding: 2px 4px;
&:hover {
background: var(--primary10, #3370ff1a);
}
}
.ed-message__closeBtn {
margin-left: 28px;
height: 16px;
width: 16px;
position: relative;
margin-right: 0;
top: 0;
right: 0;
transform: translateY(0);
color: #646a73;
}
.ed-message__icon {
height: 16px;
width: 16px;
margin-right: 8px;
}
}
.de-message-loading {
border: 1px solid var(--primary, #3370ff) !important;
background: #f0f4ff !important;
@keyframes circle {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.ed-message__icon {
color: var(--primary, #3370ff);
animation: circle infinite 0.75s linear;
}
}
.de-message-error {
border: 1px solid var(--deDanger, #f54a45) !important;
background: var(--deWhitemsgDeDanger, #fef1f1) !important;
.ed-message__icon {
color: var(--deDanger, #f54a45);
}
}
.de-message-success {
border: 1px solid var(--deSuccess, #34c724) !important;
background: var(--deWhitemsgDeSuccess, #f0fbef) !important;
.ed-message__icon {
color: var(--deSuccess, #34c724);
}
} }

View File

@ -10,6 +10,7 @@ import { PickOptions } from '@antv/g2plot/esm/core/plot'
import { innerExportDetails } from '@/api/chart' import { innerExportDetails } from '@/api/chart'
import { ElMessage } from 'element-plus-secondary' import { ElMessage } from 'element-plus-secondary'
import { useI18n } from '@/hooks/web/useI18n' import { useI18n } from '@/hooks/web/useI18n'
import { useLinkStoreWithOut } from '@/store/modules/link'
const { t } = useI18n() const { t } = useI18n()
// 同时支持将hex和rgb转换成rgba // 同时支持将hex和rgb转换成rgba
export function hexColorToRGBA(hex, alpha) { export function hexColorToRGBA(hex, alpha) {
@ -460,18 +461,24 @@ export const exportExcelDownload = (chart, callBack?) => {
viewInfo: chart, viewInfo: chart,
detailFields detailFields
} }
const linkStore = useLinkStoreWithOut()
const method = innerExportDetails const method = innerExportDetails
method(request) method(request)
.then(res => { .then(res => {
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' }) if (linkStore.getLinkToken) {
const link = document.createElement('a') const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' })
link.style.display = 'none' const link = document.createElement('a')
link.href = URL.createObjectURL(blob) link.style.display = 'none'
link.download = excelName + '.xlsx' // 下载的文件名 link.href = URL.createObjectURL(blob)
document.body.appendChild(link) link.download = excelName + '.xlsx' // 下载的文件名
link.click() document.body.appendChild(link)
document.body.removeChild(link) link.click()
callBack('success') document.body.removeChild(link)
callBack('success')
} else {
callBack && callBack(res)
}
}) })
.catch(() => { .catch(() => {
console.error('Excel download error') console.error('Excel download error')

View File

@ -0,0 +1,530 @@
<script lang="ts" setup>
import { ref, h, onUnmounted, onMounted } from 'vue'
import { EmptyBackground } from '@/components/empty-background'
import { ElButton, ElMessage, ElMessageBox, ElTabPane, ElTabs } from 'element-plus-secondary'
import { RefreshLeft } from '@element-plus/icons-vue'
import eventBus from '@/utils/eventBus'
import {
exportTasks,
exportRetry,
downloadFile,
exportDelete,
exportDeleteAll,
exportDeletePost
} from '@/api/dataset'
import { useI18n } from '@/hooks/web/useI18n'
import { useEmitt } from '@/hooks/web/useEmitt'
import Icon from '@/components/icon-custom/src/Icon.vue'
const { t } = useI18n()
const tableData = ref([])
const drawerLoading = ref(false)
const drawer = ref(false)
const exportDatasetLoading = ref(false)
const activeName = ref('ALL')
const multipleSelection = ref([])
const description = ref('暂无任务')
const tabList = ref([
{
label: '导出中(0)',
name: 'IN_PROGRESS'
},
{
label: '成功(0)',
name: 'SUCCESS'
},
{
label: '失败(0)',
name: 'FAILED'
},
{
label: '等待中(0)',
name: 'PENDING'
},
{
label: '全部(0)',
name: 'ALL'
}
])
let timer
const handleClose = () => {
drawer.value = false
clearInterval(timer)
}
onMounted(() => {
eventBus.on('task-export-topic-call', taskExportTopicCall)
})
onUnmounted(() => {
clearInterval(timer)
})
const handleClick = tab => {
if (tab) {
activeName.value = tab.paneName
}
if (activeName.value === 'ALL') {
description.value = t('data_export.no_file')
} else if (activeName.value === 'FAILED') {
description.value = t('data_export.no_failed_file')
} else {
description.value = t('data_export.no_task')
}
tableData.value = []
drawerLoading.value = true
exportTasks(activeName.value)
.then(res => {
tabList.value.forEach(item => {
if (item.name === 'ALL') {
item.label = '全部' + '(' + res.data.length + ')'
}
if (item.name === 'IN_PROGRESS') {
item.label =
'导出中' +
'(' +
res.data.filter(task => task.exportStatus === 'IN_PROGRESS').length +
')'
}
if (item.name === 'SUCCESS') {
item.label =
'成功' + '(' + res.data.filter(task => task.exportStatus === 'SUCCESS').length + ')'
}
if (item.name === 'FAILED') {
item.label =
'失败' + '(' + res.data.filter(task => task.exportStatus === 'FAILED').length + ')'
}
if (item.name === 'PENDING') {
item.label =
'等待中' + '(' + res.data.filter(task => task.exportStatus === 'PENDING').length + ')'
}
})
if (activeName.value === 'ALL') {
tableData.value = res.data
} else {
tableData.value = res.data.filter(task => task.exportStatus === activeName.value)
}
})
.finally(() => {
drawerLoading.value = false
})
}
const init = () => {
drawer.value = true
handleClick()
timer = setInterval(() => {
if (activeName.value === 'IN_PROGRESS') {
exportTasks(activeName.value).then(res => {
tabList.value.forEach(item => {
if (item.name === 'ALL') {
item.label = '全部' + '(' + res.data.length + ')'
}
if (item.name === 'IN_PROGRESS') {
item.label =
'导出中' +
'(' +
res.data.filter(task => task.exportStatus === 'IN_PROGRESS').length +
')'
}
if (item.name === 'SUCCESS') {
item.label =
'成功' + '(' + res.data.filter(task => task.exportStatus === 'SUCCESS').length + ')'
}
if (item.name === 'FAILED') {
item.label =
'失败' + '(' + res.data.filter(task => task.exportStatus === 'FAILED').length + ')'
}
if (item.name === 'PENDING') {
item.label =
'等待中' + '(' + res.data.filter(task => task.exportStatus === 'PENDING').length + ')'
}
})
if (activeName.value === 'ALL') {
tableData.value = res.data
} else {
tableData.value = res.data.filter(task => task.exportStatus === activeName.value)
}
})
}
}, 5000)
}
const taskExportTopicCall = task => {
if (JSON.parse(task).exportStatus === 'SUCCESS') {
openMessageLoading(
JSON.parse(task).exportFromName + ' 导出成功,前往',
'success',
callbackExport
)
}
if (JSON.parse(task).exportStatus === 'FAILED') {
openMessageLoading(JSON.parse(task).exportFromName + ' 导出失败,前往', 'error', callbackExport)
}
}
const openMessageLoading = (text, type = 'success', cb) => {
// success error loading
const customClass = `de-message-${type || 'success'} de-message-export`
ElMessage({
message: h('p', null, [
t(text),
h(
ElButton,
{
text: true,
size: 'mini',
class: 'btn-text',
onClick: () => {
cb()
}
},
t('data_export.export_center')
)
]),
icon: type === 'loading' ? h(RefreshLeft) : '',
duration: 0,
type,
showClose: true,
customClass
})
}
const callbackExport = () => {
useEmitt().emitter.emit('data-export-center')
}
const downLoadAll = () => {
if (multipleSelection.value.length === 0) {
tableData.value.forEach(item => {
downloadFile(item.id)
.then(res => {
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = item.fileName //
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
.finally(() => {
exportDatasetLoading.value = false
})
})
return
}
multipleSelection.value.map(ele => {
downloadFile(ele.id)
.then(res => {
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = ele.fileName //
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
.finally(() => {
exportDatasetLoading.value = false
})
})
}
const timestampFormatDate = value => {
if (!value) {
return '-'
}
return new Date(value).toLocaleString()
}
const downloadClick = item => {
downloadFile(item.id)
.then(res => {
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = item.fileName //
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
.finally(() => {
exportDatasetLoading.value = false
})
}
const retry = item => {
exportRetry(item.id).then(() => {
handleClick()
})
}
const deleteField = item => {
ElMessageBox.confirm(t('data_export.sure_del'), {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
})
.then(() => {
exportDelete(item.id).then(() => {
ElMessage.success(t('commons.delete_success'))
handleClick()
})
})
.catch(() => {
// info(t('commons.delete_cancel'))
})
}
const handleSelectionChange = val => {
multipleSelection.value = val
}
const confirmDelete = () => {
const options = {
title: '确定删除该任务吗?',
type: 'primary',
cb: deleteField
}
// handlerConfirm(options)
}
const delAll = () => {
if (multipleSelection.value.length === 0) {
ElMessageBox.confirm(t('data_export.sure_del_all'), {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
})
.then(() => {
exportDeleteAll(
activeName.value,
multipleSelection.value.map(ele => ele.id)
).then(() => {
ElMessage.success(t('commons.delete_success'))
handleClick()
})
})
.catch(() => {
// info(t('commons.delete_cancel'))
})
return
}
ElMessageBox.confirm(t('data_export.sure_del'), {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
})
.then(() => {
exportDeletePost(multipleSelection.value.map(ele => ele.id)).then(() => {
ElMessage.success(t('commons.delete_success'))
handleClick()
})
})
.catch(() => {
// info(t('commons.delete_cancel'))
})
}
defineExpose({
init
})
</script>
<template>
<el-drawer
v-loading="drawerLoading"
custom-class="de-export-excel"
:title="$t('data_export.export_center')"
v-model="drawer"
direction="rtl"
size="1000px"
append-to-body
:before-close="handleClose"
>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane v-for="tab in tabList" :key="tab.name" :label="tab.label" :name="tab.name" />
</el-tabs>
<el-button
v-show="activeName === 'SUCCESS' && multipleSelection.length === 0"
secondary
@click="downLoadAll"
>
<template #icon>
<Icon name="de-delete"></Icon>
</template>
{{ $t('data_export.download_all') }}
</el-button>
<el-button
v-show="activeName === 'SUCCESS' && multipleSelection.length !== 0"
secondary
@click="downLoadAll"
><template #icon> <Icon name="de-delete"></Icon> </template>{{ $t('data_export.download') }}
</el-button>
<el-button v-show="multipleSelection.length === 0" secondary @click="delAll"
><template #icon> <Icon name="de-delete"></Icon> </template>{{ $t('data_export.del_all') }}
</el-button>
<el-button v-show="multipleSelection.length !== 0" secondary @click="delAll"
><template #icon> <Icon name="de-delete"></Icon> </template>{{ $t('commons.delete') }}
</el-button>
<div class="table-container" :class="!tableData.length && 'hidden-bottom'">
<el-table
ref="multipleTable"
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="fileName" :label="$t('driver.file_name')" width="332">
<template #default="scope">
<div class="name-excel">
<el-icon>
<Icon name="file-excel_colorful"></Icon>
</el-icon>
<div class="name-content">
<div class="fileName">{{ scope.row.fileName }}</div>
<div v-if="scope.row.exportStatus === 'FAILED'" class="failed">
{{ $t('data_export.export_failed') }}
</div>
<div v-if="scope.row.exportStatus === 'SUCCESS'" class="success">
{{ scope.row.fileSize }}{{ scope.row.fileSizeUnit }}
</div>
</div>
</div>
<div v-if="scope.row.exportStatus === 'FAILED'" class="red-line" />
<el-progress
v-if="scope.row.exportStatus === 'IN_PROGRESS'"
:percentage="+scope.row.exportPogress"
/>
</template>
</el-table-column>
<el-table-column prop="exportFromName" :label="$t('data_export.export_obj')" width="200" />
<el-table-column prop="exportFromType" width="120" :label="$t('data_export.export_from')">
<template #default="scope">
<span v-if="scope.row.exportFromType === 'dataset'">数据集</span>
<span v-if="scope.row.exportFromType === 'chart'">视图</span>
</template>
</el-table-column>
<el-table-column prop="exportTime" width="180" :label="$t('data_export.export_time')">
<template #default="scope">
<span>{{ timestampFormatDate(scope.row.exportTime) }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" prop="operate" width="150" :label="$t('commons.operating')">
<template #default="scope">
<el-button
v-if="scope.row.exportStatus === 'SUCCESS'"
type="text"
@click="downloadClick(scope.row)"
>
<div class="download-export">
<el-icon>
<Icon name="dv-preview-download"></Icon>
</el-icon>
</div>
</el-button>
<el-button type="text" @click="retry(scope.row)">
<template #icon>
<Icon name="de-refresh"></Icon>
</template>
</el-button>
<el-button type="text" @click="deleteField(scope.row)">
<template #icon>
<Icon name="de-delete"></Icon>
</template>
</el-button>
</template>
</el-table-column>
<template #empty>
<empty-background :description="description" img-type="noneWhite" />
</template>
</el-table>
</div>
</el-drawer>
</template>
<style lang="less">
.de-export-excel {
.ed-drawer__header {
border-bottom: none;
}
.ed-tabs {
margin-top: -25px;
.ed-tabs__header {
margin-bottom: 24px;
}
}
.download-export {
font-size: 16px;
}
.table-container {
margin-top: 16px;
.ed-table .cell {
padding-left: 12px;
padding-right: 12px;
}
&.hidden-bottom {
.ed-table::before {
display: none;
}
}
.name-excel {
display: flex;
align-items: center;
.name-content {
max-width: 280px;
margin-left: 4px;
.fileName {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
.failed {
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: #f54a45;
}
.success {
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: #8f959e;
}
}
}
.ed-table__header {
border-top: 1px solid #1f232926;
}
th.ed-table__cell.is-leaf {
border-color: #1f232926;
}
.red-line {
width: 100%;
height: 4px;
background: #f54a45;
position: absolute;
left: 0;
bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,35 @@
import SockJS from 'sockjs-client/dist/sockjs.min.js'
import Stomp from 'stompjs'
import eventBus from '@/utils/eventBus'
let stompClient: Stomp.Client
export default {
install() {
const channels = [
{
topic: '/task-export-topic',
event: 'task-export-topic-call'
}
]
const socket = new SockJS('http://localhost:8100' + '/websocket' + '?userId=1')
stompClient = Stomp.over(socket)
const heads = {
userId: 1
}
stompClient.connect(
heads,
res => {
console.log('连接成功: ' + res)
channels.forEach(channel => {
stompClient.subscribe('/user/' + 1 + channel.topic, res => {
res && res.body && eventBus.emit(channel.event, res.body)
})
})
},
error => {
console.log('连接失败: ' + error)
}
)
}
}

View File

@ -13,6 +13,7 @@ services:
- ${DE_BASE}/dataease2.0/cache:/opt/dataease2.0/cache - ${DE_BASE}/dataease2.0/cache:/opt/dataease2.0/cache
- ${DE_BASE}/dataease2.0/data/geo:/opt/dataease2.0/data/geo - ${DE_BASE}/dataease2.0/data/geo:/opt/dataease2.0/data/geo
- ${DE_BASE}/dataease2.0/data/appearance:/opt/dataease2.0/data/appearance - ${DE_BASE}/dataease2.0/data/appearance:/opt/dataease2.0/data/appearance
- ${DE_BASE}/dataease2.0/data/exportData:/opt/dataease2.0/data/exportData
depends_on: depends_on:
DE_MYSQL_HOST: DE_MYSQL_HOST:
condition: service_healthy condition: service_healthy

View File

@ -27,7 +27,7 @@ function prop {
function check_and_prepare_env_params() { function check_and_prepare_env_params() {
log "当前时间 : $(date)" log "当前时间 : $(date)"
log_title "检查安装环境并初始化环境变量" log_title "检查安装环境并初始化环境变量"
cd ${CURRENT_DIR} cd ${CURRENT_DIR}
if [ -f /usr/bin/dectl ]; then if [ -f /usr/bin/dectl ]; then
@ -39,10 +39,10 @@ function check_and_prepare_env_params() {
log_content "停止 DataEase 服务" log_content "停止 DataEase 服务"
if [[ -f /etc/systemd/system/dataease.service ]];then if [[ -f /etc/systemd/system/dataease.service ]];then
systemctl stop dataease systemctl stop dataease
else else
dectl stop dectl stop
fi fi
INSTALL_TYPE='upgrade' INSTALL_TYPE='upgrade'
v2_version=$(dectl version | head -n 2 | grep "v2.") v2_version=$(dectl version | head -n 2 | grep "v2.")
@ -99,7 +99,7 @@ function prepare_de_run_base() {
env | grep DE_ >.env env | grep DE_ >.env
mkdir -p ${DE_RUN_BASE}/{cache,logs,conf} mkdir -p ${DE_RUN_BASE}/{cache,logs,conf}
mkdir -p ${DE_RUN_BASE}/data/{mysql,static-resource,map,etcd_data,geo,appearance} mkdir -p ${DE_RUN_BASE}/data/{mysql,static-resource,map,etcd_data,geo,appearance,exportData}
mkdir -p ${DE_RUN_BASE}/apisix/logs mkdir -p ${DE_RUN_BASE}/apisix/logs
mkdir -p ${DE_RUN_BASE}/task/logs mkdir -p ${DE_RUN_BASE}/task/logs
chmod 777 ${DE_RUN_BASE}/apisix/logs ${DE_RUN_BASE}/data/etcd_data ${DE_RUN_BASE}/task/logs chmod 777 ${DE_RUN_BASE}/apisix/logs ${DE_RUN_BASE}/data/etcd_data ${DE_RUN_BASE}/task/logs
@ -132,7 +132,7 @@ function update_dectl() {
\cp dectl /usr/local/bin && chmod +x /usr/local/bin/dectl \cp dectl /usr/local/bin && chmod +x /usr/local/bin/dectl
if [ ! -f /usr/bin/dectl ]; then if [ ! -f /usr/bin/dectl ]; then
ln -s /usr/local/bin/dectl /usr/bin/dectl 2>/dev/null ln -s /usr/local/bin/dectl /usr/bin/dectl 2>/dev/null
fi fi
} }
function prepare_system_settings() { function prepare_system_settings() {
@ -312,4 +312,4 @@ function main() {
start_de_service start_de_service
} }
main main

View File

@ -75,6 +75,13 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency> <dependency>
<groupId>org.mybatis</groupId> <groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId> <artifactId>mybatis-spring</artifactId>

View File

@ -0,0 +1,41 @@
package io.dataease.api.exportCenter;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.dataease.api.exportCenter.vo.ExportTaskDTO;
import io.dataease.auth.DeApiPath;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
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;
import static io.dataease.constant.AuthResourceEnum.DATASOURCE;
@Tag(name = "数据导出中心")
@ApiSupport(order = 971)
@DeApiPath(value = "/exportCenter", rt = DATASOURCE)
public interface ExportCenterApi {
@PostMapping("/exportTasks/{status}")
public List<ExportTaskDTO> exportTasks(@PathVariable String status) ;
@GetMapping("/delete/{id}")
public void delete(@PathVariable String id);
@PostMapping("/delete")
public void delete(@RequestBody List<String> ids);
@PostMapping("/deleteAll/{type}")
public void deleteAll(@PathVariable String type);
@GetMapping("/download/{id}")
public void download(@PathVariable String id, HttpServletResponse response) throws Exception ;
@PostMapping("/retry/{id}")
public void retry(@PathVariable String id);
}

View File

@ -0,0 +1,33 @@
package io.dataease.api.exportCenter.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
@Data
public class ExportTaskDTO {
@JsonSerialize(using= ToStringSerializer.class)
private String id;
@JsonSerialize(using= ToStringSerializer.class)
private Long userId;
private String fileName;
private Double fileSize;
private String fileSizeUnit;
private String exportFrom;
private String exportStatus;
private String exportFromType;
private Long exportTime;
private String exportProgress;
private String exportMachineName;
private String exportFromName;
}

View File

@ -269,4 +269,24 @@ public class FileUtils {
return bytes; return bytes;
} }
public static boolean deleteDirectoryRecursively(String directoryPath) {
File directory = new File(directoryPath);
if (!directory.exists()) {
return true;
}
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
deleteDirectoryRecursively(file.getAbsolutePath());
} else {
boolean deletionSuccess = file.delete();
}
}
}
return directory.delete();
}
} }

View File

@ -63,6 +63,7 @@ public class WhitelistUtils {
|| StringUtils.startsWithAny(requestURI, "/share/proxyInfo") || StringUtils.startsWithAny(requestURI, "/share/proxyInfo")
|| StringUtils.startsWithAny(requestURI, "/xpackComponent/content/") || StringUtils.startsWithAny(requestURI, "/xpackComponent/content/")
|| StringUtils.startsWithAny(requestURI, "/geo/") || StringUtils.startsWithAny(requestURI, "/geo/")
|| StringUtils.startsWithAny(requestURI, "/websocket")
|| StringUtils.startsWithAny(requestURI, "/map/"); || StringUtils.startsWithAny(requestURI, "/map/");
} }
} }