From c07422376b9f3763216dff3603f1cb9b5d29ae17 Mon Sep 17 00:00:00 2001 From: wxy <14293288+zysysys@user.noreply.gitee.com> Date: Fri, 24 May 2024 14:04:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/exception/file/FileException.java | 19 ++ .../FileNameLengthLimitExceededException.java | 16 ++ .../file/FileSizeLimitExceededException.java | 16 ++ .../exception/file/FileUploadException.java | 61 +++++ .../common/core/utils/file/FileTypeUtils.java | 95 +++++++ .../common/core/utils/file/FileUtils.java | 253 ++++++++++++++++++ .../com/jing/file/utils/FileUploadUtils.java | 185 +++++++++++++ .../java/com/jing/gen/config/GenConfig.java | 66 +++++ jing-ui/src/api/tool/gen.js | 76 ++++++ .../layout/components/Sidebar/FixiOSBug.js | 25 ++ 10 files changed, 812 insertions(+) create mode 100644 jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileException.java create mode 100644 jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileNameLengthLimitExceededException.java create mode 100644 jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileSizeLimitExceededException.java create mode 100644 jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileUploadException.java create mode 100644 jing-common/jing-common-core/src/main/java/com/jing/common/core/utils/file/FileTypeUtils.java create mode 100644 jing-common/jing-common-core/src/main/java/com/jing/common/core/utils/file/FileUtils.java create mode 100644 jing-modules/jing-file/src/main/java/com/jing/file/utils/FileUploadUtils.java create mode 100644 jing-modules/jing-gen/src/main/java/com/jing/gen/config/GenConfig.java create mode 100644 jing-ui/src/api/tool/gen.js create mode 100644 jing-ui/src/layout/components/Sidebar/FixiOSBug.js diff --git a/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileException.java b/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileException.java new file mode 100644 index 0000000..49f8816 --- /dev/null +++ b/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileException.java @@ -0,0 +1,19 @@ +package com.jing.common.core.exception.file; + +import com.jing.common.core.exception.base.BaseException; + +/** + * 文件信息异常类 + * + * @author ruoyi + */ +public class FileException extends BaseException +{ + private static final long serialVersionUID = 1L; + + public FileException(String code, Object[] args, String msg) + { + super("file", code, args, msg); + } + +} diff --git a/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileNameLengthLimitExceededException.java b/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileNameLengthLimitExceededException.java new file mode 100644 index 0000000..1e85c40 --- /dev/null +++ b/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileNameLengthLimitExceededException.java @@ -0,0 +1,16 @@ +package com.jing.common.core.exception.file; + +/** + * 文件名称超长限制异常类 + * + * @author ruoyi + */ +public class FileNameLengthLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileNameLengthLimitExceededException(int defaultFileNameLength) + { + super("upload.filename.exceed.length", new Object[] { defaultFileNameLength }, "the filename is too long"); + } +} diff --git a/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileSizeLimitExceededException.java b/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileSizeLimitExceededException.java new file mode 100644 index 0000000..0fc44fb --- /dev/null +++ b/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileSizeLimitExceededException.java @@ -0,0 +1,16 @@ +package com.jing.common.core.exception.file; + +/** + * 文件名大小限制异常类 + * + * @author ruoyi + */ +public class FileSizeLimitExceededException extends FileException +{ + private static final long serialVersionUID = 1L; + + public FileSizeLimitExceededException(long defaultMaxSize) + { + super("upload.exceed.maxSize", new Object[] { defaultMaxSize }, "the filesize is too large"); + } +} diff --git a/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileUploadException.java b/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileUploadException.java new file mode 100644 index 0000000..7c7b01a --- /dev/null +++ b/jing-common/jing-common-core/src/main/java/com/jing/common/core/exception/file/FileUploadException.java @@ -0,0 +1,61 @@ +package com.jing.common.core.exception.file; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * 文件上传异常类 + * + * @author ruoyi + */ +public class FileUploadException extends Exception +{ + + private static final long serialVersionUID = 1L; + + private final Throwable cause; + + public FileUploadException() + { + this(null, null); + } + + public FileUploadException(final String msg) + { + this(msg, null); + } + + public FileUploadException(String msg, Throwable cause) + { + super(msg); + this.cause = cause; + } + + @Override + public void printStackTrace(PrintStream stream) + { + super.printStackTrace(stream); + if (cause != null) + { + stream.println("Caused by:"); + cause.printStackTrace(stream); + } + } + + @Override + public void printStackTrace(PrintWriter writer) + { + super.printStackTrace(writer); + if (cause != null) + { + writer.println("Caused by:"); + cause.printStackTrace(writer); + } + } + + @Override + public Throwable getCause() + { + return cause; + } +} diff --git a/jing-common/jing-common-core/src/main/java/com/jing/common/core/utils/file/FileTypeUtils.java b/jing-common/jing-common-core/src/main/java/com/jing/common/core/utils/file/FileTypeUtils.java new file mode 100644 index 0000000..9e7e214 --- /dev/null +++ b/jing-common/jing-common-core/src/main/java/com/jing/common/core/utils/file/FileTypeUtils.java @@ -0,0 +1,95 @@ +package com.jing.common.core.utils.file; + +import java.io.File; +import java.util.Objects; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +/** + * 文件类型工具类 + * + * @author ruoyi + */ +public class FileTypeUtils +{ + /** + * 获取文件类型 + *

+ * 例如: ruoyi.txt, 返回: txt + * + * @param file 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(File file) + { + if (null == file) + { + return StringUtils.EMPTY; + } + return getFileType(file.getName()); + } + + /** + * 获取文件类型 + *

+ * 例如: ruoyi.txt, 返回: txt + * + * @param fileName 文件名 + * @return 后缀(不含".") + */ + public static String getFileType(String fileName) + { + int separatorIndex = fileName.lastIndexOf("."); + if (separatorIndex < 0) + { + return ""; + } + return fileName.substring(separatorIndex + 1).toLowerCase(); + } + + /** + * 获取文件名的后缀 + * + * @param file 表单文件 + * @return 后缀名 + */ + public static final String getExtension(MultipartFile file) + { + String extension = FilenameUtils.getExtension(file.getOriginalFilename()); + if (StringUtils.isEmpty(extension)) + { + extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType())); + } + return extension; + } + + /** + * 获取文件类型 + * + * @param photoByte 文件字节码 + * @return 后缀(不含".") + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "JPG"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "GIF"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "JPG"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "BMP"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "PNG"; + } + return strFileExtendName; + } +} \ No newline at end of file diff --git a/jing-common/jing-common-core/src/main/java/com/jing/common/core/utils/file/FileUtils.java b/jing-common/jing-common-core/src/main/java/com/jing/common/core/utils/file/FileUtils.java new file mode 100644 index 0000000..1002f1a --- /dev/null +++ b/jing-common/jing-common-core/src/main/java/com/jing/common/core/utils/file/FileUtils.java @@ -0,0 +1,253 @@ +package com.jing.common.core.utils.file; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.ArrayUtils; +import com.jing.common.core.utils.StringUtils; + +/** + * 文件处理工具类 + * + * @author ruoyi + */ +public class FileUtils +{ + /** 字符常量:斜杠 {@code '/'} */ + public static final char SLASH = '/'; + + /** 字符常量:反斜杠 {@code '\\'} */ + public static final char BACKSLASH = '\\'; + + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 输出指定文件的byte数组 + * + * @param filePath 文件路径 + * @param os 输出流 + * @return + */ + public static void writeBytes(String filePath, OutputStream os) throws IOException + { + FileInputStream fis = null; + try + { + File file = new File(filePath); + if (!file.exists()) + { + throw new FileNotFoundException(filePath); + } + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + int length; + while ((length = fis.read(b)) > 0) + { + os.write(b, 0, length); + } + } + catch (IOException e) + { + throw e; + } + finally + { + if (os != null) + { + try + { + os.close(); + } + catch (IOException e1) + { + e1.printStackTrace(); + } + } + if (fis != null) + { + try + { + fis.close(); + } + catch (IOException e1) + { + e1.printStackTrace(); + } + } + } + } + + /** + * 删除文件 + * + * @param filePath 文件 + * @return + */ + public static boolean deleteFile(String filePath) + { + boolean flag = false; + File file = new File(filePath); + // 路径为文件且不为空则进行删除 + if (file.isFile() && file.exists()) + { + flag = file.delete(); + } + return flag; + } + + /** + * 文件名称验证 + * + * @param filename 文件名称 + * @return true 正常 false 非法 + */ + public static boolean isValidFilename(String filename) + { + return filename.matches(FILENAME_PATTERN); + } + + /** + * 检查文件是否可下载 + * + * @param resource 需要下载的文件 + * @return true 正常 false 非法 + */ + public static boolean checkAllowDownload(String resource) + { + // 禁止目录上跳级别 + if (StringUtils.contains(resource, "..")) + { + return false; + } + // 判断是否在允许下载的文件规则内 + return ArrayUtils.contains(MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION, FileTypeUtils.getFileType(resource)); + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException + { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) + { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } + else if (agent.contains("Firefox")) + { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } + else if (agent.contains("Chrome")) + { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + else + { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } + + /** + * 返回文件名 + * + * @param filePath 文件 + * @return 文件名 + */ + public static String getName(String filePath) + { + if (null == filePath) + { + return null; + } + int len = filePath.length(); + if (0 == len) + { + return filePath; + } + if (isFileSeparator(filePath.charAt(len - 1))) + { + // 以分隔符结尾的去掉结尾分隔符 + len--; + } + + int begin = 0; + char c; + for (int i = len - 1; i > -1; i--) + { + c = filePath.charAt(i); + if (isFileSeparator(c)) + { + // 查找最后一个路径分隔符(/或者\) + begin = i + 1; + break; + } + } + + return filePath.substring(begin, len); + } + + /** + * 是否为Windows或者Linux(Unix)文件分隔符
+ * Windows平台下分隔符为\,Linux(Unix)为/ + * + * @param c 字符 + * @return 是否为Windows或者Linux(Unix)文件分隔符 + */ + public static boolean isFileSeparator(char c) + { + return SLASH == c || BACKSLASH == c; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + * @return + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException + { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException + { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } +} diff --git a/jing-modules/jing-file/src/main/java/com/jing/file/utils/FileUploadUtils.java b/jing-modules/jing-file/src/main/java/com/jing/file/utils/FileUploadUtils.java new file mode 100644 index 0000000..eed8ec3 --- /dev/null +++ b/jing-modules/jing-file/src/main/java/com/jing/file/utils/FileUploadUtils.java @@ -0,0 +1,185 @@ +package com.jing.file.utils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Objects; +import org.apache.commons.io.FilenameUtils; +import org.springframework.web.multipart.MultipartFile; +import com.jing.common.core.exception.file.FileException; +import com.jing.common.core.exception.file.FileNameLengthLimitExceededException; +import com.jing.common.core.exception.file.FileSizeLimitExceededException; +import com.jing.common.core.exception.file.InvalidExtensionException; +import com.jing.common.core.utils.DateUtils; +import com.jing.common.core.utils.StringUtils; +import com.jing.common.core.utils.file.FileTypeUtils; +import com.jing.common.core.utils.file.MimeTypeUtils; +import com.jing.common.core.utils.uuid.Seq; + +/** + * 文件上传工具类 + * + * @author ruoyi + */ +public class FileUploadUtils +{ + /** + * 默认大小 50M + */ + public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024; + + /** + * 默认的文件名最大长度 100 + */ + public static final int DEFAULT_FILE_NAME_LENGTH = 100; + + /** + * 根据文件路径上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @return 文件名称 + * @throws IOException + */ + public static final String upload(String baseDir, MultipartFile file) throws IOException + { + try + { + return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); + } + catch (FileException fe) + { + throw new IOException(fe.getDefaultMessage(), fe); + } + catch (Exception e) + { + throw new IOException(e.getMessage(), e); + } + } + + /** + * 文件上传 + * + * @param baseDir 相对应用的基目录 + * @param file 上传的文件 + * @param allowedExtension 上传文件类型 + * @return 返回上传成功的文件名 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws FileNameLengthLimitExceededException 文件名太长 + * @throws IOException 比如读写文件出错时 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, IOException, FileNameLengthLimitExceededException, + InvalidExtensionException + { + int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length(); + if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) + { + throw new FileNameLengthLimitExceededException(FileUploadUtils.DEFAULT_FILE_NAME_LENGTH); + } + + assertAllowed(file, allowedExtension); + + String fileName = extractFilename(file); + + String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath(); + file.transferTo(Paths.get(absPath)); + return getPathFileName(fileName); + } + + /** + * 编码文件名 + */ + public static final String extractFilename(MultipartFile file) + { + return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(), + FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), FileTypeUtils.getExtension(file)); + } + + private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException + { + File desc = new File(uploadDir + File.separator + fileName); + + if (!desc.exists()) + { + if (!desc.getParentFile().exists()) + { + desc.getParentFile().mkdirs(); + } + } + return desc.isAbsolute() ? desc : desc.getAbsoluteFile(); + } + + private static final String getPathFileName(String fileName) throws IOException + { + String pathFileName = "/" + fileName; + return pathFileName; + } + + /** + * 文件大小校验 + * + * @param file 上传的文件 + * @throws FileSizeLimitExceededException 如果超出最大大小 + * @throws InvalidExtensionException 文件校验异常 + */ + public static final void assertAllowed(MultipartFile file, String[] allowedExtension) + throws FileSizeLimitExceededException, InvalidExtensionException + { + long size = file.getSize(); + if (size > DEFAULT_MAX_SIZE) + { + throw new FileSizeLimitExceededException(DEFAULT_MAX_SIZE / 1024 / 1024); + } + + String fileName = file.getOriginalFilename(); + String extension = FileTypeUtils.getExtension(file); + if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) + { + if (allowedExtension == MimeTypeUtils.IMAGE_EXTENSION) + { + throw new InvalidExtensionException.InvalidImageExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.FLASH_EXTENSION) + { + throw new InvalidExtensionException.InvalidFlashExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.MEDIA_EXTENSION) + { + throw new InvalidExtensionException.InvalidMediaExtensionException(allowedExtension, extension, + fileName); + } + else if (allowedExtension == MimeTypeUtils.VIDEO_EXTENSION) + { + throw new InvalidExtensionException.InvalidVideoExtensionException(allowedExtension, extension, + fileName); + } + else + { + throw new InvalidExtensionException(allowedExtension, extension, fileName); + } + } + } + + /** + * 判断MIME类型是否是允许的MIME类型 + * + * @param extension 上传文件类型 + * @param allowedExtension 允许上传文件类型 + * @return true/false + */ + public static final boolean isAllowedExtension(String extension, String[] allowedExtension) + { + for (String str : allowedExtension) + { + if (str.equalsIgnoreCase(extension)) + { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/jing-modules/jing-gen/src/main/java/com/jing/gen/config/GenConfig.java b/jing-modules/jing-gen/src/main/java/com/jing/gen/config/GenConfig.java new file mode 100644 index 0000000..459069c --- /dev/null +++ b/jing-modules/jing-gen/src/main/java/com/jing/gen/config/GenConfig.java @@ -0,0 +1,66 @@ +package com.jing.gen.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 代码生成相关配置 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "gen") +public class GenConfig +{ + /** 作者 */ + public static String author; + + /** 生成包路径 */ + public static String packageName; + + /** 自动去除表前缀,默认是false */ + public static boolean autoRemovePre; + + /** 表前缀(类名不会包含表前缀) */ + public static String tablePrefix; + + public static String getAuthor() + { + return author; + } + + public void setAuthor(String author) + { + GenConfig.author = author; + } + + public static String getPackageName() + { + return packageName; + } + + public void setPackageName(String packageName) + { + GenConfig.packageName = packageName; + } + + public static boolean getAutoRemovePre() + { + return autoRemovePre; + } + + public void setAutoRemovePre(boolean autoRemovePre) + { + GenConfig.autoRemovePre = autoRemovePre; + } + + public static String getTablePrefix() + { + return tablePrefix; + } + + public void setTablePrefix(String tablePrefix) + { + GenConfig.tablePrefix = tablePrefix; + } +} diff --git a/jing-ui/src/api/tool/gen.js b/jing-ui/src/api/tool/gen.js new file mode 100644 index 0000000..7524a0a --- /dev/null +++ b/jing-ui/src/api/tool/gen.js @@ -0,0 +1,76 @@ +import request from '@/utils/request' + +// 查询生成表数据 +export function listTable(query) { + return request({ + url: '/code/gen/list', + method: 'get', + params: query + }) +} +// 查询db数据库列表 +export function listDbTable(query) { + return request({ + url: '/code/gen/db/list', + method: 'get', + params: query + }) +} + +// 查询表详细信息 +export function getGenTable(tableId) { + return request({ + url: '/code/gen/' + tableId, + method: 'get' + }) +} + +// 修改代码生成信息 +export function updateGenTable(data) { + return request({ + url: '/code/gen', + method: 'put', + data: data + }) +} + +// 导入表 +export function importTable(data) { + return request({ + url: '/code/gen/importTable', + method: 'post', + params: data + }) +} + +// 预览生成代码 +export function previewTable(tableId) { + return request({ + url: '/code/gen/preview/' + tableId, + method: 'get' + }) +} + +// 删除表数据 +export function delTable(tableId) { + return request({ + url: '/code/gen/' + tableId, + method: 'delete' + }) +} + +// 生成代码(自定义路径) +export function genCode(tableName) { + return request({ + url: '/code/gen/genCode/' + tableName, + method: 'get' + }) +} + +// 同步数据库 +export function synchDb(tableName) { + return request({ + url: '/code/gen/synchDb/' + tableName, + method: 'get' + }) +} diff --git a/jing-ui/src/layout/components/Sidebar/FixiOSBug.js b/jing-ui/src/layout/components/Sidebar/FixiOSBug.js new file mode 100644 index 0000000..6823726 --- /dev/null +++ b/jing-ui/src/layout/components/Sidebar/FixiOSBug.js @@ -0,0 +1,25 @@ +export default { + computed: { + device() { + return this.$store.state.app.device + } + }, + mounted() { + // In order to fix the click on menu on the ios device will trigger the mouseleave bug + this.fixBugIniOS() + }, + methods: { + fixBugIniOS() { + const $subMenu = this.$refs.subMenu + if ($subMenu) { + const handleMouseleave = $subMenu.handleMouseleave + $subMenu.handleMouseleave = (e) => { + if (this.device === 'mobile') { + return + } + handleMouseleave(e) + } + } + } + } +}