457 lines
9.0 KiB
Vue
457 lines
9.0 KiB
Vue
|
|
<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
|
|||
|
|
|
|||
|
|
// 判断是否还有更多数据
|
|||
|
|
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>
|