11 Commits
dev ... main

21 changed files with 62 additions and 216 deletions

View File

@@ -30,31 +30,27 @@
- [🍑 更多](#-更多) - [🍑 更多](#-更多)
- [🍒 部分技术选型](#-部分技术选型) - [🍒 部分技术选型](#-部分技术选型)
- [🔮 防失联,关注各大社区账号](#-防失联关注各大社区账号) - [🔮 防失联,关注各大社区账号](#-防失联关注各大社区账号)
- [💌 微信打赏](#-微信打赏)
## 🥝 产品社群 ## 🥝 产品社群
**加 QQ 群或微信群立送以下装备,瞬间秒杀全服!!**
1. 一键部署脚本(包含数据库 Redis 消息队列等所有中间件!) 1. 一键部署脚本(包含数据库 Redis 消息队列等所有中间件!)
2. 永久免费的 Https 证书 2. 永久免费的 Https 证书
3. 永久免费的分布式对象存储 3. 永久免费的分布式对象存储
4. 永久免费的 AI 模型 4. 永久免费的 AI 模型
5. 永久免费的 Node、Docker、Maven 国内镜像仓库 5. 永久免费的 Node、Docker、Maven 国内镜像仓库
![group](assets/group.png)
[![点击按钮加入 QQ群](https://img.shields.io/badge/-white?style=social&logo=QQ&label=或点击按钮加入QQ群)](https://qm.qq.com/q/9mvVC57jPO) [![点击按钮加入 QQ群](https://img.shields.io/badge/-white?style=social&logo=QQ&label=或点击按钮加入QQ群)](https://qm.qq.com/q/9mvVC57jPO)
- QQ群638254979(目前人较多) - QQ群638254979
- 微信Chuck9996(若微信群已过期可以加我 vx)
## 🍅 相关课程 ## 🍅 相关课程
已上线: 已上线:
- [国内首个无幻觉式 AI 编程指南](https://www.bilibili.com/cheese/play/ep1615343) - [AI 时代的 Java 测试驱动开发](https://www.bilibili.com/cheese/play/ep1615343)
敬请期待:(加群获取) 敬请期待:(加群获取)
@@ -209,9 +205,3 @@
[![Github](https://img.shields.io/badge/-white?style=social&logo=github&label=github)](https://github.com/ccmjga) [![Github](https://img.shields.io/badge/-white?style=social&logo=github&label=github)](https://github.com/ccmjga)
[![QQ](https://img.shields.io/badge/-white?style=social&logo=QQ&label=QQ群)](https://qm.qq.com/q/9mvVC57jPO) [![QQ](https://img.shields.io/badge/-white?style=social&logo=QQ&label=QQ群)](https://qm.qq.com/q/9mvVC57jPO)
## 💌 微信打赏
知路管理后台的发展离不开您的支持;再次对所有支持本项目的人们致以诚挚的谢意~
![pay](/assets/pay.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 KiB

After

Width:  |  Height:  |  Size: 580 KiB

View File

@@ -102,14 +102,14 @@ tasks.jacocoTestReport {
} }
jacoco { jacoco {
toolVersion = "0.8.12" toolVersion = "0.8.13"
reportsDirectory.set(layout.buildDirectory.dir("reports/jacoco")) reportsDirectory.set(layout.buildDirectory.dir("reports/jacoco"))
} }
pmd { pmd {
sourceSets = listOf(java.sourceSets.findByName("main")) sourceSets = listOf(java.sourceSets.findByName("main"))
isConsoleOutput = true isConsoleOutput = true
toolVersion = "7.9.0" toolVersion = "7.15.0"
rulesMinimumPriority.set(5) rulesMinimumPriority.set(5)
ruleSetFiles = files("pmd-rules.xml") ruleSetFiles = files("pmd-rules.xml")
} }
@@ -125,7 +125,7 @@ spotless {
} }
java { java {
googleJavaFormat("1.25.2").reflowLongStrings() googleJavaFormat("1.28.0").reflowLongStrings()
formatAnnotations() formatAnnotations()
} }

View File

@@ -6,40 +6,10 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
/**
* 跳过AOP日志记录注解
*
* <p>在方法上添加此注解该方法将不会被AOP日志切面拦截和记录。
*
* <p>使用场景:
*
* <ul>
* <li>敏感操作方法,不希望记录日志
* <li>高频调用方法,避免产生过多日志
* <li>内部工具方法,不需要业务日志记录
* </ul>
*
* <p>使用示例:
*
* <pre>{@code
* @SkipAopLog
* public void sensitiveMethod() {
* // 此方法不会被AOP日志记录
* }
* }</pre>
*
* @author AOP Log System
* @since 1.0
*/
@Target(ElementType.METHOD) @Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Documented
public @interface SkipAopLog { public @interface SkipAopLog {
/**
* 跳过日志记录的原因说明(可选)
*
* @return 跳过原因
*/
String reason() default ""; String reason() default "";
} }

View File

@@ -48,11 +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);
// } // }
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()) {

View File

@@ -1,8 +1,6 @@
package com.zl.mjga.dto.aoplog; package com.zl.mjga.dto.aoplog;
import java.time.LocalDateTime;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;

View File

@@ -47,14 +47,16 @@ public class AopLogRepository extends AopLogDao {
.where(buildConditions(queryDto)); .where(buildConditions(queryDto));
} }
public SelectConditionStep<Record> selectByWithoutReturnValue(AopLogQueryDto queryDto) { public SelectConditionStep<Record> selectByWithoutReturnValue(AopLogQueryDto queryDto) {
return ctx() return ctx()
.select(AOP_LOG.asterisk().except(AOP_LOG.RETURN_VALUE, AOP_LOG.METHOD_ARGS), USER.USERNAME, DSL.count().over().as("total_count")) .select(
.from(AOP_LOG) AOP_LOG.asterisk().except(AOP_LOG.RETURN_VALUE, AOP_LOG.METHOD_ARGS),
.leftJoin(USER) USER.USERNAME,
.on(AOP_LOG.USER_ID.eq(USER.ID)) DSL.count().over().as("total_count"))
.where(buildConditions(queryDto)); .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) {

View File

@@ -53,7 +53,7 @@ public class SignE2ETest {
.uri("/auth/sign-up") .uri("/auth/sign-up")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.bodyValue( .bodyValue(
""" """
{ {
"username": "test_5fab32c22a3e", "username": "test_5fab32c22a3e",
"password": "test_eab28b939ba1" "password": "test_eab28b939ba1"
@@ -75,7 +75,7 @@ public class SignE2ETest {
.uri("/auth/sign-in") .uri("/auth/sign-in")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.bodyValue( .bodyValue(
""" """
{ {
"username": "test_5fab32c22a3e", "username": "test_5fab32c22a3e",
"password": "test_eab28b939ba1" "password": "test_eab28b939ba1"

View File

@@ -1,103 +0,0 @@
# LoggingAspect generateCurlCommand 方法单元测试
## 测试概述
本测试文件 `LoggingAspectCurlGenerationTest.java` 专门针对 `LoggingAspect` 类中的 `generateCurlCommand` 方法进行全面的单元测试,验证该方法在各种场景下生成 curl 命令的正确性。
## 测试架构
测试采用 **嵌套测试类** 的结构,按功能模块组织:
### 1. GET 请求测试 (`GetRequestTests`)
- ✅ 基本 GET 请求 - 无查询参数
- ✅ GET 请求 - 包含查询参数
- ✅ GET 请求 - HTTPS 协议
- ✅ GET 请求 - 自定义端口
### 2. POST 请求测试 (`PostRequestTests`)
- ✅ POST 请求 - JSON 请求体
- ✅ POST 请求 - 空 JSON 请求体
- ✅ POST 请求 - 包含单引号的 JSON
### 3. PUT 和 PATCH 请求测试 (`PutAndPatchRequestTests`)
- ✅ PUT 请求 - JSON 请求体
- ✅ PATCH 请求 - JSON 请求体
### 4. 表单数据请求测试 (`FormDataRequestTests`)
- ✅ POST 请求 - 表单数据
- ✅ POST 请求 - 多值表单参数
- ✅ POST 请求 - 空表单数据
### 5. 请求头处理测试 (`HeaderProcessingTests`)
- ✅ 包含常规请求头
- ✅ 跳过特定请求头
- ✅ 验证跳过的请求头不会出现在 curl 命令中
### 6. 异常情况测试 (`ExceptionHandlingTests`)
- ✅ 读取请求体时发生 IOException
- ✅ 请求参数为 null
- ✅ 请求方法为 null
- ✅ 服务器信息为 null
### 7. 边界用例测试 (`BoundaryTests`)
- ✅ 最小化 GET 请求
- ✅ 复杂查询参数 - 包含特殊字符
- ✅ DELETE 请求 - 不应包含请求体
- ✅ HTTPS 请求 - 标准端口 443
- ✅ JSON 请求体为 null
## 测试覆盖的功能点
### 核心功能验证
1. **HTTP 方法处理**: GET, POST, PUT, PATCH, DELETE
2. **URL 构建**: 协议、主机名、端口、路径、查询参数
3. **请求头处理**: 包含/排除特定请求头
4. **请求体处理**: JSON、表单数据、空请求体
5. **异常处理**: 各种异常情况的优雅处理
### 特殊场景验证
1. **端口处理**: 标准端口省略,非标准端口包含
2. **字符转义**: JSON 中的单引号转义
3. **空值处理**: null 值的安全处理
4. **多值参数**: 表单中同名参数的多个值
## 测试技术特点
### 使用的测试技术
- **JUnit 5**: 现代化的测试框架
- **Mockito**: Mock 对象和行为验证
- **AssertJ**: 流畅的断言 API
- **嵌套测试**: 清晰的测试组织结构
### Mock 策略
- Mock `HttpServletRequest` 对象模拟各种 HTTP 请求场景
- Mock 依赖服务避免外部依赖
- 精确控制测试数据和行为
### 断言策略
- 验证生成的 curl 命令包含预期内容
- 验证不应包含的内容确实被排除
- 验证异常情况的错误消息
## 运行测试
```bash
# 运行所有 generateCurlCommand 相关测试
./gradlew test --tests "com.zl.mjga.unit.LoggingAspectCurlGenerationTest"
# 运行特定测试类别
./gradlew test --tests "com.zl.mjga.unit.LoggingAspectCurlGenerationTest\$GetRequestTests"
```
## 测试价值
这套测试确保了 `generateCurlCommand` 方法在各种复杂场景下都能正确工作,为 AOP 日志功能的 curl 命令生成提供了可靠的质量保证。通过全面的测试覆盖,可以:
1. **防止回归**: 代码修改时及时发现问题
2. **文档作用**: 测试用例本身就是最好的使用文档
3. **重构支持**: 安全地进行代码重构
4. **质量保证**: 确保功能在各种边界条件下正常工作
## 测试结果
所有 **24 个测试用例** 均通过,覆盖了 `generateCurlCommand` 方法的所有主要功能和边界情况。

1
frontend/.gitignore vendored
View File

@@ -18,6 +18,7 @@ coverage
/cypress/screenshots/ /cypress/screenshots/
# Editor directories and files # Editor directories and files
.vscode
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea

View File

@@ -1,7 +0,0 @@
{
"recommendations": [
"Vue.volar",
"vitest.explorer",
"ms-playwright.playwright"
]
}

View File

@@ -1,18 +0,0 @@
{
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"tsconfig.json": "tsconfig.*.json, env.d.ts",
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .prettier*, prettier*, .editorconfig"
},
"files.associations": {
"*.css": "tailwindcss"
},
"editor.quickSuggestions": {
"strings": "on"
},
"tailwindCSS.classAttributes": ["class", "ui"],
"tailwindCSS.experimental.classRegex": [
["ui:\\s*{([^)]*)\\s*}", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}

View File

@@ -21,7 +21,7 @@
"marked": "^15.0.12", "marked": "^15.0.12",
"openapi-fetch": "^0.13.5", "openapi-fetch": "^0.13.5",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"tailwindcss": "^4.0.14", "tailwindcss": "^4.1.11",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"zod": "^3.24.2" "zod": "^3.24.2"
@@ -1832,6 +1832,12 @@
"tailwindcss": "4.1.6" "tailwindcss": "4.1.6"
} }
}, },
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
"version": "4.1.6",
"resolved": "http://mirrors.tencent.com/npm/tailwindcss/-/tailwindcss-4.1.6.tgz",
"integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==",
"license": "MIT"
},
"node_modules/@tailwindcss/oxide": { "node_modules/@tailwindcss/oxide": {
"version": "4.1.6", "version": "4.1.6",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.6.tgz",
@@ -2079,6 +2085,12 @@
"vite": "^5.2.0 || ^6" "vite": "^5.2.0 || ^6"
} }
}, },
"node_modules/@tailwindcss/vite/node_modules/tailwindcss": {
"version": "4.1.6",
"resolved": "http://mirrors.tencent.com/npm/tailwindcss/-/tailwindcss-4.1.6.tgz",
"integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==",
"license": "MIT"
},
"node_modules/@testing-library/dom": { "node_modules/@testing-library/dom": {
"version": "10.4.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -3655,7 +3667,7 @@
}, },
"node_modules/flowbite": { "node_modules/flowbite": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-3.1.2.tgz", "resolved": "http://mirrors.tencent.com/npm/flowbite/-/flowbite-3.1.2.tgz",
"integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==", "integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -5705,9 +5717,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.6", "version": "4.1.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz", "resolved": "http://mirrors.tencent.com/npm/tailwindcss/-/tailwindcss-4.1.11.tgz",
"integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tapable": { "node_modules/tapable": {

View File

@@ -31,7 +31,7 @@
"marked": "^15.0.12", "marked": "^15.0.12",
"openapi-fetch": "^0.13.5", "openapi-fetch": "^0.13.5",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"tailwindcss": "^4.0.14", "tailwindcss": "^4.1.11",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"zod": "^3.24.2" "zod": "^3.24.2"

BIN
frontend/public/mjga.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -443,13 +443,7 @@ onMounted(async () => {
const $userDeleteModalElement: HTMLElement | null = const $userDeleteModalElement: HTMLElement | null =
document.querySelector("#user-delete-modal"); document.querySelector("#user-delete-modal");
if ($userDeleteModalElement) { if ($userDeleteModalElement) {
userDeleteModal.value = new Modal( userDeleteModal.value = new Modal($userDeleteModalElement, {});
$userDeleteModalElement,
{},
{
id: "user-delete-modal",
},
);
} }
const $departmentDeleteModalElement: HTMLElement | null = const $departmentDeleteModalElement: HTMLElement | null =
document.querySelector("#department-delete-modal"); document.querySelector("#department-delete-modal");

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="flex justify-center items-center my-4"> <div class="flex justify-center items-center my-4">
<a class="group relative w-full sm:w-2/3 md:w-1/2 lg:w-2/5 xl:w-1/3 max-w-xs overflow-hidden rounded-lg border border-gray-200 shadow-lg transition-all duration-300 hover:shadow-xl hover:scale-[1.02]" <a class="group relative w-full max-w-xs overflow-hidden rounded-lg border border-gray-200 shadow-lg transition-all duration-300 hover:shadow-xl hover:scale-[1.02]"
:href="href" target="_blank"> :href="href" target="_blank">
<div class="absolute top-0 right-0 bg-blue-600 text-white text-xs px-2 py-1 rounded-bl-lg z-10">{{ label }}</div> <div class="absolute top-0 right-0 bg-blue-600 text-white text-xs px-2 py-1 rounded-bl-lg z-10">{{ label }}</div>
<img :src="imageSrc" :alt="imageAlt" <img :src="imageSrc" :alt="imageAlt"
class="w-full h-auto transition-transform duration-500 group-hover:scale-105"> class="w-full h-52 transition-transform duration-500 group-hover:scale-105">
<div <div
class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 transform transition-all duration-300"> class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 transform transition-all duration-300">
<span class="text-white text-sm font-medium group-hover:underline flex items-center"> <span class="text-white text-sm font-medium group-hover:underline flex items-center">

View File

@@ -29,7 +29,7 @@
</svg> </svg>
<span class="text-sm pl-0.5 pr-2 font-medium">Star</span> <span class="text-sm pl-0.5 pr-2 font-medium">Star</span>
</span> </span>
<span class="text-sm py-0.5 px-2 font-medium">0.2k</span> <span class="text-sm py-0.5 px-2 font-medium">0.3k</span>
</a> </a>
<button class="cursor-pointer pt-1" @click="changeAssistantVisible"> <button class="cursor-pointer pt-1" @click="changeAssistantVisible">
<AiChatIcon /> <AiChatIcon />

View File

@@ -26,7 +26,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Modal, initFlowbite } from "flowbite"; import { initFlowbite } from "flowbite";
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
export type ModalSize = export type ModalSize =
@@ -72,7 +72,6 @@ const maxWidthClass = computed(() => {
return sizes[props.size || "md"]; return sizes[props.size || "md"];
}); });
// 确保Flowbite初始化这对于PopupModal的正常工作至关重要
onMounted(() => { onMounted(() => {
initFlowbite(); initFlowbite();
}); });

View File

@@ -111,19 +111,23 @@ export const useAiChat = () => {
const searchAction = async (message: string) => { const searchAction = async (message: string) => {
isLoading.value = true; isLoading.value = true;
try { try {
const { data } = await client.POST("/ai/action/search", {
body: message,
});
messages.value.push({ messages.value.push({
content: data?.action content: "",
? "搜索到功能,请您执行。"
: "未搜索到指定功能,请告诉我更加准确的信息。",
type: "action", type: "action",
isUser: false, isUser: false,
username: "知路智能体", username: "知路智能体",
command: data?.action, command: undefined,
}); });
return data; const { data } = await client.POST("/ai/action/search", {
body: message,
});
messages.value[messages.value.length - 1].content = data?.action
? "搜索到功能,请您执行。"
: "未搜索到指定功能,请告诉我更加准确的信息。";
messages.value[messages.value.length - 1].command = data?.action;
} catch (error) {
messages.value.pop();
throw error;
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }

View File

@@ -1,7 +1,11 @@
<template> <template>
<div class="px-2 sm:px-4 pt-6 sm:rounded-lg"> <div class="px-2 sm:px-4 pt-6 sm:rounded-lg">
<PromotionBanner href="https://www.bilibili.com/cheese/play/ss198449120" imageSrc="/ai-tdd.png" <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
imageAlt="ai-tdd-tutorial" label="官方教程" text="无幻觉式 AI 编程方法论" /> <PromotionBanner href="https://www.bilibili.com/cheese/play/ss198449120" imageSrc="/ai-tdd.png"
imageAlt="ai-tdd-tutorial" label="官方教程" text="AI 时代的 Java 测试驱动开发" />
<PromotionBanner href="https://www.mjga.cc" imageSrc="/mjga.png" imageAlt="后端脚手架" label="后端脚手架"
text="国内唯一可选配组件和元信息的脚手架" />
</div>
<div class="mb-4"> <div class="mb-4">
<Breadcrumbs :names="['用户管理']" /> <Breadcrumbs :names="['用户管理']" />
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">用户管理</h1> <h1 class="text-xl font-semibold text-gray-900 sm:text-2xl">用户管理</h1>