mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-04-03 10:43:42 +08:00
重构AOP日志功能,新增日志查询、删除接口及相关页面,优化日志管理体验;更新前端组件以支持日志展示和操作。
This commit is contained in:
@@ -48,17 +48,11 @@ public class LoggingAspect {
|
|||||||
return processWithLogging(joinPoint, aopLog);
|
return processWithLogging(joinPoint, aopLog);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Around("execution(* com.zl.mjga.service..*(..))")
|
// @Around("execution(* com.zl.mjga.service..*(..))")
|
||||||
public Object logService(ProceedingJoinPoint joinPoint) throws Throwable {
|
// public Object logService(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
AopLog aopLog = new AopLog();
|
// AopLog aopLog = new AopLog();
|
||||||
return processWithLogging(joinPoint, aopLog);
|
// return processWithLogging(joinPoint, aopLog);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Around("execution(* com.zl.mjga.repository..*(..))")
|
|
||||||
public Object logRepository(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
||||||
AopLog aopLog = new AopLog();
|
|
||||||
return processWithLogging(joinPoint, aopLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object processWithLogging(ProceedingJoinPoint joinPoint, AopLog aopLog) throws Throwable {
|
private Object processWithLogging(ProceedingJoinPoint joinPoint, AopLog aopLog) throws Throwable {
|
||||||
if (shouldSkipLogging(joinPoint) || !isUserAuthenticated()) {
|
if (shouldSkipLogging(joinPoint) || !isUserAuthenticated()) {
|
||||||
@@ -82,7 +76,7 @@ public class LoggingAspect {
|
|||||||
|
|
||||||
private Long getCurrentUserId() {
|
private Long getCurrentUserId() {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
String username = (String) authentication.getPrincipal();
|
String username = authentication.getName();
|
||||||
User user = userRepository.fetchOneByUsername(username);
|
User user = userRepository.fetchOneByUsername(username);
|
||||||
return user.getId();
|
return user.getId();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.zl.mjga.controller;
|
package com.zl.mjga.controller;
|
||||||
|
|
||||||
|
import com.zl.mjga.annotation.SkipAopLog;
|
||||||
import com.zl.mjga.dto.PageRequestDto;
|
import com.zl.mjga.dto.PageRequestDto;
|
||||||
import com.zl.mjga.dto.PageResponseDto;
|
import com.zl.mjga.dto.PageResponseDto;
|
||||||
import com.zl.mjga.dto.department.DepartmentBindDto;
|
import com.zl.mjga.dto.department.DepartmentBindDto;
|
||||||
@@ -56,6 +57,7 @@ public class IdentityAccessController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/me")
|
@PostMapping("/me")
|
||||||
|
@SkipAopLog
|
||||||
void upsertMe(Principal principal, @RequestBody UserUpsertDto userUpsertDto) {
|
void upsertMe(Principal principal, @RequestBody UserUpsertDto userUpsertDto) {
|
||||||
String name = principal.getName();
|
String name = principal.getName();
|
||||||
User user = userRepository.fetchOneByUsername(name);
|
User user = userRepository.fetchOneByUsername(name);
|
||||||
@@ -65,6 +67,7 @@ public class IdentityAccessController {
|
|||||||
|
|
||||||
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
|
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
|
||||||
@PostMapping("/user")
|
@PostMapping("/user")
|
||||||
|
@SkipAopLog
|
||||||
void upsertUser(@RequestBody @Valid UserUpsertDto userUpsertDto) {
|
void upsertUser(@RequestBody @Valid UserUpsertDto userUpsertDto) {
|
||||||
identityAccessService.upsertUser(userUpsertDto);
|
identityAccessService.upsertUser(userUpsertDto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.zl.mjga.controller;
|
package com.zl.mjga.controller;
|
||||||
|
|
||||||
|
import com.zl.mjga.annotation.SkipAopLog;
|
||||||
import com.zl.mjga.config.security.Jwt;
|
import com.zl.mjga.config.security.Jwt;
|
||||||
import com.zl.mjga.dto.sign.SignInDto;
|
import com.zl.mjga.dto.sign.SignInDto;
|
||||||
import com.zl.mjga.dto.sign.SignUpDto;
|
import com.zl.mjga.dto.sign.SignUpDto;
|
||||||
@@ -22,6 +23,7 @@ public class SignController {
|
|||||||
|
|
||||||
@ResponseStatus(HttpStatus.OK)
|
@ResponseStatus(HttpStatus.OK)
|
||||||
@PostMapping("/sign-in")
|
@PostMapping("/sign-in")
|
||||||
|
@SkipAopLog
|
||||||
void signIn(
|
void signIn(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class AopLogRepository extends AopLogDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto, AopLogQueryDto queryDto) {
|
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto, AopLogQueryDto queryDto) {
|
||||||
return selectBy(queryDto)
|
return selectByWithoutReturnValue(queryDto)
|
||||||
.orderBy(pageRequestDto.getSortFields())
|
.orderBy(pageRequestDto.getSortFields())
|
||||||
.limit(pageRequestDto.getSize())
|
.limit(pageRequestDto.getSize())
|
||||||
.offset(pageRequestDto.getOffset())
|
.offset(pageRequestDto.getOffset())
|
||||||
@@ -47,6 +47,16 @@ public class AopLogRepository extends AopLogDao {
|
|||||||
.where(buildConditions(queryDto));
|
.where(buildConditions(queryDto));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public SelectConditionStep<Record> selectByWithoutReturnValue(AopLogQueryDto queryDto) {
|
||||||
|
return ctx()
|
||||||
|
.select(AOP_LOG.asterisk().except(AOP_LOG.RETURN_VALUE, AOP_LOG.METHOD_ARGS), USER.USERNAME, DSL.count().over().as("total_count"))
|
||||||
|
.from(AOP_LOG)
|
||||||
|
.leftJoin(USER)
|
||||||
|
.on(AOP_LOG.USER_ID.eq(USER.ID))
|
||||||
|
.where(buildConditions(queryDto));
|
||||||
|
}
|
||||||
|
|
||||||
private Condition buildConditions(AopLogQueryDto queryDto) {
|
private Condition buildConditions(AopLogQueryDto queryDto) {
|
||||||
Condition condition = noCondition();
|
Condition condition = noCondition();
|
||||||
|
|
||||||
|
|||||||
84
frontend/src/api/mocks/aopLogHandlers.ts
Normal file
84
frontend/src/api/mocks/aopLogHandlers.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { faker } from "@faker-js/faker";
|
||||||
|
import { http, HttpResponse } from "msw";
|
||||||
|
|
||||||
|
// 生成AOP日志数据
|
||||||
|
const generateAopLog = () => ({
|
||||||
|
id: faker.number.int({ min: 1, max: 1000 }),
|
||||||
|
className: faker.helpers.arrayElement([
|
||||||
|
"com.example.controller.UserController",
|
||||||
|
"com.example.service.UserService",
|
||||||
|
"com.example.controller.RoleController",
|
||||||
|
"com.example.service.RoleService",
|
||||||
|
"com.example.controller.DepartmentController",
|
||||||
|
"com.example.service.DepartmentService",
|
||||||
|
]),
|
||||||
|
methodName: faker.helpers.arrayElement([
|
||||||
|
"findById",
|
||||||
|
"save",
|
||||||
|
"update",
|
||||||
|
"delete",
|
||||||
|
"findAll",
|
||||||
|
"findByName",
|
||||||
|
"pageQuery",
|
||||||
|
]),
|
||||||
|
methodArgs: JSON.stringify([
|
||||||
|
{ name: "id", value: faker.number.int({ min: 1, max: 100 }) },
|
||||||
|
{ name: "name", value: faker.person.fullName() },
|
||||||
|
]),
|
||||||
|
returnValue: JSON.stringify({
|
||||||
|
id: faker.number.int({ min: 1, max: 100 }),
|
||||||
|
name: faker.person.fullName(),
|
||||||
|
success: true,
|
||||||
|
}),
|
||||||
|
executionTime: faker.number.int({ min: 10, max: 5000 }),
|
||||||
|
success: faker.datatype.boolean(0.9), // 90%成功率
|
||||||
|
errorMessage: faker.helpers.maybe(() => faker.lorem.sentence(), { probability: 0.1 }),
|
||||||
|
userId: faker.number.int({ min: 1, max: 100 }),
|
||||||
|
username: faker.internet.userName(),
|
||||||
|
ipAddress: faker.internet.ip(),
|
||||||
|
userAgent: faker.internet.userAgent(),
|
||||||
|
curl: `curl -X GET "${faker.internet.url()}" -H "Authorization: Bearer ${faker.string.alphanumeric(32)}"`,
|
||||||
|
createTime: faker.date.recent({ days: 30 }).toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// 分页查询AOP日志
|
||||||
|
http.get("/aop-log/page-query", () => {
|
||||||
|
const mockData = {
|
||||||
|
data: faker.helpers.multiple(generateAopLog, { count: 10 }),
|
||||||
|
total: 100,
|
||||||
|
};
|
||||||
|
return HttpResponse.json(mockData);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 查询单条日志详情
|
||||||
|
http.get("/aop-log/:id", ({ params }) => {
|
||||||
|
const id = params.id;
|
||||||
|
return HttpResponse.json({
|
||||||
|
...generateAopLog(),
|
||||||
|
id: Number(id),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 删除单条日志
|
||||||
|
http.delete("/aop-log/:id", ({ params }) => {
|
||||||
|
console.log(`Captured a "DELETE /aop-log/${params.id}" request`);
|
||||||
|
return HttpResponse.json({ success: true });
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 批量删除日志
|
||||||
|
http.delete("/aop-log/batch", async ({ request }) => {
|
||||||
|
const ids = await request.json();
|
||||||
|
console.log(`Captured a "DELETE /aop-log/batch" request with ids: ${ids}`);
|
||||||
|
return HttpResponse.json(ids.length);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 删除指定时间前的日志
|
||||||
|
http.delete("/aop-log/before", ({ params }) => {
|
||||||
|
const { beforeTime } = Object.fromEntries(
|
||||||
|
new URL(params.request.url).searchParams
|
||||||
|
);
|
||||||
|
console.log(`Captured a "DELETE /aop-log/before" request with time: ${beforeTime}`);
|
||||||
|
return HttpResponse.json(faker.number.int({ min: 5, max: 50 }));
|
||||||
|
}),
|
||||||
|
];
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { setupWorker } from "msw/browser";
|
import { setupWorker } from "msw/browser";
|
||||||
import authHandlers from "./authHandlers";
|
|
||||||
import jobHandlers from "./schedulerHandlers";
|
|
||||||
import permissionHandlers from "./permissionHandlers";
|
|
||||||
import roleHandlers from "./roleHandlers";
|
|
||||||
import userHandlers from "./iamHandlers";
|
|
||||||
import departmentHandlers from "./departmentHandlers";
|
|
||||||
import positionHandlers from "./positionHandlers";
|
|
||||||
import aiHandlers from "./aiHandlers";
|
import aiHandlers from "./aiHandlers";
|
||||||
|
import aopLogHandlers from "./aopLogHandlers";
|
||||||
|
import authHandlers from "./authHandlers";
|
||||||
|
import departmentHandlers from "./departmentHandlers";
|
||||||
|
import userHandlers from "./iamHandlers";
|
||||||
import knowledgeHandlers from "./knowledgeHandlers";
|
import knowledgeHandlers from "./knowledgeHandlers";
|
||||||
|
import permissionHandlers from "./permissionHandlers";
|
||||||
|
import positionHandlers from "./positionHandlers";
|
||||||
|
import roleHandlers from "./roleHandlers";
|
||||||
|
import jobHandlers from "./schedulerHandlers";
|
||||||
|
|
||||||
export const worker = setupWorker(
|
export const worker = setupWorker(
|
||||||
...userHandlers,
|
...userHandlers,
|
||||||
@@ -19,4 +20,5 @@ export const worker = setupWorker(
|
|||||||
...positionHandlers,
|
...positionHandlers,
|
||||||
...aiHandlers,
|
...aiHandlers,
|
||||||
...knowledgeHandlers,
|
...knowledgeHandlers,
|
||||||
|
...aopLogHandlers,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
"description": "Generated server url"
|
"description": "Generated server url"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "AOP日志管理",
|
||||||
|
"description": "AOP日志查看和管理接口"
|
||||||
|
}
|
||||||
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"/scheduler/job/update": {
|
"/scheduler/job/update": {
|
||||||
"put": {
|
"put": {
|
||||||
@@ -1406,6 +1412,105 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/aop-log/{id}": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"AOP日志管理"
|
||||||
|
],
|
||||||
|
"summary": "查询日志详情",
|
||||||
|
"description": "根据ID查询单条日志的详细信息",
|
||||||
|
"operationId": "getAopLogById",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"description": "日志ID",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AopLogRespDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"AOP日志管理"
|
||||||
|
],
|
||||||
|
"summary": "删除单条日志",
|
||||||
|
"description": "根据ID删除单条日志",
|
||||||
|
"operationId": "deleteAopLog",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"description": "日志ID",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/aop-log/page-query": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"AOP日志管理"
|
||||||
|
],
|
||||||
|
"summary": "分页查询AOP日志",
|
||||||
|
"description": "支持多种条件筛选的分页查询",
|
||||||
|
"operationId": "pageQueryAopLogs",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "pageRequestDto",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PageRequestDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "queryDto",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AopLogQueryDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PageResponseDtoListAopLogRespDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/ai/llm/page-query": {
|
"/ai/llm/page-query": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -1444,6 +1549,79 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/aop-log/before": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"AOP日志管理"
|
||||||
|
],
|
||||||
|
"summary": "删除指定时间前的日志",
|
||||||
|
"description": "删除指定时间之前的所有日志",
|
||||||
|
"operationId": "deleteLogsBeforeTime",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "beforeTime",
|
||||||
|
"in": "query",
|
||||||
|
"description": "截止时间",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/aop-log/batch": {
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"AOP日志管理"
|
||||||
|
],
|
||||||
|
"summary": "批量删除日志",
|
||||||
|
"description": "根据ID列表批量删除日志",
|
||||||
|
"operationId": "deleteAopLogs",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "日志ID列表",
|
||||||
|
"items": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"content": {
|
||||||
|
"*/*": {
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/ai/action/user": {
|
"/ai/action/user": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -2477,6 +2655,113 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"AopLogRespDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"className": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"methodName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"methodArgs": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"returnValue": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"executionTime": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"errorMessage": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ipAddress": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userAgent": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"curl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createTime": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AopLogQueryDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"className": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"methodName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"ipAddress": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"startTime": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"endTime": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"minExecutionTime": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"maxExecutionTime": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PageResponseDtoListAopLogRespDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"total": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AopLogRespDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"LlmQueryDto": {
|
"LlmQueryDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
4959
frontend/src/api/types/schema.d.ts
vendored
4959
frontend/src/api/types/schema.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,54 +1,70 @@
|
|||||||
/* 日期选择器亮色主题 - 与 Flowbite 蓝色主题匹配 */ .dp__theme_light {
|
/* 日期选择器亮色主题 - 与 Flowbite 蓝色主题匹配 */ .dp__theme_light {
|
||||||
/* 基础颜色 */
|
/* 基础颜色 */
|
||||||
--dp-background-color: #fff;
|
--dp-background-color: #fff;
|
||||||
--dp-text-color: #1f2937; /* 对应 Flowbite 的 gray-800 */
|
--dp-text-color: #1f2937; /* 对应 Flowbite 的 gray-800 */
|
||||||
|
|
||||||
/* 主色调 */
|
/* 主色调 */
|
||||||
--dp-primary-color: #2563eb; /* 对应 Flowbite 的 primary-600 */
|
--dp-primary-color: #2563eb; /* 对应 Flowbite 的 primary-600 */
|
||||||
--dp-primary-disabled-color: #93c5fd; /* 对应 Flowbite 的 primary-300 */
|
--dp-primary-disabled-color: #93c5fd; /* 对应 Flowbite 的 primary-300 */
|
||||||
--dp-primary-text-color: #fff;
|
--dp-primary-text-color: #fff;
|
||||||
|
|
||||||
/* 次要颜色 */
|
/* 次要颜色 */
|
||||||
--dp-secondary-color: #9ca3af; /* 对应 Flowbite 的 gray-400 */
|
--dp-secondary-color: #9ca3af; /* 对应 Flowbite 的 gray-400 */
|
||||||
|
|
||||||
/* 背景颜色 */
|
/* 背景颜色 */
|
||||||
--dp-background-color: var(--color-gray-50);
|
--dp-background-color: var(--color-gray-50);
|
||||||
|
|
||||||
/* 边框颜色 */
|
/* 边框颜色 */
|
||||||
--dp-border-color: var(--color-gray-300); /* 对应 Flowbite 的 gray-200 */
|
--dp-border-color: var(--color-gray-300); /* 对应 Flowbite 的 gray-200 */
|
||||||
--dp-menu-border-color: #e5e7eb;
|
--dp-menu-border-color: #e5e7eb;
|
||||||
|
|
||||||
/* 禁用状态 */
|
/* 禁用状态 */
|
||||||
--dp-disabled-color: #f3f4f6; /* 对应 Flowbite 的 gray-100 */
|
--dp-disabled-color: #f3f4f6; /* 对应 Flowbite 的 gray-100 */
|
||||||
--dp-disabled-color-text: #9ca3af; /* 对应 Flowbite 的 gray-400 */
|
--dp-disabled-color-text: #9ca3af; /* 对应 Flowbite 的 gray-400 */
|
||||||
|
|
||||||
/* 滚动条 */
|
/* 滚动条 */
|
||||||
--dp-scroll-bar-background: #f3f4f6; /* 对应 Flowbite 的 gray-100 */
|
--dp-scroll-bar-background: #f3f4f6; /* 对应 Flowbite 的 gray-100 */
|
||||||
--dp-scroll-bar-color: #9ca3af; /* 对应 Flowbite 的 gray-400 */
|
--dp-scroll-bar-color: #9ca3af; /* 对应 Flowbite 的 gray-400 */
|
||||||
|
|
||||||
/* 成功状态 */
|
/* 成功状态 */
|
||||||
--dp-success-color: #10b981; /* 对应 Tailwind 的 emerald-500 */
|
--dp-success-color: #10b981; /* 对应 Tailwind 的 emerald-500 */
|
||||||
--dp-success-color-disabled: #6ee7b7; /* 对应 Tailwind 的 emerald-300 */
|
--dp-success-color-disabled: #6ee7b7; /* 对应 Tailwind 的 emerald-300 */
|
||||||
|
|
||||||
/* 图标颜色 */
|
/* 图标颜色 */
|
||||||
--dp-icon-color: #6b7280; /* 对应 Flowbite 的 gray-500 */
|
--dp-icon-color: #6b7280; /* 对应 Flowbite 的 gray-500 */
|
||||||
|
|
||||||
/* 危险/错误状态 */
|
/* 危险/错误状态 */
|
||||||
--dp-danger-color: #ef4444; /* 对应 Tailwind 的 red-500 */
|
--dp-danger-color: #ef4444; /* 对应 Tailwind 的 red-500 */
|
||||||
--dp-marker-color: #ef4444;
|
--dp-marker-color: #ef4444;
|
||||||
|
|
||||||
/* 提示颜色 */
|
/* 提示颜色 */
|
||||||
--dp-tooltip-color: #f9fafb; /* 对应 Flowbite 的 gray-50 */
|
--dp-tooltip-color: #f9fafb; /* 对应 Flowbite 的 gray-50 */
|
||||||
|
|
||||||
/* 高亮颜色 */
|
/* 高亮颜色 */
|
||||||
--dp-highlight-color: rgb(37 99 235 / 10%); /* 对应 Flowbite 的 primary-600 透明度 */
|
--dp-highlight-color: rgb(37 99 235 / 10%); /* 对应 Flowbite 的 primary-600 透明度 */
|
||||||
|
|
||||||
/* 日期范围相关 */
|
/* 日期范围相关 */
|
||||||
--dp-range-between-dates-background-color: var(--dp-hover-color, #eff6ff);
|
--dp-range-between-dates-background-color: var(--dp-hover-color, #eff6ff);
|
||||||
--dp-range-between-dates-text-color: var(--dp-hover-text-color, #1f2937);
|
--dp-range-between-dates-text-color: var(--dp-hover-text-color, #1f2937);
|
||||||
--dp-range-between-border-color: var(--dp-hover-color, #eff6ff);
|
--dp-range-between-border-color: var(--dp-hover-color, #eff6ff);
|
||||||
|
|
||||||
|
/* 圆角设置 - 匹配项目中的 rounded-lg */
|
||||||
|
--dp-border-radius: 0.5rem; /* 8px,匹配 Tailwind 的 rounded-lg */
|
||||||
|
--dp-cell-border-radius: 0.375rem; /* 6px,稍微小一点,更美观 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复日期文本与图标重叠的问题 */
|
||||||
|
.dp__input_wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__input {
|
||||||
|
padding-left: 2rem !important; /* 确保文本不会与图标重叠 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__input_icon {
|
||||||
|
position: absolute;
|
||||||
|
/* left: 0.75rem !important; */
|
||||||
|
right: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* 圆角设置 - 匹配项目中的 rounded-lg */
|
|
||||||
--dp-border-radius: 0.5rem; /* 8px,匹配 Tailwind 的 rounded-lg */
|
|
||||||
--dp-cell-border-radius: 0.375rem; /* 6px,稍微小一点,更美观 */
|
|
||||||
}
|
|
||||||
|
|||||||
24
frontend/src/components/common/LogStatusBadge.vue
Normal file
24
frontend/src/components/common/LogStatusBadge.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-flex items-center px-2 py-1 text-xs font-medium rounded-full',
|
||||||
|
success
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-red-100 text-red-800'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'w-2 h-2 mr-1 rounded-full',
|
||||||
|
success ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
]"
|
||||||
|
></span>
|
||||||
|
{{ success ? '成功' : '失败' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
success: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import Assistant from "./Assistant.vue";
|
||||||
import CardBase from "./CardBase.vue";
|
import CardBase from "./CardBase.vue";
|
||||||
|
import LogStatusBadge from "./LogStatusBadge.vue";
|
||||||
import PromotionBanner from "./PromotionBanner.vue";
|
import PromotionBanner from "./PromotionBanner.vue";
|
||||||
|
|
||||||
export { CardBase, PromotionBanner };
|
export { Assistant, CardBase, LogStatusBadge, PromotionBanner };
|
||||||
|
|||||||
10
frontend/src/components/icons/LogIcon.vue
Normal file
10
frontend/src/components/icons/LogIcon.vue
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" width="24" height="24">
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
|
||||||
|
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2Z" />
|
||||||
|
<path d="M9 9h1" />
|
||||||
|
<path d="M9 13h6" />
|
||||||
|
<path d="M9 17h6" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -1,14 +1,32 @@
|
|||||||
// 统一导出所有图标组件
|
// 统一导出所有图标组件
|
||||||
export { default as StopIcon } from "./StopIcon.vue";
|
import AiChatIcon from "./AiChatIcon.vue";
|
||||||
export { default as PlusIcon } from "./PlusIcon.vue";
|
import DepartmentIcon from "./DepartmentIcon.vue";
|
||||||
export { default as AiChatIcon } from "./AiChatIcon.vue";
|
import KnowledgeIcon from "./KnowledgeIcon.vue";
|
||||||
export { default as PositionIcon } from "./PositionIcon.vue";
|
import LlmConfigIcon from "./LlmConfigIcon.vue";
|
||||||
export { default as SchedulerIcon } from "./SchedulerIcon.vue";
|
import LoadingIcon from "./LoadingIcon.vue";
|
||||||
export { default as DepartmentIcon } from "./DepartmentIcon.vue";
|
import LogIcon from "./LogIcon.vue";
|
||||||
export { default as LlmConfigIcon } from "./LlmConfigIcon.vue";
|
import PermissionIcon from "./PermissionIcon.vue";
|
||||||
export { default as LoadingIcon } from "./LoadingIcon.vue";
|
import PlusIcon from "./PlusIcon.vue";
|
||||||
export { default as RoleIcon } from "./RoleIcon.vue";
|
import PositionIcon from "./PositionIcon.vue";
|
||||||
export { default as SettingsIcon } from "./SettingsIcon.vue";
|
import RoleIcon from "./RoleIcon.vue";
|
||||||
export { default as UsersIcon } from "./UsersIcon.vue";
|
import SchedulerIcon from "./SchedulerIcon.vue";
|
||||||
export { default as PermissionIcon } from "./PermissionIcon.vue";
|
import SettingsIcon from "./SettingsIcon.vue";
|
||||||
export { default as KnowledgeIcon } from "./KnowledgeIcon.vue";
|
import StopIcon from "./StopIcon.vue";
|
||||||
|
import UsersIcon from "./UsersIcon.vue";
|
||||||
|
|
||||||
|
export {
|
||||||
|
AiChatIcon,
|
||||||
|
DepartmentIcon,
|
||||||
|
KnowledgeIcon,
|
||||||
|
LlmConfigIcon,
|
||||||
|
LoadingIcon,
|
||||||
|
LogIcon,
|
||||||
|
PermissionIcon,
|
||||||
|
PlusIcon,
|
||||||
|
PositionIcon,
|
||||||
|
RoleIcon,
|
||||||
|
SchedulerIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
StopIcon,
|
||||||
|
UsersIcon,
|
||||||
|
};
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ import { onMounted, ref } from "vue";
|
|||||||
import { RouterLink, useRoute } from "vue-router";
|
import { RouterLink, useRoute } from "vue-router";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AiChatIcon,
|
||||||
DepartmentIcon,
|
DepartmentIcon,
|
||||||
KnowledgeIcon,
|
KnowledgeIcon,
|
||||||
LlmConfigIcon,
|
LlmConfigIcon,
|
||||||
|
LogIcon,
|
||||||
PermissionIcon,
|
PermissionIcon,
|
||||||
PositionIcon,
|
PositionIcon,
|
||||||
RoleIcon,
|
RoleIcon,
|
||||||
@@ -119,6 +121,11 @@ const menuItems = [
|
|||||||
path: Routes.KNOWLEDGEVIEW.fullPath(),
|
path: Routes.KNOWLEDGEVIEW.fullPath(),
|
||||||
icon: KnowledgeIcon,
|
icon: KnowledgeIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "日志管理",
|
||||||
|
path: Routes.AOPLOGVIEW.fullPath(),
|
||||||
|
icon: LogIcon,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 gap-y-3 sm:gap-y-0">
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 gap-y-3 sm:gap-y-0">
|
||||||
<form
|
<form
|
||||||
class="grid grid-cols-2 sm:grid-cols-1 w-full min-w-[200px] sm:w-auto gap-2 xs:gap-3 items-stretch xs:items-center">
|
class="grid grid-cols-2 sm:grid-cols-2 w-full min-w-[200px] sm:w-auto gap-2 xs:gap-3 items-stretch xs:items-center">
|
||||||
<template v-for="(filter, index) in filters" :key="index">
|
<template v-for="(filter, index) in filters" :key="index">
|
||||||
<!-- 输入框类型 -->
|
<!-- 输入框类型 -->
|
||||||
<div v-if="filter.type === 'input'" class="flex-grow">
|
<div v-if="filter.type === 'input'" class="flex-grow">
|
||||||
@@ -17,10 +17,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 日期范围选择器 -->
|
<!-- 日期范围选择器 -->
|
||||||
<div v-else-if="filter.type === 'date-range'" class="flex-grow">
|
<div v-else-if="filter.type === 'date-range'" class="flex-grow datepicker-container">
|
||||||
<VueDatePicker v-model="filterValues[filter.name]" locale="zh-CN" range
|
<VueDatePicker v-model="filterValues[filter.name]" locale="zh-CN" range
|
||||||
:format="filter.format || 'yyyy/MM/dd HH:mm:ss - yyy/MM/dd HH:mm:ss'" :placeholder="filter.placeholder"
|
:format="filter.format || 'yyyy/MM/dd HH:mm:ss - yyy/MM/dd HH:mm:ss'" :placeholder="filter.placeholder"
|
||||||
:enable-time-picker="filter.enableTimePicker !== false" :auto-apply="filter.autoApply !== false" />
|
:enable-time-picker="filter.enableTimePicker !== false" :auto-apply="filter.autoApply !== false"
|
||||||
|
class="filter-datepicker" teleport="body" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 选择器 -->
|
<!-- 选择器 -->
|
||||||
@@ -33,15 +34,17 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<Button variant="primary" size="sm" @click.prevent="handleSearch">
|
<div class="col-span-full flex mt-2">
|
||||||
<template #icon>
|
<Button variant="primary" size="sm" @click.prevent="handleSearch" class="w-full sm:w-1/2">
|
||||||
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
<template #icon>
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||||
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
</svg>
|
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
||||||
</template>
|
</svg>
|
||||||
搜索
|
</template>
|
||||||
</Button>
|
<span class="ps-1.5">搜索</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- 额外操作按钮插槽 -->
|
<!-- 额外操作按钮插槽 -->
|
||||||
@@ -52,8 +55,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, reactive, watch } from "vue";
|
|
||||||
import { Button } from "@/components/ui";
|
import { Button } from "@/components/ui";
|
||||||
|
import { onMounted, reactive, watch } from "vue";
|
||||||
|
|
||||||
export interface FilterOption {
|
export interface FilterOption {
|
||||||
value: string | number | boolean;
|
value: string | number | boolean;
|
||||||
@@ -129,3 +132,36 @@ const handleSearch = () => {
|
|||||||
emit("search", { ...filterValues });
|
emit("search", { ...filterValues });
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 调整日期选择器的高度与其他输入框一致 */
|
||||||
|
.datepicker-container .dp__main {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker-container .dp__input {
|
||||||
|
height: 42px;
|
||||||
|
/* 与input的p-2.5相匹配 */
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
/* gray-300 */
|
||||||
|
background-color: #f9fafb;
|
||||||
|
/* gray-50 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker-container .dp__input:focus {
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-color: #3b82f6;
|
||||||
|
/* blue-500 */
|
||||||
|
box-shadow: 0 0 0 1px #3b82f6;
|
||||||
|
/* blue-500 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.datepicker-container .dp__input_icon {
|
||||||
|
right: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,78 +1,130 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="flex items-center flex-col md:flex-row flex-wrap justify-between py-4 px-3 sm:px-5"
|
<nav class="flex items-center flex-col md:flex-row flex-wrap justify-between py-4 px-3 sm:px-5"
|
||||||
aria-label="Table navigation">
|
aria-label="Table navigation">
|
||||||
<span class="text-xs sm:text-sm font-normal text-gray-500 mb-4 md:mb-0 block w-full md:inline md:w-auto">
|
<span class="text-xs sm:text-sm font-normal text-gray-500 mb-4 md:mb-0 block w-full md:inline md:w-auto">
|
||||||
显示
|
显示
|
||||||
<span class="font-semibold text-gray-900">
|
<span class="font-semibold text-gray-900">
|
||||||
{{ displayRange.start }}-{{ displayRange.end }}
|
{{ displayRange.start }}-{{ displayRange.end }}
|
||||||
</span>
|
</span>
|
||||||
共
|
共
|
||||||
<span class="font-semibold text-gray-900">{{ total }}</span> 条
|
<span class="font-semibold text-gray-900">{{ total }}</span> 条
|
||||||
</span>
|
</span>
|
||||||
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
|
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
|
||||||
<li>
|
<li>
|
||||||
<a href="#" @click.prevent="handlePageChangeClick(currentPage - 1)" :class="[
|
<a href="#" @click.prevent="handlePageChangeClick(currentPage - 1)" :class="[
|
||||||
'flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700',
|
'flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700',
|
||||||
{ 'opacity-50 cursor-not-allowed': isFirstPage }
|
{ 'opacity-50 cursor-not-allowed': isFirstPage }
|
||||||
]">上一页</a>
|
]">上一页</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-for="page in pageNumbers" :key="page">
|
<li v-for="(item, index) in visiblePageNumbers" :key="index">
|
||||||
<button @click.prevent="handlePageChangeClick(page)" :class="[
|
<template v-if="item.type === 'page'">
|
||||||
'flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 hover:bg-gray-100 hover:text-gray-700',
|
<button @click.prevent="item.page && handlePageChangeClick(item.page)" :class="[
|
||||||
currentPage === page
|
'flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 hover:bg-gray-100 hover:text-gray-700',
|
||||||
? 'text-blue-600 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 font-medium'
|
currentPage === item.page
|
||||||
: 'text-gray-500 bg-white'
|
? 'text-blue-600 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 font-medium'
|
||||||
]">{{ page }}</button>
|
: 'text-gray-500 bg-white'
|
||||||
</li>
|
]">{{ item.page }}</button>
|
||||||
|
</template>
|
||||||
<li>
|
<template v-else-if="item.type === 'ellipsis'">
|
||||||
<button @click.prevent="handlePageChangeClick(currentPage + 1)" :class="[
|
<span
|
||||||
|
class="flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 bg-white text-gray-500">
|
||||||
|
…
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button @click.prevent="handlePageChangeClick(currentPage + 1)" :class="[
|
||||||
'flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700',
|
'flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700',
|
||||||
{ 'opacity-50 cursor-not-allowed': isLastPage }
|
{ 'opacity-50 cursor-not-allowed': isLastPage }
|
||||||
]">下一页</button>
|
]">下一页</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePagination } from "@/composables/common/usePagination";
|
import { usePagination } from "@/composables/common/usePagination";
|
||||||
import { watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
const { pageChange, total } = defineProps<{
|
const props = defineProps<{
|
||||||
pageChange: (page: number, size: number) => Promise<void>;
|
pageChange?: (page: number, size: number) => Promise<void>;
|
||||||
total: number;
|
total: number;
|
||||||
|
currentPage?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
maxVisiblePages?: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'change-page': [page: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 创建一个本地的totalPages引用
|
||||||
|
const localTotalPages = ref<number | undefined>(props.totalPages);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentPage,
|
currentPage,
|
||||||
pageNumbers,
|
pageNumbers,
|
||||||
|
visiblePageNumbers,
|
||||||
pageSize,
|
pageSize,
|
||||||
displayRange,
|
displayRange,
|
||||||
isFirstPage,
|
isFirstPage,
|
||||||
isLastPage,
|
isLastPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
updatePaginationState,
|
updatePaginationState,
|
||||||
} = usePagination();
|
} = usePagination({
|
||||||
|
initialPage: props.currentPage,
|
||||||
|
initialTotal: props.total,
|
||||||
|
maxVisiblePages: props.maxVisiblePages || 7 // 默认显示7个页码
|
||||||
|
});
|
||||||
|
|
||||||
const handlePageChangeClick = async (page: number) => {
|
const handlePageChangeClick = async (page: number) => {
|
||||||
if (page < 1 || page > totalPages.value) return;
|
if (page < 1 || page > totalPages.value) return;
|
||||||
await pageChange(page, pageSize.value);
|
|
||||||
|
if (props.pageChange) {
|
||||||
|
// 如果传入了pageChange函数,则调用它
|
||||||
|
await props.pageChange(page, pageSize.value);
|
||||||
|
} else {
|
||||||
|
// 否则触发change-page事件
|
||||||
|
emit('change-page', page);
|
||||||
|
}
|
||||||
|
|
||||||
updatePaginationState({
|
updatePaginationState({
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
total,
|
total: props.total,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => total,
|
() => props.total,
|
||||||
() => {
|
(newTotal) => {
|
||||||
updatePaginationState({
|
updatePaginationState({
|
||||||
currentPage: currentPage.value,
|
currentPage: currentPage.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
total,
|
total: newTotal,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.currentPage,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal !== undefined && newVal !== currentPage.value) {
|
||||||
|
updatePaginationState({
|
||||||
|
currentPage: newVal,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
total: props.total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.totalPages,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal !== undefined) {
|
||||||
|
localTotalPages.value = newVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
101
frontend/src/composables/aop/useAopLogDelete.ts
Normal file
101
frontend/src/composables/aop/useAopLogDelete.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import client from "@/api/client";
|
||||||
|
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
|
import useAlertStore from "@/composables/store/useAlertStore";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AOP日志删除 Composable
|
||||||
|
* @returns 日志删除相关的状态和方法
|
||||||
|
*/
|
||||||
|
export function useAopLogDelete() {
|
||||||
|
const alertStore = useAlertStore();
|
||||||
|
const actionExcStore = useActionExcStore();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除单条日志
|
||||||
|
* @param id 日志ID
|
||||||
|
*/
|
||||||
|
const deleteLog = async (id: number) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
await client.DELETE("/aop-log/{id}", {
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
alertStore.showAlert({
|
||||||
|
level: "success",
|
||||||
|
content: "日志删除成功",
|
||||||
|
});
|
||||||
|
|
||||||
|
actionExcStore.notify(true);
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除日志
|
||||||
|
* @param ids 日志ID列表
|
||||||
|
*/
|
||||||
|
const batchDeleteLogs = async (ids: number[]) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const response = await client.DELETE("/aop-log/batch", {
|
||||||
|
body: ids,
|
||||||
|
});
|
||||||
|
|
||||||
|
alertStore.showAlert({
|
||||||
|
level: "success",
|
||||||
|
content: `成功删除 ${response.data || 0} 条日志`,
|
||||||
|
});
|
||||||
|
|
||||||
|
actionExcStore.notify(true);
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定时间前的日志
|
||||||
|
* @param beforeTime 时间点
|
||||||
|
*/
|
||||||
|
const deleteLogsBefore = async (beforeTime: string) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const response = await client.DELETE("/aop-log/before", {
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
beforeTime,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
alertStore.showAlert({
|
||||||
|
level: "success",
|
||||||
|
content: `成功删除 ${response.data || 0} 条日志`,
|
||||||
|
});
|
||||||
|
|
||||||
|
actionExcStore.notify(true);
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
deleteLog,
|
||||||
|
batchDeleteLogs,
|
||||||
|
deleteLogsBefore,
|
||||||
|
};
|
||||||
|
}
|
||||||
131
frontend/src/composables/aop/useAopLogQuery.ts
Normal file
131
frontend/src/composables/aop/useAopLogQuery.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import client from "@/api/client";
|
||||||
|
import type { components } from "@/api/types/schema";
|
||||||
|
import { useErrorHandling } from "@/composables/common/useErrorHandling";
|
||||||
|
import { usePagination } from "@/composables/common/usePagination";
|
||||||
|
import { useSorting } from "@/composables/common/useSorting";
|
||||||
|
import type { AopLogQueryParams } from "@/types/AlertTypes";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AOP日志查询 Composable
|
||||||
|
* @returns 日志查询相关的状态和方法
|
||||||
|
*/
|
||||||
|
export function useAopLogQuery() {
|
||||||
|
const { currentPage, pageSize, total, updatePaginationState } = usePagination();
|
||||||
|
const { sortBy, handleSort, getSortField } = useSorting();
|
||||||
|
|
||||||
|
const logs = ref<components["schemas"]["AopLogRespDto"][]>([]);
|
||||||
|
const currentLog = ref<components["schemas"]["AopLogRespDto"]>();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询日志列表
|
||||||
|
* @param params 查询参数
|
||||||
|
*/
|
||||||
|
const fetchLogs = async (params: AopLogQueryParams = {}) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
// 处理日期范围
|
||||||
|
const queryParams: AopLogQueryParams = { ...params };
|
||||||
|
|
||||||
|
const response = await client.GET("/aop-log/page-query", {
|
||||||
|
params: {
|
||||||
|
query: {
|
||||||
|
pageRequestDto: {
|
||||||
|
page: currentPage.value,
|
||||||
|
size: pageSize.value,
|
||||||
|
sortBy: sortBy.value,
|
||||||
|
},
|
||||||
|
queryDto: queryParams,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
logs.value = response.data.data || [];
|
||||||
|
updatePaginationState({ total: response.data.total || 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs.value;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单条日志详情
|
||||||
|
* @param id 日志ID
|
||||||
|
*/
|
||||||
|
const fetchLogDetail = async (id: number) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const response = await client.GET("/aop-log/{id}", {
|
||||||
|
params: {
|
||||||
|
path: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
currentLog.value = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentLog.value;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期时间
|
||||||
|
* @param dateTime 日期时间字符串
|
||||||
|
*/
|
||||||
|
const formatDateTime = (dateTime?: string) => {
|
||||||
|
if (!dateTime) return '';
|
||||||
|
return dayjs(dateTime).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化执行时间
|
||||||
|
* @param time 执行时间(毫秒)
|
||||||
|
*/
|
||||||
|
const formatExecutionTime = (time?: number) => {
|
||||||
|
if (!time) return '';
|
||||||
|
if (time < 1000) return `${time}ms`;
|
||||||
|
return `${(time / 1000).toFixed(2)}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化JSON字符串
|
||||||
|
* @param jsonString JSON字符串
|
||||||
|
*/
|
||||||
|
const formatJson = (jsonString?: string) => {
|
||||||
|
if (!jsonString) return '';
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(jsonString);
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
return jsonString;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
logs,
|
||||||
|
currentLog,
|
||||||
|
loading,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
fetchLogs,
|
||||||
|
fetchLogDetail,
|
||||||
|
handleSort,
|
||||||
|
getSortField,
|
||||||
|
formatDateTime,
|
||||||
|
formatExecutionTime,
|
||||||
|
formatJson,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,10 +7,11 @@ export interface PaginationState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UsePaginationOptions {
|
export interface UsePaginationOptions {
|
||||||
initialPage?: number;
|
initialPage?: number;
|
||||||
initialPageSize?: number;
|
initialPageSize?: number;
|
||||||
initialTotal?: number;
|
initialTotal?: number;
|
||||||
}
|
maxVisiblePages?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页逻辑Composable - 提供分页相关的状态和操作
|
* 分页逻辑Composable - 提供分页相关的状态和操作
|
||||||
@@ -18,7 +19,12 @@ export interface UsePaginationOptions {
|
|||||||
* @returns 分页状态和方法
|
* @returns 分页状态和方法
|
||||||
*/
|
*/
|
||||||
export function usePagination(options: UsePaginationOptions = {}) {
|
export function usePagination(options: UsePaginationOptions = {}) {
|
||||||
const { initialPage = 1, initialPageSize = 10, initialTotal = 0 } = options;
|
const {
|
||||||
|
initialPage = 1,
|
||||||
|
initialPageSize = 10,
|
||||||
|
initialTotal = 0,
|
||||||
|
maxVisiblePages = 5, // 最多显示的页码数量
|
||||||
|
} = options;
|
||||||
|
|
||||||
const currentPage = ref(initialPage);
|
const currentPage = ref(initialPage);
|
||||||
const pageSize = ref(initialPageSize);
|
const pageSize = ref(initialPageSize);
|
||||||
@@ -26,6 +32,7 @@ export function usePagination(options: UsePaginationOptions = {}) {
|
|||||||
|
|
||||||
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
|
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
|
||||||
|
|
||||||
|
// 所有页码
|
||||||
const pageNumbers = computed(() => {
|
const pageNumbers = computed(() => {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
for (let i = 1; i <= totalPages.value; i++) {
|
for (let i = 1; i <= totalPages.value; i++) {
|
||||||
@@ -34,6 +41,54 @@ export function usePagination(options: UsePaginationOptions = {}) {
|
|||||||
return pages;
|
return pages;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 智能分页显示,处理页码过多的情况
|
||||||
|
const visiblePageNumbers = computed(() => {
|
||||||
|
const totalPageCount = totalPages.value;
|
||||||
|
const current = currentPage.value;
|
||||||
|
|
||||||
|
// 如果总页数小于等于最大显示页数,则全部显示
|
||||||
|
if (totalPageCount <= maxVisiblePages) {
|
||||||
|
return pageNumbers.value.map((page) => ({ page, type: "page" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
const sidePages = Math.floor((maxVisiblePages - 3) / 2); // 当前页两侧显示的页数
|
||||||
|
|
||||||
|
// 添加第一页
|
||||||
|
result.push({ page: 1, type: "page" });
|
||||||
|
|
||||||
|
// 添加省略号或第二页
|
||||||
|
if (current - sidePages > 2) {
|
||||||
|
result.push({ page: null, type: "ellipsis" });
|
||||||
|
} else if (current > 1) {
|
||||||
|
result.push({ page: 2, type: "page" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加当前页附近的页码
|
||||||
|
const startPage = Math.max(current - sidePages, 2);
|
||||||
|
const endPage = Math.min(current + sidePages, totalPageCount - 1);
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
if (i > 2 && i < totalPageCount - 1) {
|
||||||
|
result.push({ page: i, type: "page" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加省略号或倒数第二页
|
||||||
|
if (current + sidePages < totalPageCount - 1) {
|
||||||
|
result.push({ page: null, type: "ellipsis" });
|
||||||
|
} else if (current < totalPageCount) {
|
||||||
|
result.push({ page: totalPageCount - 1, type: "page" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加最后一页
|
||||||
|
if (totalPageCount > 1) {
|
||||||
|
result.push({ page: totalPageCount, type: "page" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
const displayRange = computed(() => {
|
const displayRange = computed(() => {
|
||||||
const start =
|
const start =
|
||||||
total.value === 0 ? 0 : (currentPage.value - 1) * pageSize.value + 1;
|
total.value === 0 ? 0 : (currentPage.value - 1) * pageSize.value + 1;
|
||||||
@@ -64,6 +119,7 @@ export function usePagination(options: UsePaginationOptions = {}) {
|
|||||||
total,
|
total,
|
||||||
totalPages,
|
totalPages,
|
||||||
pageNumbers,
|
pageNumbers,
|
||||||
|
visiblePageNumbers,
|
||||||
displayRange,
|
displayRange,
|
||||||
isFirstPage,
|
isFirstPage,
|
||||||
isLastPage,
|
isLastPage,
|
||||||
|
|||||||
@@ -33,5 +33,9 @@ enableMocking().then(() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
app.component("VueDatePicker", VueDatePicker);
|
app.component("VueDatePicker", VueDatePicker);
|
||||||
|
app.provide("vueDatePickerOptions", {
|
||||||
|
teleport: "body",
|
||||||
|
teleportCenter: true,
|
||||||
|
});
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -176,11 +176,31 @@ export const AiRoutes = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
// 系统管理相关路由
|
||||||
|
export const SystemRoutes = {
|
||||||
|
AOPLOGVIEW: {
|
||||||
|
path: "aop-logs",
|
||||||
|
name: "aop-logs",
|
||||||
|
fullPath: () => `${BaseRoutes.DASHBOARD.path}/aop-logs`,
|
||||||
|
withParams: () => ({ name: "aop-logs" }),
|
||||||
|
},
|
||||||
|
AOPLOGDETAILVIEW: {
|
||||||
|
path: "aop-logs/:id",
|
||||||
|
name: "aop-log-detail",
|
||||||
|
fullPath: () => `${BaseRoutes.DASHBOARD.path}/aop-logs/:id`,
|
||||||
|
withParams: <T extends { id: string | number }>(params: T) => ({
|
||||||
|
name: "aop-log-detail",
|
||||||
|
params: { id: params.id.toString() },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const Routes = {
|
export const Routes = {
|
||||||
...BaseRoutes,
|
...BaseRoutes,
|
||||||
...DashboardRoutes,
|
...DashboardRoutes,
|
||||||
...UserRoutes,
|
...UserRoutes,
|
||||||
...AiRoutes,
|
...AiRoutes,
|
||||||
|
...SystemRoutes,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export enum ERole {
|
export enum ERole {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { RouteRecordRaw } from "vue-router";
|
|||||||
import Dashboard from "../../components/layout/Dashboard.vue";
|
import Dashboard from "../../components/layout/Dashboard.vue";
|
||||||
import { EPermission, Routes } from "../constants";
|
import { EPermission, Routes } from "../constants";
|
||||||
import aiRoutes from "./ai";
|
import aiRoutes from "./ai";
|
||||||
|
import systemRoutes from "./system";
|
||||||
import userManagementRoutes from "./user";
|
import userManagementRoutes from "./user";
|
||||||
|
|
||||||
const dashboardRoutes: RouteRecordRaw = {
|
const dashboardRoutes: RouteRecordRaw = {
|
||||||
@@ -14,6 +15,7 @@ const dashboardRoutes: RouteRecordRaw = {
|
|||||||
children: [
|
children: [
|
||||||
...userManagementRoutes,
|
...userManagementRoutes,
|
||||||
...aiRoutes,
|
...aiRoutes,
|
||||||
|
...systemRoutes,
|
||||||
{
|
{
|
||||||
path: Routes.OVERVIEW.path,
|
path: Routes.OVERVIEW.path,
|
||||||
name: Routes.OVERVIEW.name,
|
name: Routes.OVERVIEW.name,
|
||||||
|
|||||||
23
frontend/src/router/modules/system.ts
Normal file
23
frontend/src/router/modules/system.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
|
import { Routes } from "../constants";
|
||||||
|
|
||||||
|
const systemRoutes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: Routes.AOPLOGVIEW.path,
|
||||||
|
name: Routes.AOPLOGVIEW.name,
|
||||||
|
component: () => import("@/views/AopLogManagementPage.vue"),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: Routes.AOPLOGDETAILVIEW.path,
|
||||||
|
name: Routes.AOPLOGDETAILVIEW.name,
|
||||||
|
component: () => import("@/views/AopLogDetailPage.vue"),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default systemRoutes;
|
||||||
@@ -1 +1,31 @@
|
|||||||
export type AlertLevel = "info" | "warning" | "success" | "error";
|
export type AlertLevel = "info" | "success" | "warning" | "error";
|
||||||
|
|
||||||
|
export interface AopLogQueryParams {
|
||||||
|
id?: number;
|
||||||
|
className?: string;
|
||||||
|
methodName?: string;
|
||||||
|
success?: boolean;
|
||||||
|
userId?: number;
|
||||||
|
ipAddress?: string;
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
minExecutionTime?: number;
|
||||||
|
maxExecutionTime?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AopLog {
|
||||||
|
id?: number;
|
||||||
|
className?: string;
|
||||||
|
methodName?: string;
|
||||||
|
methodArgs?: string;
|
||||||
|
returnValue?: string;
|
||||||
|
executionTime?: number;
|
||||||
|
success?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
userId?: number;
|
||||||
|
username?: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
curl?: string;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
|||||||
300
frontend/src/views/AopLogDetailPage.vue
Normal file
300
frontend/src/views/AopLogDetailPage.vue
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
<template>
|
||||||
|
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
|
||||||
|
<Breadcrumbs :names="['日志管理', '日志详情']" :routes="[Routes.AOPLOGVIEW.fullPath()]" />
|
||||||
|
<div class="mb-4 flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">日志详情</h1>
|
||||||
|
<div class="mt-2 sm:mt-0">
|
||||||
|
<Button variant="secondary" size="sm" @click="navigateBack">
|
||||||
|
<template #icon>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
返回日志列表
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading" class="flex justify-center items-center py-10">
|
||||||
|
<div class="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日志详情内容 -->
|
||||||
|
<div v-else-if="currentLog" class="bg-white shadow rounded-lg">
|
||||||
|
<!-- curl命令 -->
|
||||||
|
<div v-if="currentLog.curl" class="p-4 border-b border-gray-200">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-lg font-medium text-gray-900">CURL</h2>
|
||||||
|
<button
|
||||||
|
@click="copyCurl"
|
||||||
|
class="text-sm font-medium text-blue-600 hover:text-blue-800 flex items-center"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
{{ isCopied ? '已复制' : '复制' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<pre class="text-sm text-gray-800 overflow-x-auto whitespace-pre-wrap">{{ currentLog.curl }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="p-4 border-b border-gray-200">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-lg font-medium text-gray-900">基本信息</h2>
|
||||||
|
<LogStatusBadge :success="currentLog.success || false" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-500">类名</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-900">{{ currentLog.className }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-500">方法名</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-900">{{ currentLog.methodName }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-500">执行时间</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-900">{{ formatExecutionTime(currentLog.executionTime) }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-500">用户名</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-900">{{ currentLog.username || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-500">用户ID</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-900">{{ currentLog.userId || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-500">IP地址</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-900">{{ currentLog.ipAddress || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-500">创建时间</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-900">{{ formatDateTime(currentLog.createTime) }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-medium text-gray-500">User Agent</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-900 truncate" :title="currentLog.userAgent">
|
||||||
|
{{ currentLog.userAgent || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 方法参数 -->
|
||||||
|
<div class="p-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-lg font-medium text-gray-900 mb-4">方法参数</h2>
|
||||||
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<pre class="text-sm text-gray-800 overflow-x-auto whitespace-pre-wrap">{{ isMethodArgsExpanded ? formatJson(currentLog.methodArgs) : methodArgsPreview }}</pre>
|
||||||
|
<div v-if="shouldCollapseMethodArgs" class="mt-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
@click="isMethodArgsExpanded = !isMethodArgsExpanded"
|
||||||
|
class="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
||||||
|
>
|
||||||
|
{{ isMethodArgsExpanded ? '折叠' : '展开' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 返回值 -->
|
||||||
|
<div class="p-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-lg font-medium text-gray-900 mb-4">返回值</h2>
|
||||||
|
<div class="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<pre class="text-sm text-gray-800 overflow-x-auto whitespace-pre-wrap">{{ isReturnValueExpanded ? formatJson(currentLog.returnValue) : returnValuePreview }}</pre>
|
||||||
|
<div v-if="shouldCollapseReturnValue" class="mt-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
@click="isReturnValueExpanded = !isReturnValueExpanded"
|
||||||
|
class="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
||||||
|
>
|
||||||
|
{{ isReturnValueExpanded ? '折叠' : '展开' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误信息 -->
|
||||||
|
<div v-if="currentLog.errorMessage" class="p-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-lg font-medium text-gray-900 mb-4">错误信息</h2>
|
||||||
|
<div class="bg-red-50 p-4 rounded-lg">
|
||||||
|
<pre class="text-sm text-red-800 overflow-x-auto whitespace-pre-wrap">{{ isErrorMessageExpanded ? currentLog.errorMessage : errorMessagePreview }}</pre>
|
||||||
|
<div v-if="shouldCollapseErrorMessage" class="mt-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
@click="isErrorMessageExpanded = !isErrorMessageExpanded"
|
||||||
|
class="text-blue-600 hover:text-blue-800 text-sm font-medium"
|
||||||
|
>
|
||||||
|
{{ isErrorMessageExpanded ? '折叠' : '展开' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<div class="p-4 bg-gray-50 rounded-b-lg flex justify-end">
|
||||||
|
<Button variant="danger" @click="handleDeleteClick">
|
||||||
|
<template #icon>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
删除日志
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<div v-else class="flex flex-col items-center justify-center py-10">
|
||||||
|
<div class="text-gray-500 text-lg mb-4">未找到日志详情</div>
|
||||||
|
<Button variant="secondary" @click="navigateBack">返回日志列表</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
|
<ConfirmationDialog id="delete-log-modal" title="删除日志" content="确定要删除此日志吗?此操作不可撤销。"
|
||||||
|
:closeModal="() => deleteLogModal?.hide()" :onSubmit="confirmDelete" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
|
import LogStatusBadge from "@/components/common/LogStatusBadge.vue";
|
||||||
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
|
import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
|
import { useAopLogDelete } from "@/composables/aop/useAopLogDelete";
|
||||||
|
import { useAopLogQuery } from "@/composables/aop/useAopLogQuery";
|
||||||
|
import { useErrorHandling } from "@/composables/common/useErrorHandling";
|
||||||
|
import { Routes } from "@/router/constants";
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const logId = Number(route.params.id);
|
||||||
|
|
||||||
|
// 获取错误处理
|
||||||
|
const { handleError } = useErrorHandling();
|
||||||
|
|
||||||
|
// 获取日志查询和删除的composables
|
||||||
|
const {
|
||||||
|
currentLog,
|
||||||
|
loading,
|
||||||
|
fetchLogDetail,
|
||||||
|
formatDateTime,
|
||||||
|
formatExecutionTime,
|
||||||
|
formatJson,
|
||||||
|
} = useAopLogQuery();
|
||||||
|
|
||||||
|
const { deleteLog } = useAopLogDelete();
|
||||||
|
|
||||||
|
// 模态框引用
|
||||||
|
const deleteLogModal = ref<ModalInterface>();
|
||||||
|
|
||||||
|
// 内容折叠状态
|
||||||
|
const isMethodArgsExpanded = ref(false);
|
||||||
|
const isReturnValueExpanded = ref(false);
|
||||||
|
const isErrorMessageExpanded = ref(false);
|
||||||
|
|
||||||
|
// 复制状态
|
||||||
|
const isCopied = ref(false);
|
||||||
|
|
||||||
|
// 计算预览内容
|
||||||
|
const previewLength = 300;
|
||||||
|
|
||||||
|
const methodArgsPreview = computed(() => {
|
||||||
|
const content = formatJson(currentLog.value?.methodArgs);
|
||||||
|
return content && content.length > previewLength
|
||||||
|
? `${content.substring(0, previewLength)}...`
|
||||||
|
: content;
|
||||||
|
});
|
||||||
|
|
||||||
|
const returnValuePreview = computed(() => {
|
||||||
|
const content = formatJson(currentLog.value?.returnValue);
|
||||||
|
return content && content.length > previewLength
|
||||||
|
? `${content.substring(0, previewLength)}...`
|
||||||
|
: content;
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorMessagePreview = computed(() => {
|
||||||
|
const content = currentLog.value?.errorMessage;
|
||||||
|
return content && content.length > previewLength
|
||||||
|
? `${content.substring(0, previewLength)}...`
|
||||||
|
: content;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 判断是否需要折叠
|
||||||
|
const shouldCollapseMethodArgs = computed(() => {
|
||||||
|
const content = formatJson(currentLog.value?.methodArgs);
|
||||||
|
return content && content.length > previewLength;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldCollapseReturnValue = computed(() => {
|
||||||
|
const content = formatJson(currentLog.value?.returnValue);
|
||||||
|
return content && content.length > previewLength;
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldCollapseErrorMessage = computed(() => {
|
||||||
|
const content = currentLog.value?.errorMessage;
|
||||||
|
return content && content.length > previewLength;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 复制CURL命令
|
||||||
|
const copyCurl = () => {
|
||||||
|
if (currentLog.value?.curl) {
|
||||||
|
navigator.clipboard.writeText(currentLog.value.curl);
|
||||||
|
isCopied.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
isCopied.value = false;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回日志列表
|
||||||
|
const navigateBack = () => {
|
||||||
|
router.push(Routes.AOPLOGVIEW.fullPath());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理删除点击
|
||||||
|
const handleDeleteClick = () => {
|
||||||
|
deleteLogModal.value?.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
try {
|
||||||
|
if (currentLog.value?.id) {
|
||||||
|
await deleteLog(currentLog.value.id);
|
||||||
|
navigateBack();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 初始化Flowbite
|
||||||
|
initFlowbite();
|
||||||
|
|
||||||
|
// 初始化模态框
|
||||||
|
const $deleteModalElement = document.querySelector<HTMLElement>("#delete-log-modal");
|
||||||
|
if ($deleteModalElement) {
|
||||||
|
deleteLogModal.value = new Modal($deleteModalElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载日志详情
|
||||||
|
await fetchLogDetail(logId);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
482
frontend/src/views/AopLogManagementPage.vue
Normal file
482
frontend/src/views/AopLogManagementPage.vue
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
<template>
|
||||||
|
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
|
||||||
|
<div class="mb-4 col-span-full">
|
||||||
|
<Breadcrumbs :names="['日志管理']" />
|
||||||
|
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">日志管理</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 筛选表单 -->
|
||||||
|
<TableFilterForm :filters="filterConfig" :initialValues="filterValues" @search="handleSearch"
|
||||||
|
@update:values="updateFilterValues">
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<TableButton variant="danger" @click="handleBatchDeleteClick()" :disabled="selectedLogs.length === 0"
|
||||||
|
class="w-full sm:w-auto">
|
||||||
|
<template #icon>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
批量删除
|
||||||
|
</TableButton>
|
||||||
|
<TableButton variant="info" @click="handleClearBeforeClick()" class="w-full sm:w-auto">
|
||||||
|
<template #icon>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
清理历史日志
|
||||||
|
</TableButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TableFilterForm>
|
||||||
|
|
||||||
|
<!-- PC端表格 -->
|
||||||
|
<div class="hidden md:block mt-4">
|
||||||
|
<TableFormLayout :items="logs" :columns="columns" :loading="loading" :hasCheckbox="true" v-model="selectedLogs"
|
||||||
|
@all-checked-change="handleSelectAll">
|
||||||
|
<template #className="{ item }">
|
||||||
|
<div class="truncate max-w-xs" :title="item.className">{{ item.className }}</div>
|
||||||
|
</template>
|
||||||
|
<template #methodName="{ item }">
|
||||||
|
{{ item.methodName }}
|
||||||
|
</template>
|
||||||
|
<template #executionTime="{ item }">
|
||||||
|
{{ formatExecutionTime(item.executionTime) }}
|
||||||
|
</template>
|
||||||
|
<template #success="{ item }">
|
||||||
|
<LogStatusBadge :success="item.success || false" />
|
||||||
|
</template>
|
||||||
|
<template #username="{ item }">
|
||||||
|
{{ item.username || '-' }}
|
||||||
|
</template>
|
||||||
|
<template #createTime="{ item }">
|
||||||
|
{{ formatDateTime(item.createTime) }}
|
||||||
|
</template>
|
||||||
|
<template #actions="{ item }">
|
||||||
|
<div class="flex items-center gap-x-2">
|
||||||
|
<TableButton variant="primary" @click="handleViewDetail(item)">
|
||||||
|
<template #icon>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
查看
|
||||||
|
</TableButton>
|
||||||
|
<TableButton variant="danger" @click="handleDeleteClick(item)">
|
||||||
|
<template #icon>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
删除
|
||||||
|
</TableButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</TableFormLayout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 移动端卡片列表 -->
|
||||||
|
<div class="md:hidden mt-4">
|
||||||
|
<MobileCardListWithCheckbox :items="logs" v-model="selectedLogs" idField="id">
|
||||||
|
<template #item="{ item }">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="font-medium text-gray-900 truncate max-w-[200px]" :title="item.className">
|
||||||
|
{{ item.className }}
|
||||||
|
</div>
|
||||||
|
<LogStatusBadge :success="item.success || false" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<div class="text-gray-500">方法</div>
|
||||||
|
<div>{{ item.methodName }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-gray-500">执行时间</div>
|
||||||
|
<div>{{ formatExecutionTime(item.executionTime) }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-gray-500">用户</div>
|
||||||
|
<div>{{ item.username || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-gray-500">创建时间</div>
|
||||||
|
<div>{{ formatDateTime(item.createTime) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2 mt-2">
|
||||||
|
<TableButton variant="primary" size="xs" @click="handleViewDetail(item)">
|
||||||
|
<template #icon>
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
查看
|
||||||
|
</TableButton>
|
||||||
|
<TableButton variant="danger" size="xs" @click="handleDeleteClick(item)">
|
||||||
|
<template #icon>
|
||||||
|
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
删除
|
||||||
|
</TableButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MobileCardListWithCheckbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<TablePagination :current-page="currentPage" :total-pages="Math.ceil(total / pageSize)" :total="total"
|
||||||
|
@change-page="handlePageChange" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 删除确认对话框 -->
|
||||||
|
<ConfirmationDialog id="delete-log-modal" title="删除日志" content="确定要删除选中的日志吗?此操作不可撤销。"
|
||||||
|
:closeModal="() => deleteLogModal?.hide()" :onSubmit="confirmDelete" />
|
||||||
|
|
||||||
|
<!-- 批量删除确认对话框 -->
|
||||||
|
<ConfirmationDialog id="batch-delete-logs-modal" title="批量删除日志"
|
||||||
|
:content="`确定要删除选中的 ${selectedLogs.length} 条日志吗?此操作不可撤销。`" :closeModal="() => batchDeleteLogsModal?.hide()"
|
||||||
|
:onSubmit="confirmBatchDelete" />
|
||||||
|
|
||||||
|
<!-- 清理历史日志对话框 -->
|
||||||
|
<BaseDialog id="clear-before-modal" title="清理历史日志" :closeModal="closeClearBeforeModal">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="clearBeforeDate" class="block mb-2 text-sm font-medium text-gray-900">
|
||||||
|
删除此日期之前的所有日志
|
||||||
|
</label>
|
||||||
|
<div class="datepicker-container">
|
||||||
|
<VueDatePicker v-model="clearBeforeDate" locale="zh-CN" format="yyyy-MM-dd" :enable-time-picker="false"
|
||||||
|
:auto-apply="true" class="filter-datepicker" teleport="body" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button type="button" @click="closeClearBeforeModal"
|
||||||
|
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="confirmClearBefore"
|
||||||
|
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5">
|
||||||
|
确认清理
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Modal, type ModalInterface, initFlowbite } from "flowbite";
|
||||||
|
import { onMounted, reactive, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
import LogStatusBadge from "@/components/common/LogStatusBadge.vue";
|
||||||
|
import Breadcrumbs from "@/components/layout/Breadcrumbs.vue";
|
||||||
|
import BaseDialog from "@/components/modals/BaseDialog.vue";
|
||||||
|
import ConfirmationDialog from "@/components/modals/ConfirmationDialog.vue";
|
||||||
|
import MobileCardListWithCheckbox from "@/components/tables/MobileCardListWithCheckbox.vue";
|
||||||
|
import TableButton from "@/components/tables/TableButton.vue";
|
||||||
|
import TableFilterForm from "@/components/tables/TableFilterForm.vue";
|
||||||
|
import type { FilterItem } from "@/components/tables/TableFilterForm.vue";
|
||||||
|
import TableFormLayout from "@/components/tables/TableFormLayout.vue";
|
||||||
|
import TablePagination from "@/components/tables/TablePagination.vue";
|
||||||
|
|
||||||
|
import type { components } from "@/api/types/schema";
|
||||||
|
import { useAopLogDelete } from "@/composables/aop/useAopLogDelete";
|
||||||
|
import { useAopLogQuery } from "@/composables/aop/useAopLogQuery";
|
||||||
|
import { useErrorHandling } from "@/composables/common/useErrorHandling";
|
||||||
|
import { useActionExcStore } from "@/composables/store/useActionExcStore";
|
||||||
|
import { Routes } from "@/router/constants";
|
||||||
|
import type { AopLogQueryParams } from "@/types/AlertTypes";
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 获取错误处理
|
||||||
|
const { handleError } = useErrorHandling();
|
||||||
|
|
||||||
|
// 获取日志查询和删除的composables
|
||||||
|
const {
|
||||||
|
logs,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
loading,
|
||||||
|
fetchLogs,
|
||||||
|
formatDateTime,
|
||||||
|
formatExecutionTime,
|
||||||
|
handleSort,
|
||||||
|
getSortField,
|
||||||
|
} = useAopLogQuery();
|
||||||
|
|
||||||
|
const { deleteLog, batchDeleteLogs, deleteLogsBefore } = useAopLogDelete();
|
||||||
|
|
||||||
|
// 选中的日志
|
||||||
|
const selectedLogs = ref<number[]>([]);
|
||||||
|
|
||||||
|
// 清理历史日志的日期
|
||||||
|
const clearBeforeDate = ref(new Date());
|
||||||
|
|
||||||
|
// 模态框引用
|
||||||
|
const deleteLogModal = ref<ModalInterface>();
|
||||||
|
const batchDeleteLogsModal = ref<ModalInterface>();
|
||||||
|
const clearBeforeModal = ref<ModalInterface>();
|
||||||
|
|
||||||
|
// 当前要删除的日志
|
||||||
|
const currentLogToDelete = ref<components["schemas"]["AopLogRespDto"]>();
|
||||||
|
|
||||||
|
// 筛选配置
|
||||||
|
const filterConfig = [
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "className",
|
||||||
|
placeholder: "类名",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "methodName",
|
||||||
|
placeholder: "方法名",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "select",
|
||||||
|
name: "success",
|
||||||
|
placeholder: "状态",
|
||||||
|
options: [
|
||||||
|
{ value: "", label: "全部" },
|
||||||
|
{ value: "true", label: "成功" },
|
||||||
|
{ value: "false", label: "失败" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "input",
|
||||||
|
name: "username",
|
||||||
|
placeholder: "用户名",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "date-range",
|
||||||
|
name: "dateRange",
|
||||||
|
},
|
||||||
|
] as FilterItem[];
|
||||||
|
|
||||||
|
// 筛选值
|
||||||
|
const filterValues = reactive<{
|
||||||
|
className: string;
|
||||||
|
methodName: string;
|
||||||
|
success: string;
|
||||||
|
username: string;
|
||||||
|
dateRange: Date[];
|
||||||
|
}>({
|
||||||
|
className: "",
|
||||||
|
methodName: "",
|
||||||
|
success: "",
|
||||||
|
username: "",
|
||||||
|
dateRange: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = [
|
||||||
|
{ title: "类名", field: "className", sortable: true },
|
||||||
|
{ title: "方法名", field: "methodName", sortable: true },
|
||||||
|
{ title: "执行时间", field: "executionTime", sortable: true },
|
||||||
|
{ title: "状态", field: "success" },
|
||||||
|
{ title: "用户", field: "username" },
|
||||||
|
{ title: "创建时间", field: "createTime", sortable: true },
|
||||||
|
{ title: "操作", field: "actions" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 更新筛选值
|
||||||
|
const updateFilterValues = (
|
||||||
|
values: Record<string, string | number | boolean | Date[] | undefined>
|
||||||
|
) => {
|
||||||
|
if (values.className !== undefined) {
|
||||||
|
filterValues.className = values.className as string;
|
||||||
|
}
|
||||||
|
if (values.methodName !== undefined) {
|
||||||
|
filterValues.methodName = values.methodName as string;
|
||||||
|
}
|
||||||
|
if (values.success !== undefined) {
|
||||||
|
filterValues.success = values.success as string;
|
||||||
|
}
|
||||||
|
if (values.username !== undefined) {
|
||||||
|
filterValues.username = values.username as string;
|
||||||
|
}
|
||||||
|
if (values.dateRange !== undefined) {
|
||||||
|
filterValues.dateRange = values.dateRange as Date[];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = async () => {
|
||||||
|
try {
|
||||||
|
const params: AopLogQueryParams = {
|
||||||
|
className: filterValues.className || undefined,
|
||||||
|
methodName: filterValues.methodName || undefined,
|
||||||
|
success: filterValues.success ? filterValues.success === "true" : undefined,
|
||||||
|
username: filterValues.username || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理日期范围
|
||||||
|
if (filterValues.dateRange && filterValues.dateRange.length === 2) {
|
||||||
|
params.startTime = filterValues.dateRange[0].toISOString();
|
||||||
|
params.endTime = filterValues.dateRange[1].toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchLogs(params);
|
||||||
|
selectedLogs.value = [];
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理页码变化
|
||||||
|
const handlePageChange = async (page: number) => {
|
||||||
|
try {
|
||||||
|
currentPage.value = page;
|
||||||
|
await handleSearch();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理查看详情
|
||||||
|
const handleViewDetail = (log: components["schemas"]["AopLogRespDto"]) => {
|
||||||
|
if (log.id) {
|
||||||
|
router.push(Routes.AOPLOGDETAILVIEW.withParams({ id: log.id }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理删除点击
|
||||||
|
const handleDeleteClick = (log: components["schemas"]["AopLogRespDto"]) => {
|
||||||
|
currentLogToDelete.value = log;
|
||||||
|
deleteLogModal.value?.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认删除
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
try {
|
||||||
|
if (currentLogToDelete.value?.id) {
|
||||||
|
await deleteLog(currentLogToDelete.value.id);
|
||||||
|
await handleSearch();
|
||||||
|
deleteLogModal.value?.hide();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理批量删除点击
|
||||||
|
const handleBatchDeleteClick = () => {
|
||||||
|
if (selectedLogs.value.length > 0) {
|
||||||
|
batchDeleteLogsModal.value?.show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认批量删除
|
||||||
|
const confirmBatchDelete = async () => {
|
||||||
|
try {
|
||||||
|
if (selectedLogs.value.length > 0) {
|
||||||
|
await batchDeleteLogs(selectedLogs.value);
|
||||||
|
await handleSearch();
|
||||||
|
batchDeleteLogsModal.value?.hide();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理清理历史日志点击
|
||||||
|
const handleClearBeforeClick = () => {
|
||||||
|
clearBeforeModal.value?.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭清理历史日志对话框
|
||||||
|
const closeClearBeforeModal = () => {
|
||||||
|
clearBeforeModal.value?.hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认清理历史日志
|
||||||
|
const confirmClearBefore = async () => {
|
||||||
|
try {
|
||||||
|
if (clearBeforeDate.value) {
|
||||||
|
const dateString = clearBeforeDate.value.toISOString().split("T")[0];
|
||||||
|
await deleteLogsBefore(dateString);
|
||||||
|
await handleSearch();
|
||||||
|
clearBeforeModal.value?.hide();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理全选
|
||||||
|
const handleSelectAll = (selected: boolean) => {
|
||||||
|
if (selected) {
|
||||||
|
selectedLogs.value = logs.value.map(log => log.id).filter((id): id is number => id !== undefined);
|
||||||
|
} else {
|
||||||
|
selectedLogs.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
const actionExcStore = useActionExcStore();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 初始化Flowbite
|
||||||
|
initFlowbite();
|
||||||
|
|
||||||
|
// 初始化模态框
|
||||||
|
const $deleteModalElement = document.querySelector<HTMLElement>("#delete-log-modal");
|
||||||
|
const $batchDeleteModalElement = document.querySelector<HTMLElement>("#batch-delete-logs-modal");
|
||||||
|
const $clearBeforeModalElement = document.querySelector<HTMLElement>("#clear-before-modal");
|
||||||
|
|
||||||
|
if ($deleteModalElement) {
|
||||||
|
deleteLogModal.value = new Modal($deleteModalElement);
|
||||||
|
}
|
||||||
|
if ($batchDeleteModalElement) {
|
||||||
|
batchDeleteLogsModal.value = new Modal($batchDeleteModalElement);
|
||||||
|
}
|
||||||
|
if ($clearBeforeModalElement) {
|
||||||
|
clearBeforeModal.value = new Modal($clearBeforeModalElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载日志数据
|
||||||
|
await fetchLogs();
|
||||||
|
|
||||||
|
// 设置刷新回调
|
||||||
|
actionExcStore.setCallback((result) => {
|
||||||
|
if (result) {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user