u
This commit is contained in:
1 parent
a4e59dd5d1
commit
c7240ea7c9
16 files changed
+507
-385
No files matched your search
@@ -2,7 +2,9 @@ package cn.g3soft.config;
|
|||||||
|
|
||||||
import cn.g3soft.interceptor.AuthInterceptor;
|
import cn.g3soft.interceptor.AuthInterceptor;
|
||||||
import cn.g3soft.interceptor.DataSourceInterceptor;
|
import cn.g3soft.interceptor.DataSourceInterceptor;
|
||||||
import cn.g3soft.interceptor.RequestBodyCacheInterceptor;
|
import cn.g3soft.interceptor.RequestWrapperFilter;
|
||||||
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
@@ -11,9 +13,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
public class WebMvcConfig implements WebMvcConfigurer {
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
@Override
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
registry.addInterceptor(new RequestBodyCacheInterceptor())
|
|
||||||
.addPathPatterns("/**");
|
|
||||||
|
|
||||||
registry.addInterceptor(new AuthInterceptor())
|
registry.addInterceptor(new AuthInterceptor())
|
||||||
.addPathPatterns("/**")
|
.addPathPatterns("/**")
|
||||||
.excludePathPatterns(
|
.excludePathPatterns(
|
||||||
@@ -28,4 +27,13 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
"/error"
|
"/error"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public FilterRegistrationBean<RequestWrapperFilter> requestWrapperFilter() {
|
||||||
|
FilterRegistrationBean<RequestWrapperFilter> registration = new FilterRegistrationBean<>();
|
||||||
|
registration.setFilter(new RequestWrapperFilter());
|
||||||
|
registration.addUrlPatterns("/*");
|
||||||
|
registration.setOrder(1); // 一定要靠前
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1
-4
@@ -1,6 +1,5 @@
|
|||||||
package cn.g3soft.exception;
|
package cn.g3soft.exception;
|
||||||
|
|
||||||
import cn.g3soft.interceptor.RequestBodyCacheInterceptor;
|
|
||||||
import cn.g3soft.utils.ExceptionUtils;
|
import cn.g3soft.utils.ExceptionUtils;
|
||||||
import cn.g3soft.utils.Result;
|
import cn.g3soft.utils.Result;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -14,9 +13,7 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public Result<ExceptionDetailInfo> handleException(Exception ex, HttpServletRequest request) {
|
public Result<ExceptionDetailInfo> handleException(Exception ex, HttpServletRequest request) {
|
||||||
String requestBody = RequestBodyCacheInterceptor.getRequestBodyFromThreadLocal();
|
ExceptionDetailInfo info = ExceptionUtils.extractExceptionInfo(ex, request, "cn.g3soft");
|
||||||
|
|
||||||
ExceptionDetailInfo info = ExceptionUtils.extractExceptionInfo(ex, requestBody, "cn.g3soft");
|
|
||||||
|
|
||||||
log.error("异常发生在 {}#{}({}:{}),ID: {},信息: {}",
|
log.error("异常发生在 {}#{}({}:{}),ID: {},信息: {}",
|
||||||
info.getClassName(),
|
info.getClassName(),
|
||||||
|
|||||||
-60
@@ -1,60 +0,0 @@
|
|||||||
package cn.g3soft.interceptor;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class RequestBodyCacheInterceptor implements HandlerInterceptor {
|
|
||||||
|
|
||||||
private static final ThreadLocal<String> requestBodyThreadLocal = new ThreadLocal<>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
|
||||||
// 仅在异常发生时读取请求体
|
|
||||||
if (response.isCommitted()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存请求体到 ThreadLocal
|
|
||||||
if (isExceptionRequest(request)) {
|
|
||||||
String requestBody = getRequestBody(request);
|
|
||||||
requestBodyThreadLocal.set(requestBody); // 将请求体保存到 ThreadLocal
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isExceptionRequest(HttpServletRequest request) {
|
|
||||||
// 你可以根据某些条件判断是否需要读取请求体,比如请求头或者请求的 URI
|
|
||||||
// 这里简单示范,假设在异常时才读取
|
|
||||||
return true; // 仅在异常发生时才读取请求体
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getRequestBody(HttpServletRequest request) throws IOException {
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8));
|
|
||||||
StringBuilder bodyBuilder = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
bodyBuilder.append(line);
|
|
||||||
}
|
|
||||||
return bodyBuilder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提供获取请求体的方法
|
|
||||||
public static String getRequestBodyFromThreadLocal() {
|
|
||||||
return requestBodyThreadLocal.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
|
||||||
// 清除ThreadLocal中的请求体
|
|
||||||
requestBodyThreadLocal.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package cn.g3soft.interceptor;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||||
|
|
||||||
|
import javax.servlet.*;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class RequestWrapperFilter implements Filter {
|
||||||
|
private static final int MAX_CACHE_SIZE = 1024 * 50; // 50KB
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
HttpServletRequest req = (HttpServletRequest) request;
|
||||||
|
|
||||||
|
String contentType = req.getContentType();
|
||||||
|
|
||||||
|
// 只处理 JSON,排除文件上传
|
||||||
|
if (contentType != null
|
||||||
|
&& contentType.contains("application/json")
|
||||||
|
&& !contentType.contains("multipart/form-data")) {
|
||||||
|
|
||||||
|
ContentCachingRequestWrapper wrapper =
|
||||||
|
new ContentCachingRequestWrapper(req, MAX_CACHE_SIZE);
|
||||||
|
|
||||||
|
chain.doFilter(wrapper, response);
|
||||||
|
} else {
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-48
@@ -3,6 +3,7 @@ package cn.g3soft.utils;
|
|||||||
import cn.g3soft.exception.ExceptionDetailInfo;
|
import cn.g3soft.exception.ExceptionDetailInfo;
|
||||||
import cn.g3soft.utils.uniqueId.idgen.UniqueId;
|
import cn.g3soft.utils.uniqueId.idgen.UniqueId;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.web.util.ContentCachingRequestWrapper;
|
||||||
|
|
||||||
import javax.servlet.ServletInputStream;
|
import javax.servlet.ServletInputStream;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
@@ -10,10 +11,7 @@ import java.io.BufferedReader;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Date;
|
import java.util.*;
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class ExceptionUtils {
|
public class ExceptionUtils {
|
||||||
|
|
||||||
@@ -43,7 +41,6 @@ public class ExceptionUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
|
||||||
String briefMessage;
|
String briefMessage;
|
||||||
|
|
||||||
if (ex.getMessage() != null) {
|
if (ex.getMessage() != null) {
|
||||||
@@ -67,54 +64,38 @@ public class ExceptionUtils {
|
|||||||
briefMessage = ex.getClass().getSimpleName();
|
briefMessage = ex.getClass().getSimpleName();
|
||||||
}
|
}
|
||||||
|
|
||||||
String paramJson = null;
|
// ================= 参数获取 =================
|
||||||
// 先尝试从parameterMap获取普通参数
|
String paramJson = "{}";
|
||||||
Map<String, String> paramMap = new HashMap<>();
|
|
||||||
Enumeration<String> paramNames = request.getParameterNames();
|
|
||||||
while (paramNames.hasMoreElements()) {
|
|
||||||
String paramName = paramNames.nextElement();
|
|
||||||
paramMap.put(paramName, request.getParameter(paramName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果普通参数为空,尝试读取请求体(用于@RequestBody)
|
|
||||||
if (paramMap.isEmpty()) {
|
|
||||||
try {
|
try {
|
||||||
// 获取请求体内容
|
// 1️⃣ URL参数
|
||||||
ServletInputStream inputStream = request.getInputStream();
|
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||||
BufferedReader reader = new BufferedReader(
|
if (!parameterMap.isEmpty()) {
|
||||||
new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
Map<String, String> simpleMap = new HashMap<>();
|
||||||
StringBuilder bodyBuilder = new StringBuilder();
|
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
|
||||||
String line;
|
simpleMap.put(entry.getKey(), Arrays.toString(entry.getValue()));
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
bodyBuilder.append(line);
|
|
||||||
}
|
}
|
||||||
String requestBody = bodyBuilder.toString();
|
paramJson = JsonUtils.toJson(simpleMap);
|
||||||
|
} else {
|
||||||
|
// 2️⃣ Body(重点)
|
||||||
|
if (request instanceof ContentCachingRequestWrapper) {
|
||||||
|
ContentCachingRequestWrapper wrapper =
|
||||||
|
(ContentCachingRequestWrapper) request;
|
||||||
|
|
||||||
if (!requestBody.isEmpty()) {
|
byte[] buf = wrapper.getContentAsByteArray();
|
||||||
// 如果是JSON格式,可以尝试解析
|
|
||||||
try {
|
if (buf.length > 0) {
|
||||||
// 尝试将JSON字符串格式化为可读格式
|
String body = new String(buf, StandardCharsets.UTF_8);
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
|
||||||
Object jsonObject = mapper.readValue(requestBody, Object.class);
|
// 截断,防止过长
|
||||||
paramJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject);
|
paramJson = body.length() > 1000
|
||||||
|
? body.substring(0, 1000) + "..."
|
||||||
|
: body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 如果不是JSON,直接使用原始字符串
|
paramJson = "参数解析失败: " + e.getMessage();
|
||||||
paramJson = requestBody.length() > 1000 ?
|
|
||||||
requestBody.substring(0, 1000) + "..." : requestBody;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
paramJson = "{}";
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
paramJson = "请求体读取失败: " + e.getMessage();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果有普通参数,则序列化普通参数
|
|
||||||
try {
|
|
||||||
paramJson = JsonUtils.toJson(paramMap);
|
|
||||||
} catch (Exception e) {
|
|
||||||
paramJson = "参数序列化失败: " + e.getMessage();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExceptionDetailInfo.builder()
|
return ExceptionDetailInfo.builder()
|
||||||
|
|||||||
@@ -1,187 +1,110 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>数据看板</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>集装箱录入</title>
|
||||||
<!-- Tailwind -->
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
<!-- ECharts -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gray-100">
|
<body class="bg-gray-100">
|
||||||
|
|
||||||
<div class="p-4">
|
|
||||||
|
|
||||||
<!-- 顶部 -->
|
<!-- 顶部 -->
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="bg-white shadow-sm px-4 py-3 text-lg font-semibold">
|
||||||
<h1 class="text-xl font-bold">数据看板</h1>
|
集装箱录入
|
||||||
<button id="refreshBtn"
|
</div>
|
||||||
class="bg-blue-500 text-white px-3 py-1 rounded"
|
|
||||||
onclick="refreshData()">
|
<div class="p-4 space-y-4">
|
||||||
🔄 刷新
|
|
||||||
|
<!-- Step 1 -->
|
||||||
|
<div class="bg-white rounded-2xl shadow p-4 space-y-4">
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-6 h-6 rounded-full bg-blue-500 text-white text-sm flex items-center justify-center">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div class="font-medium text-gray-800">
|
||||||
|
集装箱识别
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 输入框 -->
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-gray-500">集装箱号</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入或识别集装箱号"
|
||||||
|
class="w-full mt-1 border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 拍照 -->
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<div class="flex-1 border-2 border-dashed rounded-xl h-28 flex flex-col items-center justify-center text-gray-400">
|
||||||
|
📷
|
||||||
|
<div class="text-xs mt-1">拍照识别</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 border-2 border-dashed rounded-xl h-28 flex flex-col items-center justify-center text-gray-400">
|
||||||
|
🖼
|
||||||
|
<div class="text-xs mt-1">相册上传</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- OCR结果 -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-3 text-sm text-gray-600">
|
||||||
|
OCR识别结果:<span class="text-gray-400">暂无</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2 -->
|
||||||
|
<div class="bg-white rounded-2xl shadow p-4 space-y-4">
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-6 h-6 rounded-full bg-green-500 text-white text-sm flex items-center justify-center">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div class="font-medium text-gray-800">
|
||||||
|
其他照片录入
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片列表 -->
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
|
||||||
|
<!-- 已上传 -->
|
||||||
|
<div class="relative">
|
||||||
|
<div class="h-24 bg-gray-200 rounded-lg"></div>
|
||||||
|
<div class="absolute top-1 right-1 bg-black/50 text-white text-xs px-1 rounded">
|
||||||
|
✓
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传按钮 -->
|
||||||
|
<div class="h-24 border-2 border-dashed rounded-lg flex flex-col items-center justify-center text-gray-400">
|
||||||
|
➕
|
||||||
|
<div class="text-xs mt-1">添加</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 说明 -->
|
||||||
|
<div class="text-xs text-gray-400">
|
||||||
|
可上传箱体、封条、货物等相关照片
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部按钮 -->
|
||||||
|
<div class="fixed bottom-0 left-0 right-0 bg-white border-t p-3">
|
||||||
|
<button class="w-full bg-blue-500 text-white py-3 rounded-xl text-lg">
|
||||||
|
提交
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- KPI -->
|
|
||||||
<div id="kpi" class="grid grid-cols-6 gap-3 mb-4"></div>
|
|
||||||
|
|
||||||
<!-- 趋势 -->
|
|
||||||
<div class="bg-white p-4 rounded shadow mb-4">
|
|
||||||
<div class="mb-2 font-semibold">近7天上传趋势</div>
|
|
||||||
<div id="trend" style="height:260px;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 中间区域 -->
|
|
||||||
<div class="grid grid-cols-3 gap-4 mb-4">
|
|
||||||
|
|
||||||
<!-- 排行 -->
|
|
||||||
<div class="bg-white p-4 rounded shadow col-span-2">
|
|
||||||
<div class="mb-2 font-semibold">操作员上传排行</div>
|
|
||||||
<div id="rank" style="height:260px;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右侧 -->
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
|
|
||||||
<!-- 最新上传 -->
|
|
||||||
<div class="bg-white p-4 rounded shadow">
|
|
||||||
<div class="mb-2 font-semibold">最新上传</div>
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<thead>
|
|
||||||
<tr class="text-gray-500">
|
|
||||||
<th>箱号</th><th>操作员</th><th>时间</th><th>数量</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="table"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 企业公告 -->
|
|
||||||
<div class="bg-white p-4 rounded shadow">
|
|
||||||
<div class="mb-2 font-semibold">📢 企业公告</div>
|
|
||||||
<div id="notice" class="text-sm space-y-2 max-h-[120px] overflow-auto"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 异常 -->
|
|
||||||
<div class="bg-white p-4 rounded shadow">
|
|
||||||
<div class="mb-2 font-semibold text-red-500">⚠️ 数据异常</div>
|
|
||||||
<div id="warn" class="text-sm text-red-500 space-y-1"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// ===== 模拟数据 =====
|
|
||||||
function getData() {
|
|
||||||
return {
|
|
||||||
kpi: [
|
|
||||||
["今日箱数", rand(80,150)],
|
|
||||||
["照片总数", rand(300,800)],
|
|
||||||
["活跃操作员", rand(5,20)],
|
|
||||||
["最近上传", "10:" + rand(10,59)],
|
|
||||||
["异常箱数", rand(0,10)],
|
|
||||||
["待处理", rand(0,5)],
|
|
||||||
],
|
|
||||||
trend: Array.from({length:7},()=>rand(50,150)),
|
|
||||||
rank: Array.from({length:5},()=>rand(40,120)),
|
|
||||||
users: ["张三","李四","王五","赵六","钱七"],
|
|
||||||
list: [
|
|
||||||
["MSCU1234567","张三","10:30",5],
|
|
||||||
["TEMU7654321","李四","10:20",4],
|
|
||||||
["CMAU9988776","王五","10:10",6],
|
|
||||||
],
|
|
||||||
notice: [
|
|
||||||
"📌 今日起必须上传封条照片",
|
|
||||||
"📌 晚班请补录遗漏数据",
|
|
||||||
"📌 周五晚系统维护"
|
|
||||||
],
|
|
||||||
warn: [
|
|
||||||
"图片少于3张:" + rand(0,5),
|
|
||||||
"箱号为空:" + rand(0,3),
|
|
||||||
"OCR失败:" + rand(0,3)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rand(min,max){return Math.floor(Math.random()*(max-min)+min)}
|
|
||||||
|
|
||||||
// ===== 渲染 =====
|
|
||||||
let trendChart = echarts.init(document.getElementById('trend'));
|
|
||||||
let rankChart = echarts.init(document.getElementById('rank'));
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
const data = getData();
|
|
||||||
|
|
||||||
// KPI
|
|
||||||
document.getElementById("kpi").innerHTML = data.kpi.map(i=>`
|
|
||||||
<div class="bg-white p-3 rounded shadow">
|
|
||||||
<div class="text-gray-400 text-xs">${i[0]}</div>
|
|
||||||
<div class="text-xl font-bold mt-1">${i[1]}</div>
|
|
||||||
</div>
|
|
||||||
`).join("");
|
|
||||||
|
|
||||||
// 表格
|
|
||||||
document.getElementById("table").innerHTML = data.list.map(i=>`
|
|
||||||
<tr class="border-t">
|
|
||||||
<td>${i[0]}</td>
|
|
||||||
<td>${i[1]}</td>
|
|
||||||
<td>${i[2]}</td>
|
|
||||||
<td>${i[3]}</td>
|
|
||||||
</tr>
|
|
||||||
`).join("");
|
|
||||||
|
|
||||||
// 公告
|
|
||||||
document.getElementById("notice").innerHTML =
|
|
||||||
data.notice.map(i=>`<div>${i}</div>`).join("");
|
|
||||||
|
|
||||||
// 异常
|
|
||||||
document.getElementById("warn").innerHTML =
|
|
||||||
data.warn.map(i=>`<div>${i}</div>`).join("");
|
|
||||||
|
|
||||||
// 趋势图
|
|
||||||
trendChart.setOption({
|
|
||||||
xAxis: { type: 'category', data: ['一','二','三','四','五','六','日'] },
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
series: [{
|
|
||||||
data: data.trend,
|
|
||||||
type: 'line',
|
|
||||||
smooth: true
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 排行
|
|
||||||
rankChart.setOption({
|
|
||||||
xAxis: { type: 'category', data: data.users },
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
series: [{
|
|
||||||
data: data.rank,
|
|
||||||
type: 'bar'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== 刷新 =====
|
|
||||||
function refreshData() {
|
|
||||||
const btn = document.getElementById("refreshBtn");
|
|
||||||
btn.innerText = "刷新中...";
|
|
||||||
btn.disabled = true;
|
|
||||||
|
|
||||||
setTimeout(()=>{
|
|
||||||
render();
|
|
||||||
btn.innerText = "🔄 刷新";
|
|
||||||
btn.disabled = false;
|
|
||||||
}, 800);
|
|
||||||
}
|
|
||||||
|
|
||||||
render();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+2
-4
@@ -15,7 +15,6 @@ declare module 'vue' {
|
|||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
|
||||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||||
@@ -34,8 +33,8 @@ declare module 'vue' {
|
|||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
ElResult: typeof import('element-plus/es')['ElResult']
|
||||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSplitter: typeof import('element-plus/es')['ElSplitter']
|
ElSplitter: typeof import('element-plus/es')['ElSplitter']
|
||||||
ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
|
ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
|
||||||
@@ -43,13 +42,12 @@ declare module 'vue' {
|
|||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
|
||||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
|
||||||
ElText: typeof import('element-plus/es')['ElText']
|
ElText: typeof import('element-plus/es')['ElText']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
ElTree: typeof import('element-plus/es')['ElTree']
|
ElTree: typeof import('element-plus/es')['ElTree']
|
||||||
G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default']
|
G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default']
|
||||||
G3Detail: typeof import('./src/components/g3-detail/index.vue')['default']
|
G3Detail: typeof import('./src/components/g3-detail/index.vue')['default']
|
||||||
|
G3Error: typeof import('./src/components/g3-error/index.vue')['default']
|
||||||
G3File: typeof import('./src/components/g3-file/index.vue')['default']
|
G3File: typeof import('./src/components/g3-file/index.vue')['default']
|
||||||
G3Grid: typeof import('./src/components/g3-grid/g3-grid.vue')['default']
|
G3Grid: typeof import('./src/components/g3-grid/g3-grid.vue')['default']
|
||||||
G3GridGroup: typeof import('./src/components/g3-grid/g3-grid-group.vue')['default']
|
G3GridGroup: typeof import('./src/components/g3-grid/g3-grid-group.vue')['default']
|
||||||
|
|||||||
@@ -1,15 +1,33 @@
|
|||||||
import ErrorModal from "./index.vue";
|
import ErrorModal from "./index.vue";
|
||||||
|
|
||||||
|
let isShowingErrorModal = false;
|
||||||
|
let currentApp = null;
|
||||||
|
|
||||||
export const ErrorModalService = {
|
export const ErrorModalService = {
|
||||||
show(responseData) {
|
show(responseData) {
|
||||||
|
if (isShowingErrorModal && currentApp) {
|
||||||
|
currentApp._instance.props.responseData = responseData;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isShowingErrorModal = true;
|
||||||
|
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
document.body.appendChild(container);
|
document.body.appendChild(container);
|
||||||
|
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
data: responseData,
|
||||||
|
};
|
||||||
|
},
|
||||||
render() {
|
render() {
|
||||||
return h(ErrorModal, {
|
return h(ErrorModal, {
|
||||||
responseData: responseData,
|
responseData: this.data,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
|
isShowingErrorModal = false;
|
||||||
|
currentApp = null;
|
||||||
|
|
||||||
app.unmount();
|
app.unmount();
|
||||||
document.body.removeChild(container);
|
document.body.removeChild(container);
|
||||||
},
|
},
|
||||||
@@ -17,6 +35,7 @@ export const ErrorModalService = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
currentApp = app;
|
||||||
app.mount(container);
|
app.mount(container);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -2,11 +2,13 @@
|
|||||||
<div class="error-modal-wrapper" @click.self="handleClose">
|
<div class="error-modal-wrapper" @click.self="handleClose">
|
||||||
<div v-if="visible" class="error-modal" @click.stop>
|
<div v-if="visible" class="error-modal" @click.stop>
|
||||||
<div class="error-modal-header">
|
<div class="error-modal-header">
|
||||||
<span class="error-modal-title"
|
<span class="error-modal-title">⚠️ 系统异常提示 </span>
|
||||||
>⚠️ 系统异常提示 - {{ props.responseData.message }} -
|
<div
|
||||||
{{ props.responseData.code || 500 }}</span
|
class="error-modal-close-btn"
|
||||||
|
@click="handleClose"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div class="error-modal-close-btn" @click="handleClose" role="button" tabindex="0">
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -25,9 +27,12 @@
|
|||||||
<div class="log-item" v-for="item in logList" :key="item.label">
|
<div class="log-item" v-for="item in logList" :key="item.label">
|
||||||
<div class="log-label">{{ item.label }}</div>
|
<div class="log-label">{{ item.label }}</div>
|
||||||
<div class="log-value">
|
<div class="log-value">
|
||||||
<pre v-if="item.label === '堆栈跟踪' || item.label === '请求参数'" class="code-block">{{
|
<pre
|
||||||
item.value
|
v-if="item.label === '堆栈跟踪' || item.label === '请求参数'"
|
||||||
}}</pre>
|
class="code-block"
|
||||||
|
>
|
||||||
|
{{ item.value }}
|
||||||
|
</pre>
|
||||||
<div v-else>{{ item.value }}</div>
|
<div v-else>{{ item.value }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,10 +53,21 @@
|
|||||||
<line x1="12" y1="16" x2="12" y2="12" />
|
<line x1="12" y1="16" x2="12" y2="12" />
|
||||||
<line x1="12" y1="8" x2="12.01" y2="8" />
|
<line x1="12" y1="8" x2="12.01" y2="8" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>请将错误信息 导出 或 复制 发送给管理员!</span>
|
<span style="margin-left: 4px">
|
||||||
|
请将错误信息
|
||||||
|
<span class="highlight" @click="exportAsTxt">导出</span>
|
||||||
|
或
|
||||||
|
<span class="highlight" @click="copyInfo">复制</span>
|
||||||
|
发送给管理员!
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="error-modal-actions">
|
<div class="error-modal-actions">
|
||||||
<div class="btn txt-btn" role="button" tabindex="0" @click="exportAsTxt">
|
<div
|
||||||
|
class="btn txt-btn"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="exportAsTxt"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
class="btn-icon"
|
class="btn-icon"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -61,14 +77,21 @@
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" />
|
<path
|
||||||
|
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"
|
||||||
|
/>
|
||||||
<path d="M14 2v6h6m-9 4H9m3 3H9m6-9h-1" />
|
<path d="M14 2v6h6m-9 4H9m3 3H9m6-9h-1" />
|
||||||
<path d="M12 18v-6m2 3h-4" />
|
<path d="M12 18v-6m2 3h-4" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>导出TXT</span>
|
<span>导出TXT</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn data-btn" role="button" tabindex="0" @click="copyInfo">
|
<div
|
||||||
|
class="btn data-btn"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="copyInfo"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
class="btn-icon"
|
class="btn-icon"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -79,12 +102,19 @@
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" />
|
<rect x="9" y="9" width="13" height="13" rx="2" />
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
<path
|
||||||
|
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>复制信息</span>
|
<span>复制信息</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn cancel-btn" role="button" tabindex="0" @click="handleClose">
|
<div
|
||||||
|
class="btn cancel-btn"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@click="handleClose"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
class="btn-icon"
|
class="btn-icon"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -157,7 +187,9 @@ function handleClose() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportAsTxt() {
|
function exportAsTxt() {
|
||||||
const content = logList.value.map((item) => `${item.label}:\n${item.value}\n`).join("\n");
|
const content = logList.value
|
||||||
|
.map((item) => `${item.label}:\n${item.value}\n`)
|
||||||
|
.join("\n");
|
||||||
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
@@ -177,6 +209,8 @@ function copyInfo() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use "@/styles/mixins.scss" as *;
|
||||||
|
|
||||||
@keyframes modalFadeIn {
|
@keyframes modalFadeIn {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -263,7 +297,6 @@ function copyInfo() {
|
|||||||
.log-item {
|
.log-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 120px 1fr;
|
grid-template-columns: 120px 1fr;
|
||||||
gap: 16px;
|
|
||||||
padding: 14px 0;
|
padding: 14px 0;
|
||||||
border-bottom: 1px solid #d9d9d9;
|
border-bottom: 1px solid #d9d9d9;
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@@ -274,6 +307,7 @@ function copyInfo() {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
.log-value {
|
.log-value {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@@ -302,12 +336,23 @@ function copyInfo() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: #ff7d00;
|
color: #ff7d00;
|
||||||
gap: 8px;
|
font-size: 14px;
|
||||||
font-size: 16px;
|
background: #fff7e6;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.highlight {
|
||||||
|
color: #fff;
|
||||||
|
background: #ff7d00;
|
||||||
|
padding: 2px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.error-modal-actions {
|
.error-modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
@include flexGapRow(12px);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ const props = defineProps({
|
|||||||
searchColumns: { type: Array, default: () => [] },
|
searchColumns: { type: Array, default: () => [] },
|
||||||
orderField: { type: String, default: "" },
|
orderField: { type: String, default: "" },
|
||||||
sql: { type: String, default: "" },
|
sql: { type: String, default: "" },
|
||||||
|
keepInput: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
@@ -42,12 +46,9 @@ const inputChange = ref(false); // 输入变化标记
|
|||||||
const pagination = reactive({ page: 1, size: 10, total: 0 }); // 分页
|
const pagination = reactive({ page: 1, size: 10, total: 0 }); // 分页
|
||||||
|
|
||||||
const popperPaneRef = computed(() => popoverRef.value?.popperRef?.contentRef); // Popover 内容 DOM
|
const popperPaneRef = computed(() => popoverRef.value?.popperRef?.contentRef); // Popover 内容 DOM
|
||||||
const searchColumns = computed(() => [
|
const searchColumns = computed(() => {
|
||||||
props.labelKey,
|
return [...new Set([props.labelKey, props.valueKey, ...props.searchColumns])];
|
||||||
props.valueKey,
|
});
|
||||||
...props.searchColumns,
|
|
||||||
]); // 搜索列
|
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// Watchers
|
// Watchers
|
||||||
// ===============================
|
// ===============================
|
||||||
@@ -76,6 +77,7 @@ function onPopoverAfterLeave() {
|
|||||||
|
|
||||||
function onPopoverBeforeLeave() {
|
function onPopoverBeforeLeave() {
|
||||||
// 弹窗关闭前恢复或清空状态
|
// 弹窗关闭前恢复或清空状态
|
||||||
|
if (!props.keepInput) {
|
||||||
if (props.label) {
|
if (props.label) {
|
||||||
if (innerInput.value === "") {
|
if (innerInput.value === "") {
|
||||||
emits("update:label", "");
|
emits("update:label", "");
|
||||||
@@ -85,6 +87,10 @@ function onPopoverBeforeLeave() {
|
|||||||
innerInput.value = props.label;
|
innerInput.value = props.label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
emits("update:label", innerInput.value);
|
||||||
|
emits("update:modelValue", innerInput.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
@@ -199,7 +205,7 @@ function _fetchOptions(searchTitle) {
|
|||||||
: [...options.value, ...res.data.rows];
|
: [...options.value, ...res.data.rows];
|
||||||
pagination.total = res.data.total;
|
pagination.total = res.data.total;
|
||||||
|
|
||||||
popoverVisible.value = true
|
popoverVisible.value = true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ function onFullScreenChange(val) {
|
|||||||
<el-splitter-panel>
|
<el-splitter-panel>
|
||||||
<div class="g3-layout-right">
|
<div class="g3-layout-right">
|
||||||
<Topbar
|
<Topbar
|
||||||
|
v-if="!fullScreen"
|
||||||
:collapse="isCollapse"
|
:collapse="isCollapse"
|
||||||
@collapse="handleCollapse"
|
@collapse="handleCollapse"
|
||||||
:style="fullScreen ? { height: '0', border: 'none' } : {}"
|
|
||||||
/>
|
/>
|
||||||
<Tab @full-screen-change="onFullScreenChange" />
|
<Tab @full-screen-change="onFullScreenChange" />
|
||||||
<Content />
|
<Content />
|
||||||
|
|||||||
@@ -30,3 +30,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-message {
|
||||||
|
z-index: 99999 !important;
|
||||||
|
}
|
||||||
@@ -56,7 +56,9 @@ const viewName = "v_container";
|
|||||||
const showPreview = ref(false);
|
const showPreview = ref(false);
|
||||||
const previewIndex = ref(-1);
|
const previewIndex = ref(-1);
|
||||||
const previewImageList = computed(() => {
|
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({});
|
const currentRow = ref({});
|
||||||
@@ -189,9 +191,12 @@ async function deleteRows(rows = []) {
|
|||||||
const safeDeleteIds = deleteIds.map((id) => Number(id));
|
const safeDeleteIds = deleteIds.map((id) => Number(id));
|
||||||
|
|
||||||
// 使用安全的 SQL 查询
|
// 使用安全的 SQL 查询
|
||||||
const fileRes = await loadDataApi("bf_file", `father IN (${safeDeleteIds.join(",")})`, "", [
|
const fileRes = await loadDataApi(
|
||||||
"subid",
|
"bf_file",
|
||||||
]);
|
`father IN (${safeDeleteIds.join(",")})`,
|
||||||
|
"",
|
||||||
|
["subid"],
|
||||||
|
);
|
||||||
|
|
||||||
if (!fileRes.success) {
|
if (!fileRes.success) {
|
||||||
return;
|
return;
|
||||||
@@ -267,7 +272,11 @@ async function handleDownloadOne(data) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
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) {
|
if (!fileRes.success) {
|
||||||
ElMessage.error("获取附件失败");
|
ElMessage.error("获取附件失败");
|
||||||
@@ -275,7 +284,11 @@ async function handleDownloadOne(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const downloadFiles = fileRes.data.map((item, index) => ({
|
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,
|
mx_mapfilename: item.mx_mapfilename,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -338,7 +351,11 @@ async function handleDownloadBatch() {
|
|||||||
|
|
||||||
// 👉 先统计总文件数(用于进度)
|
// 👉 先统计总文件数(用于进度)
|
||||||
for (const item of records) {
|
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) {
|
if (fileRes.success) {
|
||||||
total += (fileRes.data || []).length;
|
total += (fileRes.data || []).length;
|
||||||
}
|
}
|
||||||
@@ -350,7 +367,11 @@ async function handleDownloadBatch() {
|
|||||||
|
|
||||||
const folder = zip.folder(folderName);
|
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) {
|
if (!fileRes.success) {
|
||||||
ElMessage.error("获取附件失败");
|
ElMessage.error("获取附件失败");
|
||||||
@@ -367,7 +388,8 @@ async function handleDownloadBatch() {
|
|||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
|
|
||||||
const fileName =
|
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);
|
folder.file(fileName, blob);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -432,7 +454,15 @@ function buildFileName(containerNo, index, fileName) {
|
|||||||
</G3GridItem>
|
</G3GridItem>
|
||||||
|
|
||||||
<G3GridItem label="操作员">
|
<G3GridItem label="操作员">
|
||||||
<el-input v-model="searchForm.username" />
|
<G3Select
|
||||||
|
v-model="searchForm.username"
|
||||||
|
v-model:label="searchForm.username"
|
||||||
|
search-condition="b_id <> 'g3soft'"
|
||||||
|
view-name="b_user"
|
||||||
|
value-key="b_name"
|
||||||
|
label-key="b_name"
|
||||||
|
keep-input
|
||||||
|
/>
|
||||||
</G3GridItem>
|
</G3GridItem>
|
||||||
</G3GridGroup>
|
</G3GridGroup>
|
||||||
</G3Grid>
|
</G3Grid>
|
||||||
@@ -534,7 +564,14 @@ function buildFileName(containerNo, index, fileName) {
|
|||||||
}"
|
}"
|
||||||
height="100%"
|
height="100%"
|
||||||
>
|
>
|
||||||
<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 type="checkbox" width="50" align="center" fixed="left" />
|
||||||
<vxe-column
|
<vxe-column
|
||||||
v-for="column in columns"
|
v-for="column in columns"
|
||||||
@@ -555,7 +592,12 @@ function buildFileName(containerNo, index, fileName) {
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ formatValueByFormat(row[column.col_FieldName], column.col_DisplayFormat) }}
|
{{
|
||||||
|
formatValueByFormat(
|
||||||
|
row[column.col_FieldName],
|
||||||
|
column.col_DisplayFormat,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</vxe-column>
|
</vxe-column>
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ const dialogData = ref({});
|
|||||||
const showFile = ref(false);
|
const showFile = ref(false);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const loadRes = await loadDataApi("oa_notices", ` b_visible='1'`, "b_inputdate desc");
|
const loadRes = await loadDataApi(
|
||||||
|
"v_oa_notices",
|
||||||
|
` b_visible='1'`,
|
||||||
|
"b_inputdatetime desc",
|
||||||
|
);
|
||||||
|
|
||||||
if (loadRes.success) {
|
if (loadRes.success) {
|
||||||
dataList.value = loadRes.data;
|
dataList.value = loadRes.data;
|
||||||
@@ -31,10 +35,18 @@ function handleShowDialog(data) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-for="item in dataList" class="notice-item" @click="handleShowDialog(item)">
|
<div
|
||||||
|
v-if="dataList.length > 0"
|
||||||
|
v-for="item in dataList"
|
||||||
|
class="notice-item"
|
||||||
|
@click="handleShowDialog(item)"
|
||||||
|
>
|
||||||
{{ item.b_subject }}
|
{{ item.b_subject }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-result v-else icon="primary" title="空空如也" sub-title="快去发布公告吧">
|
||||||
|
</el-result>
|
||||||
|
|
||||||
<el-dialog v-model="showDialog" :title="dialogData.b_subject" :width="800">
|
<el-dialog v-model="showDialog" :title="dialogData.b_subject" :width="800">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">发布人</div>
|
<div class="section-title">发布人</div>
|
||||||
@@ -45,7 +57,11 @@ function handleShowDialog(data) {
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">发布时间</div>
|
<div class="section-title">发布时间</div>
|
||||||
<div class="section-value">
|
<div class="section-value">
|
||||||
{{ formatToDateTime(dialogData.b_updatedatetime || dialogData.b_inputdatetime) }}
|
{{
|
||||||
|
formatToDateTime(
|
||||||
|
dialogData.b_updatedatetime || dialogData.b_inputdatetime,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -69,7 +85,12 @@ function handleShowDialog(data) {
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<G3File v-model="showFile" :father="dialogData.b_id" module-id="oa_notices" readonly />
|
<G3File
|
||||||
|
v-model="showFile"
|
||||||
|
:father="dialogData.b_id"
|
||||||
|
module-id="oa_notices"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
<script setup lang="js">
|
|
||||||
import { saveAs } from "file-saver";
|
|
||||||
import JSZip from "jszip";
|
|
||||||
|
|
||||||
// 假设这些是文件数据和它们的路径
|
|
||||||
const files = [
|
|
||||||
{ name: "file1.txt", folder: "folder1", content: "This is file 1 content" },
|
|
||||||
{ name: "file2.txt", folder: "folder2", content: "This is file 2 content" },
|
|
||||||
{ name: "file3.txt", folder: "folder1", content: "This is file 3 content" },
|
|
||||||
{ name: "file4.txt", folder: "folder3", content: "This is file 4 content" },
|
|
||||||
];
|
|
||||||
|
|
||||||
async function createZip() {
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
// 遍历文件列表,将文件按文件夹添加到 ZIP 文件中
|
|
||||||
files.forEach((file) => {
|
|
||||||
// 创建文件夹并将文件添加到对应的文件夹下
|
|
||||||
zip.folder(file.folder).file(file.name, file.content);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成并下载 ZIP 文件
|
|
||||||
const content = await zip.generateAsync({ type: "blob" });
|
|
||||||
saveAs(content, "files.zip");
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<el-button @click="createZip">测试</el-button>
|
|
||||||
</template>
|
|
||||||
@@ -2,7 +2,12 @@
|
|||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import EditDrawer from "./edit-drawer.vue";
|
import EditDrawer from "./edit-drawer.vue";
|
||||||
import CategoryDialog from "./category-dialog.vue";
|
import CategoryDialog from "./category-dialog.vue";
|
||||||
import { loadDataBySqlApi, saveObjectApi } from "@/request/api";
|
import {
|
||||||
|
loadDataApi,
|
||||||
|
loadDataBySqlApi,
|
||||||
|
loadPageDataApi,
|
||||||
|
saveObjectApi,
|
||||||
|
} from "@/request/api";
|
||||||
|
|
||||||
const tableRef = ref(null);
|
const tableRef = ref(null);
|
||||||
|
|
||||||
@@ -15,7 +20,7 @@ const columnData = ref([
|
|||||||
{ prop: "v_inputuser_name", label: "发布人", width: 120 },
|
{ prop: "v_inputuser_name", label: "发布人", width: 120 },
|
||||||
{ prop: "b_inputdate", label: "发布日期", width: 140 },
|
{ prop: "b_inputdate", label: "发布日期", width: 140 },
|
||||||
]);
|
]);
|
||||||
const treeId = ref("");
|
const treeId = ref(-1);
|
||||||
const tableData = ref([]);
|
const tableData = ref([]);
|
||||||
const loading = reactive({
|
const loading = reactive({
|
||||||
table: false,
|
table: false,
|
||||||
@@ -24,12 +29,27 @@ const loading = reactive({
|
|||||||
error: "",
|
error: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const searchForm = ref({
|
||||||
|
username: "",
|
||||||
|
subject: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 0,
|
||||||
|
size: 20,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
children: "children",
|
children: "children",
|
||||||
label: "label",
|
label: "label",
|
||||||
isLeaf: "leaf",
|
isLeaf: "leaf",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadTableDatas();
|
||||||
|
});
|
||||||
|
|
||||||
async function handleNodeClick(node) {
|
async function handleNodeClick(node) {
|
||||||
if (!node.id) {
|
if (!node.id) {
|
||||||
return;
|
return;
|
||||||
@@ -122,7 +142,8 @@ async function handleEditRow(row) {
|
|||||||
|
|
||||||
function handleEditChange() {
|
function handleEditChange() {
|
||||||
drawerVisible.value = false;
|
drawerVisible.value = false;
|
||||||
loadNodeData(treeId.value, "10");
|
// loadNodeData(treeId.value, "10");
|
||||||
|
loadTableDatas();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditClosed() {
|
function handleEditClosed() {
|
||||||
@@ -130,6 +151,58 @@ function handleEditClosed() {
|
|||||||
}
|
}
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
|
|
||||||
|
function loadTableDatas() {
|
||||||
|
loading.table = true;
|
||||||
|
|
||||||
|
let searchCondition = "";
|
||||||
|
|
||||||
|
if (searchForm.value.subject) {
|
||||||
|
searchCondition += `b_subject LIKE '%${searchForm.value.subject}%' AND `;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchForm.value.username) {
|
||||||
|
searchCondition += `v_inputuser_name LIKE '%${searchForm.value.username}%' AND `;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchCondition.endsWith(" AND ")) {
|
||||||
|
searchCondition = searchCondition.slice(0, -4);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPageDataApi({
|
||||||
|
viewName: "v_oa_notices",
|
||||||
|
page: pagination.page,
|
||||||
|
size: pagination.size,
|
||||||
|
orderField: "b_updatedatetime desc",
|
||||||
|
searchCondition,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
tableData.value = res.data.rows;
|
||||||
|
pagination.total = res.data.total;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.table = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
pagination.size = val;
|
||||||
|
|
||||||
|
loadTableDatas();
|
||||||
|
};
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
pagination.page = val;
|
||||||
|
|
||||||
|
loadTableDatas();
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
pagination.page = 1;
|
||||||
|
|
||||||
|
loadTableDatas();
|
||||||
|
}
|
||||||
|
|
||||||
// 删除单行
|
// 删除单行
|
||||||
async function handleDeleteRow(row) {
|
async function handleDeleteRow(row) {
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
@@ -182,7 +255,8 @@ async function deleteRows(rows = []) {
|
|||||||
if (res.success) {
|
if (res.success) {
|
||||||
ElMessage.success("删除成功");
|
ElMessage.success("删除成功");
|
||||||
|
|
||||||
loadNodeData(treeId.value, "10");
|
// loadNodeData(treeId.value, "10");
|
||||||
|
loadTableDatas()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -190,16 +264,16 @@ async function deleteRows(rows = []) {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="g3-flex-container">
|
<div class="g3-flex-container">
|
||||||
<div class="g3-header">
|
<!-- <div class="g3-header">
|
||||||
<el-button type="primary" @click="showCategory = true">
|
<el-button type="primary" @click="showCategory = true">
|
||||||
<el-icon class="el-icon--left">
|
<el-icon class="el-icon--left">
|
||||||
<G3Icon icon="lucide:settings" />
|
<G3Icon icon="lucide:settings" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
分类设置
|
分类设置
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="g3-container">
|
<div class="g3-container">
|
||||||
<el-splitter>
|
<!-- <el-splitter>
|
||||||
<el-splitter-panel size="220">
|
<el-splitter-panel size="220">
|
||||||
<div class="g3-container-left">
|
<div class="g3-container-left">
|
||||||
<div class="g3-title">
|
<div class="g3-title">
|
||||||
@@ -217,12 +291,20 @@ async function deleteRows(rows = []) {
|
|||||||
@node-click="handleNodeClick"
|
@node-click="handleNodeClick"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</el-splitter-panel> -->
|
||||||
|
|
||||||
<el-splitter-panel>
|
<!-- <el-splitter-panel> -->
|
||||||
<div class="g3-container-right">
|
<div class="g3-container-right">
|
||||||
<div style="width: 100%">
|
<div
|
||||||
<div style="display: flex; height: 32px; margin-bottom: 8px">
|
style="
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div style="display: flex">
|
||||||
<el-button type="primary" @click="handleAdd">
|
<el-button type="primary" @click="handleAdd">
|
||||||
<el-icon class="el-icon--left">
|
<el-icon class="el-icon--left">
|
||||||
<G3Icon icon="lucide:plus" />
|
<G3Icon icon="lucide:plus" />
|
||||||
@@ -236,6 +318,37 @@ async function deleteRows(rows = []) {
|
|||||||
批量删除
|
批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center">
|
||||||
|
<G3Grid style="width: 400px">
|
||||||
|
<G3GridGroup :label-width="60" :column="2">
|
||||||
|
<G3GridItem label="主题">
|
||||||
|
<el-input v-model="searchForm.subject" />
|
||||||
|
</G3GridItem>
|
||||||
|
<G3GridItem label="发布人">
|
||||||
|
<G3Select
|
||||||
|
v-model="searchForm.username"
|
||||||
|
v-model:label="searchForm.username"
|
||||||
|
search-condition="b_id <> 'g3soft'"
|
||||||
|
view-name="b_user"
|
||||||
|
value-key="b_name"
|
||||||
|
label-key="b_name"
|
||||||
|
keep-input
|
||||||
|
/>
|
||||||
|
</G3GridItem>
|
||||||
|
</G3GridGroup>
|
||||||
|
</G3Grid>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
style="margin-left: 8px"
|
||||||
|
@click="handleSearch"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--left">
|
||||||
|
<G3Icon icon="lucide:search" />
|
||||||
|
</el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="g3-table">
|
<div class="g3-table">
|
||||||
<vxe-table
|
<vxe-table
|
||||||
@@ -294,12 +407,7 @@ async function deleteRows(rows = []) {
|
|||||||
</template>
|
</template>
|
||||||
</vxe-column>
|
</vxe-column>
|
||||||
|
|
||||||
<vxe-column
|
<vxe-column width="200" align="center" fixed="right" title="操作">
|
||||||
width="200"
|
|
||||||
align="center"
|
|
||||||
fixed="right"
|
|
||||||
title="操作"
|
|
||||||
>
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button type="primary" link @click="handleEditRow(row)">
|
<el-button type="primary" link @click="handleEditRow(row)">
|
||||||
编辑
|
编辑
|
||||||
@@ -312,9 +420,21 @@ async function deleteRows(rows = []) {
|
|||||||
</vxe-column>
|
</vxe-column>
|
||||||
</vxe-table>
|
</vxe-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="g3-pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:page-sizes="[20, 50, 100, 200]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-splitter-panel>
|
</div>
|
||||||
</el-splitter>
|
<!-- </el-splitter-panel> -->
|
||||||
|
<!-- </el-splitter> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditDrawer
|
<EditDrawer
|
||||||
@@ -390,6 +510,15 @@ async function deleteRows(rows = []) {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.g3-pagination {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.g3-footer {
|
.g3-footer {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in new issue
Block a user