1
This commit is contained in:
22
src/api/monitor/logfile.js
Normal file
22
src/api/monitor/logfile.js
Normal 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 }
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
168
src/views/monitor/logfile/index.vue
Normal file
168
src/views/monitor/logfile/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user