Files
EmailBill/Web/src/views/LogView.vue

464 lines
9.3 KiB
Vue
Raw Normal View History

2025-12-29 16:45:51 +08:00
<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
2025-12-29 16:45:51 +08:00
} else {
// 如果有明确的总数,则判断是否已加载完全部数据
if (logList.value.length >= total.value || newLogs.length < pageSize.value) {
finished.value = true
} else {
finished.value = false
}
2025-12-29 16:45:51 +08:00
}
} 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>