mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-04-08 06:27:36 +00:00
重构AOP日志功能,新增日志查询、删除接口及相关页面,优化日志管理体验;更新前端组件以支持日志展示和操作。
This commit is contained in:
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 LogStatusBadge from "./LogStatusBadge.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";
|
||||
export { default as PlusIcon } from "./PlusIcon.vue";
|
||||
export { default as AiChatIcon } from "./AiChatIcon.vue";
|
||||
export { default as PositionIcon } from "./PositionIcon.vue";
|
||||
export { default as SchedulerIcon } from "./SchedulerIcon.vue";
|
||||
export { default as DepartmentIcon } from "./DepartmentIcon.vue";
|
||||
export { default as LlmConfigIcon } from "./LlmConfigIcon.vue";
|
||||
export { default as LoadingIcon } from "./LoadingIcon.vue";
|
||||
export { default as RoleIcon } from "./RoleIcon.vue";
|
||||
export { default as SettingsIcon } from "./SettingsIcon.vue";
|
||||
export { default as UsersIcon } from "./UsersIcon.vue";
|
||||
export { default as PermissionIcon } from "./PermissionIcon.vue";
|
||||
export { default as KnowledgeIcon } from "./KnowledgeIcon.vue";
|
||||
import AiChatIcon from "./AiChatIcon.vue";
|
||||
import DepartmentIcon from "./DepartmentIcon.vue";
|
||||
import KnowledgeIcon from "./KnowledgeIcon.vue";
|
||||
import LlmConfigIcon from "./LlmConfigIcon.vue";
|
||||
import LoadingIcon from "./LoadingIcon.vue";
|
||||
import LogIcon from "./LogIcon.vue";
|
||||
import PermissionIcon from "./PermissionIcon.vue";
|
||||
import PlusIcon from "./PlusIcon.vue";
|
||||
import PositionIcon from "./PositionIcon.vue";
|
||||
import RoleIcon from "./RoleIcon.vue";
|
||||
import SchedulerIcon from "./SchedulerIcon.vue";
|
||||
import SettingsIcon from "./SettingsIcon.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 {
|
||||
AiChatIcon,
|
||||
DepartmentIcon,
|
||||
KnowledgeIcon,
|
||||
LlmConfigIcon,
|
||||
LogIcon,
|
||||
PermissionIcon,
|
||||
PositionIcon,
|
||||
RoleIcon,
|
||||
@@ -119,6 +121,11 @@ const menuItems = [
|
||||
path: Routes.KNOWLEDGEVIEW.fullPath(),
|
||||
icon: KnowledgeIcon,
|
||||
},
|
||||
{
|
||||
title: "日志管理",
|
||||
path: Routes.AOPLOGVIEW.fullPath(),
|
||||
icon: LogIcon,
|
||||
},
|
||||
];
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-4 gap-y-3 sm:gap-y-0">
|
||||
<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">
|
||||
<!-- 输入框类型 -->
|
||||
<div v-if="filter.type === 'input'" class="flex-grow">
|
||||
@@ -17,10 +17,11 @@
|
||||
</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
|
||||
: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>
|
||||
|
||||
<!-- 选择器 -->
|
||||
@@ -33,15 +34,17 @@
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
<Button variant="primary" size="sm" @click.prevent="handleSearch">
|
||||
<template #icon>
|
||||
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
||||
</svg>
|
||||
</template>
|
||||
搜索
|
||||
</Button>
|
||||
<div class="col-span-full flex mt-2">
|
||||
<Button variant="primary" size="sm" @click.prevent="handleSearch" class="w-full sm:w-1/2">
|
||||
<template #icon>
|
||||
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" />
|
||||
</svg>
|
||||
</template>
|
||||
<span class="ps-1.5">搜索</span>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 额外操作按钮插槽 -->
|
||||
@@ -52,8 +55,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, watch } from "vue";
|
||||
import { Button } from "@/components/ui";
|
||||
import { onMounted, reactive, watch } from "vue";
|
||||
|
||||
export interface FilterOption {
|
||||
value: string | number | boolean;
|
||||
@@ -129,3 +132,36 @@ const handleSearch = () => {
|
||||
emit("search", { ...filterValues });
|
||||
};
|
||||
</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>
|
||||
<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">
|
||||
<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">
|
||||
{{ displayRange.start }}-{{ displayRange.end }}
|
||||
</span>
|
||||
共
|
||||
<span class="font-semibold text-gray-900">{{ total }}</span> 条
|
||||
</span>
|
||||
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
|
||||
<li>
|
||||
<a href="#" @click.prevent="handlePageChangeClick(currentPage - 1)" :class="[
|
||||
<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">
|
||||
<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">
|
||||
{{ displayRange.start }}-{{ displayRange.end }}
|
||||
</span>
|
||||
共
|
||||
<span class="font-semibold text-gray-900">{{ total }}</span> 条
|
||||
</span>
|
||||
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
|
||||
<li>
|
||||
<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',
|
||||
{ 'opacity-50 cursor-not-allowed': isFirstPage }
|
||||
]">上一页</a>
|
||||
</li>
|
||||
<li v-for="page in pageNumbers" :key="page">
|
||||
<button @click.prevent="handlePageChangeClick(page)" :class="[
|
||||
'flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 hover:bg-gray-100 hover:text-gray-700',
|
||||
currentPage === page
|
||||
? 'text-blue-600 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 font-medium'
|
||||
: 'text-gray-500 bg-white'
|
||||
]">{{ page }}</button>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<button @click.prevent="handlePageChangeClick(currentPage + 1)" :class="[
|
||||
</li>
|
||||
<li v-for="(item, index) in visiblePageNumbers" :key="index">
|
||||
<template v-if="item.type === 'page'">
|
||||
<button @click.prevent="item.page && handlePageChangeClick(item.page)" :class="[
|
||||
'flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 hover:bg-gray-100 hover:text-gray-700',
|
||||
currentPage === item.page
|
||||
? 'text-blue-600 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 font-medium'
|
||||
: 'text-gray-500 bg-white'
|
||||
]">{{ item.page }}</button>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'ellipsis'">
|
||||
<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',
|
||||
{ 'opacity-50 cursor-not-allowed': isLastPage }
|
||||
]">下一页</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePagination } from "@/composables/common/usePagination";
|
||||
import { watch } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const { pageChange, total } = defineProps<{
|
||||
pageChange: (page: number, size: number) => Promise<void>;
|
||||
const props = defineProps<{
|
||||
pageChange?: (page: number, size: number) => Promise<void>;
|
||||
total: number;
|
||||
currentPage?: number;
|
||||
totalPages?: number;
|
||||
maxVisiblePages?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'change-page': [page: number];
|
||||
}>();
|
||||
|
||||
// 创建一个本地的totalPages引用
|
||||
const localTotalPages = ref<number | undefined>(props.totalPages);
|
||||
|
||||
const {
|
||||
currentPage,
|
||||
pageNumbers,
|
||||
visiblePageNumbers,
|
||||
pageSize,
|
||||
displayRange,
|
||||
isFirstPage,
|
||||
isLastPage,
|
||||
totalPages,
|
||||
updatePaginationState,
|
||||
} = usePagination();
|
||||
} = usePagination({
|
||||
initialPage: props.currentPage,
|
||||
initialTotal: props.total,
|
||||
maxVisiblePages: props.maxVisiblePages || 7 // 默认显示7个页码
|
||||
});
|
||||
|
||||
const handlePageChangeClick = async (page: number) => {
|
||||
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({
|
||||
currentPage: page,
|
||||
pageSize: pageSize.value,
|
||||
total,
|
||||
total: props.total,
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => total,
|
||||
() => {
|
||||
() => props.total,
|
||||
(newTotal) => {
|
||||
updatePaginationState({
|
||||
currentPage: currentPage.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>
|
||||
|
||||
Reference in New Issue
Block a user