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',
|
path: '/tool/gen-edit',
|
||||||
component: Layout,
|
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