feat: 样式调整

This commit is contained in:
tbphp
2025-07-12 16:20:56 +08:00
parent 6fb908952c
commit 55881b9ab9
5 changed files with 372 additions and 5 deletions

2
.gitignore vendored
View File

@@ -40,8 +40,6 @@ config/secrets/
# Logs
*.log
logs/
log/
# Runtime data
pids

View File

@@ -177,6 +177,7 @@ function resetPage() {
</div>
</template>
<n-divider style="margin: 0; margin-bottom: 12px" />
<!-- 统计摘要区 -->
<div class="stats-summary">
<n-spin :show="loading" size="small">
@@ -274,6 +275,7 @@ function resetPage() {
</n-grid>
</n-spin>
</div>
<n-divider style="margin: 0" />
<!-- 详细信息区可折叠 -->
<div class="details-section">

View 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>

View File

@@ -96,7 +96,7 @@ function handleGroupDelete(deletedGroup: Group) {
<style scoped>
.keys-container {
display: flex;
gap: 12px;
gap: 8px;
width: 100%;
}

View File

@@ -61,9 +61,9 @@ async function handleSubmit() {
</script>
<template>
<n-space vertical size="large">
<n-space vertical>
<n-form ref="formRef" :model="form" label-placement="top">
<n-space vertical size="large">
<n-space vertical>
<n-card
size="small"
v-for="category in settingList"
@@ -103,12 +103,14 @@ async function handleSubmit() {
placeholder="请输入数值"
clearable
style="width: 100%"
size="small"
/>
<n-input
v-else
v-model:value="form[item.key] as string"
placeholder="请输入内容"
clearable
size="small"
/>
</n-form-item>
</n-grid-item>