* 增加Docker开发环境

* 更新Dockerfile构建文件,升级NodeJS依赖版本。
 * 遵循ESLint重新格式化代码,修复部分警告
 * 登录Token失效增加登出提示
 * 网络请求问题增加错误错误提示
 * 升级UI依赖库
This commit is contained in:
yaziming
2020-06-19 16:57:00 +08:00
parent 04a94d2546
commit c671d113c9
129 changed files with 18222 additions and 14180 deletions

View File

@@ -10,7 +10,7 @@
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__info">版权所有
<a class="link-type" href="https://crawlab.cn/" target="_blank">Crawlab Team</a>
<a class="link-type" href="https://wallstreetcn.com" target="_blank">华尔街见闻</a>
</div>
<div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">请检查您输入的网址是否正确请点击以下按钮返回主页或者发送错误报告</div>
@@ -22,19 +22,14 @@
<script>
export default {
name: 'Page404',
computed: {
message () {
return '这个页面似乎不存在......'
export default {
name: 'Page404',
computed: {
message() {
return '网管说这个页面你不能进......'
}
}
},
mounted () {
// remove loading-placeholder
const elLoading = document.querySelector('#loading-placeholder')
elLoading.remove()
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>

View File

@@ -8,31 +8,30 @@
>
<el-card>
<div class="title" :title="lang === 'zh' ? c.title_cn : c.title_en">
{{lang === 'zh' ? c.title_cn : c.title_en}}
{{ lang === 'zh' ? c.title_cn : c.title_en }}
</div>
<div class="rating block">
<span class="label">{{$t('Difficulty')}}: </span>
<span class="label">{{ $t('Difficulty') }}: </span>
<el-rate
v-model="c.difficulty"
disabled
>
</el-rate>
/>
</div>
<div class="achieved block">
<span class="label">{{$t('Status')}}: </span>
<span class="label">{{ $t('Status') }}: </span>
<div class="content">
<div v-if="c.achieved" class="status is-achieved">
<i class="fa fa-check-square-o"></i>
<span>{{$t('Achieved')}}</span>
<i class="fa fa-check-square-o" />
<span>{{ $t('Achieved') }}</span>
</div>
<div v-else class="status is-not-achieved">
<i class="fa fa-square-o"></i>
<span>{{$t('Not Achieved')}}</span>
<i class="fa fa-square-o" />
<span>{{ $t('Not Achieved') }}</span>
</div>
</div>
</div>
<div class="description">
{{lang === 'zh' ? c.description_cn : c.description_en}}
{{ lang === 'zh' ? c.description_cn : c.description_en }}
</div>
<div class="actions">
<el-button
@@ -42,7 +41,7 @@
icon="el-icon-check"
disabled
>
{{$t('Achieved')}}
{{ $t('Achieved') }}
</el-button>
<el-button
v-else
@@ -51,7 +50,7 @@
icon="el-icon-s-flag"
@click="onStartChallenge(c)"
>
{{$t('Start Challenge')}}
{{ $t('Start Challenge') }}
</el-button>
</div>
</el-card>
@@ -61,40 +60,40 @@
</template>
<script>
import {
mapState
} from 'vuex'
export default {
name: 'ChallengeList',
data () {
return {
challenges: []
}
},
computed: {
...mapState('lang', [
'lang'
])
},
methods: {
async getData () {
await this.$request.post('/challenges-check')
const res = await this.$request.get('/challenges')
this.challenges = res.data.data || []
},
onStartChallenge (c) {
if (c.path) {
this.$router.push(c.path)
} else {
this.$message.success(this.$t('You have started the challenge.'))
import {
mapState
} from 'vuex'
export default {
name: 'ChallengeList',
data() {
return {
challenges: []
}
},
computed: {
...mapState('lang', [
'lang'
])
},
async created() {
await this.getData()
},
methods: {
async getData() {
await this.$request.post('/challenges-check')
const res = await this.$request.get('/challenges')
this.challenges = res.data.data || []
},
onStartChallenge(c) {
if (c.path) {
this.$router.push(c.path)
} else {
this.$message.success(this.$t('You have started the challenge.'))
}
this.$st.sendEv('挑战', '开始挑战')
}
this.$st.sendEv('挑战', '开始挑战')
}
},
async created () {
await this.getData()
}
}
</script>
<style scoped>

View File

@@ -5,9 +5,9 @@
</template>
<script>
export default {
name: 'NodeDetail'
}
export default {
name: 'NodeDetail'
}
</script>
<style scoped>

View File

@@ -2,157 +2,167 @@
<div class="app-container">
<!--filter-->
<div class="filter">
<el-input prefix-icon="el-icon-search"
:placeholder="$t('Search')"
class="filter-search"
v-model="filter.keyword"
@change="onSearch">
</el-input>
<el-input
v-model="filter.keyword"
prefix-icon="el-icon-search"
:placeholder="$t('Search')"
class="filter-search"
@change="onSearch"
/>
<div class="right">
<el-button type="success"
icon="el-icon-refresh"
class="refresh"
@click="onRefresh">
{{$t('Refresh')}}
<el-button
type="success"
icon="el-icon-refresh"
class="refresh"
@click="onRefresh"
>
{{ $t('Refresh') }}
</el-button>
</div>
</div>
<!--table list-->
<el-table :data="filteredTableData"
class="table"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
border>
<el-table
:data="filteredTableData"
class="table"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
border
>
<template v-for="col in columns">
<el-table-column v-if="col.name === 'spider_name'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
align="center"
:width="col.width">
<el-table-column
v-if="col.name === 'spider_name'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
align="center"
:width="col.width"
>
<template slot-scope="scope">
<a class="a-tag" href="javascript:" @click="onClickSpider(scope.row)">{{scope.row[col.name]}}</a>
<a class="a-tag" href="javascript:" @click="onClickSpider(scope.row)">{{ scope.row[col.name] }}</a>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'node_id'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
align="center"
:width="col.width">
<el-table-column
v-else-if="col.name === 'node_id'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
align="center"
:width="col.width"
>
<template slot-scope="scope">
<a class="a-tag" href="javascript:" @click="onClickNode(scope.row)">{{scope.row[col.name]}}</a>
<a class="a-tag" href="javascript:" @click="onClickNode(scope.row)">{{ scope.row[col.name] }}</a>
</template>
</el-table-column>
<el-table-column v-else
:key="col.name"
:property="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
align="center"
:width="col.width">
</el-table-column>
<el-table-column
v-else
:key="col.name"
:property="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
align="center"
:width="col.width"
/>
</template>
<el-table-column :label="$t('Action')" align="center" width="160">
<template slot-scope="scope">
<el-tooltip :content="$t('View')" placement="top">
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)"></el-button>
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)" />
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
@current-change="onPageChange"
@size-change="onPageChange"
:current-page.sync="pagination.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size.sync="pagination.pageSize"
layout="sizes, prev, pager, next"
:total="deployList.length">
</el-pagination>
:total="deployList.length"
@current-change="onPageChange"
@size-change="onPageChange"
/>
</div>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'DeployList',
data () {
return {
pagination: {
pageNum: 0,
pageSize: 10
export default {
name: 'DeployList',
data() {
return {
pagination: {
pageNum: 0,
pageSize: 10
},
filter: {
keyword: ''
},
// tableData,
columns: [
// { name: 'version', label: 'Version', width: '180' },
// { name: 'ip', label: 'IP', width: '160' },
// { name: 'port', label: 'Port', width: '80' },
{ name: 'finish_ts', label: 'Time', width: '180' },
{ name: 'spider_name', label: 'Spider', width: '180', sortable: true },
{ name: 'node_id', label: 'Node', width: 'auto' }
],
nodeFormRules: {
name: [{ required: true, message: 'Required Field', trigger: 'change' }]
}
}
},
computed: {
...mapState('deploy', [
'deployList',
'deployForm'
]),
filteredTableData() {
return this.deployList.filter(d => {
if (!this.filter.keyword) return true
for (let i = 0; i < this.columns.length; i++) {
const colName = this.columns[i].name
if (d[colName] && d[colName].toLowerCase().indexOf(this.filter.keyword.toLowerCase()) > -1) {
return true
}
}
return false
})
.filter((d, index) => {
// pagination
const { pageNum, pageSize } = this.pagination
return (pageSize * (pageNum - 1) <= index) && (index < pageSize * pageNum)
})
}
},
created() {
this.$store.dispatch('deploy/getDeployList')
},
methods: {
onSearch(value) {
console.log(value)
},
filter: {
keyword: ''
onRefresh() {
this.$store.dispatch('deploy/getDeployList')
this.$st.sendEv('部署', '刷新')
},
// tableData,
columns: [
// { name: 'version', label: 'Version', width: '180' },
// { name: 'ip', label: 'IP', width: '160' },
// { name: 'port', label: 'Port', width: '80' },
{ name: 'finish_ts', label: 'Time', width: '180' },
{ name: 'spider_name', label: 'Spider', width: '180', sortable: true },
{ name: 'node_id', label: 'Node', width: 'auto' }
],
nodeFormRules: {
name: [{ required: true, message: 'Required Field', trigger: 'change' }]
onView(row) {
this.$router.push(`/deploys/${row._id}`)
},
onClickSpider(row) {
this.$router.push(`/spiders/${row.spider_id}`)
},
onClickNode(row) {
this.$router.push(`/nodes/${row.node_id}`)
},
onPageChange() {
this.$store.dispatch('deploy/getDeployList')
}
}
},
computed: {
...mapState('deploy', [
'deployList',
'deployForm'
]),
filteredTableData () {
return this.deployList.filter(d => {
if (!this.filter.keyword) return true
for (let i = 0; i < this.columns.length; i++) {
const colName = this.columns[i].name
if (d[colName] && d[colName].toLowerCase().indexOf(this.filter.keyword.toLowerCase()) > -1) {
return true
}
}
return false
})
.filter((d, index) => {
// pagination
const { pageNum, pageSize } = this.pagination
return (pageSize * (pageNum - 1) <= index) && (index < pageSize * pageNum)
})
}
},
methods: {
onSearch (value) {
console.log(value)
},
onRefresh () {
this.$store.dispatch('deploy/getDeployList')
this.$st.sendEv('部署', '刷新')
},
onView (row) {
this.$router.push(`/deploys/${row._id}`)
},
onClickSpider (row) {
this.$router.push(`/spiders/${row.spider_id}`)
},
onClickNode (row) {
this.$router.push(`/nodes/${row.node_id}`)
},
onPageChange () {
this.$store.dispatch('deploy/getDeployList')
}
},
created () {
this.$store.dispatch('deploy/getDeployList')
}
}
</script>
<style scoped lang="scss">

View File

@@ -1,37 +1,24 @@
<template>
<div class="app-container disclaimer">
<el-card>
<div v-html="text"></div>
<div v-html="text" />
</el-card>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import showdown from 'showdown'
import {
mapState
} from 'vuex'
import showdown from 'showdown'
export default {
name: 'Disclaimer',
computed: {
...mapState('lang', [
'lang'
]),
text () {
if (!this.converter) return ''
if (this.lang === 'zh') {
return this.converter.makeHtml(this.textZh)
} else {
return this.converter.makeHtml(this.textEn)
}
}
},
data () {
const converter = new showdown.Converter()
return {
converter,
textEn: `
export default {
name: 'Disclaimer',
data() {
const converter = new showdown.Converter()
return {
converter,
textEn: `
# Disclaimer
This Disclaimer and privacy protection statement (hereinafter referred to as "disclaimer statement" or "this statement") is applicable to the series of software (hereinafter referred to as "crawlab") developed by crawlab development group (hereinafter referred to as "development group") after you read this statement, if you do not agree with any terms in this statement or have doubts about this statement, please stop using our software immediately. If you have started or are using crawlab, you have read and agree to all terms of this statement.
@@ -45,7 +32,7 @@ This Disclaimer and privacy protection statement (hereinafter referred to as "di
7. Copyright of the system: the crawleb development team owns the intellectual property rights, copyrights, copyrights and use rights for all developed or jointly developed products, which are protected by applicable intellectual property rights, copyrights, trademarks, service trademarks, patents or other laws.
8. Communication: any company or individual who publishes or disseminates our software on the Internet is allowed, but the crawlab development team shall not be responsible for any legal and criminal events that may be caused by the company or individual disseminating the software.
`,
textZh: `
textZh: `
# 免责声明
本免责及隐私保护声明(下简称“免责声明”或“本声明”)适用于 Crawlab 开发组 (以下简称“开发组”)研发的系列软件(以下简称"Crawlab") 在您阅读本声明后若不同意此声明中的任何条款,或对本声明存在质疑,请立刻停止使用我们的软件。若您已经开始或正在使用 Crawlab则表示您已阅读并同意本声明的所有条款之约定。
@@ -59,14 +46,28 @@ This Disclaimer and privacy protection statement (hereinafter referred to as "di
7. 系统的版权Crawlab 开发组对所有开发的或合作开发的产品拥有知识产权,著作权,版权和使用权,这些产品受到适用的知识产权、版权、商标、服务商标、专利或其他法律的保护。
8. 传播:任何公司或个人在网络上发布,传播我们软件的行为都是允许的,但因公司或个人传播软件可能造成的任何法律和刑事事件 Crawlab 开发组不负任何责任。
`
}
},
computed: {
...mapState('lang', [
'lang'
]),
text() {
if (!this.converter) return ''
if (this.lang === 'zh') {
return this.converter.makeHtml(this.textZh)
} else {
return this.converter.makeHtml(this.textEn)
}
}
},
mounted() {
this.$request.put('/actions', {
type: 'view_disclaimer'
})
}
},
mounted () {
this.$request.put('/actions', {
type: 'view_disclaimer'
})
}
}
</script>
<style scoped>

View File

@@ -51,9 +51,9 @@
required
>
<el-input
v-model="form.content"
type="textarea"
rows="5"
v-model="form.content"
:placeholder="$t('Please enter your feedback content')"
/>
</el-form-item>
@@ -76,7 +76,7 @@
:disabled="isLoading"
@click="submit"
>
{{$t('Submit')}}
{{ $t('Submit') }}
</el-button>
</div>
</el-form-item>
@@ -87,65 +87,65 @@
</template>
<script>
import axios from 'axios'
import {
mapState
} from 'vuex'
import axios from 'axios'
import {
mapState
} from 'vuex'
export default {
name: 'Feedback',
data () {
return {
form: {
email: '',
wechat: '',
content: '',
rating: 0
},
isLoading: false
}
},
computed: {
...mapState('lang', [
'lang'
])
},
methods: {
submit () {
this.$refs['form'].validate(async valid => {
if (!valid) return
this.isLoading = true
try {
const res = await axios.put(process.env.VUE_APP_CRAWLAB_BASE_URL + '/feedback', {
uid: localStorage.getItem('uid'),
sid: sessionStorage.getItem('sid'),
email: this.form.email,
wechat: this.form.wechat,
content: this.form.content,
rating: this.form.rating,
v: sessionStorage.getItem('v')
})
if (res && res.data.error) {
this.$message.error(res.data.error)
return
export default {
name: 'Feedback',
data() {
return {
form: {
email: '',
wechat: '',
content: '',
rating: 0
},
isLoading: false
}
},
computed: {
...mapState('lang', [
'lang'
])
},
methods: {
submit() {
this.$refs['form'].validate(async valid => {
if (!valid) return
this.isLoading = true
try {
const res = await axios.put(process.env.VUE_APP_CRAWLAB_BASE_URL + '/feedback', {
uid: localStorage.getItem('uid'),
sid: sessionStorage.getItem('sid'),
email: this.form.email,
wechat: this.form.wechat,
content: this.form.content,
rating: this.form.rating,
v: sessionStorage.getItem('v')
})
if (res && res.data.error) {
this.$message.error(res.data.error)
return
}
this.form = {
email: '',
wechat: '',
content: '',
rating: 0
}
this.$message.success(this.$t('Submitted successfully'))
} catch (e) {
this.$message.error(e.toString())
} finally {
this.isLoading = false
}
this.form = {
email: '',
wechat: '',
content: '',
rating: 0
}
this.$message.success(this.$t('Submitted successfully'))
} catch (e) {
this.$message.error(e.toString())
} finally {
this.isLoading = false
}
this.$st.sendEv('反馈', '提交反馈')
})
this.$st.sendEv('反馈', '提交反馈')
})
}
}
}
}
</script>
<style scoped>

View File

@@ -2,18 +2,18 @@
<div class="app-container">
<el-row>
<ul class="metric-list">
<li class="metric-item" v-for="m in metrics" @click="onClickMetric(m)" :key="m.name">
<li v-for="m in metrics" :key="m.name" class="metric-item" @click="onClickMetric(m)">
<div class="metric-icon" :class="m.color">
<!-- <font-awesome-icon :icon="m.icon"/>-->
<i :class="m.icon"></i>
<i :class="m.icon" />
</div>
<div class="metric-content" :class="m.color">
<div class="metric-wrapper">
<div class="metric-number">
{{overviewStats[m.name]}}
{{ overviewStats[m.name] }}
</div>
<div class="metric-name">
{{$t(m.label)}}
{{ $t(m.label) }}
</div>
</div>
</div>
@@ -35,76 +35,76 @@
</el-row>
<el-row>
<el-card shadow="hover">
<h4 class="title">{{$t('Daily New Tasks')}}</h4>
<div id="echarts-daily-tasks" class="echarts-box"></div>
<h4 class="title">{{ $t('Daily New Tasks') }}</h4>
<div id="echarts-daily-tasks" class="echarts-box" />
</el-card>
</el-row>
</div>
</template>
<script>
import request from '../../api/request'
import echarts from 'echarts'
import request from '../../api/request'
import echarts from 'echarts'
export default {
name: 'Home',
data () {
return {
echarts: {},
overviewStats: {},
dailyTasks: [],
metrics: [
{ name: 'task_count', label: 'Total Tasks', icon: 'fa fa-check', color: 'blue', path: 'tasks' },
{ name: 'spider_count', label: 'Spiders', icon: 'fa fa-bug', color: 'green', path: 'spiders' },
{ name: 'active_node_count', label: 'Active Nodes', icon: 'fa fa-server', color: 'red', path: 'nodes' },
{ name: 'schedule_count', label: 'Schedules', icon: 'fa fa-clock-o', color: 'orange', path: 'schedules' },
{ name: 'project_count', label: 'Projects', icon: 'fa fa-code-fork', color: 'grey', path: 'projects' }
]
}
},
methods: {
initEchartsDailyTasks () {
const option = {
xAxis: {
type: 'category',
data: this.dailyTasks.map(d => d.date)
},
yAxis: {
type: 'value'
},
series: [{
data: this.dailyTasks.map(d => d.task_count),
type: 'line',
areaStyle: {},
smooth: true
}],
tooltip: {
trigger: 'axis',
show: true
}
export default {
name: 'Home',
data() {
return {
echarts: {},
overviewStats: {},
dailyTasks: [],
metrics: [
{ name: 'task_count', label: 'Total Tasks', icon: 'fa fa-check', color: 'blue', path: 'tasks' },
{ name: 'spider_count', label: 'Spiders', icon: 'fa fa-bug', color: 'green', path: 'spiders' },
{ name: 'active_node_count', label: 'Active Nodes', icon: 'fa fa-server', color: 'red', path: 'nodes' },
{ name: 'schedule_count', label: 'Schedules', icon: 'fa fa-clock-o', color: 'orange', path: 'schedules' },
{ name: 'project_count', label: 'Projects', icon: 'fa fa-code-fork', color: 'grey', path: 'projects' }
]
}
this.echarts.dailyTasks = echarts.init(this.$el.querySelector('#echarts-daily-tasks'))
this.echarts.dailyTasks.setOption(option)
},
onClickMetric (m) {
this.$router.push(`/${m.path}`)
}
},
created () {
request.get('/stats/home')
.then(response => {
// overview stats
this.overviewStats = response.data.data.overview
created() {
request.get('/stats/home')
.then(response => {
// overview stats
this.overviewStats = response.data.data.overview
// daily tasks
this.dailyTasks = response.data.data.daily
this.initEchartsDailyTasks()
})
},
mounted () {
// daily tasks
this.dailyTasks = response.data.data.daily
this.initEchartsDailyTasks()
})
},
mounted() {
// this.$ba.trackPageview('/')
},
methods: {
initEchartsDailyTasks() {
const option = {
xAxis: {
type: 'category',
data: this.dailyTasks.map(d => d.date)
},
yAxis: {
type: 'value'
},
series: [{
data: this.dailyTasks.map(d => d.task_count),
type: 'line',
areaStyle: {},
smooth: true
}],
tooltip: {
trigger: 'axis',
show: true
}
}
this.echarts.dailyTasks = echarts.init(this.$el.querySelector('#echarts-daily-tasks'))
this.echarts.dailyTasks.setOption(option)
},
onClickMetric(m) {
this.$router.push(`/${m.path}`)
}
}
}
}
</script>
<style scoped lang="scss">

View File

@@ -1,16 +1,16 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"></div>
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<!--sidebar-->
<sidebar class="sidebar-container"/>
<sidebar class="sidebar-container" />
<!--./sidebar-->
<!--main container-->
<div class="main-container">
<navbar/>
<tags-view/>
<app-main/>
<navbar />
<tags-view />
<app-main />
</div>
<!--./main container-->
@@ -19,7 +19,7 @@
<el-tooltip
:content="$t('Click to view related Documentation')"
>
<i class="el-icon-question" @click="onClickDocumentation"></i>
<i class="el-icon-question" @click="onClickDocumentation" />
</el-tooltip>
<el-drawer
:title="$t('Related Documentation')"
@@ -27,7 +27,7 @@
:before-close="onCloseDocumentation"
size="300px"
>
<documentation/>
<documentation />
</el-drawer>
</div>
<!--./documentation-->
@@ -35,63 +35,63 @@
</template>
<script>
import {
Navbar,
Sidebar,
AppMain,
TagsView
} from './components'
import ResizeMixin from './mixin/ResizeHandler'
import Documentation from '../../components/Documentation/Documentation'
export default {
name: 'Layout',
components: {
Documentation,
import {
Navbar,
Sidebar,
TagsView,
AppMain
},
mixins: [ResizeMixin],
data () {
return {
isShowDocumentation: false
}
},
computed: {
sidebar () {
return this.$store.state.app.sidebar
AppMain,
TagsView
} from './components'
import ResizeMixin from './mixin/ResizeHandler'
import Documentation from '../../components/Documentation/Documentation'
export default {
name: 'Layout',
components: {
Documentation,
Navbar,
Sidebar,
TagsView,
AppMain
},
device () {
return this.$store.state.app.device
},
classObj () {
mixins: [ResizeMixin],
data() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
isShowDocumentation: false
}
},
computed: {
sidebar() {
return this.$store.state.app.sidebar
},
device() {
return this.$store.state.app.device
},
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
}
},
async created() {
await this.$store.dispatch('doc/getDocData')
},
methods: {
handleClickOutside() {
this.$store.dispatch('CloseSideBar', { withoutAnimation: false })
},
onClickDocumentation() {
this.isShowDocumentation = true
this.$st.sendEv('全局', '打开右侧文档')
},
onCloseDocumentation() {
this.isShowDocumentation = false
this.$st.sendEv('全局', '关闭右侧文档')
}
}
},
methods: {
handleClickOutside () {
this.$store.dispatch('CloseSideBar', { withoutAnimation: false })
},
onClickDocumentation () {
this.isShowDocumentation = true
this.$st.sendEv('全局', '打开右侧文档')
},
onCloseDocumentation () {
this.isShowDocumentation = false
this.$st.sendEv('全局', '关闭右侧文档')
}
},
async created () {
await this.$store.dispatch('doc/getDocData')
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>

View File

@@ -2,21 +2,21 @@
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<!-- or name="fade" -->
<router-view :key="key"></router-view>
<router-view :key="key" />
<!--<router-view/>-->
</transition>
</section>
</template>
<script>
export default {
name: 'AppMain',
computed: {
key () {
return this.$route.name !== undefined ? this.$route.name + +new Date() : this.$route + +new Date()
export default {
name: 'AppMain',
computed: {
key() {
return this.$route.name !== undefined ? this.$route.name + +new Date() : this.$route + +new Date()
}
}
}
}
</script>
<style scoped>

View File

@@ -6,45 +6,37 @@
>
<el-tabs v-model="activeTabName">
<el-tab-pane :label="$t('Release Note')" name="release-note">
<div class="content markdown-body" v-html="latestReleaseNoteHtml">
</div>
<div class="content markdown-body" v-html="latestReleaseNoteHtml" />
<template slot="footer">
<el-button type="primary" size="small" @click="isLatestReleaseNoteVisible = false">{{$t('Ok')}}</el-button>
<el-button type="primary" size="small" @click="isLatestReleaseNoteVisible = false">{{ $t('Ok') }}</el-button>
</template>
</el-tab-pane>
<el-tab-pane :label="$t('How to Upgrade')" name="how-to-upgrade">
<div class="content markdown-body" v-html="howToUpgradeHtml">
</div>
<div class="content markdown-body" v-html="howToUpgradeHtml" />
</el-tab-pane>
</el-tabs>
<template slot="footer">
<el-button type="primary" size="small" @click="isLatestReleaseNoteVisible = false">{{$t('Ok')}}</el-button>
<el-button type="primary" size="small" @click="isLatestReleaseNoteVisible = false">{{ $t('Ok') }}</el-button>
</template>
</el-dialog>
<hamburger :toggle-click="toggleSideBar" :is-active="sidebar.opened" class="hamburger-container"/>
<breadcrumb class="breadcrumb"/>
<hamburger :toggle-click="toggleSideBar" :is-active="sidebar.opened" class="hamburger-container" />
<breadcrumb class="breadcrumb" />
<el-dropdown class="avatar-container right" trigger="click">
<span class="el-dropdown-link">
{{username}}
<i class="el-icon-arrow-down el-icon--right"></i>
{{ username }}
<i class="el-icon-arrow-down el-icon--right" />
</span>
<el-dropdown-menu slot="dropdown" class="user-dropdown">
<el-dropdown-item>
<span style="display:block;" @click="() => this.$router.push('/disclaimer')">{{$t('Disclaimer')}}</span>
</el-dropdown-item>
<el-dropdown-item>
<span style="display:block;" @click="() => this.$router.push('/feedback')">{{$t('Feedback')}}</span>
</el-dropdown-item>
<el-dropdown-item>
<span style="display:block;" @click="logout">{{$t('Logout')}}</span>
<span style="display:block;" @click="logout">{{ $t('Logout') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-dropdown class="lang-list right" trigger="click">
<span class="el-dropdown-link">
{{$t($store.getters['lang/lang'])}}
<i class="el-icon-arrow-down el-icon--right"></i>
{{ $t($store.getters['lang/lang']) }}
<i class="el-icon-arrow-down el-icon--right" />
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="setLang('zh')">
@@ -57,14 +49,14 @@
</el-dropdown>
<div class="documentation right">
<a href="http://docs.crawlab.cn" target="_blank">
<font-awesome-icon :icon="['far', 'question-circle']"/>
<span style="margin-left: 5px;">{{$t('Documentation')}}</span>
<font-awesome-icon :icon="['far', 'question-circle']" />
<span style="margin-left: 5px;">{{ $t('Documentation') }}</span>
</a>
</div>
<div v-if="isUpgradable" class="upgrade right" @click="onClickUpgrade">
<font-awesome-icon :icon="['fas', 'arrow-up']"/>
<font-awesome-icon :icon="['fas', 'arrow-up']" />
<el-badge is-dot>
<span style="margin-left: 5px;">{{$t('Upgrade')}}</span>
<span style="margin-left: 5px;">{{ $t('Upgrade') }}</span>
</el-badge>
</div>
<el-popover
@@ -72,13 +64,13 @@
trigger="click"
>
<div style="margin-bottom: 5px">
<label>{{$t('Add Wechat to join discussion group')}}</label>
<label>{{ $t('Add Wechat to join discussion group') }}</label>
</div>
<div>
<img class="wechat-img" src="http://static-docs.crawlab.cn/wechat.jpg">
</div>
<div slot="reference">
<i class="fa fa-wechat"></i>
<i class="fa fa-wechat" />
</div>
</el-popover>
<div class="github right">
@@ -98,29 +90,29 @@
</template>
<script>
import {
mapState,
mapGetters
} from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
import GithubButton from 'vue-github-button'
import showdown from 'showdown'
import 'github-markdown-css/github-markdown.css'
import {
mapState,
mapGetters
} from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
import GithubButton from 'vue-github-button'
import showdown from 'showdown'
import 'github-markdown-css/github-markdown.css'
export default {
components: {
Breadcrumb,
Hamburger,
GithubButton
},
data () {
const converter = new showdown.Converter()
return {
isLatestReleaseNoteVisible: false,
converter,
activeTabName: 'release-note',
howToUpgradeHtmlZh: `
export default {
components: {
Breadcrumb,
Hamburger,
GithubButton
},
data() {
const converter = new showdown.Converter()
return {
isLatestReleaseNoteVisible: false,
converter,
activeTabName: 'release-note',
howToUpgradeHtmlZh: `
### Docker 部署
\`\`\`bash
# 拉取最新镜像
@@ -139,7 +131,7 @@ docker-compose up -d
2. 重新构建前后端应用
3. 启动前后端应用
`,
howToUpgradeHtmlEn: `
howToUpgradeHtmlEn: `
### Docker Deployment
\`\`\`bash
# pull the latest image
@@ -157,80 +149,80 @@ docker-compose up -d
2. Build frontend and backend applications
3. Start frontend and backend applications
`
}
},
computed: {
...mapState('version', [
'latestRelease'
]),
...mapState('lang', [
'lang'
]),
...mapGetters([
'sidebar',
'avatar'
]),
username () {
if (!this.$store.getters['user/userInfo']) return this.$t('User')
if (!this.$store.getters['user/userInfo'].username) return this.$t('User')
return this.$store.getters['user/userInfo'].username
},
isUpgradable () {
if (!this.latestRelease.name) return false
const currentVersion = sessionStorage.getItem('v')
const latestVersion = this.latestRelease.name.replace('v', '')
if (!latestVersion || !currentVersion) return false
const currentVersionList = currentVersion.split('.')
const latestVersionList = latestVersion.split('.')
for (let i = 0; i < currentVersionList.length; i++) {
let nc = Number(currentVersionList[i])
let nl = Number(latestVersionList[i])
if (isNaN(nl)) nl = 0
if (nc < nl) return true
}
return false
},
latestReleaseNoteHtml () {
if (!this.latestRelease.body) return ''
const body = this.latestRelease.body
return this.converter.makeHtml(body)
},
howToUpgradeHtml () {
if (this.lang === 'zh') {
return this.converter.makeHtml(this.howToUpgradeHtmlZh)
} else if (this.lang === 'en') {
return this.converter.makeHtml(this.howToUpgradeHtmlEn)
} else {
return ''
}
}
},
methods: {
toggleSideBar () {
this.$store.dispatch('ToggleSideBar')
},
logout () {
this.$store.dispatch('user/logout')
this.$store.dispatch('delAllViews')
this.$router.push('/login')
this.$st.sendEv('全局', '登出')
},
setLang (lang) {
window.localStorage.setItem('lang', lang)
this.$i18n.locale = lang
this.$store.commit('lang/SET_LANG', lang)
computed: {
...mapState('version', [
'latestRelease'
]),
...mapState('lang', [
'lang'
]),
...mapGetters([
'sidebar',
'avatar'
]),
username() {
if (!this.$store.getters['user/userInfo']) return this.$t('User')
if (!this.$store.getters['user/userInfo'].username) return this.$t('User')
return this.$store.getters['user/userInfo'].username
},
isUpgradable() {
if (!this.latestRelease.name) return false
this.$st.sendEv('全局', '切换中英文', lang)
const currentVersion = sessionStorage.getItem('v')
const latestVersion = this.latestRelease.name.replace('v', '')
if (!latestVersion || !currentVersion) return false
const currentVersionList = currentVersion.split('.')
const latestVersionList = latestVersion.split('.')
for (let i = 0; i < currentVersionList.length; i++) {
const nc = Number(currentVersionList[i])
let nl = Number(latestVersionList[i])
if (isNaN(nl)) nl = 0
if (nc < nl) return true
}
return false
},
latestReleaseNoteHtml() {
if (!this.latestRelease.body) return ''
const body = this.latestRelease.body
return this.converter.makeHtml(body)
},
howToUpgradeHtml() {
if (this.lang === 'zh') {
return this.converter.makeHtml(this.howToUpgradeHtmlZh)
} else if (this.lang === 'en') {
return this.converter.makeHtml(this.howToUpgradeHtmlEn)
} else {
return ''
}
}
},
onClickUpgrade () {
this.isLatestReleaseNoteVisible = true
this.$st.sendEv('全局', '点击版本升级')
methods: {
toggleSideBar() {
this.$store.dispatch('ToggleSideBar')
},
logout() {
this.$store.dispatch('user/logout')
this.$store.dispatch('delAllViews')
this.$router.push('/login')
this.$st.sendEv('全局', '登出')
},
setLang(lang) {
window.localStorage.setItem('lang', lang)
this.$i18n.locale = lang
this.$store.commit('lang/SET_LANG', lang)
this.$st.sendEv('全局', '切换中英文', lang)
},
onClickUpgrade() {
this.isLatestReleaseNoteVisible = true
this.$st.sendEv('全局', '点击版本升级')
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>

View File

@@ -1,34 +1,34 @@
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render (h, context) {
const { icon, title } = context.props
const vnodes = []
if (icon) {
// vnodes.push(<svg-icon icon-class={icon}/>)
const style = {
'margin-right': '5px',
'z-index': 999
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
vnodes.push(<span class={icon} style={style}/>)
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = []
if (title) {
vnodes.push(<span class="title" slot='title'>{(title)}</span>)
if (icon) {
// vnodes.push(<svg-icon icon-class={icon}/>)
const style = {
'margin-right': '5px',
'z-index': 999
}
vnodes.push(<span class={icon} style={style}/>)
}
if (title) {
vnodes.push(<span class='title' slot='title'>{(title)}</span>)
}
return vnodes
}
return vnodes
}
}
</script>

View File

@@ -1,35 +1,35 @@
<template>
<!-- eslint-disable vue/require-component-is -->
<component v-bind="linkProps(to)">
<slot/>
<slot />
</component>
</template>
<script>
import { isExternal } from '@/utils/validate'
import { isExternal } from '@/utils/validate'
export default {
props: {
to: {
type: String,
required: true
}
},
methods: {
linkProps (url) {
if (isExternal(url)) {
return {
is: 'a',
href: url,
target: '_blank',
rel: 'noopener'
}
export default {
props: {
to: {
type: String,
required: true
}
return {
is: 'router-link',
to: url
},
methods: {
linkProps(url) {
if (isExternal(url)) {
return {
is: 'a',
href: url,
target: '_blank',
rel: 'noopener'
}
}
return {
is: 'router-link',
to: url
}
}
}
}
}
</script>

View File

@@ -2,31 +2,36 @@
<div v-if="!item.hidden&&item.children" class="menu-wrapper">
<template
v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"
>
<app-link :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item v-if="onlyOneChild.meta" :icon="onlyOneChild.meta.icon||item.meta.icon"
:title="$t(onlyOneChild.meta.title)"/>
<item
v-if="onlyOneChild.meta"
:icon="onlyOneChild.meta.icon||item.meta.icon"
:title="$t(onlyOneChild.meta.title)"
/>
</el-menu-item>
</app-link>
</template>
<el-submenu v-else :index="resolvePath(item.path)">
<template slot="title">
<item v-if="item.meta" :icon="item.meta.icon" :title="$t(item.meta.title)"/>
<item v-if="item.meta" :icon="item.meta.icon" :title="$t(item.meta.title)" />
</template>
<template v-for="child in item.children">
<sidebar-item
v-if="!child.hidden&&child.children&&child.children.length>0"
:is-nest="true"
:item="child"
:key="child.path"
:base-path="resolvePath(child.path)"
class="nest-menu"/>
<app-link v-else :to="resolvePath(child.path)" :key="child.name">
v-if="!child.hidden&&child.children&&child.children.length>0"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
<app-link v-else :key="child.name" :to="resolvePath(child.path)">
<el-menu-item :index="resolvePath(child.path)">
<item v-if="child.meta" :icon="child.meta.icon" :title="$t(child.meta.title)"/>
<item v-if="child.meta" :icon="child.meta.icon" :title="$t(child.meta.title)" />
</el-menu-item>
</app-link>
</template>
@@ -36,70 +41,70 @@
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
props: {
// route object
item: {
type: Object,
required: true
export default {
name: 'SidebarItem',
components: { Item, AppLink },
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
isNest: {
type: Boolean,
default: false
data() {
return {
onlyOneChild: null
}
},
basePath: {
type: String,
default: ''
}
},
data () {
return {
onlyOneChild: null
}
},
methods: {
hasOneShowingChild (children, parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
methods: {
hasOneShowingChild(children, parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
return true
return false
},
resolvePath(routePath) {
if (this.isExternalLink(routePath)) {
return routePath
}
return path.resolve(this.basePath, routePath)
},
isExternalLink(routePath) {
return isExternal(routePath)
}
return false
},
resolvePath (routePath) {
if (this.isExternalLink(routePath)) {
return routePath
}
return path.resolve(this.basePath, routePath)
},
isExternalLink (routePath) {
return isExternal(routePath)
}
}
}
</script>
<style scoped>

View File

@@ -1,7 +1,7 @@
<template>
<el-scrollbar wrap-class="scrollbar-wrapper">
<div class="sidebar-logo" :class="isCollapse ? 'collapsed' : ''">
<span>C</span><span v-show="!isCollapse">rawlab<span class="version">v{{version}}</span></span>
<span>C</span><span v-show="!isCollapse">rawlab<span class="version">v{{ version }}</span></span>
</div>
<el-menu
:show-timeout="200"
@@ -14,8 +14,8 @@
>
<sidebar-item
v-for="route in routes"
:class="route.path.replace('/', '')"
:key="route.path"
:class="route.path.replace('/', '')"
:item="route"
:base-path="route.path"
/>
@@ -24,48 +24,49 @@
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import variables from '@/styles/variables.scss'
import SidebarItem from './SidebarItem'
import { mapState, mapGetters } from 'vuex'
import variables from '@/styles/variables.scss'
import SidebarItem from './SidebarItem'
export default {
components: { SidebarItem },
computed: {
...mapState('user', [
'adminPaths'
]),
...mapGetters([
'sidebar'
]),
routeLevel1 () {
let pathArray = this.$route.path.split('/')
return `/${pathArray[1]}`
export default {
components: { SidebarItem },
data() {
return {}
},
routes () {
return this.$router.options.routes.filter(d => {
const role = this.$store.getters['user/userInfo'].role
if (role === 'admin') return true
return !this.adminPaths.includes(d.path)
})
computed: {
...mapState('user', [
'adminPaths'
]),
...mapGetters([
'sidebar'
]),
routeLevel1() {
const pathArray = this.$route.path.split('/')
return `/${pathArray[1]}`
},
routes() {
return this.$router.options.routes.filter(d => {
const role = this.$store.getters['user/userInfo'].role
if (role === 'admin') return true
return !this.adminPaths.includes(d.path)
})
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
},
version() {
return this.$store.state.version.version || window.sessionStorage.getItem('v')
}
},
variables () {
return variables
mounted() {
},
isCollapse () {
return !this.sidebar.opened
},
version () {
return this.$store.state.version.version || window.sessionStorage.getItem('v')
async created() {
}
},
data () {
return {}
},
async created () {
},
mounted () {
}
}
</script>
<style>

View File

@@ -4,16 +4,17 @@
<router-link
v-for="tag in visitedViews"
ref="tag"
:key="tag.path"
:class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
:key="tag.path"
tag="span"
class="tags-view-item"
@click.native="clickSelectedTag(tag)"
@click.middle.native="closeSelectedTag(tag)"
@contextmenu.prevent.native="openMenu(tag,$event)">
@contextmenu.prevent.native="openMenu(tag,$event)"
>
{{ $t(generateTitle(tag.title)) }}
<span v-if="!tag.meta.affix" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)"/>
<span v-if="!tag.meta.affix" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
@@ -28,172 +29,172 @@
</template>
<script>
import ScrollPane from '@/components/ScrollPane'
import { generateTitle } from '@/utils/i18n'
import path from 'path'
import ScrollPane from '@/components/ScrollPane'
import { generateTitle } from '@/utils/i18n'
import path from 'path'
export default {
components: { ScrollPane },
data () {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: []
}
},
computed: {
visitedViews () {
return this.$store.state.tagsView.visitedViews
export default {
components: { ScrollPane },
data() {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: []
}
},
routers () {
return this.$store.state.permission ? this.$store.state.permission.routers : []
}
},
watch: {
$route () {
computed: {
visitedViews() {
return this.$store.state.tagsView.visitedViews
},
routers() {
return this.$store.state.permission ? this.$store.state.permission.routers : []
}
},
watch: {
$route() {
this.addTags()
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
mounted() {
this.initTags()
this.addTags()
this.moveToCurrentTag()
},
visible (value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
mounted () {
this.initTags()
this.addTags()
},
methods: {
generateTitle, // generateTitle by vue-i18n
isActive (route) {
return route.path === this.$route.path
},
filterAffixTags (routes, basePath = '/') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
tags.push({
path: path.resolve(basePath, route.path),
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
methods: {
generateTitle, // generateTitle by vue-i18n
isActive(route) {
return route.path === this.$route.path
},
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
tags.push({
path: path.resolve(basePath, route.path),
name: route.name,
meta: { ...route.meta }
})
}
}
})
return tags
},
initTags () {
const affixTags = this.affixTags = this.filterAffixTags(this.routers)
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('addVisitedView', tag)
}
}
},
addTags () {
const { name } = this.$route
if (name) {
this.$store.dispatch('addView', this.$route)
}
return false
},
moveToCurrentTag () {
const tags = this.$refs.tag
if (tags) {
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('updateVisitedView', this.$route)
}
break
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
}
},
refreshSelectedTag (view) {
this.$store.dispatch('delCachedView', view).then(() => {
const { fullPath } = view
console.log('fullPath', fullPath)
this.$nextTick(() => {
this.$router.replace({
path: fullPath
return tags
},
initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routers)
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('addVisitedView', tag)
}
}
},
addTags() {
const { name } = this.$route
if (name) {
this.$store.dispatch('addView', this.$route)
}
return false
},
moveToCurrentTag() {
const tags = this.$refs.tag
if (tags) {
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('updateVisitedView', this.$route)
}
break
}
}
})
}
},
refreshSelectedTag(view) {
this.$store.dispatch('delCachedView', view).then(() => {
const { fullPath } = view
console.log('fullPath', fullPath)
this.$nextTick(() => {
this.$router.replace({
path: fullPath
})
})
})
})
},
clickSelectedTag (tag) {
this.$st.sendEv('全局', '点击标签', tag.name)
},
closeSelectedTag (view) {
this.$store.dispatch('delView', view).then(({ visitedViews }) => {
if (this.isActive(view)) {
},
clickSelectedTag(tag) {
this.$st.sendEv('全局', '点击标签', tag.name)
},
closeSelectedTag(view) {
this.$store.dispatch('delView', view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag)
this.$store.dispatch('delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$store.dispatch('delAllViews').then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === view.path)) {
return
}
this.toLastView(visitedViews)
})
},
toLastView(visitedViews) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView)
} else {
// You can set another route
this.$router.push('/')
}
})
},
closeOthersTags () {
this.$router.push(this.selectedTag)
this.$store.dispatch('delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags (view) {
this.$store.dispatch('delAllViews').then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === view.path)) {
return
},
openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
this.left = maxLeft
} else {
this.left = left
}
this.toLastView(visitedViews)
})
},
toLastView (visitedViews) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView)
} else {
// You can set another route
this.$router.push('/')
}
},
openMenu (tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
this.top = e.clientY
if (left > maxLeft) {
this.left = maxLeft
} else {
this.left = left
this.visible = true
this.selectedTag = tag
},
closeMenu() {
this.visible = false
}
this.top = e.clientY
this.visible = true
this.selectedTag = tag
},
closeMenu () {
this.visible = false
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>

View File

@@ -6,16 +6,16 @@ const RATIO = 3
export default {
watch: {
$route (route) {
$route(route) {
if (this.device === 'mobile' && this.sidebar.opened) {
store.dispatch('CloseSideBar', { withoutAnimation: false })
}
}
},
beforeMount () {
beforeMount() {
window.addEventListener('resize', this.resizeHandler)
},
mounted () {
mounted() {
const isMobile = this.isMobile()
if (isMobile) {
store.dispatch('ToggleDevice', 'mobile')
@@ -23,11 +23,11 @@ export default {
}
},
methods: {
isMobile () {
isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - RATIO < WIDTH
},
resizeHandler () {
resizeHandler() {
if (!document.hidden) {
const isMobile = this.isMobile()
store.dispatch('ToggleDevice', isMobile ? 'mobile' : 'desktop')

View File

@@ -1,8 +1,14 @@
<template>
<div class="login-container">
<canvas id="canvas"></canvas>
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on"
label-position="left">
<canvas id="canvas" />
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
class="login-form"
auto-complete="on"
label-position="left"
>
<h3 class="title">
<span><img style="width:48px;margin-bottom:-5px;margin-right:2px" src="../../assets/logo.svg"></span>RAWLAB
</h3>
@@ -17,17 +23,18 @@
</el-form-item>
<el-form-item prop="password" style="margin-bottom: 28px;">
<el-input
:type="pwdType"
v-model="loginForm.password"
:type="pwdType"
name="password"
auto-complete="on"
:placeholder="$t('Password')"
@keyup.enter.native="onKeyEnter"/>
@keyup.enter.native="onKeyEnter"
/>
</el-form-item>
<el-form-item v-if="isSignUp" prop="confirmPassword" style="margin-bottom: 28px;">
<el-input
:type="pwdType"
v-model="loginForm.confirmPassword"
:type="pwdType"
name="password"
auto-complete="on"
:placeholder="$t('Confirm Password')"
@@ -43,43 +50,53 @@
/>
</el-form-item>
<el-form-item style="border: none">
<el-button v-if="isSignUp" :loading="loading" type="primary" style="width:100%;"
@click.native.prevent="handleSignup">
{{$t('Sign up')}}
<el-button
v-if="isSignUp"
:loading="loading"
type="primary"
style="width:100%;"
@click.native.prevent="handleSignup"
>
{{ $t('Sign up') }}
</el-button>
<el-button v-if="!isSignUp" :loading="loading" type="primary" style="width:100%;"
@click.native.prevent="handleLogin">
{{$t('Sign in')}}
<el-button
v-if="!isSignUp"
:loading="loading"
type="primary"
style="width:100%;"
@click.native.prevent="handleLogin"
>
{{ $t('Sign in') }}
</el-button>
</el-form-item>
<div class="alternatives">
<div class="left">
<span v-if="!isSignUp" class="forgot-password">{{$t('Forgot Password')}}</span>
<span v-if="!isSignUp" class="forgot-password">{{ $t('Forgot Password') }}</span>
</div>
<div class="right" v-if="setting.allow_register === 'Y'">
<span v-if="isSignUp">{{$t('Has Account')}}, </span>
<span v-if="isSignUp" class="sign-in" @click="$router.push('/login')">{{$t('Sign-in')}} ></span>
<span v-if="!isSignUp">{{$t('New to Crawlab')}}, </span>
<span v-if="!isSignUp" class="sign-up" @click="$router.push('/signup')">{{$t('Sign-up')}} ></span>
<div v-if="setting.allow_register === 'Y'" class="right">
<span v-if="isSignUp">{{ $t('Has Account') }}, </span>
<span v-if="isSignUp" class="sign-in" @click="$router.push('/login')">{{ $t('Sign-in') }} ></span>
<span v-if="!isSignUp">{{ $t('New to Crawlab') }}, </span>
<span v-if="!isSignUp" class="sign-up" @click="$router.push('/signup')">{{ $t('Sign-up') }} ></span>
</div>
</div>
<div class="tips">
<span>{{$t('Initial Username/Password')}}: admin/admin</span>
<span>{{ $t('Initial Username/Password') }}: admin/admin</span>
<a href="https://github.com/crawlab-team/crawlab" target="_blank" style="float:right">
<img src="https://img.shields.io/github/stars/crawlab-team/crawlab?logo=github">
</a>
</div>
<div class="lang">
<span @click="setLang('zh')" :class="lang==='zh'?'active':''">中文</span>
<span :class="lang==='zh'?'active':''" @click="setLang('zh')">中文</span>
|
<span @click="setLang('en')" :class="lang==='en'?'active':''">English</span>
<span :class="lang==='en'?'active':''" @click="setLang('en')">English</span>
</div>
<div class="documentation">
<a href="http://docs.crawlab.cn" target="_blank">{{$t('Documentation')}}</a>
<a href="http://docs.crawlab.cn" target="_blank">{{ $t('Documentation') }}</a>
</div>
<div v-if="isShowMobileWarning" class="mobile-warning">
<el-alert type="error" :closable="false">
{{$t('You are running on a mobile device, which is not optimized yet. Please try with a laptop or desktop.')}}
{{ $t('You are running on a mobile device, which is not optimized yet. Please try with a laptop or desktop.') }}
</el-alert>
</div>
</el-form>
@@ -87,258 +104,258 @@
</template>
<script>
import {
mapState
} from 'vuex'
import { isValidUsername } from '../../utils/validate'
import {
mapState
} from 'vuex'
import { isValidUsername } from '../../utils/validate'
export default {
name: 'Login',
data () {
const validateUsername = (rule, value, callback) => {
if (!isValidUsername(value)) {
callback(new Error(this.$t('Please enter the correct username')))
} else {
callback()
}
}
const validatePass = (rule, value, callback) => {
if (value.length < 5) {
callback(new Error(this.$t('Password length should be no shorter than 5')))
} else {
callback()
}
}
const validateConfirmPass = (rule, value, callback) => {
if (!this.isSignUp) return callback()
if (value !== this.loginForm.password) {
callback(new Error(this.$t('Two passwords must be the same')))
} else {
callback()
}
}
return {
loginForm: {
username: '',
password: '',
confirmPassword: '',
email: ''
},
loginRules: {
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
password: [{ required: true, trigger: 'blur', validator: validatePass }],
confirmPassword: [{ required: true, trigger: 'blur', validator: validateConfirmPass }]
},
loading: false,
pwdType: 'password',
isShowMobileWarning: false
}
},
computed: {
...mapState('setting', [
'setting'
]),
...mapState('lang', [
'lang'
]),
isSignUp () {
return this.$route.path === '/signup'
},
redirect () {
return this.$route.query.redirect
}
},
methods: {
handleLogin () {
this.$refs.loginForm.validate(async valid => {
if (!valid) return
this.loading = true
const res = await this.$store.dispatch('user/login', this.loginForm)
if (res.status === 200) {
// success
this.$router.push({ path: this.redirect || '/' })
this.$st.sendEv('全局', '登录', '成功')
await this.$store.dispatch('user/getInfo')
} else if (res.message === 'Network Error' || !res.response) {
// no response
this.$message({
type: 'error',
message: this.$t('No response from the server. Please make sure your server is running correctly. You can also refer to the documentation to solve this issue.'),
customClass: 'message-error',
duration: 5000
})
this.$st.sendEv('全局', '登录', '服务器无响应')
} else if (res.response.status === 401) {
// incorrect username or password
this.$message({
type: 'error',
message: '[401] ' + this.$t('Incorrect username or password')
})
this.$st.sendEv('全局', '登录', '用户名密码错误')
export default {
name: 'Login',
data() {
const validateUsername = (rule, value, callback) => {
if (!isValidUsername(value)) {
callback(new Error(this.$t('Please enter the correct username')))
} else {
// other error
this.$message({
type: 'error',
message: `[${res.response.status}] ${res.response.data.error}`,
customClass: 'message-error'
})
this.$st.sendEv('全局', '登录', '其他错误')
}
this.loading = false
})
},
handleSignup () {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
this.$store.dispatch('user/register', this.loginForm).then(() => {
this.handleLogin()
this.loading = false
this.$st.sendEv('全局', '注册', '成功')
}).catch(err => {
this.$message.error(this.$t(err))
this.loading = false
this.$st.sendEv('全局', '注册', '失败')
})
}
})
},
onKeyEnter () {
const func = this.isSignUp ? this.handleSignup : this.handleLogin
func()
},
setLang (lang) {
window.localStorage.setItem('lang', lang)
this.$set(this.$i18n, 'locale', lang)
this.$store.commit('lang/SET_LANG', lang)
}
},
mounted () {
if (window.innerWidth >= 1024) {
initCanvas()
} else {
this.isShowMobileWarning = true
}
}
}
const initCanvas = () => {
var canvas = document.getElementById('canvas')
var ctx = canvas.getContext('2d')
resize()
window.onresize = resize
function resize () {
canvas.width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
canvas.height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
}
var RAF = (function () {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
window.setTimeout(callback, 1000 / 60)
}
})()
// 鼠标活动时,获取鼠标坐标
var warea = { x: null, y: null, max: 20000 }
// window.onmousemove = function (e) {
// e = e || window.event
//
// warea.x = e.clientX
// warea.y = e.clientY
// }
// window.onmouseout = function (e) {
// warea.x = null
// warea.y = null
// }
// 添加粒子
// xy为粒子坐标xa, ya为粒子xy轴加速度max为连线的最大距离
var dots = []
for (var i = 0; i < 300; i++) {
var x = Math.random() * canvas.width
var y = Math.random() * canvas.height
var xa = Math.random() * 2 - 1
var ya = Math.random() * 2 - 1
dots.push({
x: x,
y: y,
xa: xa,
ya: ya,
max: 6000
})
}
// 延迟100秒开始执行动画如果立即执行有时位置计算会出错
setTimeout(function () {
animate()
}, 100)
// 每一帧循环的逻辑
function animate () {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 将鼠标坐标添加进去,产生一个用于比对距离的点数组
var ndots = [warea].concat(dots)
dots.forEach(function (dot) {
// 粒子位移
dot.x += dot.xa
dot.y += dot.ya
// 遇到边界将加速度反向
dot.xa *= (dot.x > canvas.width || dot.x < 0) ? -1 : 1
dot.ya *= (dot.y > canvas.height || dot.y < 0) ? -1 : 1
// 绘制点
ctx.fillRect(dot.x - 0.5, dot.y - 0.5, 1, 1)
// 循环比对粒子间的距离
for (var i = 0; i < ndots.length; i++) {
var d2 = ndots[i]
if (dot === d2 || d2.x === null || d2.y === null) continue
var xc = dot.x - d2.x
var yc = dot.y - d2.y
// 两个粒子之间的距离
var dis = xc * xc + yc * yc
// 距离比
var ratio
// 如果两个粒子之间的距离小于粒子对象的max值则在两个粒子间画线
if (dis < d2.max) {
// 如果是鼠标,则让粒子向鼠标的位置移动
if (d2 === warea && dis > (d2.max / 2)) {
dot.x -= xc * 0.03
dot.y -= yc * 0.03
}
// 计算距离比
ratio = (d2.max - dis) / d2.max
// 画线
ctx.beginPath()
ctx.lineWidth = ratio / 2
// 线条颜色
ctx.strokeStyle = 'rgba(64,158,255,' + (ratio + 0.1) + ')'
ctx.moveTo(dot.x, dot.y)
ctx.lineTo(d2.x, d2.y)
ctx.stroke()
callback()
}
}
// 将已经计算过的粒子从数组中删除
ndots.splice(ndots.indexOf(dot), 1)
})
RAF(animate)
const validatePass = (rule, value, callback) => {
if (value.length < 5) {
callback(new Error(this.$t('Password length should be no shorter than 5')))
} else {
callback()
}
}
const validateConfirmPass = (rule, value, callback) => {
if (!this.isSignUp) return callback()
if (value !== this.loginForm.password) {
callback(new Error(this.$t('Two passwords must be the same')))
} else {
callback()
}
}
return {
loginForm: {
username: '',
password: '',
confirmPassword: '',
email: ''
},
loginRules: {
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
password: [{ required: true, trigger: 'blur', validator: validatePass }],
confirmPassword: [{ required: true, trigger: 'blur', validator: validateConfirmPass }]
},
loading: false,
pwdType: 'password',
isShowMobileWarning: false
}
},
computed: {
...mapState('setting', [
'setting'
]),
...mapState('lang', [
'lang'
]),
isSignUp() {
return this.$route.path === '/signup'
},
redirect() {
return this.$route.query.redirect
}
},
mounted() {
if (window.innerWidth >= 1024) {
initCanvas()
} else {
this.isShowMobileWarning = true
}
},
methods: {
handleLogin() {
this.$refs.loginForm.validate(async valid => {
if (!valid) return
this.loading = true
const res = await this.$store.dispatch('user/login', this.loginForm)
if (res.status === 200) {
// success
this.$router.push({ path: this.redirect || '/' })
this.$st.sendEv('全局', '登录', '成功')
await this.$store.dispatch('user/getInfo')
} else if (res.message === 'Network Error' || !res.response) {
// no response
this.$message({
type: 'error',
message: this.$t('No response from the server. Please make sure your server is running correctly. You can also refer to the documentation to solve this issue.'),
customClass: 'message-error',
duration: 5000
})
this.$st.sendEv('全局', '登录', '服务器无响应')
} else if (res.response.status === 401) {
// incorrect username or password
this.$message({
type: 'error',
message: '[401] ' + this.$t('Incorrect username or password')
})
this.$st.sendEv('全局', '登录', '用户名密码错误')
} else {
// other error
this.$message({
type: 'error',
message: `[${res.response.status}] ${res.response.data.error}`,
customClass: 'message-error'
})
this.$st.sendEv('全局', '登录', '其他错误')
}
this.loading = false
})
},
handleSignup() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
this.$store.dispatch('user/register', this.loginForm).then(() => {
this.handleLogin()
this.loading = false
this.$st.sendEv('全局', '注册', '成功')
}).catch(err => {
this.$message.error(this.$t(err))
this.loading = false
this.$st.sendEv('全局', '注册', '失败')
})
}
})
},
onKeyEnter() {
const func = this.isSignUp ? this.handleSignup : this.handleLogin
func()
},
setLang(lang) {
window.localStorage.setItem('lang', lang)
this.$set(this.$i18n, 'locale', lang)
this.$store.commit('lang/SET_LANG', lang)
}
}
}
const initCanvas = () => {
var canvas = document.getElementById('canvas')
var ctx = canvas.getContext('2d')
resize()
window.onresize = resize
function resize() {
canvas.width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
canvas.height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
}
var RAF = (function() {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) {
window.setTimeout(callback, 1000 / 60)
}
})()
// 鼠标活动时,获取鼠标坐标
var warea = { x: null, y: null, max: 20000 }
// window.onmousemove = function (e) {
// e = e || window.event
//
// warea.x = e.clientX
// warea.y = e.clientY
// }
// window.onmouseout = function (e) {
// warea.x = null
// warea.y = null
// }
// 添加粒子
// xy为粒子坐标xa, ya为粒子xy轴加速度max为连线的最大距离
var dots = []
for (var i = 0; i < 300; i++) {
var x = Math.random() * canvas.width
var y = Math.random() * canvas.height
var xa = Math.random() * 2 - 1
var ya = Math.random() * 2 - 1
dots.push({
x: x,
y: y,
xa: xa,
ya: ya,
max: 6000
})
}
// 延迟100秒开始执行动画如果立即执行有时位置计算会出错
setTimeout(function() {
animate()
}, 100)
// 每一帧循环的逻辑
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 将鼠标坐标添加进去,产生一个用于比对距离的点数组
var ndots = [warea].concat(dots)
dots.forEach(function(dot) {
// 粒子位移
dot.x += dot.xa
dot.y += dot.ya
// 遇到边界将加速度反向
dot.xa *= (dot.x > canvas.width || dot.x < 0) ? -1 : 1
dot.ya *= (dot.y > canvas.height || dot.y < 0) ? -1 : 1
// 绘制点
ctx.fillRect(dot.x - 0.5, dot.y - 0.5, 1, 1)
// 循环比对粒子间的距离
for (var i = 0; i < ndots.length; i++) {
var d2 = ndots[i]
if (dot === d2 || d2.x === null || d2.y === null) continue
var xc = dot.x - d2.x
var yc = dot.y - d2.y
// 两个粒子之间的距离
var dis = xc * xc + yc * yc
// 距离比
var ratio
// 如果两个粒子之间的距离小于粒子对象的max值则在两个粒子间画线
if (dis < d2.max) {
// 如果是鼠标,则让粒子向鼠标的位置移动
if (d2 === warea && dis > (d2.max / 2)) {
dot.x -= xc * 0.03
dot.y -= yc * 0.03
}
// 计算距离比
ratio = (d2.max - dis) / d2.max
// 画线
ctx.beginPath()
ctx.lineWidth = ratio / 2
// 线条颜色
ctx.strokeStyle = 'rgba(64,158,255,' + (ratio + 0.1) + ')'
ctx.moveTo(dot.x, dot.y)
ctx.lineTo(d2.x, d2.y)
ctx.stroke()
}
}
// 将已经计算过的粒子从数组中删除
ndots.splice(ndots.indexOf(dot), 1)
})
RAF(animate)
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss">

View File

@@ -11,123 +11,123 @@
<!--selector-->
<div class="selector">
<label class="label">{{$t('Node')}}: </label>
<el-select size="small" v-model="nodeForm._id" @change="onNodeChange">
<el-option v-for="op in nodeList" :key="op._id" :value="op._id" :label="op.name"></el-option>
<label class="label">{{ $t('Node') }}: </label>
<el-select v-model="nodeForm._id" size="small" @change="onNodeChange">
<el-option v-for="op in nodeList" :key="op._id" :value="op._id" :label="op.name" />
</el-select>
</div>
<!--tabs-->
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="border-card">
<el-tabs v-model="activeTabName" type="border-card" @tab-click="onTabClick">
<el-tab-pane :label="$t('Overview')" name="overview">
<node-overview></node-overview>
<node-overview />
</el-tab-pane>
<el-tab-pane :label="$t('Installation')" name="installation">
<node-installation></node-installation>
<node-installation />
</el-tab-pane>
<el-tab-pane :label="$t('Deployed Spiders')" name="spiders" v-if="false">
{{$t('Deployed Spiders')}}
<el-tab-pane v-if="false" :label="$t('Deployed Spiders')" name="spiders">
{{ $t('Deployed Spiders') }}
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import NodeOverview from '../../components/Overview/NodeOverview'
import NodeInstallation from '../../components/Node/NodeInstallation'
import {
mapState
} from 'vuex'
import NodeOverview from '../../components/Overview/NodeOverview'
import NodeInstallation from '../../components/Node/NodeInstallation'
export default {
name: 'NodeDetail',
components: {
NodeOverview,
NodeInstallation
},
data () {
return {
activeTabName: 'overview',
tourSteps: [
// overview
{
target: '.selector',
content: this.$t('Switch between different nodes.')
},
{
target: '.latest-tasks-wrapper',
content: this.$t('You can view the latest executed spider tasks.'),
params: {
placement: 'right'
export default {
name: 'NodeDetail',
components: {
NodeOverview,
NodeInstallation
},
data() {
return {
activeTabName: 'overview',
tourSteps: [
// overview
{
target: '.selector',
content: this.$t('Switch between different nodes.')
},
{
target: '.latest-tasks-wrapper',
content: this.$t('You can view the latest executed spider tasks.'),
params: {
placement: 'right'
}
},
{
target: '.node-info-view-wrapper',
content: this.$t('This is the detailed node info.'),
params: {
placement: 'left'
}
},
// installation
{
target: '#tab-installation',
content: this.$t('Here you can install<br> dependencies and modules<br> that are required<br> in your spiders.')
},
{
target: '.search-box',
content: this.$t('You can search dependencies in the search box and install them by clicking the "Install" button below.')
}
},
{
target: '.node-info-view-wrapper',
content: this.$t('This is the detailed node info.'),
params: {
placement: 'left'
],
tourCallbacks: {
onStop: () => {
this.$utils.tour.finishTour('node-detail')
},
onPreviousStep: (currentStep) => {
if (currentStep === 3) {
this.activeTabName = 'overview'
}
this.$utils.tour.prevStep('node-detail', currentStep)
},
onNextStep: (currentStep) => {
if (currentStep === 2) {
this.activeTabName = 'installation'
}
this.$utils.tour.nextStep('node-detail', currentStep)
}
},
// installation
{
target: '#tab-installation',
content: this.$t('Here you can install<br> dependencies and modules<br> that are required<br> in your spiders.')
},
{
target: '.search-box',
content: this.$t('You can search dependencies in the search box and install them by clicking the "Install" button below.')
}
],
tourCallbacks: {
onStop: () => {
this.$utils.tour.finishTour('node-detail')
},
onPreviousStep: (currentStep) => {
if (currentStep === 3) {
this.activeTabName = 'overview'
}
this.$utils.tour.prevStep('node-detail', currentStep)
},
onNextStep: (currentStep) => {
if (currentStep === 2) {
this.activeTabName = 'installation'
}
this.$utils.tour.nextStep('node-detail', currentStep)
}
}
}
},
computed: {
...mapState('node', [
'nodeList',
'nodeForm'
])
},
methods: {
onTabClick (tab) {
this.$st.sendEv('节点详情', '切换标签', tab.name)
},
onNodeChange (id) {
this.$router.push(`/nodes/${id}`)
this.$st.sendEv('节点详情', '切换节点')
}
},
created () {
// get list of nodes
this.$store.dispatch('node/getNodeList')
computed: {
...mapState('node', [
'nodeList',
'nodeForm'
])
},
created() {
// get list of nodes
this.$store.dispatch('node/getNodeList')
// get node basic info
this.$store.dispatch('node/getNodeData', this.$route.params.id)
// get node basic info
this.$store.dispatch('node/getNodeData', this.$route.params.id)
// get node task list
this.$store.dispatch('node/getTaskList', this.$route.params.id)
},
mounted () {
if (!this.$utils.tour.isFinishedTour('node-detail')) {
this.$utils.tour.startTour(this, 'node-detail')
// get node task list
this.$store.dispatch('node/getTaskList', this.$route.params.id)
},
mounted() {
if (!this.$utils.tour.isFinishedTour('node-detail')) {
this.$utils.tour.startTour(this, 'node-detail')
}
},
methods: {
onTabClick(tab) {
this.$st.sendEv('节点详情', '切换标签', tab.name)
},
onNodeChange(id) {
this.$router.push(`/nodes/${id}`)
this.$st.sendEv('节点详情', '切换节点')
}
}
}
}
</script>
<style scoped>

View File

@@ -6,13 +6,12 @@
width="720px"
>
<div
v-html="addNodeInstructionHtml"
class="content markdown-body"
>
</div>
v-html="addNodeInstructionHtml"
/>
</el-dialog>
<el-tabs type="border-card" v-model="activeTab">
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane :label="$t('Node List')">
<!--filter-->
<div class="filter-wrapper">
@@ -22,28 +21,30 @@
icon="el-icon-plus"
@click="onAddNode"
>
{{$t('Add Node')}}
{{ $t('Add Node') }}
</el-button>
</div>
<!--./filter-->
<!--table list-->
<el-table :data="filteredTableData"
class="table"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
border
@row-click="onRowClick"
@expand-change="onRowExpand">
<el-table
:data="filteredTableData"
class="table"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
border
@row-click="onRowClick"
@expand-change="onRowExpand"
>
<el-table-column type="expand">
<template slot-scope="scope">
<el-form class="node-detail" :model="scope.row" label-width="120px" inline>
<el-form-item :label="$t('OS')">
<span>{{scope.row.systemInfo ? getOSName(scope.row.systemInfo.os) : ''}}</span>
<span>{{ scope.row.systemInfo ? getOSName(scope.row.systemInfo.os) : '' }}</span>
</el-form-item>
<el-form-item :label="$t('ARCH')">
<span>{{scope.row.systemInfo ? scope.row.systemInfo.arch : ''}}</span>
<span>{{ scope.row.systemInfo ? scope.row.systemInfo.arch : '' }}</span>
</el-form-item>
<el-form-item :label="$t('Number of CPU')">
<span>{{scope.row.systemInfo ? scope.row.systemInfo.num_cpu : ''}}</span>
<span>{{ scope.row.systemInfo ? scope.row.systemInfo.num_cpu : '' }}</span>
</el-form-item>
</el-form>
<el-form class="node-detail executable" :model="scope.row" label-width="120px">
@@ -57,24 +58,24 @@
<el-tooltip :content="ex.path">
<div>
<template v-if="ex.file_name.match(/^python/)">
<font-awesome-icon :icon="['fab','python']"/>
<font-awesome-icon :icon="['fab','python']" />
</template>
<template v-else-if="ex.file_name.match(/^java/)">
<font-awesome-icon :icon="['fab','java']"/>
<font-awesome-icon :icon="['fab','java']" />
</template>
<template v-else-if="ex.file_name.match(/^bash$|^sh$/)">
<font-awesome-icon :icon="['fab','linux']"/>
<font-awesome-icon :icon="['fab','linux']" />
</template>
<template v-else-if="ex.file_name.match(/^node/)">
<font-awesome-icon :icon="['fab','node-js']"/>
<font-awesome-icon :icon="['fab','node-js']" />
</template>
<template v-else-if="ex.file_name.match(/^php/)">
<font-awesome-icon :icon="['fab','php']"/>
<font-awesome-icon :icon="['fab','php']" />
</template>
<template v-else>
<font-awesome-icon :icon="['fas', 'terminal']"/>
<font-awesome-icon :icon="['fas', 'terminal']" />
</template>
<span class="executable-label">{{ex.display_name}}</span>
<span class="executable-label">{{ ex.display_name }}</span>
</div>
</el-tooltip>
</li>
@@ -84,240 +85,250 @@
</template>
</el-table-column>
<template v-for="col in columns">
<el-table-column v-if="col.name === 'status'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:width="col.width">
<el-table-column
v-if="col.name === 'status'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:width="col.width"
>
<template slot-scope="scope">
<el-tag type="info" v-if="scope.row.status === 'offline'">{{$t('Offline')}}</el-tag>
<el-tag type="success" v-else-if="scope.row.status === 'online'">{{$t('Online')}}</el-tag>
<el-tag type="danger" v-else>{{$t('Unavailable')}}</el-tag>
<el-tag v-if="scope.row.status === 'offline'" type="info">{{ $t('Offline') }}</el-tag>
<el-tag v-else-if="scope.row.status === 'online'" type="success">{{ $t('Online') }}</el-tag>
<el-tag v-else type="danger">{{ $t('Unavailable') }}</el-tag>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'type'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:width="col.width">
<el-table-column
v-else-if="col.name === 'type'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:width="col.width"
>
<template slot-scope="scope">
<el-tag type="primary" v-if="scope.row.is_master">{{$t('Master')}}</el-tag>
<el-tag type="warning" v-else>{{$t('Worker')}}</el-tag>
<el-tag v-if="scope.row.is_master" type="primary">{{ $t('Master') }}</el-tag>
<el-tag v-else type="warning">{{ $t('Worker') }}</el-tag>
</template>
</el-table-column>
<el-table-column v-else
:key="col.name"
:property="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align || 'left'"
:width="col.width">
</el-table-column>
<el-table-column
v-else
:key="col.name"
:property="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align || 'left'"
:width="col.width"
/>
</template>
<el-table-column :label="$t('Action')" align="left" width="160" fixed="right">
<template slot-scope="scope">
<el-tooltip :content="$t('View')" placement="top">
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)"></el-button>
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)" />
</el-tooltip>
<el-tooltip :content="$t('Remove')" placement="top">
<el-button v-if="scope.row.status !== 'online'" type="danger" icon="el-icon-delete" size="mini"
@click="onRemove(scope.row)"></el-button>
<el-button
v-if="scope.row.status !== 'online'"
type="danger"
icon="el-icon-delete"
size="mini"
@click="onRemove(scope.row)"
/>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
@current-change="onPageChange"
@size-change="onPageChange"
:current-page.sync="pagination.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size.sync="pagination.pageSize"
layout="sizes, prev, pager, next"
:total="nodeList.length">
</el-pagination>
:total="nodeList.length"
@current-change="onPageChange"
@size-change="onPageChange"
/>
</div>
<!--./table list-->
</el-tab-pane>
<el-tab-pane :label="$t('Installation')">
<node-installation-matrix :active-tab="activeTab"/>
<node-installation-matrix :active-tab="activeTab" />
</el-tab-pane>
<el-tab-pane :label="$t('Network')">
<node-network :active-tab="activeTab"/>
<node-network :active-tab="activeTab" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import showdown from 'showdown'
import {
mapState
} from 'vuex'
import 'github-markdown-css/github-markdown.css'
import NodeNetwork from '../../components/Node/NodeNetwork'
import NodeInstallationMatrix from '../../components/Node/NodeInstallationMatrix'
import showdown from 'showdown'
import {
mapState
} from 'vuex'
import 'github-markdown-css/github-markdown.css'
import NodeNetwork from '../../components/Node/NodeNetwork'
import NodeInstallationMatrix from '../../components/Node/NodeInstallationMatrix'
export default {
name: 'NodeList',
components: { NodeInstallationMatrix, NodeNetwork },
data () {
return {
pagination: {
pageNum: 0,
pageSize: 10
},
isEditMode: false,
dialogVisible: false,
filter: {
keyword: ''
},
// tableData,
columns: [
{ name: 'name', label: 'Name', width: '220' },
{ name: 'ip', label: 'IP', width: '160' },
{ name: 'type', label: 'nodeList.type', width: '120' },
// { name: 'port', label: 'Port', width: '80' },
{ name: 'status', label: 'Status', width: '120' },
{ name: 'description', label: 'Description', width: 'auto' }
],
nodeFormRules: {
name: [{ required: true, message: 'Required Field', trigger: 'change' }]
},
activeTab: undefined,
isButtonClicked: false,
isShowAddNodeInstruction: false,
converter: new showdown.Converter(),
addNodeInstructionMarkdown: 'addNodeInstruction'
}
},
computed: {
...mapState('node', [
'nodeList',
'nodeForm'
]),
filteredTableData () {
return this.nodeList.filter(d => {
if (!this.filter.keyword) return true
for (let i = 0; i < this.columns.length; i++) {
const colName = this.columns[i].name
if (d[colName] && d[colName].toLowerCase().indexOf(this.filter.keyword.toLowerCase()) > -1) {
return true
export default {
name: 'NodeList',
components: { NodeInstallationMatrix, NodeNetwork },
data() {
return {
pagination: {
pageNum: 0,
pageSize: 10
},
isEditMode: false,
dialogVisible: false,
filter: {
keyword: ''
},
// tableData,
columns: [
{ name: 'name', label: 'Name', width: '220' },
{ name: 'ip', label: 'IP', width: '160' },
{ name: 'type', label: 'nodeList.type', width: '120' },
// { name: 'port', label: 'Port', width: '80' },
{ name: 'status', label: 'Status', width: '120' },
{ name: 'description', label: 'Description', width: 'auto' }
],
nodeFormRules: {
name: [{ required: true, message: 'Required Field', trigger: 'change' }]
},
activeTab: undefined,
isButtonClicked: false,
isShowAddNodeInstruction: false,
converter: new showdown.Converter(),
addNodeInstructionMarkdown: 'addNodeInstruction'
}
},
computed: {
...mapState('node', [
'nodeList',
'nodeForm'
]),
filteredTableData() {
return this.nodeList.filter(d => {
if (!this.filter.keyword) return true
for (let i = 0; i < this.columns.length; i++) {
const colName = this.columns[i].name
if (d[colName] && d[colName].toLowerCase().indexOf(this.filter.keyword.toLowerCase()) > -1) {
return true
}
}
}
return false
})
},
addNodeInstructionHtml () {
if (!this.converter) return
return this.converter.makeHtml(this.$t(this.addNodeInstructionMarkdown))
}
},
methods: {
onSearch () {
},
onAddNode () {
this.isShowAddNodeInstruction = true
},
// onAdd () {
// this.$store.commit('node/SET_NODE_FORM', [])
// this.isEditMode = false
// this.dialogVisible = true
// },
onRefresh () {
this.$store.dispatch('node/getNodeList')
this.$st.sendEv('节点列表', '刷新')
},
onSubmit () {
const vm = this
const formName = 'nodeForm'
this.$refs[formName].validate((valid) => {
if (valid) {
if (this.isEditMode) {
vm.$store.dispatch('node/editNode')
} else {
vm.$store.dispatch('node/addNode')
}
vm.dialogVisible = false
} else {
return false
}
})
})
},
addNodeInstructionHtml() {
if (!this.converter) return
return this.converter.makeHtml(this.$t(this.addNodeInstructionMarkdown))
}
},
onCancel () {
this.$store.commit('node/SET_NODE_FORM', {})
this.dialogVisible = false
},
onDialogClose () {
this.$store.commit('node/SET_NODE_FORM', {})
this.dialogVisible = false
},
onEdit (row) {
this.isEditMode = true
this.$store.commit('node/SET_NODE_FORM', row)
this.dialogVisible = true
},
onRemove (row) {
this.isButtonClicked = true
setTimeout(() => {
this.isButtonClicked = false
}, 100)
this.$confirm(this.$t('Are you sure to delete this node?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('node/deleteNode', row._id)
.then(() => {
this.$message({
type: 'success',
message: 'Deleted successfully'
})
})
this.$st.sendEv('节点列表', '删除节点')
})
},
onView (row) {
this.isButtonClicked = true
setTimeout(() => {
this.isButtonClicked = false
}, 100)
this.$router.push(`/nodes/${row._id}`)
this.$st.sendEv('节点列表', '查看节点')
},
onPageChange () {
created() {
this.$store.dispatch('node/getNodeList')
},
onRowExpand (row) {
this.$store.dispatch('node/getNodeSystemInfo', row._id)
},
onRowClick (row) {
if (this.isButtonClicked) return
this.onView(row)
},
getExecutables (row) {
if (!row.systemInfo || !row.systemInfo.executables) return []
return row.systemInfo.executables
},
getOSName (os) {
if (os === 'linux') {
return 'Linux'
} else if (os === 'windows') {
return 'Windows'
} else if (os === 'darwin') {
return 'Mac OS (darwin)'
} else {
return os
methods: {
onSearch() {
},
onAddNode() {
this.isShowAddNodeInstruction = true
},
// onAdd () {
// this.$store.commit('node/SET_NODE_FORM', [])
// this.isEditMode = false
// this.dialogVisible = true
// },
onRefresh() {
this.$store.dispatch('node/getNodeList')
this.$st.sendEv('节点列表', '刷新')
},
onSubmit() {
const vm = this
const formName = 'nodeForm'
this.$refs[formName].validate((valid) => {
if (valid) {
if (this.isEditMode) {
vm.$store.dispatch('node/editNode')
} else {
vm.$store.dispatch('node/addNode')
}
vm.dialogVisible = false
} else {
return false
}
})
},
onCancel() {
this.$store.commit('node/SET_NODE_FORM', {})
this.dialogVisible = false
},
onDialogClose() {
this.$store.commit('node/SET_NODE_FORM', {})
this.dialogVisible = false
},
onEdit(row) {
this.isEditMode = true
this.$store.commit('node/SET_NODE_FORM', row)
this.dialogVisible = true
},
onRemove(row) {
this.isButtonClicked = true
setTimeout(() => {
this.isButtonClicked = false
}, 100)
this.$confirm(this.$t('Are you sure to delete this node?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('node/deleteNode', row._id)
.then(() => {
this.$message({
type: 'success',
message: 'Deleted successfully'
})
})
this.$st.sendEv('节点列表', '删除节点')
})
},
onView(row) {
this.isButtonClicked = true
setTimeout(() => {
this.isButtonClicked = false
}, 100)
this.$router.push(`/nodes/${row._id}`)
this.$st.sendEv('节点列表', '查看节点')
},
onPageChange() {
this.$store.dispatch('node/getNodeList')
},
onRowExpand(row) {
this.$store.dispatch('node/getNodeSystemInfo', row._id)
},
onRowClick(row) {
if (this.isButtonClicked) return
this.onView(row)
},
getExecutables(row) {
if (!row.systemInfo || !row.systemInfo.executables) return []
return row.systemInfo.executables
},
getOSName(os) {
if (os === 'linux') {
return 'Linux'
} else if (os === 'windows') {
return 'Windows'
} else if (os === 'darwin') {
return 'Mac OS (darwin)'
} else {
return os
}
}
}
},
created () {
this.$store.dispatch('node/getNodeList')
}
}
</script>
<style scoped lang="scss">

View File

@@ -4,21 +4,24 @@
<el-dialog
:visible.sync="dialogVisible"
width="640px"
:before-close="onDialogClose">
<el-form label-width="180px"
class="add-form"
:model="projectForm"
:inline-message="true"
ref="projectForm"
label-position="right">
:before-close="onDialogClose"
>
<el-form
ref="projectForm"
label-width="180px"
class="add-form"
:model="projectForm"
:inline-message="true"
label-position="right"
>
<el-form-item :label="$t('Project Name')" prop="name" required>
<el-input id="name" v-model="projectForm.name" :placeholder="$t('Project Name')"></el-input>
<el-input id="name" v-model="projectForm.name" :placeholder="$t('Project Name')" />
</el-form-item>
<el-form-item :label="$t('Project Description')" prop="description">
<el-input
id="description"
type="textarea"
v-model="projectForm.description"
type="textarea"
:placeholder="$t('Project Description')"
/>
</el-form-item>
@@ -30,14 +33,13 @@
allow-create
filterable
multiple
>
</el-select>
/>
</el-form-item>
</el-form>
<!--取消保存-->
<span slot="footer" class="dialog-footer">
<el-button size="small" @click="onDialogClose">{{$t('Cancel')}}</el-button>
<el-button id="btn-submit" size="small" type="primary" @click="onAddSubmit">{{$t('Submit')}}</el-button>
<el-button size="small" @click="onDialogClose">{{ $t('Cancel') }}</el-button>
<el-button id="btn-submit" size="small" type="primary" @click="onAddSubmit">{{ $t('Submit') }}</el-button>
</span>
</el-dialog>
<!--./add popup-->
@@ -50,7 +52,7 @@
:placeholder="$t('Select Tag')"
@change="onFilterChange"
>
<el-option value="" :label="$t('All Tags')"/>
<el-option value="" :label="$t('All Tags')" />
<el-option
v-for="tag in projectTags"
:key="tag"
@@ -66,36 +68,36 @@
size="small"
@click="onAdd"
>
{{$t('Add Project')}}
{{ $t('Add Project') }}
</el-button>
</div>
</div>
<div class="content">
<div v-if="projectList.length === 0" class="empty-list">
{{ $t('You have no projects created. You can create a project by clicking the "Add" button.')}}
{{ $t('You have no projects created. You can create a project by clicking the "Add" button.') }}
</div>
<ul v-else class="list">
<li
class="item"
v-for="item in projectList.filter(d => d._id !== '000000000000000000000000')"
:key="item._id"
class="item"
@click="onView(item)"
>
<el-card
class="item-card"
>
<i v-if="!isNoProject(item)" class="btn-edit fa fa-edit" @click="onEdit(item)"></i>
<i v-if="!isNoProject(item)" class="btn-close fa fa-trash-o" @click="onRemove(item)"></i>
<i v-if="!isNoProject(item)" class="btn-edit fa fa-edit" @click="onEdit(item)" />
<i v-if="!isNoProject(item)" class="btn-close fa fa-trash-o" @click="onRemove(item)" />
<el-row>
<h4 class="title">{{ item.name }}</h4>
</el-row>
<el-row>
<div style="display: flex; justify-content: space-between">
<span class="spider-count">
{{$t('Spider Count')}}: {{ item.spiders.length }}
{{ $t('Spider Count') }}: {{ item.spiders.length }}
</span>
<span class="owner">
{{item.username}}
{{ item.username }}
</span>
</div>
</el-row>
@@ -124,124 +126,124 @@
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'ProjectList',
data () {
return {
defaultTags: [],
dialogVisible: false,
isClickAction: false,
filter: {
tag: ''
export default {
name: 'ProjectList',
data() {
return {
defaultTags: [],
dialogVisible: false,
isClickAction: false,
filter: {
tag: ''
}
}
},
computed: {
...mapState('project', [
'projectForm',
'projectList',
'projectTags'
])
},
async created() {
await this.$store.dispatch('project/getProjectList', this.filter)
await this.$store.dispatch('project/getProjectTags')
},
methods: {
onDialogClose() {
this.dialogVisible = false
},
onFilterChange() {
this.$store.dispatch('project/getProjectList', this.filter)
this.$st.sendEv('项目', '筛选项目')
},
onAdd() {
this.isEdit = false
this.dialogVisible = true
this.$store.commit('project/SET_PROJECT_FORM', { tags: [] })
this.$st.sendEv('项目', '添加项目')
},
onAddSubmit() {
this.$refs.projectForm.validate(res => {
if (res) {
const form = JSON.parse(JSON.stringify(this.projectForm))
if (this.isEdit) {
this.$request.post(`/projects/${this.projectForm._id}`, form).then(response => {
if (response.data.error) {
this.$message.error(response.data.error)
return
}
this.dialogVisible = false
this.$store.dispatch('project/getProjectList')
this.$message.success(this.$t('The project has been saved'))
})
} else {
this.$request.put('/projects', form).then(response => {
if (response.data.error) {
this.$message.error(response.data.error)
return
}
this.dialogVisible = false
this.$store.dispatch('project/getProjectList')
this.$message.success(this.$t('The project has been added'))
})
}
}
})
this.$st.sendEv('项目', '提交项目')
},
onEdit(row) {
this.isClickAction = true
setTimeout(() => {
this.isClickAction = false
}, 100)
this.$store.commit('project/SET_PROJECT_FORM', row)
this.dialogVisible = true
this.isEdit = true
this.$st.sendEv('项目', '修改项目')
},
onRemove(row) {
this.isClickAction = true
setTimeout(() => {
this.isClickAction = false
}, 100)
this.$confirm(this.$t('Are you sure to delete the project?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('project/removeProject', row._id)
.then(() => {
setTimeout(() => {
this.$store.dispatch('project/getProjectList')
this.$message.success(this.$t('The project has been removed'))
}, 100)
})
}).catch(() => {
})
this.$st.sendEv('项目', '删除项目')
},
onView(row) {
if (this.isClickAction) return
this.$router.push({
name: 'SpiderList',
params: {
project_id: row._id
}
})
},
isNoProject(row) {
return row._id === '000000000000000000000000'
}
}
},
computed: {
...mapState('project', [
'projectForm',
'projectList',
'projectTags'
])
},
methods: {
onDialogClose () {
this.dialogVisible = false
},
onFilterChange () {
this.$store.dispatch('project/getProjectList', this.filter)
this.$st.sendEv('项目', '筛选项目')
},
onAdd () {
this.isEdit = false
this.dialogVisible = true
this.$store.commit('project/SET_PROJECT_FORM', { tags: [] })
this.$st.sendEv('项目', '添加项目')
},
onAddSubmit () {
this.$refs.projectForm.validate(res => {
if (res) {
const form = JSON.parse(JSON.stringify(this.projectForm))
if (this.isEdit) {
this.$request.post(`/projects/${this.projectForm._id}`, form).then(response => {
if (response.data.error) {
this.$message.error(response.data.error)
return
}
this.dialogVisible = false
this.$store.dispatch('project/getProjectList')
this.$message.success(this.$t('The project has been saved'))
})
} else {
this.$request.put('/projects', form).then(response => {
if (response.data.error) {
this.$message.error(response.data.error)
return
}
this.dialogVisible = false
this.$store.dispatch('project/getProjectList')
this.$message.success(this.$t('The project has been added'))
})
}
}
})
this.$st.sendEv('项目', '提交项目')
},
onEdit (row) {
this.isClickAction = true
setTimeout(() => {
this.isClickAction = false
}, 100)
this.$store.commit('project/SET_PROJECT_FORM', row)
this.dialogVisible = true
this.isEdit = true
this.$st.sendEv('项目', '修改项目')
},
onRemove (row) {
this.isClickAction = true
setTimeout(() => {
this.isClickAction = false
}, 100)
this.$confirm(this.$t('Are you sure to delete the project?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('project/removeProject', row._id)
.then(() => {
setTimeout(() => {
this.$store.dispatch('project/getProjectList')
this.$message.success(this.$t('The project has been removed'))
}, 100)
})
}).catch(() => {
})
this.$st.sendEv('项目', '删除项目')
},
onView (row) {
if (this.isClickAction) return
this.$router.push({
name: 'SpiderList',
params: {
project_id: row._id
}
})
},
isNoProject (row) {
return row._id === '000000000000000000000000'
}
},
async created () {
await this.$store.dispatch('project/getProjectList', this.filter)
await this.$store.dispatch('project/getProjectTags')
}
}
</script>
<style scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -10,60 +10,67 @@
<!--./tour-->
<!--新增全局变量-->
<el-dialog :title="$t('Add Global Variable')"
:visible.sync="addDialogVisible">
<el-form label-width="80px" ref="globalVariableForm">
<el-dialog
:title="$t('Add Global Variable')"
:visible.sync="addDialogVisible"
>
<el-form ref="globalVariableForm" label-width="80px">
<el-form-item :label="$t('Key')">
<el-input size="small" v-model="globalVariableForm.key"/>
<el-input v-model="globalVariableForm.key" size="small" />
</el-form-item>
<el-form-item :label="$t('Value')">
<el-input size="small" v-model="globalVariableForm.value"/>
<el-input v-model="globalVariableForm.value" size="small" />
</el-form-item>
<el-form-item :label="$t('Remark')">
<el-input size="small" v-model="globalVariableForm.remark"/>
<el-input v-model="globalVariableForm.remark" size="small" />
</el-form-item>
<el-form-item>
<div style="text-align: right">
<el-button @click="addDialogVisible = false" type="danger" size="small">{{$t('Cancel')}}</el-button>
<el-button @click="addGlobalVariableHandle(false)" type="success" size="small">{{$t('Save')}}</el-button>
<el-button type="danger" size="small" @click="addDialogVisible = false">{{ $t('Cancel') }}</el-button>
<el-button type="success" size="small" @click="addGlobalVariableHandle(false)">{{ $t('Save') }}</el-button>
</div>
</el-form-item>
</el-form>
</el-dialog>
<!--./新增全局变量-->
<el-tabs v-model="activeName" @tab-click="tabActiveHandle" type="border-card">
<el-tabs v-model="activeName" type="border-card" @tab-click="tabActiveHandle">
<!--通用-->
<el-tab-pane :label="$t('General')" name="general">
<el-form :model="userInfo" class="setting-form" ref="setting-form" label-width="200px"
:rules="rulesNotification"
inline-message>
<el-form
ref="setting-form"
:model="userInfo"
class="setting-form"
label-width="200px"
:rules="rulesNotification"
inline-message
>
<el-form-item prop="username" :label="$t('Username')">
<el-input v-model="userInfo.username" disabled></el-input>
<el-input v-model="userInfo.username" disabled />
</el-form-item>
<el-form-item prop="password" :label="$t('Password')">
<el-input v-model="userInfo.password" type="password" :placeholder="$t('Password')"></el-input>
<el-input v-model="userInfo.password" type="password" :placeholder="$t('Password')" />
</el-form-item>
<el-form-item :label="$t('Allow Sending Statistics')">
<el-switch
v-model="isAllowSendingStatistics"
@change="onAllowSendingStatisticsChange"
active-color="#67C23A"
inactive-color="#909399"
@change="onAllowSendingStatisticsChange"
/>
</el-form-item>
<el-form-item :label="$t('Enable Tutorial')">
<el-switch
v-model="isEnableTutorial"
@change="onEnableTutorialChange"
active-color="#67C23A"
inactive-color="#909399"
@change="onEnableTutorialChange"
/>
</el-form-item>
<el-form-item>
<div style="text-align: right">
<el-button type="success" size="small" @click="saveUserInfo">
{{$t('Save')}}
{{ $t('Save') }}
</el-button>
</div>
</el-form-item>
@@ -73,44 +80,53 @@
<!--消息通知-->
<el-tab-pane :label="$t('Notifications')" name="notify">
<el-form :model="userInfo" class="setting-form" ref="setting-form" label-width="200px"
:rules="rulesNotification"
inline-message>
<el-form
ref="setting-form"
:model="userInfo"
class="setting-form"
label-width="200px"
:rules="rulesNotification"
inline-message
>
<el-form-item :label="$t('Notification Trigger Timing')">
<el-radio-group v-model="userInfo.setting.notification_trigger">
<el-radio label="notification_trigger_on_task_end">
{{$t('On Task End')}}
{{ $t('On Task End') }}
</el-radio>
<el-radio label="notification_trigger_on_task_error">
{{$t('On Task Error')}}
{{ $t('On Task Error') }}
</el-radio>
<el-radio label="notification_trigger_never">
{{$t('Never')}}
{{ $t('Never') }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="enabledNotifications" :label="$t('消息通知方式')">
<el-checkbox-group v-model="userInfo.setting.enabled_notifications">
<el-checkbox label="notification_type_mail">{{$t('邮件')}}</el-checkbox>
<el-checkbox label="notification_type_ding_talk">{{$t('钉钉')}}</el-checkbox>
<el-checkbox label="notification_type_wechat">{{$t('企业微信')}}</el-checkbox>
<el-checkbox label="notification_type_mail">{{ $t('邮件') }}</el-checkbox>
<el-checkbox label="notification_type_ding_talk">{{ $t('钉钉') }}</el-checkbox>
<el-checkbox label="notification_type_wechat">{{ $t('企业微信') }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item prop="email" :label="$t('Email')">
<el-input v-model="userInfo.email" :placeholder="$t('Email')"></el-input>
<el-input v-model="userInfo.email" :placeholder="$t('Email')" />
</el-form-item>
<el-form-item prop="setting.ding_talk_robot_webhook" :label="$t('DingTalk Robot Webhook')">
<el-input v-model="userInfo.setting.ding_talk_robot_webhook"
:placeholder="$t('DingTalk Robot Webhook')"></el-input>
<el-input
v-model="userInfo.setting.ding_talk_robot_webhook"
:placeholder="$t('DingTalk Robot Webhook')"
/>
</el-form-item>
<el-form-item prop="setting.wechat_robot_webhook" :label="$t('Wechat Robot Webhook')">
<el-input v-model="userInfo.setting.wechat_robot_webhook"
:placeholder="$t('Wechat Robot Webhook')"></el-input>
<el-input
v-model="userInfo.setting.wechat_robot_webhook"
:placeholder="$t('Wechat Robot Webhook')"
/>
</el-form-item>
<el-form-item>
<div style="text-align: right">
<el-button type="success" size="small" @click="saveUserInfo">
{{$t('Save')}}
{{ $t('Save') }}
</el-button>
</div>
</el-form-item>
@@ -120,8 +136,14 @@
<!--日志-->
<el-tab-pane :label="$t('Log')" name="log">
<el-form :model="userInfo" class="setting-form" ref="log-form" label-width="200px" :rules="rulesLog"
inline-message>
<el-form
ref="log-form"
:model="userInfo"
class="setting-form"
label-width="200px"
:rules="rulesLog"
inline-message
>
<el-form-item :label="$t('Error Regex Pattern')" prop="setting.error_regex_pattern">
<el-input
v-model="userInfo.setting.error_regex_pattern"
@@ -134,11 +156,11 @@
v-model="userInfo.setting.max_error_log"
clearable
>
<el-option :value="100" label="100"/>
<el-option :value="500" label="500"/>
<el-option :value="1000" label="1000"/>
<el-option :value="5000" label="5000"/>
<el-option :value="10000" label="10000"/>
<el-option :value="100" label="100" />
<el-option :value="500" label="500" />
<el-option :value="1000" label="1000" />
<el-option :value="5000" label="5000" />
<el-option :value="10000" label="10000" />
</el-select>
</el-form-item>
<el-form-item :label="$t('Log Expire Duration')" prop="setting.log_expire_duration">
@@ -146,22 +168,22 @@
v-model="userInfo.setting.log_expire_duration"
clearable
>
<el-option :value="99999999" :label="$t('No Expire')"/>
<el-option :value="3600" :label="'1 ' + $t('Hour')"/>
<el-option :value="3600 * 6" :label="'6 ' + $t('Hours')"/>
<el-option :value="3600 * 12" :label="'12 ' + $t('Hours')"/>
<el-option :value="3600 * 24" :label="'1 ' + $t('Day')"/>
<el-option :value="3600 * 24 * 7" :label="'7 ' + $t('Days')"/>
<el-option :value="3600 * 24 * 14" :label="'14 ' + $t('Days')"/>
<el-option :value="3600 * 24 * 30" :label="'30 ' + $t('Days')"/>
<el-option :value="3600 * 24 * 30 * 3" :label="'90 ' + $t('Days')"/>
<el-option :value="3600 * 24 * 30 * 6" :label="'180 ' + $t('Days')"/>
<el-option :value="0" :label="$t('No Expire')" />
<el-option :value="3600" :label="'1 ' + $t('Hour')" />
<el-option :value="3600 * 6" :label="'6 ' + $t('Hours')" />
<el-option :value="3600 * 12" :label="'12 ' + $t('Hours')" />
<el-option :value="3600 * 24" :label="'1 ' + $t('Day')" />
<el-option :value="3600 * 24 * 7" :label="'7 ' + $t('Days')" />
<el-option :value="3600 * 24 * 14" :label="'14 ' + $t('Days')" />
<el-option :value="3600 * 24 * 30" :label="'30 ' + $t('Days')" />
<el-option :value="3600 * 24 * 30 * 3" :label="'90 ' + $t('Days')" />
<el-option :value="3600 * 24 * 30 * 6" :label="'180 ' + $t('Days')" />
</el-select>
</el-form-item>
<el-form-item>
<div style="text-align: right">
<el-button type="success" size="small" @click="saveUserInfo">
{{$t('Save')}}
{{ $t('Save') }}
</el-button>
</div>
</el-form-item>
@@ -174,15 +196,14 @@
<input id="clipboard">
<el-alert
type="primary"
>
</el-alert>
/>
<div class="actions">
<el-button
size="small"
type="primary"
@click="onAddApiToken"
>
{{$t('Add')}}
{{ $t('Add') }}
</el-button>
</div>
<el-table
@@ -193,7 +214,7 @@
label="Token"
>
<template slot-scope="scope">
{{scope.row.visible ? scope.row.token : getMaskValue(scope.row.token)}}
{{ scope.row.visible ? scope.row.token : getMaskValue(scope.row.token) }}
</template>
</el-table-column>
<el-table-column
@@ -212,15 +233,15 @@
type="primary"
size="mini"
icon="el-icon-document-copy"
@click="copyToken(scope.row.token)"
circle
@click="copyToken(scope.row.token)"
/>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="onDeleteToken(scope.row)"
circle
@click="onDeleteToken(scope.row)"
/>
</template>
</el-table-column>
@@ -231,21 +252,28 @@
<!--全局变量-->
<el-tab-pane :label="$t('Global Variable')" name="global-variable">
<div style="text-align: right;margin-bottom: 10px">
<el-button size="small" @click="addGlobalVariableHandle(true)"
icon="el-icon-plus"
type="primary">
{{$t('Add')}}
<el-button
size="small"
icon="el-icon-plus"
type="primary"
@click="addGlobalVariableHandle(true)"
>
{{ $t('Add') }}
</el-button>
<el-button size="small" type="success" @click="saveUserInfo">{{$t('Save')}}</el-button>
<el-button size="small" type="success" @click="saveUserInfo">{{ $t('Save') }}</el-button>
</div>
<el-table :data="globalVariableList" border>
<el-table-column prop="key" :label="$t('Key')"/>
<el-table-column prop="value" :label="$t('Value')"/>
<el-table-column prop="remark" :label="$t('Remark')"/>
<el-table-column prop="key" :label="$t('Key')" />
<el-table-column prop="value" :label="$t('Value')" />
<el-table-column prop="remark" :label="$t('Remark')" />
<el-table-column prop="" :label="$t('Action')" width="80">
<template slot-scope="scope">
<el-button @click="deleteGlobalVariableHandle(scope.row._id)" icon="el-icon-delete" type="danger"
size="mini"></el-button>
<el-button
icon="el-icon-delete"
type="danger"
size="mini"
@click="deleteGlobalVariableHandle(scope.row._id)"
/>
</template>
</el-table-column>
</el-table>
@@ -256,242 +284,242 @@
</template>
<script>
import { mapState } from 'vuex'
import { mapState } from 'vuex'
export default {
name: 'Setting',
data () {
const validatePass = (rule, value, callback) => {
if (!value) return callback()
if (value.length < 5) {
callback(new Error(this.$t('Password length should be no shorter than 5')))
} else {
callback()
}
}
const validateEmail = (rule, value, callback) => {
if (!value) return callback()
if (!value.match(/.+@.+/i)) {
callback(new Error(this.$t('Email format invalid')))
} else {
callback()
}
}
const validateDingTalkRobotWebhook = (rule, value, callback) => {
if (!value) return callback()
if (!value.match(/^https:\/\/oapi.dingtalk.com\/robot\/send\?access_token=[a-f0-9]+/i)) {
callback(new Error(this.$t('DingTalk Robot Webhook format invalid')))
} else {
callback()
}
}
const validateWechatRobotWebhook = (rule, value, callback) => {
if (!value) return callback()
if (!value.match(/^https:\/\/qyapi.weixin.qq.com\/cgi-bin\/webhook\/send\?key=.+/i)) {
callback(new Error(this.$t('DingTalk Robot Webhook format invalid')))
} else {
callback()
}
}
return {
userInfo: { setting: { enabled_notifications: [] } },
rulesNotification: {
password: [{ trigger: 'blur', validator: validatePass }],
email: [{ trigger: 'blur', validator: validateEmail }],
'setting.ding_talk_robot_webhook': [{ trigger: 'blur', validator: validateDingTalkRobotWebhook }],
'setting.wechat_robot_webhook': [{ trigger: 'blur', validator: validateWechatRobotWebhook }]
},
rulesLog: {},
isShowDingTalkAppSecret: false,
activeName: 'general',
addDialogVisible: false,
tourSteps: [
{
target: '#tab-general',
content: this.$t('Here you can set your general settings.'),
params: {
placement: 'right'
}
},
{
target: '#tab-notify',
content: this.$t('In this tab you can configure your notification settings.')
},
{
target: '#tab-global-variable',
content: this.$t('Here you can add/edit/delete global environment variables which will be passed into your spider programs.')
}
],
tourCallbacks: {
onStop: () => {
this.$utils.tour.finishTour('setting')
},
onPreviousStep: (currentStep) => {
if (currentStep === 1) {
this.activeName = 'password'
} else if (currentStep === 2) {
this.activeName = 'notify'
}
this.$utils.tour.prevStep('setting', currentStep)
},
onNextStep: (currentStep) => {
if (currentStep === 0) {
this.activeName = 'notify'
} else if (currentStep === 1) {
this.activeName = 'global-variable'
}
this.$utils.tour.nextStep('setting', currentStep)
}
},
isAllowSendingStatistics: localStorage.getItem('useStats') === '1',
isEnableTutorial: localStorage.getItem('enableTutorial') === '1',
apiTokens: []
}
},
computed: {
...mapState('user', [
'globalVariableList',
'globalVariableForm'
])
},
watch: {
async userInfoStr () {
await this.saveUserInfo()
await this.$store.dispatch('user/getInfo')
}
},
methods: {
deleteGlobalVariableHandle (id) {
this.$confirm(this.$t('Are you sure to delete this global variable'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('user/deleteGlobalVariable', id).then(() => {
this.$store.dispatch('user/getGlobalVariable')
})
}).catch(() => {
})
},
addGlobalVariableHandle (isShow) {
if (isShow) {
this.addDialogVisible = true
return
}
this.$store.dispatch('user/addGlobalVariable')
.then(() => {
this.addDialogVisible = false
this.$st.sendEv('设置', '添加全局变量')
})
.then(() => {
this.$store.dispatch('user/getGlobalVariable')
})
},
getUserInfo () {
const data = localStorage.getItem('user_info')
if (!data) {
return {}
}
this.userInfo = JSON.parse(data)
if (!this.userInfo.setting) {
this.userInfo.setting = {}
}
if (!this.userInfo.setting.enabled_notifications) {
this.userInfo.setting.enabled_notifications = []
}
},
saveUserInfo () {
this.$refs['setting-form'].validate(async valid => {
if (!valid) return
const res = await this.$store.dispatch('user/postInfo', this.userInfo)
if (!res || res.error) {
this.$message.error(res.error)
export default {
name: 'Setting',
data() {
const validatePass = (rule, value, callback) => {
if (!value) return callback()
if (value.length < 5) {
callback(new Error(this.$t('Password length should be no shorter than 5')))
} else {
this.$message.success(this.$t('Saved successfully'))
callback()
}
})
this.$st.sendEv('设置', '保存')
},
tabActiveHandle () {
},
onAllowSendingStatisticsChange (value) {
if (value) {
this.$st.sendPv('/allow_stats')
this.$st.sendEv('全局', '允许/禁止统计', '允许')
} else {
this.$st.sendPv('/disallow_stats')
this.$st.sendEv('全局', '允许/禁止统计', '禁止')
}
this.$message.success(this.$t('Saved successfully'))
localStorage.setItem('useStats', value ? '1' : '0')
},
onEnableTutorialChange (value) {
this.$message.success(this.$t('Saved successfully'))
localStorage.setItem('enableTutorial', value ? '1' : '0')
},
onAddApiToken () {
this.$confirm(this.$t('Are you sure to add an API token?'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(async () => {
const res = await this.$request.put('/tokens')
if (!res.data.error) {
this.$message.success(this.$t('Added API token successfully'))
await this.getApiTokens()
const validateEmail = (rule, value, callback) => {
if (!value) return callback()
if (!value.match(/.+@.+/i)) {
callback(new Error(this.$t('Email format invalid')))
} else {
callback()
}
})
},
onDeleteToken (row) {
this.$confirm(this.$t('Are you sure to delete this API token?'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(async () => {
const res = await this.$request.delete(`/tokens/${row._id}`)
if (!res.data.error) {
this.$message.success(this.$t('Deleted API token successfully'))
await this.getApiTokens()
}
})
},
async addApiToken () {
await this.$request.put('/tokens')
},
async getApiTokens () {
const res = await this.$request.get('/tokens')
this.apiTokens = res.data.data
},
toggleTokenVisible (row) {
this.$set(row, 'visible', !row.visible)
},
getMaskValue (str) {
let s = ''
for (let i = 0; i < str.length; i++) {
s += '*'
}
return s
const validateDingTalkRobotWebhook = (rule, value, callback) => {
if (!value) return callback()
if (!value.match(/^https:\/\/oapi.dingtalk.com\/robot\/send\?access_token=[a-f0-9]+/i)) {
callback(new Error(this.$t('DingTalk Robot Webhook format invalid')))
} else {
callback()
}
}
const validateWechatRobotWebhook = (rule, value, callback) => {
if (!value) return callback()
if (!value.match(/^https:\/\/qyapi.weixin.qq.com\/cgi-bin\/webhook\/send\?key=.+/i)) {
callback(new Error(this.$t('DingTalk Robot Webhook format invalid')))
} else {
callback()
}
}
return {
userInfo: { setting: { enabled_notifications: [] }},
rulesNotification: {
password: [{ trigger: 'blur', validator: validatePass }],
email: [{ trigger: 'blur', validator: validateEmail }],
'setting.ding_talk_robot_webhook': [{ trigger: 'blur', validator: validateDingTalkRobotWebhook }],
'setting.wechat_robot_webhook': [{ trigger: 'blur', validator: validateWechatRobotWebhook }]
},
rulesLog: {},
isShowDingTalkAppSecret: false,
activeName: 'general',
addDialogVisible: false,
tourSteps: [
{
target: '#tab-general',
content: this.$t('Here you can set your general settings.'),
params: {
placement: 'right'
}
},
{
target: '#tab-notify',
content: this.$t('In this tab you can configure your notification settings.')
},
{
target: '#tab-global-variable',
content: this.$t('Here you can add/edit/delete global environment variables which will be passed into your spider programs.')
}
],
tourCallbacks: {
onStop: () => {
this.$utils.tour.finishTour('setting')
},
onPreviousStep: (currentStep) => {
if (currentStep === 1) {
this.activeName = 'password'
} else if (currentStep === 2) {
this.activeName = 'notify'
}
this.$utils.tour.prevStep('setting', currentStep)
},
onNextStep: (currentStep) => {
if (currentStep === 0) {
this.activeName = 'notify'
} else if (currentStep === 1) {
this.activeName = 'global-variable'
}
this.$utils.tour.nextStep('setting', currentStep)
}
},
isAllowSendingStatistics: localStorage.getItem('useStats') === '1',
isEnableTutorial: localStorage.getItem('enableTutorial') === '1',
apiTokens: []
}
},
copyToken (str) {
const input = document.getElementById('clipboard')
input.value = str
input.select()
document.execCommand('copy')
this.$message.success(this.$t('Token copied'))
}
},
async created () {
await this.$store.dispatch('user/getInfo')
await this.$store.dispatch('user/getGlobalVariable')
this.getUserInfo()
await this.getApiTokens()
},
mounted () {
if (!this.$utils.tour.isFinishedTour('setting')) {
this.$utils.tour.startTour(this, 'setting')
computed: {
...mapState('user', [
'globalVariableList',
'globalVariableForm'
])
},
watch: {
async userInfoStr() {
await this.saveUserInfo()
await this.$store.dispatch('user/getInfo')
}
},
async created() {
await this.$store.dispatch('user/getInfo')
await this.$store.dispatch('user/getGlobalVariable')
this.getUserInfo()
await this.getApiTokens()
},
mounted() {
if (!this.$utils.tour.isFinishedTour('setting')) {
this.$utils.tour.startTour(this, 'setting')
}
},
methods: {
deleteGlobalVariableHandle(id) {
this.$confirm(this.$t('Are you sure to delete this global variable'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('user/deleteGlobalVariable', id).then(() => {
this.$store.dispatch('user/getGlobalVariable')
})
}).catch(() => {
})
},
addGlobalVariableHandle(isShow) {
if (isShow) {
this.addDialogVisible = true
return
}
this.$store.dispatch('user/addGlobalVariable')
.then(() => {
this.addDialogVisible = false
this.$st.sendEv('设置', '添加全局变量')
})
.then(() => {
this.$store.dispatch('user/getGlobalVariable')
})
},
getUserInfo() {
const data = localStorage.getItem('user_info')
if (!data) {
return {}
}
this.userInfo = JSON.parse(data)
if (!this.userInfo.setting) {
this.userInfo.setting = {}
}
if (!this.userInfo.setting.enabled_notifications) {
this.userInfo.setting.enabled_notifications = []
}
},
saveUserInfo() {
this.$refs['setting-form'].validate(async valid => {
if (!valid) return
const res = await this.$store.dispatch('user/postInfo', this.userInfo)
if (!res || res.error) {
this.$message.error(res.error)
} else {
this.$message.success(this.$t('Saved successfully'))
}
})
this.$st.sendEv('设置', '保存')
},
tabActiveHandle() {
},
onAllowSendingStatisticsChange(value) {
if (value) {
this.$st.sendPv('/allow_stats')
this.$st.sendEv('全局', '允许/禁止统计', '允许')
} else {
this.$st.sendPv('/disallow_stats')
this.$st.sendEv('全局', '允许/禁止统计', '禁止')
}
this.$message.success(this.$t('Saved successfully'))
localStorage.setItem('useStats', value ? '1' : '0')
},
onEnableTutorialChange(value) {
this.$message.success(this.$t('Saved successfully'))
localStorage.setItem('enableTutorial', value ? '1' : '0')
},
onAddApiToken() {
this.$confirm(this.$t('Are you sure to add an API token?'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(async() => {
const res = await this.$request.put('/tokens')
if (!res.data.error) {
this.$message.success(this.$t('Added API token successfully'))
await this.getApiTokens()
}
})
},
onDeleteToken(row) {
this.$confirm(this.$t('Are you sure to delete this API token?'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(async() => {
const res = await this.$request.delete(`/tokens/${row._id}`)
if (!res.data.error) {
this.$message.success(this.$t('Deleted API token successfully'))
await this.getApiTokens()
}
})
},
async addApiToken() {
await this.$request.put('/tokens')
},
async getApiTokens() {
const res = await this.$request.get('/tokens')
this.apiTokens = res.data.data
},
toggleTokenVisible(row) {
this.$set(row, 'visible', !row.visible)
},
getMaskValue(str) {
let s = ''
for (let i = 0; i < str.length; i++) {
s += '*'
}
return s
},
copyToken(str) {
const input = document.getElementById('clipboard')
input.value = str
input.select()
document.execCommand('copy')
this.$message.success(this.$t('Token copied'))
}
}
}
}
</script>
<style scoped>

View File

@@ -2,33 +2,50 @@
<div class="app-container">
<!--filter-->
<div class="filter">
<el-input prefix-icon="el-icon-search"
:placeholder="$t('Search')"
class="filter-search"
v-model="keyword">
</el-input>
<el-select v-model="filter.mainCategory" class="filter-category" :placeholder="$t('Select Main Category')"
clearable filterable @change="onSelectMainCategory">
<el-option v-for="op in mainCategoryList" :key="op" :value="op" :label="op"></el-option>
<el-input
v-model="keyword"
prefix-icon="el-icon-search"
:placeholder="$t('Search')"
class="filter-search"
/>
<el-select
v-model="filter.mainCategory"
class="filter-category"
:placeholder="$t('Select Main Category')"
clearable
filterable
@change="onSelectMainCategory"
>
<el-option v-for="op in mainCategoryList" :key="op" :value="op" :label="op" />
</el-select>
<el-select v-model="filter.category" class="filter-category" :placeholder="$t('Select Category')"
clearable filterable @change="onSelectCategory">
<el-option v-for="op in categoryList" :key="op" :value="op" :label="op"></el-option>
<el-select
v-model="filter.category"
class="filter-category"
:placeholder="$t('Select Category')"
clearable
filterable
@change="onSelectCategory"
>
<el-option v-for="op in categoryList" :key="op" :value="op" :label="op" />
</el-select>
<el-button type="success"
icon="el-icon-search"
class="btn refresh"
@click="onSearch">
{{$t('Search')}}
<el-button
type="success"
icon="el-icon-search"
class="btn refresh"
@click="onSearch"
>
{{ $t('Search') }}
</el-button>
</div>
<!--table list-->
<el-table :data="siteList"
class="table"
:cell-class-name="getCellClassName"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
border>
<el-table
:data="siteList"
class="table"
:cell-class-name="getCellClassName"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
border
>
<template v-for="col in columns">
<!--<el-table-column v-if="col.name === 'category'"-->
<!--:key="col.name"-->
@@ -47,41 +64,51 @@
<!--</el-select>-->
<!--</template>-->
<!--</el-table-column>-->
<el-table-column v-if="col.name === 'domain'"
:key="col.name"
:label="$t(col.label)"
:width="col.width"
:align="col.align">
<el-table-column
v-if="col.name === 'domain'"
:key="col.name"
:label="$t(col.label)"
:width="col.width"
:align="col.align"
>
<template slot-scope="scope">
<a class="domain" :href="'http://' + scope.row[col.name]" target="_blank"
@click="onClickDomain(scope.row._id)">
{{scope.row[col.name]}}
<a
class="domain"
:href="'http://' + scope.row[col.name]"
target="_blank"
@click="onClickDomain(scope.row._id)"
>
{{ scope.row[col.name] }}
</a>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'spider_count'"
:key="col.name"
:label="$t(col.label)"
:width="col.width"
:align="col.align">
<el-table-column
v-else-if="col.name === 'spider_count'"
:key="col.name"
:label="$t(col.label)"
:width="col.width"
:align="col.align"
>
<template slot-scope="scope">
<div>
<template v-if="scope.row[col.name] > 0">
<a href="javascript:" @click="goToSpiders(scope.row._id)">
{{scope.row[col.name]}}
{{ scope.row[col.name] }}
</a>
</template>
<template v-else>
{{scope.row[col.name]}}
{{ scope.row[col.name] }}
</template>
</div>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'has_robots'"
:key="col.name"
:label="$t(col.label)"
:width="col.width"
:align="col.align">
<el-table-column
v-else-if="col.name === 'has_robots'"
:key="col.name"
:label="$t(col.label)"
:width="col.width"
:align="col.align"
>
<template slot-scope="scope">
<div>
<template v-if="scope.row[col.name]">
@@ -90,42 +117,47 @@
</a>
</template>
<template v-else>
{{scope.row[col.name] === undefined ? 'N/A' : 'N'}}
{{ scope.row[col.name] === undefined ? 'N/A' : 'N' }}
</template>
</div>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'home_response_time'"
:key="col.name"
:label="$t(col.label)"
:width="col.width"
:align="col.align">
<el-table-column
v-else-if="col.name === 'home_response_time'"
:key="col.name"
:label="$t(col.label)"
:width="col.width"
:align="col.align"
>
<template slot-scope="scope">
{{scope.row[col.name] ? scope.row[col.name].toFixed(1) : 'N/A'}}
{{ scope.row[col.name] ? scope.row[col.name].toFixed(1) : 'N/A' }}
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'home_http_status'"
:key="col.name"
:label="$t(col.label)"
:width="col.width"
:align="col.align">
<el-table-column
v-else-if="col.name === 'home_http_status'"
:key="col.name"
:label="$t(col.label)"
:width="col.width"
:align="col.align"
>
<template slot-scope="scope">
{{scope.row[col.name] ? scope.row[col.name].toFixed(0) : 'N/A'}}
{{ scope.row[col.name] ? scope.row[col.name].toFixed(0) : 'N/A' }}
</template>
</el-table-column>
<el-table-column v-else
:key="col.name"
:property="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align || 'center'"
:width="col.width">
</el-table-column>
<el-table-column
v-else
:key="col.name"
:property="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align || 'center'"
:width="col.width"
/>
</template>
<el-table-column :label="$t('Action')" align="left" width="120" v-if="false">
<el-table-column v-if="false" :label="$t('Action')" align="left" width="120">
<template slot-scope="scope">
<el-tooltip :content="$t('View')" placement="top">
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)"></el-button>
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)" />
</el-tooltip>
<!--<el-tooltip :content="$t('Remove')" placement="top">-->
<!--<el-button type="danger" icon="el-icon-delete" size="mini" @click="onRemove(scope.row)"></el-button>-->
@@ -135,198 +167,198 @@
</el-table>
<div class="pagination">
<el-pagination
@current-change="onPageChange"
@size-change="onPageChange"
:current-page.sync="pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size.sync="pageSize"
layout="sizes, prev, pager, next"
:total="totalCount">
</el-pagination>
:total="totalCount"
@current-change="onPageChange"
@size-change="onPageChange"
/>
</div>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'SiteList',
data () {
return {
// categoryList: [
// '新闻',
// '搜索引擎',
// '综合',
// '金融',
// '购物',
// '社交',
// '视频',
// '音乐',
// '资讯',
// '政企官网',
// '其他'
// ],
columns: [
{ name: 'rank', label: 'Rank', align: 'center', width: '80' },
{ name: 'name', label: 'Name', align: 'left', width: 'auto' },
{ name: 'domain', label: 'Domain', align: 'left', width: '150' },
// { name: 'description', label: 'Description', align: 'left', width: 'auto' },
{ name: 'main_category', label: 'Main Category', align: 'center', width: '100' },
{ name: 'category', label: 'Category', align: 'center', width: '100' },
{ name: 'spider_count', label: 'Spider Count', align: 'center', width: '60' },
{ name: 'has_robots', label: 'Robots Protocol', align: 'center', width: '65' },
{ name: 'home_response_time', label: 'Home Page Response Time (sec)', align: 'right', width: '80' },
{ name: 'home_http_status', label: 'Home Page Response Status Code', align: 'right', width: '80' }
]
}
},
computed: {
...mapState('site', [
'filter',
'siteList',
'mainCategoryList',
'categoryList',
'totalCount'
]),
keyword: {
get () {
return this.$store.state.site.keyword
},
set (value) {
this.$store.commit('site/SET_KEYWORD', value)
export default {
name: 'SiteList',
data() {
return {
// categoryList: [
// '新闻',
// '搜索引擎',
// '综合',
// '金融',
// '购物',
// '社交',
// '视频',
// '音乐',
// '资讯',
// '政企官网',
// '其他'
// ],
columns: [
{ name: 'rank', label: 'Rank', align: 'center', width: '80' },
{ name: 'name', label: 'Name', align: 'left', width: 'auto' },
{ name: 'domain', label: 'Domain', align: 'left', width: '150' },
// { name: 'description', label: 'Description', align: 'left', width: 'auto' },
{ name: 'main_category', label: 'Main Category', align: 'center', width: '100' },
{ name: 'category', label: 'Category', align: 'center', width: '100' },
{ name: 'spider_count', label: 'Spider Count', align: 'center', width: '60' },
{ name: 'has_robots', label: 'Robots Protocol', align: 'center', width: '65' },
{ name: 'home_response_time', label: 'Home Page Response Time (sec)', align: 'right', width: '80' },
{ name: 'home_http_status', label: 'Home Page Response Status Code', align: 'right', width: '80' }
]
}
},
pageNum: {
get () {
return this.$store.state.site.pageNum
computed: {
...mapState('site', [
'filter',
'siteList',
'mainCategoryList',
'categoryList',
'totalCount'
]),
keyword: {
get() {
return this.$store.state.site.keyword
},
set(value) {
this.$store.commit('site/SET_KEYWORD', value)
}
},
set (value) {
this.$store.commit('site/SET_PAGE_NUM', value)
pageNum: {
get() {
return this.$store.state.site.pageNum
},
set(value) {
this.$store.commit('site/SET_PAGE_NUM', value)
}
},
pageSize: {
get() {
return this.$store.state.site.pageSize
},
set(value) {
this.$store.commit('site/SET_PAGE_SIZE', value)
}
},
mainCategory() {
return this.filter.mainCategory
},
category() {
return this.filter.category
}
},
pageSize: {
get () {
return this.$store.state.site.pageSize
},
set (value) {
this.$store.commit('site/SET_PAGE_SIZE', value)
}
},
mainCategory () {
return this.filter.mainCategory
},
category () {
return this.filter.category
}
},
watch: {
mainCategory () {
// reset category
this.filter.category = undefined
watch: {
mainCategory() {
// reset category
this.filter.category = undefined
// get category list
this.$store.dispatch('site/getCategoryList')
// get site list
this.$store.dispatch('site/getSiteList')
},
category() {
// get site list
this.$store.dispatch('site/getSiteList')
}
},
created() {
this.$store.dispatch('site/getSiteList')
this.$store.dispatch('site/getMainCategoryList')
// get category list
this.$store.dispatch('site/getCategoryList')
// get site list
this.$store.dispatch('site/getSiteList')
},
category () {
// get site list
this.$store.dispatch('site/getSiteList')
}
},
methods: {
onSearch () {
setTimeout(() => {
this.$store.dispatch('site/getSiteList')
}, 0)
this.$st.sendEv('网站', '搜索')
},
onSelectMainCategory () {
this.$st.sendEv('网站', '选择主类别')
},
onSelectCategory () {
this.$st.sendEv('网站', '选择类别')
},
onClickDomain (domain) {
this.$st.sendEv('网站', '点击域名', 'domain', domain)
},
onPageChange () {
setTimeout(() => {
this.$store.dispatch('site/getSiteList')
}, 0)
},
onRowChange (row) {
this.$store.dispatch('site/editSite', {
id: row.domain,
category: row.category
})
},
getCellClassName ({ row, columnIndex }) {
let cls = []
if (columnIndex === this.getColumnIndex('has_robots')) {
cls.push('status')
if (row.has_robots === undefined) {
cls.push('info')
} else if (row.has_robots) {
cls.push('danger')
} else {
cls.push('success')
}
} else if (columnIndex === this.getColumnIndex('home_response_time')) {
cls.push('status')
if (row.home_response_time === undefined) {
cls.push('info')
} else if (row.home_response_time <= 1) {
cls.push('success')
} else if (row.home_response_time <= 5) {
cls.push('primary')
} else if (row.home_response_time <= 10) {
cls.push('warning')
} else {
cls.push('danger')
}
} else if (columnIndex === this.getColumnIndex('home_http_status')) {
cls.push('status')
if (row.home_http_status === undefined) {
cls.push('info')
} else if (row.home_http_status >= 200 && row.home_http_status < 300) {
cls.push('success')
} else {
cls.push('danger')
}
} else if (columnIndex === this.getColumnIndex('spider_count')) {
cls.push('status')
if (row.spider_count > 0) {
cls.push('success')
} else {
cls.push('info')
methods: {
onSearch() {
setTimeout(() => {
this.$store.dispatch('site/getSiteList')
}, 0)
this.$st.sendEv('网站', '搜索')
},
onSelectMainCategory() {
this.$st.sendEv('网站', '选择主类别')
},
onSelectCategory() {
this.$st.sendEv('网站', '选择类别')
},
onClickDomain(domain) {
this.$st.sendEv('网站', '点击域名', 'domain', domain)
},
onPageChange() {
setTimeout(() => {
this.$store.dispatch('site/getSiteList')
}, 0)
},
onRowChange(row) {
this.$store.dispatch('site/editSite', {
id: row.domain,
category: row.category
})
},
getCellClassName({ row, columnIndex }) {
const cls = []
if (columnIndex === this.getColumnIndex('has_robots')) {
cls.push('status')
if (row.has_robots === undefined) {
cls.push('info')
} else if (row.has_robots) {
cls.push('danger')
} else {
cls.push('success')
}
} else if (columnIndex === this.getColumnIndex('home_response_time')) {
cls.push('status')
if (row.home_response_time === undefined) {
cls.push('info')
} else if (row.home_response_time <= 1) {
cls.push('success')
} else if (row.home_response_time <= 5) {
cls.push('primary')
} else if (row.home_response_time <= 10) {
cls.push('warning')
} else {
cls.push('danger')
}
} else if (columnIndex === this.getColumnIndex('home_http_status')) {
cls.push('status')
if (row.home_http_status === undefined) {
cls.push('info')
} else if (row.home_http_status >= 200 && row.home_http_status < 300) {
cls.push('success')
} else {
cls.push('danger')
}
} else if (columnIndex === this.getColumnIndex('spider_count')) {
cls.push('status')
if (row.spider_count > 0) {
cls.push('success')
} else {
cls.push('info')
}
}
return cls.join(' ')
},
getColumnIndex(columnName) {
return this.columns.map(d => d.name).indexOf(columnName)
},
goToSpiders(domain) {
this.$router.push({ name: 'SpiderList', params: { domain }})
this.$st.sendEv('网站', '点击爬虫数', 'domain', domain)
},
onClickRobots(domain) {
this.$st.sendEv('网站', '点击Robots协议', 'domain', domain)
}
return cls.join(' ')
},
getColumnIndex (columnName) {
return this.columns.map(d => d.name).indexOf(columnName)
},
goToSpiders (domain) {
this.$router.push({ name: 'SpiderList', params: { domain } })
this.$st.sendEv('网站', '点击爬虫数', 'domain', domain)
},
onClickRobots (domain) {
this.$st.sendEv('网站', '点击Robots协议', 'domain', domain)
}
},
created () {
this.$store.dispatch('site/getSiteList')
this.$store.dispatch('site/getMainCategoryList')
this.$store.dispatch('site/getCategoryList')
}
}
</script>
<style scoped>

View File

@@ -11,19 +11,19 @@
<!--selector-->
<div class="selector">
<label class="label">{{$t('Spider')}}: </label>
<label class="label">{{ $t('Spider') }}: </label>
<el-select id="spider-select" v-model="spiderForm._id" @change="onSpiderChange">
<el-option v-for="op in spiderList" :key="op._id" :value="op._id" :label="op.name"></el-option>
<el-option v-for="op in spiderList" :key="op._id" :value="op._id" :label="op.name" />
</el-select>
</div>
<!--tabs-->
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="border-card">
<el-tabs v-model="activeTabName" type="border-card" @tab-click="onTabClick">
<el-tab-pane :label="$t('Overview')" name="overview">
<spider-overview/>
<spider-overview />
</el-tab-pane>
<el-tab-pane v-if="isGit" :label="$t('Git')" name="git-settings">
<git-settings/>
<git-settings />
</el-tab-pane>
<el-tab-pane v-if="isScrapy" :label="$t('Scrapy Settings')" name="scrapy-settings">
<spider-scrapy
@@ -32,7 +32,7 @@
/>
</el-tab-pane>
<el-tab-pane v-if="isConfigurable" :label="$t('Config')" name="config">
<config-list ref="config" @convert="onConvert"/>
<config-list ref="config" @convert="onConvert" />
</el-tab-pane>
<el-tab-pane :label="$t('Files')" name="files">
<file-list
@@ -40,234 +40,235 @@
/>
</el-tab-pane>
<el-tab-pane :label="$t('Environment')" name="environment">
<environment-list/>
<environment-list />
</el-tab-pane>
<el-tab-pane :label="$t('Analytics')" name="analytics">
<spider-stats ref="spider-stats"/>
<spider-stats ref="spider-stats" />
</el-tab-pane>
<el-tab-pane :label="$t('Schedules')" name="schedules">
<spider-schedules/>
<spider-schedules />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import FileList from '../../components/File/FileList'
import SpiderOverview from '../../components/Overview/SpiderOverview'
import EnvironmentList from '../../components/Environment/EnvironmentList'
import SpiderStats from '../../components/Stats/SpiderStats'
import ConfigList from '../../components/Config/ConfigList'
import SpiderSchedules from './SpiderSchedules'
import SpiderScrapy from '../../components/Scrapy/SpiderScrapy'
import GitSettings from '../../components/Settings/GitSettings'
import {
mapState
} from 'vuex'
import FileList from '../../components/File/FileList'
import SpiderOverview from '../../components/Overview/SpiderOverview'
import EnvironmentList from '../../components/Environment/EnvironmentList'
import SpiderStats from '../../components/Stats/SpiderStats'
import ConfigList from '../../components/Config/ConfigList'
import SpiderSchedules from './SpiderSchedules'
import SpiderScrapy from '../../components/Scrapy/SpiderScrapy'
import GitSettings from '../../components/Settings/GitSettings'
export default {
name: 'SpiderDetail',
components: {
GitSettings,
SpiderScrapy,
SpiderSchedules,
ConfigList,
SpiderStats,
EnvironmentList,
FileList,
SpiderOverview
},
watch: {
configListTs () {
this.onConvert()
}
},
data () {
return {
activeTabName: 'overview',
tourSteps: [
// top bar
{
target: '.el-tabs__nav.is-top',
content: this.$t('You can switch to each section of the spider detail.')
},
{
target: '#spider-select',
content: this.$t('You can switch to different spider using this selector.')
},
// overview
{
target: '.task-table-view',
content: this.$t('You can view latest tasks for this spider and click each row to view task detail.'),
params: {
placement: 'right'
}
},
{
target: '.spider-form',
content: this.$t('You can edit the detail info for this spider.'),
params: {
placement: 'left'
}
},
{
target: '.button-container',
content: this.$t('Here you can action on the spider, including running a task, uploading a zip file and save the spider info.'),
params: {
placement: 'top'
}
},
// file
{
target: '.tree',
content: this.$t('File navigation panel.<br><br>You can right click on <br>each item to create or delete<br> a file/directory.')
},
{
target: '.add-btn',
content: this.$t('Click to add a file or directory<br> on the root directory.')
},
{
target: '.main-content',
content: this.$t('You can edit, save, rename<br> and delete the selected file <br>in this box.'),
params: {
placement: 'left'
}
},
// environment
{
target: '.environment-list',
content: this.$t('Here you can add environment variables that will be passed to the spider program when running a task.')
},
// schedules
{
target: '.schedule-list',
content: this.$t('You can add, edit and delete schedules (cron jobs) for the spider.')
}
],
tourCallbacks: {
onStop: () => {
this.$utils.tour.finishTour('spider-detail')
},
onPreviousStep: (currentStep) => {
if (currentStep === 5) {
this.activeTabName = 'overview'
} else if (currentStep === 8) {
this.activeTabName = 'files'
} else if (currentStep === 9) {
this.activeTabName = 'environment'
}
this.$utils.tour.prevStep('spider-detail', currentStep)
},
onNextStep: (currentStep) => {
if (currentStep === 4) {
this.activeTabName = 'files'
} else if (currentStep === 7) {
this.activeTabName = 'environment'
} else if (currentStep === 8) {
this.activeTabName = 'schedules'
}
this.$utils.tour.nextStep('spider-detail', currentStep)
}
},
redirectType: ''
}
},
computed: {
...mapState('spider', [
'spiderList',
'spiderForm',
'configListTs'
]),
...mapState('file', [
'currentPath'
]),
...mapState('deploy', [
'deployList'
]),
isCustomized () {
return this.spiderForm.type === 'customized'
export default {
name: 'SpiderDetail',
components: {
GitSettings,
SpiderScrapy,
SpiderSchedules,
ConfigList,
SpiderStats,
EnvironmentList,
FileList,
SpiderOverview
},
isConfigurable () {
return this.spiderForm.type === 'configurable'
},
isScrapy () {
return this.isCustomized && this.spiderForm.is_scrapy
},
isGit () {
return this.spiderForm.is_git
}
},
methods: {
async onTabClick (tab) {
if (this.activeTabName === 'analytics') {
setTimeout(() => {
this.$refs['spider-stats'].update()
}, 0)
} else if (this.activeTabName === 'config') {
setTimeout(() => {
this.$refs['config'].update()
}, 0)
if (!this.$utils.tour.isFinishedTour('spider-detail-config')) {
setTimeout(() => {
this.$utils.tour.startTour(this, 'spider-detail-config')
}, 100)
}
} else if (this.activeTabName === 'scrapy-settings') {
await this.getScrapyData()
} else if (this.activeTabName === 'files') {
await this.$store.dispatch('spider/getFileTree')
if (this.currentPath) {
await this.$store.dispatch('file/getFileContent', { path: this.currentPath })
}
data() {
return {
activeTabName: 'overview',
tourSteps: [
// top bar
{
target: '.el-tabs__nav.is-top',
content: this.$t('You can switch to each section of the spider detail.')
},
{
target: '#spider-select',
content: this.$t('You can switch to different spider using this selector.')
},
// overview
{
target: '.task-table-view',
content: this.$t('You can view latest tasks for this spider and click each row to view task detail.'),
params: {
placement: 'right'
}
},
{
target: '.spider-form',
content: this.$t('You can edit the detail info for this spider.'),
params: {
placement: 'left'
}
},
{
target: '.button-container',
content: this.$t('Here you can action on the spider, including running a task, uploading a zip file and save the spider info.'),
params: {
placement: 'top'
}
},
// file
{
target: '.tree',
content: this.$t('File navigation panel.<br><br>You can right click on <br>each item to create or delete<br> a file/directory.')
},
{
target: '.add-btn',
content: this.$t('Click to add a file or directory<br> on the root directory.')
},
{
target: '.main-content',
content: this.$t('You can edit, save, rename<br> and delete the selected file <br>in this box.'),
params: {
placement: 'left'
}
},
// environment
{
target: '.environment-list',
content: this.$t('Here you can add environment variables that will be passed to the spider program when running a task.')
},
// schedules
{
target: '.schedule-list',
content: this.$t('You can add, edit and delete schedules (cron jobs) for the spider.')
}
],
tourCallbacks: {
onStop: () => {
this.$utils.tour.finishTour('spider-detail')
},
onPreviousStep: (currentStep) => {
if (currentStep === 5) {
this.activeTabName = 'overview'
} else if (currentStep === 8) {
this.activeTabName = 'files'
} else if (currentStep === 9) {
this.activeTabName = 'environment'
}
this.$utils.tour.prevStep('spider-detail', currentStep)
},
onNextStep: (currentStep) => {
if (currentStep === 4) {
this.activeTabName = 'files'
} else if (currentStep === 7) {
this.activeTabName = 'environment'
} else if (currentStep === 8) {
this.activeTabName = 'schedules'
}
this.$utils.tour.nextStep('spider-detail', currentStep)
}
},
redirectType: ''
}
this.$st.sendEv('爬虫详情', '切换标签', tab.name)
},
onSpiderChange (id) {
this.$router.push(`/spiders/${id}`)
this.$st.sendEv('爬虫详情', '切换爬虫')
},
async getScrapyData () {
await Promise.all([
this.$store.dispatch('spider/getSpiderScrapySpiders', this.$route.params.id),
this.$store.dispatch('spider/getSpiderScrapyItems', this.$route.params.id),
this.$store.dispatch('spider/getSpiderScrapySettings', this.$route.params.id),
this.$store.dispatch('spider/getSpiderScrapyPipelines', this.$route.params.id)
])
},
async onClickScrapySpider (filepath) {
this.activeTabName = 'files'
await this.$store.dispatch('spider/getFileTree')
this.$refs['file-list'].clickSpider(filepath)
},
async onClickScrapyPipeline () {
this.activeTabName = 'files'
await this.$store.dispatch('spider/getFileTree')
this.$refs['file-list'].clickPipeline()
},
onConvert () {
this.activeTabName = 'overview'
}
},
async created () {
// get spider basic info
await this.$store.dispatch('spider/getSpiderData', this.$route.params.id)
// get spider file info
await this.$store.dispatch('file/getFileList', this.spiderForm.src)
computed: {
...mapState('spider', [
'spiderList',
'spiderForm',
'configListTs'
]),
...mapState('file', [
'currentPath'
]),
...mapState('deploy', [
'deployList'
]),
isCustomized() {
return this.spiderForm.type === 'customized'
},
isConfigurable() {
return this.spiderForm.type === 'configurable'
},
isScrapy() {
return this.isCustomized && this.spiderForm.is_scrapy
},
isGit() {
return this.spiderForm.is_git
}
},
watch: {
configListTs() {
this.onConvert()
}
},
async created() {
// get spider basic info
await this.$store.dispatch('spider/getSpiderData', this.$route.params.id)
// get spider tasks
await this.$store.dispatch('spider/getTaskList', this.$route.params.id)
// get spider file info
await this.$store.dispatch('file/getFileList', this.spiderForm.src)
// get spider list
await this.$store.dispatch('spider/getSpiderList', { owner_type: 'all' })
},
mounted () {
if (!this.$utils.tour.isFinishedTour('spider-detail')) {
this.$utils.tour.startTour(this, 'spider-detail')
// get spider tasks
await this.$store.dispatch('spider/getTaskList', this.$route.params.id)
// get spider list
await this.$store.dispatch('spider/getSpiderList', { owner_type: 'all' })
},
mounted() {
if (!this.$utils.tour.isFinishedTour('spider-detail')) {
this.$utils.tour.startTour(this, 'spider-detail')
}
},
methods: {
async onTabClick(tab) {
if (this.activeTabName === 'analytics') {
setTimeout(() => {
this.$refs['spider-stats'].update()
}, 0)
} else if (this.activeTabName === 'config') {
setTimeout(() => {
this.$refs['config'].update()
}, 0)
if (!this.$utils.tour.isFinishedTour('spider-detail-config')) {
setTimeout(() => {
this.$utils.tour.startTour(this, 'spider-detail-config')
}, 100)
}
} else if (this.activeTabName === 'scrapy-settings') {
await this.getScrapyData()
} else if (this.activeTabName === 'files') {
await this.$store.dispatch('spider/getFileTree')
if (this.currentPath) {
await this.$store.dispatch('file/getFileContent', { path: this.currentPath })
}
}
this.$st.sendEv('爬虫详情', '切换标签', tab.name)
},
onSpiderChange(id) {
this.$router.push(`/spiders/${id}`)
this.$st.sendEv('爬虫详情', '切换爬虫')
},
async getScrapyData() {
await Promise.all([
this.$store.dispatch('spider/getSpiderScrapySpiders', this.$route.params.id),
this.$store.dispatch('spider/getSpiderScrapyItems', this.$route.params.id),
this.$store.dispatch('spider/getSpiderScrapySettings', this.$route.params.id),
this.$store.dispatch('spider/getSpiderScrapyPipelines', this.$route.params.id)
])
},
async onClickScrapySpider(filepath) {
this.activeTabName = 'files'
await this.$store.dispatch('spider/getFileTree')
this.$refs['file-list'].clickSpider(filepath)
},
async onClickScrapyPipeline() {
this.activeTabName = 'files'
await this.$store.dispatch('spider/getFileTree')
this.$refs['file-list'].clickPipeline()
},
onConvert() {
this.activeTabName = 'overview'
}
}
}
}
</script>
<style scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -1,58 +1,58 @@
<script>
import ScheduleList from '../schedule/ScheduleList'
import ScheduleList from '../schedule/ScheduleList'
export default {
name: 'SpiderSchedules',
extends: ScheduleList,
computed: {
isDisabledSpiderSchedule () {
return true
},
spiderId () {
const arr = this.$route.path.split('/')
return arr[arr.length - 1]
}
},
methods: {
getNodeList () {
this.$request.get('/nodes', {}).then(response => {
this.nodeList = response.data.data.map(d => {
d.systemInfo = {
os: '',
arch: '',
num_cpu: '',
executables: []
}
return d
})
})
},
getSpiderList () {
this.$request.get('/spiders', {})
.then(response => {
this.spiderList = response.data.data.list || []
})
},
onAdd () {
this.isEdit = false
this.dialogVisible = true
this.$store.commit('schedule/SET_SCHEDULE_FORM', { node_ids: [], spider_id: this.spiderId })
if (this.spiderForm.is_scrapy) {
this.onSpiderChange(this.spiderForm._id)
export default {
name: 'SpiderSchedules',
extends: ScheduleList,
computed: {
isDisabledSpiderSchedule() {
return true
},
spiderId() {
const arr = this.$route.path.split('/')
return arr[arr.length - 1]
}
},
created() {
const arr = this.$route.path.split('/')
const id = arr[arr.length - 1]
this.$store.dispatch(`spider/getScheduleList`, { id })
// 节点列表
this.getNodeList()
// 爬虫列表
this.getSpiderList()
},
methods: {
getNodeList() {
this.$request.get('/nodes', {}).then(response => {
this.nodeList = response.data.data.map(d => {
d.systemInfo = {
os: '',
arch: '',
num_cpu: '',
executables: []
}
return d
})
})
},
getSpiderList() {
this.$request.get('/spiders', {})
.then(response => {
this.spiderList = response.data.data.list || []
})
},
onAdd() {
this.isEdit = false
this.dialogVisible = true
this.$store.commit('schedule/SET_SCHEDULE_FORM', { node_ids: [], spider_id: this.spiderId })
if (this.spiderForm.is_scrapy) {
this.onSpiderChange(this.spiderForm._id)
}
this.$st.sendEv('定时任务', '添加定时任务')
}
this.$st.sendEv('定时任务', '添加定时任务')
}
},
created () {
const arr = this.$route.path.split('/')
const id = arr[arr.length - 1]
this.$store.dispatch(`spider/getScheduleList`, { id })
// 节点列表
this.getNodeList()
// 爬虫列表
this.getSpiderList()
}
}
</script>

View File

@@ -10,260 +10,262 @@
<!--./tour-->
<!--tabs-->
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="border-card">
<el-tabs v-model="activeTabName" type="border-card" @tab-click="onTabClick">
<el-tab-pane :label="$t('Overview')" name="overview">
<task-overview @click-log="activeTabName = 'log'"/>
<task-overview @click-log="activeTabName = 'log'" />
</el-tab-pane>
<el-tab-pane :label="$t('Log')" name="log">
<log-view @search="getTaskLog(true)"/>
<log-view @search="getTaskLog(true)" />
</el-tab-pane>
<el-tab-pane :label="$t('Results')" name="results">
<div class="button-group">
<el-button size="small" class="btn-download" type="primary" icon="el-icon-download" @click="downloadCSV">
{{$t('Download CSV')}}
{{ $t('Download CSV') }}
</el-button>
</div>
<general-table-view :data="taskResultsData"
:columns="taskResultsColumns"
:page-num="resultsPageNum"
:page-size="resultsPageSize"
:total="taskResultsTotalCount"
@page-change="onResultsPageChange"/>
<general-table-view
:data="taskResultsData"
:columns="taskResultsColumns"
:page-num="resultsPageNum"
:page-size="resultsPageSize"
:total="taskResultsTotalCount"
@page-change="onResultsPageChange"
/>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import {
mapState,
mapGetters
} from 'vuex'
import TaskOverview from '../../components/Overview/TaskOverview'
import GeneralTableView from '../../components/TableView/GeneralTableView'
import LogView from '../../components/ScrollView/LogView'
import {
mapState,
mapGetters
} from 'vuex'
import TaskOverview from '../../components/Overview/TaskOverview'
import GeneralTableView from '../../components/TableView/GeneralTableView'
import LogView from '../../components/ScrollView/LogView'
export default {
name: 'TaskDetail',
components: {
LogView,
GeneralTableView,
TaskOverview
},
data () {
return {
activeTabName: 'overview',
handle: undefined,
export default {
name: 'TaskDetail',
components: {
LogView,
GeneralTableView,
TaskOverview
},
data() {
return {
activeTabName: 'overview',
handle: undefined,
// tutorial
tourSteps: [
// overview
{
target: '.task-info-overview-wrapper',
content: this.$t('This is the info of the task detail.'),
params: {
placement: 'right'
// tutorial
tourSteps: [
// overview
{
target: '.task-info-overview-wrapper',
content: this.$t('This is the info of the task detail.'),
params: {
placement: 'right'
}
},
{
target: '.task-info-spider-wrapper',
content: this.$t('This is the spider info of the task.'),
params: {
placement: 'left'
}
},
{
target: '.spider-title',
content: this.$t('You can click to view the spider detail for the task.'),
params: {
placement: 'left'
}
},
{
target: '.task-info-node-wrapper',
content: this.$t('This is the node info of the task.'),
params: {
placement: 'left'
}
},
{
target: '.node-title',
content: this.$t('You can click to view the node detail for the task.'),
params: {
placement: 'left'
}
},
// log
{
target: '#tab-log',
content: this.$t('Here you can view the log<br> details for the task. The<br> log is automatically updated.')
},
// results
{
target: '#tab-results',
content: this.$t('Here you can view the results scraped by the spider.<br><br><strong>Note:</strong> If you find your results here are empty, please refer to the <a href="https://docs.crawlab.cn/Integration/" target="_blank" style="color: #409EFF">Documentation (Chinese)</a> about how to integrate your spider into Crawlab.')
},
{
target: '.btn-download',
content: this.$t('You can download your results as a CSV file by clicking this button.')
}
},
{
target: '.task-info-spider-wrapper',
content: this.$t('This is the spider info of the task.'),
params: {
placement: 'left'
],
tourCallbacks: {
onStop: () => {
this.$utils.tour.finishTour('task-detail')
},
onPreviousStep: (currentStep) => {
if (currentStep === 5) {
this.activeTabName = 'overview'
} else if (currentStep === 6) {
this.activeTabName = 'log'
}
this.$utils.tour.prevStep('task-detail', currentStep)
},
onNextStep: (currentStep) => {
if (currentStep === 4) {
this.activeTabName = 'log'
} else if (currentStep === 5) {
this.activeTabName = 'results'
}
this.$utils.tour.nextStep('task-detail', currentStep)
}
},
{
target: '.spider-title',
content: this.$t('You can click to view the spider detail for the task.'),
params: {
placement: 'left'
}
},
{
target: '.task-info-node-wrapper',
content: this.$t('This is the node info of the task.'),
params: {
placement: 'left'
}
},
{
target: '.node-title',
content: this.$t('You can click to view the node detail for the task.'),
params: {
placement: 'left'
}
},
// log
{
target: '#tab-log',
content: this.$t('Here you can view the log<br> details for the task. The<br> log is automatically updated.')
},
// results
{
target: '#tab-results',
content: this.$t('Here you can view the results scraped by the spider.<br><br><strong>Note:</strong> If you find your results here are empty, please refer to the <a href="https://docs.crawlab.cn/Integration/" target="_blank" style="color: #409EFF">Documentation (Chinese)</a> about how to integrate your spider into Crawlab.')
},
{
target: '.btn-download',
content: this.$t('You can download your results as a CSV file by clicking this button.')
}
],
tourCallbacks: {
onStop: () => {
this.$utils.tour.finishTour('task-detail')
},
onPreviousStep: (currentStep) => {
if (currentStep === 5) {
this.activeTabName = 'overview'
} else if (currentStep === 6) {
this.activeTabName = 'log'
}
this.$utils.tour.prevStep('task-detail', currentStep)
},
onNextStep: (currentStep) => {
if (currentStep === 4) {
this.activeTabName = 'log'
} else if (currentStep === 5) {
this.activeTabName = 'results'
}
this.$utils.tour.nextStep('task-detail', currentStep)
}
}
}
},
computed: {
...mapState('task', [
'taskForm',
'taskResultsData',
'taskResultsTotalCount',
'taskLog',
'logKeyword',
'isLogAutoFetch',
'currentLogIndex',
'activeErrorLogItem'
]),
...mapGetters('task', [
'taskResultsColumns',
'logData'
]),
...mapState('file', [
'currentPath'
]),
...mapState('deploy', [
'deployList'
]),
resultsPageNum: {
get () {
return this.$store.state.task.resultsPageNum
},
computed: {
...mapState('task', [
'taskForm',
'taskResultsData',
'taskResultsTotalCount',
'taskLog',
'logKeyword',
'isLogAutoFetch',
'currentLogIndex',
'activeErrorLogItem'
]),
...mapGetters('task', [
'taskResultsColumns',
'logData'
]),
...mapState('file', [
'currentPath'
]),
...mapState('deploy', [
'deployList'
]),
resultsPageNum: {
get() {
return this.$store.state.task.resultsPageNum
},
set(value) {
this.$store.commit('task/SET_RESULTS_PAGE_NUM', value)
}
},
set (value) {
this.$store.commit('task/SET_RESULTS_PAGE_NUM', value)
}
},
resultsPageSize: {
get () {
return this.$store.state.task.resultsPageSize
resultsPageSize: {
get() {
return this.$store.state.task.resultsPageSize
},
set(value) {
this.$store.commit('task/SET_RESULTS_PAGE_SIZE', value)
}
},
set (value) {
this.$store.commit('task/SET_RESULTS_PAGE_SIZE', value)
}
},
isLogAutoScroll: {
get () {
return this.$store.state.task.isLogAutoScroll
isLogAutoScroll: {
get() {
return this.$store.state.task.isLogAutoScroll
},
set(value) {
this.$store.commit('task/SET_IS_LOG_AUTO_SCROLL', value)
}
},
set (value) {
this.$store.commit('task/SET_IS_LOG_AUTO_SCROLL', value)
}
},
isLogAutoFetch: {
get () {
return this.$store.state.task.isLogAutoFetch
isLogAutoFetch: {
get() {
return this.$store.state.task.isLogAutoFetch
},
set(value) {
this.$store.commit('task/SET_IS_LOG_AUTO_FETCH', value)
}
},
set (value) {
this.$store.commit('task/SET_IS_LOG_AUTO_FETCH', value)
}
},
isLogFetchLoading: {
get () {
return this.$store.state.task.isLogFetchLoading
isLogFetchLoading: {
get() {
return this.$store.state.task.isLogFetchLoading
},
set(value) {
this.$store.commit('task/SET_IS_LOG_FETCH_LOADING', value)
}
},
set (value) {
this.$store.commit('task/SET_IS_LOG_FETCH_LOADING', value)
}
},
currentLogIndex: {
get () {
return this.$store.state.task.currentLogIndex
currentLogIndex: {
get() {
return this.$store.state.task.currentLogIndex
},
set(value) {
this.$store.commit('task/SET_CURRENT_LOG_INDEX', value)
}
},
set (value) {
this.$store.commit('task/SET_CURRENT_LOG_INDEX', value)
logIndexMap() {
const map = new Map()
this.logData.forEach((d, index) => {
map.set(d._id, index)
})
return map
},
isRunning() {
return ['pending', 'running'].includes(this.taskForm.status)
}
},
logIndexMap () {
const map = new Map()
this.logData.forEach((d, index) => {
map.set(d._id, index)
})
return map
},
isRunning () {
return ['pending', 'running'].includes(this.taskForm.status)
}
},
methods: {
onTabClick (tab) {
this.$st.sendEv('任务详情', '切换标签', tab.name)
},
onSpiderChange (id) {
this.$router.push(`/spiders/${id}`)
},
onResultsPageChange (payload) {
const { pageNum, pageSize } = payload
this.resultsPageNum = pageNum
this.resultsPageSize = pageSize
this.$store.dispatch('task/getTaskResults', this.$route.params.id)
},
downloadCSV () {
this.$store.dispatch('task/getTaskResultExcel', this.$route.params.id)
this.$st.sendEv('任务详情', '结果', '下载CSV')
},
async getTaskLog (showLoading) {
if (showLoading) {
this.isLogFetchLoading = true
}
await this.$store.dispatch('task/getTaskLog', { id: this.$route.params.id, keyword: this.logKeyword })
this.currentLogIndex = (this.logIndexMap.get(this.activeErrorLogItem.log_id) + 1) || 0
this.isLogFetchLoading = false
await this.$store.dispatch('task/getTaskErrorLog', this.$route.params.id)
}
},
async created () {
await this.$store.dispatch('task/getTaskData', this.$route.params.id)
async created() {
await this.$store.dispatch('task/getTaskData', this.$route.params.id)
this.isLogAutoFetch = !!this.isRunning
this.isLogAutoScroll = !!this.isRunning
this.isLogAutoFetch = !!this.isRunning
this.isLogAutoScroll = !!this.isRunning
await this.$store.dispatch('task/getTaskResults', this.$route.params.id)
await this.$store.dispatch('task/getTaskResults', this.$route.params.id)
this.getTaskLog()
this.handle = setInterval(() => {
if (this.isLogAutoFetch) {
this.$store.dispatch('task/getTaskData', this.$route.params.id)
this.getTaskLog()
this.handle = setInterval(() => {
if (this.isLogAutoFetch) {
this.$store.dispatch('task/getTaskData', this.$route.params.id)
this.$store.dispatch('task/getTaskResults', this.$route.params.id)
this.getTaskLog()
}
}, 5000)
},
mounted() {
if (!this.$utils.tour.isFinishedTour('task-detail')) {
this.$utils.tour.startTour(this, 'task-detail')
}
},
destroyed() {
clearInterval(this.handle)
},
methods: {
onTabClick(tab) {
this.$st.sendEv('任务详情', '切换标签', tab.name)
},
onSpiderChange(id) {
this.$router.push(`/spiders/${id}`)
},
onResultsPageChange(payload) {
const { pageNum, pageSize } = payload
this.resultsPageNum = pageNum
this.resultsPageSize = pageSize
this.$store.dispatch('task/getTaskResults', this.$route.params.id)
this.getTaskLog()
},
downloadCSV() {
this.$store.dispatch('task/getTaskResultExcel', this.$route.params.id)
this.$st.sendEv('任务详情', '结果', '下载CSV')
},
async getTaskLog(showLoading) {
if (showLoading) {
this.isLogFetchLoading = true
}
await this.$store.dispatch('task/getTaskLog', { id: this.$route.params.id, keyword: this.logKeyword })
this.currentLogIndex = (this.logIndexMap.get(this.activeErrorLogItem.log_id) + 1) || 0
this.isLogFetchLoading = false
await this.$store.dispatch('task/getTaskErrorLog', this.$route.params.id)
}
}, 5000)
},
mounted () {
if (!this.$utils.tour.isFinishedTour('task-detail')) {
this.$utils.tour.startTour(this, 'task-detail')
}
},
destroyed () {
clearInterval(this.handle)
}
}
</script>
<style scoped>

View File

@@ -16,32 +16,37 @@
<el-form class="filter-form" :model="filter" label-width="100px" label-position="right" inline>
<el-form-item prop="node_id" :label="$t('Node')">
<el-select v-model="filter.node_id" size="small" :placeholder="$t('Node')" @change="onFilterChange">
<el-option value="" :label="$t('All')"/>
<el-option v-for="node in nodeList" :key="node._id" :value="node._id" :label="node.name"/>
<el-option value="" :label="$t('All')" />
<el-option v-for="node in nodeList" :key="node._id" :value="node._id" :label="node.name" />
</el-select>
</el-form-item>
<el-form-item prop="spider_id" :label="$t('Spider')">
<el-select v-model="filter.spider_id" size="small" :placeholder="$t('Spider')" @change="onFilterChange"
:disabled="isFilterSpiderDisabled">
<el-option value="" :label="$t('All')"/>
<el-option v-for="spider in spiderList" :key="spider._id" :value="spider._id" :label="spider.name"/>
<el-select
v-model="filter.spider_id"
size="small"
:placeholder="$t('Spider')"
:disabled="isFilterSpiderDisabled"
@change="onFilterChange"
>
<el-option value="" :label="$t('All')" />
<el-option v-for="spider in spiderList" :key="spider._id" :value="spider._id" :label="spider.name" />
</el-select>
</el-form-item>
<el-form-item prop="status" :label="$t('Status')">
<el-select v-model="filter.status" size="small" :placeholder="$t('Status')" @change="onFilterChange">
<el-option value="" :label="$t('All')"></el-option>
<el-option value="pending" :label="$t('Pending')"></el-option>
<el-option value="running" :label="$t('Running')"></el-option>
<el-option value="finished" :label="$t('Finished')"></el-option>
<el-option value="error" :label="$t('Error')"></el-option>
<el-option value="cancelled" :label="$t('Cancelled')"></el-option>
<el-option value="abnormal" :label="$t('Abnormal')"></el-option>
<el-option value="" :label="$t('All')" />
<el-option value="pending" :label="$t('Pending')" />
<el-option value="running" :label="$t('Running')" />
<el-option value="finished" :label="$t('Finished')" />
<el-option value="error" :label="$t('Error')" />
<el-option value="cancelled" :label="$t('Cancelled')" />
<el-option value="abnormal" :label="$t('Abnormal')" />
</el-select>
</el-form-item>
</el-form>
</div>
<div class="right">
<el-button class="btn-delete" @click="onRemoveMultipleTask" size="small" type="danger">
<el-button class="btn-delete" size="small" type="danger" @click="onRemoveMultipleTask">
删除任务
</el-button>
</div>
@@ -49,13 +54,13 @@
<!--./filter-->
<!--legend-->
<status-legend/>
<status-legend />
<!--./legend-->
<!--table list-->
<el-table
:data="filteredTableData"
ref="table"
:data="filteredTableData"
class="table"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
border
@@ -63,76 +68,89 @@
@row-click="onRowClick"
@selection-change="onSelectionChange"
>
<el-table-column type="selection" width="45" align="center" reserve-selection/>
<el-table-column type="selection" width="45" align="center" reserve-selection />
<template v-for="col in columns">
<el-table-column v-if="col.name === 'spider_name'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
<el-table-column
v-if="col.name === 'spider_name'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
>
<template slot-scope="scope">
<a href="javascript:" class="a-tag" @click="onClickSpider(scope.row)">{{scope.row[col.name]}}</a>
<a href="javascript:" class="a-tag" @click="onClickSpider(scope.row)">{{ scope.row[col.name] }}</a>
</template>
</el-table-column>
<el-table-column v-else-if="col.name.match(/_ts$/)"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width">
<el-table-column
v-else-if="col.name.match(/_ts$/)"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width"
>
<template slot-scope="scope">
{{getTime(scope.row[col.name])}}
{{ getTime(scope.row[col.name]) }}
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'wait_duration'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width">
<el-table-column
v-else-if="col.name === 'wait_duration'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width"
>
<template slot-scope="scope">
{{getWaitDuration(scope.row)}}
{{ getWaitDuration(scope.row) }}
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'runtime_duration'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width">
<el-table-column
v-else-if="col.name === 'runtime_duration'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width"
>
<template slot-scope="scope">
{{getRuntimeDuration(scope.row)}}
{{ getRuntimeDuration(scope.row) }}
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'total_duration'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width">
<el-table-column
v-else-if="col.name === 'total_duration'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width"
>
<template slot-scope="scope">
{{getTotalDuration(scope.row)}}
{{ getTotalDuration(scope.row) }}
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'node_name'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width">
<el-table-column
v-else-if="col.name === 'node_name'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width"
>
<template slot-scope="scope">
<a href="javascript:" class="a-tag" @click="onClickNode(scope.row)">{{scope.row[col.name]}}</a>
<a href="javascript:" class="a-tag" @click="onClickNode(scope.row)">{{ scope.row[col.name] }}</a>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'status'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width">
<el-table-column
v-else-if="col.name === 'status'"
:key="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width"
>
<template slot-scope="scope">
<status-tag :status="scope.row[col.name]"/>
<status-tag :status="scope.row[col.name]" />
<template
v-if="scope.row.error_log_count > 0"
>
@@ -141,48 +159,57 @@
type="danger"
style="margin-left: 10px"
>
<i class="el-icon-warning"></i>
<i class="el-icon-tickets"></i>
<i class="el-icon-warning" />
<i class="el-icon-tickets" />
</el-tag>
</el-tooltip>
</template>
</template>
</el-table-column>
<el-table-column v-else
:key="col.name"
:property="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width">
</el-table-column>
<el-table-column
v-else
:key="col.name"
:property="col.name"
:label="$t(col.label)"
:sortable="col.sortable"
:align="col.align"
:width="col.width"
/>
</template>
<el-table-column :label="$t('Action')" align="left" fixed="right" width="150px">
<template slot-scope="scope">
<el-tooltip :content="$t('View')" placement="top">
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)"></el-button>
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)" />
</el-tooltip>
<el-tooltip :content="$t('Restart')" placement="top">
<el-button type="warning" icon="el-icon-refresh" size="mini"
@click="onRestart(scope.row, $event)"></el-button>
<el-button
type="warning"
icon="el-icon-refresh"
size="mini"
@click="onRestart(scope.row, $event)"
/>
</el-tooltip>
<el-tooltip :content="$t('Remove')" placement="top">
<el-button type="danger" icon="el-icon-delete" size="mini"
@click="onRemove(scope.row, $event)"></el-button>
<el-button
type="danger"
icon="el-icon-delete"
size="mini"
@click="onRemove(scope.row, $event)"
/>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
@current-change="onPageChange"
@size-change="onPageChange"
:current-page.sync="pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size.sync="pageSize"
layout="sizes, prev, pager, next"
:total="taskListTotalCount">
</el-pagination>
:total="taskListTotalCount"
@current-change="onPageChange"
@size-change="onPageChange"
/>
</div>
<!--./table list-->
</el-card>
@@ -190,274 +217,274 @@
</template>
<script>
import {
mapState
} from 'vuex'
import dayjs from 'dayjs'
import StatusTag from '../../components/Status/StatusTag'
import StatusLegend from '../../components/Status/StatusLegend'
import {
mapState
} from 'vuex'
import dayjs from 'dayjs'
import StatusTag from '../../components/Status/StatusTag'
import StatusLegend from '../../components/Status/StatusLegend'
export default {
name: 'TaskList',
components: { StatusLegend, StatusTag },
data () {
return {
// setInterval handle
handle: undefined,
export default {
name: 'TaskList',
components: { StatusLegend, StatusTag },
data() {
return {
// setInterval handle
handle: undefined,
// determine if is edit mode
isEditMode: false,
// determine if is edit mode
isEditMode: false,
// dialog visibility
dialogVisible: false,
// dialog visibility
dialogVisible: false,
// table columns
columns: [
{ name: 'node_name', label: 'Node', width: '120' },
{ name: 'spider_name', label: 'Spider', width: '120' },
{ name: 'status', label: 'Status', width: '180' },
{ name: 'param', label: 'Parameters', width: '120' },
// { name: 'create_ts', label: 'Create Time', width: '100' },
{ name: 'start_ts', label: 'Start Time', width: '100' },
{ name: 'finish_ts', label: 'Finish Time', width: '100' },
{ name: 'wait_duration', label: 'Wait Duration (sec)', align: 'right' },
{ name: 'runtime_duration', label: 'Runtime Duration (sec)', align: 'right' },
{ name: 'total_duration', label: 'Total Duration (sec)', width: '80', align: 'right' },
{ name: 'result_count', label: 'Results Count', width: '80' },
{ name: 'username', label: 'Owner', width: '100' }
// table columns
columns: [
{ name: 'node_name', label: 'Node', width: '120' },
{ name: 'spider_name', label: 'Spider', width: '120' },
{ name: 'status', label: 'Status', width: '180' },
{ name: 'param', label: 'Parameters', width: '120' },
// { name: 'create_ts', label: 'Create Time', width: '100' },
{ name: 'start_ts', label: 'Start Time', width: '100' },
{ name: 'finish_ts', label: 'Finish Time', width: '100' },
{ name: 'wait_duration', label: 'Wait Duration (sec)', align: 'right' },
{ name: 'runtime_duration', label: 'Runtime Duration (sec)', align: 'right' },
{ name: 'total_duration', label: 'Total Duration (sec)', width: '80', align: 'right' },
{ name: 'result_count', label: 'Results Count', width: '80' },
{ name: 'username', label: 'Owner', width: '100' }
// { name: 'avg_num_results', label: 'Average Results Count per Second', width: '80' }
],
],
multipleSelection: [],
multipleSelection: [],
// tutorial
tourSteps: [
{
target: '.filter-form',
content: this.$t('You can filter tasks from this area.')
},
{
target: '.table',
content: this.$t('This is a list of spider tasks executed sorted in a time descending order.')
},
{
target: '.table .el-table__body-wrapper tr:nth-child(1)',
content: this.$t('Click the row to or the view button to view the task detail.')
},
{
target: '.table tr td:nth-child(1)',
content: this.$t('Tick and select the tasks you would like to delete in batches.'),
params: {
placement: 'right'
}
},
{
target: '.btn-delete',
content: this.$t('Click this button to delete selected tasks.'),
params: {
placement: 'left'
}
}
],
tourCallbacks: {
onStop: () => {
this.$utils.tour.finishTour('task-list')
},
onPreviousStep: (currentStep) => {
this.$utils.tour.prevStep('task-list', currentStep)
},
onNextStep: (currentStep) => {
this.$utils.tour.nextStep('task-list', currentStep)
}
},
isFilterSpiderDisabled: false
}
},
computed: {
...mapState('task', [
'filter',
'taskList',
'taskListTotalCount',
'taskForm'
]),
...mapState('spider', [
'spiderList'
]),
...mapState('node', [
'nodeList'
]),
pageNum: {
get () {
return this.$store.state.task.pageNum
},
set (value) {
this.$store.commit('task/SET_PAGE_NUM', value)
}
},
pageSize: {
get () {
return this.$store.state.task.pageSize
},
set (value) {
this.$store.commit('task/SET_PAGE_SIZE', value)
}
},
filteredTableData () {
return this.taskList
.map(d => d)
.sort((a, b) => a.create_ts < b.create_ts ? 1 : -1)
.filter(d => {
// keyword
if (!this.filter.keyword) return true
for (let i = 0; i < this.columns.length; i++) {
const colName = this.columns[i].name
if (d[colName] && d[colName].toLowerCase().indexOf(this.filter.keyword.toLowerCase()) > -1) {
return true
// tutorial
tourSteps: [
{
target: '.filter-form',
content: this.$t('You can filter tasks from this area.')
},
{
target: '.table',
content: this.$t('This is a list of spider tasks executed sorted in a time descending order.')
},
{
target: '.table .el-table__body-wrapper tr:nth-child(1)',
content: this.$t('Click the row to or the view button to view the task detail.')
},
{
target: '.table tr td:nth-child(1)',
content: this.$t('Tick and select the tasks you would like to delete in batches.'),
params: {
placement: 'right'
}
},
{
target: '.btn-delete',
content: this.$t('Click this button to delete selected tasks.'),
params: {
placement: 'left'
}
}
return false
})
}
},
methods: {
onSearch (value) {
},
onRefresh () {
this.$store.dispatch('task/getTaskList')
this.$st.sendEv('任务列表', '搜索')
},
onRemoveMultipleTask () {
if (this.multipleSelection.length === 0) {
this.$message({
type: 'error',
message: '请选择要删除的任务'
})
return
}
this.$confirm('确定删除任务', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
let ids = this.multipleSelection.map(item => item._id)
this.$store.dispatch('task/deleteTaskMultiple', ids).then((resp) => {
if (resp.data.status === 'ok') {
this.$message({
type: 'success',
message: '删除任务成功'
})
this.$store.dispatch('task/getTaskList')
this.$refs['table'].clearSelection()
return
],
tourCallbacks: {
onStop: () => {
this.$utils.tour.finishTour('task-list')
},
onPreviousStep: (currentStep) => {
this.$utils.tour.prevStep('task-list', currentStep)
},
onNextStep: (currentStep) => {
this.$utils.tour.nextStep('task-list', currentStep)
}
},
isFilterSpiderDisabled: false
}
},
computed: {
...mapState('task', [
'filter',
'taskList',
'taskListTotalCount',
'taskForm'
]),
...mapState('spider', [
'spiderList'
]),
...mapState('node', [
'nodeList'
]),
pageNum: {
get() {
return this.$store.state.task.pageNum
},
set(value) {
this.$store.commit('task/SET_PAGE_NUM', value)
}
},
pageSize: {
get() {
return this.$store.state.task.pageSize
},
set(value) {
this.$store.commit('task/SET_PAGE_SIZE', value)
}
},
filteredTableData() {
return this.taskList
.map(d => d)
.sort((a, b) => a.create_ts < b.create_ts ? 1 : -1)
.filter(d => {
// keyword
if (!this.filter.keyword) return true
for (let i = 0; i < this.columns.length; i++) {
const colName = this.columns[i].name
if (d[colName] && d[colName].toLowerCase().indexOf(this.filter.keyword.toLowerCase()) > -1) {
return true
}
}
return false
})
}
},
created() {
this.$store.dispatch('task/getTaskList')
this.$store.dispatch('spider/getSpiderList')
this.$store.dispatch('node/getNodeList')
},
mounted() {
this.handle = setInterval(() => {
this.$store.dispatch('task/getTaskList')
}, 5000)
if (!this.$utils.tour.isFinishedTour('task-list')) {
this.$utils.tour.startTour(this, 'task-list')
}
},
destroyed() {
clearInterval(this.handle)
},
methods: {
onSearch(value) {
},
onRefresh() {
this.$store.dispatch('task/getTaskList')
this.$st.sendEv('任务列表', '搜索')
},
onRemoveMultipleTask() {
if (this.multipleSelection.length === 0) {
this.$message({
type: 'error',
message: resp.data.error
message: '请选择要删除的任务'
})
return
}
this.$confirm('确定删除任务', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const ids = this.multipleSelection.map(item => item._id)
this.$store.dispatch('task/deleteTaskMultiple', ids).then((resp) => {
if (resp.data.status === 'ok') {
this.$message({
type: 'success',
message: '删除任务成功'
})
this.$store.dispatch('task/getTaskList')
this.$refs['table'].clearSelection()
return
}
this.$message({
type: 'error',
message: resp.data.error
})
})
}).catch(() => {
})
}).catch(() => {
})
},
onRemove (row, ev) {
ev.stopPropagation()
this.$confirm(this.$t('Are you sure to delete this task?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('task/deleteTask', row._id)
.then(() => {
this.$message({
type: 'success',
message: this.$t('Deleted successfully')
},
onRemove(row, ev) {
ev.stopPropagation()
this.$confirm(this.$t('Are you sure to delete this task?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('task/deleteTask', row._id)
.then(() => {
this.$message({
type: 'success',
message: this.$t('Deleted successfully')
})
})
})
this.$st.sendEv('任务列表', '删除任务')
})
},
onRestart (row, ev) {
ev.stopPropagation()
this.$confirm(this.$t('Are you sure to restart this task?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('task/restartTask', row._id)
.then(() => {
this.$message({
type: 'success',
message: this.$t('Restarted successfully')
this.$st.sendEv('任务列表', '删除任务')
})
},
onRestart(row, ev) {
ev.stopPropagation()
this.$confirm(this.$t('Are you sure to restart this task?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('task/restartTask', row._id)
.then(() => {
this.$message({
type: 'success',
message: this.$t('Restarted successfully')
})
})
})
this.$st.sendEv('任务列表', '重新开始任务')
})
},
onView (row) {
this.$router.push(`/tasks/${row._id}`)
this.$st.sendEv('任务列表', '查看任务')
},
onClickSpider (row) {
this.$router.push(`/spiders/${row.spider_id}`)
this.$st.sendEv('任务列表', '点击爬虫详情')
},
onClickNode (row) {
this.$router.push(`/nodes/${row.node_id}`)
this.$st.sendEv('任务列表', '点击节点详情')
},
onPageChange () {
setTimeout(() => {
this.$st.sendEv('任务列表', '重新开始任务')
})
},
onView(row) {
this.$router.push(`/tasks/${row._id}`)
this.$st.sendEv('任务列表', '查看任务')
},
onClickSpider(row) {
this.$router.push(`/spiders/${row.spider_id}`)
this.$st.sendEv('任务列表', '点击爬虫详情')
},
onClickNode(row) {
this.$router.push(`/nodes/${row.node_id}`)
this.$st.sendEv('任务列表', '点击节点详情')
},
onPageChange() {
setTimeout(() => {
this.$store.dispatch('task/getTaskList')
}, 0)
},
getTime(str) {
if (str.match('^0001')) return 'NA'
return dayjs(str).format('YYYY-MM-DD HH:mm:ss')
},
getWaitDuration(row) {
if (row.start_ts.match('^0001')) return 'NA'
return dayjs(row.start_ts).diff(row.create_ts, 'second')
},
getRuntimeDuration(row) {
if (row.finish_ts.match('^0001')) return 'NA'
return dayjs(row.finish_ts).diff(row.start_ts, 'second')
},
getTotalDuration(row) {
if (row.finish_ts.match('^0001')) return 'NA'
return dayjs(row.finish_ts).diff(row.create_ts, 'second')
},
onRowClick(row, event, column) {
if (column.label !== this.$t('Action')) {
this.onView(row)
}
},
onSelectionChange(val) {
this.multipleSelection = val
},
onFilterChange() {
this.$store.dispatch('task/getTaskList')
}, 0)
},
getTime (str) {
if (str.match('^0001')) return 'NA'
return dayjs(str).format('YYYY-MM-DD HH:mm:ss')
},
getWaitDuration (row) {
if (row.start_ts.match('^0001')) return 'NA'
return dayjs(row.start_ts).diff(row.create_ts, 'second')
},
getRuntimeDuration (row) {
if (row.finish_ts.match('^0001')) return 'NA'
return dayjs(row.finish_ts).diff(row.start_ts, 'second')
},
getTotalDuration (row) {
if (row.finish_ts.match('^0001')) return 'NA'
return dayjs(row.finish_ts).diff(row.create_ts, 'second')
},
onRowClick (row, event, column) {
if (column.label !== this.$t('Action')) {
this.onView(row)
this.$st.sendEv('任务列表', '筛选任务')
}
},
onSelectionChange (val) {
this.multipleSelection = val
},
onFilterChange () {
this.$store.dispatch('task/getTaskList')
this.$st.sendEv('任务列表', '筛选任务')
}
},
created () {
this.$store.dispatch('task/getTaskList')
this.$store.dispatch('spider/getSpiderList')
this.$store.dispatch('node/getNodeList')
},
mounted () {
this.handle = setInterval(() => {
this.$store.dispatch('task/getTaskList')
}, 5000)
if (!this.$utils.tour.isFinishedTour('task-list')) {
this.$utils.tour.startTour(this, 'task-list')
}
},
destroyed () {
clearInterval(this.handle)
}
}
</script>
<style scoped lang="scss">

View File

@@ -4,31 +4,31 @@
<el-dialog :visible.sync="dialogVisible" width="640px" :title="$t('Edit User')">
<el-form ref="form" :model="userForm" label-width="80px" :rules="rules" inline-message>
<el-form-item prop="username" :label="$t('Username')" required>
<el-input v-model="userForm.username" :placeholder="$t('Username')" :disabled="!isAdd"></el-input>
<el-input v-model="userForm.username" :placeholder="$t('Username')" :disabled="!isAdd" />
</el-form-item>
<el-form-item prop="password" :label="$t('Password')" required>
<el-input type="password" v-model="userForm.password" :placeholder="$t('Password')"></el-input>
<el-input v-model="userForm.password" type="password" :placeholder="$t('Password')" />
</el-form-item>
<el-form-item prop="role" :label="$t('Role')" required>
<el-select v-model="userForm.role" :placeholder="$t('Role')">
<el-option value="admin" :label="$t('admin')"></el-option>
<el-option value="normal" :label="$t('normal')"></el-option>
<el-option value="admin" :label="$t('admin')" />
<el-option value="normal" :label="$t('normal')" />
</el-select>
</el-form-item>
<el-form-item prop="email" :label="$t('Email')">
<el-input v-model="userForm.email" :placeholder="$t('Email')"/>
<el-input v-model="userForm.email" :placeholder="$t('Email')" />
</el-form-item>
</el-form>
<template slot="footer">
<el-button size="small" @click="dialogVisible=false">{{$t('Cancel')}}</el-button>
<el-button type="primary" size="small" @click="onConfirm">{{$t('Confirm')}}</el-button>
<el-button size="small" @click="dialogVisible=false">{{ $t('Cancel') }}</el-button>
<el-button type="primary" size="small" @click="onConfirm">{{ $t('Confirm') }}</el-button>
</template>
</el-dialog>
<!--./dialog-->
<el-card>
<div class="filter">
<div class="left"></div>
<div class="left" />
<div class="right">
<el-button type="success" icon="el-icon-plus" size="small" @click="onClickAddUser">添加用户</el-button>
</div>
@@ -43,8 +43,7 @@
width="120px"
:label="$t('Username')"
prop="username"
>
</el-table-column>
/>
<el-table-column
width="150px"
:label="$t('Role')"
@@ -66,7 +65,7 @@
:label="$t('Create Time')"
>
<template slot-scope="scope">
{{getTime(scope.row.create_ts)}}
{{ getTime(scope.row.create_ts) }}
</template>
</el-table-column>
<el-table-column
@@ -94,14 +93,14 @@
<div class="pagination">
<el-pagination
@current-change="onPageChange"
@size-change="onPageChange"
:current-page.sync="pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size.sync="pageSize"
layout="sizes, prev, pager, next"
:total="totalCount">
</el-pagination>
:total="totalCount"
@current-change="onPageChange"
@size-change="onPageChange"
/>
</div>
<!--./table-->
</el-card>
@@ -109,150 +108,150 @@
</template>
<script>
import {
mapState,
mapGetters
} from 'vuex'
import dayjs from 'dayjs'
import {
mapState,
mapGetters
} from 'vuex'
import dayjs from 'dayjs'
export default {
name: 'UserList',
data () {
const validatePass = (rule, value, callback) => {
if (!value) return callback()
if (value.length < 5) {
callback(new Error(this.$t('Password length should be no shorter than 5')))
} else {
callback()
export default {
name: 'UserList',
data() {
const validatePass = (rule, value, callback) => {
if (!value) return callback()
if (value.length < 5) {
callback(new Error(this.$t('Password length should be no shorter than 5')))
} else {
callback()
}
}
}
const validateEmail = (rule, value, callback) => {
if (!value) return callback()
if (!value.match(/.+@.+/i)) {
callback(new Error(this.$t('Email format invalid')))
} else {
callback()
const validateEmail = (rule, value, callback) => {
if (!value) return callback()
if (!value.match(/.+@.+/i)) {
callback(new Error(this.$t('Email format invalid')))
} else {
callback()
}
}
}
return {
dialogVisible: false,
isAdd: false,
rules: {
password: [{ validator: validatePass }],
email: [{ validator: validateEmail }]
}
}
},
computed: {
...mapState('user', [
'userList',
'userForm',
'totalCount'
]),
...mapGetters('user', [
'userInfo'
]),
pageSize: {
get () {
return this.$store.state.user.pageSize
},
set (value) {
this.$store.commit('user/SET_PAGE_SIZE', value)
return {
dialogVisible: false,
isAdd: false,
rules: {
password: [{ validator: validatePass }],
email: [{ validator: validateEmail }]
}
}
},
pageNum: {
get () {
return this.$store.state.user.pageNum
computed: {
...mapState('user', [
'userList',
'userForm',
'totalCount'
]),
...mapGetters('user', [
'userInfo'
]),
pageSize: {
get() {
return this.$store.state.user.pageSize
},
set(value) {
this.$store.commit('user/SET_PAGE_SIZE', value)
}
},
set (value) {
this.$store.commit('user/SET_PAGE_NUM', value)
pageNum: {
get() {
return this.$store.state.user.pageNum
},
set(value) {
this.$store.commit('user/SET_PAGE_NUM', value)
}
}
}
},
methods: {
onPageChange () {
},
created() {
this.$store.dispatch('user/getUserList')
},
getTime (ts) {
return dayjs(ts).format('YYYY-MM-DD HH:mm:ss')
},
onEdit (row) {
this.isAdd = false
this.$store.commit('user/SET_USER_FORM', row)
this.dialogVisible = true
},
onRemove (row) {
this.$confirm(this.$t('Are you sure to delete this user?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('user/deleteUser', row._id)
.then(() => {
this.$message({
type: 'success',
message: this.$t('Deleted successfully')
})
})
.then(() => {
this.$store.dispatch('user/getUserList')
})
this.$st.sendEv('用户列表', '删除用户')
})
// this.$store.commit('user/SET_USER_FORM', row)
},
onConfirm () {
this.$refs.form.validate(valid => {
if (!valid) return
if (this.isAdd) {
// 添加用户
this.$store.dispatch('user/addUser')
methods: {
onPageChange() {
this.$store.dispatch('user/getUserList')
},
getTime(ts) {
return dayjs(ts).format('YYYY-MM-DD HH:mm:ss')
},
onEdit(row) {
this.isAdd = false
this.$store.commit('user/SET_USER_FORM', row)
this.dialogVisible = true
},
onRemove(row) {
this.$confirm(this.$t('Are you sure to delete this user?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
}).then(() => {
this.$store.dispatch('user/deleteUser', row._id)
.then(() => {
this.$message({
type: 'success',
message: this.$t('Saved successfully')
message: this.$t('Deleted successfully')
})
this.dialogVisible = false
this.$st.sendEv('用户列表', '添加用户')
})
.then(() => {
this.$store.dispatch('user/getUserList')
})
} else {
// 编辑用户
this.$store.dispatch('user/editUser')
.then(() => {
this.$message({
type: 'success',
message: this.$t('Saved successfully')
this.$st.sendEv('用户列表', '删除用户')
})
// this.$store.commit('user/SET_USER_FORM', row)
},
onConfirm() {
this.$refs.form.validate(valid => {
if (!valid) return
if (this.isAdd) {
// 添加用户
this.$store.dispatch('user/addUser')
.then(() => {
this.$message({
type: 'success',
message: this.$t('Saved successfully')
})
this.dialogVisible = false
this.$st.sendEv('用户列表', '添加用户')
})
this.dialogVisible = false
this.$st.sendEv('用户列表', '编辑用户')
})
.then(() => {
this.$store.dispatch('user/getUserList')
})
} else {
// 编辑用户
this.$store.dispatch('user/editUser')
.then(() => {
this.$message({
type: 'success',
message: this.$t('Saved successfully')
})
this.dialogVisible = false
this.$st.sendEv('用户列表', '编辑用户')
})
}
})
},
onClickAddUser() {
this.isAdd = true
this.$store.commit('user/SET_USER_FORM', {})
this.dialogVisible = true
},
onValidateEmail(value) {
},
isShowEdit(row) {
if (row.username === 'admin') {
return this.userInfo.username === 'admin'
}
})
},
onClickAddUser () {
this.isAdd = true
this.$store.commit('user/SET_USER_FORM', {})
this.dialogVisible = true
},
onValidateEmail (value) {
},
isShowEdit (row) {
if (row.username === 'admin') {
return this.userInfo.username === 'admin'
return true
},
isShowRemove(row) {
return row.username !== 'admin'
}
return true
},
isShowRemove (row) {
return row.username !== 'admin'
}
},
created () {
this.$store.dispatch('user/getUserList')
}
}
</script>
<style lang="scss" scoped>