u
This commit is contained in:
1 parent
11bcef03e1
commit
a4e59dd5d1
39 files changed
+2333
-459
No files matched your search
@@ -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(
|
||||
|
||||
+45
@@ -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;
|
||||
}
|
||||
+33
@@ -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<ExceptionDetailInfo> 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);
|
||||
}
|
||||
}
|
||||
+60
@@ -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<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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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<String, String[]> 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<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 {
|
||||
// 获取请求体内容
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,10 @@ public class Result<T> {
|
||||
return new Result<>(code, message, null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> fail(Integer code, T data) {
|
||||
return new Result<>(code, "", data);
|
||||
}
|
||||
|
||||
public static <T> Result<T> fail(String message, T data) {
|
||||
return new Result<>(BUSINESS_ERROR_CODE, message, data);
|
||||
}
|
||||
|
||||
+12
@@ -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']
|
||||
|
||||
@@ -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",
|
||||
|
||||
+23
@@ -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
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,386 @@
|
||||
<template>
|
||||
<div class="error-modal-wrapper" @click.self="handleClose">
|
||||
<div v-if="visible" class="error-modal" @click.stop>
|
||||
<div class="error-modal-header">
|
||||
<span class="error-modal-title"
|
||||
>⚠️ 系统异常提示 - {{ props.responseData.message }} -
|
||||
{{ props.responseData.code || 500 }}</span
|
||||
>
|
||||
<div class="error-modal-close-btn" @click="handleClose" role="button" tabindex="0">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-modal-body">
|
||||
<div class="log-item" v-for="item in logList" :key="item.label">
|
||||
<div class="log-label">{{ item.label }}</div>
|
||||
<div class="log-value">
|
||||
<pre v-if="item.label === '堆栈跟踪' || item.label === '请求参数'" class="code-block">{{
|
||||
item.value
|
||||
}}</pre>
|
||||
<div v-else>{{ item.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="error-modal-footer">
|
||||
<div class="error-modal-tip">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="16" x2="12" y2="12" />
|
||||
<line x1="12" y1="8" x2="12.01" y2="8" />
|
||||
</svg>
|
||||
<span>请将错误信息 导出 或 复制 发送给管理员!</span>
|
||||
</div>
|
||||
<div class="error-modal-actions">
|
||||
<div class="btn txt-btn" role="button" tabindex="0" @click="exportAsTxt">
|
||||
<svg
|
||||
class="btn-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="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 2v6h6m-9 4H9m3 3H9m6-9h-1" />
|
||||
<path d="M12 18v-6m2 3h-4" />
|
||||
</svg>
|
||||
<span>导出TXT</span>
|
||||
</div>
|
||||
|
||||
<div class="btn data-btn" role="button" tabindex="0" @click="copyInfo">
|
||||
<svg
|
||||
class="btn-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<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" />
|
||||
</svg>
|
||||
<span>复制信息</span>
|
||||
</div>
|
||||
|
||||
<div class="btn cancel-btn" role="button" tabindex="0" @click="handleClose">
|
||||
<svg
|
||||
class="btn-icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M15 9l-6 6m0-6l6 6" />
|
||||
</svg>
|
||||
<span>取消</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="js">
|
||||
import { ref, computed, onMounted, nextTick } from "vue";
|
||||
import { copyText } from "@/utils/common";
|
||||
|
||||
const props = defineProps({
|
||||
responseData: {
|
||||
type: [Object],
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
visible.value = true;
|
||||
});
|
||||
|
||||
const logList = computed(() => [
|
||||
{ label: "ID 唯一标识", value: props.responseData.data.id ?? "" },
|
||||
{ label: "发生时间", value: props.responseData.data.time?.toString() ?? "" },
|
||||
{ label: "请求路径", value: props.responseData.data.uri ?? "" },
|
||||
{ label: "HTTP 方法", value: props.responseData.data.httpMethod ?? "" },
|
||||
{
|
||||
label: "类 - 方法",
|
||||
value: `${props.responseData.data.className ?? ""} - ${props.responseData.data.methodName ?? ""}`,
|
||||
},
|
||||
{
|
||||
label: "代码位置",
|
||||
value: `${props.responseData.data.fileName ?? ""} - ${props.responseData.data.lineNumber ?? ""}`,
|
||||
},
|
||||
{ label: "错误主要信息", value: props.responseData.data.summary ?? "" },
|
||||
{ label: "堆栈跟踪", value: props.responseData.data.detailMessage ?? "" },
|
||||
{
|
||||
label: "请求参数",
|
||||
value: props.responseData.data.params
|
||||
? JSON.stringify(JSON.parse(props.responseData.data.params), null, 2)
|
||||
: "",
|
||||
},
|
||||
]);
|
||||
|
||||
function handleClose() {
|
||||
if (visible.value) {
|
||||
visible.value = false;
|
||||
props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function exportAsTxt() {
|
||||
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 url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "错误信息日志.txt";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function copyInfo() {
|
||||
copyText(JSON.stringify(props.responseData));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@keyframes modalFadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
.error-modal-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
|
||||
.error-modal {
|
||||
animation: modalFadeIn 0.3s forwards;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 4px;
|
||||
width: 1000px;
|
||||
overflow: hidden;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.error-modal-header {
|
||||
height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid rgb(229, 230, 235);
|
||||
line-height: 20px;
|
||||
background-color: #e04848;
|
||||
.error-modal-title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
.error-modal-close-btn {
|
||||
padding: 4px;
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
|
||||
color: #fff;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.error-modal-close-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
.error-modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
padding: 16px 0px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box; /* 包含内边距 */
|
||||
|
||||
.log-item {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 16px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.log-label {
|
||||
color: #2b2b2b;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.log-value {
|
||||
word-break: break-all;
|
||||
.code-block {
|
||||
background: #f1f1f1;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
max-height: 120px;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap; /* 保留换行和缩进空格 */
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.error-modal-footer {
|
||||
border-top: 1px solid rgb(229, 230, 235);
|
||||
padding: 16px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.error-modal-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #ff7d00;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.error-modal-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-left: auto;
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid #1976d2;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 导出按钮样式 */
|
||||
&.txt-btn {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
&:active {
|
||||
background: #0d47a1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 复制按钮样式 */
|
||||
&.data-btn {
|
||||
color: #1976d2;
|
||||
border-color: #90caf9;
|
||||
|
||||
&:active {
|
||||
background: #90caf9;
|
||||
}
|
||||
}
|
||||
|
||||
/* 取消按钮样式 */
|
||||
&.cancel-btn {
|
||||
background: #f5f5f5;
|
||||
color: #757575;
|
||||
|
||||
&:hover {
|
||||
background: #eeeeee;
|
||||
}
|
||||
&:active {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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) {
|
||||
<div class="g3-menu">
|
||||
<div class="g3-logo">
|
||||
<div class="logo-inner">
|
||||
<img src="@/assets/logo.png" alt="logo" />
|
||||
<!-- <img src="@/assets/logo.png" alt="logo" /> -->
|
||||
<div class="title">{{ title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,7 +123,6 @@ function handleMenuChange(val) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100% - 90px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
<div class="g3-topbar-left">
|
||||
<div class="g3-action-icon" @click="onCollapse()">
|
||||
<G3Icon
|
||||
:icon="
|
||||
props.collapse
|
||||
? 'lucide:list-indent-increase'
|
||||
: 'lucide:list-indent-decrease'
|
||||
"
|
||||
:icon="props.collapse ? 'lucide:list-indent-increase' : 'lucide:list-indent-decrease'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,9 +54,7 @@ function logout() {
|
||||
<G3Icon icon="lucide:circle-user-round" size="18" />
|
||||
<div class="user-info">
|
||||
<span class="user-name">
|
||||
{{
|
||||
userStore.userInfo.username || userStore.userInfo.userid || "空"
|
||||
}}
|
||||
{{ userStore.userInfo.username || userStore.userInfo.userid || "空" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
// ==================== 个人中心 ====================
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="js" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class=""></div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,9 +0,0 @@
|
||||
<script lang="js" setup></script>
|
||||
|
||||
<template>
|
||||
<div class="">
|
||||
<div></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped></style>
|
||||
@@ -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) {
|
||||
<div class="g3-content">
|
||||
<div class="content-top">
|
||||
<div>
|
||||
<el-button type="primary" @click="handleDownloadBatch">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleDownloadBatch"
|
||||
:disabled="!permissionStore.hasActionPermission(1)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:cloud-download" />
|
||||
</el-icon>
|
||||
批量下载
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDeleteRows">
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="handleDeleteRows"
|
||||
:disabled="!permissionStore.hasActionPermission(2)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:trash-2" />
|
||||
</el-icon>
|
||||
@@ -500,7 +487,7 @@ function buildFileName(containerNo, index, fileName) {
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新">
|
||||
<el-button circle>
|
||||
<el-button circle @click="loadTableDatas">
|
||||
<el-icon>
|
||||
<G3Icon icon="lucide:refresh-ccw" />
|
||||
</el-icon>
|
||||
@@ -547,14 +534,7 @@ function buildFileName(containerNo, index, fileName) {
|
||||
}"
|
||||
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
|
||||
v-for="column in columns"
|
||||
@@ -575,30 +555,40 @@ function buildFileName(containerNo, index, fileName) {
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{
|
||||
formatValueByFormat(
|
||||
row[column.col_FieldName],
|
||||
column.col_DisplayFormat,
|
||||
)
|
||||
}}
|
||||
{{ formatValueByFormat(row[column.col_FieldName], column.col_DisplayFormat) }}
|
||||
</template>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column width="220" align="center" fixed="right" title="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleDownloadOne(row)">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDownloadOne(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(1)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:cloud-download" />
|
||||
</el-icon>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="handleFileShow(row)">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleFileShow(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(3)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:file-image" />
|
||||
</el-icon>
|
||||
图片
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDeleteRow(row)">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDeleteRow(row)"
|
||||
:disabled="!permissionStore.hasActionPermission(2)"
|
||||
>
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:trash-2" />
|
||||
</el-icon>
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="g3-bar">
|
||||
<el-card style="width: 100%; border: 0px" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>{{ props.item.b_name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div style="background-color: #fff; height: 336px">
|
||||
<G3Echarts v-if="option" :option="option" style="height: 330px" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import G3Echarts from "../g3-echarts.vue";
|
||||
import { useUserStore } from "@/store/user";
|
||||
import { loadDataBySqlApi } from "@/request/api";
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const option = ref(null);
|
||||
const update_datetime = ref("");
|
||||
|
||||
const props = defineProps({
|
||||
item: Object,
|
||||
});
|
||||
|
||||
const loadChartData = async (item) => {
|
||||
if (!item) return;
|
||||
|
||||
update_datetime.value = item.b_update_datetime;
|
||||
|
||||
let seriesData = [];
|
||||
const b_graph_name = item.b_graph_name;
|
||||
|
||||
const b_graph_x = item.b_graph_x.split(",").map((m) => {
|
||||
const date = new Date();
|
||||
date.setMonth(date.getMonth() - Number(m));
|
||||
return `${date.getFullYear()}-${date.getMonth() + 1}月`;
|
||||
});
|
||||
|
||||
// 数据来源
|
||||
if (item.b_result) {
|
||||
seriesData = item.b_result.split(",").map(Number);
|
||||
} else {
|
||||
const sqlQuery = item.b_polyline.replace(/:b_inputuser_id/g, `'${userStore.userInfo.userid}'`);
|
||||
const res = await loadDataBySqlApi(sqlQuery);
|
||||
if (res.success && res.data.length > 0) {
|
||||
seriesData = Object.values(res.data[0]);
|
||||
}
|
||||
}
|
||||
|
||||
option.value = {
|
||||
color: ["#87cefa"],
|
||||
tooltip: { trigger: "axis" },
|
||||
legend: {
|
||||
top: 20,
|
||||
data: [b_graph_name],
|
||||
},
|
||||
grid: { y2: 24 },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: b_graph_x,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: b_graph_name,
|
||||
type: "bar",
|
||||
data: seriesData,
|
||||
barWidth: "25%",
|
||||
label: {
|
||||
show: true,
|
||||
position: "top",
|
||||
formatter: "{c}",
|
||||
},
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#2378f7" },
|
||||
{ offset: 0.5, color: "#2378f7" },
|
||||
{ offset: 1, color: "#7fbcf6" },
|
||||
]),
|
||||
},
|
||||
markLine: {
|
||||
data: [{ type: "average", name: "平均值" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.item?.b_id,
|
||||
() => {
|
||||
loadChartData(props.item);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.g3-bar {
|
||||
width: 50%;
|
||||
height: 394px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
:deep(.el-card__header) {
|
||||
padding: 13px 20px 13px 20px;
|
||||
}
|
||||
.g3-refresh {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="g3-bar">
|
||||
<el-card style="width: 100%; border: 0px" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>{{ props.item.b_name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div style="background-color: #fff; height: 336px">
|
||||
<G3Echarts v-if="option" :option="option" style="height: 330px" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js" setup>
|
||||
import G3Echarts from "../g3-echarts.vue";
|
||||
import { useUserStore } from "@/store/user";
|
||||
import { loadDataBySqlApi } from "@/request/api";
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const option = ref(null);
|
||||
const update_datetime = ref("");
|
||||
|
||||
const props = defineProps({
|
||||
item: Object,
|
||||
});
|
||||
|
||||
const loadChartData = async (item) => {
|
||||
if (!item) return;
|
||||
|
||||
update_datetime.value = item.b_update_datetime;
|
||||
|
||||
let seriesData = [];
|
||||
let seriesData2 = [];
|
||||
const b_graph_name = item.b_graph_name.split(",");
|
||||
|
||||
const b_graph_x = item.b_graph_x.split(",");
|
||||
const b_graph_y = item.b_graph_y_name;
|
||||
|
||||
// 数据来源
|
||||
if (item.b_result) {
|
||||
seriesData = item.b_result.split(",").map(Number);
|
||||
seriesData2 = item.b_result2.split(",").map(Number);
|
||||
} else {
|
||||
const sqlQuery = item.b_polyline.replace(/:b_inputuser_id/g, `'${userStore.userInfo.userid}'`);
|
||||
const res = await loadDataBySqlApi(sqlQuery);
|
||||
if (res.success && res.data.length > 0) {
|
||||
seriesData = Object.values(res.data[0]);
|
||||
}
|
||||
const sqlQuery2 = item.b_polyline.replace(/:b_inputuser_id/g, `'${userStore.userInfo.userid}'`);
|
||||
const res2 = await getDataBySql(sqlQuery2);
|
||||
if (res2.success && res2.data.length > 0) {
|
||||
seriesData2 = Object.values(res2.data[0]);
|
||||
}
|
||||
}
|
||||
|
||||
option.value = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "cross",
|
||||
label: {
|
||||
backgroundColor: "#6a7985",
|
||||
},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
top: 20,
|
||||
data: b_graph_name, // Legend labels for both datasets
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: b_graph_x, // Replace with your x-axis categories
|
||||
},
|
||||
yAxis: {
|
||||
name: b_graph_y,
|
||||
type: "value",
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: b_graph_name[0], // Name of the first dataset
|
||||
type: "line",
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
}, // Line chart type
|
||||
data: seriesData, // Replace with your first dataset values
|
||||
smooth: true, // Smooth curve
|
||||
areaStyle: {
|
||||
// Area chart style (filled area)
|
||||
color: "rgba(119,201,255)", // Set the fill color with transparency
|
||||
},
|
||||
lineStyle: {
|
||||
color: "#37a2da", // Line color for the first dataset
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#37a2da",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: b_graph_name[1], // Name of the second dataset
|
||||
type: "line",
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
}, // Line chart type
|
||||
data: seriesData2, // Replace with your second dataset values
|
||||
smooth: true, // Smooth curve
|
||||
areaStyle: {
|
||||
// Area chart style (filled area)
|
||||
color: "rgba(120,216,216)", // Set the fill color with transparency
|
||||
},
|
||||
lineStyle: {
|
||||
color: "#ffd85c", // Line color for the second dataset
|
||||
},
|
||||
itemStyle: {
|
||||
color: "#ffd85c",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.item?.b_id,
|
||||
() => {
|
||||
loadChartData(props.item);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.g3-bar {
|
||||
width: 50%;
|
||||
height: 394px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 13px 20px 13px 20px;
|
||||
}
|
||||
|
||||
.g3-refresh {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="g3-bar">
|
||||
<el-card style="width: 100%; border: 0px" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>{{ props.item.b_name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div style="background-color: #fff; height: 336px">
|
||||
<G3Echarts v-if="option" :option="option" style="height: 330px" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js" setup>
|
||||
import G3Echarts from "../g3-echarts.vue";
|
||||
import { useUserStore } from "@/store/user";
|
||||
import { loadDataBySqlApi } from "@/request/api";
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const option = ref(null);
|
||||
const update_datetime = ref("");
|
||||
|
||||
const props = defineProps({
|
||||
item: Object,
|
||||
});
|
||||
|
||||
const loadChartData = async (item) => {
|
||||
if (!item) return;
|
||||
|
||||
update_datetime.value = item.b_update_datetime;
|
||||
|
||||
let seriesData = [];
|
||||
let data_splice = [];
|
||||
const b_graph_name = item.b_graph_name.split(",");
|
||||
|
||||
const b_graph_x = item.b_graph_x.split(",");
|
||||
|
||||
// 数据来源
|
||||
if (item.b_result) {
|
||||
data_splice = item.b_result.split(",").map(Number);
|
||||
seriesData = data_splice.map((value, index) => ({
|
||||
value,
|
||||
name: b_graph_x[index],
|
||||
}));
|
||||
} else {
|
||||
const sqlQuery = item.b_polyline.replace(/:b_inputuser_id/g, `'${userStore.userInfo.userid}'`);
|
||||
const res = await loadDataBySqlApi(sqlQuery);
|
||||
if (res.success && res.data.length > 0) {
|
||||
data_splice = Object.values(res.data[0]);
|
||||
}
|
||||
seriesData = data_splice.map((value, index) => ({
|
||||
value,
|
||||
name: b_graph_x[index],
|
||||
}));
|
||||
}
|
||||
|
||||
option.value = {
|
||||
title: {
|
||||
text: b_graph_name[0],
|
||||
subtext: b_graph_name[1],
|
||||
left: "center",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
},
|
||||
legend: {
|
||||
orient: "vertical",
|
||||
left: "left",
|
||||
top: 20,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: b_graph_name[0],
|
||||
type: "pie",
|
||||
radius: "50%",
|
||||
data: seriesData,
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: function (params) {
|
||||
const colorList = ["#37a2da", "#67e0e3", "#ffdb5c", "#ff9f7f", "#e062ae"];
|
||||
return colorList[params.dataIndex];
|
||||
},
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.item?.b_id,
|
||||
() => {
|
||||
loadChartData(props.item);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.g3-bar {
|
||||
width: 50%;
|
||||
height: 394px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 13px 20px 13px 20px;
|
||||
}
|
||||
|
||||
.g3-refresh {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="g3-bar">
|
||||
<el-card style="width: 100%; border: 0px" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>{{ props.item.b_name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div style="background-color: #fff; height: 336px">
|
||||
<G3Echarts v-if="option" :option="option" style="height: 330px" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import G3Echarts from "../g3-echarts.vue";
|
||||
import { useUserStore } from "@/store/user";
|
||||
import { loadDataBySqlApi } from "@/request/api";
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const option = ref(null);
|
||||
const update_datetime = ref("");
|
||||
|
||||
const props = defineProps({
|
||||
item: Object,
|
||||
});
|
||||
|
||||
const loadChartData = async (item) => {
|
||||
if (!item) return;
|
||||
|
||||
update_datetime.value = item.b_update_datetime;
|
||||
|
||||
let seriesData = [];
|
||||
const b_graph_name = item.b_graph_name;
|
||||
|
||||
const b_graph_x = item.b_graph_x.split(",").map((m) => {
|
||||
const date = new Date();
|
||||
date.setMonth(date.getMonth() - Number(m));
|
||||
return `${date.getFullYear()}-${date.getMonth() + 1}月`;
|
||||
});
|
||||
|
||||
// 数据来源
|
||||
if (item.b_result) {
|
||||
seriesData = item.b_result.split(",").map(Number);
|
||||
} else {
|
||||
const sqlQuery = item.b_polyline.replace(/:b_inputuser_id/g, `'${userStore.userInfo.userid}'`);
|
||||
const res = await loadDataBySqlApi(sqlQuery);
|
||||
if (res.success && res.data.length > 0) {
|
||||
seriesData = Object.values(res.records[0]);
|
||||
}
|
||||
}
|
||||
|
||||
option.value = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow",
|
||||
},
|
||||
},
|
||||
legend: {},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
top: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "value",
|
||||
boundaryGap: [0, 0.01],
|
||||
},
|
||||
yAxis: {
|
||||
type: "category",
|
||||
data: b_graph_x, // Your categories
|
||||
axisLine: {
|
||||
show: true, // Show or hide the axis line
|
||||
lineStyle: {
|
||||
color: "#dcdcdc", // Color of the axis line
|
||||
width: 1, // Width of the axis line
|
||||
type: "solid", // Style of the axis line (solid, dashed, dotted)
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: false, // Hide the ticks if not needed
|
||||
},
|
||||
axisLabel: {
|
||||
show: true, // Show axis labels
|
||||
textStyle: {
|
||||
color: "#333", // Font color
|
||||
fontSize: 12, // Font size
|
||||
fontFamily: "Arial", // Font family
|
||||
fontWeight: "normal", // Font weight
|
||||
},
|
||||
interval: 0,
|
||||
margin: 15,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
//name: b_graph_name,
|
||||
type: "bar",
|
||||
data: seriesData,
|
||||
label: {
|
||||
show: true, // 显示标签
|
||||
position: "right", // 标签位置
|
||||
// 可以通过 formatter 自定义显示格式
|
||||
formatter: "{c}", // {c} 表示数据值
|
||||
},
|
||||
barCategoryGap: "60%",
|
||||
itemStyle: {
|
||||
color: function (params) {
|
||||
var colors = [
|
||||
"#1990ff",
|
||||
"#1990ff",
|
||||
"#1990ff",
|
||||
"#1990ff",
|
||||
"#1990ff",
|
||||
"#1990ff",
|
||||
"#1990ff",
|
||||
"#48bfe3",
|
||||
"#ff904d",
|
||||
"#fc619d",
|
||||
]; // Define your custom colors here
|
||||
return colors[params.dataIndex]; // Use dataIndex to assign a color to each bar
|
||||
},
|
||||
borderRadius: [25, 25, 25, 25],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.item?.b_id,
|
||||
() => {
|
||||
loadChartData(props.item);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.g3-bar {
|
||||
width: 50%;
|
||||
height: 394px;
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
:deep(.el-card__header) {
|
||||
padding: 13px 20px 13px 20px;
|
||||
}
|
||||
.g3-refresh {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script setup lang="js">
|
||||
import { useUserStore } from "@/store/user";
|
||||
import { loadDataBySqlApi } from "@/request/api";
|
||||
import { getDateTime } from "@/utils/time";
|
||||
|
||||
const props = defineProps({
|
||||
item: Object,
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
const value = ref("0");
|
||||
const flag_date = ref(false);
|
||||
const update_datetime = ref("");
|
||||
|
||||
async function load() {
|
||||
if (props.item.b_result) {
|
||||
value.value = props.item.b_result;
|
||||
update_datetime.value = props.item.b_update_datetime;
|
||||
flag_date.value = true;
|
||||
} else {
|
||||
const sql = props.item.b_sql.replace(/:b_inputuser_id/g, `'${userStore.userInfo.userid}'`);
|
||||
const res = await loadDataBySqlApi(sql);
|
||||
|
||||
if (res.success && res.data.length > 0) {
|
||||
value.value = Object.values(res.data[0]).join(",");
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
update_datetime.value = getDateTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-value-card">
|
||||
<div class="header">
|
||||
<span class="label">{{ props.item.b_name }}</span>
|
||||
<G3Icon :icon="props.item.b_img" class="icon" />
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<span class="num"> {{ value }}</span>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<span> 刷新时间:{{ update_datetime }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-value-card {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #f2f6fc;
|
||||
background-color: #fff;
|
||||
color: #303133;
|
||||
user-select: none;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
min-height: 50px;
|
||||
|
||||
.label {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 30px;
|
||||
background-color: #f5f7fa;
|
||||
padding: 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 20px;
|
||||
min-height: 50px;
|
||||
|
||||
.num {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
margin: 0 5px;
|
||||
padding: 0 15px;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div ref="chartRef" class="g3-echarts"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 在 G3Echarts 组件中
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const chartRef = ref(null);
|
||||
let chartInstance = null;
|
||||
let resizeObserver = null;
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value || chartInstance) return;
|
||||
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
chartInstance.setOption(props.option);
|
||||
|
||||
// 使用 ResizeObserver 监听容器尺寸变化
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
chartInstance?.resize();
|
||||
});
|
||||
resizeObserver.observe(chartRef.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 直接使用 nextTick 通常就够了
|
||||
nextTick(() => {
|
||||
initChart();
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
chartInstance?.dispose();
|
||||
chartInstance = null;
|
||||
});
|
||||
|
||||
// 监听 option 更新
|
||||
watch(
|
||||
() => props.option,
|
||||
(val) => {
|
||||
if (chartInstance && val) {
|
||||
chartInstance.setOption(val, true);
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-echarts {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="js" setup>
|
||||
import Notice from "./notice.vue";
|
||||
import Service from "./service.vue";
|
||||
import UserReminder from "./user-reminder.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-container">
|
||||
<div class="g3-main">
|
||||
<div class="notice">
|
||||
<Notice />
|
||||
</div>
|
||||
<div class="service">
|
||||
<Service />
|
||||
</div>
|
||||
</div>
|
||||
<div class="g3-other">
|
||||
<UserReminder />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.g3-main {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 340px;
|
||||
|
||||
.notice {
|
||||
min-width: 0;
|
||||
width: calc(25% - 16px);
|
||||
}
|
||||
|
||||
.service {
|
||||
margin-left: 16px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-other {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script lang="js" setup>
|
||||
import { loadDataApi } from "@/request/api";
|
||||
import { formatToDateTime } from "@/utils/time";
|
||||
const dataList = ref([]);
|
||||
|
||||
const showDialog = ref(false);
|
||||
const dialogData = ref({});
|
||||
|
||||
const showFile = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
const loadRes = await loadDataApi("oa_notices", ` b_visible='1'`, "b_inputdate desc");
|
||||
|
||||
if (loadRes.success) {
|
||||
dataList.value = loadRes.data;
|
||||
}
|
||||
});
|
||||
|
||||
function handleShowDialog(data) {
|
||||
showDialog.value = true;
|
||||
|
||||
dialogData.value = data;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never" class="g3-card">
|
||||
<template #header>
|
||||
<div class="g3-card-header">
|
||||
<span class="title">企业公告</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-for="item in dataList" class="notice-item" @click="handleShowDialog(item)">
|
||||
{{ item.b_subject }}
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="dialogData.b_subject" :width="800">
|
||||
<div class="section">
|
||||
<div class="section-title">发布人</div>
|
||||
<div class="section-value">
|
||||
{{ dialogData.b_updateuser_id || dialogData.b_inputuser_id }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section-title">发布时间</div>
|
||||
<div class="section-value">
|
||||
{{ formatToDateTime(dialogData.b_updatedatetime || dialogData.b_inputdatetime) }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="section-title">公告内容</div>
|
||||
|
||||
<WangEditor
|
||||
v-model="dialogData.b_info"
|
||||
:father="dialogData.b_id"
|
||||
module-id="oa_notices"
|
||||
display-mode="view"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="showFile = true">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:paperclip" />
|
||||
</el-icon>
|
||||
查看附件
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<G3File v-model="showFile" :father="dialogData.b_id" module-id="oa_notices" readonly />
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-card {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 10px 12px !important;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
width: 100%;
|
||||
padding: 10px 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-card-header {
|
||||
.title {
|
||||
color: #676a6c;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
padding: 4px 0px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgb(239, 239, 245);
|
||||
width: 100%;
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
|
||||
&:hover {
|
||||
color: #337ab7;
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
.section-title {
|
||||
color: #606266;
|
||||
width: 80px;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.section-value {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script lang="js" setup>
|
||||
import { loadDataApi, loadDataBySqlApi } from "@/request/api";
|
||||
import G3Echarts from "./g3-echarts.vue";
|
||||
|
||||
const dataList = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
const loadRes = await loadDataApi("v_cotaniner_echarts");
|
||||
|
||||
if (loadRes.success) {
|
||||
dataList.value = loadRes.data;
|
||||
}
|
||||
});
|
||||
|
||||
const activeSegment = ref(0);
|
||||
const segOptions = [
|
||||
{ label: "按周", value: 0 },
|
||||
{ label: "按月", value: 1 },
|
||||
{ label: "按年", value: 2 },
|
||||
];
|
||||
|
||||
// Computed property to dynamically change the title
|
||||
const chartTitle = computed(() => {
|
||||
switch (activeSegment.value) {
|
||||
case 0:
|
||||
return "最近一周上传数据";
|
||||
case 1:
|
||||
return "最近一个月上传数据";
|
||||
case 2:
|
||||
return "最近一年上传数据";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
const chartOption = computed(() => {
|
||||
const dates = dataList.value.map((item) => item.b_date);
|
||||
const uploadCounts = dataList.value.map((item) => item.b_count);
|
||||
|
||||
return {
|
||||
title: {
|
||||
text: chartTitle.value,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
legend: {
|
||||
data: ["上传数量"],
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: dates,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisLabel: {
|
||||
formatter: "{value}", // 直接输出整数
|
||||
},
|
||||
axisTick: {
|
||||
alignWithLabel: true, // 确保刻度对齐
|
||||
},
|
||||
minInterval: 1, // 确保间隔为 1
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "上传数量",
|
||||
data: uploadCounts,
|
||||
type: "line", // 修改为折线图
|
||||
smooth: true, // 平滑曲线
|
||||
symbol: "circle", // 数据点使用圆形
|
||||
symbolSize: 6, // 数据点的大小
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
function handleSegmentChange(val) {}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="g3-card">
|
||||
<!-- <el-segmented
|
||||
v-model="activeSegment"
|
||||
:options="segOptions"
|
||||
class="segmented-control"
|
||||
@change="handleSegmentChange"
|
||||
/> -->
|
||||
|
||||
<G3Echarts :option="chartOption" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-card {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.segmented-control {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="g3-user-reminder">
|
||||
<template v-for="item in data" :key="item.b_id">
|
||||
<ChartPie v-if="item.b_graph === '饼图'" :item="item" />
|
||||
<ChartLine v-else-if="item.b_graph === '折线图'" :item="item" />
|
||||
<ChartReleaseBar
|
||||
v-else-if="item.b_graph === '纵柱状图'"
|
||||
:item="item"
|
||||
style="margin-bottom: 25px"
|
||||
/>
|
||||
<ChartBar v-else-if="item.b_graph === '柱状图'" :item="item" />
|
||||
<ValueCard v-else :item="item" class="value-card" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="js">
|
||||
import ChartBar from "./components/chart-bar.vue";
|
||||
import ChartReleaseBar from "./components/chart-release-bar.vue";
|
||||
import ChartLine from "./components/chart-line.vue";
|
||||
import ChartPie from "./components/chart-pie.vue";
|
||||
import ValueCard from "./components/value-card.vue";
|
||||
|
||||
import { loadDataBySqlApi } from "@/request/api";
|
||||
|
||||
const data = ref([]);
|
||||
|
||||
const init = async () => {
|
||||
const res = await loadDataBySqlApi(`select * from s_awokemodule order by b_xh`);
|
||||
if (res.success && res.data.length > 0) {
|
||||
data.value = res.data;
|
||||
|
||||
console.log(data.value);
|
||||
}
|
||||
};
|
||||
onMounted(async () => {
|
||||
await init();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/mixins.scss" as *;
|
||||
|
||||
.g3-user-reminder {
|
||||
width: 100%;
|
||||
@include flexGapWrap(16px, 16px);
|
||||
.value-card {
|
||||
width: calc(25% - 16px);
|
||||
}
|
||||
|
||||
.value-card:last-child {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
.g3-content {
|
||||
color: #337ab7;
|
||||
padding: 10px 0px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #e7eaec;
|
||||
}
|
||||
:deep(.el-card__body) {
|
||||
padding: 0px 20px;
|
||||
}
|
||||
.el-card {
|
||||
border: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { loadDataBySqlApi, saveObjectApi } from "@/request/api";
|
||||
import { getUniqueIdApi, loadDataBySqlApi, saveObjectApi, saveObjectKeysApi } from "@/request/api";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -54,9 +54,7 @@ const handleRowClick = (row, column, event) => {
|
||||
}
|
||||
// 关闭之前行的编辑状态
|
||||
if (currentEditRow.value !== null) {
|
||||
const prevRow = qxData.value.find(
|
||||
(item) => item.id === currentEditRow.value,
|
||||
);
|
||||
const prevRow = qxData.value.find((item) => item.id === currentEditRow.value);
|
||||
if (prevRow) {
|
||||
console.log(prevRow);
|
||||
const oldrow = prevRow.originalData;
|
||||
@@ -95,12 +93,10 @@ const isDataModified = (obj1, obj2) => {
|
||||
};
|
||||
|
||||
// 新增记录
|
||||
const addNewRow = () => {
|
||||
async function addNewRow() {
|
||||
// 如果当前有编辑中的行,先处理
|
||||
if (currentEditRow.value !== null) {
|
||||
const prevRow = qxData.value.find(
|
||||
(item) => item.id === currentEditRow.value,
|
||||
);
|
||||
const prevRow = qxData.value.find((item) => item.id === currentEditRow.value);
|
||||
if (prevRow) {
|
||||
// handleRowChange(prevRow)
|
||||
prevRow.isEditing = false;
|
||||
@@ -109,9 +105,13 @@ const addNewRow = () => {
|
||||
|
||||
// 计算新的排序号(最大值+1)
|
||||
const maxSort =
|
||||
qxData.value.length > 0
|
||||
? Math.max(...qxData.value.map((item) => item.b_xh || 0))
|
||||
: 0;
|
||||
qxData.value.length > 0 ? Math.max(...qxData.value.map((item) => item.b_xh || 0)) : 0;
|
||||
|
||||
const uniqueRes = await getUniqueIdApi();
|
||||
|
||||
if (!uniqueRes.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRow = {
|
||||
id: generateId(),
|
||||
@@ -141,7 +141,7 @@ const addNewRow = () => {
|
||||
});
|
||||
|
||||
console.log("新增记录:", newRow);
|
||||
};
|
||||
}
|
||||
// 生成唯一ID
|
||||
let idCounter = 2;
|
||||
const generateId = () => {
|
||||
@@ -194,7 +194,7 @@ async function Load_powerData() {
|
||||
formPowerData.value = [];
|
||||
|
||||
let sql = `
|
||||
select b_ivalue,max(b_caption) b_caption,max(b_des) b_des from s_module_power
|
||||
select b_ivalue,max(b_caption) b_caption,max(b_des) b_des from s_module_power
|
||||
group by b_ivalue order by b_ivalue
|
||||
`;
|
||||
console.log("原始sql:", sql);
|
||||
@@ -307,7 +307,7 @@ const close = (resetData = true) => {
|
||||
const handleSave = async () => {
|
||||
// 验证所有修改过的行
|
||||
const invalidRows = qxData.value.filter((row) => row.isModified || row.isNew);
|
||||
/*
|
||||
/*
|
||||
if (invalidRows.length > 0) {
|
||||
ElMessage.error('请填写完整的模块和Tag值')
|
||||
return
|
||||
@@ -345,7 +345,7 @@ const handleSave = async () => {
|
||||
const requestData = [
|
||||
{
|
||||
tableName: "s_module_power",
|
||||
keyField: "b_id",
|
||||
keyFields: ['ModuleID','b_ivalue'],
|
||||
inserts,
|
||||
updates,
|
||||
deletes,
|
||||
@@ -353,13 +353,13 @@ const handleSave = async () => {
|
||||
];
|
||||
|
||||
try {
|
||||
const response = await saveObjectApi(requestData);
|
||||
const response = await saveObjectKeysApi(requestData);
|
||||
|
||||
if (response.data?.success) {
|
||||
if (response.success) {
|
||||
ElMessage.success("保存成功");
|
||||
await initData(); // 重新加载数据,这会更新组列表
|
||||
} else {
|
||||
throw new Error(response.data?.message || "保存失败");
|
||||
throw new Error(response.message || "保存失败");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("保存出错:", err);
|
||||
@@ -407,10 +407,7 @@ const sqlContent = ref("");
|
||||
const showqxSql = (row) => {
|
||||
dialogSQLVisible.value = true;
|
||||
try {
|
||||
let sql =
|
||||
`delete from s_module_power where ModuleID=' ` +
|
||||
loading.current_moduleid +
|
||||
`';\n\n`;
|
||||
let sql = `delete from s_module_power where ModuleID=' ` + loading.current_moduleid + `';\n\n`;
|
||||
// 使用 reduce 或 forEach 拼接
|
||||
qxData.value.forEach((item, index) => {
|
||||
// 建议添加数据校验
|
||||
@@ -476,29 +473,15 @@ const handleCopySql = async () => {
|
||||
:max-height="tableHeight"
|
||||
@row-click="handlemoduleClick"
|
||||
>
|
||||
<el-table-column
|
||||
prop="b_id"
|
||||
label="模块ID"
|
||||
align="center"
|
||||
width="150"
|
||||
>
|
||||
<el-table-column prop="b_id" label="模块ID" align="center" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-if="row.isEditing" v-model="row.b_id" size="small" />
|
||||
<span v-else>{{ row.b_id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="b_ChineseName"
|
||||
label="模块名称"
|
||||
align="center"
|
||||
width="120"
|
||||
>
|
||||
<el-table-column prop="b_ChineseName" label="模块名称" align="center" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.isEditing"
|
||||
v-model="row.b_ChineseName"
|
||||
size="small"
|
||||
/>
|
||||
<el-input v-if="row.isEditing" v-model="row.b_ChineseName" size="small" />
|
||||
<span v-else>{{ row.b_ChineseName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -509,24 +492,14 @@ const handleCopySql = async () => {
|
||||
<div class="g3-row-main">
|
||||
<div style="width: 100%; margin-bottom: 4px">
|
||||
<div style="display: flex">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="loading.save"
|
||||
@click="addNewRow"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button type="primary" :loading="loading.save" @click="addNewRow">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
<el-button type="primary" @click="showqxSql">SQL语句</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
"
|
||||
>
|
||||
<div style="flex: 1; display: flex; flex-direction: column; min-height: 0">
|
||||
<el-table
|
||||
v-loading="loading.qxtable"
|
||||
:data="qxData"
|
||||
@@ -536,59 +509,26 @@ const handleCopySql = async () => {
|
||||
:max-height="tableHeight"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column
|
||||
prop="b_xh"
|
||||
label="排序"
|
||||
align="center"
|
||||
width="50"
|
||||
>
|
||||
<el-table-column prop="b_xh" label="排序" align="center" width="50">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.isEditing"
|
||||
v-model="row.b_xh"
|
||||
size="small"
|
||||
/>
|
||||
<el-input v-if="row.isEditing" v-model="row.b_xh" size="small" />
|
||||
<span v-else>{{ row.b_xh }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="false"
|
||||
prop="b_id"
|
||||
label="流水号"
|
||||
align="center"
|
||||
width="0"
|
||||
>
|
||||
<el-table-column v-if="false" prop="b_id" label="流水号" align="center" width="0">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.isEditing"
|
||||
v-model="row.b_id"
|
||||
size="small"
|
||||
/>
|
||||
<el-input v-if="row.isEditing" v-model="row.b_id" size="small" />
|
||||
<span v-else>{{ row.b_id }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="ModuleID"
|
||||
label="对应模块"
|
||||
align="center"
|
||||
width="120"
|
||||
>
|
||||
<el-table-column prop="ModuleID" label="对应模块" align="center" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.isEditing"
|
||||
v-model="row.ModuleID"
|
||||
size="small"
|
||||
/>
|
||||
<el-input v-if="row.isEditing" v-model="row.ModuleID" size="small" />
|
||||
<span v-else>{{ row.ModuleID }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="b_ivalue"
|
||||
label="Tag值"
|
||||
align="center"
|
||||
width="60"
|
||||
>
|
||||
<el-table-column prop="b_ivalue" label="Tag值" align="center" width="60">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.isEditing"
|
||||
@@ -600,12 +540,7 @@ const handleCopySql = async () => {
|
||||
<span v-else>{{ row.b_ivalue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="b_caption"
|
||||
label="Caption"
|
||||
align="center"
|
||||
width="130"
|
||||
>
|
||||
<el-table-column prop="b_caption" label="Caption" align="center" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.isEditing"
|
||||
@@ -617,12 +552,7 @@ const handleCopySql = async () => {
|
||||
<span v-else>{{ row.b_caption }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="b_des"
|
||||
label="描述"
|
||||
align="center"
|
||||
width="170"
|
||||
>
|
||||
<el-table-column prop="b_des" label="描述" align="center" width="170">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.isEditing"
|
||||
@@ -634,19 +564,9 @@ const handleCopySql = async () => {
|
||||
<span v-else>{{ row.b_des }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
fixed="right"
|
||||
label="操作"
|
||||
align="center"
|
||||
width="100"
|
||||
>
|
||||
<el-table-column fixed="right" label="操作" align="center" width="100">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button
|
||||
:disabled="false"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteRow(row, $index)"
|
||||
>
|
||||
<el-button :disabled="false" type="primary" link @click="deleteRow(row, $index)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -659,22 +579,10 @@ const handleCopySql = async () => {
|
||||
<div class="g3-row-main">
|
||||
<div style="width: 100%; margin-bottom: 4px">
|
||||
<div style="display: flex">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="loading.save"
|
||||
@click="gethispower"
|
||||
>←导</el-button
|
||||
>
|
||||
<el-button type="primary" :loading="loading.save" @click="gethispower">←导</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
"
|
||||
>
|
||||
<div style="flex: 1; display: flex; flex-direction: column; min-height: 0">
|
||||
<el-table
|
||||
v-loading="loading.powertable"
|
||||
:data="formPowerData"
|
||||
@@ -685,40 +593,17 @@ const handleCopySql = async () => {
|
||||
ref="powerTableRef"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="40"
|
||||
align="center"
|
||||
fixed
|
||||
/>
|
||||
<el-table-column type="selection" width="40" align="center" fixed />
|
||||
|
||||
<el-table-column
|
||||
prop="b_ivalue"
|
||||
label="Tag值"
|
||||
align="center"
|
||||
width="60"
|
||||
>
|
||||
<el-table-column prop="b_ivalue" label="Tag值" align="center" width="60">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.isEditing"
|
||||
v-model="row.b_ivalue"
|
||||
size="small"
|
||||
/>
|
||||
<el-input v-if="row.isEditing" v-model="row.b_ivalue" size="small" />
|
||||
<span v-else>{{ row.b_ivalue }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="b_caption"
|
||||
label="Caption"
|
||||
align="center"
|
||||
width="100"
|
||||
>
|
||||
<el-table-column prop="b_caption" label="Caption" align="center" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-if="row.isEditing"
|
||||
v-model="row.b_caption"
|
||||
size="small"
|
||||
/>
|
||||
<el-input v-if="row.isEditing" v-model="row.b_caption" size="small" />
|
||||
<span v-else>{{ row.b_caption }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -53,12 +53,12 @@ function getTableData() {
|
||||
|
||||
let searchCondition = "";
|
||||
|
||||
if (searchForm.value.name !== "") {
|
||||
searchCondition = ` (b_id LIKE '%${searchForm.value.name}%' OR b_name LIKE '%${searchForm.value.name}%')`;
|
||||
if (userid !== "g3soft") {
|
||||
searchCondition = ` b_id <> 'g3soft' `;
|
||||
}
|
||||
|
||||
if (userid !== "g3soft") {
|
||||
searchCondition += `AND b_id != 'g3soft' `;
|
||||
if (searchForm.value.name !== "") {
|
||||
searchCondition += `AND (b_id LIKE '%${searchForm.value.name}%' OR b_name LIKE '%${searchForm.value.name}%')`;
|
||||
}
|
||||
|
||||
loadPageDataApi({
|
||||
@@ -209,9 +209,7 @@ async function deleteRows(rows = []) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = rows
|
||||
.filter((row) => row.b_id !== "g3soft")
|
||||
.map((row) => row.b_id);
|
||||
const selectedIds = rows.filter((row) => row.b_id !== "g3soft").map((row) => row.b_id);
|
||||
|
||||
const delectedIds = rows
|
||||
.filter((row) => !$table.isInsertByRow(row) && row.b_id !== "g3soft")
|
||||
@@ -280,8 +278,7 @@ async function handleSaveRow(row) {
|
||||
|
||||
// 1. 检查表格内重复(本地数据)
|
||||
const duplicate = tableData.value.some(
|
||||
(item) =>
|
||||
item.b_id?.trim().toUpperCase() === editId && item.b_id !== row.b_id, // 排除自己
|
||||
(item) => item.b_id?.trim().toUpperCase() === editId && item.b_id !== row.b_id, // 排除自己
|
||||
);
|
||||
if (duplicate) {
|
||||
ElMessage.warning("账号重复,请修改");
|
||||
@@ -392,9 +389,7 @@ function handleShowBatchCopyDialog() {
|
||||
if ($table) {
|
||||
const records = $table.getCheckboxRecords();
|
||||
|
||||
const filteredRecords = records.filter(
|
||||
(item) => !$table.isInsertByRow(item),
|
||||
);
|
||||
const filteredRecords = records.filter((item) => !$table.isInsertByRow(item));
|
||||
|
||||
const bIds = filteredRecords.map((item) => item.b_id);
|
||||
|
||||
@@ -437,21 +432,13 @@ function handleShowBatchCopyDialog() {
|
||||
</el-icon>
|
||||
批量复制权限
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleShowCopyDialog"
|
||||
:disabled="!isSelectedRow"
|
||||
>
|
||||
<el-button type="primary" @click="handleShowCopyDialog" :disabled="!isSelectedRow">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:copy" />
|
||||
</el-icon>
|
||||
复制权限
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!isSelectedRow"
|
||||
@click="showEmployee = true"
|
||||
>
|
||||
<el-button type="primary" :disabled="!isSelectedRow" @click="showEmployee = true">
|
||||
<el-icon class="el-icon--left">
|
||||
<G3Icon icon="lucide:book-user" />
|
||||
</el-icon>
|
||||
@@ -573,18 +560,10 @@ function handleShowBatchCopyDialog() {
|
||||
}"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox
|
||||
v-model="row.b_canuse"
|
||||
true-value="1"
|
||||
false-value="0"
|
||||
/>
|
||||
<el-checkbox v-model="row.b_canuse" true-value="1" false-value="0" />
|
||||
</template>
|
||||
<template #edit="{ row }">
|
||||
<el-checkbox
|
||||
v-model="row.b_canuse"
|
||||
true-value="1"
|
||||
false-value="0"
|
||||
/>
|
||||
<el-checkbox v-model="row.b_canuse" true-value="1" false-value="0" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
@@ -628,12 +607,7 @@ function handleShowBatchCopyDialog() {
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
@click="cancelEditRow(row)"
|
||||
@click.stop
|
||||
style="margin-left: 6px"
|
||||
>
|
||||
<el-button link @click="cancelEditRow(row)" @click.stop style="margin-left: 6px">
|
||||
取消
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
@@ -1,101 +1,170 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>自适应 Descriptions 每行自动填满</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
}
|
||||
<meta charset="UTF-8" />
|
||||
<title>数据看板</title>
|
||||
|
||||
/* 每行一个 grid 容器 */
|
||||
.row {
|
||||
display: grid;
|
||||
border-top: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc;
|
||||
margin-bottom: 10px;
|
||||
gap: 0;
|
||||
/* 紧凑像表格 */
|
||||
}
|
||||
|
||||
/* item 样式 */
|
||||
.item {
|
||||
display: flex;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 0 0 100px;
|
||||
/* label 固定宽度 */
|
||||
background-color: #e0f7fa;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
background-color: #fff9c4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 响应式:小屏幕每个 item 占满整行 */
|
||||
@media (max-width: 500px) {
|
||||
.row {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="bg-gray-100">
|
||||
|
||||
<h2>自适应 Descriptions 示例</h2>
|
||||
<div class="p-4">
|
||||
|
||||
<!-- 第一行 2 列 -->
|
||||
<div class="row" style="grid-template-columns: repeat(2, 1fr);">
|
||||
<div class="item">
|
||||
<span class="label">姓名:</span>
|
||||
<span class="value">张三</span>
|
||||
<!-- 顶部 -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-xl font-bold">数据看板</h1>
|
||||
<button id="refreshBtn" class="bg-blue-500 text-white px-3 py-1 rounded" onclick="refreshData()">
|
||||
🔄 刷新
|
||||
</button>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">年龄:</span>
|
||||
<span class="value">18</span>
|
||||
|
||||
<!-- KPI -->
|
||||
<div id="kpi" class="grid grid-cols-6 gap-3 mb-4"></div>
|
||||
|
||||
<!-- ⭐ 公告(不占全宽,简洁版) -->
|
||||
<div class="bg-orange-100 border border-orange-300 text-orange-800 px-4 py-3 rounded mb-4 w-full sm:w-2/3 md:w-1/2">
|
||||
<div class="text-lg font-bold">📢 公告:</div>
|
||||
<div id="notice" class="text-sm space-y-3">
|
||||
<!-- 公告内容会由 JS 填充 -->
|
||||
</div>
|
||||
</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">
|
||||
|
||||
<!-- 排行 -->
|
||||
<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="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>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 第二行 1 列 -->
|
||||
<div class="row" style="grid-template-columns: repeat(1, 1fr);">
|
||||
<div class="item">
|
||||
<span class="label">邮箱:</span>
|
||||
<span class="value">zhangsan@example.com</span>
|
||||
</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: [
|
||||
"📌 今日起必须上传封条照片",
|
||||
"📌 晚班请补录遗漏数据",
|
||||
"📌 周五晚系统维护"
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
<!-- 第三行 3 列 -->
|
||||
<div class="row" style="grid-template-columns: repeat(3, 1fr);">
|
||||
<div class="item">
|
||||
<span class="label">城市:</span>
|
||||
<span class="value">北京</span>
|
||||
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>
|
||||
<div class="item">
|
||||
<span class="label">职业:</span>
|
||||
<span class="value">学生</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">爱好:</span>
|
||||
<span class="value">篮球</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join("");
|
||||
|
||||
// 公告内容
|
||||
document.getElementById("notice").innerHTML =
|
||||
data.notice.map(i => `
|
||||
<div class="bg-orange-50 border-l-4 border-orange-400 px-4 py-2 rounded shadow-sm">
|
||||
${i}
|
||||
</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("");
|
||||
|
||||
// 趋势图
|
||||
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>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import userReminder from '@/views/home-page/userReminder.vue'
|
||||
<serviceVolume />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="g3-row g3-userReminder">
|
||||
<userReminder />
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user