This commit is contained in:
oneao committed 2026-03-26 17:25:23 +08:00
1 parent c7240ea7c9
commit 189a085353
26 files changed
+2650 -201

No files matched your search

+13
View File
@@ -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);
}
}
@@ -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);
}
@@ -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;
}
@@ -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
View File
@@ -1,3 +1,3 @@
VITE_APP_PORT = 8081
VITE_ALIYUN_BUCKET = 'g3hy'
VITE_ALIYUN_BUCKET = 'g3hd'
+1 -1
View File
@@ -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) || ""
);
}
+135 -71
View File
@@ -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>