u
This commit is contained in:
1 parent
c7240ea7c9
commit
189a085353
26 files changed
+2650
-201
No files matched your search
@@ -70,6 +70,19 @@
|
||||
<version>1.18.32</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 解析客户端操作系统、浏览器等 -->
|
||||
<dependency>
|
||||
<groupId>nl.basjes.parse.useragent</groupId>
|
||||
<artifactId>yauaa</artifactId>
|
||||
<version>7.26.0</version>
|
||||
</dependency>
|
||||
<!-- 获取系统信息 -->
|
||||
<dependency>
|
||||
<groupId>com.github.oshi</groupId>
|
||||
<artifactId>oshi-core</artifactId>
|
||||
<version>6.10.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -6,11 +6,13 @@ import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@SpringBootApplication(exclude = {
|
||||
DataSourceAutoConfiguration.class,
|
||||
DruidDataSourceAutoConfigure.class ,
|
||||
})
|
||||
@EnableAsync
|
||||
public class ApiApplication {
|
||||
public static void main(String[] args){
|
||||
// 初始化雪花id
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package cn.g3soft.async;
|
||||
|
||||
import cn.g3soft.utils.DbUtils;
|
||||
import cn.g3soft.utils.ServletUtils;
|
||||
import cn.g3soft.utils.TimeUtils;
|
||||
import cn.g3soft.utils.UserAgentUtils;
|
||||
import cn.g3soft.utils.ip.AddressUtils;
|
||||
import cn.g3soft.utils.ip.IpUtils;
|
||||
import cn.g3soft.utils.uniqueId.idgen.UniqueId;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class AsyncService {
|
||||
@Resource
|
||||
private DbUtils dbUtils;
|
||||
|
||||
// 异步记录登录日志方法
|
||||
@Async
|
||||
public void recordLoginLog(String userId, String userAgent, String ip, String address, String os, String browser) {
|
||||
if (StringUtils.isBlank(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userId.equals("g3soft")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("b_id", UniqueId.nextId());
|
||||
map.put("b_userid", userId);
|
||||
map.put("b_login_time", TimeUtils.getDateTime());
|
||||
map.put("b_ip", ip);
|
||||
map.put("b_address", address);
|
||||
map.put("b_os", os);
|
||||
map.put("b_browser", browser);
|
||||
|
||||
// 将数据插入数据库
|
||||
dbUtils.insert("b_login_log", "b_id", map);
|
||||
}
|
||||
|
||||
// 主线程捕获请求信息并传递给异步方法
|
||||
public void invokeAsyncLoginLog(String userId) {
|
||||
if (StringUtils.isBlank(userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 捕获请求信息
|
||||
final String userAgent = ServletUtils.getRequest().getHeader("User-Agent");
|
||||
final String ip = IpUtils.getIpAddr();
|
||||
String address = AddressUtils.getRealAddressByIP(ip);
|
||||
|
||||
// 获取客户端操作系统和浏览器
|
||||
String os = UserAgentUtils.getOperatingSystem(userAgent);
|
||||
String browser = UserAgentUtils.getBrowser(userAgent);
|
||||
|
||||
// 调用异步方法时传递请求信息
|
||||
recordLoginLog(userId, userAgent, ip, address, os, browser);
|
||||
}
|
||||
}
|
||||
+5
@@ -1,5 +1,6 @@
|
||||
package cn.g3soft.controller;
|
||||
|
||||
import cn.g3soft.async.AsyncService;
|
||||
import cn.g3soft.config.datasource.DataSourceManager;
|
||||
import cn.g3soft.context.DataSourceContextHolder;
|
||||
import cn.g3soft.context.UserContext;
|
||||
@@ -25,6 +26,8 @@ import java.util.Objects;
|
||||
public class AuthController {
|
||||
@Resource
|
||||
private DbUtils dbUtils;
|
||||
@Resource
|
||||
private AsyncService asyncService;
|
||||
|
||||
/**
|
||||
* 用户登录接口
|
||||
@@ -90,6 +93,8 @@ public class AuthController {
|
||||
map.put("token",token);
|
||||
map.put("username",Objects.toString(user.get("b_name"), ""));
|
||||
|
||||
asyncService.invokeAsyncLoginLog(userid);
|
||||
|
||||
return Result.ok(map);
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -64,7 +64,7 @@ public class DataSaveController {
|
||||
}
|
||||
|
||||
if (logEnabled){
|
||||
addOperationLog(logList,tableName,"INSERT",insertList);
|
||||
addOperationLog(logList,tableName,"INSERT",insertList,userId,currentDateTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ public class DataSaveController {
|
||||
}
|
||||
|
||||
if (logEnabled) {
|
||||
addOperationLog(logList, tableName, "UPDATE", updateList);
|
||||
addOperationLog(logList, tableName, "UPDATE", updateList,userId,currentDateTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public class DataSaveController {
|
||||
}
|
||||
|
||||
if (logEnabled) {
|
||||
addOperationLog(logList, tableName, "DELETE", deleteList);
|
||||
addOperationLog(logList, tableName, "DELETE", deleteList,userId,currentDateTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ public class DataSaveController {
|
||||
}
|
||||
|
||||
if (logEnabled) {
|
||||
addOperationLog(logList, tableName, "INSERT", insertList);
|
||||
addOperationLog(logList, tableName, "INSERT", insertList,userId,currentDateTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ public class DataSaveController {
|
||||
dbUtils.update(tableName, keyFields, updateMap);
|
||||
}
|
||||
if (logEnabled) {
|
||||
addOperationLog(logList, tableName, "UPDATE", updateList);
|
||||
addOperationLog(logList, tableName, "UPDATE", updateList,userId,currentDateTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ public class DataSaveController {
|
||||
dbUtils.delete(tableName,deleteMap);
|
||||
}
|
||||
if (logEnabled) {
|
||||
addOperationLog(logList, tableName, "DELETE", deleteList);
|
||||
addOperationLog(logList, tableName, "DELETE", deleteList,userId,currentDateTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ public class DataSaveController {
|
||||
}
|
||||
}
|
||||
|
||||
private void addOperationLog(List<Map<String,Object>> logList,String table, String operation, List<Map<String, Object>> data) throws JsonProcessingException {
|
||||
private void addOperationLog(List<Map<String,Object>> logList,String table, String operation, List<Map<String, Object>> data,String userId,String dateTime) throws JsonProcessingException {
|
||||
if (ObjectUtils.isEmpty(data)){
|
||||
return;
|
||||
}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ public class OcrController {
|
||||
|
||||
String fileUrl = (String) params.get("fileUrl");
|
||||
|
||||
String requestUrl = "http://116.204.97.131:80/ocr";
|
||||
String requestUrl = "http://192.168.1.137:8867/ocr";
|
||||
|
||||
Map<String, Object> reqPayload = new HashMap<>();
|
||||
reqPayload.put("file", fileUrl);
|
||||
|
||||
@@ -1,85 +1,318 @@
|
||||
package cn.g3soft.utils;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.ConnectException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Map;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
@Slf4j
|
||||
public class HttpUtils {
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
private static String request(String method, String url, String body, String contentType) {
|
||||
HttpURLConnection conn = null;
|
||||
/**
|
||||
* 通用http发送方法
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class HttpUtils
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(HttpUtils.class);
|
||||
|
||||
/**
|
||||
* 向指定 URL 发送GET方法的请求
|
||||
*
|
||||
* @param url 发送请求的 URL
|
||||
* @return 所代表远程资源的响应结果
|
||||
*/
|
||||
public static String sendGet(String url)
|
||||
{
|
||||
return sendGet(url, StringUtils.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定 URL 发送GET方法的请求
|
||||
*
|
||||
* @param url 发送请求的 URL
|
||||
* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
|
||||
* @return 所代表远程资源的响应结果
|
||||
*/
|
||||
public static String sendGet(String url, String param)
|
||||
{
|
||||
return sendGet(url, param, "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定 URL 发送GET方法的请求
|
||||
*
|
||||
* @param url 发送请求的 URL
|
||||
* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
|
||||
* @param contentType 编码类型
|
||||
* @return 所代表远程资源的响应结果
|
||||
*/
|
||||
public static String sendGet(String url, String param, String contentType)
|
||||
{
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
try {
|
||||
URL realUrl = new URL(url);
|
||||
conn = (HttpURLConnection) realUrl.openConnection();
|
||||
|
||||
conn.setRequestMethod(method);
|
||||
conn.setConnectTimeout(10000);
|
||||
conn.setReadTimeout(10000);
|
||||
|
||||
conn.setRequestProperty("accept", "*/*");
|
||||
conn.setRequestProperty("connection", "Keep-Alive");
|
||||
conn.setRequestProperty("user-agent", "Mozilla/5.0");
|
||||
conn.setRequestProperty("Content-Type", contentType);
|
||||
|
||||
conn.setDoInput(true);
|
||||
|
||||
// 👉 有 body 才写
|
||||
if (body != null && !body.isEmpty()) {
|
||||
conn.setDoOutput(true);
|
||||
try (OutputStreamWriter out =
|
||||
new OutputStreamWriter(conn.getOutputStream(), StandardCharsets.UTF_8)) {
|
||||
out.write(body);
|
||||
out.flush();
|
||||
BufferedReader in = null;
|
||||
try
|
||||
{
|
||||
String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url;
|
||||
log.info("sendGet - {}", urlNameString);
|
||||
URL realUrl = new URL(urlNameString);
|
||||
URLConnection connection = realUrl.openConnection();
|
||||
connection.setRequestProperty("accept", "*/*");
|
||||
connection.setRequestProperty("connection", "Keep-Alive");
|
||||
connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
|
||||
connection.connect();
|
||||
in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null)
|
||||
{
|
||||
result.append(line);
|
||||
}
|
||||
log.info("recv - {}", result);
|
||||
}
|
||||
catch (ConnectException e)
|
||||
{
|
||||
log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
catch (SocketTimeoutException e)
|
||||
{
|
||||
log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (in != null)
|
||||
{
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
|
||||
try (BufferedReader in = new BufferedReader(
|
||||
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
result.append(line);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("HTTP 请求失败", e);
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static String sendGet(String url) {
|
||||
return request("GET", url, null, "application/x-www-form-urlencoded");
|
||||
/**
|
||||
* 向指定 URL 发送POST方法的请求
|
||||
*
|
||||
* @param url 发送请求的 URL
|
||||
* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
|
||||
* @return 所代表远程资源的响应结果
|
||||
*/
|
||||
public static String sendPost(String url, String param)
|
||||
{
|
||||
return sendPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
}
|
||||
|
||||
public static String sendGet(String url, String param) {
|
||||
String fullUrl = StringUtils.isNotBlank(param) ? url + "?" + param : url;
|
||||
return request("GET", fullUrl, null, "application/x-www-form-urlencoded");
|
||||
/**
|
||||
* 向指定 URL 发送POST方法的请求
|
||||
*
|
||||
* @param url 发送请求的 URL
|
||||
* @param param 请求参数
|
||||
* @param contentType 内容类型
|
||||
* @return 所代表远程资源的响应结果
|
||||
*/
|
||||
public static String sendPost(String url, String param, String contentType)
|
||||
{
|
||||
PrintWriter out = null;
|
||||
BufferedReader in = null;
|
||||
StringBuilder result = new StringBuilder();
|
||||
try
|
||||
{
|
||||
log.info("sendPost - {}", url);
|
||||
URL realUrl = new URL(url);
|
||||
URLConnection conn = realUrl.openConnection();
|
||||
conn.setRequestProperty("accept", "*/*");
|
||||
conn.setRequestProperty("connection", "Keep-Alive");
|
||||
conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
|
||||
conn.setRequestProperty("Accept-Charset", "utf-8");
|
||||
conn.setRequestProperty("Content-Type", contentType);
|
||||
conn.setDoOutput(true);
|
||||
conn.setDoInput(true);
|
||||
out = new PrintWriter(conn.getOutputStream());
|
||||
out.print(param);
|
||||
out.flush();
|
||||
in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null)
|
||||
{
|
||||
result.append(line);
|
||||
}
|
||||
log.info("recv - {}", result);
|
||||
}
|
||||
catch (ConnectException e)
|
||||
{
|
||||
log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
catch (SocketTimeoutException e)
|
||||
{
|
||||
log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (out != null)
|
||||
{
|
||||
out.close();
|
||||
}
|
||||
if (in != null)
|
||||
{
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static String sendPost(String url, String param) {
|
||||
return request("POST", url, param, "application/x-www-form-urlencoded; charset=UTF-8");
|
||||
/**
|
||||
* 向指定 URL 发送 POST 请求,内容类型为 JSON
|
||||
*
|
||||
* @param url 请求的 URL
|
||||
* @param reqPayload 请求参数,Map 类型
|
||||
* @return 响应结果
|
||||
*/
|
||||
public static String sendPostJson(String url, Map<String, Object> reqPayload) {
|
||||
// 创建一个 ObjectMapper 实例,用于将 Map 转换为 JSON 字符串
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String jsonParam = null;
|
||||
try {
|
||||
// 将 Map 转换为 JSON 字符串
|
||||
jsonParam = objectMapper.writeValueAsString(reqPayload);
|
||||
} catch (Exception e) {
|
||||
log.error("请求参数转换为 JSON 失败,url=" + url, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 调用 sendPost 方法,发送请求并返回响应
|
||||
return sendPost(url, jsonParam, "application/json");
|
||||
}
|
||||
|
||||
public static String sendPostJson(String url, Map<String, Object> params) {
|
||||
String json = JsonUtils.toJson(params);
|
||||
return request("POST", url, json, "application/json; charset=UTF-8");
|
||||
public static String sendSSLPost(String url, String param)
|
||||
{
|
||||
return sendSSLPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
}
|
||||
|
||||
}
|
||||
public static String sendSSLPost(String url, String param, String contentType)
|
||||
{
|
||||
StringBuilder result = new StringBuilder();
|
||||
String urlNameString = url + "?" + param;
|
||||
try
|
||||
{
|
||||
log.info("sendSSLPost - {}", urlNameString);
|
||||
SSLContext sc = SSLContext.getInstance("SSL");
|
||||
sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
|
||||
URL console = new URL(urlNameString);
|
||||
HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
|
||||
conn.setRequestProperty("accept", "*/*");
|
||||
conn.setRequestProperty("connection", "Keep-Alive");
|
||||
conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
|
||||
conn.setRequestProperty("Accept-Charset", "utf-8");
|
||||
conn.setRequestProperty("Content-Type", contentType);
|
||||
conn.setDoOutput(true);
|
||||
conn.setDoInput(true);
|
||||
|
||||
conn.setSSLSocketFactory(sc.getSocketFactory());
|
||||
conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
|
||||
conn.connect();
|
||||
InputStream is = conn.getInputStream();
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(is));
|
||||
String ret = "";
|
||||
while ((ret = br.readLine()) != null)
|
||||
{
|
||||
if (ret != null && !"".equals(ret.trim()))
|
||||
{
|
||||
result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
log.info("recv - {}", result);
|
||||
conn.disconnect();
|
||||
br.close();
|
||||
}
|
||||
catch (ConnectException e)
|
||||
{
|
||||
log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
catch (SocketTimeoutException e)
|
||||
{
|
||||
log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e);
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static class TrustAnyTrustManager implements X509TrustManager
|
||||
{
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers()
|
||||
{
|
||||
return new X509Certificate[] {};
|
||||
}
|
||||
}
|
||||
|
||||
private static class TrustAnyHostnameVerifier implements HostnameVerifier
|
||||
{
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package cn.g3soft.utils;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 客户端工具类
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class ServletUtils
|
||||
{
|
||||
// /**
|
||||
// * 获取String参数
|
||||
// */
|
||||
// public static String getParameter(String name)
|
||||
// {
|
||||
// return getRequest().getParameter(name);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 获取String参数
|
||||
// */
|
||||
// public static String getParameter(String name, String defaultValue)
|
||||
// {
|
||||
// return Convert.toStr(getRequest().getParameter(name), defaultValue);
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 获取Integer参数
|
||||
// */
|
||||
// public static Integer getParameterToInt(String name)
|
||||
// {
|
||||
// return Convert.toInt(getRequest().getParameter(name));
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 获取Integer参数
|
||||
// */
|
||||
// public static Integer getParameterToInt(String name, Integer defaultValue)
|
||||
// {
|
||||
// return Convert.toInt(getRequest().getParameter(name), defaultValue);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 获取Boolean参数
|
||||
// */
|
||||
// public static Boolean getParameterToBool(String name)
|
||||
// {
|
||||
// return Convert.toBool(getRequest().getParameter(name));
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * 获取Boolean参数
|
||||
// */
|
||||
// public static Boolean getParameterToBool(String name, Boolean defaultValue)
|
||||
// {
|
||||
// return Convert.toBool(getRequest().getParameter(name), defaultValue);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 获得所有请求参数
|
||||
*
|
||||
* @param request 请求对象{@link ServletRequest}
|
||||
* @return Map
|
||||
*/
|
||||
public static Map<String, String[]> getParams(ServletRequest request)
|
||||
{
|
||||
final Map<String, String[]> map = request.getParameterMap();
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得所有请求参数
|
||||
*
|
||||
* @param request 请求对象{@link ServletRequest}
|
||||
* @return Map
|
||||
*/
|
||||
public static Map<String, String> getParamMap(ServletRequest request)
|
||||
{
|
||||
Map<String, String> params = new HashMap<>();
|
||||
for (Map.Entry<String, String[]> entry : getParams(request).entrySet())
|
||||
{
|
||||
params.put(entry.getKey(), StringUtils.join(entry.getValue(), ","));
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取request
|
||||
*/
|
||||
public static HttpServletRequest getRequest()
|
||||
{
|
||||
return getRequestAttributes().getRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取response
|
||||
*/
|
||||
public static HttpServletResponse getResponse()
|
||||
{
|
||||
return getRequestAttributes().getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取session
|
||||
*/
|
||||
public static HttpSession getSession()
|
||||
{
|
||||
return getRequest().getSession();
|
||||
}
|
||||
|
||||
public static ServletRequestAttributes getRequestAttributes()
|
||||
{
|
||||
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
|
||||
return (ServletRequestAttributes) attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串渲染到客户端
|
||||
*
|
||||
* @param response 渲染对象
|
||||
* @param string 待渲染的字符串
|
||||
*/
|
||||
public static void renderString(HttpServletResponse response, String string)
|
||||
{
|
||||
try
|
||||
{
|
||||
response.setStatus(200);
|
||||
response.setContentType("application/json");
|
||||
response.setCharacterEncoding("utf-8");
|
||||
response.getWriter().print(string);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是Ajax异步请求
|
||||
*
|
||||
* @param request
|
||||
*/
|
||||
public static boolean isAjaxRequest(HttpServletRequest request)
|
||||
{
|
||||
String accept = request.getHeader("accept");
|
||||
if (accept != null && accept.contains("application/json"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
String xRequestedWith = request.getHeader("X-Requested-With");
|
||||
if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
String uri = request.getRequestURI();
|
||||
if (inStringIgnoreCase(uri, ".json", ".xml"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
String ajax = request.getParameter("__ajax");
|
||||
return inStringIgnoreCase(ajax, "json", "xml");
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容编码
|
||||
*
|
||||
* @param str 内容
|
||||
* @return 编码后的内容
|
||||
*/
|
||||
public static String urlEncode(String str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return URLEncoder.encode(str, "UTF-8");
|
||||
}
|
||||
catch (UnsupportedEncodingException e)
|
||||
{
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容解码
|
||||
*
|
||||
* @param str 内容
|
||||
* @return 解码后的内容
|
||||
*/
|
||||
public static String urlDecode(String str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return URLDecoder.decode(str, "UTF-8");
|
||||
}
|
||||
catch (UnsupportedEncodingException e)
|
||||
{
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 去空格
|
||||
*/
|
||||
public static String trim(String str)
|
||||
{
|
||||
return (str == null ? "" : str.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否包含字符串
|
||||
*
|
||||
* @param str 验证字符串
|
||||
* @param strs 字符串组
|
||||
* @return 包含返回true
|
||||
*/
|
||||
public static boolean inStringIgnoreCase(String str, String... strs)
|
||||
{
|
||||
if (str != null && strs != null)
|
||||
{
|
||||
for (String s : strs)
|
||||
{
|
||||
if (str.equalsIgnoreCase(trim(s)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package cn.g3soft.utils;
|
||||
|
||||
import nl.basjes.parse.useragent.UserAgent;
|
||||
import nl.basjes.parse.useragent.UserAgentAnalyzer;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class UserAgentUtils
|
||||
{
|
||||
public static final String UNKNOWN = "";
|
||||
|
||||
// 浏览器正则表达式模式
|
||||
private static final Pattern CHROME_PATTERN = Pattern.compile("Chrome/(\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern FIREFOX_PATTERN = Pattern.compile("Firefox/(\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern EDGE_PATTERN = Pattern.compile("Edg(?:e)?/(\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern SAFARI_PATTERN = Pattern.compile("Version/(\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern OPERA_PATTERN = Pattern.compile("Opera/(\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern IE_PATTERN = Pattern.compile("(?:MSIE |Trident/.*rv:)(\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern SAMSUNG_PATTERN = Pattern.compile("SamsungBrowser/(\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern UC_PATTERN = Pattern.compile("UCBrowser/(\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern QQ_PATTERN = Pattern.compile("QQBrowser/(\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern WECHAT_PATTERN = Pattern.compile("MicroMessenger/(\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern BAIDU_PATTERN = Pattern.compile("baidubrowser/(\\d+)(?:\\.\\d+)*");
|
||||
|
||||
// 操作系统正则表达式模式
|
||||
private static final Pattern WINDOWS_PATTERN = Pattern.compile("Windows NT (\\d+\\.\\d+)");
|
||||
private static final Pattern MACOS_PATTERN = Pattern.compile("Mac OS X (\\d+[_\\d]*)");
|
||||
private static final Pattern ANDROID_PATTERN = Pattern.compile("Android (\\d+)(?:\\.\\d+)*");
|
||||
private static final Pattern IOS_PATTERN = Pattern.compile("OS[\\s_](\\d+)(?:_\\d+)*");
|
||||
private static final Pattern LINUX_PATTERN = Pattern.compile("Linux");
|
||||
private static final Pattern CHROMEOS_PATTERN = Pattern.compile("CrOS");
|
||||
|
||||
private static final UserAgentAnalyzer userAgentAnalyzer = UserAgentAnalyzer
|
||||
.newBuilder().hideMatcherLoadStats()
|
||||
.withCache(5000)
|
||||
.showMinimalVersion()
|
||||
.withField(UserAgent.AGENT_NAME_VERSION)
|
||||
.withField(UserAgent.OPERATING_SYSTEM_NAME_VERSION)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* 获取客户端浏览器
|
||||
*/
|
||||
public static String getBrowser(String userAgent)
|
||||
{
|
||||
UserAgent.ImmutableUserAgent iua = userAgentAnalyzer.parse(userAgent);
|
||||
String agentNameVersion = iua.get(UserAgent.AGENT_NAME_VERSION).getValue();
|
||||
if (StringUtils.isBlank(agentNameVersion) || agentNameVersion.contains("??"))
|
||||
{
|
||||
return formatBrowser(userAgent);
|
||||
}
|
||||
return agentNameVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端操作系统
|
||||
*/
|
||||
public static String getOperatingSystem(String userAgent)
|
||||
{
|
||||
UserAgent.ImmutableUserAgent iua = userAgentAnalyzer.parse(userAgent);
|
||||
String operatingSystemNameVersion = iua.get(UserAgent.OPERATING_SYSTEM_NAME_VERSION).getValue();
|
||||
if (StringUtils.isBlank(operatingSystemNameVersion) || operatingSystemNameVersion.contains("??"))
|
||||
{
|
||||
return formatOperatingSystem(userAgent);
|
||||
}
|
||||
return operatingSystemNameVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全面浏览器检测
|
||||
*/
|
||||
private static String formatBrowser(String browser)
|
||||
{
|
||||
// Chrome系列浏览器
|
||||
Matcher chromeMatcher = CHROME_PATTERN.matcher(browser);
|
||||
if (chromeMatcher.find() && (browser.contains("Chrome") || browser.contains("CriOS")))
|
||||
{
|
||||
return "Chrome" + chromeMatcher.group(1);
|
||||
}
|
||||
// Firefox
|
||||
Matcher firefoxMatcher = FIREFOX_PATTERN.matcher(browser);
|
||||
if (firefoxMatcher.find())
|
||||
{
|
||||
return "Firefox" + firefoxMatcher.group(1);
|
||||
}
|
||||
// Edge浏览器
|
||||
Matcher edgeMatcher = EDGE_PATTERN.matcher(browser);
|
||||
if (edgeMatcher.find())
|
||||
{
|
||||
return "Edge" + edgeMatcher.group(1);
|
||||
}
|
||||
// Safari浏览器(需排除Chrome)
|
||||
Matcher safariMatcher = SAFARI_PATTERN.matcher(browser);
|
||||
if (safariMatcher.find() && !browser.contains("Chrome"))
|
||||
{
|
||||
return "Safari" + safariMatcher.group(1);
|
||||
}
|
||||
// 微信内置浏览器
|
||||
Matcher wechatMatcher = WECHAT_PATTERN.matcher(browser);
|
||||
if (wechatMatcher.find())
|
||||
{
|
||||
return "WeChat" + wechatMatcher.group(1);
|
||||
}
|
||||
// UC浏览器
|
||||
Matcher ucMatcher = UC_PATTERN.matcher(browser);
|
||||
if (ucMatcher.find())
|
||||
{
|
||||
return "UC Browser" + ucMatcher.group(1);
|
||||
}
|
||||
// QQ浏览器
|
||||
Matcher qqMatcher = QQ_PATTERN.matcher(browser);
|
||||
if (qqMatcher.find())
|
||||
{
|
||||
return "QQ Browser" + qqMatcher.group(1);
|
||||
}
|
||||
// 百度浏览器
|
||||
Matcher baiduMatcher = BAIDU_PATTERN.matcher(browser);
|
||||
if (baiduMatcher.find())
|
||||
{
|
||||
return "Baidu Browser" + baiduMatcher.group(1);
|
||||
}
|
||||
// Samsung浏览器
|
||||
Matcher samsungMatcher = SAMSUNG_PATTERN.matcher(browser);
|
||||
if (samsungMatcher.find())
|
||||
{
|
||||
return "Samsung Browser" + samsungMatcher.group(1);
|
||||
}
|
||||
// Opera浏览器
|
||||
Matcher operaMatcher = OPERA_PATTERN.matcher(browser);
|
||||
if (operaMatcher.find())
|
||||
{
|
||||
return "Opera" + operaMatcher.group(1);
|
||||
}
|
||||
// IE浏览器
|
||||
Matcher ieMatcher = IE_PATTERN.matcher(browser);
|
||||
if (ieMatcher.find())
|
||||
{
|
||||
return "Internet Explorer" + ieMatcher.group(1);
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测操作系统
|
||||
*/
|
||||
private static String formatOperatingSystem(String operatingSystem)
|
||||
{
|
||||
// Windows系统
|
||||
Matcher windowsMatcher = WINDOWS_PATTERN.matcher(operatingSystem);
|
||||
if (windowsMatcher.find())
|
||||
{
|
||||
return "Windows" + getWindowsVersionDisplay(windowsMatcher.group(1));
|
||||
}
|
||||
// macOS系统
|
||||
Matcher macMatcher = MACOS_PATTERN.matcher(operatingSystem);
|
||||
if (macMatcher.find())
|
||||
{
|
||||
String version = macMatcher.group(1).replace("_", ".");
|
||||
return "macOS" + extractMajorVersion(version);
|
||||
}
|
||||
// Android系统
|
||||
Matcher androidMatcher = ANDROID_PATTERN.matcher(operatingSystem);
|
||||
if (androidMatcher.find())
|
||||
{
|
||||
return "Android" + extractMajorVersion(androidMatcher.group(1));
|
||||
}
|
||||
// iOS系统
|
||||
Matcher iosMatcher = IOS_PATTERN.matcher(operatingSystem);
|
||||
if (iosMatcher.find() && (operatingSystem.contains("iPhone") || operatingSystem.contains("iPad")))
|
||||
{
|
||||
return "iOS" + extractMajorVersion(iosMatcher.group(1));
|
||||
}
|
||||
// Linux系统
|
||||
if (LINUX_PATTERN.matcher(operatingSystem).find() && !operatingSystem.contains("Android"))
|
||||
{
|
||||
return "Linux";
|
||||
}
|
||||
// Chrome OS
|
||||
if (CHROMEOS_PATTERN.matcher(operatingSystem).find())
|
||||
{
|
||||
return "Chrome OS";
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取优化的主版本号
|
||||
*/
|
||||
private static String extractMajorVersion(String fullVersion)
|
||||
{
|
||||
if (StringUtils.isEmpty(fullVersion))
|
||||
{
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
try
|
||||
{
|
||||
// 清理版本号中的非数字字符
|
||||
String cleanVersion = fullVersion.replaceAll("[^0-9.]", "");
|
||||
String[] parts = cleanVersion.split("\\.");
|
||||
if (parts.length > 0)
|
||||
{
|
||||
String firstPart = parts[0];
|
||||
if (firstPart.matches("\\d+"))
|
||||
{
|
||||
int version = Integer.parseInt(firstPart);
|
||||
|
||||
// 处理三位数版本号(如142 -> 14)
|
||||
if (version >= 100)
|
||||
{
|
||||
return String.valueOf(version / 10);
|
||||
}
|
||||
return firstPart;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
// 版本号解析失败,返回原始值
|
||||
}
|
||||
return fullVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows版本号显示优化
|
||||
*/
|
||||
private static String getWindowsVersionDisplay(String version)
|
||||
{
|
||||
switch (version)
|
||||
{
|
||||
case "10.0":
|
||||
return "10";
|
||||
case "6.3":
|
||||
return "8.1";
|
||||
case "6.2":
|
||||
return "8";
|
||||
case "6.1":
|
||||
return "7";
|
||||
case "6.0":
|
||||
return "Vista";
|
||||
case "5.1":
|
||||
return "XP";
|
||||
case "5.0":
|
||||
return "2000";
|
||||
default:
|
||||
return extractMajorVersion(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cn.g3soft.utils.ip;
|
||||
|
||||
import cn.g3soft.utils.HttpUtils;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AddressUtils
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(AddressUtils.class);
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// IP地址查询
|
||||
public static final String IP_URL = "https://whois.pconline.com.cn/ipJson.jsp";
|
||||
|
||||
// 未知地址
|
||||
public static final String UNKNOWN = "XX XX";
|
||||
|
||||
public static String getRealAddressByIP(String ip)
|
||||
{
|
||||
// 内网不查询
|
||||
if (IpUtils.internalIp(ip))
|
||||
{
|
||||
return "内网IP";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
String rspStr = HttpUtils.sendGet(IP_URL, "ip=" + ip + "&json=true", "GBK");
|
||||
if (StringUtils.isEmpty(rspStr))
|
||||
{
|
||||
log.error("获取地理位置异常 {}", ip);
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
// 使用 Jackson 解析 JSON 字符串
|
||||
JsonNode obj = objectMapper.readTree(rspStr);
|
||||
String region = obj.path("pro").asText();
|
||||
String city = obj.path("city").asText();
|
||||
|
||||
return String.format("%s %s", region, city);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("获取地理位置异常 {}", ip, e);
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
package cn.g3soft.utils.ip;
|
||||
|
||||
import cn.g3soft.utils.ServletUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
|
||||
public class IpUtils
|
||||
{
|
||||
public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)";
|
||||
// 匹配 ip
|
||||
public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")";
|
||||
public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))";
|
||||
// 匹配网段
|
||||
public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")";
|
||||
|
||||
/**
|
||||
* 获取客户端IP
|
||||
*
|
||||
* @return IP地址
|
||||
*/
|
||||
public static String getIpAddr()
|
||||
{
|
||||
return getIpAddr(ServletUtils.getRequest());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @return IP地址
|
||||
*/
|
||||
public static String getIpAddr(HttpServletRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
String ip = request.getHeader("x-forwarded-for");
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
|
||||
{
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
|
||||
{
|
||||
ip = request.getHeader("X-Forwarded-For");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
|
||||
{
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
|
||||
{
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
|
||||
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip))
|
||||
{
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为内部IP地址
|
||||
*
|
||||
* @param ip IP地址
|
||||
* @return 结果
|
||||
*/
|
||||
public static boolean internalIp(String ip)
|
||||
{
|
||||
byte[] addr = textToNumericFormatV4(ip);
|
||||
return internalIp(addr) || "127.0.0.1".equals(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为内部IP地址
|
||||
*
|
||||
* @param addr byte地址
|
||||
* @return 结果
|
||||
*/
|
||||
private static boolean internalIp(byte[] addr)
|
||||
{
|
||||
if (addr == null || addr.length < 2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
final byte b0 = addr[0];
|
||||
final byte b1 = addr[1];
|
||||
// 10.x.x.x/8
|
||||
final byte SECTION_1 = 0x0A;
|
||||
// 172.16.x.x/12
|
||||
final byte SECTION_2 = (byte) 0xAC;
|
||||
final byte SECTION_3 = (byte) 0x10;
|
||||
final byte SECTION_4 = (byte) 0x1F;
|
||||
// 192.168.x.x/16
|
||||
final byte SECTION_5 = (byte) 0xC0;
|
||||
final byte SECTION_6 = (byte) 0xA8;
|
||||
switch (b0)
|
||||
{
|
||||
case SECTION_1:
|
||||
return true;
|
||||
case SECTION_2:
|
||||
if (b1 >= SECTION_3 && b1 <= SECTION_4)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
case SECTION_5:
|
||||
switch (b1)
|
||||
{
|
||||
case SECTION_6:
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将IPv4地址转换成字节
|
||||
*
|
||||
* @param text IPv4地址
|
||||
* @return byte 字节
|
||||
*/
|
||||
public static byte[] textToNumericFormatV4(String text)
|
||||
{
|
||||
if (text.length() == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[4];
|
||||
String[] elements = text.split("\\.", -1);
|
||||
try
|
||||
{
|
||||
long l;
|
||||
int i;
|
||||
switch (elements.length)
|
||||
{
|
||||
case 1:
|
||||
l = Long.parseLong(elements[0]);
|
||||
if ((l < 0L) || (l > 4294967295L))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
bytes[0] = (byte) (int) (l >> 24 & 0xFF);
|
||||
bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
|
||||
bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
|
||||
bytes[3] = (byte) (int) (l & 0xFF);
|
||||
break;
|
||||
case 2:
|
||||
l = Integer.parseInt(elements[0]);
|
||||
if ((l < 0L) || (l > 255L))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
bytes[0] = (byte) (int) (l & 0xFF);
|
||||
l = Integer.parseInt(elements[1]);
|
||||
if ((l < 0L) || (l > 16777215L))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
bytes[1] = (byte) (int) (l >> 16 & 0xFF);
|
||||
bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
|
||||
bytes[3] = (byte) (int) (l & 0xFF);
|
||||
break;
|
||||
case 3:
|
||||
for (i = 0; i < 2; ++i)
|
||||
{
|
||||
l = Integer.parseInt(elements[i]);
|
||||
if ((l < 0L) || (l > 255L))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
bytes[i] = (byte) (int) (l & 0xFF);
|
||||
}
|
||||
l = Integer.parseInt(elements[2]);
|
||||
if ((l < 0L) || (l > 65535L))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
bytes[2] = (byte) (int) (l >> 8 & 0xFF);
|
||||
bytes[3] = (byte) (int) (l & 0xFF);
|
||||
break;
|
||||
case 4:
|
||||
for (i = 0; i < 4; ++i)
|
||||
{
|
||||
l = Integer.parseInt(elements[i]);
|
||||
if ((l < 0L) || (l > 255L))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
bytes[i] = (byte) (int) (l & 0xFF);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取IP地址
|
||||
*
|
||||
* @return 本地IP地址
|
||||
*/
|
||||
public static String getHostIp()
|
||||
{
|
||||
try
|
||||
{
|
||||
return InetAddress.getLocalHost().getHostAddress();
|
||||
}
|
||||
catch (UnknownHostException e)
|
||||
{
|
||||
}
|
||||
return "127.0.0.1";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主机名
|
||||
*
|
||||
* @return 本地主机名
|
||||
*/
|
||||
public static String getHostName()
|
||||
{
|
||||
try
|
||||
{
|
||||
return InetAddress.getLocalHost().getHostName();
|
||||
}
|
||||
catch (UnknownHostException e)
|
||||
{
|
||||
}
|
||||
return "未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从多级反向代理中获得第一个非unknown IP地址
|
||||
*
|
||||
* @param ip 获得的IP地址
|
||||
* @return 第一个非unknown IP地址
|
||||
*/
|
||||
public static String getMultistageReverseProxyIp(String ip)
|
||||
{
|
||||
// 多级反向代理检测
|
||||
if (ip != null && ip.indexOf(",") > 0)
|
||||
{
|
||||
final String[] ips = ip.trim().split(",");
|
||||
for (String subIp : ips)
|
||||
{
|
||||
if (false == isUnknown(subIp))
|
||||
{
|
||||
ip = subIp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return StringUtils.substring(ip, 0, 255);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测给定字符串是否为未知,多用于检测HTTP请求相关
|
||||
*
|
||||
* @param checkString 被检测的字符串
|
||||
* @return 是否未知
|
||||
*/
|
||||
public static boolean isUnknown(String checkString)
|
||||
{
|
||||
return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为IP
|
||||
*/
|
||||
public static boolean isIP(String ip)
|
||||
{
|
||||
return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为IP,或 *为间隔的通配符地址
|
||||
*/
|
||||
public static boolean isIpWildCard(String ip)
|
||||
{
|
||||
return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测参数是否在ip通配符里
|
||||
*/
|
||||
public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip)
|
||||
{
|
||||
String[] s1 = ipWildCard.split("\\.");
|
||||
String[] s2 = ip.split("\\.");
|
||||
boolean isMatchedSeg = true;
|
||||
for (int i = 0; i < s1.length && !s1[i].equals("*"); i++)
|
||||
{
|
||||
if (!s1[i].equals(s2[i]))
|
||||
{
|
||||
isMatchedSeg = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isMatchedSeg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串
|
||||
*/
|
||||
public static boolean isIPSegment(String ipSeg)
|
||||
{
|
||||
return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断ip是否在指定网段中
|
||||
*/
|
||||
public static boolean ipIsInNetNoCheck(String iparea, String ip)
|
||||
{
|
||||
int idx = iparea.indexOf('-');
|
||||
String[] sips = iparea.substring(0, idx).split("\\.");
|
||||
String[] sipe = iparea.substring(idx + 1).split("\\.");
|
||||
String[] sipt = ip.split("\\.");
|
||||
long ips = 0L, ipe = 0L, ipt = 0L;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
ips = ips << 8 | Integer.parseInt(sips[i]);
|
||||
ipe = ipe << 8 | Integer.parseInt(sipe[i]);
|
||||
ipt = ipt << 8 | Integer.parseInt(sipt[i]);
|
||||
}
|
||||
if (ips > ipe)
|
||||
{
|
||||
long t = ips;
|
||||
ips = ipe;
|
||||
ipe = t;
|
||||
}
|
||||
return ips <= ipt && ipt <= ipe;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验ip是否符合过滤串规则
|
||||
*
|
||||
* @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99`
|
||||
* @param ip 校验IP地址
|
||||
* @return boolean 结果
|
||||
*/
|
||||
public static boolean isMatchedIp(String filter, String ip)
|
||||
{
|
||||
if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
String[] ips = filter.split(";");
|
||||
for (String iStr : ips)
|
||||
{
|
||||
if (isIP(iStr) && iStr.equals(ip))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,7 @@ spring:
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: -1
|
||||
max-request-size: -1
|
||||
max-request-size: -1
|
||||
jackson:
|
||||
time-zone: "Asia/Shanghai"
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
VITE_APP_PORT = 8081
|
||||
|
||||
VITE_ALIYUN_BUCKET = 'g3hy'
|
||||
VITE_ALIYUN_BUCKET = 'g3hd'
|
||||
@@ -12,4 +12,4 @@ PUBLIC_SERVER_PORT=8080
|
||||
# 后端 API 基础地址
|
||||
PUBLIC_API_BASE_URL=http://127.0.0.1:8085/api
|
||||
|
||||
PUBLIC_ALIYUN_BUCKET = g3hy
|
||||
PUBLIC_ALIYUN_BUCKET = g3hd
|
||||
@@ -32,7 +32,7 @@ import { loadDataApi, saveObjectKeysApi, deleteFileApi } from "@/request/api";
|
||||
|
||||
const props = defineProps({
|
||||
father: {
|
||||
type: [String, Number],
|
||||
type: null,
|
||||
required: true,
|
||||
},
|
||||
moduleId: {
|
||||
|
||||
@@ -50,6 +50,14 @@ const menuList = ref([
|
||||
label: "模块配置",
|
||||
path: "/system/module",
|
||||
},
|
||||
{
|
||||
label: "数据日志",
|
||||
path: "/system/log/data",
|
||||
},
|
||||
{
|
||||
label: "登录日志",
|
||||
path: "/system/log/login",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -19,11 +19,19 @@ router.beforeEach(async (to) => {
|
||||
const userStore = useUserStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
|
||||
if (to.path === "/login") {
|
||||
if (userStore.isLogined()) {
|
||||
return "/";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!userStore.isLogined() && to.path !== "/login") {
|
||||
ElMessage.warning("登录已过期,请先登录");
|
||||
return "/login";
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (to.meta.moduleId) {
|
||||
await permissionStore.init();
|
||||
|
||||
@@ -35,6 +43,7 @@ router.beforeEach(async (to) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 tab 页路径
|
||||
handleTabPath(to);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -68,6 +68,22 @@ export const routes = [
|
||||
moduleId: "s_awokemodule",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/system/log/data",
|
||||
component: () => import("@/views/system/log/data.vue"),
|
||||
meta: {
|
||||
title: "数据日志",
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/system/log/login",
|
||||
component: () => import("@/views/system/log/login.vue"),
|
||||
meta: {
|
||||
title: "登录日志",
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
// ==================== 个人中心 ====================
|
||||
{
|
||||
path: "/user-center",
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
.el-loading-mask {
|
||||
&.is-fullscreen {
|
||||
background: "rgba(0, 0, 0, 0.5)";
|
||||
|
||||
.el-loading-spinner .path {
|
||||
stroke: #fff !important;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export function updateViewTableColumns(tableRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const newColumns = columns
|
||||
.filter((item) => item.type === undefined && item.params && item.params.b_id)
|
||||
.map((item, index) => ({
|
||||
@@ -119,3 +118,35 @@ export function formatValueByFormat(value, format) {
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 format 格式化值
|
||||
* @param {any} value 原始值
|
||||
* @param {string} format 格式描述,如 money(2)、percent(1)
|
||||
*/
|
||||
export function formatValueByFullFormat(value, format) {
|
||||
if (value === null || value === undefined || value === "") return "";
|
||||
|
||||
if (!format || typeof format !== "string") return value;
|
||||
|
||||
const fmt = format.trim().toLowerCase();
|
||||
|
||||
// money(n)
|
||||
if (fmt.startsWith("money")) {
|
||||
const match = fmt.match(/^money\((\d+)\)$/);
|
||||
const decimals = match ? Number(match[1]) : 2;
|
||||
|
||||
const num = Number.parseFloat(value);
|
||||
if (Number.isNaN(num)) return value;
|
||||
|
||||
return num.toFixed(decimals);
|
||||
} else if (fmt === "date") {
|
||||
return formatToDate(value);
|
||||
} else if (fmt === "datetime") {
|
||||
return formatToDateTime(value);
|
||||
} else if (fmt === "number") {
|
||||
return Number(value) || 0;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
import ExcelJS from "exceljs";
|
||||
import { formatValueByFullFormat, getAlignByFormat } from "@/utils/columns";
|
||||
import { ElLoading, ElMessage } from "element-plus";
|
||||
import { getAliyunFileBlob } from "./file";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
export async function ExportVxeTableExcel(tableRef) {
|
||||
const $table = unref(tableRef);
|
||||
|
||||
if (!$table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: "正在下载Excel文件中,请稍候...",
|
||||
});
|
||||
|
||||
// columns
|
||||
const fullColumns = $table.getFullColumns();
|
||||
const exportColumns = buildColumns(fullColumns) || [];
|
||||
const exportDatas =
|
||||
$table.getCheckboxRecords().length > 0 ? $table.getCheckboxRecords() : $table.getFullData();
|
||||
|
||||
// const footerDatas = updateTableFooterData(exportColumns, exportDatas)
|
||||
const footerDatas = [];
|
||||
|
||||
const options = {
|
||||
columns: exportColumns,
|
||||
data: exportDatas,
|
||||
};
|
||||
|
||||
try {
|
||||
await exportExcel(options);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function urlToBase64(url) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const blob = await getAliyunFileBlob(url);
|
||||
|
||||
// Create an object URL from the blob
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const img = new Image();
|
||||
|
||||
img.crossOrigin = "Anonymous";
|
||||
img.onload = function () {
|
||||
canvas.height = img.height;
|
||||
canvas.width = img.width;
|
||||
ctx.drawImage(img, 0, 0, img.width, img.height);
|
||||
var dataURL = canvas.toDataURL("image/png");
|
||||
resolve(dataURL);
|
||||
|
||||
// Optional: Revoke the object URL once done to free memory
|
||||
URL.revokeObjectURL(objectURL);
|
||||
};
|
||||
|
||||
img.src = objectURL;
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildColumns(arr) {
|
||||
return arr
|
||||
.map((item) => {
|
||||
// 先处理 children(关键!)
|
||||
let children = [];
|
||||
if (item.children && item.children.length > 0) {
|
||||
children = buildColumns(item.children);
|
||||
}
|
||||
|
||||
// 判断当前节点是否需要保留
|
||||
const valid = item.type === undefined && item.field !== undefined;
|
||||
|
||||
// 如果当前不合法 + 没有子节点,直接丢
|
||||
if (!valid && children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建 column
|
||||
const column = {
|
||||
label: item.title,
|
||||
width: item.params?.col_Width || item.renderWidth,
|
||||
field: item.field,
|
||||
type: item.params?.col_DisplayFormat,
|
||||
};
|
||||
|
||||
// 有子节点就挂上
|
||||
if (children.length > 0) {
|
||||
column.children = children;
|
||||
}
|
||||
|
||||
return column;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出Excel文件(支持多级表头、表尾、图片)
|
||||
* @param {Object} options 导出配置
|
||||
* @param {String} fileName 文件名(默认:export.xlsx)
|
||||
*/
|
||||
export async function exportExcel(options, fileName = "export.xlsx") {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet("Sheet1");
|
||||
|
||||
// 1. 处理多级表头
|
||||
const { headerRows, leafColumns, mergeCells } = buildMultiLevelHeader(options.columns);
|
||||
|
||||
// 写入表头行
|
||||
headerRows.forEach((row, rowIndex) => {
|
||||
const headerRow = worksheet.addRow(row);
|
||||
|
||||
// 设置表头样式(突出显示)
|
||||
headerRow.eachCell((cell) => {
|
||||
cell.style = {
|
||||
font: { bold: true, size: 12, color: { argb: "FFFFFFFF" } },
|
||||
fill: {
|
||||
type: "pattern",
|
||||
pattern: "solid",
|
||||
fgColor: { argb: "FF4F81BD" }, // 蓝色背景
|
||||
},
|
||||
border: {
|
||||
top: { style: "thin", color: { argb: "FF000000" } },
|
||||
left: { style: "thin", color: { argb: "FF000000" } },
|
||||
bottom: { style: "thin", color: { argb: "FF000000" } },
|
||||
right: { style: "thin", color: { argb: "FF000000" } },
|
||||
},
|
||||
alignment: { vertical: "middle", horizontal: "center" },
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// 合并表头单元格
|
||||
mergeCells.forEach((merge) => {
|
||||
worksheet.mergeCells(merge.start.row, merge.start.col, merge.end.row, merge.end.col);
|
||||
});
|
||||
|
||||
// 2. 写入数据行
|
||||
const dataStartRow = headerRows.length + 1;
|
||||
|
||||
for (const [rowIndex, item] of options.data.entries()) {
|
||||
const dataRow = worksheet.addRow([]);
|
||||
|
||||
for (const [colIndex, col] of leafColumns.entries()) {
|
||||
const cell = dataRow.getCell(colIndex + 1);
|
||||
const value = getNestedValue(item, col.field);
|
||||
|
||||
// 处理图片类型
|
||||
if (col.type === "image" && value) {
|
||||
try {
|
||||
|
||||
const base64 = await urlToBase64(value)
|
||||
// 添加图片到工作簿
|
||||
const imageId = workbook.addImage({
|
||||
base64,
|
||||
extension: "png",
|
||||
});
|
||||
|
||||
// 计算图片位置(跳过表头行)
|
||||
const imageRow = dataStartRow + rowIndex;
|
||||
worksheet.addImage(imageId, {
|
||||
tl: { col: colIndex, row: imageRow - 1 },
|
||||
ext: { width: col.imageWidth || 80, height: col.imageHeight || 80 },
|
||||
});
|
||||
|
||||
// 调整行高以适应图片
|
||||
dataRow.height = Math.max(dataRow.height || 20, (col.imageHeight || 80) * 0.75);
|
||||
} catch (error) {
|
||||
console.warn(`图片导出失败: ${error.message}`);
|
||||
cell.value = "[图片]";
|
||||
}
|
||||
} else {
|
||||
cell.value = value;
|
||||
}
|
||||
|
||||
// 设置数据行样式
|
||||
cell.style = {
|
||||
alignment: { vertical: "middle", horizontal: "left" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 写入表尾数据(突出显示)
|
||||
if (options.footer && options.footer.length > 0) {
|
||||
const footerStartRow = dataStartRow + options.data.length;
|
||||
|
||||
options.footer.forEach((footerItem, rowIndex) => {
|
||||
const footerRow = worksheet.addRow([]);
|
||||
|
||||
leafColumns.forEach((col, colIndex) => {
|
||||
const cell = footerRow.getCell(colIndex + 1);
|
||||
const value = getNestedValue(footerItem, col.field);
|
||||
cell.value = value;
|
||||
|
||||
// 设置表尾样式(突出显示)
|
||||
cell.style = {
|
||||
font: { bold: true },
|
||||
// fill: {
|
||||
// type: "pattern",
|
||||
// pattern: "solid",
|
||||
// // fgColor: { argb: "FFC00000" }, // 红色背景
|
||||
// },
|
||||
// border: {
|
||||
// top: { style: "medium", color: { argb: "FF000000" } },
|
||||
// left: { style: "medium", color: { argb: "FF000000" } },
|
||||
// bottom: { style: "medium", color: { argb: "FF000000" } },
|
||||
// right: { style: "medium", color: { argb: "FF000000" } },
|
||||
// },
|
||||
alignment: { vertical: "middle", horizontal: "center" },
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 设置列宽
|
||||
leafColumns.forEach((col, index) => {
|
||||
const column = worksheet.getColumn(index + 1);
|
||||
column.width = col.width ? col.width / 7 : 20; // 转换像素为Excel单位
|
||||
});
|
||||
|
||||
// 5. 生成并保存文件
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
saveAs(blob, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建多级表头
|
||||
* @param {Array} columns 表头配置
|
||||
* @returns {Object} { headerRows, leafColumns, mergeCells }
|
||||
*/
|
||||
function buildMultiLevelHeader(columns) {
|
||||
// 获取表头最大深度
|
||||
const maxDepth = getMaxDepth(columns);
|
||||
|
||||
// 获取所有叶子节点列
|
||||
const leafColumns = getLeafColumns(columns);
|
||||
|
||||
// 初始化表头二维数组
|
||||
const headerRows = Array.from({ length: maxDepth }, () => Array(leafColumns.length).fill(""));
|
||||
|
||||
const mergeCells = [];
|
||||
|
||||
// 递归填充表头
|
||||
function fillHeader(nodes, startCol = 0, depth = 0) {
|
||||
let currentCol = startCol;
|
||||
|
||||
nodes.forEach((node) => {
|
||||
// 计算当前节点跨越的列数
|
||||
const colSpan = node.children ? getLeafCount(node.children) : 1;
|
||||
|
||||
// 填充当前节点的表头
|
||||
headerRows[depth][currentCol] = node.label || "";
|
||||
|
||||
// 如果有子节点,递归处理
|
||||
if (node.children && node.children.length > 0) {
|
||||
fillHeader(node.children, currentCol, depth + 1);
|
||||
|
||||
// 如果跨多列,需要合并单元格
|
||||
if (colSpan > 1) {
|
||||
mergeCells.push({
|
||||
start: { row: depth + 1, col: currentCol + 1 },
|
||||
end: { row: depth + 1, col: currentCol + colSpan },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当前节点有父节点且不是叶子节点,需要纵向合并
|
||||
if (depth < maxDepth - 1 && !node.children) {
|
||||
mergeCells.push({
|
||||
start: { row: depth + 1, col: currentCol + 1 },
|
||||
end: { row: maxDepth, col: currentCol + 1 },
|
||||
});
|
||||
}
|
||||
|
||||
currentCol += colSpan;
|
||||
});
|
||||
}
|
||||
|
||||
fillHeader(columns);
|
||||
|
||||
return { headerRows, leafColumns, mergeCells };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取树的最大深度
|
||||
*/
|
||||
function getMaxDepth(nodes, currentDepth = 1) {
|
||||
if (!nodes || nodes.length === 0) return currentDepth;
|
||||
|
||||
let maxDepth = currentDepth;
|
||||
nodes.forEach((node) => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
const depth = getMaxDepth(node.children, currentDepth + 1);
|
||||
maxDepth = Math.max(maxDepth, depth);
|
||||
}
|
||||
});
|
||||
|
||||
return maxDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有叶子节点列
|
||||
*/
|
||||
function getLeafColumns(nodes, result = []) {
|
||||
nodes.forEach((node) => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
getLeafColumns(node.children, result);
|
||||
} else {
|
||||
result.push({
|
||||
field: node.field,
|
||||
label: node.label,
|
||||
width: node.width,
|
||||
type: node.type,
|
||||
imageWidth: node.imageWidth,
|
||||
imageHeight: node.imageHeight,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取叶子节点数量
|
||||
*/
|
||||
function getLeafCount(nodes) {
|
||||
if (!nodes || nodes.length === 0) return 0;
|
||||
|
||||
return nodes.reduce((count, node) => {
|
||||
if (node.children && node.children.length > 0) {
|
||||
return count + getLeafCount(node.children);
|
||||
}
|
||||
return count + 1;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取嵌套对象的值
|
||||
*/
|
||||
function getNestedValue(obj, path) {
|
||||
if (!path) return "";
|
||||
|
||||
return (
|
||||
path.split(".").reduce((prev, curr) => {
|
||||
return prev ? prev[curr] : "";
|
||||
}, obj) || ""
|
||||
);
|
||||
}
|
||||
@@ -203,14 +203,113 @@ export function getFileExtension(fileName) {
|
||||
return fileExt;
|
||||
}
|
||||
|
||||
// 上传到阿里云
|
||||
// blob fileName fileSize xh moduleid father cateid
|
||||
export function downloadZip(files, fileName = "文件合集", callback) {
|
||||
const zip = new JSZip();
|
||||
const addedFiles = new Set();
|
||||
|
||||
const tasks = files.map((item) => {
|
||||
return getAliyunFileBlob(item.mx_mapfilename)
|
||||
.then((res) => {
|
||||
const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles);
|
||||
zip.file(uniqueFileName, res, { binary: true });
|
||||
addedFiles.add(uniqueFileName);
|
||||
})
|
||||
.catch((err) => {
|
||||
// 捕获单个文件下载失败的错误,不影响其他文件下载
|
||||
console.error(`Failed to download file: ${item.mx_filename}`, err);
|
||||
});
|
||||
});
|
||||
|
||||
// 关键:使用 Promise.all 确保所有任务完成
|
||||
return Promise.all(tasks)
|
||||
.then(() => zip.generateAsync({ type: "blob" }))
|
||||
.then((res) => {
|
||||
saveAs(res, `${fileName}.zip`);
|
||||
callback && callback();
|
||||
})
|
||||
.catch((err) => {
|
||||
callback && callback();
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// 处理文件名重复的函数
|
||||
function getUniqueFileName(fileName, addedFiles) {
|
||||
let uniqueFileName = fileName;
|
||||
let counter = 1;
|
||||
|
||||
// 如果文件名已经存在,则在文件名后加上 (1), (2), ...
|
||||
while (addedFiles.has(uniqueFileName)) {
|
||||
const fileNameWithoutExtension = uniqueFileName.replace(/\.[^/.]+$/, "");
|
||||
const extension = uniqueFileName.slice(uniqueFileName.lastIndexOf("."));
|
||||
uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return uniqueFileName;
|
||||
}
|
||||
|
||||
// ============================================ 阿里云上传和下载 ============================================
|
||||
|
||||
let cachedCredentials = null; // 用来缓存凭证
|
||||
let isFetchingCredentials = false; // 控制是否正在请求凭证
|
||||
|
||||
// 获取阿里云文件的上传凭证
|
||||
export function getAliyunUploadCredentials() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 如果缓存中有有效的凭证,则直接使用
|
||||
if (cachedCredentials && !isCredentialsExpired(cachedCredentials)) {
|
||||
resolve(cachedCredentials); // 凭证未过期,直接返回缓存的凭证
|
||||
} else {
|
||||
// 如果正在请求凭证,则等待
|
||||
if (isFetchingCredentials) {
|
||||
const interval = setInterval(() => {
|
||||
// 等待直到凭证被请求完毕
|
||||
if (!isFetchingCredentials) {
|
||||
clearInterval(interval); // 请求完成后停止轮询
|
||||
resolve(cachedCredentials); // 返回缓存的凭证
|
||||
}
|
||||
}, 100); // 每100ms检查一次
|
||||
} else {
|
||||
// 如果凭证过期,重新请求新的凭证
|
||||
isFetchingCredentials = true; // 设置为正在请求凭证状态
|
||||
getAliyunKeyApi()
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
const { accessid, policy, signature, expire, dir, host } = res.data;
|
||||
|
||||
// 缓存凭证,并记录过期时间
|
||||
cachedCredentials = {
|
||||
accessid,
|
||||
policy,
|
||||
signature,
|
||||
expire: parseInt(expire, 10),
|
||||
dir,
|
||||
host,
|
||||
};
|
||||
|
||||
isFetchingCredentials = false; // 请求完成,解除锁定
|
||||
resolve(cachedCredentials); // 返回新凭证
|
||||
} else {
|
||||
isFetchingCredentials = false; // 请求失败,解除锁定
|
||||
reject(new Error("获取凭证失败"));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
isFetchingCredentials = false; // 请求失败,解除锁定
|
||||
reject(new Error("请求凭证失败:" + err.message));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 上传文件到阿里云
|
||||
export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, father, cateid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getAliyunKeyApi()
|
||||
.then(async (res) => {
|
||||
getAliyunUploadCredentials()
|
||||
.then(async (aliyunKey) => {
|
||||
try {
|
||||
const aliyunKey = res.data;
|
||||
const formData = new FormData();
|
||||
|
||||
let uniqueName = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
@@ -219,12 +318,17 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath
|
||||
uniqueName += `.${fileExt}`;
|
||||
}
|
||||
|
||||
// 构造上传的文件路径
|
||||
formData.append("key", `${aliyunKey.dir}${uniqueName}`);
|
||||
formData.append("OSSAccessKeyId", aliyunKey.accessid);
|
||||
formData.append("policy", aliyunKey.policy);
|
||||
formData.append("Signature", aliyunKey.signature);
|
||||
formData.append("file", blob, uniqueName);
|
||||
|
||||
console.log(formData);
|
||||
console.log(aliyunKey.host);
|
||||
console.log(aliyunKey.dir);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", aliyunKey.host, true);
|
||||
|
||||
@@ -232,6 +336,7 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath
|
||||
if (xhr.status === 200 || xhr.status === 204) {
|
||||
const allFilePath = `${aliyunKey.host}/${aliyunKey.dir}${uniqueName}`;
|
||||
|
||||
// 上传后处理(插入文件记录等)
|
||||
const insertRes = await insertFileApi({
|
||||
father,
|
||||
mx_moduleid: moduleid,
|
||||
@@ -278,76 +383,35 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath
|
||||
});
|
||||
}
|
||||
|
||||
// 检查凭证是否过期
|
||||
function isCredentialsExpired(credentials) {
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
return currentTime > credentials.expire;
|
||||
}
|
||||
|
||||
// 获取阿里云文件的 Blob 数据(用于下载)
|
||||
export function getAliyunFileBlob(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 创建一个独立的异步函数来处理 fetch 请求
|
||||
async function fetchFile() {
|
||||
try {
|
||||
const response = await fetch(url, { mode: "cors" });
|
||||
getAliyunUploadCredentials()
|
||||
.then((aliyunKey) => {
|
||||
const downloadUrl = `${url}?OSSAccessKeyId=${aliyunKey.accessid}&policy=${aliyunKey.policy}&Signature=${aliyunKey.signature}`;
|
||||
|
||||
// 检查响应状态
|
||||
if (!response.ok) {
|
||||
reject(new Error(`下载失败:${response.status}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取文件内容
|
||||
const blob = await response.blob();
|
||||
|
||||
// 返回blob
|
||||
resolve(blob);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 调用异步函数
|
||||
fetchFile();
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadZip(files, fileName = "文件合集", callback) {
|
||||
const zip = new JSZip();
|
||||
const addedFiles = new Set();
|
||||
|
||||
const tasks = files.map((item) => {
|
||||
return getAliyunFileBlob(item.mx_mapfilename)
|
||||
.then((res) => {
|
||||
const uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles);
|
||||
zip.file(uniqueFileName, res, { binary: true });
|
||||
addedFiles.add(uniqueFileName);
|
||||
fetch(downloadUrl, { mode: "cors" })
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败,状态码:${response.status}`);
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then((blob) => {
|
||||
resolve(blob);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
// 捕获单个文件下载失败的错误,不影响其他文件下载
|
||||
console.error(`Failed to download file: ${item.mx_filename}`, err);
|
||||
});
|
||||
.catch(reject);
|
||||
});
|
||||
|
||||
// 关键:使用 Promise.all 确保所有任务完成
|
||||
return Promise.all(tasks)
|
||||
.then(() => zip.generateAsync({ type: "blob" }))
|
||||
.then((res) => {
|
||||
saveAs(res, `${fileName}.zip`);
|
||||
callback && callback();
|
||||
})
|
||||
.catch((err) => {
|
||||
callback && callback();
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// 处理文件名重复的函数
|
||||
function getUniqueFileName(fileName, addedFiles) {
|
||||
let uniqueFileName = fileName;
|
||||
let counter = 1;
|
||||
|
||||
// 如果文件名已经存在,则在文件名后加上 (1), (2), ...
|
||||
while (addedFiles.has(uniqueFileName)) {
|
||||
const fileNameWithoutExtension = uniqueFileName.replace(/\.[^/.]+$/, "");
|
||||
const extension = uniqueFileName.slice(uniqueFileName.lastIndexOf("."));
|
||||
uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return uniqueFileName;
|
||||
}
|
||||
// ============================================ 阿里云上传和下载 ============================================
|
||||
@@ -100,8 +100,6 @@ async function onVcodeSuccess() {
|
||||
res.data.token,
|
||||
);
|
||||
|
||||
await permissionStore.init();
|
||||
|
||||
ElMessage.success("登录成功");
|
||||
|
||||
router.push("/");
|
||||
|
||||
@@ -8,12 +8,13 @@ import {
|
||||
} from "@/request/api";
|
||||
import { getAlignByFormat, formatValueByFormat } from "@/utils/columns";
|
||||
import { updateViewTableColumns } from "@/utils/columns";
|
||||
import { downloadZip } from "@/utils/file";
|
||||
import { downloadZip, getAliyunFileBlob } from "@/utils/file";
|
||||
import { formatToDate } from "@/utils/time";
|
||||
import { ElLoading, ElMessage } from "element-plus";
|
||||
import { saveAs } from "file-saver";
|
||||
import JSZip from "jszip";
|
||||
import { usePermissionStore } from "@/store/permission";
|
||||
import { ExportVxeTableExcel, exportExcel } from "@/utils/excel";
|
||||
|
||||
const tableRef = ref();
|
||||
|
||||
@@ -56,9 +57,7 @@ const viewName = "v_container";
|
||||
const showPreview = ref(false);
|
||||
const previewIndex = ref(-1);
|
||||
const previewImageList = computed(() => {
|
||||
return tableData.value
|
||||
.map((item) => item.b_container_img)
|
||||
.filter((img) => img);
|
||||
return tableData.value.map((item) => item.b_container_img).filter((img) => img);
|
||||
});
|
||||
|
||||
const currentRow = ref({});
|
||||
@@ -191,12 +190,9 @@ async function deleteRows(rows = []) {
|
||||
const safeDeleteIds = deleteIds.map((id) => Number(id));
|
||||
|
||||
// 使用安全的 SQL 查询
|
||||
const fileRes = await loadDataApi(
|
||||
"bf_file",
|
||||
`father IN (${safeDeleteIds.join(",")})`,
|
||||
"",
|
||||
["subid"],
|
||||
);
|
||||
const fileRes = await loadDataApi("bf_file", `father IN (${safeDeleteIds.join(",")})`, "", [
|
||||
"subid",
|
||||
]);
|
||||
|
||||
if (!fileRes.success) {
|
||||
return;
|
||||
@@ -272,11 +268,7 @@ async function handleDownloadOne(data) {
|
||||
});
|
||||
|
||||
try {
|
||||
const fileRes = await loadDataApi(
|
||||
"bf_file",
|
||||
`father = ${data.b_id}`,
|
||||
"mx_xh asc",
|
||||
);
|
||||
const fileRes = await loadDataApi("bf_file", `father = ${data.b_id}`, "mx_xh asc");
|
||||
|
||||
if (!fileRes.success) {
|
||||
ElMessage.error("获取附件失败");
|
||||
@@ -284,11 +276,7 @@ async function handleDownloadOne(data) {
|
||||
}
|
||||
|
||||
const downloadFiles = fileRes.data.map((item, index) => ({
|
||||
mx_filename: buildFileName(
|
||||
data.b_container_no,
|
||||
index + 1,
|
||||
item.mx_filename,
|
||||
),
|
||||
mx_filename: buildFileName(data.b_container_no, index + 1, item.mx_filename),
|
||||
mx_mapfilename: item.mx_mapfilename,
|
||||
}));
|
||||
|
||||
@@ -349,29 +337,19 @@ async function handleDownloadBatch() {
|
||||
let total = 0;
|
||||
let finished = 0;
|
||||
|
||||
// 👉 先统计总文件数(用于进度)
|
||||
for (const item of records) {
|
||||
const fileRes = await loadDataApi(
|
||||
"bf_file",
|
||||
`father = '${item.b_id}'`,
|
||||
"",
|
||||
);
|
||||
const fileRes = await loadDataApi("bf_file", `father = '${item.b_id}'`, "");
|
||||
if (fileRes.success) {
|
||||
total += (fileRes.data || []).length;
|
||||
}
|
||||
}
|
||||
|
||||
// 👉 正式处理
|
||||
for (const item of records) {
|
||||
const folderName = `${item.b_container_no}_${formatToDate(item.b_inputdatetime)}`;
|
||||
|
||||
const folder = zip.folder(folderName);
|
||||
|
||||
const fileRes = await loadDataApi(
|
||||
"bf_file",
|
||||
`father = '${item.b_id}'`,
|
||||
"mx_xh asc",
|
||||
);
|
||||
const fileRes = await loadDataApi("bf_file", `father = '${item.b_id}'`, "mx_xh asc");
|
||||
|
||||
if (!fileRes.success) {
|
||||
ElMessage.error("获取附件失败");
|
||||
@@ -384,12 +362,10 @@ async function handleDownloadBatch() {
|
||||
const fileUrl = file.mx_mapfilename;
|
||||
|
||||
try {
|
||||
const res = await fetch(fileUrl);
|
||||
const blob = await res.blob();
|
||||
const blob = getAliyunFileBlob(fileUrl);
|
||||
|
||||
const fileName =
|
||||
buildFileName(item.b_container_no, index + 1, file.mx_filename) ||
|
||||
"未命名文件";
|
||||
buildFileName(item.b_container_no, index + 1, file.mx_filename) || "未命名文件";
|
||||
|
||||
folder.file(fileName, blob);
|
||||
} catch (e) {
|
||||
@@ -401,11 +377,9 @@ async function handleDownloadBatch() {
|
||||
}
|
||||
});
|
||||
|
||||
// 控制并发数
|
||||
await runWithLimit(tasks, 3);
|
||||
}
|
||||
|
||||
// 👉 生成 zip
|
||||
loading.setText("正在生成压缩包...");
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
|
||||
@@ -425,6 +399,110 @@ function buildFileName(containerNo, index, fileName) {
|
||||
|
||||
return `${containerNo}_${formattedIndex}.${fileName.split(".").pop()}`;
|
||||
}
|
||||
|
||||
function testExport() {
|
||||
|
||||
const options = {
|
||||
columns: [
|
||||
{
|
||||
label: "基础信息",
|
||||
field: "basicInfo",
|
||||
width: 120,
|
||||
children: [
|
||||
{
|
||||
label: "姓名",
|
||||
field: "name",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
label: "年龄",
|
||||
field: "age",
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
label: "头像",
|
||||
field: "avatar",
|
||||
width: 100,
|
||||
type: "image",
|
||||
imageWidth: 80,
|
||||
imageHeight: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "联系方式",
|
||||
field: "contactInfo",
|
||||
width: 160,
|
||||
children: [
|
||||
{
|
||||
label: "电话",
|
||||
field: "phone",
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
label: "邮箱",
|
||||
field: "email",
|
||||
width: 200,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "其他信息",
|
||||
field: "otherInfo",
|
||||
width: 100,
|
||||
children: [
|
||||
{
|
||||
label: "地址信息",
|
||||
field: "address",
|
||||
width: 150,
|
||||
children: [
|
||||
{ label: "省份", field: "province", width: 80 },
|
||||
{ label: "城市", field: "city", width: 80 },
|
||||
],
|
||||
},
|
||||
{ label: "备注", field: "remark", width: 200 },
|
||||
],
|
||||
},
|
||||
],
|
||||
data: [
|
||||
{
|
||||
name: "张三",
|
||||
age: 25,
|
||||
phone: "13800138000",
|
||||
email: "zhangsan@example.com",
|
||||
province: "浙江",
|
||||
city: "杭州",
|
||||
remark: "测试数据1",
|
||||
avatar:
|
||||
"data:image/png;base64,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 truncated
|
||||
},
|
||||
{
|
||||
name: "李四",
|
||||
age: 30,
|
||||
phone: "13900139000",
|
||||
email: "lisi@example.com",
|
||||
province: "江苏",
|
||||
city: "南京",
|
||||
remark: "测试数据2",
|
||||
avatar:
|
||||
"data:image/png;base64,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 truncated
|
||||
},
|
||||
],
|
||||
footer: [
|
||||
{
|
||||
name: "总计",
|
||||
age: 55,
|
||||
phone: "",
|
||||
email: "",
|
||||
province: "",
|
||||
city: "",
|
||||
remark: "2条记录",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
exportExcel(options);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -534,7 +612,7 @@ function buildFileName(containerNo, index, fileName) {
|
||||
<el-dropdown-item @click="updateViewTableColumns(tableRef)">
|
||||
栏位保存
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>导出Execl</el-dropdown-item>
|
||||
<el-dropdown-item @click="ExportVxeTableExcel(tableRef)"> 导出Execl </el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -563,15 +641,10 @@ function buildFileName(containerNo, index, fileName) {
|
||||
trigger: 'cell',
|
||||
}"
|
||||
height="100%"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
>
|
||||
<vxe-column
|
||||
field="seq"
|
||||
type="seq"
|
||||
width="50"
|
||||
align="center"
|
||||
title=" "
|
||||
fixed="left"
|
||||
/>
|
||||
<vxe-column field="seq" type="seq" width="50" align="center" title=" " fixed="left" />
|
||||
<vxe-column type="checkbox" width="50" align="center" fixed="left" />
|
||||
<vxe-column
|
||||
v-for="column in columns"
|
||||
@@ -592,12 +665,7 @@ function buildFileName(containerNo, index, fileName) {
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{
|
||||
formatValueByFormat(
|
||||
row[column.col_FieldName],
|
||||
column.col_DisplayFormat,
|
||||
)
|
||||
}}
|
||||
{{ formatValueByFormat(row[column.col_FieldName], column.col_DisplayFormat) }}
|
||||
</template>
|
||||
</template>
|
||||
</vxe-column>
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
<script lang="js" setup>
|
||||
import { updateViewTableColumns } from "@/utils/columns";
|
||||
import { loadViewColumnsApi, loadPageDataApi } from "@/request/api";
|
||||
import { ExportVxeTableExcel } from "@/utils/excel";
|
||||
|
||||
const viewName = "log_datas";
|
||||
|
||||
const showSearch = ref(true);
|
||||
|
||||
const tableRef = ref();
|
||||
|
||||
const columns = ref([]);
|
||||
const tableData = ref([]);
|
||||
|
||||
const loading = reactive({
|
||||
table: true,
|
||||
});
|
||||
|
||||
const searchForm = ref({
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
datas: "",
|
||||
option: "",
|
||||
table: "",
|
||||
});
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 50,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: "INSERT",
|
||||
value: "INSERT",
|
||||
},
|
||||
{
|
||||
label: "UPDATE",
|
||||
value: "UPDATE",
|
||||
},
|
||||
{
|
||||
label: "DELETE",
|
||||
value: "DELETE",
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(async () => {
|
||||
loading.table = true;
|
||||
|
||||
await loadTableColumns();
|
||||
await loadTableDatas();
|
||||
|
||||
loading.table = false;
|
||||
});
|
||||
|
||||
async function loadTableColumns() {
|
||||
const columnRes = await loadViewColumnsApi(viewName);
|
||||
|
||||
if (columnRes.success) {
|
||||
columns.value = columnRes.data;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTableDatas() {
|
||||
loading.table = true;
|
||||
|
||||
let searchCondition = "";
|
||||
|
||||
if (searchForm.value.startTime) {
|
||||
searchCondition += `b_inputdatetime >= '${searchForm.value.startTime}' AND `;
|
||||
}
|
||||
|
||||
if (searchForm.value.endTime) {
|
||||
searchCondition += `b_inputdatetime <= '${searchForm.value.endTime}' AND `;
|
||||
}
|
||||
|
||||
if (searchCondition.endsWith(" AND ")) {
|
||||
searchCondition = searchCondition.slice(0, -4);
|
||||
}
|
||||
|
||||
await loadPageDataApi({
|
||||
viewName,
|
||||
page: pagination.page,
|
||||
size: pagination.size,
|
||||
searchCondition,
|
||||
orderField: "b_inputdatetime DESC",
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
tableData.value = res.data.rows;
|
||||
pagination.total = res.data.total;
|
||||
|
||||
loading.table = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.size = val;
|
||||
|
||||
loadTableDatas();
|
||||
};
|
||||
const handleCurrentChange = (val) => {
|
||||
pagination.page = val;
|
||||
|
||||
loadTableDatas();
|
||||
};
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1;
|
||||
|
||||
loadTableDatas();
|
||||
}
|
||||
|
||||
function handleSearchReset() {
|
||||
searchForm.value.startTime = "";
|
||||
searchForm.value.endTime = "";
|
||||
searchForm.value.datas = "";
|
||||
searchForm.value.table = "";
|
||||
searchForm.value.option = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-container">
|
||||
<div v-if="showSearch" class="g3-search">
|
||||
<G3Grid class="grid">
|
||||
<G3GridGroup :column="2" :label-width="80">
|
||||
<G3GridItem label="操作时间" :span="2">
|
||||
<el-date-picker
|
||||
type="datetime"
|
||||
placeholder="开始日期"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
v-model="searchForm.startTime"
|
||||
/>
|
||||
<el-divider direction="vertical" />
|
||||
<el-date-picker
|
||||
type="datetime"
|
||||
placeholder="结束日期"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
v-model="searchForm.endTime"
|
||||
/>
|
||||
</G3GridItem>
|
||||
<G3GridItem label="操作内容" :span="2">
|
||||
<el-input v-model="searchForm.datas" />
|
||||
</G3GridItem>
|
||||
<G3GridItem label="操作" :span="1">
|
||||
<el-select v-model="searchForm.option" placeholder="">
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</G3GridItem>
|
||||
<G3GridItem label="操作表" :span="1">
|
||||
<el-input v-model="searchForm.table" />
|
||||
</G3GridItem>
|
||||
</G3GridGroup>
|
||||
</G3Grid>
|
||||
|
||||
<div class="search-extra">
|
||||
<el-button type="primary">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:search" @click="handleSearch" />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
|
||||
<el-button>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:refresh-ccw" @click="handleSearchReset" />
|
||||
</el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="g3-content">
|
||||
<div class="content-top">
|
||||
<div />
|
||||
<div>
|
||||
<el-tooltip content="显示/关闭 搜索">
|
||||
<el-button circle @click="showSearch = !showSearch">
|
||||
<el-icon>
|
||||
<G3Icon icon="lucide:search" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新">
|
||||
<el-button circle @click="loadTableDatas">
|
||||
<el-icon>
|
||||
<G3Icon icon="lucide:refresh-ccw" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-dropdown>
|
||||
<el-button circle style="margin-left: 12px">
|
||||
<el-icon>
|
||||
<G3Icon icon="lucide:ellipsis" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="updateViewTableColumns(tableRef)">
|
||||
栏位保存
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="ExportVxeTableExcel(tableRef)">
|
||||
导出Excel
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="g3-table">
|
||||
<vxe-table
|
||||
ref="tableRef"
|
||||
:loading="loading.table"
|
||||
:data="tableData"
|
||||
:column-config="{
|
||||
resizable: true,
|
||||
drag: true,
|
||||
}"
|
||||
:row-config="{
|
||||
useKey: true,
|
||||
isCurrent: true,
|
||||
isHover: true,
|
||||
}"
|
||||
:filter-config="{
|
||||
showIcon: false,
|
||||
}"
|
||||
:column-drag-config="{
|
||||
showIcon: false,
|
||||
trigger: 'cell',
|
||||
}"
|
||||
height="100%"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
>
|
||||
<vxe-column field="seq" type="seq" width="50" align="center" title=" " fixed="left" />
|
||||
<vxe-column type="checkbox" width="50" align="center" fixed="left" />
|
||||
<vxe-column
|
||||
v-for="column in columns"
|
||||
:key="column.col_FieldName"
|
||||
:field="column.col_FieldName"
|
||||
:title="column.col_Caption"
|
||||
:width="column.col_Width"
|
||||
align="center"
|
||||
:params="column"
|
||||
:sortable="true"
|
||||
>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</div>
|
||||
|
||||
<div class="g3-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[50, 100, 200, 500]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.g3-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.grid {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.search-extra {
|
||||
display: flex;
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-content {
|
||||
flex-grow: 1;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.content-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
::v-deep(.el-button) {
|
||||
height: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.g3-pagination {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,310 @@
|
||||
<script lang="js" setup>
|
||||
import { updateViewTableColumns } from "@/utils/columns";
|
||||
import { loadViewColumnsApi, loadPageDataApi } from "@/request/api";
|
||||
import { ExportVxeTableExcel } from "@/utils/excel";
|
||||
|
||||
const viewName = "b_login_log";
|
||||
|
||||
const showSearch = ref(true);
|
||||
|
||||
const tableRef = ref();
|
||||
|
||||
const columns = ref([]);
|
||||
const tableData = ref([]);
|
||||
|
||||
const loading = reactive({
|
||||
table: true,
|
||||
});
|
||||
|
||||
const searchForm = ref({
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
userid: "",
|
||||
});
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 50,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
loading.table = true;
|
||||
|
||||
await loadTableColumns();
|
||||
await loadTableDatas();
|
||||
|
||||
loading.table = false;
|
||||
});
|
||||
|
||||
async function loadTableColumns() {
|
||||
const columnRes = await loadViewColumnsApi(viewName);
|
||||
|
||||
if (columnRes.success) {
|
||||
columns.value = columnRes.data;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTableDatas() {
|
||||
loading.table = true;
|
||||
|
||||
let searchCondition = "";
|
||||
|
||||
if (searchForm.value.startTime) {
|
||||
searchCondition += `b_login_time >= '${searchForm.value.startTime}' AND `;
|
||||
}
|
||||
|
||||
if (searchForm.value.endTime) {
|
||||
searchCondition += `b_login_time <= '${searchForm.value.endTime}' AND `;
|
||||
}
|
||||
|
||||
if (searchCondition.endsWith(" AND ")) {
|
||||
searchCondition = searchCondition.slice(0, -4);
|
||||
}
|
||||
|
||||
await loadPageDataApi({
|
||||
viewName,
|
||||
page: pagination.page,
|
||||
size: pagination.size,
|
||||
searchCondition,
|
||||
orderField: "b_login_time DESC",
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
tableData.value = res.data.rows;
|
||||
pagination.total = res.data.total;
|
||||
|
||||
loading.table = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.size = val;
|
||||
|
||||
loadTableDatas();
|
||||
};
|
||||
const handleCurrentChange = (val) => {
|
||||
pagination.page = val;
|
||||
|
||||
loadTableDatas();
|
||||
};
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1;
|
||||
|
||||
loadTableDatas();
|
||||
}
|
||||
|
||||
function handleSearchReset() {
|
||||
searchForm.value.startTime = "";
|
||||
searchForm.value.endTime = "";
|
||||
searchForm.value.userid = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-container">
|
||||
<div v-if="showSearch" class="g3-search">
|
||||
<G3Grid class="grid">
|
||||
<G3GridGroup :column="3" :label-width="80">
|
||||
<G3GridItem label="登录时间" :span="2">
|
||||
<el-date-picker
|
||||
type="datetime"
|
||||
placeholder="开始日期"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
v-model="searchForm.startTime"
|
||||
/>
|
||||
<el-divider direction="vertical" />
|
||||
<el-date-picker
|
||||
type="datetime"
|
||||
placeholder="结束日期"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
v-model="searchForm.endTime"
|
||||
/>
|
||||
</G3GridItem>
|
||||
<G3GridItem label="用户账号" :span="1">
|
||||
<!-- <el-input v-model="searchForm.datas" /> -->
|
||||
<G3Select
|
||||
v-model="searchForm.userid"
|
||||
v-model:label="searchForm.userid"
|
||||
search-condition="b_id <> 'g3soft'"
|
||||
view-name="b_user"
|
||||
value-key="b_id"
|
||||
label-key="b_id"
|
||||
keep-input
|
||||
/>
|
||||
</G3GridItem>
|
||||
</G3GridGroup>
|
||||
</G3Grid>
|
||||
|
||||
<div class="search-extra">
|
||||
<el-button type="primary">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:search" @click="handleSearch" />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
|
||||
<el-button>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:refresh-ccw" @click="handleSearchReset" />
|
||||
</el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="g3-content">
|
||||
<div class="content-top">
|
||||
<div />
|
||||
<div>
|
||||
<el-tooltip content="显示/关闭 搜索">
|
||||
<el-button circle @click="showSearch = !showSearch">
|
||||
<el-icon>
|
||||
<G3Icon icon="lucide:search" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新">
|
||||
<el-button circle @click="loadTableDatas">
|
||||
<el-icon>
|
||||
<G3Icon icon="lucide:refresh-ccw" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-dropdown>
|
||||
<el-button circle style="margin-left: 12px">
|
||||
<el-icon>
|
||||
<G3Icon icon="lucide:ellipsis" />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="updateViewTableColumns(tableRef)">
|
||||
栏位保存
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="ExportVxeTableExcel(tableRef)">
|
||||
导出Execl
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="g3-table">
|
||||
<vxe-table
|
||||
ref="tableRef"
|
||||
:loading="loading.table"
|
||||
:data="tableData"
|
||||
:column-config="{
|
||||
resizable: true,
|
||||
drag: true,
|
||||
}"
|
||||
:row-config="{
|
||||
useKey: true,
|
||||
isCurrent: true,
|
||||
isHover: true,
|
||||
}"
|
||||
:filter-config="{
|
||||
showIcon: false,
|
||||
}"
|
||||
:column-drag-config="{
|
||||
showIcon: false,
|
||||
trigger: 'cell',
|
||||
}"
|
||||
height="100%"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
>
|
||||
<vxe-column field="seq" type="seq" width="50" align="center" title=" " fixed="left" />
|
||||
<vxe-column type="checkbox" width="50" align="center" fixed="left" />
|
||||
<vxe-column
|
||||
v-for="column in columns"
|
||||
:key="column.col_FieldName"
|
||||
:field="column.col_FieldName"
|
||||
:title="column.col_Caption"
|
||||
:width="column.col_Width"
|
||||
align="center"
|
||||
:params="column"
|
||||
:sortable="true"
|
||||
>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</div>
|
||||
|
||||
<div class="g3-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[50, 100, 200, 500]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.g3-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: #fff;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.grid {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.search-extra {
|
||||
display: flex;
|
||||
margin-left: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-content {
|
||||
flex-grow: 1;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.content-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
::v-deep(.el-button) {
|
||||
height: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.g3-pagination {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user