Files
EmailBill/Web/src/views/LogView.vue
孙诚 8ba279e957
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
优化日志加载和清理功能,增加流式读取和定期清理日志服务
2025-12-30 11:07:14 +08:00

464 lines
9.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page-container-flex log-view">
<van-nav-bar
title="查看日志"
left-text="返回"
left-arrow
@click-left="handleBack"
placeholder
/>
<div class="scroll-content">
<!-- 搜索和筛选 -->
<div class="filter-section">
<van-search
v-model="searchKeyword"
placeholder="输入关键词筛选日志"
@search="handleSearch"
@clear="handleClear"
/>
<div class="filter-row">
<van-dropdown-menu>
<van-dropdown-item v-model="selectedLevel" :options="levelOptions" @change="handleSearch" />
<van-dropdown-item v-model="selectedDate" :options="dateOptions" @change="handleSearch" />
</van-dropdown-menu>
</div>
</div>
<!-- 下拉刷新区域 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<!-- 加载提示 -->
<van-loading v-if="loading && !logList.length" vertical style="padding: 50px 0">
加载中...
</van-loading>
<!-- 日志列表 -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
class="log-list"
>
<div
v-for="(log, index) in logList"
:key="index"
class="log-item"
:class="getLevelClass(log.level)"
>
<div class="log-header">
<span class="log-level">{{ log.level }}</span>
<span class="log-time">{{ formatTime(log.timestamp) }}</span>
</div>
<div class="log-message">{{ log.message }}</div>
</div>
<!-- 空状态 -->
<van-empty
v-if="!loading && !logList.length"
description="暂无日志"
image="search"
/>
</van-list>
<!-- 底部安全距离 -->
<div style="height: 20px"></div>
</van-pull-refresh>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { getLogList, getAvailableDates } from '@/api/log'
const router = useRouter()
// 数据状态
const logList = ref([])
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const fetching = ref(false)
// 分页参数
const pageIndex = ref(1)
const pageSize = ref(100)
const total = ref(0)
// 筛选参数
const searchKeyword = ref('')
const selectedLevel = ref('')
const selectedDate = ref('')
// 日志级别选项
const levelOptions = ref([
{ text: '全部级别', value: '' },
{ text: 'VRB', value: 'VRB' },
{ text: 'DBG', value: 'DBG' },
{ text: 'INF', value: 'INF' },
{ text: 'WRN', value: 'WRN' },
{ text: 'ERR', value: 'ERR' },
{ text: 'FTL', value: 'FTL' }
])
// 日期选项
const dateOptions = ref([
{ text: '全部日期', value: '' }
])
/**
* 返回上一页
*/
const handleBack = () => {
router.back()
}
/**
* 获取日志级别对应的样式类
*/
const getLevelClass = (level) => {
const levelMap = {
'ERR': 'level-error',
'FTL': 'level-fatal',
'WRN': 'level-warning',
'INF': 'level-info',
'DBG': 'level-debug',
'VRB': 'level-verbose'
}
return levelMap[level] || 'level-default'
}
/**
* 格式化时间显示
*/
const formatTime = (timestamp) => {
// 提取时间部分,去掉时区
const match = timestamp.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})/)
return match ? match[1] : timestamp
}
/**
* 加载日志数据
*/
const loadLogs = async (reset = false) => {
if (fetching.value) return
fetching.value = true
try {
if (reset) {
pageIndex.value = 1
logList.value = []
finished.value = false
}
const params = {
pageIndex: pageIndex.value,
pageSize: pageSize.value
}
if (searchKeyword.value) {
params.searchKeyword = searchKeyword.value
}
if (selectedLevel.value) {
params.logLevel = selectedLevel.value
}
if (selectedDate.value) {
params.date = selectedDate.value
}
const response = await getLogList(params)
if (response.success) {
const newLogs = response.data || []
if (reset) {
logList.value = newLogs
} else {
logList.value = [...logList.value, ...newLogs]
}
total.value = response.total
// 判断是否还有更多数据
// total = -1 表示总数未知,此时只根据返回数据量判断
if (total.value === -1) {
// 如果返回的数据少于请求的数量,说明没有更多了
finished.value = newLogs.length < pageSize.value
} else {
// 如果有明确的总数,则判断是否已加载完全部数据
if (logList.value.length >= total.value || newLogs.length < pageSize.value) {
finished.value = true
} else {
finished.value = false
}
}
} else {
showToast(response.message || '获取日志失败')
finished.value = true
}
} catch (error) {
console.error('加载日志失败:', error)
showToast('加载日志失败')
finished.value = true
} finally {
fetching.value = false
loading.value = false
refreshing.value = false
}
}
/**
* 下拉刷新
*/
const onRefresh = async () => {
await loadLogs(true)
}
/**
* 加载更多
*/
const onLoad = async () => {
if (finished.value || fetching.value) return
// 如果是第一次加载
if (pageIndex.value === 1 && logList.value.length === 0) {
await loadLogs(false)
} else {
// 后续分页加载
pageIndex.value++
await loadLogs(false)
}
}
/**
* 搜索处理
*/
const handleSearch = () => {
loadLogs(true)
}
/**
* 清空搜索
*/
const handleClear = () => {
searchKeyword.value = ''
handleSearch()
}
/**
* 加载可用日期列表
*/
const loadAvailableDates = async () => {
try {
const response = await getAvailableDates()
if (response.success && response.data) {
const dates = response.data.map(date => ({
text: formatDate(date),
value: date
}))
dateOptions.value = [
{ text: '全部日期', value: '' },
...dates
]
}
} catch (error) {
console.error('加载日期列表失败:', error)
}
}
/**
* 格式化日期显示
*/
const formatDate = (dateStr) => {
// dateStr 格式: 20251229
if (dateStr.length === 8) {
return `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`
}
return dateStr
}
// 组件挂载时加载数据
onMounted(() => {
loadAvailableDates()
// 不在这里调用 loadLogs让 van-list 的 @load 事件自动触发
})
</script>
<style scoped>
.log-view {
height: 100vh;
background-color: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
.log-view {
background-color: #1a1a1a;
}
}
.filter-section {
background-color: #ffffff;
position: sticky;
top: 0;
z-index: 10;
}
@media (prefers-color-scheme: dark) {
.filter-section {
background-color: #2c2c2c;
}
}
.filter-row {
padding: 0;
}
.log-list {
padding: 4px 12px;
}
.log-item {
background-color: #ffffff;
border-left: 3px solid #1989fa;
margin-bottom: 4px;
padding: 6px 10px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
transition: all 0.2s;
}
.log-item:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@media (prefers-color-scheme: dark) {
.log-item {
background-color: #2c2c2c;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}
.log-item:hover {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
}
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
font-size: 11px;
}
.log-level {
display: inline-block;
padding: 1px 6px;
border-radius: 2px;
font-weight: bold;
color: #ffffff;
min-width: 36px;
text-align: center;
font-size: 10px;
}
.log-time {
color: #969799;
font-size: 10px;
}
@media (prefers-color-scheme: dark) {
.log-time {
color: #858585;
}
}
.log-message {
color: #323233;
line-height: 1.4;
word-break: break-word;
white-space: pre-wrap;
}
@media (prefers-color-scheme: dark) {
.log-message {
color: #e0e0e0;
}
}
/* 不同日志级别的颜色 */
.level-verbose .log-level {
background-color: #b0b0b0;
}
.level-debug .log-level {
background-color: #1989fa;
}
.level-info .log-level {
background-color: #07c160;
}
.level-warning .log-level {
background-color: #ff976a;
border-left-color: #ff976a;
}
.level-error .log-level {
background-color: #ee0a24;
}
.level-fatal .log-level {
background-color: #8b0000;
}
.level-default .log-level {
background-color: #646566;
}
.level-verbose {
border-left-color: #b0b0b0;
}
.level-debug {
border-left-color: #1989fa;
}
.level-info {
border-left-color: #07c160;
}
.level-warning {
border-left-color: #ff976a;
}
.level-error {
border-left-color: #ee0a24;
}
.level-fatal {
border-left-color: #8b0000;
}
.level-default {
border-left-color: #646566;
}
/* 优化下拉菜单样式 */
:deep(.van-dropdown-menu) {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
}
@media (prefers-color-scheme: dark) {
:deep(.van-dropdown-menu) {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
}
</style>