feat: 样式调整
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -40,8 +40,6 @@ config/secrets/
|
|||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
logs/
|
|
||||||
log/
|
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
pids
|
pids
|
||||||
|
@@ -177,6 +177,7 @@ function resetPage() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<n-divider style="margin: 0; margin-bottom: 12px" />
|
||||||
<!-- 统计摘要区 -->
|
<!-- 统计摘要区 -->
|
||||||
<div class="stats-summary">
|
<div class="stats-summary">
|
||||||
<n-spin :show="loading" size="small">
|
<n-spin :show="loading" size="small">
|
||||||
@@ -274,6 +275,7 @@ function resetPage() {
|
|||||||
</n-grid>
|
</n-grid>
|
||||||
</n-spin>
|
</n-spin>
|
||||||
</div>
|
</div>
|
||||||
|
<n-divider style="margin: 0" />
|
||||||
|
|
||||||
<!-- 详细信息区(可折叠) -->
|
<!-- 详细信息区(可折叠) -->
|
||||||
<div class="details-section">
|
<div class="details-section">
|
||||||
|
365
web/src/components/logs/LogTable.vue
Normal file
365
web/src/components/logs/LogTable.vue
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { logApi } from "@/api/logs";
|
||||||
|
import type { LogFilter, RequestLog } from "@/types/models";
|
||||||
|
import { maskKey } from "@/utils/display";
|
||||||
|
import { EyeOffOutline, EyeOutline, Search } from "@vicons/ionicons5";
|
||||||
|
import {
|
||||||
|
NButton,
|
||||||
|
NDataTable,
|
||||||
|
NDatePicker,
|
||||||
|
NEllipsis,
|
||||||
|
NEmpty,
|
||||||
|
NIcon,
|
||||||
|
NInput,
|
||||||
|
NInputGroup,
|
||||||
|
NSelect,
|
||||||
|
NSpace,
|
||||||
|
NSpin,
|
||||||
|
NTag,
|
||||||
|
} from "naive-ui";
|
||||||
|
import { computed, h, onMounted, reactive, ref, watch } from "vue";
|
||||||
|
|
||||||
|
interface LogRow extends RequestLog {
|
||||||
|
is_key_visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const loading = ref(false);
|
||||||
|
const logs = ref<LogRow[]>([]);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(15);
|
||||||
|
const total = ref(0);
|
||||||
|
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const filters = reactive({
|
||||||
|
group_name: "",
|
||||||
|
key_value: "",
|
||||||
|
is_success: "" as "true" | "false" | "",
|
||||||
|
status_code: "",
|
||||||
|
source_ip: "",
|
||||||
|
error_contains: "",
|
||||||
|
start_time: null as number | null,
|
||||||
|
end_time: null as number | null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const successOptions = [
|
||||||
|
{ label: "全部状态", value: "" },
|
||||||
|
{ label: "成功", value: "true" },
|
||||||
|
{ label: "失败", value: "false" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fetch data
|
||||||
|
const loadLogs = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params: LogFilter = {
|
||||||
|
page: currentPage.value,
|
||||||
|
page_size: pageSize.value,
|
||||||
|
group_name: filters.group_name || undefined,
|
||||||
|
key_value: filters.key_value || undefined,
|
||||||
|
is_success: filters.is_success === "" ? undefined : filters.is_success === "true",
|
||||||
|
status_code: filters.status_code ? parseInt(filters.status_code, 10) : undefined,
|
||||||
|
source_ip: filters.source_ip || undefined,
|
||||||
|
error_contains: filters.error_contains || undefined,
|
||||||
|
start_time: filters.start_time ? new Date(filters.start_time).toISOString() : undefined,
|
||||||
|
end_time: filters.end_time ? new Date(filters.end_time).toISOString() : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await logApi.getLogs(params);
|
||||||
|
if (res.code === 0 && res.data) {
|
||||||
|
logs.value = res.data.items.map(log => ({ ...log, is_key_visible: false }));
|
||||||
|
total.value = res.data.pagination.total_items;
|
||||||
|
} else {
|
||||||
|
logs.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
window.$message.error(res.message || "加载日志失败");
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
window.$message.error("加载日志请求失败");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (timestamp: string) => {
|
||||||
|
if (!timestamp) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleString("zh-CN", { hour12: false }).replace(/\//g, "-");
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleKeyVisibility = (row: LogRow) => {
|
||||||
|
row.is_key_visible = !row.is_key_visible;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Columns definition
|
||||||
|
const createColumns = () => [
|
||||||
|
{
|
||||||
|
title: "时间",
|
||||||
|
key: "timestamp",
|
||||||
|
width: 180,
|
||||||
|
render: (row: LogRow) => formatDateTime(row.timestamp),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "状态",
|
||||||
|
key: "is_success",
|
||||||
|
width: 80,
|
||||||
|
render: (row: LogRow) =>
|
||||||
|
h(
|
||||||
|
NTag,
|
||||||
|
{ type: row.is_success ? "success" : "error", size: "small", round: true },
|
||||||
|
{ default: () => (row.is_success ? "成功" : "失败") }
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ title: "状态码", key: "status_code", width: 80 },
|
||||||
|
{ title: "耗时(ms)", key: "duration_ms", width: 100 },
|
||||||
|
{ title: "分组", key: "group_name", width: 120 },
|
||||||
|
{
|
||||||
|
title: "Key",
|
||||||
|
key: "key_value",
|
||||||
|
width: 200,
|
||||||
|
render: (row: LogRow) =>
|
||||||
|
h(NSpace, { align: "center", wrap: false }, () => [
|
||||||
|
h(
|
||||||
|
NEllipsis,
|
||||||
|
{ style: "max-width: 150px" },
|
||||||
|
{ default: () => (row.is_key_visible ? row.key_value : maskKey(row.key_value || "")) }
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
NButton,
|
||||||
|
{ size: "tiny", text: true, onClick: () => toggleKeyVisibility(row) },
|
||||||
|
{
|
||||||
|
icon: () =>
|
||||||
|
h(NIcon, null, { default: () => h(row.is_key_visible ? EyeOffOutline : EyeOutline) }),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "请求路径",
|
||||||
|
key: "request_path",
|
||||||
|
render: (row: LogRow) =>
|
||||||
|
h(NEllipsis, { style: "max-width: 200px" }, { default: () => row.request_path }),
|
||||||
|
},
|
||||||
|
{ title: "源IP", key: "source_ip", width: 130 },
|
||||||
|
{
|
||||||
|
title: "错误信息",
|
||||||
|
key: "error_message",
|
||||||
|
render: (row: LogRow) =>
|
||||||
|
h(NEllipsis, { style: "max-width: 250px" }, { default: () => row.error_message || "-" }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const columns = createColumns();
|
||||||
|
|
||||||
|
// Lifecycle and Watchers
|
||||||
|
onMounted(loadLogs);
|
||||||
|
watch([currentPage, pageSize], loadLogs);
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
currentPage.value = 1;
|
||||||
|
loadLogs();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
filters.group_name = "";
|
||||||
|
filters.key_value = "";
|
||||||
|
filters.is_success = "";
|
||||||
|
filters.status_code = "";
|
||||||
|
filters.source_ip = "";
|
||||||
|
filters.error_contains = "";
|
||||||
|
filters.start_time = null;
|
||||||
|
filters.end_time = null;
|
||||||
|
handleSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
function changePage(page: number) {
|
||||||
|
currentPage.value = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePageSize(size: number) {
|
||||||
|
pageSize.value = size;
|
||||||
|
currentPage.value = 1;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="log-table-container">
|
||||||
|
<n-space vertical>
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<n-space>
|
||||||
|
<n-select
|
||||||
|
v-model:value="filters.is_success"
|
||||||
|
:options="successOptions"
|
||||||
|
size="small"
|
||||||
|
style="width: 120px"
|
||||||
|
@update:value="handleSearch"
|
||||||
|
/>
|
||||||
|
<n-date-picker
|
||||||
|
v-model:value="filters.start_time"
|
||||||
|
type="datetime"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
placeholder="开始时间"
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
<n-date-picker
|
||||||
|
v-model:value="filters.end_time"
|
||||||
|
type="datetime"
|
||||||
|
clearable
|
||||||
|
size="small"
|
||||||
|
placeholder="结束时间"
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<n-space>
|
||||||
|
<n-input
|
||||||
|
v-model:value="filters.group_name"
|
||||||
|
placeholder="分组名"
|
||||||
|
size="small"
|
||||||
|
clearable
|
||||||
|
style="width: 120px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
<n-input
|
||||||
|
v-model:value="filters.key_value"
|
||||||
|
placeholder="Key 片段"
|
||||||
|
size="small"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input
|
||||||
|
v-model:value="filters.error_contains"
|
||||||
|
placeholder="错误信息"
|
||||||
|
size="small"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
<n-button ghost size="small" :disabled="loading" @click="handleSearch">
|
||||||
|
<n-icon :component="Search" />
|
||||||
|
</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
<n-button size="small" @click="resetFilters">重置</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-main">
|
||||||
|
<!-- 表格 -->
|
||||||
|
<div class="table-container">
|
||||||
|
<n-spin :show="loading">
|
||||||
|
<n-data-table :columns="columns" :data="logs" :bordered="false" remote size="small" />
|
||||||
|
<div v-if="logs.length === 0 && !loading" class="empty-container">
|
||||||
|
<n-empty description="没有找到匹配的日志" />
|
||||||
|
</div>
|
||||||
|
</n-spin>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<div class="pagination-info">
|
||||||
|
<span>共 {{ total }} 条记录</span>
|
||||||
|
<n-select
|
||||||
|
v-model:value="pageSize"
|
||||||
|
:options="[
|
||||||
|
{ label: '15条/页', value: 15 },
|
||||||
|
{ label: '30条/页', value: 30 },
|
||||||
|
{ label: '50条/页', value: 50 },
|
||||||
|
{ label: '100条/页', value: 100 },
|
||||||
|
]"
|
||||||
|
size="small"
|
||||||
|
style="width: 100px; margin-left: 12px"
|
||||||
|
@update:value="changePageSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="pagination-controls">
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:disabled="currentPage <= 1"
|
||||||
|
@click="changePage(currentPage - 1)"
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</n-button>
|
||||||
|
<span class="page-info">第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:disabled="currentPage >= totalPages"
|
||||||
|
@click="changePage(currentPage + 1)"
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.log-table-container {
|
||||||
|
/* background: white; */
|
||||||
|
/* border-radius: 8px; */
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* height: 100%; */
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.table-main {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.table-container {
|
||||||
|
/* background: white;
|
||||||
|
border-radius: 8px; */
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.empty-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.pagination-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.pagination-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.page-info {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -96,7 +96,7 @@ function handleGroupDelete(deletedGroup: Group) {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.keys-container {
|
.keys-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -61,9 +61,9 @@ async function handleSubmit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-space vertical size="large">
|
<n-space vertical>
|
||||||
<n-form ref="formRef" :model="form" label-placement="top">
|
<n-form ref="formRef" :model="form" label-placement="top">
|
||||||
<n-space vertical size="large">
|
<n-space vertical>
|
||||||
<n-card
|
<n-card
|
||||||
size="small"
|
size="small"
|
||||||
v-for="category in settingList"
|
v-for="category in settingList"
|
||||||
@@ -103,12 +103,14 @@ async function handleSubmit() {
|
|||||||
placeholder="请输入数值"
|
placeholder="请输入数值"
|
||||||
clearable
|
clearable
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
<n-input
|
<n-input
|
||||||
v-else
|
v-else
|
||||||
v-model:value="form[item.key] as string"
|
v-model:value="form[item.key] as string"
|
||||||
placeholder="请输入内容"
|
placeholder="请输入内容"
|
||||||
clearable
|
clearable
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
|
Reference in New Issue
Block a user