This commit is contained in:
Leo
2026-01-30 19:43:15 +08:00
parent 53edda8a02
commit 6ff8f18604
3 changed files with 203 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
import request from '@/utils/request'
/** 获取可选的日志文件列表 */
export function listLogfiles() {
return request({
url: '/monitor/logfile/list',
method: 'get'
})
}
/**
* 读取日志文件末尾 N 行HTTPS 轮询,无需 WebSocket
* @param {string} file - 文件名,如 sys-info.log
* @param {number} lines - 行数,默认 500最大 5000
*/
export function tailLogfile(file, lines = 500) {
return request({
url: '/monitor/logfile/tail',
method: 'get',
params: { file, lines }
})
}

View File

@@ -362,6 +362,19 @@ export const dynamicRoutes = [
}
]
},
{
path: '/monitor/logfile',
component: Layout,
permissions: ['monitor:server:list'],
children: [
{
path: '',
component: () => import('@/views/monitor/logfile/index'),
name: 'Logfile',
meta: { title: '日志文件', icon: 'documentation' }
}
]
},
{
path: '/tool/gen-edit',
component: Layout,

View File

@@ -0,0 +1,168 @@
<template>
<div class="app-container">
<el-card>
<div slot="header" class="clearfix">
<span>日志文件查看</span>
<span class="hint">通过 HTTPS 轮询读取最新内容无需 SSH</span>
</div>
<el-form :inline="true" size="small" class="toolbar">
<el-form-item label="日志文件">
<el-select v-model="currentFile" placeholder="请选择" style="width: 180px" @change="handleFileChange">
<el-option
v-for="f in fileList"
:key="f"
:label="f"
:value="f"
/>
</el-select>
</el-form-item>
<el-form-item label="行数">
<el-input-number v-model="lines" :min="100" :max="5000" :step="100" style="width: 120px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-refresh" :loading="loading" @click="fetchLog">刷新</el-button>
</el-form-item>
<el-form-item>
<el-checkbox v-model="autoRefresh">自动刷新</el-checkbox>
</el-form-item>
<el-form-item v-if="autoRefresh">
<el-select v-model="refreshInterval" style="width: 100px" @change="restartTimer">
<el-option label="5 秒" :value="5" />
<el-option label="10 秒" :value="10" />
<el-option label="30 秒" :value="30" />
</el-select>
</el-form-item>
</el-form>
<div v-if="infoText" class="info-bar">
{{ infoText }}
</div>
<div class="log-wrap">
<pre ref="logPre" class="log-content">{{ logContent }}</pre>
</div>
</el-card>
</div>
</template>
<script>
import { listLogfiles, tailLogfile } from '@/api/monitor/logfile'
export default {
name: 'LogfileViewer',
data() {
return {
fileList: [],
currentFile: 'all.log',
lines: 200,
loading: false,
logContent: '',
totalLines: 0,
fromLine: 0,
toLine: 0,
autoRefresh: false,
refreshInterval: 10,
timer: null
}
},
computed: {
infoText() {
if (!this.currentFile || !this.totalLines) return ''
return `文件: ${this.currentFile} | 显示第 ${this.fromLine} - ${this.toLine} 行,共 ${this.totalLines}`
}
},
created() {
this.loadFileList()
},
beforeDestroy() {
this.stopTimer()
},
methods: {
loadFileList() {
listLogfiles().then(res => {
const list = res.data || []
this.fileList = Array.isArray(list) ? list : []
if (this.fileList.length && !this.fileList.includes(this.currentFile)) {
this.currentFile = this.fileList[0]
}
this.fetchLog()
}).catch(() => {
this.$message.error('获取日志列表失败')
})
},
handleFileChange() {
this.fetchLog()
},
fetchLog() {
if (!this.currentFile) return
this.loading = true
tailLogfile(this.currentFile, this.lines).then(res => {
const d = res.data || {}
this.logContent = d.content != null ? d.content : ''
this.totalLines = d.totalLines || 0
this.fromLine = d.fromLine || 0
this.toLine = d.toLine || 0
this.$nextTick(() => this.scrollToBottom())
}).catch(e => {
this.$message.error(e.msg || '读取日志失败')
this.logContent = ''
}).finally(() => {
this.loading = false
})
},
scrollToBottom() {
const el = this.$refs.logPre
if (el) el.scrollTop = el.scrollHeight
},
startTimer() {
this.stopTimer()
this.timer = setInterval(() => this.fetchLog(), this.refreshInterval * 1000)
},
stopTimer() {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
restartTimer() {
if (this.autoRefresh) this.startTimer()
}
},
watch: {
autoRefresh(v) {
if (v) this.startTimer()
else this.stopTimer()
}
}
}
</script>
<style scoped>
.hint {
font-size: 12px;
color: #909399;
margin-left: 12px;
}
.toolbar {
margin-bottom: 12px;
}
.info-bar {
font-size: 12px;
color: #606266;
margin-bottom: 8px;
}
.log-wrap {
background: #1e1e1e;
border-radius: 4px;
padding: 12px;
max-height: 70vh;
overflow: auto;
}
.log-content {
margin: 0;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
color: #d4d4d4;
white-space: pre-wrap;
word-break: break-all;
}
</style>