Files
crawlab/frontend/src/components/ScrollView/LogView.vue
2020-02-14 15:18:08 +08:00

322 lines
7.2 KiB
Vue

<template>
<div class="log-view-container">
<div class="filter-wrapper">
<div class="left">
<el-button
size="small"
type="primary"
icon="el-icon-download"
style="margin-right: 10px"
:disabled="isToBottom"
@click="onAutoScroll"
>
{{$t('Auto-Scroll')}}
</el-button>
<el-input
v-model="searchString"
size="small"
suffix-icon="el-icon-search"
:placeholder="$t('Search Log')"
style="width: 240px; margin-right: 10px"
/>
</div>
<div class="right">
<el-badge
v-if="errorLogData.length > 0"
:value="errorLogData.length"
>
<el-button
type="danger"
size="small"
icon="el-icon-warning-outline"
@click="toggleErrors"
>
{{$t('Error Count')}}
</el-button>
</el-badge>
</div>
</div>
<div class="content">
<div
class="log-view-wrapper"
:class="isErrorsCollapsed ? 'errors-collapsed' : ''"
>
<virtual-list
class="log-view"
ref="log-view"
:start="currentLogIndex - 1"
:offset="0"
:size="18"
:remain="remainSize"
:item="item"
:itemcount="filteredLogData.length"
:itemprops="getItemProps"
:tobottom="onToBottom"
:onscroll="onScroll"
/>
</div>
<div
v-show="!isErrorsCollapsed && !isErrorCollapsing"
class="errors-wrapper"
:class="isErrorsCollapsed ? 'collapsed' : ''"
>
<ul class="error-list">
<li
v-for="item in errorLogData"
:key="item.index"
class="error-item"
:class="currentLogIndex === item.index ? 'active' : ''"
@click="onClickError(item)"
>
<span class="line-no">
{{item.index}}
</span>
<span class="line-content">
{{item.data}}
</span>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import {
mapState,
mapGetters
} from 'vuex'
import VirtualList from 'vue-virtual-scroll-list'
import Convert from 'ansi-to-html'
import hasAnsi from 'has-ansi'
import LogItem from './LogItem'
const convert = new Convert()
export default {
name: 'LogView',
components: {
VirtualList
},
props: {
data: {
type: String,
default: ''
}
},
data () {
return {
item: LogItem,
searchString: '',
isToBottom: false,
isScrolling: false,
isScrolling2nd: false,
errorRegex: this.$utils.log.errorRegex,
currentOffset: 0,
isErrorsCollapsed: true,
isErrorCollapsing: false
}
},
computed: {
...mapState('task', [
'taskForm'
]),
...mapGetters('task', [
'logData',
'errorLogData'
]),
currentLogIndex: {
get () {
return this.$store.state.task.currentLogIndex
},
set (value) {
this.$store.commit('task/SET_CURRENT_LOG_INDEX', value)
}
},
filteredLogData () {
return this.logData.filter(d => {
if (!this.searchString) return true
return !!d.data.toLowerCase().match(this.searchString.toLowerCase())
})
},
remainSize () {
const height = document.querySelector('body').clientHeight
return (height - 240) / 18
}
},
watch: {
searchString () {
this.$st.sendEv('任务详情', '日志', '搜索日志')
}
},
methods: {
getItemProps (index) {
const logItem = this.filteredLogData[index]
const isAnsi = hasAnsi(logItem.data)
return {
// <item/> will render with itemProps.
// https://vuejs.org/v2/guide/render-function.html#createElement-Arguments
props: {
index: logItem.index,
data: isAnsi ? convert.toHtml(logItem.data) : logItem.data,
searchString: this.searchString,
active: logItem.active,
isAnsi
}
}
},
onToBottom () {
if (this.isScrolling) return
this.isToBottom = true
},
onScroll () {
if (this.isScrolling2nd) {
this.isToBottom = false
}
this.isScrolling = true
setTimeout(() => {
this.isScrolling2nd = true
setTimeout(() => {
this.isScrolling2nd = false
}, 50)
}, 50)
setTimeout(() => {
this.isScrolling = false
}, 100)
},
toBottom () {
this.$el.querySelector('.log-view').scrollTo({ top: 99999999 })
setTimeout(() => {
this.isToBottom = true
}, 50)
},
onAutoScroll () {
this.toBottom()
this.$st.sendEv('任务详情', '日志', '点击自动滚动')
},
toggleErrors () {
this.isErrorsCollapsed = !this.isErrorsCollapsed
this.isErrorCollapsing = true
setTimeout(() => {
this.isErrorCollapsing = false
}, 300)
},
onClickError (item) {
this.currentLogIndex = item.index
this.isToBottom = false
const handle = setInterval(() => {
this.isToBottom = false
}, 10)
setTimeout(() => {
clearInterval(handle)
}, 500)
}
},
mounted () {
this.currentLogIndex = 0
this.handle = setInterval(() => {
if (this.isToBottom) {
this.toBottom()
}
}, 200)
},
destroyed () {
clearInterval(this.handle)
}
}
</script>
<style scoped>
.filter-wrapper {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.content {
display: block;
}
.log-view-wrapper {
float: left;
flex-basis: calc(100% - 240px);
width: calc(100% - 300px);
transition: width 0.3s;
}
.log-view-wrapper.errors-collapsed {
flex-basis: 100%;
width: 100%;
}
.log-view {
margin-top: 0 !important;
overflow-y: scroll !important;
list-style: none;
color: #A9B7C6;
background: #2B2B2B;
border: none;
}
.errors-wrapper {
float: left;
display: inline-block;
margin: 0;
padding: 0;
flex-basis: 240px;
width: 300px;
transition: opacity 0.3s;
border-top: 1px solid #DCDFE6;
border-right: 1px solid #DCDFE6;
border-bottom: 1px solid #DCDFE6;
height: calc(100vh - 240px);
font-size: 16px;
overflow: auto;
}
.errors-wrapper.collapsed {
width: 0;
}
.errors-wrapper .error-list {
list-style: none;
padding: 0;
margin: 0;
}
.errors-wrapper .error-list .error-item {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
/*height: 18px;*/
border-bottom: 1px solid white;
padding: 5px 0;
background: #F56C6C;
color: white;
cursor: pointer;
}
.errors-wrapper .error-list .error-item.active {
background: #E6A23C;
font-weight: bolder;
text-decoration: underline;
}
.errors-wrapper .error-list .error-item:hover {
font-weight: bolder;
text-decoration: underline;
}
.errors-wrapper .error-list .error-item .line-no {
display: inline-block;
text-align: right;
width: 70px;
}
.errors-wrapper .error-list .error-item .line-content {
display: inline;
width: calc(100% - 70px);
padding-left: 10px;
}
</style>