Spaces:
Paused
Paused
File size: 3,786 Bytes
a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 1f4cfcb a0dfc48 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | <template>
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-ink-950">日志</h2>
<div class="flex items-center gap-3">
<span class="text-[11px] text-ink-500 hidden sm:inline">保留最新 1000 条,页面隐藏时暂停轮询</span>
<label class="flex items-center gap-2 text-sm text-ink-600">
<input type="checkbox" v-model="autoScroll" class="rounded border-hairline accent-indigo-600" />
自动滚动
</label>
<AtButton variant="secondary" size="sm" :loading="loading" @click="fetchLogs">
<template #icon>
<RefreshCw class="w-3.5 h-3.5" :stroke-width="2" />
</template>
刷新
</AtButton>
<button @click="clearLogs"
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-surface hover:bg-ink-100 text-sm rounded-lg border border-hairline transition text-ink-600 hover:text-ink-950 focus-ring">
<Trash2 class="w-3.5 h-3.5" :stroke-width="2" />
清空
</button>
</div>
</div>
<div ref="logContainer"
class="bg-surface border border-hairline rounded-lg p-3 md:p-4 font-mono text-xs leading-relaxed h-[calc(100vh-200px)] md:h-[600px] overflow-y-auto">
<div v-if="logs.length === 0" class="text-ink-400 text-center py-8">暂无日志</div>
<div v-for="(log, i) in logs" :key="i"
class="py-0.5 flex gap-3 hover:bg-ink-50">
<span class="text-ink-500 shrink-0">{{ formatTime(log.time) }}</span>
<span class="shrink-0 w-16"
:class="{
'text-rose-700': log.level === 'ERROR',
'text-amber-700': log.level === 'WARNING',
'text-sky-700': log.level === 'INFO',
'text-ink-500': log.level === 'DEBUG',
}">{{ log.level }}</span>
<span class="text-ink-700 break-all">{{ log.message }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { api } from '../api.js'
import AtButton from './AtButton.vue'
import { RefreshCw, Trash2 } from 'lucide-vue-next'
const logs = ref([])
const loading = ref(false)
const autoScroll = ref(true)
const logContainer = ref(null)
let pollTimer = null
let lastTime = 0
const LOG_RENDER_LIMIT = 1000
function formatTime(ts) {
const d = new Date(ts * 1000)
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
}
async function fetchLogs() {
loading.value = true
try {
const result = await api.getLogs(500, lastTime)
if (result.logs.length > 0) {
if (lastTime === 0) {
logs.value = result.logs
} else {
logs.value.push(...result.logs)
// 保留最新 N 条
if (logs.value.length > LOG_RENDER_LIMIT) {
logs.value = logs.value.slice(-LOG_RENDER_LIMIT)
}
}
lastTime = result.logs[result.logs.length - 1].time
if (autoScroll.value) {
nextTick(() => {
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight
}
})
}
}
} catch (e) {
console.error('获取日志失败:', e)
} finally {
loading.value = false
}
}
function clearLogs() {
logs.value = []
lastTime = 0
}
onMounted(() => {
fetchLogs()
pollTimer = setInterval(() => {
if (!document.hidden) fetchLogs()
}, 3000)
document.addEventListener('visibilitychange', handleVisibility)
})
onUnmounted(() => {
if (pollTimer) clearInterval(pollTimer)
document.removeEventListener('visibilitychange', handleVisibility)
})
function handleVisibility() {
if (!document.hidden) fetchLogs()
}
</script>
|