From a4e59dd5d14765258e8e1c52b34997a170a1fc9c Mon Sep 17 00:00:00 2001 From: ZhangAo Date: Wed, 25 Mar 2026 17:31:22 +0800 Subject: [PATCH] u --- .../java/cn/g3soft/config/WebMvcConfig.java | 4 + .../g3soft/exception/ExceptionDetailInfo.java | 45 ++ .../exception/GlobalExceptionHandler.java | 33 ++ .../RequestBodyCacheInterceptor.java | 60 +++ .../main/java/cn/g3soft/utils/DbUtils.java | 60 +-- .../java/cn/g3soft/utils/ExceptionUtils.java | 134 ++++++ .../src/main/java/cn/g3soft/utils/Result.java | 4 + code/container-ocr/ocr-vue/components.d.ts | 12 + code/container-ocr/ocr-vue/package.json | 1 + code/container-ocr/ocr-vue/pnpm-lock.yaml | 23 ++ .../ocr-vue/src/components/g3-error/index.js | 22 + .../ocr-vue/src/components/g3-error/index.vue | 386 ++++++++++++++++++ .../ocr-vue/src/layout/components/menu.vue | 33 +- .../ocr-vue/src/layout/components/topbar.vue | 13 +- .../container-ocr/ocr-vue/src/plugins/icon.js | 2 + .../ocr-vue/src/request/index.js | 3 + .../container-ocr/ocr-vue/src/router/index.js | 25 +- .../ocr-vue/src/router/routes.js | 15 +- .../ocr-vue/src/store/permission.js | 101 +++++ .../container-ocr/ocr-vue/src/utils/common.js | 53 +++ code/container-ocr/ocr-vue/src/utils/file.js | 19 +- .../ocr-vue/src/views/auth/login.vue | 48 +-- .../ocr-vue/src/views/business/detail.vue | 11 - .../ocr-vue/src/views/business/index.vue | 9 - .../ocr-vue/src/views/container/index.vue | 98 ++--- .../src/views/home/components/chart-bar.vue | 119 ++++++ .../src/views/home/components/chart-line.vue | 158 +++++++ .../src/views/home/components/chart-pie.vue | 128 ++++++ .../home/components/chart-release-bar.vue | 160 ++++++++ .../src/views/home/components/value-card.vue | 102 +++++ .../ocr-vue/src/views/home/g3-echarts.vue | 66 +++ .../ocr-vue/src/views/home/index.vue | 50 +++ .../ocr-vue/src/views/home/notice.vue | 131 ++++++ .../ocr-vue/src/views/home/service.vue | 108 +++++ .../ocr-vue/src/views/home/user-reminder.vue | 65 +++ .../views/system/module/module-powervalue.vue | 203 ++------- .../src/views/system/permission/index.vue | 50 +-- code/container-ocr/ocr-vue/test.html | 237 +++++++---- .../g3hd-vue/src/views/home-page/index.vue | 1 + 39 files changed, 2333 insertions(+), 459 deletions(-) create mode 100644 code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/exception/ExceptionDetailInfo.java create mode 100644 code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/exception/GlobalExceptionHandler.java create mode 100644 code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/interceptor/RequestBodyCacheInterceptor.java create mode 100644 code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ExceptionUtils.java create mode 100644 code/container-ocr/ocr-vue/src/components/g3-error/index.js create mode 100644 code/container-ocr/ocr-vue/src/components/g3-error/index.vue create mode 100644 code/container-ocr/ocr-vue/src/store/permission.js create mode 100644 code/container-ocr/ocr-vue/src/utils/common.js delete mode 100644 code/container-ocr/ocr-vue/src/views/business/detail.vue delete mode 100644 code/container-ocr/ocr-vue/src/views/business/index.vue create mode 100644 code/container-ocr/ocr-vue/src/views/home/components/chart-bar.vue create mode 100644 code/container-ocr/ocr-vue/src/views/home/components/chart-line.vue create mode 100644 code/container-ocr/ocr-vue/src/views/home/components/chart-pie.vue create mode 100644 code/container-ocr/ocr-vue/src/views/home/components/chart-release-bar.vue create mode 100644 code/container-ocr/ocr-vue/src/views/home/components/value-card.vue create mode 100644 code/container-ocr/ocr-vue/src/views/home/g3-echarts.vue create mode 100644 code/container-ocr/ocr-vue/src/views/home/index.vue create mode 100644 code/container-ocr/ocr-vue/src/views/home/notice.vue create mode 100644 code/container-ocr/ocr-vue/src/views/home/service.vue create mode 100644 code/container-ocr/ocr-vue/src/views/home/user-reminder.vue diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/WebMvcConfig.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/WebMvcConfig.java index 47395892..21948f42 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/WebMvcConfig.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/config/WebMvcConfig.java @@ -2,6 +2,7 @@ package cn.g3soft.config; import cn.g3soft.interceptor.AuthInterceptor; import cn.g3soft.interceptor.DataSourceInterceptor; +import cn.g3soft.interceptor.RequestBodyCacheInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -10,6 +11,9 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new RequestBodyCacheInterceptor()) + .addPathPatterns("/**"); + registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/**") .excludePathPatterns( diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/exception/ExceptionDetailInfo.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/exception/ExceptionDetailInfo.java new file mode 100644 index 00000000..0d9c2615 --- /dev/null +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/exception/ExceptionDetailInfo.java @@ -0,0 +1,45 @@ +package cn.g3soft.exception; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Builder; +import lombok.Data; + +import java.util.Date; + +@Data +@Builder +public class ExceptionDetailInfo { + /** 异常记录的唯一ID */ + private Long id; + + /** 异常发生时间 */ + private Date time; + + /** 请求的URI地址 */ + private String uri; + + /** HTTP请求方法,如GET、POST等 */ + private String httpMethod; + + /** 抛出异常的类名 */ + private String className; + + /** 抛出异常的方法名 */ + private String methodName; + + /** 抛出异常的文件名 */ + private String fileName; + + /** 抛出异常的代码行号 */ + private Integer lineNumber; + + /** 异常的简短信息 */ + private String summary; + + /** 异常的详细堆栈信息 */ + private String detailMessage; + + /** 请求参数(可能为JSON字符串等) */ + private String params; +} diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/exception/GlobalExceptionHandler.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..ca21b60d --- /dev/null +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/exception/GlobalExceptionHandler.java @@ -0,0 +1,33 @@ +package cn.g3soft.exception; + +import cn.g3soft.interceptor.RequestBodyCacheInterceptor; +import cn.g3soft.utils.ExceptionUtils; +import cn.g3soft.utils.Result; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.servlet.http.HttpServletRequest; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(Exception.class) + public Result handleException(Exception ex, HttpServletRequest request) { + String requestBody = RequestBodyCacheInterceptor.getRequestBodyFromThreadLocal(); + + ExceptionDetailInfo info = ExceptionUtils.extractExceptionInfo(ex, requestBody, "cn.g3soft"); + + log.error("异常发生在 {}#{}({}:{}),ID: {},信息: {}", + info.getClassName(), + info.getMethodName(), + info.getFileName(), + info.getLineNumber(), + info.getId(), + info.getSummary()); + + ex.printStackTrace(); + + return Result.fail(Result.SYSTEM_ERROR_CODE,info); + } +} diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/interceptor/RequestBodyCacheInterceptor.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/interceptor/RequestBodyCacheInterceptor.java new file mode 100644 index 00000000..983acd26 --- /dev/null +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/interceptor/RequestBodyCacheInterceptor.java @@ -0,0 +1,60 @@ +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 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(); + } +} \ No newline at end of file diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/DbUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/DbUtils.java index 04bd0589..9d1426af 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/DbUtils.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/DbUtils.java @@ -219,18 +219,18 @@ public class DbUtils { throw new RuntimeException("只允许执行SELECT查询"); } - Matcher matcher = ILLEGAL_SQL_PATTERN.matcher(sql); - if (matcher.find()) { - throw new RuntimeException("检测到非法SQL关键字: " + matcher.group()); - } - - if (sql.contains(";")) { - throw new RuntimeException("SQL包含非法字符 ';'"); - } - - if (sql.contains("--") || sql.contains("/*") || sql.contains("*/")) { - throw new RuntimeException("SQL包含非法注释"); - } +// Matcher matcher = ILLEGAL_SQL_PATTERN.matcher(sql); +// if (matcher.find()) { +// throw new RuntimeException("检测到非法SQL关键字: " + matcher.group()); +// } +// +// if (sql.contains(";")) { +// throw new RuntimeException("SQL包含非法字符 ';'"); +// } +// +// if (sql.contains("--") || sql.contains("/*") || sql.contains("*/")) { +// throw new RuntimeException("SQL包含非法注释"); +// } } /** @@ -244,11 +244,11 @@ public class DbUtils { throw new RuntimeException("插入数据不能为空"); } - Object pkValue = getPkValue(data, pkColumn); - - if(ObjectUtils.isEmpty(pkValue)){ - throw new RuntimeException("主键不能为空"); - } +// Object pkValue = getPkValue(data, pkColumn); +// +// if(ObjectUtils.isEmpty(pkValue)){ +// throw new RuntimeException("主键不能为空"); +// } String columns = data.keySet() .stream() @@ -277,19 +277,19 @@ public class DbUtils { throw new RuntimeException("插入数据不能为空"); } - if (pkColumns == null || pkColumns.isEmpty()) { - throw new RuntimeException("主键不能为空"); - } - - // 校验主键 - for (String pk : pkColumns) { - validateIdentifier(pk); - - Object pkValue = getPkValue(data, pk); - if (ObjectUtils.isEmpty(pkValue)) { - throw new RuntimeException("主键不能为空:" + pk); - } - } +// if (pkColumns == null || pkColumns.isEmpty()) { +// throw new RuntimeException("主键不能为空"); +// } +// +// // 校验主键 +// for (String pk : pkColumns) { +// validateIdentifier(pk); +// +// Object pkValue = getPkValue(data, pk); +// if (ObjectUtils.isEmpty(pkValue)) { +// throw new RuntimeException("主键不能为空:" + pk); +// } +// } String columns = data.keySet() .stream() diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ExceptionUtils.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ExceptionUtils.java new file mode 100644 index 00000000..b3e32aa9 --- /dev/null +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/ExceptionUtils.java @@ -0,0 +1,134 @@ +package cn.g3soft.utils; + +import cn.g3soft.exception.ExceptionDetailInfo; +import cn.g3soft.utils.uniqueId.idgen.UniqueId; +import com.fasterxml.jackson.databind.ObjectMapper; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +public class ExceptionUtils { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 根据异常和请求信息,生成统一的异常信息对象 ExceptionDetailInfo + * + * @param ex 异常对象 + * @param request HttpServletRequest,用来获取请求路径和方法等 + * @param projectBasePackage 项目基础包名,如 "com.error",用来定位业务代码异常栈帧 + * @return ExceptionDetailInfo 封装的异常信息 + */ + public static ExceptionDetailInfo extractExceptionInfo(Exception ex, HttpServletRequest request, String projectBasePackage) { + StackTraceElement[] stackTrace = ex.getStackTrace(); + + StackTraceElement source = null; + if (stackTrace != null) { + for (StackTraceElement element : stackTrace) { + if (element.getClassName().startsWith(projectBasePackage)) { + source = element; + break; + } + } + if (source == null && stackTrace.length > 0) { + source = stackTrace[0]; + } + } + + Map parameterMap = request.getParameterMap(); + String briefMessage; + + if (ex.getMessage() != null) { + String[] lines = ex.getMessage().split("\n"); + // 遍历每一行,找到第一行“非空、非符号”的有效信息 + String usefulLine = null; + for (String line : lines) { + String trimmed = line.trim(); + if (!trimmed.isEmpty()) { + usefulLine = trimmed; + break; + } + } + // 如果找到有效行,就用它;否则仅用异常类名 + if (usefulLine != null) { + briefMessage = ex.getClass().getSimpleName() + ": " + usefulLine; + } else { + briefMessage = ex.getClass().getSimpleName(); + } + } else { + briefMessage = ex.getClass().getSimpleName(); + } + + String paramJson = null; + // 先尝试从parameterMap获取普通参数 + Map paramMap = new HashMap<>(); + Enumeration paramNames = request.getParameterNames(); + while (paramNames.hasMoreElements()) { + String paramName = paramNames.nextElement(); + paramMap.put(paramName, request.getParameter(paramName)); + } + + // 如果普通参数为空,尝试读取请求体(用于@RequestBody) + if (paramMap.isEmpty()) { + try { + // 获取请求体内容 + ServletInputStream inputStream = request.getInputStream(); + BufferedReader reader = new BufferedReader( + new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + StringBuilder bodyBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + bodyBuilder.append(line); + } + String requestBody = bodyBuilder.toString(); + + if (!requestBody.isEmpty()) { + // 如果是JSON格式,可以尝试解析 + try { + // 尝试将JSON字符串格式化为可读格式 + ObjectMapper mapper = new ObjectMapper(); + Object jsonObject = mapper.readValue(requestBody, Object.class); + paramJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject); + } catch (Exception e) { + // 如果不是JSON,直接使用原始字符串 + 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() + .id(UniqueId.nextId()) + .uri(request.getRequestURI()) + .httpMethod(request.getMethod()) + .className(source != null ? source.getClassName() : null) + .methodName(source != null ? source.getMethodName() : null) + .fileName(source != null ? source.getFileName() : null) + .lineNumber(source != null ? source.getLineNumber() : null) + .summary(briefMessage) + .detailMessage(org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(ex)) + .time(new Date()) + .params(paramJson) + .build(); + } +} diff --git a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/Result.java b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/Result.java index 2b24df53..8b3dbddc 100644 --- a/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/Result.java +++ b/code/container-ocr/ocr-springboot/src/main/java/cn/g3soft/utils/Result.java @@ -60,6 +60,10 @@ public class Result { return new Result<>(code, message, null); } + public static Result fail(Integer code, T data) { + return new Result<>(code, "", data); + } + public static Result fail(String message, T data) { return new Result<>(BUSINESS_ERROR_CODE, message, data); } diff --git a/code/container-ocr/ocr-vue/components.d.ts b/code/container-ocr/ocr-vue/components.d.ts index cfdd1710..048a47eb 100644 --- a/code/container-ocr/ocr-vue/components.d.ts +++ b/code/container-ocr/ocr-vue/components.d.ts @@ -15,9 +15,11 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] + ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDivider: typeof import('element-plus/es')['ElDivider'] + ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] @@ -27,13 +29,23 @@ declare module 'vue' { ElInput: typeof import('element-plus/es')['ElInput'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElOption: typeof import('element-plus/es')['ElOption'] ElPagination: typeof import('element-plus/es')['ElPagination'] ElPopover: typeof import('element-plus/es')['ElPopover'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] + ElSegmented: typeof import('element-plus/es')['ElSegmented'] + ElSelect: typeof import('element-plus/es')['ElSelect'] ElSplitter: typeof import('element-plus/es')['ElSplitter'] ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTable: typeof import('element-plus/es')['ElTable'] + 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'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTree: typeof import('element-plus/es')['ElTree'] G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default'] diff --git a/code/container-ocr/ocr-vue/package.json b/code/container-ocr/ocr-vue/package.json index 44916fb2..097deaf4 100644 --- a/code/container-ocr/ocr-vue/package.json +++ b/code/container-ocr/ocr-vue/package.json @@ -15,6 +15,7 @@ "@wangeditor/editor-for-vue": "^5.1.12", "axios": "^1.13.6", "dayjs": "^1.11.20", + "echarts": "^6.0.0", "element-plus": "^2.13.3", "exceljs": "^4.4.0", "file-saver": "^2.0.5", diff --git a/code/container-ocr/ocr-vue/pnpm-lock.yaml b/code/container-ocr/ocr-vue/pnpm-lock.yaml index 5227f57e..54d1c68f 100644 --- a/code/container-ocr/ocr-vue/pnpm-lock.yaml +++ b/code/container-ocr/ocr-vue/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: dayjs: specifier: ^1.11.20 version: 1.11.20 + echarts: + specifier: ^6.0.0 + version: 6.0.0 element-plus: specifier: ^2.13.3 version: 2.13.5(vue@3.5.30) @@ -1147,6 +1150,9 @@ packages: duplexer2@0.1.4: resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + echarts@6.0.0: + resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + electron-to-chromium@1.5.313: resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} @@ -1925,6 +1931,9 @@ packages: traverse@0.3.9: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2074,6 +2083,9 @@ packages: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} + zrender@6.0.0: + resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} + snapshots: '@antfu/install-pkg@1.1.0': @@ -3236,6 +3248,11 @@ snapshots: dependencies: readable-stream: 2.3.8 + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + electron-to-chromium@1.5.313: {} element-plus@2.13.5(vue@3.5.30): @@ -3948,6 +3965,8 @@ snapshots: traverse@0.3.9: {} + tslib@2.3.0: {} + tslib@2.8.1: {} type@2.7.3: {} @@ -4117,3 +4136,7 @@ snapshots: archiver-utils: 3.0.4 compress-commons: 4.1.2 readable-stream: 3.6.2 + + zrender@6.0.0: + dependencies: + tslib: 2.3.0 diff --git a/code/container-ocr/ocr-vue/src/components/g3-error/index.js b/code/container-ocr/ocr-vue/src/components/g3-error/index.js new file mode 100644 index 00000000..fb471875 --- /dev/null +++ b/code/container-ocr/ocr-vue/src/components/g3-error/index.js @@ -0,0 +1,22 @@ +import ErrorModal from "./index.vue"; + +export const ErrorModalService = { + show(responseData) { + const container = document.createElement("div"); + document.body.appendChild(container); + + const app = createApp({ + render() { + return h(ErrorModal, { + responseData: responseData, + onClose: () => { + app.unmount(); + document.body.removeChild(container); + }, + }); + }, + }); + + app.mount(container); + }, +}; diff --git a/code/container-ocr/ocr-vue/src/components/g3-error/index.vue b/code/container-ocr/ocr-vue/src/components/g3-error/index.vue new file mode 100644 index 00000000..3a391513 --- /dev/null +++ b/code/container-ocr/ocr-vue/src/components/g3-error/index.vue @@ -0,0 +1,386 @@ + + + + + diff --git a/code/container-ocr/ocr-vue/src/layout/components/menu.vue b/code/container-ocr/ocr-vue/src/layout/components/menu.vue index 3455504b..d7e3cb48 100644 --- a/code/container-ocr/ocr-vue/src/layout/components/menu.vue +++ b/code/container-ocr/ocr-vue/src/layout/components/menu.vue @@ -2,18 +2,17 @@ import { loadDataApi } from "@/request/api"; import MenuItem from "./menu-item.vue"; import { useRoute } from "vue-router"; - -const title = ref("G3后台管理系统"); +const title = ref("集装箱管理系统"); const route = useRoute(); const router = useRouter(); onMounted(() => { - loadDataApi("s_keyvalue", `b_key='bodycaption'`, ``, ["b_value"]).then((res) => { - if (res.success) { - title.value = res.data[0].b_value; - } - }); + // loadDataApi("s_keyvalue", `b_key='bodycaption'`, ``, ["b_value"]).then((res) => { + // if (res.success) { + // title.value = res.data[0].b_value; + // } + // }); }); const menuList = ref([ @@ -32,17 +31,12 @@ const menuList = ref([ icon: "lucide:settings", children: [ { - label: "办公管理", - children: [ - { - label: "企业公告", - path: "/system/oa/notice", - }, - { - label: "用户提醒", - path: "/system/oa/reminder", - }, - ], + label: "企业公告", + path: "/system/oa/notice", + }, + { + label: "用户提醒", + path: "/system/oa/reminder", }, { label: "字段配置", @@ -71,7 +65,7 @@ function handleMenuChange(val) {
@@ -129,7 +123,6 @@ function handleMenuChange(val) { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: calc(100% - 90px); } } diff --git a/code/container-ocr/ocr-vue/src/layout/components/topbar.vue b/code/container-ocr/ocr-vue/src/layout/components/topbar.vue index 13d0ac96..75814bc3 100644 --- a/code/container-ocr/ocr-vue/src/layout/components/topbar.vue +++ b/code/container-ocr/ocr-vue/src/layout/components/topbar.vue @@ -4,6 +4,7 @@ import { useTabStore } from "@/store/tab"; import { useRouter } from "vue-router"; import { ElMessage } from "element-plus"; import UpdatePassword from "./update-password.vue"; +import { usePermissionStore } from "@/store/permission"; const props = defineProps({ collapse: { @@ -18,6 +19,7 @@ const router = useRouter(); const tabStore = useTabStore(); const userStore = useUserStore(); +const permissionStore = usePermissionStore(); const showUpdatePassword = ref(false); @@ -28,6 +30,7 @@ function onCollapse() { function logout() { userStore.resetUserInfo(); tabStore.resetTabs(); + permissionStore.reset(); ElMessage.success("已成功退出"); @@ -40,11 +43,7 @@ function logout() {
@@ -55,9 +54,7 @@ function logout() {
diff --git a/code/container-ocr/ocr-vue/src/plugins/icon.js b/code/container-ocr/ocr-vue/src/plugins/icon.js index 784883fa..7940cee0 100644 --- a/code/container-ocr/ocr-vue/src/plugins/icon.js +++ b/code/container-ocr/ocr-vue/src/plugins/icon.js @@ -5,8 +5,10 @@ import { addCollection } from "@iconify/vue"; import Lucide from "@iconify/json/json/lucide.json"; import ElementPlus from "@iconify/json/json/ep.json"; +import Mingcute from "@iconify/json/json/mingcute.json"; export function SetupIcons() { addCollection(Lucide); addCollection(ElementPlus); + addCollection(Mingcute); } diff --git a/code/container-ocr/ocr-vue/src/request/index.js b/code/container-ocr/ocr-vue/src/request/index.js index 698ff019..cbf33b76 100644 --- a/code/container-ocr/ocr-vue/src/request/index.js +++ b/code/container-ocr/ocr-vue/src/request/index.js @@ -2,6 +2,7 @@ import axios from "axios"; import { ElMessage } from "element-plus"; import { useUserStore } from "@/store/user"; import router from "@/router"; +import { ErrorModalService } from "@/components/g3-error"; let hasShown401 = false; @@ -40,6 +41,8 @@ axiosInstance.interceptors.response.use( router.replace("/login"); } } else if (resData.code === 500) { + ErrorModalService.show(resData); + // 系统异常 ElMessage.error(resData.message || "系统异常,请稍后再试"); } else if (resData.code === 1000) { diff --git a/code/container-ocr/ocr-vue/src/router/index.js b/code/container-ocr/ocr-vue/src/router/index.js index e11e118a..786ecdeb 100644 --- a/code/container-ocr/ocr-vue/src/router/index.js +++ b/code/container-ocr/ocr-vue/src/router/index.js @@ -4,6 +4,7 @@ import { useUserStore } from "@/store/user"; import { ElMessage } from "element-plus"; import { useTabStore } from "@/store/tab"; import NProgress from "nprogress"; +import { usePermissionStore } from "@/store/permission"; NProgress.configure({ easing: "ease", speed: 500 }); @@ -12,19 +13,31 @@ const router = createRouter({ routes, }); -router.beforeEach((to) => { +router.beforeEach(async (to) => { NProgress?.start?.(); const userStore = useUserStore(); + const permissionStore = usePermissionStore(); - // if (!userStore.isLogined() && to.path !== "/login") { - // ElMessage.warning("登录已过期,请先登录"); - // return "/login"; - // } + if (!userStore.isLogined() && to.path !== "/login") { + ElMessage.warning("登录已过期,请先登录"); + return "/login"; + } + + if (to.meta.moduleId) { + await permissionStore.init(); + + const hasPermission = permissionStore.hasModulePermission(to.meta.moduleId); + + if (!hasPermission) { + ElMessage.warning("您没有权限访问此页面"); + return "/"; + } + } handleTabPath(to); - return; + return true; }); router.afterEach(() => { diff --git a/code/container-ocr/ocr-vue/src/router/routes.js b/code/container-ocr/ocr-vue/src/router/routes.js index 3224f16d..62043738 100644 --- a/code/container-ocr/ocr-vue/src/router/routes.js +++ b/code/container-ocr/ocr-vue/src/router/routes.js @@ -7,27 +7,20 @@ export const routes = [ children: [ { path: "/", - component: () => import("@/views/index.vue"), + component: () => import("@/views/home/index.vue"), meta: { title: "首页", keepAlive: true, }, }, // ==================== 业务管理 ==================== - { - path: "/business", - component: () => import("@/views/business/index.vue"), - meta: { - title: "海运业务", - keepAlive: true, - }, - }, { path: "/container", component: () => import("@/views/container/index.vue"), meta: { title: "集装箱管理", keepAlive: true, + moduleId: "v_container", }, }, // ==================== 系统管理 ==================== @@ -45,6 +38,7 @@ export const routes = [ meta: { title: "权限管理", keepAlive: true, + moduleId: "s_SetQX", }, }, { @@ -53,6 +47,7 @@ export const routes = [ meta: { title: "模块设置", keepAlive: true, + moduleId: "s_module", }, }, { @@ -61,6 +56,7 @@ export const routes = [ meta: { title: "企业公告", keepAlive: true, + moduleId: "oa_notices", }, }, { @@ -69,6 +65,7 @@ export const routes = [ meta: { title: "用户提醒", keepAlive: true, + moduleId: "s_awokemodule", }, }, // ==================== 个人中心 ==================== diff --git a/code/container-ocr/ocr-vue/src/store/permission.js b/code/container-ocr/ocr-vue/src/store/permission.js new file mode 100644 index 00000000..3fd454d8 --- /dev/null +++ b/code/container-ocr/ocr-vue/src/store/permission.js @@ -0,0 +1,101 @@ +import { loadDataApi } from "@/request/api"; +import { useUserStore } from "./user"; +import { defineStore } from "pinia"; +import { ref, computed } from "vue"; +import { useRoute } from "vue-router"; + +export const usePermissionStore = defineStore("permission", () => { + const route = useRoute(); + const userStore = useUserStore(); + + const modulePermissions = ref([]); + const actionPermissions = ref([]); + const isInitialized = ref(false); + + function reset() { + modulePermissions.value = []; + actionPermissions.value = []; + isInitialized.value = false; + } + + async function init() { + if (isInitialized.value || !userStore.isLogined()) { + // 如果已初始化或者未登录,直接跳过 + return; + } + try { + const moduleResponse = await loadDataApi( + "b_User_Qx", + `father='${userStore.userInfo.userid}'`, + "", + ["ModuleID", "QxType", "ControlType", "ShareType"], + ); + + if (moduleResponse.success) { + modulePermissions.value = moduleResponse.data || []; + } else { + throw new Error(moduleResponse.message || "查询可用账号失败"); + } + + const powerResponse = await loadDataApi( + "b_User_power", + `UserID='${userStore.userInfo.userid}'`, + "", + ["ModuleID", "b_canuse", "b_caption", "b_ivalue"], + ); + + if (powerResponse.success) { + actionPermissions.value = powerResponse.data || []; + } else { + throw new Error(powerResponse.message || "查询可用账号失败"); + } + + isInitialized.value = true; + } catch (error) { + console.error("初始化权限失败", error); + } + } + + function hasModulePermission(moduleid) { + if (userStore.userInfo.userid === "g3soft") { + return true; + } + + const module = modulePermissions.value.find((item) => item.ModuleID === moduleid); + + if (!module) return false; + if (module.QxType === "禁用") return false; + + // 其他情况允许 + return true; + } + + function hasActionPermission(id) { + if (userStore.userInfo.userid === "g3soft") { + return true; + } + + const moduleid = route.meta.moduleId; + if (!moduleid) return false; + + if (!hasModulePermission(moduleid)) return false; + + const actions = actionPermissions.value.filter((item) => item.ModuleID === moduleid); + + const action = actions.find((item) => Number(item.b_ivalue) === Number(id)); + + if (!action) return false; + if (action.b_canuse === "0") return false; + + // 其他情况允许 + return true; + } + + return { + init, + reset, + modulePermissions, + hasModulePermission, + hasActionPermission, + }; +}); diff --git a/code/container-ocr/ocr-vue/src/utils/common.js b/code/container-ocr/ocr-vue/src/utils/common.js new file mode 100644 index 00000000..238696a2 --- /dev/null +++ b/code/container-ocr/ocr-vue/src/utils/common.js @@ -0,0 +1,53 @@ +import { ElMessage } from "element-plus"; +/** + * 复制文本到剪切板 + * @param text 复制内容 + * @returns 是否复制成功 + */ +export function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + // 现代浏览器和安全环境下,直接用 Clipboard API + return navigator.clipboard + .writeText(text) + .then(() => { + ElMessage.success("复制成功"); + }) + .catch((err) => { + ElMessage.error("复制失败:" + err); + }); + } else { + // 退回到传统方式 + fallbackCopyText(text); + } +} + +/** + * 复制文本到剪切板的传统方式 + * @param text 复制内容 + */ +function fallbackCopyText(text) { + const textArea = document.createElement("textarea"); + textArea.value = text; + + // 避免滚动到页面底部 + textArea.style.position = "fixed"; + textArea.style.top = "0"; + textArea.style.left = "0"; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + const successful = document.execCommand("copy"); + if (successful) { + console.log("复制成功(fallback)"); + } else { + console.log("复制失败(fallback)"); + } + } catch (err) { + console.error("复制失败(fallback)", err); + } + + document.body.removeChild(textArea); +} diff --git a/code/container-ocr/ocr-vue/src/utils/file.js b/code/container-ocr/ocr-vue/src/utils/file.js index 690bc091..94a5a764 100644 --- a/code/container-ocr/ocr-vue/src/utils/file.js +++ b/code/container-ocr/ocr-vue/src/utils/file.js @@ -311,14 +311,19 @@ export function downloadZip(files, fileName = "文件合集", callback) { 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); - }); + 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); + }); }); - // ✅ 关键:return + // 关键:使用 Promise.all 确保所有任务完成 return Promise.all(tasks) .then(() => zip.generateAsync({ type: "blob" })) .then((res) => { @@ -327,7 +332,7 @@ export function downloadZip(files, fileName = "文件合集", callback) { }) .catch((err) => { callback && callback(); - throw err; // ✅ 继续往外抛 + throw err; }); } diff --git a/code/container-ocr/ocr-vue/src/views/auth/login.vue b/code/container-ocr/ocr-vue/src/views/auth/login.vue index b805f252..5a505ceb 100644 --- a/code/container-ocr/ocr-vue/src/views/auth/login.vue +++ b/code/container-ocr/ocr-vue/src/views/auth/login.vue @@ -6,8 +6,11 @@ import { useRouter } from "vue-router"; import Vcode from "vue3-puzzle-vcode"; import { reset401Flag } from "@/request"; +import { usePermissionStore } from "@/store/permission"; + const router = useRouter(); const userStore = useUserStore(); +const permissionStore = usePermissionStore(); const form = ref({ orgid: userStore.userInfo.orgid, @@ -21,7 +24,7 @@ const loginLoading = ref(false); const linkList = ref([ { title: "新手上路", - icon: "solar:key-bold", + icon: "mingcute:key-2-fill", href: "http://bbs.g3soft.cn/forum.php?mod=viewthread&tid=115&extra=page%3D1", }, { @@ -31,12 +34,12 @@ const linkList = ref([ }, { title: "视频教程", - icon: "material-symbols:auto-videocam-rounded", + icon: "mingcute:video-fill", href: "http://bbs.g3soft.cn/forum.php?mod=viewthread&tid=86&extra=page%3D1", }, { title: "常见问题", - icon: "ic:baseline-help", + icon: "mingcute:question-fill", href: "http://bbs.g3soft.cn/forum.php?mod=forumdisplay&fid=37", }, ]); @@ -82,29 +85,30 @@ function onVcodeClose() { showVcode.value = false; } -function onVcodeSuccess() { +async function onVcodeSuccess() { onVcodeClose(); loginLoading.value = true; try { - loginApi(form.value.orgid, form.value.userid, form.value.password).then( - (res) => { - if (res.success) { - ElMessage.success("登录成功"); + loginApi(form.value.orgid, form.value.userid, form.value.password).then(async (res) => { + if (res.success) { + userStore.setUserInfo( + form.value.orgid, + form.value.userid, + res.data.username, + res.data.token, + ); - userStore.setUserInfo( - form.value.orgid, - form.value.userid, - res.data.username, - res.data.token, - ); - router.push("/"); + await permissionStore.init(); - reset401Flag(); - } - }, - ); + ElMessage.success("登录成功"); + + router.push("/"); + + reset401Flag(); + } + }); } finally { loginLoading.value = false; } @@ -157,11 +161,7 @@ function onVcodeSuccess() { size="large" long type="primary" - style=" - background-color: #33dcd0; - border-color: #00d3c4; - font-size: 18px; - " + style="background-color: #33dcd0; border-color: #00d3c4; font-size: 18px" @click="onVcodeShow" :loading="loginLoading" > diff --git a/code/container-ocr/ocr-vue/src/views/business/detail.vue b/code/container-ocr/ocr-vue/src/views/business/detail.vue deleted file mode 100644 index 1edc2f6a..00000000 --- a/code/container-ocr/ocr-vue/src/views/business/detail.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/code/container-ocr/ocr-vue/src/views/business/index.vue b/code/container-ocr/ocr-vue/src/views/business/index.vue deleted file mode 100644 index d3a3fcdb..00000000 --- a/code/container-ocr/ocr-vue/src/views/business/index.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/code/container-ocr/ocr-vue/src/views/container/index.vue b/code/container-ocr/ocr-vue/src/views/container/index.vue index 63d25d98..a5007480 100644 --- a/code/container-ocr/ocr-vue/src/views/container/index.vue +++ b/code/container-ocr/ocr-vue/src/views/container/index.vue @@ -13,9 +13,12 @@ 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"; const tableRef = ref(); +const permissionStore = usePermissionStore(); + const columns = ref([]); const tableData = ref([]); @@ -53,9 +56,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({}); @@ -188,12 +189,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; @@ -269,11 +267,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("获取附件失败"); @@ -281,11 +275,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, })); @@ -348,11 +338,7 @@ async function handleDownloadBatch() { // 👉 先统计总文件数(用于进度) 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; } @@ -360,17 +346,11 @@ async function handleDownloadBatch() { // 👉 正式处理 for (const item of records) { - const folderName = `${item.b_container_no}_${formatToDate( - item.b_inputdatetime, - )}`; + 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("获取附件失败"); @@ -387,8 +367,7 @@ async function handleDownloadBatch() { const blob = await res.blob(); 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) { @@ -478,13 +457,21 @@ function buildFileName(containerNo, index, fileName) {
- + 批量下载 - + @@ -500,7 +487,7 @@ function buildFileName(containerNo, index, fileName) { - + @@ -547,14 +534,7 @@ function buildFileName(containerNo, index, fileName) { }" height="100%" > - +