* 增加Docker开发环境

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

View File

@@ -2,8 +2,10 @@
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noredirect'||index==levelList.length-1"
class="no-redirect">{{$t(item.meta.title) }}</span>
<span
v-if="item.redirect==='noredirect'||index==levelList.length-1"
class="no-redirect"
>{{ $t(item.meta.title) }}</span>
<a v-else @click.prevent="handleLink(item)">{{ $t(item.meta.title) }}</a>
</el-breadcrumb-item>
</transition-group>
@@ -11,64 +13,64 @@
</template>
<script>
import pathToRegexp from 'path-to-regexp'
import pathToRegexp from 'path-to-regexp'
export default {
data () {
return {
levelList: null
}
},
watch: {
$route () {
export default {
data() {
return {
levelList: null
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
}
},
created () {
this.getBreadcrumb()
},
methods: {
getBreadcrumb () {
let matched = this.$route.matched.filter(item => item.name)
const first = matched[0]
if (first && first.name !== 'Home') {
matched = [{ path: '/home', meta: { title: 'Home' } }].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
pathCompile (path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink (item) {
const { redirect } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.getGoToPath(item))
},
getGoToPath (item) {
if (item.path) {
var path = item.path
var startPos = path.indexOf(':')
methods: {
getBreadcrumb() {
let matched = this.$route.matched.filter(item => item.name)
if (startPos !== -1) {
var endPos = path.indexOf('/', startPos)
var key = path.substring(startPos + 1, endPos)
path = path.replace(':' + key, this.$route.params[key])
return path
const first = matched[0]
if (first && first.name !== 'Home') {
matched = [{ path: '/home', meta: { title: 'Home' }}].concat(matched)
}
}
return item.redirect || item.path
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink(item) {
const { redirect } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.getGoToPath(item))
},
getGoToPath(item) {
if (item.path) {
var path = item.path
var startPos = path.indexOf(':')
if (startPos !== -1) {
var endPos = path.indexOf('/', startPos)
var key = path.substring(startPos + 1, endPos)
path = path.replace(':' + key, this.$route.params[key])
return path
}
}
return item.redirect || item.path
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>

View File

@@ -13,17 +13,22 @@
width="580px"
:before-close="beforeClose"
>
<div style="margin-bottom: 20px;">{{$t('Are you sure to run this spider?')}}</div>
<el-form label-width="140px" :model="form" ref="form">
<div style="margin-bottom: 20px;">{{ $t('Are you sure to run this spider?') }}</div>
<el-form ref="form" label-width="140px" :model="form">
<el-form-item :label="$t('Run Type')" prop="runType" required inline-message>
<el-select v-model="form.runType" :placeholder="$t('Run Type')">
<el-option value="all-nodes" :label="$t('All Nodes')"/>
<el-option value="selected-nodes" :label="$t('Selected Nodes')"/>
<el-option value="random" :label="$t('Random')"/>
<el-option value="all-nodes" :label="$t('All Nodes')" />
<el-option value="selected-nodes" :label="$t('Selected Nodes')" />
<el-option value="random" :label="$t('Random')" />
</el-select>
</el-form-item>
<el-form-item v-if="form.runType === 'selected-nodes'" prop="nodeIds" :label="$t('Node')" required
inline-message>
<el-form-item
v-if="form.runType === 'selected-nodes'"
prop="nodeIds"
:label="$t('Node')"
required
inline-message
>
<el-select v-model="form.nodeIds" :placeholder="$t('Node')" multiple clearable>
<el-option
v-for="op in nodeList"
@@ -34,8 +39,13 @@
/>
</el-select>
</el-form-item>
<el-form-item v-if="spiderForm.is_scrapy && !multiple" :label="$t('Scrapy Spider')" prop="spider" required
inline-message>
<el-form-item
v-if="spiderForm.is_scrapy && !multiple"
:label="$t('Scrapy Spider')"
prop="spider"
required
inline-message
>
<el-select v-model="form.spider" :placeholder="$t('Scrapy Spider')" :disabled="isLoading">
<el-option
v-for="s in spiderForm.spider_names"
@@ -53,24 +63,24 @@
inline-message
>
<el-select v-model="form.scrapy_log_level" :placeholder="$t('Scrapy Log Level')">
<el-option value="INFO" label="INFO"/>
<el-option value="DEBUG" label="DEBUG"/>
<el-option value="WARN" label="WARN"/>
<el-option value="ERROR" label="ERROR"/>
<el-option value="INFO" label="INFO" />
<el-option value="DEBUG" label="DEBUG" />
<el-option value="WARN" label="WARN" />
<el-option value="ERROR" label="ERROR" />
</el-select>
</el-form-item>
<el-form-item v-if="spiderForm.type === 'customized'" :label="$t('Parameters')" prop="param" inline-message>
<template v-if="spiderForm.is_scrapy && !multiple">
<el-input v-model="form.param" :placeholder="$t('Parameters')" class="param-input"/>
<el-button type="primary" icon="el-icon-edit" class="param-btn" @click="onOpenParameters"/>
<el-input v-model="form.param" :placeholder="$t('Parameters')" class="param-input" />
<el-button type="primary" icon="el-icon-edit" class="param-btn" @click="onOpenParameters" />
</template>
<template v-else>
<el-input v-model="form.param" :placeholder="$t('Parameters')"></el-input>
<el-input v-model="form.param" :placeholder="$t('Parameters')" />
</template>
</el-form-item>
<el-form-item class="checkbox-wrapper">
<div>
<el-checkbox v-model="isAllowDisclaimer"/>
<el-checkbox v-model="isAllowDisclaimer" />
<span v-if="lang === 'zh'" style="margin-left: 5px">
我已阅读并同意
<a href="javascript:" @click="onClickDisclaimer">
@@ -86,19 +96,19 @@
</span>
</div>
<div v-if="!spiderForm.is_long_task && !multiple">
<el-checkbox v-model="isRedirect"/>
<span style="margin-left: 5px">{{$t('Redirect to task detail')}}</span>
<el-checkbox v-model="isRedirect" />
<span style="margin-left: 5px">{{ $t('Redirect to task detail') }}</span>
</div>
<div v-if="false">
<el-checkbox v-model="isRetry"/>
<span style="margin-left: 5px">{{$t('Retry (Maximum 5 Times)')}}</span>
<el-checkbox v-model="isRetry" />
<span style="margin-left: 5px">{{ $t('Retry (Maximum 5 Times)') }}</span>
</div>
</el-form-item>
</el-form>
<template slot="footer">
<el-button type="plain" size="small" @click="$emit('close')">{{$t('Cancel')}}</el-button>
<el-button type="primary" size="small" @click="onConfirm" :disabled="isConfirmDisabled">
{{$t('Confirm')}}
<el-button type="plain" size="small" @click="$emit('close')">{{ $t('Cancel') }}</el-button>
<el-button type="primary" size="small" :disabled="isConfirmDisabled" @click="onConfirm">
{{ $t('Confirm') }}
</el-button>
</template>
</el-dialog>
@@ -106,222 +116,222 @@
</template>
<script>
import {
mapState
} from 'vuex'
import ParametersDialog from './ParametersDialog'
import {
mapState
} from 'vuex'
import ParametersDialog from './ParametersDialog'
export default {
name: 'CrawlConfirmDialog',
components: { ParametersDialog },
props: {
spiderId: {
type: String,
default: ''
},
spiders: {
type: Array,
default () {
return []
}
},
visible: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: false
}
},
data () {
return {
form: {
runType: 'random',
nodeIds: undefined,
spider: undefined,
scrapy_log_level: 'INFO',
param: '',
nodeList: []
export default {
name: 'CrawlConfirmDialog',
components: { ParametersDialog },
props: {
spiderId: {
type: String,
default: ''
},
isAllowDisclaimer: true,
isRetry: false,
isRedirect: true,
isLoading: false,
isParametersVisible: false,
scrapySpidersNamesDict: {}
}
},
computed: {
...mapState('spider', [
'spiderForm'
]),
...mapState('setting', [
'setting'
]),
...mapState('lang', [
'lang'
]),
isConfirmDisabled () {
if (this.isLoading) return true
if (!this.isAllowDisclaimer) return true
return false
},
scrapySpiders () {
return this.spiders.filter(d => d.type === 'customized' && d.is_scrapy)
}
},
watch: {
visible (value) {
if (value) {
this.onOpen()
}
}
},
methods: {
beforeClose () {
this.$emit('close')
},
beforeParameterClose () {
this.isParametersVisible = false
},
async fetchScrapySpiderName (id) {
const res = await this.$request.get(`/spiders/${id}/scrapy/spiders`)
this.scrapySpidersNamesDict[id] = res.data.data
},
onConfirm () {
this.$refs['form'].validate(async valid => {
if (!valid) return
// 请求响应
let res
if (!this.multiple) {
// 运行单个爬虫
// 参数
let param = this.form.param
// Scrapy爬虫特殊处理
if (this.spiderForm.type === 'customized' && this.spiderForm.is_scrapy) {
param = `${this.form.spider} --loglevel=${this.form.scrapy_log_level} ${this.form.param}`
}
// 发起请求
res = await this.$store.dispatch('spider/crawlSpider', {
spiderId: this.spiderId,
nodeIds: this.form.nodeIds,
param,
runType: this.form.runType
})
} else {
// 运行多个爬虫
// 发起请求
res = await this.$store.dispatch('spider/crawlSelectedSpiders', {
nodeIds: this.form.nodeIds,
runType: this.form.runType,
taskParams: this.spiders.map(d => {
// 参数
let param = this.form.param
// Scrapy爬虫特殊处理
if (d.type === 'customized' && d.is_scrapy) {
param = `${this.scrapySpidersNamesDict[d._id] ? this.scrapySpidersNamesDict[d._id][0] : ''} --loglevel=${this.form.scrapy_log_level} ${this.form.param}`
}
return {
spider_id: d._id,
param
}
})
})
spiders: {
type: Array,
default() {
return []
}
// 消息提示
this.$message.success(this.$t('A task has been scheduled successfully'))
},
visible: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: false
}
},
data() {
return {
form: {
runType: 'random',
nodeIds: undefined,
spider: undefined,
scrapy_log_level: 'INFO',
param: '',
nodeList: []
},
isAllowDisclaimer: true,
isRetry: false,
isRedirect: true,
isLoading: false,
isParametersVisible: false,
scrapySpidersNamesDict: {}
}
},
computed: {
...mapState('spider', [
'spiderForm'
]),
...mapState('setting', [
'setting'
]),
...mapState('lang', [
'lang'
]),
isConfirmDisabled() {
if (this.isLoading) return true
if (!this.isAllowDisclaimer) return true
return false
},
scrapySpiders() {
return this.spiders.filter(d => d.type === 'customized' && d.is_scrapy)
}
},
watch: {
visible(value) {
if (value) {
this.onOpen()
}
}
},
methods: {
beforeClose() {
this.$emit('close')
if (this.multiple) {
this.$st.sendEv('爬虫确认', '确认批量运行', this.form.runType)
} else {
this.$st.sendEv('爬虫确认', '确认运行', this.form.runType)
}
},
beforeParameterClose() {
this.isParametersVisible = false
},
async fetchScrapySpiderName(id) {
const res = await this.$request.get(`/spiders/${id}/scrapy/spiders`)
this.scrapySpidersNamesDict[id] = res.data.data
},
onConfirm() {
this.$refs['form'].validate(async valid => {
if (!valid) return
// 是否重定向
if (
this.isRedirect &&
!this.spiderForm.is_long_task &&
!this.multiple
) {
// 返回任务id
const id = res.data.data[0]
this.$router.push('/tasks/' + id)
this.$st.sendEv('爬虫确认', '跳转到任务详情')
}
// 请求响应
let res
this.$emit('confirm')
})
},
onClickDisclaimer () {
this.$router.push('/disclaimer')
},
async onOpen () {
// 节点列表
this.$request.get('/nodes', {}).then(response => {
this.nodeList = response.data.data.map(d => {
d.systemInfo = {
os: '',
arch: '',
num_cpu: '',
executables: []
}
return d
})
})
if (!this.multiple) {
// 运行单个爬虫
// 爬虫列表
if (!this.multiple) {
// 单个爬虫
this.isLoading = true
try {
await this.$store.dispatch('spider/getSpiderData', this.spiderId)
if (this.spiderForm.is_scrapy) {
await this.$store.dispatch('spider/getSpiderScrapySpiders', this.spiderId)
if (this.spiderForm.spider_names && this.spiderForm.spider_names.length > 0) {
this.$set(this.form, 'spider', this.spiderForm.spider_names[0])
// 参数
let param = this.form.param
// Scrapy爬虫特殊处理
if (this.spiderForm.type === 'customized' && this.spiderForm.is_scrapy) {
param = `${this.form.spider} --loglevel=${this.form.scrapy_log_level} ${this.form.param}`
}
// 发起请求
res = await this.$store.dispatch('spider/crawlSpider', {
spiderId: this.spiderId,
nodeIds: this.form.nodeIds,
param,
runType: this.form.runType
})
} else {
// 运行多个爬虫
// 发起请求
res = await this.$store.dispatch('spider/crawlSelectedSpiders', {
nodeIds: this.form.nodeIds,
runType: this.form.runType,
taskParams: this.spiders.map(d => {
// 参数
let param = this.form.param
// Scrapy爬虫特殊处理
if (d.type === 'customized' && d.is_scrapy) {
param = `${this.scrapySpidersNamesDict[d._id] ? this.scrapySpidersNamesDict[d._id][0] : ''} --loglevel=${this.form.scrapy_log_level} ${this.form.param}`
}
return {
spider_id: d._id,
param
}
})
})
}
// 消息提示
this.$message.success(this.$t('A task has been scheduled successfully'))
this.$emit('close')
if (this.multiple) {
this.$st.sendEv('爬虫确认', '确认批量运行', this.form.runType)
} else {
this.$st.sendEv('爬虫确认', '确认运行', this.form.runType)
}
// 是否重定向
if (
this.isRedirect &&
!this.spiderForm.is_long_task &&
!this.multiple
) {
// 返回任务id
const id = res.data.data[0]
this.$router.push('/tasks/' + id)
this.$st.sendEv('爬虫确认', '跳转到任务详情')
}
this.$emit('confirm')
})
},
onClickDisclaimer() {
this.$router.push('/disclaimer')
},
async onOpen() {
// 节点列表
this.$request.get('/nodes', {}).then(response => {
this.nodeList = response.data.data.map(d => {
d.systemInfo = {
os: '',
arch: '',
num_cpu: '',
executables: []
}
return d
})
})
// 爬虫列表
if (!this.multiple) {
// 单个爬虫
this.isLoading = true
try {
await this.$store.dispatch('spider/getSpiderData', this.spiderId)
if (this.spiderForm.is_scrapy) {
await this.$store.dispatch('spider/getSpiderScrapySpiders', this.spiderId)
if (this.spiderForm.spider_names && this.spiderForm.spider_names.length > 0) {
this.$set(this.form, 'spider', this.spiderForm.spider_names[0])
}
}
} finally {
this.isLoading = false
}
} else {
// 多个爬虫
this.isLoading = true
try {
// 遍历 Scrapy 爬虫列表
await Promise.all(this.scrapySpiders.map(async d => {
return this.fetchScrapySpiderName(d._id)
}))
} finally {
this.isLoading = false
}
} finally {
this.isLoading = false
}
} else {
// 多个爬虫
this.isLoading = true
try {
// 遍历 Scrapy 爬虫列表
await Promise.all(this.scrapySpiders.map(async d => {
return this.fetchScrapySpiderName(d._id)
}))
} finally {
this.isLoading = false
}
},
onOpenParameters() {
this.isParametersVisible = true
},
onParametersConfirm(value) {
this.form.param = value
this.isParametersVisible = false
},
isNodeDisabled(node) {
if (node.status !== 'online') return true
if (node.is_master && this.setting.run_on_master === 'N') return true
return false
}
},
onOpenParameters () {
this.isParametersVisible = true
},
onParametersConfirm (value) {
this.form.param = value
this.isParametersVisible = false
},
isNodeDisabled (node) {
if (node.status !== 'online') return true
if (node.is_master && this.setting.run_on_master === 'N') return true
return false
}
}
}
</script>
<style scoped>

View File

@@ -4,18 +4,19 @@
class="deploy-dialog"
:title="title"
:visible.sync="dialogVisible"
width="40%">
width="40%"
>
<!--message-->
<label>{{message}}</label>
<label>{{ message }}</label>
<!--selection for node-->
<el-select v-if="type === 'node'" v-model="activeSpider._id">
<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>
<!--selection for spider-->
<el-select v-else-if="type === 'spider'" v-model="activeNode._id">
<el-option v-for="op in nodeList" :key="op._id" :value="op._id" :label="op.name"></el-option>
<el-option v-for="op in nodeList" :key="op._id" :value="op._id" :label="op.name" />
</el-select>
<!--action buttons-->
@@ -29,132 +30,133 @@
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'DialogView',
computed: {
...mapState('spider', [
'spiderList',
'spiderForm'
]),
...mapState('node', [
'nodeList'
]),
...mapState('dialogView', [
'dialogType'
]),
type () {
if (this.dialogType === 'nodeDeploy') {
return 'node'
} else if (this.dialogType === 'nodeRun') {
return 'node'
} else if (this.dialogType === 'spiderDeploy') {
return 'spider'
} else if (this.dialogType === 'spiderRun') {
return 'spider'
} else {
return ''
}
},
activeNode: {
get () {
return this.$store.state.spider.activeNode
export default {
name: 'DialogView',
computed: {
...mapState('spider', [
'spiderList',
'spiderForm'
]),
...mapState('node', [
'nodeList'
]),
...mapState('dialogView', [
'dialogType'
]),
type() {
if (this.dialogType === 'nodeDeploy') {
return 'node'
} else if (this.dialogType === 'nodeRun') {
return 'node'
} else if (this.dialogType === 'spiderDeploy') {
return 'spider'
} else if (this.dialogType === 'spiderRun') {
return 'spider'
} else {
return ''
}
},
set () {
this.$store.commit('spider/SET_ACTIVE_NODE')
}
},
activeSpider: {
get () {
return this.$store.state.node.activeSpider
activeNode: {
get() {
return this.$store.state.spider.activeNode
},
set() {
this.$store.commit('spider/SET_ACTIVE_NODE')
}
},
set () {
this.$store.commit('node/SET_ACTIVE_SPIDER')
}
},
dialogVisible: {
get () {
return this.$store.state.dialogView.dialogVisible
activeSpider: {
get() {
return this.$store.state.node.activeSpider
},
set() {
this.$store.commit('node/SET_ACTIVE_SPIDER')
}
},
set (value) {
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', value)
dialogVisible: {
get() {
return this.$store.state.dialogView.dialogVisible
},
set(value) {
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', value)
}
},
title() {
if (this.dialogType === 'nodeDeploy') {
return 'Deploy'
} else if (this.dialogType === 'nodeRun') {
return 'Run'
} else if (this.dialogType === 'spiderDeploy') {
return 'Deploy'
} else if (this.dialogType === 'spiderRun') {
return 'Run'
} else {
return ''
}
},
message() {
if (this.dialogType === 'nodeDeploy') {
return 'Please select spider you would like to deploy'
} else if (this.dialogType === 'nodeRun') {
return 'Please select spider you would like to run'
} else if (this.dialogType === 'spiderDeploy') {
return 'Please select node you would like to deploy'
} else if (this.dialogType === 'spiderRun') {
return 'Please select node you would like to run'
} else {
return ''
}
}
},
title () {
if (this.dialogType === 'nodeDeploy') {
return 'Deploy'
} else if (this.dialogType === 'nodeRun') {
return 'Run'
} else if (this.dialogType === 'spiderDeploy') {
return 'Deploy'
} else if (this.dialogType === 'spiderRun') {
return 'Run'
} else {
return ''
}
mounted() {
// if (!this.spiderList || !this.spiderList.length) this.$store.dispatch('spider/getSpiderList')
if (!this.nodeList || !this.nodeList.length) this.$store.dispatch('node/getNodeList')
},
message () {
if (this.dialogType === 'nodeDeploy') {
return 'Please select spider you would like to deploy'
} else if (this.dialogType === 'nodeRun') {
return 'Please select spider you would like to run'
} else if (this.dialogType === 'spiderDeploy') {
return 'Please select node you would like to deploy'
} else if (this.dialogType === 'spiderRun') {
return 'Please select node you would like to run'
} else {
return ''
methods: {
onCancel() {
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', false)
},
onConfirm() {
if (this.dialogType === 'nodeDeploy') {
return
} else if (this.dialogType === 'nodeRun') {
return
} else if (this.dialogType === 'spiderDeploy') {
this.$store.dispatch('spider/deploySpider', {
id: this.spiderForm._id,
nodeId: this.activeNode._id
})
.then(() => {
this.$message.success(`Spider "${this.spiderForm.name}" has been deployed on node "${this.activeNode._id}" successfully`)
})
.finally(() => {
// get spider deploys
this.$store.dispatch('spider/getDeployList', this.$route.params.id)
// close dialog
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', false)
})
} else if (this.dialogType === 'spiderRun') {
this.$store.dispatch('spider/crawlSpider', this.spiderForm._id)
.then(() => {
this.$message.success(`Spider "${this.spiderForm.name}" started to run on node "${this.activeNode._id}"`)
})
.finally(() => {
// get spider tasks
setTimeout(() => {
this.$store.dispatch('spider/getTaskList', this.$route.params.id)
}, 500)
// close dialog
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', false)
})
}
}
}
},
methods: {
onCancel () {
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', false)
},
onConfirm () {
if (this.dialogType === 'nodeDeploy') {
} else if (this.dialogType === 'nodeRun') {
} else if (this.dialogType === 'spiderDeploy') {
this.$store.dispatch('spider/deploySpider', {
id: this.spiderForm._id,
nodeId: this.activeNode._id
})
.then(() => {
this.$message.success(`Spider "${this.spiderForm.name}" has been deployed on node "${this.activeNode._id}" successfully`)
})
.finally(() => {
// get spider deploys
this.$store.dispatch('spider/getDeployList', this.$route.params.id)
// close dialog
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', false)
})
} else if (this.dialogType === 'spiderRun') {
this.$store.dispatch('spider/crawlSpider', this.spiderForm._id)
.then(() => {
this.$message.success(`Spider "${this.spiderForm.name}" started to run on node "${this.activeNode._id}"`)
})
.finally(() => {
// get spider tasks
setTimeout(() => {
this.$store.dispatch('spider/getTaskList', this.$route.params.id)
}, 500)
// close dialog
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', false)
})
} else {
}
}
},
mounted () {
// if (!this.spiderList || !this.spiderList.length) this.$store.dispatch('spider/getSpiderList')
if (!this.nodeList || !this.nodeList.length) this.$store.dispatch('node/getNodeList')
}
}
</script>
<style scoped>

View File

@@ -12,7 +12,7 @@
size="small"
@click="onAdd"
>
{{$t('Add')}}
{{ $t('Add') }}
</el-button>
</div>
<el-table
@@ -68,7 +68,7 @@
:label="$t('Parameter Value')"
>
<template slot-scope="scope">
<el-input v-model="scope.row.value" size="small" suffix-icon="el-icon-edit"/>
<el-input v-model="scope.row.value" size="small" suffix-icon="el-icon-edit" />
</template>
</el-table-column>
<el-table-column
@@ -78,121 +78,121 @@
>
<template slot-scope="scope">
<div class="action-btn-wrapper">
<el-button type="danger" icon="el-icon-delete" size="mini" @click="onRemove(scope.$index)" circle/>
<el-button type="danger" icon="el-icon-delete" size="mini" circle @click="onRemove(scope.$index)" />
</div>
</template>
</el-table-column>
</el-table>
<template slot="footer">
<el-button type="plain" size="small" @click="$emit('close')">{{$t('Cancel')}}</el-button>
<el-button type="plain" size="small" @click="$emit('close')">{{ $t('Cancel') }}</el-button>
<el-button type="primary" size="small" @click="onConfirm">
{{$t('Confirm')}}
{{ $t('Confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<script>
export default {
name: 'ParametersDialog',
props: {
visible: {
type: Boolean,
default: false
},
param: {
type: String,
default: ''
}
},
data () {
return {
paramData: []
}
},
watch: {
visible (value) {
if (value) this.initParamData()
}
},
methods: {
beforeClose () {
this.$emit('close')
},
initParamData () {
const mArr = this.param.match(/((?:-[-a-zA-Z0-9] )?(?:\w+=)?\w+)/g)
if (!mArr) {
this.paramData = []
this.paramData.push({ type: 'spider', name: '', value: '' })
return
export default {
name: 'ParametersDialog',
props: {
visible: {
type: Boolean,
default: false
},
param: {
type: String,
default: ''
}
this.paramData = []
mArr.forEach(s => {
s = s.trim()
let d = {}
const arr = s.split(' ')
if (arr.length === 1) {
d.type = 'other'
d.value = s
} else {
const arr2 = arr[1].split('=')
d.name = arr2[0]
d.value = arr2[1]
if (arr[0] === '-a') {
d.type = 'spider'
} else if (arr[0] === '-s') {
d.type = 'setting'
} else {
},
data() {
return {
paramData: []
}
},
watch: {
visible(value) {
if (value) this.initParamData()
}
},
methods: {
beforeClose() {
this.$emit('close')
},
initParamData() {
const mArr = this.param.match(/((?:-[-a-zA-Z0-9] )?(?:\w+=)?\w+)/g)
if (!mArr) {
this.paramData = []
this.paramData.push({ type: 'spider', name: '', value: '' })
return
}
this.paramData = []
mArr.forEach(s => {
s = s.trim()
const d = {}
const arr = s.split(' ')
if (arr.length === 1) {
d.type = 'other'
d.value = s
} else {
const arr2 = arr[1].split('=')
d.name = arr2[0]
d.value = arr2[1]
if (arr[0] === '-a') {
d.type = 'spider'
} else if (arr[0] === '-s') {
d.type = 'setting'
} else {
d.type = 'other'
d.value = s
}
}
}
this.paramData.push(d)
})
if (this.paramData.length === 0) {
this.paramData.push({ type: 'spider', name: '', value: '' })
}
},
onConfirm () {
const param = this.paramData
.filter(d => d.value)
.map(d => {
let s = ''
if (d.type === 'setting') {
s = `-s ${d.name}=${d.value}`
} else if (d.type === 'spider') {
s = `-a ${d.name}=${d.value}`
} else if (d.type === 'other') {
s = d.value
}
return s
this.paramData.push(d)
})
.filter(s => !!s)
.join(' ')
this.$emit('confirm', param)
},
onRemove (index) {
this.paramData.splice(index, 1)
},
onAdd () {
this.paramData.push({ type: 'spider', name: '', value: '' })
},
querySearch (queryString, cb) {
let data = this.$utils.scrapy.settingParamNames
if (!queryString) {
return cb(data.map(s => {
if (this.paramData.length === 0) {
this.paramData.push({ type: 'spider', name: '', value: '' })
}
},
onConfirm() {
const param = this.paramData
.filter(d => d.value)
.map(d => {
let s = ''
if (d.type === 'setting') {
s = `-s ${d.name}=${d.value}`
} else if (d.type === 'spider') {
s = `-a ${d.name}=${d.value}`
} else if (d.type === 'other') {
s = d.value
}
return s
})
.filter(s => !!s)
.join(' ')
this.$emit('confirm', param)
},
onRemove(index) {
this.paramData.splice(index, 1)
},
onAdd() {
this.paramData.push({ type: 'spider', name: '', value: '' })
},
querySearch(queryString, cb) {
let data = this.$utils.scrapy.settingParamNames
if (!queryString) {
return cb(data.map(s => {
return { value: s, label: s }
}))
}
data = data
.filter(s => s.match(new RegExp(queryString, 'i')))
.sort((a, b) => a < b ? 1 : -1)
cb(data.map(s => {
return { value: s, label: s }
}))
}
data = data
.filter(s => s.match(new RegExp(queryString, 'i')))
.sort((a, b) => a < b ? 1 : -1)
cb(data.map(s => {
return { value: s, label: s }
}))
}
}
}
</script>
<style scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -47,153 +47,153 @@
<div id="change-crontab">
<div class="cron-wrapper">
<label>
{{$t('Cron Expression')}}:
{{ $t('Cron Expression') }}:
</label>
<el-tag type="success" size="small">
{{cron}}
{{ cron }}
</el-tag>
</div>
<el-tabs type="border-card">
<el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> {{text.Minutes.name}}</span>
<span slot="label"><i class="el-icon-date" /> {{ text.Minutes.name }}</span>
<div class="tabBody">
<el-row>
<el-radio v-model="minute.cronEvery" label="1">{{text.Minutes.every}}</el-radio>
<el-radio v-model="minute.cronEvery" label="1">{{ text.Minutes.every }}</el-radio>
</el-row>
<el-row>
<el-radio v-model="minute.cronEvery" label="2">{{text.Minutes.interval[0]}}
<el-input-number size="small" v-model="minute.incrementIncrement" :min="0" :max="59"></el-input-number>
{{text.Minutes.interval[1]}}
<el-input-number size="small" v-model="minute.incrementStart" :min="0" :max="59"></el-input-number>
{{text.Minutes.interval[2]||''}}
<el-radio v-model="minute.cronEvery" label="2">{{ text.Minutes.interval[0] }}
<el-input-number v-model="minute.incrementIncrement" size="small" :min="0" :max="59" />
{{ text.Minutes.interval[1] }}
<el-input-number v-model="minute.incrementStart" size="small" :min="0" :max="59" />
{{ text.Minutes.interval[2]||'' }}
</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="minute.cronEvery" label="3">{{text.Minutes.specific}}
<el-select size="small" multiple v-model="minute.specificSpecific">
<el-option v-for="val in 60" :key="val" :value="(val-1).toString()" :label="val-1"></el-option>
<el-radio v-model="minute.cronEvery" class="long" label="3">{{ text.Minutes.specific }}
<el-select v-model="minute.specificSpecific" size="small" multiple>
<el-option v-for="val in 60" :key="val" :value="(val-1).toString()" :label="val-1" />
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="minute.cronEvery" label="4">{{text.Minutes.cycle[0]}}
<el-input-number size="small" v-model="minute.rangeStart" :min="0" :max="59"></el-input-number>
{{text.Minutes.cycle[1]}}
<el-input-number size="small" v-model="minute.rangeEnd" :min="0" :max="59"></el-input-number>
{{text.Minutes.cycle[2]}}
<el-radio v-model="minute.cronEvery" label="4">{{ text.Minutes.cycle[0] }}
<el-input-number v-model="minute.rangeStart" size="small" :min="0" :max="59" />
{{ text.Minutes.cycle[1] }}
<el-input-number v-model="minute.rangeEnd" size="small" :min="0" :max="59" />
{{ text.Minutes.cycle[2] }}
</el-radio>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> {{text.Hours.name}}</span>
<span slot="label"><i class="el-icon-date" /> {{ text.Hours.name }}</span>
<div class="tabBody">
<el-row>
<el-radio v-model="hour.cronEvery" label="1">{{text.Hours.every}}</el-radio>
<el-radio v-model="hour.cronEvery" label="1">{{ text.Hours.every }}</el-radio>
</el-row>
<el-row>
<el-radio v-model="hour.cronEvery" label="2">{{text.Hours.interval[0]}}
<el-input-number size="small" v-model="hour.incrementIncrement" :min="0" :max="23"></el-input-number>
{{text.Hours.interval[1]}}
<el-input-number size="small" v-model="hour.incrementStart" :min="0" :max="23"></el-input-number>
{{text.Hours.interval[2]}}
<el-radio v-model="hour.cronEvery" label="2">{{ text.Hours.interval[0] }}
<el-input-number v-model="hour.incrementIncrement" size="small" :min="0" :max="23" />
{{ text.Hours.interval[1] }}
<el-input-number v-model="hour.incrementStart" size="small" :min="0" :max="23" />
{{ text.Hours.interval[2] }}
</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="hour.cronEvery" label="3">{{text.Hours.specific}}
<el-select size="small" multiple v-model="hour.specificSpecific">
<el-option v-for="val in 24" :key="val" :value="(val-1).toString()" :label="val-1"></el-option>
<el-radio v-model="hour.cronEvery" class="long" label="3">{{ text.Hours.specific }}
<el-select v-model="hour.specificSpecific" size="small" multiple>
<el-option v-for="val in 24" :key="val" :value="(val-1).toString()" :label="val-1" />
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="hour.cronEvery" label="4">{{text.Hours.cycle[0]}}
<el-input-number size="small" v-model="hour.rangeStart" :min="0" :max="23"></el-input-number>
{{text.Hours.cycle[1]}}
<el-input-number size="small" v-model="hour.rangeEnd" :min="0" :max="23"></el-input-number>
{{text.Hours.cycle[2]}}
<el-radio v-model="hour.cronEvery" label="4">{{ text.Hours.cycle[0] }}
<el-input-number v-model="hour.rangeStart" size="small" :min="0" :max="23" />
{{ text.Hours.cycle[1] }}
<el-input-number v-model="hour.rangeEnd" size="small" :min="0" :max="23" />
{{ text.Hours.cycle[2] }}
</el-radio>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> {{text.Day.name}}</span>
<span slot="label"><i class="el-icon-date" /> {{ text.Day.name }}</span>
<div class="tabBody">
<el-row>
<el-radio v-model="day.cronEvery" label="1">{{text.Day.every}}</el-radio>
<el-radio v-model="day.cronEvery" label="1">{{ text.Day.every }}</el-radio>
</el-row>
<el-row>
<el-radio v-model="day.cronEvery" label="2">{{text.Day.intervalDay[0]}}
<el-input-number size="small" v-model="day.incrementIncrement" :min="1" :max="31"></el-input-number>
{{text.Day.intervalDay[1]}}
<el-input-number size="small" v-model="day.incrementStart" :min="1" :max="31"></el-input-number>
{{text.Day.intervalDay[2]}}
<el-radio v-model="day.cronEvery" label="2">{{ text.Day.intervalDay[0] }}
<el-input-number v-model="day.incrementIncrement" size="small" :min="1" :max="31" />
{{ text.Day.intervalDay[1] }}
<el-input-number v-model="day.incrementStart" size="small" :min="1" :max="31" />
{{ text.Day.intervalDay[2] }}
</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="day.cronEvery" label="3">{{text.Day.specificDay}}
<el-select size="small" multiple v-model="day.specificSpecific">
<el-option v-for="val in 31" :key="val" :value="val.toString()" :label="val"></el-option>
<el-radio v-model="day.cronEvery" class="long" label="3">{{ text.Day.specificDay }}
<el-select v-model="day.specificSpecific" size="small" multiple>
<el-option v-for="val in 31" :key="val" :value="val.toString()" :label="val" />
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="day.cronEvery" label="4">{{text.Day.cycle[0]}}
<el-input-number size="small" v-model="day.rangeStart" :min="1" :max="31"></el-input-number>
{{text.Day.cycle[1]}}
<el-input-number size="small" v-model="day.rangeEnd" :min="1" :max="31"></el-input-number>
<el-radio v-model="day.cronEvery" label="4">{{ text.Day.cycle[0] }}
<el-input-number v-model="day.rangeStart" size="small" :min="1" :max="31" />
{{ text.Day.cycle[1] }}
<el-input-number v-model="day.rangeEnd" size="small" :min="1" :max="31" />
</el-radio>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> {{text.Month.name}}</span>
<span slot="label"><i class="el-icon-date" /> {{ text.Month.name }}</span>
<div class="tabBody">
<el-row>
<el-radio v-model="month.cronEvery" label="1">{{text.Month.every}}</el-radio>
<el-radio v-model="month.cronEvery" label="1">{{ text.Month.every }}</el-radio>
</el-row>
<el-row>
<el-radio v-model="month.cronEvery" label="2">{{text.Month.interval[0]}}
<el-input-number size="small" v-model="month.incrementIncrement" :min="0" :max="12"></el-input-number>
{{text.Month.interval[1]}}
<el-input-number size="small" v-model="month.incrementStart" :min="0" :max="12"></el-input-number>
<el-radio v-model="month.cronEvery" label="2">{{ text.Month.interval[0] }}
<el-input-number v-model="month.incrementIncrement" size="small" :min="0" :max="12" />
{{ text.Month.interval[1] }}
<el-input-number v-model="month.incrementStart" size="small" :min="0" :max="12" />
</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="month.cronEvery" label="3">{{text.Month.specific}}
<el-select size="small" multiple v-model="month.specificSpecific">
<el-option v-for="val in 12" :key="val" :label="val" :value="val.toString()"></el-option>
<el-radio v-model="month.cronEvery" class="long" label="3">{{ text.Month.specific }}
<el-select v-model="month.specificSpecific" size="small" multiple>
<el-option v-for="val in 12" :key="val" :label="val" :value="val.toString()" />
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="month.cronEvery" label="4">{{text.Month.cycle[0]}}
<el-input-number size="small" v-model="month.rangeStart" :min="1" :max="12"></el-input-number>
{{text.Month.cycle[1]}}
<el-input-number size="small" v-model="month.rangeEnd" :min="1" :max="12"></el-input-number>
{{text.Month.cycle[2]}}
<el-radio v-model="month.cronEvery" label="4">{{ text.Month.cycle[0] }}
<el-input-number v-model="month.rangeStart" size="small" :min="1" :max="12" />
{{ text.Month.cycle[1] }}
<el-input-number v-model="month.rangeEnd" size="small" :min="1" :max="12" />
{{ text.Month.cycle[2] }}
</el-radio>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane>
<span slot="label"><i class="el-icon-date"></i> {{text.Week.name}}</span>
<span slot="label"><i class="el-icon-date" /> {{ text.Week.name }}</span>
<div class="tabBody">
<el-row>
<el-radio v-model="week.cronEvery" label="1">{{text.Week.every}}</el-radio>
<el-radio v-model="week.cronEvery" label="1">{{ text.Week.every }}</el-radio>
</el-row>
<el-row>
<el-radio class="long" v-model="week.cronEvery" label="3">{{text.Week.specific}}
<el-select size="small" multiple v-model="week.specificSpecific">
<el-option v-for="i in 7" :key="i" :label="text.Week.list[i - 1]" :value="i.toString()"></el-option>
<el-radio v-model="week.cronEvery" class="long" label="3">{{ text.Week.specific }}
<el-select v-model="week.specificSpecific" size="small" multiple>
<el-option v-for="i in 7" :key="i" :label="text.Week.list[i - 1]" :value="i.toString()" />
</el-select>
</el-radio>
</el-row>
<el-row>
<el-radio v-model="week.cronEvery" label="4">{{text.Week.cycle[0]}}
<el-input-number size="small" v-model="week.rangeStart" :min="1" :max="7"></el-input-number>
{{text.Week.cycle[1]}}
<el-input-number size="small" v-model="week.rangeEnd" :min="1" :max="7"></el-input-number>
<el-radio v-model="week.cronEvery" label="4">{{ text.Week.cycle[0] }}
<el-input-number v-model="week.rangeStart" size="small" :min="1" :max="7" />
{{ text.Week.cycle[1] }}
<el-input-number v-model="week.rangeEnd" size="small" :min="1" :max="7" />
</el-radio>
</el-row>
</div>
@@ -202,93 +202,85 @@
</div>
</template>
<script>
import Language from './language/index'
import Language from './language/index'
export default {
name: 'VueCronLinux',
props: ['data', 'i18n'],
data () {
return {
second: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: [0]
},
minute: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: ['0']
},
hour: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: ['0']
},
day: {
cronEvery: '',
incrementStart: '1',
incrementIncrement: '1',
rangeStart: '',
rangeEnd: '',
specificSpecific: ['1'],
cronLastSpecificDomDay: 1,
cronDaysBeforeEomMinus: '',
cronDaysNearestWeekday: ''
},
month: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: ['1']
},
week: {
cronEvery: '',
incrementStart: '1',
incrementIncrement: '1',
specificSpecific: ['1'],
cronNthDayDay: 1,
cronNthDayNth: '1',
rangeStart: '',
rangeEnd: ''
},
output: {
second: '',
minute: '',
hour: '',
day: '',
month: '',
Week: '',
year: ''
export default {
name: 'VueCronLinux',
props: ['data', 'i18n'],
data() {
return {
second: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: [0]
},
minute: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: ['0']
},
hour: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: ['0']
},
day: {
cronEvery: '',
incrementStart: '1',
incrementIncrement: '1',
rangeStart: '',
rangeEnd: '',
specificSpecific: ['1'],
cronLastSpecificDomDay: 1,
cronDaysBeforeEomMinus: '',
cronDaysNearestWeekday: ''
},
month: {
cronEvery: '',
incrementStart: '3',
incrementIncrement: '5',
rangeStart: '',
rangeEnd: '',
specificSpecific: ['1']
},
week: {
cronEvery: '',
incrementStart: '1',
incrementIncrement: '1',
specificSpecific: ['1'],
cronNthDayDay: 1,
cronNthDayNth: '1',
rangeStart: '',
rangeEnd: ''
},
output: {
second: '',
minute: '',
hour: '',
day: '',
month: '',
Week: '',
year: ''
}
}
}
},
watch: {
data () {
this.updateCronFromData()
},
cron () {
this.$emit('change', this.cron)
}
},
computed: {
text () {
return Language[this.i18n || 'cn']
},
minutesText () {
let minutes = ''
let cronEvery = this.minute.cronEvery
switch (cronEvery.toString()) {
computed: {
text() {
return Language[this.i18n || 'cn']
},
minutesText() {
let minutes = ''
const cronEvery = this.minute.cronEvery
switch (cronEvery.toString()) {
case '1':
minutes = '*'
break
@@ -304,13 +296,13 @@ export default {
case '4':
minutes = this.minute.rangeStart + '-' + this.minute.rangeEnd
break
}
return minutes
},
hoursText () {
let hours = ''
let cronEvery = this.hour.cronEvery
switch (cronEvery.toString()) {
}
return minutes
},
hoursText() {
let hours = ''
const cronEvery = this.hour.cronEvery
switch (cronEvery.toString()) {
case '1':
hours = '*'
break
@@ -326,13 +318,13 @@ export default {
case '4':
hours = this.hour.rangeStart + '-' + this.hour.rangeEnd
break
}
return hours
},
daysText () {
let days = ''
let cronEvery = this.day.cronEvery
switch (cronEvery.toString()) {
}
return hours
},
daysText() {
let days = ''
const cronEvery = this.day.cronEvery
switch (cronEvery.toString()) {
case '1':
days = '*'
break
@@ -348,13 +340,13 @@ export default {
case '4':
days = this.day.rangeStart + '-' + this.day.rangeEnd
break
}
return days
},
monthsText () {
let months = ''
let cronEvery = this.month.cronEvery
switch (cronEvery.toString()) {
}
return days
},
monthsText() {
let months = ''
const cronEvery = this.month.cronEvery
switch (cronEvery.toString()) {
case '1':
months = '*'
break
@@ -370,13 +362,13 @@ export default {
case '4':
months = this.month.rangeStart + '-' + this.month.rangeEnd
break
}
return months
},
weeksText () {
let weeks = ''
let cronEvery = this.week.cronEvery
switch (cronEvery.toString()) {
}
return months
},
weeksText() {
let weeks = ''
const cronEvery = this.week.cronEvery
switch (cronEvery.toString()) {
case '1':
weeks = '*'
break
@@ -389,77 +381,85 @@ export default {
case '4':
weeks = this.week.rangeStart + '-' + this.week.rangeEnd
break
}
return weeks
},
cron () {
return [this.minutesText, this.hoursText, this.daysText, this.monthsText, this.weeksText]
.filter(v => !!v)
.join(' ')
}
},
methods: {
change () {
this.$emit('change', this.cron)
this.close()
},
close () {
this.$emit('close')
},
submit () {
if (!this.validate()) {
this.$message.error(this.$t('Cron expression is invalid'))
return false
}
this.$emit('submit', this.cron)
return true
},
validate () {
if (!this.minutesText) return false
if (!this.hoursText) return false
if (!this.daysText) return false
if (!this.monthsText) return false
if (!this.weeksText) return false
return true
},
updateCronItem (key, value) {
if (value === undefined) {
this[key].cronEvery = '0'
return
}
if (value.match(/^\*$/)) {
this[key].cronEvery = '1'
} else if (value.match(/\//)) {
this[key].cronEvery = '2'
this[key].incrementStart = value.split('/')[0]
this[key].incrementIncrement = value.split('/')[1]
} else if (value.match(/,|^\d+$/)) {
this[key].cronEvery = '3'
this[key].specificSpecific = value.split(',')
} else if (value.match(/-/)) {
this[key].cronEvery = '4'
this[key].rangeStart = value.split('-')[0]
this[key].rangeEnd = value.split('-')[1]
} else {
this[key].cronEvery = '0'
}
return weeks
},
cron() {
return [this.minutesText, this.hoursText, this.daysText, this.monthsText, this.weeksText]
.filter(v => !!v)
.join(' ')
}
},
updateCronFromData () {
const arr = this.data.split(' ')
const minute = arr[0]
const hour = arr[1]
const day = arr[2]
const month = arr[3]
const week = arr[4]
watch: {
data() {
this.updateCronFromData()
},
cron() {
this.$emit('change', this.cron)
}
},
mounted() {
this.updateCronFromData()
},
methods: {
change() {
this.$emit('change', this.cron)
this.close()
},
close() {
this.$emit('close')
},
submit() {
if (!this.validate()) {
this.$message.error(this.$t('Cron expression is invalid'))
return false
}
this.$emit('submit', this.cron)
return true
},
validate() {
if (!this.minutesText) return false
if (!this.hoursText) return false
if (!this.daysText) return false
if (!this.monthsText) return false
if (!this.weeksText) return false
return true
},
updateCronItem(key, value) {
if (value === undefined) {
this[key].cronEvery = '0'
return
}
if (value.match(/^\*$/)) {
this[key].cronEvery = '1'
} else if (value.match(/\//)) {
this[key].cronEvery = '2'
this[key].incrementStart = value.split('/')[0]
this[key].incrementIncrement = value.split('/')[1]
} else if (value.match(/,|^\d+$/)) {
this[key].cronEvery = '3'
this[key].specificSpecific = value.split(',')
} else if (value.match(/-/)) {
this[key].cronEvery = '4'
this[key].rangeStart = value.split('-')[0]
this[key].rangeEnd = value.split('-')[1]
} else {
this[key].cronEvery = '0'
}
},
updateCronFromData() {
const arr = this.data.split(' ')
const minute = arr[0]
const hour = arr[1]
const day = arr[2]
const month = arr[3]
const week = arr[4]
this.updateCronItem('minute', minute)
this.updateCronItem('hour', hour)
this.updateCronItem('day', day)
this.updateCronItem('month', month)
this.updateCronItem('week', week)
this.updateCronItem('minute', minute)
this.updateCronItem('hour', hour)
this.updateCronItem('day', day)
this.updateCronItem('month', month)
this.updateCronItem('week', week)
}
}
},
mounted () {
this.updateCronFromData()
}
}</script>
}</script>

View File

@@ -31,7 +31,9 @@ export default {
lastWeekday: 'On the last weekday of the month',
lastWeek: ['On the last', ' of the month'],
beforeEndMonth: ['day(s) before the end of the month'],
nearestWeekday: ['Nearest weekday (Monday to Friday) to the', 'of the month'],
nearestWeekday: [
'Nearest weekday (Monday to Friday) to the',
'of the month'],
someWeekday: ['On the', 'of the month'],
cycle: ['From', 'to']
},
@@ -39,7 +41,14 @@ export default {
name: 'Week',
every: 'Every day',
specific: 'Specific weekday (choose on or many)',
list: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
list: [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday'],
cycle: ['From', 'to']
},
// Week:['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],

View File

@@ -1,18 +1,21 @@
<template>
<el-tree
:data="docData"
ref="documentation-tree"
:data="docData"
node-key="fullUrl"
>
<span class="custom-tree-node" :class="[data.active ? 'active' : '', `level-${data.level}`]"
slot-scope="{ node, data }">
<span
slot-scope="{ node, data }"
class="custom-tree-node"
:class="[data.active ? 'active' : '', `level-${data.level}`]"
>
<template v-if="data.level === 1 && data.children && data.children.length">
<span>{{node.label}}</span>
<span>{{ node.label }}</span>
</template>
<template v-else>
<span>
<a :href="data.fullUrl" target="_blank" style="display: block" @click="onClickDocumentationLink">
{{node.label}}
{{ node.label }}
</a>
</span>
</template>
@@ -21,86 +24,86 @@
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'Documentation',
data () {
return {
data: []
}
},
computed: {
...mapState('doc', [
'docData'
]),
pathLv1 () {
if (this.$route.path === '/') return '/'
const m = this.$route.path.match(/(^\/\w+)/)
return m[1]
},
currentDoc () {
// find current doc
let currentDoc
for (let i = 0; i < this.$utils.doc.docs.length; i++) {
const doc = this.$utils.doc.docs[i]
if (this.pathLv1 === doc.path) {
currentDoc = doc
break
}
export default {
name: 'Documentation',
data() {
return {
data: []
}
return currentDoc
}
},
watch: {
pathLv1 () {
},
computed: {
...mapState('doc', [
'docData'
]),
pathLv1() {
if (this.$route.path === '/') return '/'
const m = this.$route.path.match(/(^\/\w+)/)
return m[1]
},
currentDoc() {
// find current doc
let currentDoc
for (let i = 0; i < this.$utils.doc.docs.length; i++) {
const doc = this.$utils.doc.docs[i]
if (this.pathLv1 === doc.path) {
currentDoc = doc
break
}
}
return currentDoc
}
},
watch: {
pathLv1() {
this.update()
}
},
async created() {
},
mounted() {
this.update()
}
},
methods: {
isActiveNode (d) {
// check match
if (!this.currentDoc) return false
return !!d.url.match(this.currentDoc.pattern)
},
update () {
// expand related documentation list
setTimeout(() => {
this.docData.forEach(d => {
// parent node
const isActive = this.isActiveNode(d)
const node = this.$refs['documentation-tree'].getNode(d)
node.expanded = isActive
this.$set(d, 'active', isActive)
methods: {
isActiveNode(d) {
// check match
if (!this.currentDoc) return false
return !!d.url.match(this.currentDoc.pattern)
},
update() {
// expand related documentation list
setTimeout(() => {
this.docData.forEach(d => {
// parent node
const isActive = this.isActiveNode(d)
const node = this.$refs['documentation-tree'].getNode(d)
node.expanded = isActive
this.$set(d, 'active', isActive)
// child nodes
d.children.forEach(c => {
const node = this.$refs['documentation-tree'].getNode(c)
const isActive = this.isActiveNode(c)
if (!node.parent.expanded && isActive) {
node.parent.expanded = true
}
this.$set(c, 'active', isActive)
// child nodes
d.children.forEach(c => {
const node = this.$refs['documentation-tree'].getNode(c)
const isActive = this.isActiveNode(c)
if (!node.parent.expanded && isActive) {
node.parent.expanded = true
}
this.$set(c, 'active', isActive)
})
})
})
}, 100)
},
async getDocumentationData () {
// fetch api data
await this.$store.dispatch('doc/getDocData')
},
onClickDocumentationLink () {
this.$st.sendEv('全局', '点击右侧文档链接')
}, 100)
},
async getDocumentationData() {
// fetch api data
await this.$store.dispatch('doc/getDocData')
},
onClickDocumentationLink() {
this.$st.sendEv('全局', '点击右侧文档链接')
}
}
},
async created () {
},
mounted () {
this.update()
}
}
</script>
<style scoped>
.el-tree >>> .custom-tree-node.active {

View File

@@ -2,25 +2,25 @@
<div class="environment-list">
<el-row>
<div class="button-group">
<el-button size="small" type="primary" @click="addEnv" icon="el-icon-plus">{{$t('Add Environment Variables')}}</el-button>
<el-button size="small" type="success" @click="save">{{$t('Save')}}</el-button>
<el-button size="small" type="primary" icon="el-icon-plus" @click="addEnv">{{ $t('Add Environment Variables') }}</el-button>
<el-button size="small" type="success" @click="save">{{ $t('Save') }}</el-button>
</div>
</el-row>
<el-row>
<el-table :data="spiderForm.envs">
<el-table-column :label="$t('Variable')">
<template slot-scope="scope">
<el-input v-model="scope.row.name" :placeholder="$t('Variable')"></el-input>
<el-input v-model="scope.row.name" :placeholder="$t('Variable')" />
</template>
</el-table-column>
<el-table-column :label="$t('Value')">
<template slot-scope="scope">
<el-input v-model="scope.row.value" :placeholder="$t('Value')"></el-input>
<el-input v-model="scope.row.value" :placeholder="$t('Value')" />
</template>
</el-table-column>
<el-table-column :label="$t('Action')">
<template slot-scope="scope">
<el-button size="mini" icon="el-icon-delete" type="danger" @click="deleteEnv(scope.$index)"></el-button>
<el-button size="mini" icon="el-icon-delete" type="danger" @click="deleteEnv(scope.$index)" />
</template>
</el-table-column>
</el-table>
@@ -29,44 +29,44 @@
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'EnvironmentList',
computed: {
...mapState('spider', [
'spiderForm'
])
},
methods: {
addEnv () {
if (!this.spiderForm.envs) {
this.$set(this.spiderForm, 'envs', [])
export default {
name: 'EnvironmentList',
computed: {
...mapState('spider', [
'spiderForm'
])
},
methods: {
addEnv() {
if (!this.spiderForm.envs) {
this.$set(this.spiderForm, 'envs', [])
}
this.spiderForm.envs.push({
name: '',
value: ''
})
this.$st.sendEv('爬虫详情', '环境', '添加')
},
deleteEnv(index) {
this.spiderForm.envs.splice(index, 1)
this.$st.sendEv('爬虫详情', '环境', '删除')
},
save() {
this.$store.dispatch('spider/editSpider')
.then(() => {
this.$message.success(this.$t('Spider info has been saved successfully'))
})
.catch(error => {
this.$message.error(error)
})
this.$st.sendEv('爬虫详情', '环境', '保存')
}
this.spiderForm.envs.push({
name: '',
value: ''
})
this.$st.sendEv('爬虫详情', '环境', '添加')
},
deleteEnv (index) {
this.spiderForm.envs.splice(index, 1)
this.$st.sendEv('爬虫详情', '环境', '删除')
},
save () {
this.$store.dispatch('spider/editSpider')
.then(() => {
this.$message.success(this.$t('Spider info has been saved successfully'))
})
.catch(error => {
this.$message.error(error)
})
this.$st.sendEv('爬虫详情', '环境', '保存')
}
}
}
</script>
<style scoped>

View File

@@ -1,96 +1,96 @@
<template>
<div class="file-detail">
<codemirror
v-model="fileContent"
class="file-content"
:options="options"
v-model="fileContent"
/>
</div>
</template>
<script>
import {
mapState,
mapGetters
} from 'vuex'
import { codemirror } from 'vue-codemirror-lite'
import {
mapState,
mapGetters
} from 'vuex'
import { codemirror } from 'vue-codemirror-lite'
import 'codemirror/lib/codemirror.js'
import 'codemirror/lib/codemirror.js'
// language
import 'codemirror/mode/python/python.js'
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/mode/go/go.js'
import 'codemirror/mode/shell/shell.js'
import 'codemirror/mode/markdown/markdown.js'
import 'codemirror/mode/php/php.js'
import 'codemirror/mode/yaml/yaml.js'
// language
import 'codemirror/mode/python/python.js'
import 'codemirror/mode/javascript/javascript.js'
import 'codemirror/mode/go/go.js'
import 'codemirror/mode/shell/shell.js'
import 'codemirror/mode/markdown/markdown.js'
import 'codemirror/mode/php/php.js'
import 'codemirror/mode/yaml/yaml.js'
export default {
name: 'FileDetail',
components: { codemirror },
data () {
return {
internalFileContent: ''
}
},
computed: {
...mapState('spider', [
'spiderForm'
]),
...mapGetters('user', [
'userInfo'
]),
fileContent: {
get () {
return this.$store.state.file.fileContent
},
set (value) {
return this.$store.commit('file/SET_FILE_CONTENT', value)
}
},
options () {
export default {
name: 'FileDetail',
components: { codemirror },
data() {
return {
mode: this.language,
theme: 'darcula',
styleActiveLine: true,
smartIndent: true,
indentUnit: 4,
lineNumbers: true,
line: true,
matchBrackets: true,
readOnly: this.isDisabled ? 'nocursor' : false
internalFileContent: ''
}
},
language () {
const fileName = this.$store.state.file.currentPath
if (!fileName) return ''
if (fileName.match(/\.js$/)) {
return 'text/javascript'
} else if (fileName.match(/\.py$/)) {
return 'text/x-python'
} else if (fileName.match(/\.go$/)) {
return 'text/x-go'
} else if (fileName.match(/\.sh$/)) {
return 'text/x-shell'
} else if (fileName.match(/\.php$/)) {
return 'text/x-php'
} else if (fileName.match(/\.md$/)) {
return 'text/x-markdown'
} else if (fileName.match('Spiderfile')) {
return 'text/x-yaml'
} else {
return 'text'
computed: {
...mapState('spider', [
'spiderForm'
]),
...mapGetters('user', [
'userInfo'
]),
fileContent: {
get() {
return this.$store.state.file.fileContent
},
set(value) {
return this.$store.commit('file/SET_FILE_CONTENT', value)
}
},
options() {
return {
mode: this.language,
theme: 'darcula',
styleActiveLine: true,
smartIndent: true,
indentUnit: 4,
lineNumbers: true,
line: true,
matchBrackets: true,
readOnly: this.isDisabled ? 'nocursor' : false
}
},
language() {
const fileName = this.$store.state.file.currentPath
if (!fileName) return ''
if (fileName.match(/\.js$/)) {
return 'text/javascript'
} else if (fileName.match(/\.py$/)) {
return 'text/x-python'
} else if (fileName.match(/\.go$/)) {
return 'text/x-go'
} else if (fileName.match(/\.sh$/)) {
return 'text/x-shell'
} else if (fileName.match(/\.php$/)) {
return 'text/x-php'
} else if (fileName.match(/\.md$/)) {
return 'text/x-markdown'
} else if (fileName.match('Spiderfile')) {
return 'text/x-yaml'
} else {
return 'text'
}
},
isDisabled() {
return this.spiderForm.is_public && this.spiderForm.username !== this.userInfo.username && this.userInfo.role !== 'admin'
}
},
isDisabled () {
return this.spiderForm.is_public && this.spiderForm.username !== this.userInfo.username && this.userInfo.role !== 'admin'
created() {
this.internalFileContent = this.fileContent
}
},
created () {
this.internalFileContent = this.fileContent
}
}
</script>
<style scoped>

View File

@@ -3,30 +3,32 @@
<el-dialog
:title="$t('New Directory')"
:visible.sync="dirDialogVisible"
width="30%">
width="30%"
>
<el-form>
<el-form-item :label="$t('Enter new directory name')">
<el-input v-model="name" :placeholder="$t('New directory name')"></el-input>
<el-input v-model="name" :placeholder="$t('New directory name')" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dirDialogVisible = false">{{$t('Cancel')}}</el-button>
<el-button type="primary" @click="onAddDir">{{$t('Confirm')}}</el-button>
<el-button @click="dirDialogVisible = false">{{ $t('Cancel') }}</el-button>
<el-button type="primary" @click="onAddDir">{{ $t('Confirm') }}</el-button>
</span>
</el-dialog>
<el-dialog
:title="$t('New File')"
:visible.sync="fileDialogVisible"
width="30%">
width="30%"
>
<el-form>
<el-form-item :label="$t('Enter new file name')">
<el-input v-model="name" :placeholder="$t('New file name')"></el-input>
<el-input v-model="name" :placeholder="$t('New file name')" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button size="small" @click="fileDialogVisible = false">{{$t('Cancel')}}</el-button>
<el-button size="small" type="primary" @click="onAddFile">{{$t('Confirm')}}</el-button>
<el-button size="small" @click="fileDialogVisible = false">{{ $t('Cancel') }}</el-button>
<el-button size="small" type="primary" @click="onAddFile">{{ $t('Confirm') }}</el-button>
</span>
</el-dialog>
@@ -34,9 +36,9 @@
class="file-tree-wrapper"
>
<el-tree
ref="tree"
class="tree"
:data="computedFileTree"
ref="tree"
node-key="path"
:highlight-current="true"
:default-expanded-keys="expandedPaths"
@@ -45,43 +47,64 @@
@node-expand="onDirClick"
@node-collapse="onDirClick"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<el-popover v-model="isShowCreatePopoverDict[data.path]" trigger="manual" placement="right"
popper-class="create-item-popover" :visible-arrow="false" @hide="onHideCreate(data)">
<span slot-scope="{ node, data }" class="custom-tree-node">
<el-popover
v-model="isShowCreatePopoverDict[data.path]"
trigger="manual"
placement="right"
popper-class="create-item-popover"
:visible-arrow="false"
@hide="onHideCreate(data)"
>
<ul class="action-item-list">
<li class="action-item" @click="fileDialogVisible = true">
<font-awesome-icon icon="file-alt" color="rgba(3,47,98,.5)"/>
<span class="action-item-text">{{$t('Create File')}}</span>
<font-awesome-icon icon="file-alt" color="rgba(3,47,98,.5)" />
<span class="action-item-text">{{ $t('Create File') }}</span>
</li>
<li class="action-item" @click="dirDialogVisible = true">
<font-awesome-icon :icon="['fa', 'folder']" color="rgba(3,47,98,.5)"/>
<span class="action-item-text">{{$t('Create Directory')}}</span>
<font-awesome-icon :icon="['fa', 'folder']" color="rgba(3,47,98,.5)" />
<span class="action-item-text">{{ $t('Create Directory') }}</span>
</li>
</ul>
<ul class="action-item-list">
<li class="action-item" @click="onClickRemoveNav(data)">
<font-awesome-icon :icon="['fa', 'trash']" color="rgba(3,47,98,.5)"/>
<span class="action-item-text">{{$t('Remove')}}</span>
<font-awesome-icon :icon="['fa', 'trash']" color="rgba(3,47,98,.5)" />
<span class="action-item-text">{{ $t('Remove') }}</span>
</li>
</ul>
<template slot="reference">
<div>
<span class="item-icon">
<font-awesome-icon v-if="data.is_dir" :icon="['fa', 'folder']" color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else-if="data.path.match(/\.py$/)" :icon="['fab','python']"
color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else-if="data.path.match(/\.js$/)" :icon="['fab','node-js']"
color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else-if="data.path.match(/\.(java|jar|class)$/)" :icon="['fab','java']"
color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else-if="data.path.match(/\.go$/)" :icon="['fab','go']"
color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else-if="data.path.match(/\.zip$/)" :icon="['fa','file-archive']"
color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-else icon="file-alt" color="rgba(3,47,98,.5)"/>
<font-awesome-icon v-if="data.is_dir" :icon="['fa', 'folder']" color="rgba(3,47,98,.5)" />
<font-awesome-icon
v-else-if="data.path.match(/\.py$/)"
:icon="['fab','python']"
color="rgba(3,47,98,.5)"
/>
<font-awesome-icon
v-else-if="data.path.match(/\.js$/)"
:icon="['fab','node-js']"
color="rgba(3,47,98,.5)"
/>
<font-awesome-icon
v-else-if="data.path.match(/\.(java|jar|class)$/)"
:icon="['fab','java']"
color="rgba(3,47,98,.5)"
/>
<font-awesome-icon
v-else-if="data.path.match(/\.go$/)"
:icon="['fab','go']"
color="rgba(3,47,98,.5)"
/>
<font-awesome-icon
v-else-if="data.path.match(/\.zip$/)"
:icon="['fa','file-archive']"
color="rgba(3,47,98,.5)"
/>
<font-awesome-icon v-else icon="file-alt" color="rgba(3,47,98,.5)" />
</span>
<span class="item-name" :class="isActiveFile(data) ? 'active' : ''">
{{data.name}}
{{ data.name }}
</span>
</div>
</template>
@@ -91,28 +114,32 @@
<div
class="add-btn-wrapper"
>
<el-popover trigger="click" placement="right"
popper-class="create-item-popover" :visible-arrow="false">
<el-popover
trigger="click"
placement="right"
popper-class="create-item-popover"
:visible-arrow="false"
>
<ul class="action-item-list">
<li class="action-item" @click="fileDialogVisible = true">
<font-awesome-icon icon="file-alt" color="rgba(3,47,98,.5)"/>
<span class="action-item-text">{{$t('Create File')}}</span>
<font-awesome-icon icon="file-alt" color="rgba(3,47,98,.5)" />
<span class="action-item-text">{{ $t('Create File') }}</span>
</li>
<li class="action-item" @click="dirDialogVisible = true">
<font-awesome-icon :icon="['fa', 'folder']" color="rgba(3,47,98,.5)"/>
<span class="action-item-text">{{$t('Create Directory')}}</span>
<font-awesome-icon :icon="['fa', 'folder']" color="rgba(3,47,98,.5)" />
<span class="action-item-text">{{ $t('Create Directory') }}</span>
</li>
</ul>
<el-button
slot="reference"
class="add-btn"
size="small"
type="primary"
icon="el-icon-plus"
slot="reference"
:disabled="isDisabled"
@click="onEmptyClick"
>
{{$t('Add')}}
{{ $t('Add') }}
</el-button>
</el-popover>
</div>
@@ -120,7 +147,7 @@
<div class="main-content">
<div v-if="!showFile" class="file-list">
{{$t('Please select a file or click the add button on the left.')}}
{{ $t('Please select a file or click the add button on the left.') }}
</div>
<template v-else>
<div class="top-part">
@@ -128,336 +155,336 @@
<div class="action-container">
<el-popover v-model="isShowDelete" trigger="click">
<el-button size="small" type="default" @click="() => this.isShowDelete = false">
{{$t('Cancel')}}
{{ $t('Cancel') }}
</el-button>
<el-button size="small" type="danger" @click="onFileDelete">
{{$t('Confirm')}}
{{ $t('Confirm') }}
</el-button>
<template slot="reference">
<el-button type="danger" size="small" style="margin-right: 10px;" :disabled="isDisabled">
<font-awesome-icon :icon="['fa', 'trash']"/>
{{$t('Remove')}}
<font-awesome-icon :icon="['fa', 'trash']" />
{{ $t('Remove') }}
</el-button>
</template>
</el-popover>
<el-popover v-model="isShowRename" trigger="click">
<el-input v-model="name" :placeholder="$t('Name')" style="margin-bottom: 10px"/>
<el-input v-model="name" :placeholder="$t('Name')" style="margin-bottom: 10px" />
<div style="text-align: right">
<el-button size="small" type="warning" @click="onRenameFile">
{{$t('Confirm')}}
{{ $t('Confirm') }}
</el-button>
</div>
<template slot="reference">
<div>
<el-button type="warning" size="small" style="margin-right: 10px;" :disabled="isDisabled" @click="onOpenRename">
<font-awesome-icon :icon="['fa', 'redo']"/>
{{$t('Rename')}}
<font-awesome-icon :icon="['fa', 'redo']" />
{{ $t('Rename') }}
</el-button>
</div>
</template>
</el-popover>
<el-button type="success" size="small" style="margin-right: 10px;" :disabled="isDisabled" @click="onFileSave">
<font-awesome-icon :icon="['fa', 'save']"/>
{{$t('Save')}}
<font-awesome-icon :icon="['fa', 'save']" />
{{ $t('Save') }}
</el-button>
</div>
<!--./back-->
<!--file path-->
<div class="file-path-container">
<div class="file-path">{{currentPath}}</div>
<div class="file-path">{{ currentPath }}</div>
</div>
<!--./file path-->
</div>
<file-detail/>
<file-detail />
</template>
</div>
</div>
</template>
<script>
import {
mapState,
mapGetters
} from 'vuex'
import FileDetail from './FileDetail'
import {
mapState,
mapGetters
} from 'vuex'
import FileDetail from './FileDetail'
export default {
name: 'FileList',
components: { FileDetail },
data () {
return {
isEdit: false,
showFile: false,
name: '',
isShowAdd: false,
isShowDelete: false,
isShowRename: false,
isShowCreatePopoverDict: {},
currentFilePath: '.',
ignoreFileRegexList: [
'__pycache__',
'md5.txt',
'.pyc',
'.git'
],
activeFileNode: {},
dirDialogVisible: false,
fileDialogVisible: false,
nodeExpandedDict: {},
isShowDeleteNav: false
}
},
computed: {
...mapState('spider', [
'fileTree',
'spiderForm'
]),
...mapState('file', [
'fileList'
]),
...mapGetters('user', [
'userInfo'
]),
currentPath: {
set (value) {
this.$store.commit('file/SET_CURRENT_PATH', value)
},
get () {
return this.$store.state.file.currentPath
export default {
name: 'FileList',
components: { FileDetail },
data() {
return {
isEdit: false,
showFile: false,
name: '',
isShowAdd: false,
isShowDelete: false,
isShowRename: false,
isShowCreatePopoverDict: {},
currentFilePath: '.',
ignoreFileRegexList: [
'__pycache__',
'md5.txt',
'.pyc',
'.git'
],
activeFileNode: {},
dirDialogVisible: false,
fileDialogVisible: false,
nodeExpandedDict: {},
isShowDeleteNav: false
}
},
computedFileTree () {
if (!this.fileTree || !this.fileTree.children) return []
let nodes = this.sortFiles(this.fileTree.children)
nodes = this.filterFiles(nodes)
return nodes
computed: {
...mapState('spider', [
'fileTree',
'spiderForm'
]),
...mapState('file', [
'fileList'
]),
...mapGetters('user', [
'userInfo'
]),
currentPath: {
set(value) {
this.$store.commit('file/SET_CURRENT_PATH', value)
},
get() {
return this.$store.state.file.currentPath
}
},
computedFileTree() {
if (!this.fileTree || !this.fileTree.children) return []
let nodes = this.sortFiles(this.fileTree.children)
nodes = this.filterFiles(nodes)
return nodes
},
expandedPaths() {
return Object.keys(this.nodeExpandedDict)
.map(path => {
return {
path,
expanded: this.nodeExpandedDict[path]
}
})
.filter(d => d.expanded)
.map(d => d.path)
},
isDisabled() {
return this.spiderForm.is_public && this.spiderForm.username !== this.userInfo.username && this.userInfo.role !== 'admin'
}
},
expandedPaths () {
return Object.keys(this.nodeExpandedDict)
.map(path => {
return {
path,
expanded: this.nodeExpandedDict[path]
}
})
.filter(d => d.expanded)
.map(d => d.path)
async created() {
await this.getFileTree()
},
isDisabled () {
return this.spiderForm.is_public && this.spiderForm.username !== this.userInfo.username && this.userInfo.role !== 'admin'
}
},
methods: {
onEdit () {
this.isEdit = true
mounted() {
this.listener = document.querySelector('body').addEventListener('click', ev => {
this.isShowCreatePopoverDict = {}
})
},
onItemClick (item) {
if (item.is_dir) {
// 目录
this.$store.dispatch('file/getFileList', { path: item.path })
} else {
// 文件
destroyed() {
document.querySelector('body').removeEventListener('click', this.listener)
},
methods: {
onEdit() {
this.isEdit = true
},
onItemClick(item) {
if (item.is_dir) {
// 目录
this.$store.dispatch('file/getFileList', { path: item.path })
} else {
// 文件
this.showFile = true
this.$store.commit('file/SET_FILE_CONTENT', '')
this.$store.commit('file/SET_CURRENT_PATH', item.path)
this.$store.dispatch('file/getFileContent', { path: item.path })
}
this.$st.sendEv('爬虫详情', '文件', '点击')
},
async onFileSave() {
await this.$store.dispatch('file/saveFileContent', { path: this.currentPath })
this.$message.success(this.$t('Saved file successfully'))
this.$st.sendEv('爬虫详情', '文件', '保存')
},
async onAddFile() {
if (!this.name) {
this.$message.error(this.$t('Name cannot be empty'))
return
}
const arr = this.activeFileNode.path.split('/')
if (this.activeFileNode.is_dir) {
arr.push(this.name)
} else {
arr[arr.length - 1] = this.name
}
const path = arr.join('/')
await this.$store.dispatch('file/addFile', { path })
await this.$store.dispatch('spider/getFileTree')
this.isShowAdd = false
this.fileDialogVisible = false
this.showFile = true
this.$store.commit('file/SET_FILE_CONTENT', '')
this.$store.commit('file/SET_CURRENT_PATH', item.path)
this.$store.dispatch('file/getFileContent', { path: item.path })
}
this.$st.sendEv('爬虫详情', '文件', '点击')
},
async onFileSave () {
await this.$store.dispatch('file/saveFileContent', { path: this.currentPath })
this.$message.success(this.$t('Saved file successfully'))
this.$st.sendEv('爬虫详情', '文件', '保存')
},
async onAddFile () {
if (!this.name) {
this.$message.error(this.$t('Name cannot be empty'))
return
}
const arr = this.activeFileNode.path.split('/')
if (this.activeFileNode.is_dir) {
arr.push(this.name)
} else {
arr[arr.length - 1] = this.name
}
const path = arr.join('/')
await this.$store.dispatch('file/addFile', { path })
await this.$store.dispatch('spider/getFileTree')
this.isShowAdd = false
this.fileDialogVisible = false
this.showFile = true
this.$store.commit('file/SET_FILE_CONTENT', '')
this.$store.commit('file/SET_CURRENT_PATH', path)
await this.$store.dispatch('file/getFileContent', { path })
this.$st.sendEv('爬虫详情', '文件', '添加')
},
async onAddDir () {
if (!this.name) {
this.$message.error(this.$t('Name cannot be empty'))
return
}
const arr = this.activeFileNode.path.split('/')
if (this.activeFileNode.is_dir) {
arr.push(this.name)
} else {
arr[arr.length - 1] = this.name
}
const path = arr.join('/')
await this.$store.dispatch('file/addDir', { path })
await this.$store.dispatch('spider/getFileTree')
this.isShowAdd = false
this.dirDialogVisible = false
this.$st.sendEv('爬虫详情', '文件', '添加')
},
async onFileDelete () {
await this.$store.dispatch('file/deleteFile', { path: this.currentFilePath })
await this.$store.dispatch('spider/getFileTree')
this.$message.success(this.$t('Deleted successfully'))
this.isShowDelete = false
this.showFile = false
this.$st.sendEv('爬虫详情', '文件', '删除')
},
onOpenRename () {
const arr = this.currentFilePath.split('/')
this.name = arr[arr.length - 1]
},
async onRenameFile () {
await this.$store.dispatch('file/renameFile', { path: this.currentFilePath, newPath: this.name })
await this.$store.dispatch('spider/getFileTree')
const arr = this.currentFilePath.split('/')
arr[arr.length - 1] = this.name
this.currentFilePath = arr.join('/')
this.$store.commit('file/SET_CURRENT_PATH', this.currentFilePath)
this.$message.success(this.$t('Renamed successfully'))
this.isShowRename = false
this.$st.sendEv('爬虫详情', '文件', '重命名')
},
async getFileTree () {
const arr = this.$route.path.split('/')
const id = arr[arr.length - 1]
await this.$store.dispatch('spider/getFileTree', { id })
},
async onFileClick (data) {
if (data.is_dir) {
return
}
this.currentFilePath = data.path
this.onItemClick(data)
},
onDirClick (data, node) {
const vm = this
setTimeout(() => {
vm.$set(vm.nodeExpandedDict, data.path, node.expanded)
}, 0)
},
sortFiles (nodes) {
nodes.forEach(node => {
if (node.is_dir) {
if (!node.children) node.children = []
node.children = this.sortFiles(node.children)
this.$store.commit('file/SET_CURRENT_PATH', path)
await this.$store.dispatch('file/getFileContent', { path })
this.$st.sendEv('爬虫详情', '文件', '添加')
},
async onAddDir() {
if (!this.name) {
this.$message.error(this.$t('Name cannot be empty'))
return
}
})
return nodes.sort((a, b) => {
if ((a.is_dir && b.is_dir) || (!a.is_dir && !b.is_dir)) {
return a.name > b.name ? 1 : -1
const arr = this.activeFileNode.path.split('/')
if (this.activeFileNode.is_dir) {
arr.push(this.name)
} else {
return a.is_dir ? -1 : 1
arr[arr.length - 1] = this.name
}
})
},
filterFiles (nodes) {
return nodes.filter(node => {
if (node.is_dir) {
node.children = this.filterFiles(node.children)
const path = arr.join('/')
await this.$store.dispatch('file/addDir', { path })
await this.$store.dispatch('spider/getFileTree')
this.isShowAdd = false
this.dirDialogVisible = false
this.$st.sendEv('爬虫详情', '文件', '添加')
},
async onFileDelete() {
await this.$store.dispatch('file/deleteFile', { path: this.currentFilePath })
await this.$store.dispatch('spider/getFileTree')
this.$message.success(this.$t('Deleted successfully'))
this.isShowDelete = false
this.showFile = false
this.$st.sendEv('爬虫详情', '文件', '删除')
},
onOpenRename() {
const arr = this.currentFilePath.split('/')
this.name = arr[arr.length - 1]
},
async onRenameFile() {
await this.$store.dispatch('file/renameFile', { path: this.currentFilePath, newPath: this.name })
await this.$store.dispatch('spider/getFileTree')
const arr = this.currentFilePath.split('/')
arr[arr.length - 1] = this.name
this.currentFilePath = arr.join('/')
this.$store.commit('file/SET_CURRENT_PATH', this.currentFilePath)
this.$message.success(this.$t('Renamed successfully'))
this.isShowRename = false
this.$st.sendEv('爬虫详情', '文件', '重命名')
},
async getFileTree() {
const arr = this.$route.path.split('/')
const id = arr[arr.length - 1]
await this.$store.dispatch('spider/getFileTree', { id })
},
async onFileClick(data) {
if (data.is_dir) {
return
}
for (let i = 0; i < this.ignoreFileRegexList.length; i++) {
const regex = this.ignoreFileRegexList[i]
if (node.name.match(regex)) {
return false
this.currentFilePath = data.path
this.onItemClick(data)
},
onDirClick(data, node) {
const vm = this
setTimeout(() => {
vm.$set(vm.nodeExpandedDict, data.path, node.expanded)
}, 0)
},
sortFiles(nodes) {
nodes.forEach(node => {
if (node.is_dir) {
if (!node.children) node.children = []
node.children = this.sortFiles(node.children)
}
}
return true
})
},
isActiveFile (node) {
return node.path === this.currentFilePath
},
onFileRightClick (ev, data) {
this.isShowCreatePopoverDict = {}
this.$set(this.isShowCreatePopoverDict, data.path, true)
this.activeFileNode = data
this.$st.sendEv('爬虫详情', '文件', '右键点击导航栏')
},
onEmptyClick () {
const data = { path: '' }
this.isShowCreatePopoverDict = {}
this.$set(this.isShowCreatePopoverDict, data.path, true)
this.activeFileNode = data
this.$st.sendEv('爬虫详情', '文件', '空白点击添加')
},
onHideCreate (data) {
this.$set(this.isShowCreatePopoverDict, data.path, false)
this.name = ''
},
onClickRemoveNav (data) {
this.$confirm(this.$t('Are you sure to delete this file/directory?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
confirmButtonClass: 'danger',
type: 'warning'
}).then(() => {
this.onFileDeleteNav(data.path)
})
},
async onFileDeleteNav (path) {
await this.$store.dispatch('file/deleteFile', { path })
await this.$store.dispatch('spider/getFileTree')
this.$message.success(this.$t('Deleted successfully'))
this.isShowDelete = false
this.showFile = false
this.$st.sendEv('爬虫详情', '文件', '删除')
},
clickSpider (filepath) {
const node = this.$refs['tree'].getNode(filepath)
const data = node.data
this.onFileClick(data)
node.parent.expanded = true
this.$set(this.nodeExpandedDict, node.parent.data.path, true)
node.parent.parent.expanded = true
this.$set(this.nodeExpandedDict, node.parent.parent.data.path, true)
},
clickPipeline () {
const filename = 'pipelines.py'
for (let i = 0; i < this.computedFileTree.length; i++) {
const dataLv1 = this.computedFileTree[i]
const nodeLv1 = this.$refs['tree'].getNode(dataLv1.path)
if (dataLv1.is_dir) {
for (let j = 0; j < dataLv1.children.length; j++) {
const dataLv2 = dataLv1.children[j]
if (dataLv2.path.match(filename)) {
this.onFileClick(dataLv2)
nodeLv1.expanded = true
this.$set(this.nodeExpandedDict, dataLv1.path, true)
return
})
return nodes.sort((a, b) => {
if ((a.is_dir && b.is_dir) || (!a.is_dir && !b.is_dir)) {
return a.name > b.name ? 1 : -1
} else {
return a.is_dir ? -1 : 1
}
})
},
filterFiles(nodes) {
return nodes.filter(node => {
if (node.is_dir) {
node.children = this.filterFiles(node.children)
}
for (let i = 0; i < this.ignoreFileRegexList.length; i++) {
const regex = this.ignoreFileRegexList[i]
if (node.name.match(regex)) {
return false
}
}
return true
})
},
isActiveFile(node) {
return node.path === this.currentFilePath
},
onFileRightClick(ev, data) {
this.isShowCreatePopoverDict = {}
this.$set(this.isShowCreatePopoverDict, data.path, true)
this.activeFileNode = data
this.$st.sendEv('爬虫详情', '文件', '右键点击导航栏')
},
onEmptyClick() {
const data = { path: '' }
this.isShowCreatePopoverDict = {}
this.$set(this.isShowCreatePopoverDict, data.path, true)
this.activeFileNode = data
this.$st.sendEv('爬虫详情', '文件', '空白点击添加')
},
onHideCreate(data) {
this.$set(this.isShowCreatePopoverDict, data.path, false)
this.name = ''
},
onClickRemoveNav(data) {
this.$confirm(this.$t('Are you sure to delete this file/directory?'), this.$t('Notification'), {
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
confirmButtonClass: 'danger',
type: 'warning'
}).then(() => {
this.onFileDeleteNav(data.path)
})
},
async onFileDeleteNav(path) {
await this.$store.dispatch('file/deleteFile', { path })
await this.$store.dispatch('spider/getFileTree')
this.$message.success(this.$t('Deleted successfully'))
this.isShowDelete = false
this.showFile = false
this.$st.sendEv('爬虫详情', '文件', '删除')
},
clickSpider(filepath) {
const node = this.$refs['tree'].getNode(filepath)
const data = node.data
this.onFileClick(data)
node.parent.expanded = true
this.$set(this.nodeExpandedDict, node.parent.data.path, true)
node.parent.parent.expanded = true
this.$set(this.nodeExpandedDict, node.parent.parent.data.path, true)
},
clickPipeline() {
const filename = 'pipelines.py'
for (let i = 0; i < this.computedFileTree.length; i++) {
const dataLv1 = this.computedFileTree[i]
const nodeLv1 = this.$refs['tree'].getNode(dataLv1.path)
if (dataLv1.is_dir) {
for (let j = 0; j < dataLv1.children.length; j++) {
const dataLv2 = dataLv1.children[j]
if (dataLv2.path.match(filename)) {
this.onFileClick(dataLv2)
nodeLv1.expanded = true
this.$set(this.nodeExpandedDict, dataLv1.path, true)
return
}
}
}
}
}
}
},
async created () {
await this.getFileTree()
},
mounted () {
this.listener = document.querySelector('body').addEventListener('click', ev => {
this.isShowCreatePopoverDict = {}
})
},
destroyed () {
document.querySelector('body').removeEventListener('click', this.listener)
}
}
</script>
<style scoped lang="scss">

View File

@@ -7,26 +7,27 @@
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
@click="toggleClick">
@click="toggleClick"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
},
toggleClick: {
type: Function,
default: null
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
},
toggleClick: {
type: Function,
default: null
}
}
}
}
</script>
<style scoped>

View File

@@ -1,64 +1,65 @@
<template>
<div class="info-view">
<el-row>
<el-form label-width="150px"
:model="nodeForm"
ref="nodeForm"
class="node-form"
label-position="right">
<el-form
ref="nodeForm"
label-width="150px"
:model="nodeForm"
class="node-form"
label-position="right"
>
<el-form-item :label="$t('Node Name')">
<el-input v-model="nodeForm.name" :placeholder="$t('Node Name')" :disabled="isView"></el-input>
<el-input v-model="nodeForm.name" :placeholder="$t('Node Name')" :disabled="isView" />
</el-form-item>
<el-form-item :label="$t('Node IP')" prop="ip" required>
<el-input v-model="nodeForm.ip" :placeholder="$t('Node IP')" disabled></el-input>
<el-input v-model="nodeForm.ip" :placeholder="$t('Node IP')" disabled />
</el-form-item>
<el-form-item :label="$t('Node MAC')" prop="ip" required>
<el-input v-model="nodeForm.mac" :placeholder="$t('Node MAC')" disabled></el-input>
<el-input v-model="nodeForm.mac" :placeholder="$t('Node MAC')" disabled />
</el-form-item>
<el-form-item :label="$t('Description')">
<el-input type="textarea" v-model="nodeForm.description" :placeholder="$t('Description')" :disabled="isView">
</el-input>
<el-input v-model="nodeForm.description" type="textarea" :placeholder="$t('Description')" :disabled="isView" />
</el-form-item>
</el-form>
</el-row>
<el-row class="button-container" v-if="!isView">
<el-button size="small" type="success" @click="onSave">{{$t('Save')}}</el-button>
<el-row v-if="!isView" class="button-container">
<el-button size="small" type="success" @click="onSave">{{ $t('Save') }}</el-button>
</el-row>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'NodeInfoView',
props: {
isView: {
type: Boolean,
default: false
}
},
computed: {
...mapState('node', [
'nodeForm'
])
},
methods: {
onSave () {
this.$refs.nodeForm.validate(valid => {
if (valid) {
this.$store.dispatch('node/editNode')
.then(() => {
this.$message.success(this.$t('Node info has been saved successfully'))
})
}
})
this.$st.sendEv('节点详情', '概览', '保存')
export default {
name: 'NodeInfoView',
props: {
isView: {
type: Boolean,
default: false
}
},
computed: {
...mapState('node', [
'nodeForm'
])
},
methods: {
onSave() {
this.$refs.nodeForm.validate(valid => {
if (valid) {
this.$store.dispatch('node/editNode')
.then(() => {
this.$message.success(this.$t('Node info has been saved successfully'))
})
}
})
this.$st.sendEv('节点详情', '概览', '保存')
}
}
}
}
</script>
<style scoped>

View File

@@ -7,16 +7,18 @@
/>
<el-row>
<el-form label-width="150px"
:model="spiderForm"
ref="spiderForm"
class="spider-form"
label-position="right">
<el-form
ref="spiderForm"
label-width="150px"
:model="spiderForm"
class="spider-form"
label-position="right"
>
<el-form-item :label="$t('Spider ID')">
<el-input v-model="spiderForm._id" :placeholder="$t('Spider ID')" disabled></el-input>
<el-input v-model="spiderForm._id" :placeholder="$t('Spider ID')" disabled />
</el-form-item>
<el-form-item :label="$t('Spider Name')">
<el-input v-model="spiderForm.display_name" :placeholder="$t('Spider Name')" :disabled="isView || isPublic"/>
<el-input v-model="spiderForm.display_name" :placeholder="$t('Spider Name')" :disabled="isView || isPublic" />
</el-form-item>
<el-form-item :label="$t('Project')" prop="project_id" required>
<el-select
@@ -34,7 +36,7 @@
</el-select>
</el-form-item>
<el-form-item :label="$t('Source Folder')">
<el-input v-model="spiderForm.src" :placeholder="$t('Source Folder')" disabled></el-input>
<el-input v-model="spiderForm.src" :placeholder="$t('Source Folder')" disabled />
</el-form-item>
<template v-if="spiderForm.type === 'customized'">
<el-form-item :label="$t('Execute Command')" prop="cmd" required :inline-message="true">
@@ -54,14 +56,14 @@
</el-form-item>
<el-form-item :label="$t('Spider Type')">
<el-select v-model="spiderForm.type" :placeholder="$t('Spider Type')" :disabled="true" clearable>
<el-option value="configurable" :label="$t('Configurable')"></el-option>
<el-option value="customized" :label="$t('Customized')"></el-option>
<el-option value="configurable" :label="$t('Configurable')" />
<el-option value="customized" :label="$t('Customized')" />
</el-select>
</el-form-item>
<el-form-item :label="$t('Remark')">
<el-input
type="textarea"
v-model="spiderForm.remark"
type="textarea"
:placeholder="$t('Remark')"
:disabled="isView || isPublic"
/>
@@ -96,8 +98,12 @@
</el-form-item>
</el-col>
</el-row>
<el-form-item v-if="!isView" :label="$t('De-Duplication')" prop="dedup_field"
:rules="dedupRules">
<el-form-item
v-if="!isView"
:label="$t('De-Duplication')"
prop="dedup_field"
:rules="dedupRules"
>
<div style="display: flex; align-items: center; height: 40px">
<el-switch
v-model="spiderForm.is_dedup"
@@ -112,8 +118,8 @@
:disabled="isView || isPublic"
style="margin-left: 20px; width: 180px"
>
<el-option value="overwrite" :label="$t('Overwrite')"/>
<el-option value="ignore" :label="$t('Ignore')"/>
<el-option value="overwrite" :label="$t('Overwrite')" />
<el-option value="ignore" :label="$t('Ignore')" />
</el-select>
<el-input
v-if="spiderForm.is_dedup"
@@ -151,10 +157,16 @@
</el-row>
</el-form>
</el-row>
<el-row class="button-container" v-if="!isView">
<el-button size="small" v-if="isShowRun && !isPublic" type="danger" @click="onCrawl"
icon="el-icon-video-play" style="margin-right: 10px">
{{$t('Run')}}
<el-row v-if="!isView" class="button-container">
<el-button
v-if="isShowRun && !isPublic"
size="small"
type="danger"
icon="el-icon-video-play"
style="margin-right: 10px"
@click="onCrawl"
>
{{ $t('Run') }}
</el-button>
<el-upload
v-if="spiderForm.type === 'customized'"
@@ -166,166 +178,166 @@
:file-list="fileList"
style="display:inline-block;margin-right:10px"
>
<el-button v-if="!isPublic" size="small" type="primary" icon="el-icon-upload" v-loading="uploadLoading">
{{$t('Upload')}}
<el-button v-if="!isPublic" v-loading="uploadLoading" size="small" type="primary" icon="el-icon-upload">
{{ $t('Upload') }}
</el-button>
</el-upload>
<el-button v-if="!isPublic" size="small" type="success" @click="onSave" icon="el-icon-check">
{{$t('Save')}}
<el-button v-if="!isPublic" size="small" type="success" icon="el-icon-check" @click="onSave">
{{ $t('Save') }}
</el-button>
</el-row>
</div>
</template>
<script>
import {
mapState,
mapGetters
} from 'vuex'
import CrawlConfirmDialog from '../Common/CrawlConfirmDialog'
import {
mapState,
mapGetters
} from 'vuex'
import CrawlConfirmDialog from '../Common/CrawlConfirmDialog'
export default {
name: 'SpiderInfoView',
components: { CrawlConfirmDialog },
props: {
isView: {
default: false,
type: Boolean
}
},
data () {
const cronValidator = (rule, value, callback) => {
let patArr = []
for (let i = 0; i < 6; i++) {
patArr.push('[/*,0-9]+')
export default {
name: 'SpiderInfoView',
components: { CrawlConfirmDialog },
props: {
isView: {
default: false,
type: Boolean
}
const pat = '^' + patArr.join(' ') + '$'
if (this.spiderForm.cron_enabled) {
if (!value) {
callback(new Error('cron cannot be empty'))
} else if (!value.match(pat)) {
callback(new Error('cron format is invalid'))
},
data() {
const cronValidator = (rule, value, callback) => {
const patArr = []
for (let i = 0; i < 6; i++) {
patArr.push('[/*,0-9]+')
}
const pat = '^' + patArr.join(' ') + '$'
if (this.spiderForm.cron_enabled) {
if (!value) {
callback(new Error('cron cannot be empty'))
} else if (!value.match(pat)) {
callback(new Error('cron format is invalid'))
}
}
callback()
}
callback()
}
const dedupValidator = (rule, value, callback) => {
if (!this.spiderForm.is_dedup) {
return callback()
} else {
if (value) {
const dedupValidator = (rule, value, callback) => {
if (!this.spiderForm.is_dedup) {
return callback()
} else {
return callback(new Error('dedup field cannot be empty'))
if (value) {
return callback()
} else {
return callback(new Error('dedup field cannot be empty'))
}
}
}
}
return {
uploadLoading: false,
fileList: [],
crawlConfirmDialogVisible: false,
cmdRule: [
{ message: 'Execute Command should not be empty', required: true }
],
cronRules: [
{ validator: cronValidator, trigger: 'blur' }
],
dedupRules: [
{ validator: dedupValidator, trigger: 'blur' }
]
}
},
computed: {
...mapState('spider', [
'spiderForm'
]),
...mapGetters('user', [
'userInfo',
'token'
]),
...mapState('project', [
'projectList'
]),
isConfigurable () {
return this.spiderForm.type === 'configurable'
},
isShowRun () {
if (this.spiderForm.type === 'customized') {
return !!this.spiderForm.cmd
} else {
return true
return {
uploadLoading: false,
fileList: [],
crawlConfirmDialogVisible: false,
cmdRule: [
{ message: 'Execute Command should not be empty', required: true }
],
cronRules: [
{ validator: cronValidator, trigger: 'blur' }
],
dedupRules: [
{ validator: dedupValidator, trigger: 'blur' }
]
}
},
isPublic () {
return this.spiderForm.is_public && this.spiderForm.username !== this.userInfo.username && this.userInfo.role !== 'admin'
}
},
methods: {
onCrawl () {
this.crawlConfirmDialogVisible = true
this.$st.sendEv('爬虫详情', '概览', '点击运行')
},
onSave () {
this.$refs['spiderForm'].validate(async valid => {
if (!valid) return
const res = await this.$store.dispatch('spider/editSpider')
if (!res.data.error) {
this.$message.success(this.$t('Spider info has been saved successfully'))
computed: {
...mapState('spider', [
'spiderForm'
]),
...mapGetters('user', [
'userInfo',
'token'
]),
...mapState('project', [
'projectList'
]),
isConfigurable() {
return this.spiderForm.type === 'configurable'
},
isShowRun() {
if (this.spiderForm.type === 'customized') {
return !!this.spiderForm.cmd
} else {
return true
}
await this.$store.dispatch('spider/getSpiderData', this.$route.params.id)
if (this.spiderForm.is_scrapy) {
await this.$store.dispatch('spider/getSpiderScrapySpiders', this.$route.params.id)
}
})
this.$st.sendEv('爬虫详情', '概览', '保存')
},
isPublic() {
return this.spiderForm.is_public && this.spiderForm.username !== this.userInfo.username && this.userInfo.role !== 'admin'
}
},
fetchSiteSuggestions (keyword, callback) {
this.$request.get('/sites', {
keyword: keyword,
page_num: 1,
page_size: 100
}).then(response => {
const data = response.data.items.map(d => {
d.value = `${d.name} | ${d.domain}`
return d
async created() {
// fetch project list
await this.$store.dispatch('project/getProjectList')
// 兼容项目ID
if (!this.spiderForm.project_id) {
this.$set(this.spiderForm, 'project_id', '000000000000000000000000')
}
},
methods: {
onCrawl() {
this.crawlConfirmDialogVisible = true
this.$st.sendEv('爬虫详情', '概览', '点击运行')
},
onSave() {
this.$refs['spiderForm'].validate(async valid => {
if (!valid) return
const res = await this.$store.dispatch('spider/editSpider')
if (!res.data.error) {
this.$message.success(this.$t('Spider info has been saved successfully'))
}
await this.$store.dispatch('spider/getSpiderData', this.$route.params.id)
if (this.spiderForm.is_scrapy) {
await this.$store.dispatch('spider/getSpiderScrapySpiders', this.$route.params.id)
}
})
callback(data)
})
},
onSiteSelect (item) {
this.spiderForm.site = item._id
},
onUploadSuccess () {
this.$store.dispatch('spider/getFileTree')
this.$st.sendEv('爬虫详情', '概览', '保存')
},
fetchSiteSuggestions(keyword, callback) {
this.$request.get('/sites', {
keyword: keyword,
page_num: 1,
page_size: 100
}).then(response => {
const data = response.data.items.map(d => {
d.value = `${d.name} | ${d.domain}`
return d
})
callback(data)
})
},
onSiteSelect(item) {
this.spiderForm.site = item._id
},
onUploadSuccess() {
this.$store.dispatch('spider/getFileTree')
this.uploadLoading = false
this.uploadLoading = false
this.$message.success(this.$t('Uploaded spider files successfully'))
},
onUploadError () {
this.uploadLoading = false
},
onIsScrapyChange (value) {
if (value) {
this.spiderForm.cmd = 'scrapy crawl'
this.$message.success(this.$t('Uploaded spider files successfully'))
},
onUploadError() {
this.uploadLoading = false
},
onIsScrapyChange(value) {
if (value) {
this.spiderForm.cmd = 'scrapy crawl'
}
},
onIsDedupChange(value) {
if (value && !this.spiderForm.dedup_method) {
this.spiderForm.dedup_method = 'overwrite'
}
}
},
onIsDedupChange (value) {
if (value && !this.spiderForm.dedup_method) {
this.spiderForm.dedup_method = 'overwrite'
}
}
},
async created () {
// fetch project list
await this.$store.dispatch('project/getProjectList')
// 兼容项目ID
if (!this.spiderForm.project_id) {
this.$set(this.spiderForm, 'project_id', '000000000000000000000000')
}
}
}
</script>
<style scoped>

View File

@@ -1,24 +1,26 @@
<template>
<div class="info-view">
<el-row>
<el-form label-width="150px"
:model="taskForm"
ref="nodeForm"
class="node-form"
label-position="right">
<el-form
ref="nodeForm"
label-width="150px"
:model="taskForm"
class="node-form"
label-position="right"
>
<el-form-item :label="$t('Task ID')">
<el-input v-model="taskForm._id" placeholder="Task ID" disabled></el-input>
<el-input v-model="taskForm._id" placeholder="Task ID" disabled />
</el-form-item>
<el-form-item :label="$t('Status')">
<status-tag :status="taskForm.status"/>
<status-tag :status="taskForm.status" />
<el-badge
v-if="taskForm.error_log_count > 0"
:value="taskForm.error_log_count"
style="margin-left:10px; cursor:pointer;"
>
<el-tag type="danger" @click="onClickLogWithErrors">
<i class="el-icon-warning"></i>
{{$t('Log with errors')}}
<i class="el-icon-warning" />
{{ $t('Log with errors') }}
</el-tag>
</el-badge>
<el-tag
@@ -26,42 +28,42 @@
type="danger"
style="margin-left: 10px"
>
<i class="el-icon-warning"></i>
{{$t('Empty results')}}
<i class="el-icon-warning" />
{{ $t('Empty results') }}
</el-tag>
</el-form-item>
<el-form-item :label="$t('Log File Path')">
<el-input v-model="taskForm.log_path" placeholder="Log File Path" disabled></el-input>
<el-input v-model="taskForm.log_path" placeholder="Log File Path" disabled />
</el-form-item>
<el-form-item :label="$t('Parameters')">
<el-input v-model="taskForm.param" placeholder="Parameters" disabled></el-input>
<el-input v-model="taskForm.param" placeholder="Parameters" disabled />
</el-form-item>
<el-form-item :label="$t('Create Time')">
<el-input :value="getTime(taskForm.create_ts)" placeholder="Create Time" disabled></el-input>
<el-input :value="getTime(taskForm.create_ts)" placeholder="Create Time" disabled />
</el-form-item>
<el-form-item :label="$t('Start Time')">
<el-input :value="getTime(taskForm.start_ts)" placeholder="Start Time" disabled></el-input>
<el-input :value="getTime(taskForm.start_ts)" placeholder="Start Time" disabled />
</el-form-item>
<el-form-item :label="$t('Finish Time')">
<el-input :value="getTime(taskForm.finish_ts)" placeholder="Finish Time" disabled></el-input>
<el-input :value="getTime(taskForm.finish_ts)" placeholder="Finish Time" disabled />
</el-form-item>
<el-form-item :label="$t('Wait Duration (sec)')">
<el-input :value="getWaitDuration(taskForm)" placeholder="Wait Duration" disabled></el-input>
<el-input :value="getWaitDuration(taskForm)" placeholder="Wait Duration" disabled />
</el-form-item>
<el-form-item :label="$t('Runtime Duration (sec)')">
<el-input :value="getRuntimeDuration(taskForm)" placeholder="Runtime Duration" disabled></el-input>
<el-input :value="getRuntimeDuration(taskForm)" placeholder="Runtime Duration" disabled />
</el-form-item>
<el-form-item :label="$t('Total Duration (sec)')">
<el-input :value="getTotalDuration(taskForm)" placeholder="Runtime Duration" disabled></el-input>
<el-input :value="getTotalDuration(taskForm)" placeholder="Runtime Duration" disabled />
</el-form-item>
<el-form-item :label="$t('Results Count')">
<el-input v-model="taskForm.result_count" placeholder="Results Count" disabled></el-input>
<el-input v-model="taskForm.result_count" placeholder="Results Count" disabled />
</el-form-item>
<!--<el-form-item :label="$t('Average Results Count per Second')">-->
<!--<el-input v-model="taskForm.avg_num_results" placeholder="Average Results Count per Second" disabled>-->
<!--</el-input>-->
<!--</el-form-item>-->
<el-form-item :label="$t('Error Message')" v-if="taskForm.status === 'error'">
<el-form-item v-if="taskForm.status === 'error'" :label="$t('Error Message')">
<div class="error-message">
{{ taskForm.error }}
</div>
@@ -69,8 +71,8 @@
</el-form>
</el-row>
<el-row class="button-container">
<el-button v-if="isRunning" size="small" type="danger" @click="onStop" icon="el-icon-video-pause">
{{$t('Stop')}}
<el-button v-if="isRunning" size="small" type="danger" icon="el-icon-video-pause" @click="onStop">
{{ $t('Stop') }}
</el-button>
<!--<el-button type="danger" @click="onRestart">Restart</el-button>-->
</el-row>
@@ -78,56 +80,56 @@
</template>
<script>
import {
mapState
} from 'vuex'
import StatusTag from '../Status/StatusTag'
import dayjs from 'dayjs'
import {
mapState
} from 'vuex'
import StatusTag from '../Status/StatusTag'
import dayjs from 'dayjs'
export default {
name: 'NodeInfoView',
components: { StatusTag },
computed: {
...mapState('task', [
'taskForm',
'taskLog',
'errorLogData'
]),
isRunning () {
return ['pending', 'running'].includes(this.taskForm.status)
}
},
methods: {
onRestart () {
export default {
name: 'NodeInfoView',
components: { StatusTag },
computed: {
...mapState('task', [
'taskForm',
'taskLog',
'errorLogData'
]),
isRunning() {
return ['pending', 'running'].includes(this.taskForm.status)
}
},
onStop () {
this.$store.dispatch('task/cancelTask', this.$route.params.id)
.then(() => {
this.$message.success(`Task "${this.$route.params.id}" has been sent signal to stop`)
})
},
getTime (str) {
if (!str || str.match('^0001')) return 'NA'
return dayjs(str).format('YYYY-MM-DD HH:mm:ss')
},
getWaitDuration (row) {
if (!row.start_ts || row.start_ts.match('^0001')) return 'NA'
return dayjs(row.start_ts).diff(row.create_ts, 'second')
},
getRuntimeDuration (row) {
if (!row.finish_ts || row.finish_ts.match('^0001')) return 'NA'
return dayjs(row.finish_ts).diff(row.start_ts, 'second')
},
getTotalDuration (row) {
if (!row.finish_ts || row.finish_ts.match('^0001')) return 'NA'
return dayjs(row.finish_ts).diff(row.create_ts, 'second')
},
onClickLogWithErrors () {
this.$emit('click-log')
this.$st.sendEv('任务详情', '概览', '点击日志错误')
methods: {
onRestart() {
},
onStop() {
this.$store.dispatch('task/cancelTask', this.$route.params.id)
.then(() => {
this.$message.success(`Task "${this.$route.params.id}" has been sent signal to stop`)
})
},
getTime(str) {
if (!str || str.match('^0001')) return 'NA'
return dayjs(str).format('YYYY-MM-DD HH:mm:ss')
},
getWaitDuration(row) {
if (!row.start_ts || row.start_ts.match('^0001')) return 'NA'
return dayjs(row.start_ts).diff(row.create_ts, 'second')
},
getRuntimeDuration(row) {
if (!row.finish_ts || row.finish_ts.match('^0001')) return 'NA'
return dayjs(row.finish_ts).diff(row.start_ts, 'second')
},
getTotalDuration(row) {
if (!row.finish_ts || row.finish_ts.match('^0001')) return 'NA'
return dayjs(row.finish_ts).diff(row.create_ts, 'second')
},
onClickLogWithErrors() {
this.$emit('click-log')
this.$st.sendEv('任务详情', '概览', '点击日志错误')
}
}
}
}
</script>
<style scoped>

View File

@@ -3,14 +3,14 @@
<el-form class="search-form" inline>
<el-form-item>
<el-autocomplete
v-if="activeLang.executable_name === 'python'"
v-model="depName"
class="search-box"
size="small"
clearable
v-if="activeLang.executable_name === 'python'"
v-model="depName"
style="width: 240px"
:placeholder="$t('Search Dependencies')"
:fetchSuggestions="fetchAllDepList"
:fetch-suggestions="fetchAllDepList"
:minlength="2"
@select="onSearch"
@clear="onSearch"
@@ -23,20 +23,21 @@
/>
</el-form-item>
<el-form-item>
<el-button size="small"
icon="el-icon-search"
type="success"
@click="onSearch"
<el-button
size="small"
icon="el-icon-search"
type="success"
@click="onSearch"
>
{{$t('Search')}}
{{ $t('Search') }}
</el-button>
</el-form-item>
<el-form-item>
<el-checkbox v-model="isShowInstalled" :label="$t('Show installed')" @change="onIsShowInstalledChange"/>
<el-checkbox v-model="isShowInstalled" :label="$t('Show installed')" @change="onIsShowInstalledChange" />
</el-form-item>
</el-form>
<el-tabs v-model="activeTab" @tab-click="onTabChange">
<el-tab-pane v-for="lang in langList" :key="lang.name" :label="lang.name" :name="lang.executable_name"/>
<el-tab-pane v-for="lang in langList" :key="lang.name" :label="lang.name" :name="lang.executable_name" />
</el-tabs>
<template v-if="activeLang.install_status === 'installed'">
<template v-if="!['python', 'node'].includes(activeLang.executable_name)">
@@ -46,16 +47,16 @@
disabled
type="success"
>
{{$t('Installed')}}
{{ $t('Installed') }}
</el-button>
</div>
</template>
<template v-else>
<el-table
v-loading="loading"
height="calc(100vh - 280px)"
:data="computedDepList"
:empty-text="depName ? $t('No Data') : $t('Please search dependencies')"
v-loading="loading"
border
>
<el-table-column
@@ -85,7 +86,7 @@
type="primary"
@click="onClickInstallDep(scope.row)"
>
{{$t('Install')}}
{{ $t('Install') }}
</el-button>
<el-button
v-else
@@ -95,7 +96,7 @@
type="danger"
@click="onClickUninstallDep(scope.row)"
>
{{$t('Uninstall')}}
{{ $t('Uninstall') }}
</el-button>
</template>
</el-table-column>
@@ -109,7 +110,7 @@
disabled
type="warning"
>
{{$t('Installing')}}
{{ $t('Installing') }}
</el-button>
</div>
</template>
@@ -120,19 +121,19 @@
disabled
type="warning"
>
{{$t('Other language installing')}}
{{ $t('Other language installing') }}
</el-button>
</div>
</template>
<template v-else-if="activeLang.install_status === 'not-installed'">
<div class="install-wrapper">
<h4>{{$t('This language is not installed yet.')}}</h4>
<h4>{{ $t('This language is not installed yet.') }}</h4>
<el-button
icon="el-icon-check"
type="primary"
@click="onClickInstallLang"
>
{{$t('Install')}}
{{ $t('Install') }}
</el-button>
</div>
</template>
@@ -140,208 +141,208 @@
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'NodeInstallation',
data () {
return {
activeTab: '',
langList: [],
depName: '',
depList: [],
loading: false,
isShowInstalled: true,
installedDepList: [],
depLoadingDict: {},
isLoadingInstallLang: false
}
},
computed: {
...mapState('node', [
'nodeForm'
]),
activeLang () {
for (let i = 0; i < this.langList.length; i++) {
if (this.langList[i].executable_name === this.activeTab) {
return this.langList[i]
export default {
name: 'NodeInstallation',
data() {
return {
activeTab: '',
langList: [],
depName: '',
depList: [],
loading: false,
isShowInstalled: true,
installedDepList: [],
depLoadingDict: {},
isLoadingInstallLang: false
}
},
computed: {
...mapState('node', [
'nodeForm'
]),
activeLang() {
for (let i = 0; i < this.langList.length; i++) {
if (this.langList[i].executable_name === this.activeTab) {
return this.langList[i]
}
}
return {}
},
activeLangName() {
return this.activeLang.executable_name
},
computedDepList() {
if (this.isShowInstalled) {
return this.installedDepList
} else {
return this.depList
}
}
return {}
},
activeLangName () {
return this.activeLang.executable_name
},
computedDepList () {
if (this.isShowInstalled) {
return this.installedDepList
} else {
return this.depList
}
}
},
methods: {
async getDepList () {
this.loading = true
this.depList = []
const res = await this.$request.get(`/nodes/${this.nodeForm._id}/deps`, {
lang: this.activeLang.executable_name,
dep_name: this.depName
})
this.loading = false
this.depList = res.data.data
if (this.activeLangName === 'python') {
// 排序
this.depList = this.depList.sort((a, b) => a.name > b.name ? 1 : -1)
// 异步获取python附加信息
this.depList.map(async dep => {
const resp = await this.$request.get(`/system/deps/${this.activeLang.executable_name}/${dep.name}/json`)
if (resp) {
dep.version = resp.data.data.version
dep.description = resp.data.data.description
}
})
}
},
async getInstalledDepList () {
if (this.activeLang.install_status !== 'installed') return
if (!['Python', 'Node.js'].includes(this.activeLang.name)) return
this.loading = true
this.installedDepList = []
const res = await this.$request.get(`/nodes/${this.nodeForm._id}/deps/installed`, {
lang: this.activeLang.executable_name
})
this.loading = false
this.installedDepList = res.data.data
},
async fetchAllDepList (queryString, callback) {
const res = await this.$request.get(`/system/deps/${this.activeLang.executable_name}`, {
dep_name: queryString
})
callback(res.data.data ? res.data.data.map(d => {
return { value: d, label: d }
}) : [])
},
onSearch () {
this.isShowInstalled = false
this.getDepList()
this.$st.sendEv('节点详情', '安装', '搜索依赖')
},
onIsShowInstalledChange (val) {
if (val) {
this.getInstalledDepList()
} else {
this.depName = ''
this.depList = []
}
this.$st.sendEv('节点详情', '安装', '点击查看已安装')
},
async onClickInstallDep (dep) {
const name = dep.name
this.$set(this.depLoadingDict, name, true)
async created() {
const arr = this.$route.path.split('/')
const id = arr[arr.length - 1]
const data = await this.$request.post(`/nodes/${id}/deps/install`, {
lang: this.activeLang.executable_name,
dep_name: name
})
if (!data || data.error) {
this.$notify.error({
title: this.$t('Installing dependency failed'),
message: this.$t('The dependency installation is unsuccessful: ') + name
})
} else {
this.$notify.success({
title: this.$t('Installing dependency successful'),
message: this.$t('You have successfully installed a dependency: ') + name
})
dep.installed = true
}
this.$request.put('/actions', {
type: 'install_dep'
})
this.$set(this.depLoadingDict, name, false)
this.$st.sendEv('节点详情', '安装', '安装依赖')
},
async onClickUninstallDep (dep) {
const name = dep.name
this.$set(this.depLoadingDict, name, true)
const arr = this.$route.path.split('/')
const id = arr[arr.length - 1]
const data = await this.$request.post(`/nodes/${id}/deps/uninstall`, {
lang: this.activeLang.executable_name,
dep_name: name
})
if (!data || data.error) {
this.$notify.error({
title: this.$t('Uninstalling dependency failed'),
message: this.$t('The dependency uninstallation is unsuccessful: ') + name
})
} else {
this.$notify.success({
title: this.$t('Uninstalling dependency successful'),
message: this.$t('You have successfully uninstalled a dependency: ') + name
})
dep.installed = false
}
this.$set(this.depLoadingDict, name, false)
this.$st.sendEv('节点详情', '安装', '卸载依赖')
},
getDepLoading (dep) {
const name = dep.name
if (this.depLoadingDict[name] === undefined) {
return false
}
return this.depLoadingDict[name]
},
async onClickInstallLang () {
this.isLoadingInstallLang = true
const res = await this.$request.post(`/nodes/${this.nodeForm._id}/langs/install`, {
lang: this.activeLang.executable_name
})
if (!res || res.error) {
this.$notify.error({
title: this.$t('Installing language failed'),
message: this.$t('The language installation is unsuccessful: ') + this.activeLang.name
})
} else {
this.$notify.success({
title: this.$t('Installing language successful'),
message: this.$t('You have successfully installed a language: ') + this.activeLang.name
})
}
this.$request.put('/actions', {
type: 'install_lang'
})
this.isLoadingInstallLang = false
this.$st.sendEv('节点详情', '安装', '安装语言')
},
onTabChange () {
if (this.isShowInstalled) {
const res = await this.$request.get(`/nodes/${id}/langs`)
this.langList = res.data.data
this.activeTab = this.langList[0].executable_name || ''
setTimeout(() => {
this.getInstalledDepList()
} else {
this.depName = ''
}, 100)
},
methods: {
async getDepList() {
this.loading = true
this.depList = []
const res = await this.$request.get(`/nodes/${this.nodeForm._id}/deps`, {
lang: this.activeLang.executable_name,
dep_name: this.depName
})
this.loading = false
this.depList = res.data.data
if (this.activeLangName === 'python') {
// 排序
this.depList = this.depList.sort((a, b) => a.name > b.name ? 1 : -1)
// 异步获取python附加信息
this.depList.map(async dep => {
const resp = await this.$request.get(`/system/deps/${this.activeLang.executable_name}/${dep.name}/json`)
if (resp) {
dep.version = resp.data.data.version
dep.description = resp.data.data.description
}
})
}
},
async getInstalledDepList() {
if (this.activeLang.install_status !== 'installed') return
if (!['Python', 'Node.js'].includes(this.activeLang.name)) return
this.loading = true
this.installedDepList = []
const res = await this.$request.get(`/nodes/${this.nodeForm._id}/deps/installed`, {
lang: this.activeLang.executable_name
})
this.loading = false
this.installedDepList = res.data.data
},
async fetchAllDepList(queryString, callback) {
const res = await this.$request.get(`/system/deps/${this.activeLang.executable_name}`, {
dep_name: queryString
})
callback(res.data.data ? res.data.data.map(d => {
return { value: d, label: d }
}) : [])
},
onSearch() {
this.isShowInstalled = false
this.getDepList()
this.$st.sendEv('节点详情', '安装', '搜索依赖')
},
onIsShowInstalledChange(val) {
if (val) {
this.getInstalledDepList()
} else {
this.depName = ''
this.depList = []
}
this.$st.sendEv('节点详情', '安装', '点击查看已安装')
},
async onClickInstallDep(dep) {
const name = dep.name
this.$set(this.depLoadingDict, name, true)
const arr = this.$route.path.split('/')
const id = arr[arr.length - 1]
const data = await this.$request.post(`/nodes/${id}/deps/install`, {
lang: this.activeLang.executable_name,
dep_name: name
})
if (!data || data.error) {
this.$notify.error({
title: this.$t('Installing dependency failed'),
message: this.$t('The dependency installation is unsuccessful: ') + name
})
} else {
this.$notify.success({
title: this.$t('Installing dependency successful'),
message: this.$t('You have successfully installed a dependency: ') + name
})
dep.installed = true
}
this.$request.put('/actions', {
type: 'install_dep'
})
this.$set(this.depLoadingDict, name, false)
this.$st.sendEv('节点详情', '安装', '安装依赖')
},
async onClickUninstallDep(dep) {
const name = dep.name
this.$set(this.depLoadingDict, name, true)
const arr = this.$route.path.split('/')
const id = arr[arr.length - 1]
const data = await this.$request.post(`/nodes/${id}/deps/uninstall`, {
lang: this.activeLang.executable_name,
dep_name: name
})
if (!data || data.error) {
this.$notify.error({
title: this.$t('Uninstalling dependency failed'),
message: this.$t('The dependency uninstallation is unsuccessful: ') + name
})
} else {
this.$notify.success({
title: this.$t('Uninstalling dependency successful'),
message: this.$t('You have successfully uninstalled a dependency: ') + name
})
dep.installed = false
}
this.$set(this.depLoadingDict, name, false)
this.$st.sendEv('节点详情', '安装', '卸载依赖')
},
getDepLoading(dep) {
const name = dep.name
if (this.depLoadingDict[name] === undefined) {
return false
}
return this.depLoadingDict[name]
},
async onClickInstallLang() {
this.isLoadingInstallLang = true
const res = await this.$request.post(`/nodes/${this.nodeForm._id}/langs/install`, {
lang: this.activeLang.executable_name
})
if (!res || res.error) {
this.$notify.error({
title: this.$t('Installing language failed'),
message: this.$t('The language installation is unsuccessful: ') + this.activeLang.name
})
} else {
this.$notify.success({
title: this.$t('Installing language successful'),
message: this.$t('You have successfully installed a language: ') + this.activeLang.name
})
}
this.$request.put('/actions', {
type: 'install_lang'
})
this.isLoadingInstallLang = false
this.$st.sendEv('节点详情', '安装', '安装语言')
},
onTabChange() {
if (this.isShowInstalled) {
this.getInstalledDepList()
} else {
this.depName = ''
this.depList = []
}
this.$st.sendEv('节点详情', '安装', '切换标签')
}
this.$st.sendEv('节点详情', '安装', '切换标签')
}
},
async created () {
const arr = this.$route.path.split('/')
const id = arr[arr.length - 1]
const res = await this.$request.get(`/nodes/${id}/langs`)
this.langList = res.data.data
this.activeTab = this.langList[0].executable_name || ''
setTimeout(() => {
this.getInstalledDepList()
}, 100)
}
}
</script>
<style scoped>

View File

@@ -22,8 +22,8 @@
fixed
>
<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
@@ -32,9 +32,9 @@
fixed
>
<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
@@ -45,23 +45,23 @@
>
<template slot="header" slot-scope="scope">
<div class="header-with-action">
<span>{{scope.column.label}}</span>
<span>{{ scope.column.label }}</span>
<el-button type="primary" size="mini" @click="onInstallLangAll(scope.column.label, $event)">
{{$t('Install')}}
{{ $t('Install') }}
</el-button>
</div>
</template>
<template slot-scope="scope">
<template v-if="getLangInstallStatus(scope.row._id, l.name) === 'installed'">
<el-tag type="success">
<i class="el-icon-check"></i>
{{$t('Installed')}}
<i class="el-icon-check" />
{{ $t('Installed') }}
</el-tag>
</template>
<template v-else-if="getLangInstallStatus(scope.row._id, l.name) === 'installing'">
<el-tag type="warning">
<i class="el-icon-loading"></i>
{{$t('Installing')}}
<i class="el-icon-loading" />
{{ $t('Installing') }}
</el-tag>
</template>
<template
@@ -69,18 +69,18 @@
>
<div class="cell-with-action">
<el-tag type="danger">
<i class="el-icon-error"></i>
{{$t('Not Installed')}}
<i class="el-icon-error" />
{{ $t('Not Installed') }}
</el-tag>
<el-button type="primary" size="mini" @click="onInstallLang(scope.row._id, scope.column.label, $event)">
{{$t('Install')}}
{{ $t('Install') }}
</el-button>
</div>
</template>
<template v-else-if="getLangInstallStatus(scope.row._id, l.name) === 'na'">
<el-tag type="info">
<i class="el-icon-question"></i>
{{$t('N/A')}}
<i class="el-icon-question" />
{{ $t('N/A') }}
</el-tag>
</template>
</template>
@@ -98,16 +98,17 @@
/>
</el-form-item>
<el-form-item>
<el-button size="small"
icon="el-icon-search"
type="success"
@click="onSearch"
<el-button
size="small"
icon="el-icon-search"
type="success"
@click="onSearch"
>
{{$t('Search')}}
{{ $t('Search') }}
</el-button>
</el-form-item>
<el-form-item>
<el-checkbox v-model="isShowInstalled" :label="$t('Show installed')" @change="onIsShowInstalledChange"/>
<el-checkbox v-model="isShowInstalled" :label="$t('Show installed')" @change="onIsShowInstalledChange" />
</el-form-item>
</el-form>
<el-tabs v-model="activeLang">
@@ -144,7 +145,7 @@
size="mini"
type="primary"
>
{{$t('Install')}}
{{ $t('Install') }}
</el-button>
</template>
</el-table-column>
@@ -156,7 +157,7 @@
align="center"
>
<template slot="header" slot-scope="scope">
{{scope.column.label}}
{{ scope.column.label }}
</template>
<template slot-scope="scope">
<div
@@ -164,14 +165,14 @@
class="cell-with-action"
>
<el-tag type="success">
{{$t('Installed')}}
{{ $t('Installed') }}
</el-tag>
<el-button
size="mini"
type="danger"
@click="uninstallDep(n, scope.row)"
>
{{$t('Uninstall')}}
{{ $t('Uninstall') }}
</el-button>
</div>
<div
@@ -179,8 +180,8 @@
class="cell-with-action"
>
<el-tag type="warning">
<i class="el-icon-loading"></i>
{{$t('Installing')}}
<i class="el-icon-loading" />
{{ $t('Installing') }}
</el-tag>
</div>
<div
@@ -188,14 +189,14 @@
class="cell-with-action"
>
<el-tag type="danger">
{{$t('Not Installed')}}
{{ $t('Not Installed') }}
</el-tag>
<el-button
size="mini"
type="primary"
@click="installDep(n, scope.row)"
>
{{$t('Install')}}
{{ $t('Install') }}
</el-button>
</div>
</template>
@@ -207,242 +208,242 @@
</template>
<script>
import { mapState } from 'vuex'
import { mapState } from 'vuex'
export default {
name: 'NodeInstallationMatrix',
props: {
activeTab: {
type: String,
default: ''
}
},
data () {
return {
langs: [
{ label: 'Python', name: 'python', hasDeps: true },
{ label: 'Node.js', name: 'node', hasDeps: true },
{ label: 'Java', name: 'java', hasDeps: false },
{ label: '.Net Core', name: 'dotnet', hasDeps: false },
{ label: 'PHP', name: 'php', hasDeps: false }
],
langsDataDict: {},
handle: undefined,
activeTabName: 'lang',
depsDataDict: {},
depsSet: new Set(),
activeLang: 'python',
isDepsLoading: false,
depName: '',
isShowInstalled: true,
depList: []
}
},
computed: {
...mapState('node', [
'nodeList'
]),
activeNodes () {
return this.nodeList.filter(d => d.status === 'online')
},
computedDepsSet () {
return Array.from(this.depsSet).map(d => {
return {
name: d
}
})
},
langsWithDeps () {
return this.langs.filter(l => l.hasDeps)
}
},
watch: {
activeLang () {
this.getDepsData()
}
},
methods: {
async getLangsData () {
await Promise.all(this.nodeList.map(async n => {
if (n.status !== 'online') return
const res = await this.$request.get(`/nodes/${n._id}/langs`)
if (!res.data.data) return
res.data.data.forEach(l => {
const key = n._id + '|' + l.executable_name
this.$set(this.langsDataDict, key, l)
})
}))
},
async getDepsData () {
this.isDepsLoading = true
this.depsDataDict = {}
this.depsSet = new Set()
const depsSet = new Set()
await Promise.all(this.nodeList.map(async n => {
if (n.status !== 'online') return
const res = await this.$request.get(`/nodes/${n._id}/deps/installed`, { lang: this.activeLang })
if (!res.data.data) return
res.data.data.forEach(d => {
depsSet.add(d.name)
const key = n._id + '|' + d.name
this.$set(this.depsDataDict, key, 'installed')
})
}))
this.depsSet = depsSet
this.isDepsLoading = false
},
getLang (nodeId, langName) {
const key = nodeId + '|' + langName
return this.langsDataDict[key]
},
getLangInstallStatus (nodeId, langName) {
const lang = this.getLang(nodeId, langName)
if (!lang || !lang.install_status) return 'na'
return lang.install_status
},
getLangFromLabel (label) {
for (let i = 0; i < this.langs.length; i++) {
const lang = this.langs[i]
if (lang.label === label) {
return lang
}
export default {
name: 'NodeInstallationMatrix',
props: {
activeTab: {
type: String,
default: ''
}
},
async onInstallLang (nodeId, langLabel, ev) {
if (ev) {
ev.stopPropagation()
}
const lang = this.getLangFromLabel(langLabel)
this.$request.post(`/nodes/${nodeId}/langs/install`, {
lang: lang.name
})
const key = nodeId + '|' + lang.name
this.$set(this.langsDataDict[key], 'install_status', 'installing')
setTimeout(() => {
this.getLangsData()
}, 1000)
this.$request.put('/actions', {
type: 'install_lang'
})
this.$st.sendEv('节点列表', '安装', '安装语言')
},
async onInstallLangAll (langLabel, ev) {
ev.stopPropagation()
this.nodeList
.filter(n => {
if (n.status !== 'online') return false
const lang = this.getLangFromLabel(langLabel)
const key = n._id + '|' + lang.name
if (!this.langsDataDict[key]) return false
if (['installing', 'installed'].includes(this.langsDataDict[key].install_status)) return false
return true
})
.forEach(n => {
this.onInstallLang(n._id, langLabel, ev)
})
setTimeout(() => {
this.getLangsData()
}, 1000)
this.$st.sendEv('节点列表', '安装', '安装语言-所有节点')
},
onLangTableRowClick (row) {
this.$router.push(`/nodes/${row._id}`)
this.$st.sendEv('节点列表', '安装', '查看节点详情')
},
getDepStatus (node, dep) {
const key = node._id + '|' + dep.name
if (!this.depsDataDict[key]) {
return 'uninstalled'
} else {
return this.depsDataDict[key]
data() {
return {
langs: [
{ label: 'Python', name: 'python', hasDeps: true },
{ label: 'Node.js', name: 'node', hasDeps: true },
{ label: 'Java', name: 'java', hasDeps: false },
{ label: '.Net Core', name: 'dotnet', hasDeps: false },
{ label: 'PHP', name: 'php', hasDeps: false }
],
langsDataDict: {},
handle: undefined,
activeTabName: 'lang',
depsDataDict: {},
depsSet: new Set(),
activeLang: 'python',
isDepsLoading: false,
depName: '',
isShowInstalled: true,
depList: []
}
},
async installDep (node, dep) {
const key = node._id + '|' + dep.name
this.$set(this.depsDataDict, key, 'installing')
const data = await this.$request.post(`/nodes/${node._id}/deps/install`, {
lang: this.activeLang,
dep_name: dep.name
})
if (!data || data.error) {
this.$notify.error({
title: this.$t('Installing dependency failed'),
message: this.$t('The dependency installation is unsuccessful: ') + dep.name
computed: {
...mapState('node', [
'nodeList'
]),
activeNodes() {
return this.nodeList.filter(d => d.status === 'online')
},
computedDepsSet() {
return Array.from(this.depsSet).map(d => {
return {
name: d
}
})
this.$set(this.depsDataDict, key, 'uninstalled')
} else {
this.$notify.success({
title: this.$t('Installing dependency successful'),
message: this.$t('You have successfully installed a dependency: ') + dep.name
})
this.$set(this.depsDataDict, key, 'installed')
},
langsWithDeps() {
return this.langs.filter(l => l.hasDeps)
}
this.$request.put('/actions', {
type: 'install_dep'
})
this.$st.sendEv('节点列表', '安装', '安装依赖')
},
async uninstallDep (node, dep) {
const key = node._id + '|' + dep.name
this.$set(this.depsDataDict, key, 'installing')
const data = await this.$request.post(`/nodes/${node._id}/deps/uninstall`, {
lang: this.activeLang,
dep_name: dep.name
})
if (!data || data.error) {
this.$notify.error({
title: this.$t('Uninstalling dependency failed'),
message: this.$t('The dependency uninstallation is unsuccessful: ') + dep.name
})
this.$set(this.depsDataDict, key, 'installed')
} else {
this.$notify.success({
title: this.$t('Uninstalling dependency successful'),
message: this.$t('You have successfully uninstalled a dependency: ') + dep.name
})
this.$set(this.depsDataDict, key, 'uninstalled')
}
this.$st.sendEv('节点列表', '安装', '卸载依赖')
},
onSearch () {
this.isShowInstalled = false
this.getDepList()
this.$st.sendEv('节点列表', '安装', '搜索依赖')
},
async getDepList () {
const masterNode = this.nodeList.filter(n => n.is_master)[0]
this.depsSet = []
this.isDepsLoading = true
const res = await this.$request.get(`/nodes/${masterNode._id}/deps`, {
lang: this.activeLang,
dep_name: this.depName
})
this.isDepsLoading = false
this.depsSet = new Set(res.data.data.map(d => d.name))
},
onIsShowInstalledChange (val) {
if (val) {
watch: {
activeLang() {
this.getDepsData()
} else {
this.depsSet = []
}
this.$st.sendEv('节点列表', '安装', '点击查看已安装')
}
},
async created () {
setTimeout(() => {
this.getLangsData()
this.getDepsData()
}, 1000)
},
async created() {
setTimeout(() => {
this.getLangsData()
this.getDepsData()
}, 1000)
this.handle = setInterval(() => {
this.getLangsData()
}, 10000)
},
destroyed () {
clearInterval(this.handle)
this.handle = setInterval(() => {
this.getLangsData()
}, 10000)
},
destroyed() {
clearInterval(this.handle)
},
methods: {
async getLangsData() {
await Promise.all(this.nodeList.map(async n => {
if (n.status !== 'online') return
const res = await this.$request.get(`/nodes/${n._id}/langs`)
if (!res.data.data) return
res.data.data.forEach(l => {
const key = n._id + '|' + l.executable_name
this.$set(this.langsDataDict, key, l)
})
}))
},
async getDepsData() {
this.isDepsLoading = true
this.depsDataDict = {}
this.depsSet = new Set()
const depsSet = new Set()
await Promise.all(this.nodeList.map(async n => {
if (n.status !== 'online') return
const res = await this.$request.get(`/nodes/${n._id}/deps/installed`, { lang: this.activeLang })
if (!res.data.data) return
res.data.data.forEach(d => {
depsSet.add(d.name)
const key = n._id + '|' + d.name
this.$set(this.depsDataDict, key, 'installed')
})
}))
this.depsSet = depsSet
this.isDepsLoading = false
},
getLang(nodeId, langName) {
const key = nodeId + '|' + langName
return this.langsDataDict[key]
},
getLangInstallStatus(nodeId, langName) {
const lang = this.getLang(nodeId, langName)
if (!lang || !lang.install_status) return 'na'
return lang.install_status
},
getLangFromLabel(label) {
for (let i = 0; i < this.langs.length; i++) {
const lang = this.langs[i]
if (lang.label === label) {
return lang
}
}
},
async onInstallLang(nodeId, langLabel, ev) {
if (ev) {
ev.stopPropagation()
}
const lang = this.getLangFromLabel(langLabel)
this.$request.post(`/nodes/${nodeId}/langs/install`, {
lang: lang.name
})
const key = nodeId + '|' + lang.name
this.$set(this.langsDataDict[key], 'install_status', 'installing')
setTimeout(() => {
this.getLangsData()
}, 1000)
this.$request.put('/actions', {
type: 'install_lang'
})
this.$st.sendEv('节点列表', '安装', '安装语言')
},
async onInstallLangAll(langLabel, ev) {
ev.stopPropagation()
this.nodeList
.filter(n => {
if (n.status !== 'online') return false
const lang = this.getLangFromLabel(langLabel)
const key = n._id + '|' + lang.name
if (!this.langsDataDict[key]) return false
if (['installing', 'installed'].includes(this.langsDataDict[key].install_status)) return false
return true
})
.forEach(n => {
this.onInstallLang(n._id, langLabel, ev)
})
setTimeout(() => {
this.getLangsData()
}, 1000)
this.$st.sendEv('节点列表', '安装', '安装语言-所有节点')
},
onLangTableRowClick(row) {
this.$router.push(`/nodes/${row._id}`)
this.$st.sendEv('节点列表', '安装', '查看节点详情')
},
getDepStatus(node, dep) {
const key = node._id + '|' + dep.name
if (!this.depsDataDict[key]) {
return 'uninstalled'
} else {
return this.depsDataDict[key]
}
},
async installDep(node, dep) {
const key = node._id + '|' + dep.name
this.$set(this.depsDataDict, key, 'installing')
const data = await this.$request.post(`/nodes/${node._id}/deps/install`, {
lang: this.activeLang,
dep_name: dep.name
})
if (!data || data.error) {
this.$notify.error({
title: this.$t('Installing dependency failed'),
message: this.$t('The dependency installation is unsuccessful: ') + dep.name
})
this.$set(this.depsDataDict, key, 'uninstalled')
} else {
this.$notify.success({
title: this.$t('Installing dependency successful'),
message: this.$t('You have successfully installed a dependency: ') + dep.name
})
this.$set(this.depsDataDict, key, 'installed')
}
this.$request.put('/actions', {
type: 'install_dep'
})
this.$st.sendEv('节点列表', '安装', '安装依赖')
},
async uninstallDep(node, dep) {
const key = node._id + '|' + dep.name
this.$set(this.depsDataDict, key, 'installing')
const data = await this.$request.post(`/nodes/${node._id}/deps/uninstall`, {
lang: this.activeLang,
dep_name: dep.name
})
if (!data || data.error) {
this.$notify.error({
title: this.$t('Uninstalling dependency failed'),
message: this.$t('The dependency uninstallation is unsuccessful: ') + dep.name
})
this.$set(this.depsDataDict, key, 'installed')
} else {
this.$notify.success({
title: this.$t('Uninstalling dependency successful'),
message: this.$t('You have successfully uninstalled a dependency: ') + dep.name
})
this.$set(this.depsDataDict, key, 'uninstalled')
}
this.$st.sendEv('节点列表', '安装', '卸载依赖')
},
onSearch() {
this.isShowInstalled = false
this.getDepList()
this.$st.sendEv('节点列表', '安装', '搜索依赖')
},
async getDepList() {
const masterNode = this.nodeList.filter(n => n.is_master)[0]
this.depsSet = []
this.isDepsLoading = true
const res = await this.$request.get(`/nodes/${masterNode._id}/deps`, {
lang: this.activeLang,
dep_name: this.depName
})
this.isDepsLoading = false
this.depsSet = new Set(res.data.data.map(d => d.name))
},
onIsShowInstalledChange(val) {
if (val) {
this.getDepsData()
} else {
this.depsSet = []
}
this.$st.sendEv('节点列表', '安装', '点击查看已安装')
}
}
}
}
</script>
<style scoped>

View File

@@ -1,184 +1,185 @@
<template>
<div id="network-chart"></div>
<div id="network-chart"/>
</template>
<script>
import echarts from 'echarts'
import echarts from 'echarts'
export default {
name: 'NodeNetwork',
props: {
activeTab: {
type: String
}
},
watch: {
activeTab () {
setTimeout(() => {
this.render()
}, 0)
}
},
data () {
return {
chart: undefined
}
},
computed: {
masterNode () {
const nodes = this.$store.state.node.nodeList
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].is_master) {
return nodes[i]
}
export default {
name: 'NodeNetwork',
props: {
activeTab: {
type: String,
default: ''
}
return {}
},
nodes () {
let nodes = this.$store.state.node.nodeList
nodes = nodes
.filter(d => d.status !== 'offline')
.map(d => {
d.id = d._id
d.x = Math.floor(100 * Math.random())
d.y = Math.floor(100 * Math.random())
d.itemStyle = {
color: d.is_master ? '#409EFF' : '#e6a23c'
data() {
return {
chart: undefined
}
},
computed: {
masterNode() {
const nodes = this.$store.state.node.nodeList
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].is_master) {
return nodes[i]
}
return d
})
// mongodb
nodes.push({
id: 'mongodb',
name: 'MongoDB',
x: Math.floor(100 * Math.random()),
y: Math.floor(100 * Math.random()),
itemStyle: {
color: '#67c23a'
}
})
return {}
},
nodes() {
let nodes = this.$store.state.node.nodeList
nodes = nodes
.filter(d => d.status !== 'offline')
.map(d => {
d.id = d._id
d.x = Math.floor(100 * Math.random())
d.y = Math.floor(100 * Math.random())
d.itemStyle = {
color: d.is_master ? '#409EFF' : '#e6a23c'
}
return d
})
// redis
nodes.push({
id: 'redis',
name: 'Redis',
x: Math.floor(100 * Math.random()),
y: Math.floor(100 * Math.random()),
itemStyle: {
color: '#f56c6c'
}
})
return nodes
},
links () {
const links = []
for (let i = 0; i < this.nodes.length; i++) {
if (this.nodes[i].status === 'offline') continue
if (['redis', 'mongodb'].includes(this.nodes[i].id)) continue
// mongodb
links.push({
source: this.nodes[i].id,
target: 'mongodb',
value: 10,
lineStyle: {
nodes.push({
id: 'mongodb',
name: 'MongoDB',
x: Math.floor(100 * Math.random()),
y: Math.floor(100 * Math.random()),
itemStyle: {
color: '#67c23a'
}
})
// redis
links.push({
source: this.nodes[i].id,
target: 'redis',
value: 10,
lineStyle: {
nodes.push({
id: 'redis',
name: 'Redis',
x: Math.floor(100 * Math.random()),
y: Math.floor(100 * Math.random()),
itemStyle: {
color: '#f56c6c'
}
})
if (this.masterNode.id === this.nodes[i].id) continue
return nodes
},
links() {
const links = []
for (let i = 0; i < this.nodes.length; i++) {
if (this.nodes[i].status === 'offline') continue
if (['redis', 'mongodb'].includes(this.nodes[i].id)) continue
// mongodb
links.push({
source: this.nodes[i].id,
target: 'mongodb',
value: 10,
lineStyle: {
color: '#67c23a'
}
})
// master
// links.push({
// source: this.masterNode.id,
// target: this.nodes[i].id,
// value: 0.5,
// lineStyle: {
// color: '#409EFF'
// }
// })
// redis
links.push({
source: this.nodes[i].id,
target: 'redis',
value: 10,
lineStyle: {
color: '#f56c6c'
}
})
if (this.masterNode.id === this.nodes[i].id) continue
// master
// links.push({
// source: this.masterNode.id,
// target: this.nodes[i].id,
// value: 0.5,
// lineStyle: {
// color: '#409EFF'
// }
// })
}
return links
}
return links
}
},
methods: {
render () {
const option = {
title: {
text: this.$t('Node Network')
},
tooltip: {
formatter: params => {
if (!params.data.name) return
let str = '<span style="margin-right:5px;display:inline-block;height:12px;width:12px;border-radius:6px;background:' + params.color + '"></span>'
if (params.data.name) str += '<span>' + params.data.name + '</span><br>'
if (params.data.ip) str += '<span>IP: ' + params.data.ip + '</span><br>'
if (params.data.mac) str += '<span>MAC: ' + params.data.mac + '</span><br>'
return str
}
},
animationDurationUpdate: 1500,
animationEasingUpdate: 'quinticInOut',
series: [
{
type: 'graph',
layout: 'force',
symbolSize: 50,
roam: true,
label: {
normal: {
show: true
}
},
edgeSymbol: ['circle', 'arrow'],
edgeSymbolSize: [4, 10],
edgeLabel: {
normal: {
textStyle: {
fontSize: 20
},
watch: {
activeTab() {
setTimeout(() => {
this.render()
}, 0)
}
},
mounted() {
this.render()
},
methods: {
render() {
const option = {
title: {
text: this.$t('Node Network')
},
tooltip: {
formatter: params => {
if (!params.data.name) return
let str = '<span style="margin-right:5px;display:inline-block;height:12px;width:12px;border-radius:6px;background:' + params.color + '"></span>'
if (params.data.name) str += '<span>' + params.data.name + '</span><br>'
if (params.data.ip) str += '<span>IP: ' + params.data.ip + '</span><br>'
if (params.data.mac) str += '<span>MAC: ' + params.data.mac + '</span><br>'
return str
}
},
animationDurationUpdate: 1500,
animationEasingUpdate: 'quinticInOut',
series: [
{
type: 'graph',
layout: 'force',
symbolSize: 50,
roam: true,
label: {
normal: {
show: true
}
},
edgeSymbol: ['circle', 'arrow'],
edgeSymbolSize: [4, 10],
edgeLabel: {
normal: {
textStyle: {
fontSize: 20
}
}
},
focusOneNodeAdjacency: true,
force: {
initLayout: 'force',
repulsion: 30,
gravity: 0.001,
edgeLength: 30
},
draggable: true,
data: this.nodes,
links: this.links,
lineStyle: {
normal: {
opacity: 0.9,
width: 2,
curveness: 0
}
}
},
focusOneNodeAdjacency: true,
force: {
initLayout: 'force',
repulsion: 30,
gravity: 0.001,
edgeLength: 30
},
draggable: true,
data: this.nodes,
links: this.links,
lineStyle: {
normal: {
opacity: 0.9,
width: 2,
curveness: 0
}
}
}
]
]
}
this.chart = echarts.init(this.$el)
this.chart.setOption(option)
this.chart.resize()
}
this.chart = echarts.init(this.$el)
this.chart.setOption(option)
this.chart.resize()
}
},
mounted () {
this.render()
}
}
</script>
<style scoped>

View File

@@ -3,44 +3,44 @@
<el-col :span="12">
<!--last tasks-->
<el-row class="latest-tasks-wrapper">
<task-table-view :title="$t('Latest Tasks')"/>
<task-table-view :title="$t('Latest Tasks')" />
</el-row>
</el-col>
<el-col :span="12">
<el-row class="node-info-view-wrapper">
<!--basic info-->
<node-info-view/>
<node-info-view />
</el-row>
</el-col>
</el-row>
</template>
<script>
import {
mapState
} from 'vuex'
import TaskTableView from '../TableView/TaskTableView'
import NodeInfoView from '../InfoView/NodeInfoView'
import {
mapState
} from 'vuex'
import TaskTableView from '../TableView/TaskTableView'
import NodeInfoView from '../InfoView/NodeInfoView'
export default {
name: 'NodeOverview',
components: {
NodeInfoView,
TaskTableView
},
computed: {
id () {
return this.$route.params.id
export default {
name: 'NodeOverview',
components: {
NodeInfoView,
TaskTableView
},
...mapState('node', [
'nodeForm'
])
},
methods: {},
created () {
computed: {
id() {
return this.$route.params.id
},
...mapState('node', [
'nodeForm'
])
},
created() {
},
methods: {}
}
}
</script>
<style scoped>

View File

@@ -3,50 +3,50 @@
<el-col :span="12">
<!--last tasks-->
<el-row>
<task-table-view :title="$t('Latest Tasks')"/>
<task-table-view :title="$t('Latest Tasks')" />
</el-row>
</el-col>
<el-col :span="12">
<!--basic info-->
<spider-info-view/>
<spider-info-view />
</el-col>
</el-row>
</template>
<script>
import {
mapState
} from 'vuex'
import TaskTableView from '../TableView/TaskTableView'
import SpiderInfoView from '../InfoView/SpiderInfoView'
import {
mapState
} from 'vuex'
import TaskTableView from '../TableView/TaskTableView'
import SpiderInfoView from '../InfoView/SpiderInfoView'
export default {
name: 'SpiderOverview',
components: {
SpiderInfoView,
TaskTableView
},
data () {
return {
// spiderForm: {}
}
},
computed: {
id () {
return this.$route.params.id
export default {
name: 'SpiderOverview',
components: {
SpiderInfoView,
TaskTableView
},
...mapState('spider', [
'spiderForm'
]),
...mapState('deploy', [
'deployList'
])
},
methods: {},
created () {
data() {
return {
// spiderForm: {}
}
},
computed: {
id() {
return this.$route.params.id
},
...mapState('spider', [
'spiderForm'
]),
...mapState('deploy', [
'deployList'
])
},
created() {
},
methods: {}
}
}
</script>
<style scoped>

View File

@@ -7,7 +7,7 @@
icon="el-icon-position"
@click="onNavigateToSpider"
>
{{$t('Navigate to Spider')}}
{{ $t('Navigate to Spider') }}
</el-button>
<el-button
type="warning"
@@ -15,30 +15,30 @@
icon="el-icon-position"
@click="onNavigateToNode"
>
{{$t('Navigate to Node')}}
{{ $t('Navigate to Node') }}
</el-button>
</el-row>
<el-row class="content">
<el-col :span="12" style="padding-right: 20px;">
<el-row class="task-info-overview-wrapper wrapper">
<h4 class="title">{{$t('Task Info')}}</h4>
<task-info-view @click-log="() => $emit('click-log')"/>
<h4 class="title">{{ $t('Task Info') }}</h4>
<task-info-view @click-log="() => $emit('click-log')" />
</el-row>
<el-row style="border-bottom:1px solid #e4e7ed;margin:0 0 20px 0;padding-bottom:20px;"/>
<el-row style="border-bottom:1px solid #e4e7ed;margin:0 0 20px 0;padding-bottom:20px;" />
</el-col>
<el-col :span="12">
<el-row class="task-info-spider-wrapper wrapper">
<h4 class="title spider-title" @click="onNavigateToSpider">
<i class="fa fa-search" style="margin-right: 5px"></i>
{{$t('Spider Info')}}</h4>
<spider-info-view :is-view="true"/>
<i class="fa fa-search" style="margin-right: 5px" />
{{ $t('Spider Info') }}</h4>
<spider-info-view :is-view="true" />
</el-row>
<el-row class="task-info-node-wrapper wrapper">
<h4 class="title node-title" @click="onNavigateToNode">
<i class="fa fa-search" style="margin-right: 5px"></i>
{{$t('Node Info')}}</h4>
<node-info-view :is-view="true"/>
<i class="fa fa-search" style="margin-right: 5px" />
{{ $t('Node Info') }}</h4>
<node-info-view :is-view="true" />
</el-row>
</el-col>
</el-row>
@@ -46,41 +46,41 @@
</template>
<script>
import {
mapState
} from 'vuex'
import SpiderInfoView from '../InfoView/SpiderInfoView'
import NodeInfoView from '../InfoView/NodeInfoView'
import TaskInfoView from '../InfoView/TaskInfoView'
import {
mapState
} from 'vuex'
import SpiderInfoView from '../InfoView/SpiderInfoView'
import NodeInfoView from '../InfoView/NodeInfoView'
import TaskInfoView from '../InfoView/TaskInfoView'
export default {
name: 'SpiderOverview',
components: {
NodeInfoView,
SpiderInfoView,
TaskInfoView
},
computed: {
...mapState('node', [
'nodeForm'
]),
...mapState('spider', [
'spiderForm'
])
},
methods: {
onNavigateToSpider () {
this.$router.push(`/spiders/${this.spiderForm._id}`)
this.$st.sendEv('任务详情', '概览', '点击爬虫详情')
export default {
name: 'SpiderOverview',
components: {
NodeInfoView,
SpiderInfoView,
TaskInfoView
},
onNavigateToNode () {
this.$router.push(`/nodes/${this.nodeForm._id}`)
this.$st.sendEv('任务详情', '概览', '点击节点详情')
computed: {
...mapState('node', [
'nodeForm'
]),
...mapState('spider', [
'spiderForm'
])
},
created() {
},
methods: {
onNavigateToSpider() {
this.$router.push(`/spiders/${this.spiderForm._id}`)
this.$st.sendEv('任务详情', '概览', '点击爬虫详情')
},
onNavigateToNode() {
this.$router.push(`/nodes/${this.nodeForm._id}`)
this.$st.sendEv('任务详情', '概览', '点击节点详情')
}
}
},
created () {
}
}
</script>
<style scoped>

View File

@@ -9,84 +9,85 @@
:total="total"
v-bind="$attrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"/>
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
import { scrollTo } from '@/utils/scrollTo'
import { scrollTo } from '@/utils/scrollTo'
export default {
name: 'Pagination',
props: {
total: {
required: true,
type: Number
},
page: {
type: Number,
default: 1
},
limit: {
type: Number,
default: 20
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
autoScroll: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
},
computed: {
currentPage: {
get() {
return this.page
export default {
name: 'Pagination',
props: {
total: {
required: true,
type: Number
},
set(val) {
this.$emit('update:page', val)
}
},
pageSize: {
get() {
return this.limit
page: {
type: Number,
default: 1
},
set(val) {
this.$emit('update:limit', val)
}
}
},
methods: {
handleSizeChange(val) {
this.$emit('pagination', { page: this.currentPage, limit: val })
if (this.autoScroll) {
scrollTo(0, 800)
limit: {
type: Number,
default: 20
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
autoScroll: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
},
handleCurrentChange(val) {
this.$emit('pagination', { page: val, limit: this.pageSize })
if (this.autoScroll) {
scrollTo(0, 800)
computed: {
currentPage: {
get() {
return this.page
},
set(val) {
this.$emit('update:page', val)
}
},
pageSize: {
get() {
return this.limit
},
set(val) {
this.$emit('update:limit', val)
}
}
},
methods: {
handleSizeChange(val) {
this.$emit('pagination', { page: this.currentPage, limit: val })
if (this.autoScroll) {
scrollTo(0, 800)
}
},
handleCurrentChange(val) {
this.$emit('pagination', { page: val, limit: this.pageSize })
if (this.autoScroll) {
scrollTo(0, 800)
}
}
}
}
}
</script>
<style scoped>

View File

@@ -2,7 +2,7 @@
<div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
<div class="pan-info">
<div class="pan-info-roles-container">
<slot/>
<slot />
</div>
</div>
<img :src="image" class="pan-thumb">
@@ -10,27 +10,27 @@
</template>
<script>
export default {
name: 'PanThumb',
props: {
image: {
type: String,
required: true
},
zIndex: {
type: Number,
default: 1
},
width: {
type: String,
default: '150px'
},
height: {
type: String,
default: '150px'
export default {
name: 'PanThumb',
props: {
image: {
type: String,
required: true
},
zIndex: {
type: Number,
default: 1
},
width: {
type: String,
default: '150px'
},
height: {
type: String,
default: '150px'
}
}
}
}
</script>
<style scoped>

View File

@@ -1,30 +1,30 @@
<script>
import {
mapState
} from 'vuex'
import TaskList from '../../views/task/TaskList'
import {
mapState
} from 'vuex'
import TaskList from '../../views/task/TaskList'
export default {
name: 'ScheduleTaskList',
extends: TaskList,
computed: {
...mapState('task', [
'filter'
]),
...mapState('schedule', [
'scheduleForm'
])
},
methods: {
update () {
this.isFilterSpiderDisabled = true
this.$set(this.filter, 'spider_id', this.scheduleForm.spider_id)
this.filter.schedule_id = this.scheduleForm._id
this.$store.dispatch('task/getTaskList')
export default {
name: 'ScheduleTaskList',
extends: TaskList,
computed: {
...mapState('task', [
'filter'
]),
...mapState('schedule', [
'scheduleForm'
])
},
async created() {
this.update()
},
methods: {
update() {
this.isFilterSpiderDisabled = true
this.$set(this.filter, 'spider_id', this.scheduleForm.spider_id)
this.filter.schedule_id = this.scheduleForm._id
this.$store.dispatch('task/getTaskList')
}
}
},
async created () {
this.update()
}
}
</script>

View File

@@ -15,7 +15,7 @@
icon="el-icon-plus"
@click="onSettingsActiveParamAdd"
>
{{$t('Add')}}
{{ $t('Add') }}
</el-button>
</div>
<el-table
@@ -64,9 +64,9 @@
</el-table-column>
</el-table>
<template slot="footer">
<el-button type="plain" size="small" @click="onCloseDialog">{{$t('Cancel')}}</el-button>
<el-button type="plain" size="small" @click="onCloseDialog">{{ $t('Cancel') }}</el-button>
<el-button type="primary" size="small" @click="onSettingsConfirm">
{{$t('Confirm')}}
{{ $t('Confirm') }}
</el-button>
</template>
</el-dialog>
@@ -79,9 +79,9 @@
width="480px"
>
<el-form
ref="add-spider-form"
:model="addSpiderForm"
label-width="80px"
ref="add-spider-form"
inline-message
>
<el-form-item :label="$t('Name')" prop="name" required>
@@ -100,15 +100,15 @@
</el-form-item>
</el-form>
<template slot="footer">
<el-button type="plain" size="small" @click="isAddSpiderVisible = false">{{$t('Cancel')}}</el-button>
<el-button type="plain" size="small" @click="isAddSpiderVisible = false">{{ $t('Cancel') }}</el-button>
<el-button
type="primary"
size="small"
@click="onAddSpiderConfirm"
:icon="isAddSpiderLoading ? 'el-icon-loading' : ''"
:disabled="isAddSpiderLoading"
@click="onAddSpiderConfirm"
>
{{$t('Confirm')}}
{{ $t('Confirm') }}
</el-button>
</template>
</el-dialog>
@@ -119,16 +119,7 @@
>
<!--settings-->
<el-tab-pane :label="$t('Settings')" name="settings">
<div v-if="!spiderScrapySettings || !spiderScrapySettings.length" class="settings">
<span class="empty-text">
{{$t('No data available')}}
</span>
<template v-if="spiderScrapyErrors.settings">
<label class="errors-label">{{$t('Errors')}}:</label>
<el-alert type="error" v-html="getScrapyErrors('settings')"/>
</template>
</div>
<div v-else class="settings">
<div class="settings">
<div class="top-action-wrapper">
<el-button
type="primary"
@@ -136,10 +127,10 @@
icon="el-icon-plus"
@click="onSettingsAdd"
>
{{$t('Add Variable')}}
{{ $t('Add Variable') }}
</el-button>
<el-button size="small" type="success" @click="onSettingsSave" icon="el-icon-check">
{{$t('Save')}}
<el-button size="small" type="success" icon="el-icon-check" @click="onSettingsSave">
{{ $t('Save') }}
</el-button>
</div>
<el-table
@@ -188,8 +179,8 @@
/>
<el-input
v-else-if="scope.row.type === 'number'"
type="number"
v-model="scope.row.value"
type="number"
size="small"
suffix-icon="el-icon-edit"
@change="scope.row.value = Number(scope.row.value)"
@@ -208,7 +199,7 @@
v-else
style="margin-left: 10px;font-size: 12px"
>
{{JSON.stringify(scope.row.value)}}
{{ JSON.stringify(scope.row.value) }}
<el-button
type="warning"
size="mini"
@@ -241,16 +232,7 @@
<!--spiders-->
<el-tab-pane :label="$t('Spiders')" name="spiders">
<div v-if="!spiderForm.spider_names || !spiderForm.spider_names.length" class="spiders">
<span class="empty-text error">
{{$t('No data available. Please check whether your spiders are missing dependencies or no spiders created.')}}
</span>
<template v-if="spiderScrapyErrors.spiders">
<label class="errors-label">{{$t('Errors')}}:</label>
<el-alert type="error" v-html="getScrapyErrors('spiders')"/>
</template>
</div>
<div v-else class="spiders">
<div class="spiders">
<div class="action-wrapper">
<el-button
type="primary"
@@ -258,7 +240,7 @@
icon="el-icon-plus"
@click="onAddSpider"
>
{{$t('Add Spider')}}
{{ $t('Add Spider') }}
</el-button>
</div>
<ul class="list">
@@ -268,9 +250,9 @@
class="item"
@click="onClickSpider(s)"
>
<i class="el-icon-star-on"></i>
{{s}}
<i v-if="loadingDict[s]" class="el-icon-loading"></i>
<i class="el-icon-star-on"/>
{{ s }}
<i v-if="loadingDict[s]" class="el-icon-loading"/>
</li>
</ul>
</div>
@@ -279,16 +261,7 @@
<!--items-->
<el-tab-pane label="Items" name="items">
<div v-if="!spiderScrapyItems || !spiderScrapyItems.length" class="items">
<span class="empty-text">
{{$t('No data available')}}
</span>
<template v-if="spiderScrapyErrors.items">
<label class="errors-label">{{$t('Errors')}}:</label>
<el-alert type="error" v-html="getScrapyErrors('items')"/>
</template>
</div>
<div v-else class="items">
<div class="items">
<div class="action-wrapper">
<el-button
type="primary"
@@ -296,75 +269,78 @@
icon="el-icon-plus"
@click="onAddItem"
>
{{$t('Add Item')}}
{{ $t('Add Item') }}
</el-button>
<el-button size="small" type="success" @click="onItemsSave" icon="el-icon-check">
{{$t('Save')}}
<el-button size="small" type="success" icon="el-icon-check" @click="onItemsSave">
{{ $t('Save') }}
</el-button>
</div>
<el-tree
:data="spiderScrapyItems"
default-expand-all
>
<span class="custom-tree-node" :class="`level-${data.level}`" slot-scope="{ node, data }">
<template v-if="data.level === 1">
<span v-if="!node.isEdit" class="label" @click="onItemLabelEdit(node, data, $event)">
<i class="el-icon-star-on"></i>
{{ data.label }}
<i class="el-icon-edit"></i>
</span>
<el-input
v-else
:ref="`el-input-${data.id}`"
:placeholder="$t('Item Name')"
v-model="data.name"
size="mini"
@change="onItemChange(node, data, $event)"
@blur="$set(node, 'isEdit', false)"
/>
<span>
<el-button
type="primary"
<span slot-scope="{ node, data }" class="custom-tree-node" :class="`level-${data.level}`">
<template v-if="data.level === 1">
<span v-if="!node.isEdit" class="label" @click="onItemLabelEdit(node, data, $event)">
<i class="el-icon-star-on"/>
{{ data.label }}
<i class="el-icon-edit"/>
</span>
<el-input
v-else
:ref="`el-input-${data.id}`"
v-model="data.name"
:placeholder="$t('Item Name')"
size="mini"
icon="el-icon-plus"
@click="onAddItemField(data, $event)">
{{$t('Add Field')}}
</el-button>
<el-button
type="danger"
@change="onItemChange(node, data, $event)"
@blur="$set(node, 'isEdit', false)"
/>
<span>
<el-button
type="primary"
size="mini"
icon="el-icon-plus"
@click="onAddItemField(data, $event)"
>
{{ $t('Add Field') }}
</el-button>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="removeItem(data, $event)"
>
{{ $t('Remove') }}
</el-button>
</span>
</template>
<template v-if="data.level === 2">
<span v-if="!node.isEdit" class="label" @click="onItemLabelEdit(node, data, $event)">
<i class="el-icon-arrow-right"/>
{{ node.label }}
<i class="el-icon-edit"/>
</span>
<el-input
v-else
:ref="`el-input-${data.id}`"
v-model="data.name"
:placeholder="$t('Field Name')"
size="mini"
icon="el-icon-delete"
@click="removeItem(data, $event)">
{{$t('Remove')}}
</el-button>
</span>
</template>
<template v-if="data.level === 2">
<span v-if="!node.isEdit" class="label" @click="onItemLabelEdit(node, data, $event)">
<i class="el-icon-arrow-right"></i>
{{ node.label }}
<i class="el-icon-edit"></i>
</span>
<el-input
v-else
:ref="`el-input-${data.id}`"
:placeholder="$t('Field Name')"
v-model="data.name"
size="mini"
@change="onItemFieldChange(node, data, $event)"
@blur="node.isEdit = false"
/>
<span>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="onRemoveItemField(node, data, $event)">
{{$t('Remove')}}
</el-button>
</span>
</template>
</span>
@change="onItemFieldChange(node, data, $event)"
@blur="node.isEdit = false"
/>
<span>
<el-button
type="danger"
size="mini"
icon="el-icon-delete"
@click="onRemoveItemField(node, data, $event)"
>
{{ $t('Remove') }}
</el-button>
</span>
</template>
</span>
</el-tree>
</div>
</el-tab-pane>
@@ -372,15 +348,6 @@
<!--pipelines-->
<el-tab-pane label="Pipelines" name="pipelines">
<div v-if="!spiderScrapyPipelines || !spiderScrapyPipelines.length" class="pipelines">
<span class="empty-text">
{{$t('No data available')}}
</span>
<template v-if="spiderScrapyErrors.pipelines">
<label class="errors-label">{{$t('Errors')}}:</label>
<el-alert type="error" v-html="getScrapyErrors('pipelines')"/>
</template>
</div>
<div class="pipelines">
<ul class="list">
<li
@@ -389,8 +356,8 @@
class="item"
@click="$emit('click-pipeline')"
>
<i class="el-icon-star-on"></i>
{{s}}
<i class="el-icon-star-on"/>
{{ s }}
</li>
</ul>
</div>
@@ -401,300 +368,295 @@
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'SpiderScrapy',
computed: {
...mapState('spider', [
'spiderForm',
'spiderScrapySettings',
'spiderScrapyItems',
'spiderScrapyPipelines',
'spiderScrapyErrors'
]),
activeParamData () {
if (this.activeParam.type === 'array') {
return this.activeParam.value.map(s => {
return { value: s }
})
} else if (this.activeParam.type === 'object') {
return Object.keys(this.activeParam.value).map(key => {
return {
key,
value: this.activeParam.value[key]
}
})
export default {
name: 'SpiderScrapy',
data() {
return {
dialogVisible: false,
activeParam: {},
activeParamIndex: undefined,
isAddSpiderVisible: false,
addSpiderForm: {
name: '',
domain: '',
template: 'basic'
},
isAddSpiderLoading: false,
activeTabName: 'settings',
loadingDict: {}
}
return []
}
},
data () {
return {
dialogVisible: false,
activeParam: {},
activeParamIndex: undefined,
isAddSpiderVisible: false,
addSpiderForm: {
name: '',
domain: '',
template: 'basic'
},
isAddSpiderLoading: false,
activeTabName: 'settings',
loadingDict: {}
}
},
methods: {
onOpenDialog () {
this.dialogVisible = true
},
onCloseDialog () {
this.dialogVisible = false
},
onSettingsConfirm () {
if (this.activeParam.type === 'array') {
this.activeParam.value = this.activeParamData.map(d => d.value)
} else if (this.activeParam.type === 'object') {
const dict = {}
this.activeParamData.forEach(d => {
dict[d.key] = d.value
})
this.activeParam.value = dict
}
this.$set(this.spiderScrapySettings, this.activeParamIndex, JSON.parse(JSON.stringify(this.activeParam)))
this.dialogVisible = false
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '确认编辑参数')
},
onSettingsEditParam (row, index) {
this.activeParam = JSON.parse(JSON.stringify(row))
this.activeParamIndex = index
this.onOpenDialog()
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '点击编辑参数')
},
async onSettingsSave () {
const res = await this.$store.dispatch('spider/saveSpiderScrapySettings', this.$route.params.id)
if (!res.data.error) {
this.$message.success(this.$t('Saved successfully'))
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '保存设置')
},
onSettingsAdd () {
const data = JSON.parse(JSON.stringify(this.spiderScrapySettings))
data.push({
key: '',
value: '',
type: 'string'
})
this.$store.commit('spider/SET_SPIDER_SCRAPY_SETTINGS', data)
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '添加参数')
},
onSettingsRemove (index) {
const data = JSON.parse(JSON.stringify(this.spiderScrapySettings))
data.splice(index, 1)
this.$store.commit('spider/SET_SPIDER_SCRAPY_SETTINGS', data)
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '删除参数')
},
onSettingsActiveParamAdd () {
if (this.activeParam.type === 'array') {
this.activeParam.value.push('')
} else if (this.activeParam.type === 'object') {
if (!this.activeParam.value) {
this.activeParam.value = {}
computed: {
...mapState('spider', [
'spiderForm',
'spiderScrapySettings',
'spiderScrapyItems',
'spiderScrapyPipelines'
]),
activeParamData() {
if (this.activeParam.type === 'array') {
return this.activeParam.value.map(s => {
return { value: s }
})
} else if (this.activeParam.type === 'object') {
return Object.keys(this.activeParam.value).map(key => {
return {
key,
value: this.activeParam.value[key]
}
})
}
this.$set(this.activeParam.value, '', 999)
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '添加参数中参数')
},
onSettingsActiveParamRemove (index) {
if (this.activeParam.type === 'array') {
this.activeParam.value.splice(index, 1)
} else if (this.activeParam.type === 'object') {
const key = this.activeParamData[index].key
const value = JSON.parse(JSON.stringify(this.activeParam.value))
delete value[key]
this.$set(this.activeParam, 'value', value)
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '删除参数中参数')
},
settingsKeysFetchSuggestions (queryString, cb) {
const data = this.$utils.scrapy.settingParamNames
.filter(s => {
if (!queryString) return true
return !!s.match(new RegExp(queryString, 'i'))
})
.map(s => {
return {
value: s,
label: s
}
})
.sort((a, b) => {
return a > b ? -1 : 1
})
cb(data)
},
onSettingsParamTypeChange (row) {
if (row.type === 'number') {
row.value = Number(row.value)
return []
}
},
onAddSpiderConfirm () {
this.$refs['add-spider-form'].validate(async valid => {
if (!valid) return
this.isAddSpiderLoading = true
const res = await this.$store.dispatch('spider/addSpiderScrapySpider', {
id: this.$route.params.id,
form: this.addSpiderForm
})
methods: {
onOpenDialog() {
this.dialogVisible = true
},
onCloseDialog() {
this.dialogVisible = false
},
onSettingsConfirm() {
if (this.activeParam.type === 'array') {
this.activeParam.value = this.activeParamData.map(d => d.value)
} else if (this.activeParam.type === 'object') {
const dict = {}
this.activeParamData.forEach(d => {
dict[d.key] = d.value
})
this.activeParam.value = dict
}
this.$set(this.spiderScrapySettings, this.activeParamIndex, JSON.parse(JSON.stringify(this.activeParam)))
this.dialogVisible = false
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '确认编辑参数')
},
onSettingsEditParam(row, index) {
this.activeParam = JSON.parse(JSON.stringify(row))
this.activeParamIndex = index
this.onOpenDialog()
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '点击编辑参数')
},
async onSettingsSave() {
const res = await this.$store.dispatch('spider/saveSpiderScrapySettings', this.$route.params.id)
if (!res.data.error) {
this.$message.success(this.$t('Saved successfully'))
}
this.isAddSpiderVisible = false
this.isAddSpiderLoading = false
await this.$store.dispatch('spider/getSpiderScrapySpiders', this.$route.params.id)
})
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '确认添加爬虫')
},
onAddSpider () {
this.addSpiderForm = {
name: '',
domain: '',
template: 'basic'
}
this.isAddSpiderVisible = true
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '添加爬虫')
},
getMaxItemNodeId () {
let max = 0
this.spiderScrapyItems.forEach(d => {
if (max < d.id) max = d.id
d.children.forEach(f => {
if (max < f.id) max = f.id
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '保存设置')
},
onSettingsAdd() {
const data = JSON.parse(JSON.stringify(this.spiderScrapySettings))
data.push({
key: '',
value: '',
type: 'string'
})
})
return max
},
onAddItem () {
const maxId = this.getMaxItemNodeId()
this.spiderScrapyItems.push({
id: maxId + 1,
label: `Item_${+new Date()}`,
level: 1,
children: [
{
id: maxId + 2,
level: 2,
label: `field_${+new Date()}`
this.$store.commit('spider/SET_SPIDER_SCRAPY_SETTINGS', data)
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '添加参数')
},
onSettingsRemove(index) {
const data = JSON.parse(JSON.stringify(this.spiderScrapySettings))
data.splice(index, 1)
this.$store.commit('spider/SET_SPIDER_SCRAPY_SETTINGS', data)
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '删除参数')
},
onSettingsActiveParamAdd() {
if (this.activeParam.type === 'array') {
this.activeParam.value.push('')
} else if (this.activeParam.type === 'object') {
if (!this.activeParam.value) {
this.activeParam.value = {}
}
]
})
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '添加Item')
},
removeItem (data, ev) {
ev.stopPropagation()
for (let i = 0; i < this.spiderScrapyItems.length; i++) {
const item = this.spiderScrapyItems[i]
if (item.id === data.id) {
this.spiderScrapyItems.splice(i, 1)
break
this.$set(this.activeParam.value, '', 999)
}
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '删除Item')
},
onAddItemField (data, ev) {
ev.stopPropagation()
for (let i = 0; i < this.spiderScrapyItems.length; i++) {
const item = this.spiderScrapyItems[i]
if (item.id === data.id) {
item.children.push({
id: this.getMaxItemNodeId() + 1,
level: 2,
label: `field_${+new Date()}`
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '添加参数中参数')
},
onSettingsActiveParamRemove(index) {
if (this.activeParam.type === 'array') {
this.activeParam.value.splice(index, 1)
} else if (this.activeParam.type === 'object') {
const key = this.activeParamData[index].key
const value = JSON.parse(JSON.stringify(this.activeParam.value))
delete value[key]
this.$set(this.activeParam, 'value', value)
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '删除参数中参数')
},
settingsKeysFetchSuggestions(queryString, cb) {
const data = this.$utils.scrapy.settingParamNames
.filter(s => {
if (!queryString) return true
return !!s.match(new RegExp(queryString, 'i'))
})
break
}
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '添加Items字段')
},
onRemoveItemField (node, data, ev) {
ev.stopPropagation()
for (let i = 0; i < this.spiderScrapyItems.length; i++) {
const item = this.spiderScrapyItems[i]
if (item.id === node.parent.data.id) {
for (let j = 0; j < item.children.length; j++) {
const field = item.children[j]
if (field.id === data.id) {
item.children.splice(j, 1)
break
.map(s => {
return {
value: s,
label: s
}
})
.sort((a, b) => {
return a > b ? -1 : 1
})
cb(data)
},
onSettingsParamTypeChange(row) {
if (row.type === 'number') {
row.value = Number(row.value)
}
},
onAddSpiderConfirm() {
this.$refs['add-spider-form'].validate(async valid => {
if (!valid) return
this.isAddSpiderLoading = true
const res = await this.$store.dispatch('spider/addSpiderScrapySpider', {
id: this.$route.params.id,
form: this.addSpiderForm
})
if (!res.data.error) {
this.$message.success(this.$t('Saved successfully'))
}
break
}
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '删除Items字段')
},
onItemLabelEdit (node, data, ev) {
ev.stopPropagation()
this.$set(node, 'isEdit', true)
this.$set(data, 'name', node.label)
setTimeout(() => {
this.$refs[`el-input-${data.id}`].focus()
}, 0)
},
onItemChange (node, data, value) {
for (let i = 0; i < this.spiderScrapyItems.length; i++) {
const item = this.spiderScrapyItems[i]
if (item.id === data.id) {
item.label = value
break
}
}
},
onItemFieldChange (node, data, value) {
for (let i = 0; i < this.spiderScrapyItems.length; i++) {
const item = this.spiderScrapyItems[i]
if (item.id === node.parent.data.id) {
for (let j = 0; j < item.children.length; j++) {
const field = item.children[j]
if (field.id === data.id) {
item.children[j].label = value
break
}
}
break
}
}
},
async onItemsSave () {
const res = await this.$store.dispatch('spider/saveSpiderScrapyItems', this.$route.params.id)
if (!res.data.error) {
this.$message.success(this.$t('Saved successfully'))
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '保存Items')
},
async onClickSpider (spiderName) {
if (this.loadingDict[spiderName]) return
this.$set(this.loadingDict, spiderName, true)
try {
const res = await this.$store.dispatch('spider/getSpiderScrapySpiderFilepath', {
id: this.$route.params.id,
spiderName
this.isAddSpiderVisible = false
this.isAddSpiderLoading = false
await this.$store.dispatch('spider/getSpiderScrapySpiders', this.$route.params.id)
})
this.$emit('click-spider', res.data.data)
} finally {
this.$set(this.loadingDict, spiderName, false)
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '确认添加爬虫')
},
onAddSpider() {
this.addSpiderForm = {
name: '',
domain: '',
template: 'basic'
}
this.isAddSpiderVisible = true
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '添加爬虫')
},
getMaxItemNodeId() {
let max = 0
this.spiderScrapyItems.forEach(d => {
if (max < d.id) max = d.id
d.children.forEach(f => {
if (max < f.id) max = f.id
})
})
return max
},
onAddItem() {
const maxId = this.getMaxItemNodeId()
this.spiderScrapyItems.push({
id: maxId + 1,
label: `Item_${+new Date()}`,
level: 1,
children: [
{
id: maxId + 2,
level: 2,
label: `field_${+new Date()}`
}
]
})
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '添加Item')
},
removeItem(data, ev) {
ev.stopPropagation()
for (let i = 0; i < this.spiderScrapyItems.length; i++) {
const item = this.spiderScrapyItems[i]
if (item.id === data.id) {
this.spiderScrapyItems.splice(i, 1)
break
}
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '删除Item')
},
onAddItemField(data, ev) {
ev.stopPropagation()
for (let i = 0; i < this.spiderScrapyItems.length; i++) {
const item = this.spiderScrapyItems[i]
if (item.id === data.id) {
item.children.push({
id: this.getMaxItemNodeId() + 1,
level: 2,
label: `field_${+new Date()}`
})
break
}
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '添加Items字段')
},
onRemoveItemField(node, data, ev) {
ev.stopPropagation()
for (let i = 0; i < this.spiderScrapyItems.length; i++) {
const item = this.spiderScrapyItems[i]
if (item.id === node.parent.data.id) {
for (let j = 0; j < item.children.length; j++) {
const field = item.children[j]
if (field.id === data.id) {
item.children.splice(j, 1)
break
}
}
break
}
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '删除Items字段')
},
onItemLabelEdit(node, data, ev) {
ev.stopPropagation()
this.$set(node, 'isEdit', true)
this.$set(data, 'name', node.label)
setTimeout(() => {
this.$refs[`el-input-${data.id}`].focus()
}, 0)
},
onItemChange(node, data, value) {
for (let i = 0; i < this.spiderScrapyItems.length; i++) {
const item = this.spiderScrapyItems[i]
if (item.id === data.id) {
item.label = value
break
}
}
},
onItemFieldChange(node, data, value) {
for (let i = 0; i < this.spiderScrapyItems.length; i++) {
const item = this.spiderScrapyItems[i]
if (item.id === node.parent.data.id) {
for (let j = 0; j < item.children.length; j++) {
const field = item.children[j]
if (field.id === data.id) {
item.children[j].label = value
break
}
}
break
}
}
},
async onItemsSave() {
const res = await this.$store.dispatch('spider/saveSpiderScrapyItems', this.$route.params.id)
if (!res.data.error) {
this.$message.success(this.$t('Saved successfully'))
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '保存Items')
},
async onClickSpider(spiderName) {
if (this.loadingDict[spiderName]) return
this.$set(this.loadingDict, spiderName, true)
try {
const res = await this.$store.dispatch('spider/getSpiderScrapySpiderFilepath', {
id: this.$route.params.id,
spiderName
})
this.$emit('click-spider', res.data.data)
} finally {
this.$set(this.loadingDict, spiderName, false)
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '点击爬虫')
}
this.$st.sendEv('爬虫详情', 'Scrapy 设置', '点击爬虫')
},
getScrapyErrors (type) {
if (!this.spiderScrapyErrors || !this.spiderScrapyErrors[type] || (typeof this.spiderScrapyErrors[type] !== 'string')) return ''
return this.$utils.html.htmlEscape(this.spiderScrapyErrors[type]).split('\n').join('<br/>')
}
}
}
</script>
<style scoped>
@@ -821,19 +783,4 @@ export default {
.items >>> .custom-tree-node .el-input {
width: 240px;
}
.empty-text {
display: block;
margin-bottom: 20px;
}
.empty-text.error {
color: #f56c6c;
}
.errors-label {
color: #f56c6c;
display: block;
margin-bottom: 10px;
}
</style>

View File

@@ -5,38 +5,38 @@
</template>
<script>
import screenfull from 'screenfull'
import screenfull from 'screenfull'
export default {
name: 'Screenfull',
data() {
return {
isFullscreen: false
}
},
mounted() {
this.init()
},
methods: {
click() {
if (!screenfull.enabled) {
this.$message({
message: 'you browser can not work',
type: 'warning'
})
return false
export default {
name: 'Screenfull',
data() {
return {
isFullscreen: false
}
screenfull.toggle()
},
init() {
if (screenfull.enabled) {
screenfull.on('change', () => {
this.isFullscreen = screenfull.isFullscreen
})
mounted() {
this.init()
},
methods: {
click() {
if (!screenfull.enabled) {
this.$message({
message: 'you browser can not work',
type: 'warning'
})
return false
}
screenfull.toggle()
},
init() {
if (screenfull.enabled) {
screenfull.on('change', () => {
this.isFullscreen = screenfull.isFullscreen
})
}
}
}
}
}
</script>
<style scoped>

View File

@@ -1,64 +1,64 @@
<template>
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
<slot/>
<slot />
</el-scrollbar>
</template>
<script>
const tagAndTagSpacing = 4 // tagAndTagSpacing
const tagAndTagSpacing = 4 // tagAndTagSpacing
export default {
name: 'ScrollPane',
data () {
return {
left: 0
}
},
methods: {
handleScroll (e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.$refs.scrollContainer.$refs.wrap
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
},
moveToTarget (currentTag) {
const $container = this.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.$refs.scrollContainer.$refs.wrap
const tagList = this.$parent.$refs.tag
let firstTag = null
let lastTag = null
// find first tag and last tag
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
export default {
name: 'ScrollPane',
data() {
return {
left: 0
}
},
methods: {
handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.$refs.scrollContainer.$refs.wrap
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
},
moveToTarget(currentTag) {
const $container = this.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.$refs.scrollContainer.$refs.wrap
const tagList = this.$parent.$refs.tag
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
// find preTag and nextTag
const currentIndex = tagList.findIndex(item => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
let firstTag = null
let lastTag = null
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
// find first tag and last tag
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
// find preTag and nextTag
const currentIndex = tagList.findIndex(item => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
}
}
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>

View File

@@ -1,87 +1,87 @@
<template>
<div class="log-item" :style="style" :class="`log-item-${index} ${active ? 'active' : ''}`">
<div class="line-no">{{index}}</div>
<div class="line-no">{{ index }}</div>
<div class="line-content">
<span v-if="isLogEnd" style="color: #E6A23C">
<span class="loading-text">{{$t('Updating log...')}}</span>
<i class="el-icon-loading"></i>
<span class="loading-text">{{ $t('Updating log...') }}</span>
<i class="el-icon-loading" />
</span>
<span v-else-if="isAnsi" v-html="dataHtml"></span>
<span v-else v-html="dataHtml"></span>
<span v-else-if="isAnsi" v-html="dataHtml" />
<span v-else v-html="dataHtml" />
</div>
</div>
</template>
<script>
import {
mapGetters
} from 'vuex'
import {
mapGetters
} from 'vuex'
export default {
name: 'LogItem',
props: {
index: {
type: Number,
default: 1
},
logItem: {
type: Object,
default () {
return {}
export default {
name: 'LogItem',
props: {
index: {
type: Number,
default: 1
},
logItem: {
type: Object,
default() {
return {}
}
},
data: {
type: String,
default: ''
},
isAnsi: {
type: Boolean,
default: false
},
searchString: {
type: String,
default: ''
},
active: {
type: Boolean,
default: false
}
},
data: {
type: String,
default: ''
},
isAnsi: {
type: Boolean,
default: false
},
searchString: {
type: String,
default: ''
},
active: {
type: Boolean,
default: false
}
},
data () {
return {
}
},
computed: {
...mapGetters('user', [
'userInfo'
]),
errorRegex () {
if (!this.userInfo.setting.error_regex_pattern) {
return this.$utils.log.errorRegex
}
console.log(this.userInfo.setting.error_regex_pattern)
return new RegExp(this.userInfo.setting.error_regex_pattern, 'i')
},
dataHtml () {
let html = this.data.replace(this.errorRegex, ' <span style="font-weight: bolder; text-decoration: underline">$1</span> ')
if (!this.searchString) return html
html = html.replace(new RegExp(`(${this.searchString})`, 'gi'), '<mark>$1</mark>')
return html
},
style () {
let color = ''
if (this.data.match(this.errorRegex)) {
color = '#F56C6C'
}
data() {
return {
color
}
},
isLogEnd () {
return this.data === '###LOG_END###'
computed: {
...mapGetters('user', [
'userInfo'
]),
errorRegex() {
if (!this.userInfo.setting.error_regex_pattern) {
return this.$utils.log.errorRegex
}
console.log(this.userInfo.setting.error_regex_pattern)
return new RegExp(this.userInfo.setting.error_regex_pattern, 'i')
},
dataHtml() {
let html = this.data.replace(this.errorRegex, ' <span style="font-weight: bolder; text-decoration: underline">$1</span> ')
if (!this.searchString) return html
html = html.replace(new RegExp(`(${this.searchString})`, 'gi'), '<mark>$1</mark>')
return html
},
style() {
let color = ''
if (this.data.match(this.errorRegex)) {
color = '#F56C6C'
}
return {
color
}
},
isLogEnd() {
return this.data === '###LOG_END###'
}
}
}
}
</script>
<style scoped>

View File

@@ -6,8 +6,7 @@
v-model="isLogAutoScroll"
:inactive-text="$t('Auto-Scroll')"
style="margin-right: 10px"
>
</el-switch>
/>
<!-- <el-switch-->
<!-- v-model="isLogAutoFetch"-->
<!-- :inactive-text="$t('Auto-Refresh')"-->
@@ -28,7 +27,7 @@
icon="el-icon-search"
@click="onSearchLog"
>
{{$t('Search Log')}}
{{ $t('Search Log') }}
</el-button>
</div>
<div class="right">
@@ -51,7 +50,7 @@
icon="el-icon-warning-outline"
@click="toggleErrors"
>
{{$t('Error Count')}}
{{ $t('Error Count') }}
</el-button>
</el-badge>
</div>
@@ -63,8 +62,8 @@
:class="isErrorsCollapsed ? 'errors-collapsed' : ''"
>
<virtual-list
class="log-view"
ref="log-view"
class="log-view"
:start="currentLogIndex - 1"
:offset="0"
:size="18"
@@ -90,7 +89,7 @@
@click="onClickError(item)"
>
<span class="line-content">
{{item.msg}}
{{ item.msg }}
</span>
</li>
</ul>
@@ -100,198 +99,198 @@
</template>
<script>
import {
mapState,
mapGetters
} from 'vuex'
import VirtualList from 'vue-virtual-scroll-list'
import Convert from 'ansi-to-html'
import hasAnsi from 'has-ansi'
import {
mapState,
mapGetters
} from 'vuex'
import VirtualList from 'vue-virtual-scroll-list'
import Convert from 'ansi-to-html'
import hasAnsi from 'has-ansi'
import LogItem from './LogItem'
import LogItem from './LogItem'
const convert = new Convert()
export default {
name: 'LogView',
components: {
VirtualList
},
props: {
data: {
type: String,
default: ''
}
},
data () {
return {
item: LogItem,
searchString: '',
isScrolling: false,
isScrolling2nd: false,
errorRegex: this.$utils.log.errorRegex,
currentOffset: 0,
isErrorsCollapsed: true,
isErrorCollapsing: false
}
},
computed: {
...mapState('task', [
'taskForm',
'taskLogTotal',
'logKeyword',
'isLogFetchLoading',
'errorLogData'
]),
...mapGetters('task', [
'logData'
]),
currentLogIndex: {
get () {
return this.$store.state.task.currentLogIndex
},
set (value) {
this.$store.commit('task/SET_CURRENT_LOG_INDEX', value)
const convert = new Convert()
export default {
name: 'LogView',
components: {
VirtualList
},
props: {
data: {
type: String,
default: ''
}
},
logKeyword: {
get () {
return this.$store.state.task.logKeyword
},
set (value) {
this.$store.commit('task/SET_LOG_KEYWORD', value)
}
},
taskLogPage: {
get () {
return this.$store.state.task.taskLogPage
},
set (value) {
this.$store.commit('task/SET_TASK_LOG_PAGE', value)
}
},
taskLogPageSize: {
get () {
return this.$store.state.task.taskLogPageSize
},
set (value) {
this.$store.commit('task/SET_TASK_LOG_PAGE_SIZE', value)
}
},
isLogAutoScroll: {
get () {
return this.$store.state.task.isLogAutoScroll
},
set (value) {
this.$store.commit('task/SET_IS_LOG_AUTO_SCROLL', value)
}
},
isLogAutoFetch: {
get () {
return this.$store.state.task.isLogAutoFetch
},
set (value) {
this.$store.commit('task/SET_IS_LOG_AUTO_FETCH', value)
}
},
isLogFetchLoading: {
get () {
return this.$store.state.task.isLogFetchLoading
},
set (value) {
this.$store.commit('task/SET_IS_LOG_FETCH_LOADING', value)
}
},
filteredLogData () {
return this.logData.filter(d => {
if (!this.searchString) return true
return !!d.data.toLowerCase().match(this.searchString.toLowerCase())
})
},
remainSize () {
const height = document.querySelector('body').clientHeight
return (height - 240) / 18
}
},
watch: {
taskLogPage () {
this.$emit('search')
this.$st.sendEv('任务详情', '日志', '改变页数')
},
taskLogPageSize () {
this.$emit('search')
this.$st.sendEv('任务详情', '日志', '改变日志每页条数')
},
isLogAutoScroll () {
if (this.isLogAutoScroll) {
this.$store.dispatch('task/getTaskLog', {
id: this.$route.params.id,
keyword: this.logKeyword
}).then(() => {
this.toBottom()
})
this.$st.sendEv('任务详情', '日志', '点击自动滚动')
} else {
this.$st.sendEv('任务详情', '日志', '取消自动滚动')
}
}
},
methods: {
getItemProps (index) {
const logItem = this.filteredLogData[index]
const isAnsi = hasAnsi(logItem.data)
data() {
return {
// <item/> will render with itemProps.
// https://vuejs.org/v2/guide/render-function.html#createElement-Arguments
props: {
index: logItem.index,
logItem,
data: isAnsi ? convert.toHtml(logItem.data) : logItem.data,
searchString: this.logKeyword,
active: logItem.active,
isAnsi
item: LogItem,
searchString: '',
isScrolling: false,
isScrolling2nd: false,
errorRegex: this.$utils.log.errorRegex,
currentOffset: 0,
isErrorsCollapsed: true,
isErrorCollapsing: false
}
},
computed: {
...mapState('task', [
'taskForm',
'taskLogTotal',
'logKeyword',
'isLogFetchLoading',
'errorLogData'
]),
...mapGetters('task', [
'logData'
]),
currentLogIndex: {
get() {
return this.$store.state.task.currentLogIndex
},
set(value) {
this.$store.commit('task/SET_CURRENT_LOG_INDEX', value)
}
},
logKeyword: {
get() {
return this.$store.state.task.logKeyword
},
set(value) {
this.$store.commit('task/SET_LOG_KEYWORD', value)
}
},
taskLogPage: {
get() {
return this.$store.state.task.taskLogPage
},
set(value) {
this.$store.commit('task/SET_TASK_LOG_PAGE', value)
}
},
taskLogPageSize: {
get() {
return this.$store.state.task.taskLogPageSize
},
set(value) {
this.$store.commit('task/SET_TASK_LOG_PAGE_SIZE', value)
}
},
isLogAutoScroll: {
get() {
return this.$store.state.task.isLogAutoScroll
},
set(value) {
this.$store.commit('task/SET_IS_LOG_AUTO_SCROLL', value)
}
},
isLogAutoFetch: {
get() {
return this.$store.state.task.isLogAutoFetch
},
set(value) {
this.$store.commit('task/SET_IS_LOG_AUTO_FETCH', value)
}
},
isLogFetchLoading: {
get() {
return this.$store.state.task.isLogFetchLoading
},
set(value) {
this.$store.commit('task/SET_IS_LOG_FETCH_LOADING', value)
}
},
filteredLogData() {
return this.logData.filter(d => {
if (!this.searchString) return true
return !!d.data.toLowerCase().match(this.searchString.toLowerCase())
})
},
remainSize() {
const height = document.querySelector('body').clientHeight
return (height - 240) / 18
}
},
watch: {
taskLogPage() {
this.$emit('search')
this.$st.sendEv('任务详情', '日志', '改变页数')
},
taskLogPageSize() {
this.$emit('search')
this.$st.sendEv('任务详情', '日志', '改变日志每页条数')
},
isLogAutoScroll() {
if (this.isLogAutoScroll) {
this.$store.dispatch('task/getTaskLog', {
id: this.$route.params.id,
keyword: this.logKeyword
}).then(() => {
this.toBottom()
})
this.$st.sendEv('任务详情', '日志', '点击自动滚动')
} else {
this.$st.sendEv('任务详情', '日志', '取消自动滚动')
}
}
},
onToBottom () {
mounted() {
this.currentLogIndex = 0
this.handle = setInterval(() => {
if (this.isLogAutoScroll) {
this.toBottom()
}
}, 200)
},
onScroll () {
destroyed() {
clearInterval(this.handle)
},
toBottom () {
this.$el.querySelector('.log-view').scrollTo({ top: 99999999 })
},
toggleErrors () {
this.isErrorsCollapsed = !this.isErrorsCollapsed
this.isErrorCollapsing = true
setTimeout(() => {
this.isErrorCollapsing = false
}, 300)
},
async onClickError (item) {
const page = Math.ceil(item.seq / this.taskLogPageSize)
this.$store.commit('task/SET_LOG_KEYWORD', '')
this.$store.commit('task/SET_TASK_LOG_PAGE', page)
this.$store.commit('task/SET_IS_LOG_AUTO_SCROLL', false)
this.$store.commit('task/SET_ACTIVE_ERROR_LOG_ITEM', item)
this.$emit('search')
this.$st.sendEv('任务详情', '日志', '点击错误日志')
},
onSearchLog () {
this.$emit('search')
this.$st.sendEv('任务详情', '日志', '搜索日志')
}
},
mounted () {
this.currentLogIndex = 0
this.handle = setInterval(() => {
if (this.isLogAutoScroll) {
this.toBottom()
methods: {
getItemProps(index) {
const logItem = this.filteredLogData[index]
const isAnsi = hasAnsi(logItem.data)
return {
// <item/> will render with itemProps.
// https://vuejs.org/v2/guide/render-function.html#createElement-Arguments
props: {
index: logItem.index,
logItem,
data: isAnsi ? convert.toHtml(logItem.data) : logItem.data,
searchString: this.logKeyword,
active: logItem.active,
isAnsi
}
}
},
onToBottom() {
},
onScroll() {
},
toBottom() {
this.$el.querySelector('.log-view').scrollTo({ top: 99999999 })
},
toggleErrors() {
this.isErrorsCollapsed = !this.isErrorsCollapsed
this.isErrorCollapsing = true
setTimeout(() => {
this.isErrorCollapsing = false
}, 300)
},
async onClickError(item) {
const page = Math.ceil(item.seq / this.taskLogPageSize)
this.$store.commit('task/SET_LOG_KEYWORD', '')
this.$store.commit('task/SET_TASK_LOG_PAGE', page)
this.$store.commit('task/SET_IS_LOG_AUTO_SCROLL', false)
this.$store.commit('task/SET_ACTIVE_ERROR_LOG_ITEM', item)
this.$emit('search')
this.$st.sendEv('任务详情', '日志', '点击错误日志')
},
onSearchLog() {
this.$emit('search')
this.$st.sendEv('任务详情', '日志', '搜索日志')
}
}, 200)
},
destroyed () {
clearInterval(this.handle)
}
}
}
</script>
<style scoped>

View File

@@ -6,10 +6,10 @@
>
<el-tab-pane :label="$t('Settings')" name="settings">
<el-form
ref="git-settings-form"
class="git-settings-form"
label-width="150px"
:model="spiderForm"
ref="git-settings-form"
>
<el-form-item
:label="$t('Git URL')"
@@ -20,8 +20,7 @@
v-model="spiderForm.git_url"
:placeholder="$t('Git URL')"
@blur="onGitUrlChange"
>
</el-input>
/>
</el-form-item>
<el-form-item
:label="$t('Has Credential')"
@@ -41,8 +40,7 @@
<el-input
v-model="spiderForm.git_username"
:placeholder="$t('Git Username')"
>
</el-input>
/>
</el-form-item>
<el-form-item
v-if="spiderForm.git_has_credential"
@@ -53,8 +51,7 @@
v-model="spiderForm.git_password"
:placeholder="$t('Git Password')"
type="password"
>
</el-input>
/>
</el-form-item>
<el-form-item
:label="$t('Git Branch')"
@@ -111,7 +108,7 @@
type="error"
:closable="false"
>
{{spiderForm.git_sync_error}}
{{ spiderForm.git_sync_error }}
</el-alert>
</el-form-item>
<el-form-item
@@ -122,13 +119,13 @@
type="info"
:closable="false"
>
{{sshPublicKey}}
{{ sshPublicKey }}
</el-alert>
<span class="copy" @click="copySshPublicKey">
<i class="el-icon-copy-document"></i>
{{$t('Copy')}}
</span>
<input id="ssh-public-key" v-model="sshPublicKey" v-show="true">
<i class="el-icon-copy-document" />
{{ $t('Copy') }}
</span>
<input v-show="true" id="ssh-public-key" v-model="sshPublicKey">
</el-form-item>
</el-form>
<div class="action-wrapper">
@@ -139,7 +136,7 @@
:icon="isGitResetLoading ? 'el-icon-loading' : 'el-icon-refresh-left'"
@click="onReset"
>
{{$t('Reset')}}
{{ $t('Reset') }}
</el-button>
<el-button
size="small"
@@ -148,10 +145,10 @@
:disabled="!spiderForm.git_url || isGitSyncLoading"
@click="onSync"
>
{{$t('Sync')}}
{{ $t('Sync') }}
</el-button>
<el-button size="small" type="success" @click="onSave" icon="el-icon-check">
{{$t('Save')}}
<el-button size="small" type="success" icon="el-icon-check" @click="onSave">
{{ $t('Save') }}
</el-button>
</div>
</el-tab-pane>
@@ -168,10 +165,10 @@
<div class="commit">
<div class="row">
<div class="message">
{{c.message}}
{{ c.message }}
</div>
<div class="author">
{{c.author}} ({{c.email}})
{{ c.author }} ({{ c.email }})
</div>
</div>
<div class="row" style="margin-top: 10px">
@@ -181,7 +178,7 @@
type="primary"
size="mini"
>
<i class="fa fa-tag"></i>
<i class="fa fa-tag" />
HEAD
</el-tag>
<el-tag
@@ -190,8 +187,8 @@
:type="b.label === 'master' ? 'danger' : 'warning'"
size="mini"
>
<i class="fa fa-tag"></i>
{{b.label}}
<i class="fa fa-tag" />
{{ b.label }}
</el-tag>
<el-tag
v-for="b in c.remote_branches"
@@ -199,8 +196,8 @@
type="info"
size="mini"
>
<i class="fa fa-tag"></i>
{{b.label}}
<i class="fa fa-tag" />
{{ b.label }}
</el-tag>
<el-tag
v-for="t in c.tags"
@@ -208,8 +205,8 @@
type="success"
size="mini"
>
<i class="fa fa-tag"></i>
{{t.label}}
<i class="fa fa-tag" />
{{ t.label }}
</el-tag>
</div>
<div class="actions">
@@ -232,165 +229,165 @@
</template>
<script>
import dayjs from 'dayjs'
import {
mapState
} from 'vuex'
import dayjs from 'dayjs'
import {
mapState
} from 'vuex'
export default {
name: 'GitSettings',
data () {
return {
gitBranches: [],
isGitBranchesLoading: false,
isGitSyncLoading: false,
isGitResetLoading: false,
isGitCheckoutLoading: false,
syncFrequencies: [
{ label: '1m', value: '0 * * * * *' },
{ label: '5m', value: '0 0/5 * * * *' },
{ label: '15m', value: '0 0/15 * * * *' },
{ label: '30m', value: '0 0/30 * * * *' },
{ label: '1h', value: '0 0 * * * *' },
{ label: '6h', value: '0 0 0/6 * * *' },
{ label: '12h', value: '0 0 0/12 * * *' },
{ label: '1d', value: '0 0 0 0 * *' }
],
sshPublicKey: '',
activeTabName: 'settings',
commits: []
}
},
computed: {
...mapState('spider', [
'spiderForm'
])
},
methods: {
onSave () {
this.$refs['git-settings-form'].validate(async valid => {
if (!valid) return
const res = await this.$store.dispatch('spider/editSpider')
if (!res.data.error) {
this.$message.success(this.$t('Spider info has been saved successfully'))
}
})
this.$st.sendEv('爬虫详情', 'Git 设置', '保存')
},
async onGitUrlChange () {
if (!this.spiderForm.git_url) return
this.isGitBranchesLoading = true
try {
const res = await this.$request.get('/git/branches', { url: this.spiderForm.git_url })
this.gitBranches = res.data.data
if (!this.spiderForm.git_branch && this.gitBranches.length > 0) {
this.$set(this.spiderForm, 'git_branch', this.gitBranches[0])
}
} finally {
this.isGitBranchesLoading = false
export default {
name: 'GitSettings',
data() {
return {
gitBranches: [],
isGitBranchesLoading: false,
isGitSyncLoading: false,
isGitResetLoading: false,
isGitCheckoutLoading: false,
syncFrequencies: [
{ label: '1m', value: '0 * * * * *' },
{ label: '5m', value: '0 0/5 * * * *' },
{ label: '15m', value: '0 0/15 * * * *' },
{ label: '30m', value: '0 0/30 * * * *' },
{ label: '1h', value: '0 0 * * * *' },
{ label: '6h', value: '0 0 0/6 * * *' },
{ label: '12h', value: '0 0 0/12 * * *' },
{ label: '1d', value: '0 0 0 0 * *' }
],
sshPublicKey: '',
activeTabName: 'settings',
commits: []
}
},
async onSync () {
this.isGitSyncLoading = true
try {
const res = await this.$request.post(`/spiders/${this.spiderForm._id}/git/sync`)
if (!res.data.error) {
this.$message.success(this.$t('Git has been synchronized successfully'))
}
} finally {
this.isGitSyncLoading = false
await this.updateGit()
await this.$store.dispatch('spider/getSpiderData', this.$route.params.id)
computed: {
...mapState('spider', [
'spiderForm'
])
},
async created() {
if (this.spiderForm.git_url) {
this.onGitUrlChange()
}
this.$st.sendEv('爬虫详情', 'Git 设置', '同步')
},
onReset () {
this.$confirm(
this.$t('This would delete all files of the spider. Are you sure to continue?'),
this.$t('Notification'),
{
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
})
.then(async () => {
this.isGitResetLoading = true
try {
const res = await this.$request.post(`/spiders/${this.spiderForm._id}/git/reset`)
if (!res.data.error) {
this.$message.success(this.$t('Git has been reset successfully'))
this.$st.sendEv('爬虫详情', 'Git 设置', '确认重置')
}
} finally {
this.isGitResetLoading = false
// await this.updateGit()
}
})
this.$st.sendEv('爬虫详情', 'Git 设置', '点击重置')
},
async getSshPublicKey () {
const res = await this.$request.get('/git/public-key')
this.sshPublicKey = res.data.data
},
copySshPublicKey () {
const el = document.querySelector('#ssh-public-key')
el.focus()
el.setSelectionRange(0, this.sshPublicKey.length)
document.execCommand('copy')
this.$message.success(this.$t('SSH Public Key is copied to the clipboard'))
this.$st.sendEv('爬虫详情', 'Git 设置', '拷贝 SSH 公钥')
},
async getCommits () {
const res = await this.$request.get('/git/commits', { spider_id: this.spiderForm._id })
this.commits = res.data.data.map(d => {
d.ts = dayjs(d.ts).format('YYYY-MM-DD HH:mm:ss')
return d
})
},
async checkout (c) {
this.isGitCheckoutLoading = true
try {
const res = await this.$request.post('/git/checkout', { spider_id: this.spiderForm._id, hash: c.hash })
if (!res.data.error) {
this.$message.success(this.$t('Checkout success'))
}
} finally {
this.isGitCheckoutLoading = false
await this.getCommits()
}
this.$st.sendEv('爬虫详情', 'Git Log', 'Checkout')
},
async updateGit () {
this.getSshPublicKey()
this.getCommits()
},
getCommitType (c) {
if (c.is_head) return 'primary'
if (c.branches && c.branches.length) {
if (c.branches.map(d => d.label).includes('master')) {
return 'danger'
} else {
return 'warning'
methods: {
onSave() {
this.$refs['git-settings-form'].validate(async valid => {
if (!valid) return
const res = await this.$store.dispatch('spider/editSpider')
if (!res.data.error) {
this.$message.success(this.$t('Spider info has been saved successfully'))
}
})
this.$st.sendEv('爬虫详情', 'Git 设置', '保存')
},
async onGitUrlChange() {
if (!this.spiderForm.git_url) return
this.isGitBranchesLoading = true
try {
const res = await this.$request.get('/git/branches', { url: this.spiderForm.git_url })
this.gitBranches = res.data.data
if (!this.spiderForm.git_branch && this.gitBranches.length > 0) {
this.$set(this.spiderForm, 'git_branch', this.gitBranches[0])
}
} finally {
this.isGitBranchesLoading = false
}
},
async onSync() {
this.isGitSyncLoading = true
try {
const res = await this.$request.post(`/spiders/${this.spiderForm._id}/git/sync`)
if (!res.data.error) {
this.$message.success(this.$t('Git has been synchronized successfully'))
}
} finally {
this.isGitSyncLoading = false
await this.updateGit()
await this.$store.dispatch('spider/getSpiderData', this.$route.params.id)
}
this.$st.sendEv('爬虫详情', 'Git 设置', '同步')
},
onReset() {
this.$confirm(
this.$t('This would delete all files of the spider. Are you sure to continue?'),
this.$t('Notification'),
{
confirmButtonText: this.$t('Confirm'),
cancelButtonText: this.$t('Cancel'),
type: 'warning'
})
.then(async() => {
this.isGitResetLoading = true
try {
const res = await this.$request.post(`/spiders/${this.spiderForm._id}/git/reset`)
if (!res.data.error) {
this.$message.success(this.$t('Git has been reset successfully'))
this.$st.sendEv('爬虫详情', 'Git 设置', '确认重置')
}
} finally {
this.isGitResetLoading = false
// await this.updateGit()
}
})
this.$st.sendEv('爬虫详情', 'Git 设置', '点击重置')
},
async getSshPublicKey() {
const res = await this.$request.get('/git/public-key')
this.sshPublicKey = res.data.data
},
copySshPublicKey() {
const el = document.querySelector('#ssh-public-key')
el.focus()
el.setSelectionRange(0, this.sshPublicKey.length)
document.execCommand('copy')
this.$message.success(this.$t('SSH Public Key is copied to the clipboard'))
this.$st.sendEv('爬虫详情', 'Git 设置', '拷贝 SSH 公钥')
},
async getCommits() {
const res = await this.$request.get('/git/commits', { spider_id: this.spiderForm._id })
this.commits = res.data.data.map(d => {
d.ts = dayjs(d.ts).format('YYYY-MM-DD HH:mm:ss')
return d
})
},
async checkout(c) {
this.isGitCheckoutLoading = true
try {
const res = await this.$request.post('/git/checkout', { spider_id: this.spiderForm._id, hash: c.hash })
if (!res.data.error) {
this.$message.success(this.$t('Checkout success'))
}
} finally {
this.isGitCheckoutLoading = false
await this.getCommits()
}
this.$st.sendEv('爬虫详情', 'Git Log', 'Checkout')
},
async updateGit() {
this.getCommits()
},
getCommitType(c) {
if (c.is_head) return 'primary'
if (c.branches && c.branches.length) {
if (c.branches.map(d => d.label).includes('master')) {
return 'danger'
} else {
return 'warning'
}
}
if (c.tags && c.tags.length) {
return 'success'
}
if (c.remote_branches && c.remote_branches.length) {
return 'info'
}
},
onChangeTab() {
this.$st.sendEv('爬虫详情', 'Git 切换标签', this.activeTabName)
}
if (c.tags && c.tags.length) {
return 'success'
}
if (c.remote_branches && c.remote_branches.length) {
return 'info'
}
},
onChangeTab () {
this.$st.sendEv('爬虫详情', 'Git 切换标签', this.activeTabName)
}
},
async created () {
if (this.spiderForm.git_url) {
this.onGitUrlChange()
}
this.getSshPublicKey()
this.getCommits()
}
}
</script>
<style scoped>

View File

@@ -11,30 +11,30 @@
</template>
<script>
export default {
props: {
items: {
type: Array,
default: function() {
return []
export default {
props: {
items: {
type: Array,
default: function() {
return []
}
},
title: {
type: String,
default: 'vue'
}
},
title: {
type: String,
default: 'vue'
}
},
data() {
return {
isActive: false
}
},
methods: {
clickTitle() {
this.isActive = !this.isActive
data() {
return {
isActive: false
}
},
methods: {
clickTitle() {
this.isActive = !this.isActive
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" >

View File

@@ -5,51 +5,51 @@
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">{{
item.label }}</el-dropdown-item>
item.label }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
export default {
data() {
return {
sizeOptions: [
{ label: 'Default', value: 'default' },
{ label: 'Medium', value: 'medium' },
{ label: 'Small', value: 'small' },
{ label: 'Mini', value: 'mini' }
]
}
},
computed: {
size() {
return this.$store.getters.size
}
},
methods: {
handleSetSize(size) {
this.$ELEMENT.size = size
this.$store.dispatch('setSize', size)
this.refreshView()
this.$message({
message: 'Switch Size Success',
type: 'success'
})
export default {
data() {
return {
sizeOptions: [
{ label: 'Default', value: 'default' },
{ label: 'Medium', value: 'medium' },
{ label: 'Small', value: 'small' },
{ label: 'Mini', value: 'mini' }
]
}
},
refreshView() {
// In order to make the cached page re-rendered
this.$store.dispatch('delAllCachedViews', this.$route)
const { fullPath } = this.$route
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
computed: {
size() {
return this.$store.getters.size
}
},
methods: {
handleSetSize(size) {
this.$ELEMENT.size = size
this.$store.dispatch('setSize', size)
this.refreshView()
this.$message({
message: 'Switch Size Success',
type: 'success'
})
})
}
}
},
refreshView() {
// In order to make the cached page re-rendered
this.$store.dispatch('delAllCachedViews', this.$route)
}
const { fullPath } = this.$route
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
}
}
}
</script>

View File

@@ -1,26 +1,26 @@
<template>
<el-dialog
class="copy-spider-dialog"
ref="form"
class="copy-spider-dialog"
:title="$t('Copy Spider')"
:visible="visible"
width="580px"
:before-close="onClose"
>
<el-form
ref="form"
label-width="160px"
:model="form"
ref="form"
>
<el-form-item
:label="$t('New Spider Name')"
required
>
<el-input v-model="form.name" :placeholder="$t('New Spider Name')"/>
<el-input v-model="form.name" :placeholder="$t('New Spider Name')" />
</el-form-item>
</el-form>
<template slot="footer">
<el-button type="plain" size="small" @click="$emit('close')">{{$t('Cancel')}}</el-button>
<el-button type="plain" size="small" @click="$emit('close')">{{ $t('Cancel') }}</el-button>
<el-button
type="primary"
size="small"
@@ -28,56 +28,56 @@
:disabled="isLoading"
@click="onConfirm"
>
{{$t('Confirm')}}
{{ $t('Confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<script>
export default {
name: 'CopySpiderDialog',
props: {
spiderId: {
type: String,
default: ''
},
visible: {
type: Boolean,
default: false
}
},
data () {
return {
form: {
name: ''
export default {
name: 'CopySpiderDialog',
props: {
spiderId: {
type: String,
default: ''
},
isLoading: false
}
},
methods: {
onClose () {
this.$emit('close')
visible: {
type: Boolean,
default: false
}
},
onConfirm () {
this.$refs['form'].validate(async valid => {
if (!valid) return
try {
this.isLoading = true
const res = await this.$request.post(`/spiders/${this.spiderId}/copy`, this.form)
if (!res.data.error) {
this.$message.success('Copied successfully')
data() {
return {
form: {
name: ''
},
isLoading: false
}
},
methods: {
onClose() {
this.$emit('close')
},
onConfirm() {
this.$refs['form'].validate(async valid => {
if (!valid) return
try {
this.isLoading = true
const res = await this.$request.post(`/spiders/${this.spiderId}/copy`, this.form)
if (!res.data.error) {
this.$message.success('Copied successfully')
}
this.$emit('confirm')
this.$emit('close')
this.$st.sendEv('爬虫复制', '确认提交')
} finally {
this.isLoading = false
}
this.$emit('confirm')
this.$emit('close')
this.$st.sendEv('爬虫复制', '确认提交')
} finally {
this.isLoading = false
}
})
})
}
}
}
}
</script>
<style scoped>

View File

@@ -1,55 +1,56 @@
<template>
<el-card class="metric-card">
<el-col :span="6" class="icon-col">
<i :class="icon" :style="{color:color}"></i>
<i :class="icon" :style="{color:color}" />
</el-col>
<el-col :span="18" class="text-col">
<el-row>
<label class="label">{{$t(label)}}</label>
<label class="label">{{ $t(label) }}</label>
</el-row>
<el-row>
<div class="value">{{value}}</div>
<div class="value">{{ value }}</div>
</el-row>
</el-col>
</el-card>
</template>
<script>
export default {
name: 'MetricCard',
props: {
icon: {
type: String,
default: ''
export default {
name: 'MetricCard',
props: {
icon: {
type: String,
default: ''
},
label: {
type: String,
default: ''
},
value: {
type: String,
default: ''
},
type: {
type: String,
default: 'default'
}
},
label: {
type: String,
default: ''
},
value: {
default: ''
},
type: {
type: String,
default: 'default'
}
},
computed: {
color () {
if (this.type === 'primary') {
return '#409EFF'
} else if (this.type === 'warning') {
return '#e6a23c'
} else if (this.type === 'success') {
return '#67c23a'
} else if (this.type === 'danger') {
return '#f56c6c'
} else {
return 'grey'
computed: {
color() {
if (this.type === 'primary') {
return '#409EFF'
} else if (this.type === 'warning') {
return '#e6a23c'
} else if (this.type === 'success') {
return '#67c23a'
} else if (this.type === 'danger') {
return '#f56c6c'
} else {
return 'grey'
}
}
}
}
}
</script>
<style scoped>

View File

@@ -1,24 +1,32 @@
<template>
<div class="spider-stats" v-loading="loading">
<div v-loading="loading" class="spider-stats">
<!--overall stats-->
<el-row>
<div class="metric-list">
<metric-card label="30-Day Tasks"
icon="fa fa-play"
:value="overviewStats.task_count"
type="danger"/>
<metric-card label="30-Day Results"
icon="fa fa-table"
:value="overviewStats.result_count"
type="primary"/>
<metric-card label="Success Rate"
icon="fa fa-check"
:value="getPercentStr(overviewStats.success_rate)"
type="success"/>
<metric-card label="Avg Duration (sec)"
icon="fa fa-hourglass"
:value="getDecimal(overviewStats.avg_runtime_duration)"
type="warning"/>
<metric-card
label="30-Day Tasks"
icon="fa fa-play"
:value="overviewStats.task_count"
type="danger"
/>
<metric-card
label="30-Day Results"
icon="fa fa-table"
:value="overviewStats.result_count"
type="primary"
/>
<metric-card
label="Success Rate"
icon="fa fa-check"
:value="getPercentStr(overviewStats.success_rate)"
type="success"
/>
<metric-card
label="Avg Duration (sec)"
icon="fa fa-hourglass"
:value="getDecimal(overviewStats.avg_runtime_duration)"
type="warning"
/>
</div>
</el-row>
<!--./overall stats-->
@@ -26,8 +34,8 @@
<el-row>
<el-col :span="24">
<el-card class="chart-wrapper">
<h4>{{$t('Daily Tasks')}}</h4>
<div id="task-line" class="chart"></div>
<h4>{{ $t('Daily Tasks') }}</h4>
<div id="task-line" class="chart" />
</el-card>
</el-col>
</el-row>
@@ -35,8 +43,8 @@
<el-row>
<el-col :span="24">
<el-card class="chart-wrapper">
<h4>{{$t('Daily Avg Duration (sec)')}}</h4>
<div id="duration-line" class="chart"></div>
<h4>{{ $t('Daily Avg Duration (sec)') }}</h4>
<div id="duration-line" class="chart" />
</el-card>
</el-col>
</el-row>
@@ -44,117 +52,120 @@
</template>
<script>
import {
mapState
} from 'vuex'
import MetricCard from './MetricCard'
import echarts from 'echarts'
import {
mapState
} from 'vuex'
import MetricCard from './MetricCard'
import echarts from 'echarts'
export default {
name: 'SpiderStats',
components: { MetricCard },
data () {
return {
loading: false
}
},
methods: {
renderTaskLine () {
const chart = echarts.init(this.$el.querySelector('#task-line'))
const option = {
grid: {
top: 20,
bottom: 40
},
xAxis: {
type: 'category',
data: this.dailyStats.map(d => d.date)
},
yAxis: {
type: 'value'
},
series: [{
type: 'line',
data: this.dailyStats.map(d => d.task_count),
areaStyle: {},
smooth: true
}],
tooltip: {
trigger: 'axis',
show: true
}
export default {
name: 'SpiderStats',
components: { MetricCard },
data() {
return {
loading: false
}
chart.setOption(option)
},
computed: {
...mapState('spider', [
'overviewStats',
'statusStats',
'nodeStats',
'dailyStats'
])
},
renderDurationLine () {
const chart = echarts.init(this.$el.querySelector('#duration-line'))
const option = {
grid: {
top: 20,
bottom: 40
},
xAxis: {
type: 'category',
data: this.dailyStats.map(d => d.date)
},
yAxis: {
type: 'value'
},
series: [{
type: 'line',
data: this.dailyStats.map(d => d.avg_runtime_duration),
areaStyle: {},
smooth: true
}],
tooltip: {
trigger: 'axis',
show: true
mounted() {
},
methods: {
renderTaskLine() {
const chart = echarts.init(this.$el.querySelector('#task-line'))
const option = {
grid: {
top: 20,
bottom: 40
},
xAxis: {
type: 'category',
data: this.dailyStats.map(d => d.date)
},
yAxis: {
type: 'value'
},
series: [{
type: 'line',
data: this.dailyStats.map(d => d.task_count),
areaStyle: {},
smooth: true
}],
tooltip: {
trigger: 'axis',
show: true
}
}
chart.setOption(option)
},
renderDurationLine() {
const chart = echarts.init(this.$el.querySelector('#duration-line'))
const option = {
grid: {
top: 20,
bottom: 40
},
xAxis: {
type: 'category',
data: this.dailyStats.map(d => d.date)
},
yAxis: {
type: 'value'
},
series: [{
type: 'line',
data: this.dailyStats.map(d => d.avg_runtime_duration),
areaStyle: {},
smooth: true
}],
tooltip: {
trigger: 'axis',
show: true
}
}
chart.setOption(option)
},
render() {
this.renderTaskLine()
this.renderDurationLine()
},
update() {
this.loading = true
this.$store.dispatch('spider/getSpiderStats')
.then(() => {
this.render()
})
.catch(() => {
this.$message.error(this.$t('An error happened when fetching the data'))
})
.finally(() => {
this.loading = false
})
},
getPercentStr(value) {
if (value === undefined) return 'NA'
return (value * 100).toFixed(2) + '%'
},
getDecimal(value) {
if (value === undefined) return 'NA'
return value.toFixed(2)
}
chart.setOption(option)
},
render () {
this.renderTaskLine()
this.renderDurationLine()
},
update () {
this.loading = true
this.$store.dispatch('spider/getSpiderStats')
.then(() => {
this.render()
})
.catch(() => {
this.$message.error(this.$t('An error happened when fetching the data'))
})
.finally(() => {
this.loading = false
})
},
getPercentStr (value) {
if (value === undefined) return 'NA'
return (value * 100).toFixed(2) + '%'
},
getDecimal (value) {
if (value === undefined) return 'NA'
return value.toFixed(2)
}
},
computed: {
...mapState('spider', [
'overviewStats',
'statusStats',
'nodeStats',
'dailyStats'
])
},
mounted () {
}
}
</script>
<style scoped>

View File

@@ -1,36 +1,36 @@
<template>
<div class="legend">
<el-tag type="primary" size="small">
<i class="el-icon-loading"></i>
{{$t('Pending')}}
<i class="el-icon-loading" />
{{ $t('Pending') }}
</el-tag>
<el-tag type="warning" size="small">
<i class="el-icon-loading"></i>
{{$t('Running')}}
<i class="el-icon-loading" />
{{ $t('Running') }}
</el-tag>
<el-tag type="success" size="small">
<i class="el-icon-check"></i>
{{$t('Finished')}}
<i class="el-icon-check" />
{{ $t('Finished') }}
</el-tag>
<el-tag type="danger" size="small">
<i class="el-icon-error"></i>
{{$t('Error')}}
<i class="el-icon-error" />
{{ $t('Error') }}
</el-tag>
<el-tag type="info" size="small">
<i class="el-icon-video-pause"></i>
{{$t('Cancelled')}}
<i class="el-icon-video-pause" />
{{ $t('Cancelled') }}
</el-tag>
<el-tag type="danger" size="small">
<i class="el-icon-warning"></i>
{{$t('Abnormal')}}
<i class="el-icon-warning" />
{{ $t('Abnormal') }}
</el-tag>
</div>
</template>
<script>
export default {
name: 'StatusLegend'
}
export default {
name: 'StatusLegend'
}
</script>
<style scoped>

View File

@@ -1,65 +1,65 @@
<template>
<el-tag :type="type" class="status-tag">
<i :class="icon"></i>
{{$t(label)}}
<i :class="icon" />
{{ $t(label) }}
</el-tag>
</template>
<script>
export default {
name: 'StatusTag',
props: {
status: {
type: String,
default: ''
}
},
data () {
return {
statusDict: {
pending: { label: 'Pending', type: 'primary' },
running: { label: 'Running', type: 'warning' },
finished: { label: 'Finished', type: 'success' },
error: { label: 'Error', type: 'danger' },
cancelled: { label: 'Cancelled', type: 'info' },
abnormal: { label: 'Abnormal', type: 'danger' }
export default {
name: 'StatusTag',
props: {
status: {
type: String,
default: ''
}
}
},
computed: {
type () {
const s = this.statusDict[this.status]
if (s) {
return s.type
}
return ''
},
label () {
const s = this.statusDict[this.status]
if (s) {
return s.label
data() {
return {
statusDict: {
pending: { label: 'Pending', type: 'primary' },
running: { label: 'Running', type: 'warning' },
finished: { label: 'Finished', type: 'success' },
error: { label: 'Error', type: 'danger' },
cancelled: { label: 'Cancelled', type: 'info' },
abnormal: { label: 'Abnormal', type: 'danger' }
}
}
return 'NA'
},
icon () {
if (this.status === 'finished') {
return 'el-icon-check'
} else if (this.status === 'pending') {
return 'el-icon-loading'
} else if (this.status === 'running') {
return 'el-icon-loading'
} else if (this.status === 'error') {
return 'el-icon-error'
} else if (this.status === 'cancelled') {
return 'el-icon-video-pause'
} else if (this.status === 'abnormal') {
return 'el-icon-warning'
} else {
return 'el-icon-question'
computed: {
type() {
const s = this.statusDict[this.status]
if (s) {
return s.type
}
return ''
},
label() {
const s = this.statusDict[this.status]
if (s) {
return s.label
}
return 'NA'
},
icon() {
if (this.status === 'finished') {
return 'el-icon-check'
} else if (this.status === 'pending') {
return 'el-icon-loading'
} else if (this.status === 'running') {
return 'el-icon-loading'
} else if (this.status === 'error') {
return 'el-icon-error'
} else if (this.status === 'cancelled') {
return 'el-icon-video-pause'
} else if (this.status === 'abnormal') {
return 'el-icon-warning'
} else {
return 'el-icon-question'
}
}
}
}
}
</script>
<style scoped>

View File

@@ -9,80 +9,80 @@
</template>
<script>
export default {
name: 'Sticky',
props: {
stickyTop: {
type: Number,
default: 0
},
zIndex: {
type: Number,
default: 1
},
className: {
type: String,
default: ''
}
},
data() {
return {
active: false,
position: '',
width: undefined,
height: undefined,
isSticky: false
}
},
mounted() {
this.height = this.$el.getBoundingClientRect().height
window.addEventListener('scroll', this.handleScroll)
window.addEventListener('resize', this.handleReize)
},
activated() {
this.handleScroll()
},
destroyed() {
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleReize)
},
methods: {
sticky() {
if (this.active) {
return
export default {
name: 'Sticky',
props: {
stickyTop: {
type: Number,
default: 0
},
zIndex: {
type: Number,
default: 1
},
className: {
type: String,
default: ''
}
this.position = 'fixed'
this.active = true
this.width = this.width + 'px'
this.isSticky = true
},
handleReset() {
if (!this.active) {
return
data() {
return {
active: false,
position: '',
width: undefined,
height: undefined,
isSticky: false
}
this.reset()
},
reset() {
this.position = ''
this.width = 'auto'
this.active = false
this.isSticky = false
mounted() {
this.height = this.$el.getBoundingClientRect().height
window.addEventListener('scroll', this.handleScroll)
window.addEventListener('resize', this.handleReize)
},
handleScroll() {
const width = this.$el.getBoundingClientRect().width
this.width = width || 'auto'
const offsetTop = this.$el.getBoundingClientRect().top
if (offsetTop < this.stickyTop) {
this.sticky()
return
}
this.handleReset()
activated() {
this.handleScroll()
},
handleReize() {
if (this.isSticky) {
this.width = this.$el.getBoundingClientRect().width + 'px'
destroyed() {
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleReize)
},
methods: {
sticky() {
if (this.active) {
return
}
this.position = 'fixed'
this.active = true
this.width = this.width + 'px'
this.isSticky = true
},
handleReset() {
if (!this.active) {
return
}
this.reset()
},
reset() {
this.position = ''
this.width = 'auto'
this.active = false
this.isSticky = false
},
handleScroll() {
const width = this.$el.getBoundingClientRect().width
this.width = width || 'auto'
const offsetTop = this.$el.getBoundingClientRect().top
if (offsetTop < this.stickyTop) {
this.sticky()
return
}
this.handleReset()
},
handleReize() {
if (this.isSticky) {
this.width = this.$el.getBoundingClientRect().width + 'px'
}
}
}
}
}
</script>

View File

@@ -1,35 +1,35 @@
<template>
<svg :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName"/>
<use :xlink:href="iconName" />
</svg>
</template>
<script>
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
className: {
type: String,
default: ''
}
},
computed: {
iconName () {
return `#icon-${this.iconClass}`
},
svgClass () {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
computed: {
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
}
}
}
}
</script>
<style scoped>

View File

@@ -1,57 +1,60 @@
<template>
<div class="deploy-table-view">
<el-row class="title-wrapper">
<h5 class="title">{{title}}</h5>
<el-button type="success" plain class="small-btn" size="mini" icon="fa fa-refresh" @click="onRefresh"></el-button>
<h5 class="title">{{ title }}</h5>
<el-button type="success" plain class="small-btn" size="mini" icon="fa fa-refresh" @click="onRefresh" />
</el-row>
<el-table border height="240px" :data="deployList">
<el-table-column property="version" label="Ver" width="40" align="center"></el-table-column>
<el-table-column property="version" label="Ver" width="40" align="center" />
<el-table-column property="node" label="Node" width="220" align="center">
<template slot-scope="scope">
<a class="a-tag" @click="onClickNode(scope.row)">{{scope.row.node_id}}</a>
<a class="a-tag" @click="onClickNode(scope.row)">{{ scope.row.node_id }}</a>
</template>
</el-table-column>
<el-table-column property="spider_name" label="Spider" width="80" align="center">
<template slot-scope="scope">
<a class="a-tag" @click="onClickSpider(scope.row)">{{scope.row.spider_name}}</a>
<a class="a-tag" @click="onClickSpider(scope.row)">{{ scope.row.spider_name }}</a>
</template>
</el-table-column>
<el-table-column property="finish_ts" label="Finish Time" width="auto" align="center"></el-table-column>
<el-table-column property="finish_ts" label="Finish Time" width="auto" align="center" />
</el-table>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'DeployTableView',
props: {
title: String
},
computed: {
...mapState('spider', [
'spiderForm'
]),
...mapState('deploy', [
'deployList'
])
},
methods: {
onClickSpider (row) {
this.$router.push(`/spiders/${row.spider_id}`)
export default {
name: 'DeployTableView',
props: {
title: {
type: String,
default: ''
}
},
onClickNode (row) {
this.$router.push(`/nodes/${row.node_id}`)
computed: {
...mapState('spider', [
'spiderForm'
]),
...mapState('deploy', [
'deployList'
])
},
onRefresh () {
this.$store.dispatch('deploy/getDeployList', this.spiderForm._id)
methods: {
onClickSpider(row) {
this.$router.push(`/spiders/${row.spider_id}`)
},
onClickNode(row) {
this.$router.push(`/nodes/${row.node_id}`)
},
onRefresh() {
this.$store.dispatch('deploy/getDeployList', this.spiderForm._id)
}
}
}
}
</script>
<style scoped>

View File

@@ -1,24 +1,26 @@
<template>
<div class="fields-table-view">
<el-row>
<el-table :data="fields"
class="table edit"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
:cell-style="getCellClassStyle"
<el-table
:data="fields"
class="table edit"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
:cell-style="getCellClassStyle"
>
<el-table-column class-name="action" width="80px" align="right">
<template slot-scope="scope">
<i class="action-item el-icon-copy-document" @click="onCopyField(scope.row)"></i>
<i class="action-item el-icon-remove-outline" @click="onRemoveField(scope.row)"></i>
<i class="action-item el-icon-circle-plus-outline" @click="onAddField(scope.row)"></i>
<i class="action-item el-icon-copy-document" @click="onCopyField(scope.row)" />
<i class="action-item el-icon-remove-outline" @click="onRemoveField(scope.row)" />
<i class="action-item el-icon-circle-plus-outline" @click="onAddField(scope.row)" />
</template>
</el-table-column>
<el-table-column :label="$t('Field Name')" width="150px">
<template slot-scope="scope">
<el-input v-model="scope.row.name"
:placeholder="$t('Field Name')"
suffix-icon="el-icon-edit"
@change="onNameChange(scope.row)"
<el-input
v-model="scope.row.name"
:placeholder="$t('Field Name')"
suffix-icon="el-icon-edit"
@change="onNameChange(scope.row)"
/>
</template>
</el-table-column>
@@ -49,16 +51,14 @@
v-model="scope.row.css"
:placeholder="$t('CSS / XPath')"
suffix-icon="el-icon-edit"
>
</el-input>
/>
</template>
<template v-else>
<el-input
v-model="scope.row.xpath"
:placeholder="$t('CSS / XPath')"
suffix-icon="el-icon-edit"
>
</el-input>
/>
</template>
</template>
</el-table-column>
@@ -69,7 +69,7 @@
:class="!isShowAttr(scope.row) ? 'active' : 'inactive'"
type="success"
>
{{$t('Text')}}
{{ $t('Text') }}
</el-tag>
</span>
<span class="button-selector-item" @click="onClickIsAttribute(scope.row, true)">
@@ -77,7 +77,7 @@
:class="isShowAttr(scope.row) ? 'active' : 'inactive'"
type="primary"
>
{{$t('Attribute')}}
{{ $t('Attribute') }}
</el-tag>
</span>
</template>
@@ -106,14 +106,14 @@
:class="!scope.row.next_stage ? 'disabled' : ''"
@change="onChangeNextStage(scope.row)"
>
<el-option :label="$t('No Next Stage')" value=""/>
<el-option v-for="n in filteredStageNames" :key="n" :label="n" :value="n"/>
<el-option :label="$t('No Next Stage')" value="" />
<el-option v-for="n in filteredStageNames" :key="n" :label="n" :value="n" />
</el-select>
</template>
</el-table-column>
<el-table-column :label="$t('Remark')" width="auto" min-width="120px">
<template slot-scope="scope">
<el-input v-model="scope.row.remark" :placeholder="$t('Remark')" suffix-icon="el-icon-edit"/>
<el-input v-model="scope.row.remark" :placeholder="$t('Remark')" suffix-icon="el-icon-edit" />
</template>
</el-table-column>
</el-table>
@@ -122,144 +122,144 @@
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'FieldsTableView',
props: {
type: {
type: String,
default: 'list'
},
title: {
type: String,
default: ''
},
stage: {
type: Object,
default () {
return {}
}
},
stageNames: {
type: Array,
default () {
return []
}
},
fields: {
type: Array,
default () {
return []
}
}
},
computed: {
...mapState('spider', [
'spiderForm'
]),
filteredStageNames () {
return this.stageNames.filter(n => n !== this.stage.name)
}
},
methods: {
onNameChange (row) {
if (this.fields.filter(d => d.name === row.name).length > 1) {
this.$message.error(this.$t(`Duplicated field names for ${row.name}`))
}
this.$st.sendEv('爬虫详情', '配置', '更改字段')
},
onClickSelectorType (row, selectorType) {
this.$st.sendEv('爬虫详情', '配置', `点击字段选择器类别-${selectorType}`)
if (selectorType === 'css') {
if (row.xpath) this.$set(row, 'xpath', '')
if (!row.css) this.$set(row, 'css', 'body')
} else {
if (row.css) this.$set(row, 'css', '')
if (!row.xpath) this.$set(row, 'xpath', '//body')
}
},
onClickIsAttribute (row, isAttribute) {
this.$st.sendEv('爬虫详情', '配置', '设置字段属性')
if (!isAttribute) {
// 文本
if (row.attr) this.$set(row, 'attr', '')
} else {
// 属性
if (!row.attr) this.$set(row, 'attr', 'href')
}
this.$set(row, 'isAttrChange', false)
},
onCopyField (row) {
for (let i = 0; i < this.fields.length; i++) {
if (row.name === this.fields[i].name) {
this.fields.splice(i, 0, JSON.parse(JSON.stringify(row)))
break
export default {
name: 'FieldsTableView',
props: {
type: {
type: String,
default: 'list'
},
title: {
type: String,
default: ''
},
stage: {
type: Object,
default() {
return {}
}
},
stageNames: {
type: Array,
default() {
return []
}
},
fields: {
type: Array,
default() {
return []
}
}
},
onRemoveField (row) {
this.$st.sendEv('爬虫详情', '配置', '删除字段')
for (let i = 0; i < this.fields.length; i++) {
if (row.name === this.fields[i].name) {
this.fields.splice(i, 1)
break
}
}
if (this.fields.length === 0) {
this.fields.push({
xpath: '//body',
next_stage: ''
})
computed: {
...mapState('spider', [
'spiderForm'
]),
filteredStageNames() {
return this.stageNames.filter(n => n !== this.stage.name)
}
},
onAddField (row) {
this.$st.sendEv('爬虫详情', '配置', '添加字段')
for (let i = 0; i < this.fields.length; i++) {
if (row.name === this.fields[i].name) {
this.fields.splice(i + 1, 0, {
name: `field_${Math.floor(new Date().getTime()).toString()}`,
methods: {
onNameChange(row) {
if (this.fields.filter(d => d.name === row.name).length > 1) {
this.$message.error(this.$t(`Duplicated field names for ${row.name}`))
}
this.$st.sendEv('爬虫详情', '配置', '更改字段')
},
onClickSelectorType(row, selectorType) {
this.$st.sendEv('爬虫详情', '配置', `点击字段选择器类别-${selectorType}`)
if (selectorType === 'css') {
if (row.xpath) this.$set(row, 'xpath', '')
if (!row.css) this.$set(row, 'css', 'body')
} else {
if (row.css) this.$set(row, 'css', '')
if (!row.xpath) this.$set(row, 'xpath', '//body')
}
},
onClickIsAttribute(row, isAttribute) {
this.$st.sendEv('爬虫详情', '配置', '设置字段属性')
if (!isAttribute) {
// 文本
if (row.attr) this.$set(row, 'attr', '')
} else {
// 属性
if (!row.attr) this.$set(row, 'attr', 'href')
}
this.$set(row, 'isAttrChange', false)
},
onCopyField(row) {
for (let i = 0; i < this.fields.length; i++) {
if (row.name === this.fields[i].name) {
this.fields.splice(i, 0, JSON.parse(JSON.stringify(row)))
break
}
}
},
onRemoveField(row) {
this.$st.sendEv('爬虫详情', '配置', '删除字段')
for (let i = 0; i < this.fields.length; i++) {
if (row.name === this.fields[i].name) {
this.fields.splice(i, 1)
break
}
}
if (this.fields.length === 0) {
this.fields.push({
xpath: '//body',
next_stage: ''
})
break
}
}
},
getCellClassStyle ({ row, columnIndex }) {
if (columnIndex === 1) {
// 字段名称
if (!row.name) {
return {
'border': '1px solid red'
},
onAddField(row) {
this.$st.sendEv('爬虫详情', '配置', '添加字段')
for (let i = 0; i < this.fields.length; i++) {
if (row.name === this.fields[i].name) {
this.fields.splice(i + 1, 0, {
name: `field_${Math.floor(new Date().getTime()).toString()}`,
xpath: '//body',
next_stage: ''
})
break
}
}
} else if (columnIndex === 3) {
// 选择器
if (!row.css && !row.xpath) {
return {
'border': '1px solid red'
},
getCellClassStyle({ row, columnIndex }) {
if (columnIndex === 1) {
// 字段名称
if (!row.name) {
return {
'border': '1px solid red'
}
}
} else if (columnIndex === 3) {
// 选择器
if (!row.css && !row.xpath) {
return {
'border': '1px solid red'
}
}
}
},
onChangeNextStage(row) {
this.fields.forEach(f => {
if (f.name !== row.name) {
this.$set(f, 'next_stage', '')
}
})
},
onAttrChange(row) {
this.$set(row, 'isAttrChange', !row.attr)
},
isShowAttr(row) {
return (row.attr || row.isAttrChange)
}
},
onChangeNextStage (row) {
this.fields.forEach(f => {
if (f.name !== row.name) {
this.$set(f, 'next_stage', '')
}
})
},
onAttrChange (row) {
this.$set(row, 'isAttrChange', !row.attr)
},
isShowAttr (row) {
return (row.attr || row.isAttrChange)
}
}
}
</script>
<style scoped>

View File

@@ -3,18 +3,19 @@
<el-table
:data="filteredData"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
border>
border
>
<template v-for="col in columns">
<el-table-column :key="col" :label="col" :property="col" min-width="120">
<template slot-scope="scope">
<el-popover trigger="hover" :content="getString(scope.row[col])" popper-class="cell-popover">
<div v-if="isUrl(scope.row[col])" slot="reference" class="wrapper">
<a :href="getString(scope.row[col])" target="_blank" style="color: #409eff">
{{getString(scope.row[col])}}
{{ getString(scope.row[col]) }}
</a>
</div>
<div v-else slot="reference" class="wrapper">
{{getString(scope.row[col])}}
{{ getString(scope.row[col]) }}
</div>
</el-popover>
</template>
@@ -23,72 +24,72 @@
</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="total">
</el-pagination>
:total="total"
@current-change="onPageChange"
@size-change="onPageChange"
/>
</div>
</div>
</template>
<script>
export default {
name: 'GeneralTableView',
data () {
return {}
},
props: {
pageNum: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
total: {
type: Number,
default: 0
},
columns: {
type: Array,
default () {
return []
export default {
name: 'GeneralTableView',
props: {
pageNum: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
total: {
type: Number,
default: 0
},
columns: {
type: Array,
default() {
return []
}
},
data: {
type: Array,
default() {
return []
}
}
},
data: {
type: Array,
default () {
return []
data() {
return {}
},
computed: {
filteredData() {
return this.data
}
}
},
computed: {
filteredData () {
return this.data
}
},
methods: {
isUrl (value) {
if (!value) return false
if (!value.match) return false
return !!value.match(/^https?:\/\//)
},
onPageChange () {
this.$emit('page-change', { pageNum: this.pageNum, pageSize: this.pageSize })
},
getString (value) {
if (value === undefined) return ''
const str = JSON.stringify(value)
if (str.match(/^"(.*)"$/)) return str.match(/^"(.*)"$/)[1]
return str
methods: {
isUrl(value) {
if (!value) return false
if (!value.match) return false
return !!value.match(/^https?:\/\//)
},
onPageChange() {
this.$emit('page-change', { pageNum: this.pageNum, pageSize: this.pageSize })
},
getString(value) {
if (value === undefined) return ''
const str = JSON.stringify(value)
if (str.match(/^"(.*)"$/)) return str.match(/^"(.*)"$/)[1]
return str
}
}
}
}
</script>
<style scoped>

View File

@@ -1,16 +1,17 @@
<template>
<div class="setting-list-table-view">
<el-row>
<el-table :data="list"
class="table edit"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
:cell-style="getCellClassStyle"
<el-table
:data="list"
class="table edit"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
:cell-style="getCellClassStyle"
>
<el-table-column class-name="action" width="80px" align="right">
<template slot-scope="scope">
<!-- <i class="action-item el-icon-copy-document" @click="onCopyField(scope.row)"></i>-->
<i class="action-item el-icon-remove-outline" @click="onRemoveField(scope.row)"></i>
<i class="action-item el-icon-circle-plus-outline" @click="onAddField(scope.row)"></i>
<i class="action-item el-icon-remove-outline" @click="onRemoveField(scope.row)" />
<i class="action-item el-icon-circle-plus-outline" @click="onAddField(scope.row)" />
</template>
</el-table-column>
<el-table-column :label="$t('Name')" width="240px">
@@ -39,114 +40,114 @@
</template>
<script>
import {
mapState
} from 'vuex'
import {
mapState
} from 'vuex'
export default {
name: 'SettingFieldsTableView',
props: {
type: {
type: String,
default: 'list'
},
title: {
type: String,
default: ''
},
stageNames: {
type: Array,
default () {
return []
}
}
},
computed: {
...mapState('spider', [
'spiderForm'
]),
list () {
const list = []
for (let name in this.spiderForm.config.settings) {
if (this.spiderForm.config.settings.hasOwnProperty(name)) {
const value = this.spiderForm.config.settings[name]
list.push({ name, value })
export default {
name: 'SettingFieldsTableView',
props: {
type: {
type: String,
default: 'list'
},
title: {
type: String,
default: ''
},
stageNames: {
type: Array,
default() {
return []
}
}
return list
}
},
methods: {
onChange (row) {
if (this.list.filter(d => d.name === row.name).length > 1) {
this.$message.error(this.$t(`Duplicated field names for ${row.name}`))
}
this.$store.commit('spider/SET_SPIDER_FORM_CONFIG_SETTINGS', this.list)
},
onRemoveField (row) {
this.$st.sendEv('爬虫详情', '配置', '删除设置')
const list = JSON.parse(JSON.stringify(this.list))
for (let i = 0; i < list.length; i++) {
if (row.name === list[i].name) {
list.splice(i, 1)
computed: {
...mapState('spider', [
'spiderForm'
]),
list() {
const list = []
for (const name in this.spiderForm.config.settings) {
if (Object.prototype.hasOwnProperty.call(this.spiderForm.config.settings, name)) {
const value = this.spiderForm.config.settings[name]
list.push({ name, value })
}
}
return list
}
if (list.length === 0) {
list.push({
name: `VARIABLE_NAME_${Math.floor(new Date().getTime())}`,
value: `VARIABLE_VALUE_${Math.floor(new Date().getTime())}`
},
created() {
if (this.list.length === 0) {
this.$store.commit(
'spider/SET_SPIDER_FORM_CONFIG_SETTING_ITEM',
'VARIABLE_NAME_' + Math.floor(new Date().getTime()),
'VARIABLE_VALUE_' + Math.floor(new Date().getTime())
)
}
},
methods: {
onChange(row) {
if (this.list.filter(d => d.name === row.name).length > 1) {
this.$message.error(this.$t(`Duplicated field names for ${row.name}`))
}
this.$store.commit('spider/SET_SPIDER_FORM_CONFIG_SETTINGS', this.list)
},
onRemoveField(row) {
this.$st.sendEv('爬虫详情', '配置', '删除设置')
const list = JSON.parse(JSON.stringify(this.list))
for (let i = 0; i < list.length; i++) {
if (row.name === list[i].name) {
list.splice(i, 1)
}
}
if (list.length === 0) {
list.push({
name: `VARIABLE_NAME_${Math.floor(new Date().getTime())}`,
value: `VARIABLE_VALUE_${Math.floor(new Date().getTime())}`
})
}
this.$store.commit('spider/SET_SPIDER_FORM_CONFIG_SETTINGS', list)
},
onAddField(row) {
this.$st.sendEv('爬虫详情', '配置', '添加设置')
const list = JSON.parse(JSON.stringify(this.list))
for (let i = 0; i < list.length; i++) {
if (row.name === list[i].name) {
const name = 'VARIABLE_NAME_' + Math.floor(new Date().getTime())
const value = 'VARIABLE_VALUE_' + Math.floor(new Date().getTime())
list.push({ name, value })
break
}
}
this.$store.commit('spider/SET_SPIDER_FORM_CONFIG_SETTINGS', list)
},
getCellClassStyle({ row, columnIndex }) {
if (columnIndex === 1) {
// 字段名称
if (!row.name) {
return {
'border': '1px solid red'
}
}
} else if (columnIndex === 3) {
// 选择器
if (!row.css && !row.xpath) {
return {
'border': '1px solid red'
}
}
}
},
onChangeNextStage(row) {
this.list.forEach(f => {
if (f.name !== row.name) {
this.$set(f, 'next_stage', '')
}
})
}
this.$store.commit('spider/SET_SPIDER_FORM_CONFIG_SETTINGS', list)
},
onAddField (row) {
this.$st.sendEv('爬虫详情', '配置', '添加设置')
const list = JSON.parse(JSON.stringify(this.list))
for (let i = 0; i < list.length; i++) {
if (row.name === list[i].name) {
const name = 'VARIABLE_NAME_' + Math.floor(new Date().getTime())
const value = 'VARIABLE_VALUE_' + Math.floor(new Date().getTime())
list.push({ name, value })
break
}
}
this.$store.commit('spider/SET_SPIDER_FORM_CONFIG_SETTINGS', list)
},
getCellClassStyle ({ row, columnIndex }) {
if (columnIndex === 1) {
// 字段名称
if (!row.name) {
return {
'border': '1px solid red'
}
}
} else if (columnIndex === 3) {
// 选择器
if (!row.css && !row.xpath) {
return {
'border': '1px solid red'
}
}
}
},
onChangeNextStage (row) {
this.list.forEach(f => {
if (f.name !== row.name) {
this.$set(f, 'next_stage', '')
}
})
}
},
created () {
if (this.list.length === 0) {
this.$store.commit(
'spider/SET_SPIDER_FORM_CONFIG_SETTING_ITEM',
'VARIABLE_NAME_' + Math.floor(new Date().getTime()),
'VARIABLE_VALUE_' + Math.floor(new Date().getTime())
)
}
}
}
</script>
<style scoped>

View File

@@ -1,8 +1,8 @@
<template>
<div class="task-table-view">
<el-row class="title-wrapper">
<h5 class="title">{{title}}</h5>
<el-button type="success" plain class="small-btn" size="mini" icon="fa fa-refresh" @click="onRefresh"></el-button>
<h5 class="title">{{ title }}</h5>
<el-button type="success" plain class="small-btn" size="mini" icon="fa fa-refresh" @click="onRefresh" />
</el-row>
<el-table
:data="taskList"
@@ -13,36 +13,38 @@
>
<el-table-column property="node" :label="$t('Node')" width="120" align="left">
<template slot-scope="scope">
<a class="a-tag" @click="onClickNode(scope.row)">{{scope.row.node_name}}</a>
<a class="a-tag" @click="onClickNode(scope.row)">{{ scope.row.node_name }}</a>
</template>
</el-table-column>
<el-table-column property="spider_name" :label="$t('Spider')" width="120" align="left">
<template slot-scope="scope">
<a class="a-tag" @click="onClickSpider(scope.row)">{{scope.row.spider_name}}</a>
<a class="a-tag" @click="onClickSpider(scope.row)">{{ scope.row.spider_name }}</a>
</template>
</el-table-column>
<el-table-column property="param" :label="$t('Parameters')" width="120">
<template slot-scope="scope">
<span>{{scope.row.param}}</span>
<span>{{ scope.row.param }}</span>
</template>
</el-table-column>
<el-table-column property="result_count" :label="$t('Results Count')" width="60" align="right">
<template slot-scope="scope">
<span>{{scope.row.result_count}}</span>
<span>{{ scope.row.result_count }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('Status')"
align="left"
width="100">
<el-table-column
:label="$t('Status')"
align="left"
width="100"
>
<template slot-scope="scope">
<status-tag :status="scope.row.status"/>
<status-tag :status="scope.row.status" />
</template>
</el-table-column>
<!--<el-table-column property="create_ts" label="Create Time" width="auto" align="center"></el-table-column>-->
<el-table-column property="create_ts" :label="$t('Create Time')" width="150" align="left">
<template slot-scope="scope">
<a href="javascript:" class="a-tag" @click="onClickTask(scope.row)">
{{getTime(scope.row.create_ts).format('YYYY-MM-DD HH:mm:ss')}}
{{ getTime(scope.row.create_ts).format('YYYY-MM-DD HH:mm:ss') }}
</a>
</template>
</el-table-column>
@@ -52,85 +54,89 @@
</template>
<script>
import {
mapState
} from 'vuex'
import dayjs from 'dayjs'
import StatusTag from '../Status/StatusTag'
import {
mapState
} from 'vuex'
import dayjs from 'dayjs'
import StatusTag from '../Status/StatusTag'
export default {
name: 'TaskTableView',
components: { StatusTag },
data () {
return {
// setInterval handle
handle: undefined,
// 防抖
clicked: false
}
},
props: {
title: String
},
computed: {
...mapState('spider', [
'spiderForm'
]),
...mapState('task', [
'taskList'
])
},
methods: {
onClickSpider (row) {
this.clicked = true
setTimeout(() => {
this.clicked = false
}, 100)
this.$router.push(`/spiders/${row.spider_id}`)
if (this.$route.path.match(/\/nodes\//)) {
this.$st.sendEv('节点详情', '概览', '查看爬虫')
export default {
name: 'TaskTableView',
components: { StatusTag },
props: {
title: {
type: String,
default: ''
}
},
onClickNode (row) {
this.clicked = true
setTimeout(() => {
this.clicked = false
}, 100)
this.$router.push(`/nodes/${row.node_id}`)
if (this.$route.path.match(/\/spiders\//)) {
this.$st.sendEv('爬虫详情', '概览', '查看节点')
data() {
return {
// setInterval handle
handle: undefined,
// 防抖
clicked: false
}
},
onClickTask (row) {
if (!this.clicked) {
this.$router.push(`/tasks/${row._id}`)
computed: {
...mapState('spider', [
'spiderForm'
]),
...mapState('task', [
'taskList'
])
},
mounted() {
this.handle = setInterval(() => {
this.onRefresh()
}, 5000)
},
destroyed() {
clearInterval(this.handle)
},
methods: {
onClickSpider(row) {
this.clicked = true
setTimeout(() => {
this.clicked = false
}, 100)
this.$router.push(`/spiders/${row.spider_id}`)
if (this.$route.path.match(/\/nodes\//)) {
this.$st.sendEv('节点详情', '概览', '查看任务')
} else if (this.$route.path.match(/\/spiders\//)) {
this.$st.sendEv('爬虫详情', '概览', '查看任务')
this.$st.sendEv('节点详情', '概览', '查看爬虫')
}
},
onClickNode(row) {
this.clicked = true
setTimeout(() => {
this.clicked = false
}, 100)
this.$router.push(`/nodes/${row.node_id}`)
if (this.$route.path.match(/\/spiders\//)) {
this.$st.sendEv('爬虫详情', '概览', '查看节点')
}
},
onClickTask(row) {
if (!this.clicked) {
this.$router.push(`/tasks/${row._id}`)
if (this.$route.path.match(/\/nodes\//)) {
this.$st.sendEv('节点详情', '概览', '查看任务')
} else if (this.$route.path.match(/\/spiders\//)) {
this.$st.sendEv('爬虫详情', '概览', '查看任务')
}
}
},
onRefresh() {
if (this.$route.path.split('/')[1] === 'spiders') {
this.$store.dispatch('spider/getTaskList', this.$route.params.id)
} else if (this.$route.path.split('/')[1] === 'nodes') {
this.$store.dispatch('node/getTaskList', this.$route.params.id)
}
},
getTime(str) {
return dayjs(str)
}
},
onRefresh () {
if (this.$route.path.split('/')[1] === 'spiders') {
this.$store.dispatch('spider/getTaskList', this.$route.params.id)
} else if (this.$route.path.split('/')[1] === 'nodes') {
this.$store.dispatch('node/getTaskList', this.$route.params.id)
}
},
getTime (str) {
return dayjs(str)
}
},
mounted () {
this.handle = setInterval(() => {
this.onRefresh()
}, 5000)
},
destroyed () {
clearInterval(this.handle)
}
}
</script>
<style scoped>

View File

@@ -2,102 +2,120 @@
<el-color-picker
v-model="theme"
class="theme-picker"
popper-class="theme-picker-dropdown"/>
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
const version = require('element-ui/package.json').version // element-ui version from node_modules
const ORIGINAL_THEME = '#409EFF' // default color
export default {
data() {
return {
chalk: '', // content of theme-chalk css
theme: ORIGINAL_THEME
}
},
watch: {
theme(val) {
const oldVal = this.theme
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
console.log(themeCluster, originalCluster)
const getHandler = (variable, id) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
export default {
data() {
return {
chalk: '', // content of theme-chalk css
theme: ORIGINAL_THEME
}
},
watch: {
theme(val) {
const oldVal = this.theme
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
console.log(themeCluster, originalCluster)
const getHandler = (variable, id) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
}
styleTag.innerText = newStyle
}
styleTag.innerText = newStyle
}
}
const chalkHandler = getHandler('chalk', 'chalk-style')
const chalkHandler = getHandler('chalk', 'chalk-style')
if (!this.chalk) {
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
this.getCSSString(url, chalkHandler, 'chalk')
} else {
chalkHandler()
}
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const { innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$message({
message: '换肤成功',
type: 'success'
})
}
},
methods: {
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, callback, variable) {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
callback()
}
}
xhr.open('GET', url)
xhr.send()
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) { // when primary color is in its rgb space
return [red, green, blue].join(',')
if (!this.chalk) {
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
this.getCSSString(url, chalkHandler, 'chalk')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
chalkHandler()
}
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const { innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$message({
message: '换肤成功',
type: 'success'
})
}
},
methods: {
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, callback, variable) {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
callback()
}
}
xhr.open('GET', url)
xhr.send()
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) { // when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
@@ -105,33 +123,16 @@ export default {
return `#${red}${green}${blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
}
}
</script>
<style>

View File

@@ -12,7 +12,8 @@
:before-upload="beforeUpload"
class="editor-slide-upload"
action="https://httpbin.org/post"
list-type="picture-card">
list-type="picture-card"
>
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
<el-button @click="dialogVisible = false"> </el-button>
@@ -24,73 +25,73 @@
<script>
// import { getToken } from 'api/qiniu'
export default {
name: 'EditorSlideUpload',
props: {
color: {
type: String,
default: '#1890ff'
}
},
data() {
return {
dialogVisible: false,
listObj: {},
fileList: []
}
},
methods: {
checkAllSuccess() {
return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
},
handleSubmit() {
const arr = Object.keys(this.listObj).map(v => this.listObj[v])
if (!this.checkAllSuccess()) {
this.$message('请等待所有图片上传成功 或 出现了网络问题,请刷新页面重新上传!')
return
export default {
name: 'EditorSlideUpload',
props: {
color: {
type: String,
default: '#1890ff'
}
this.$emit('successCBK', arr)
this.listObj = {}
this.fileList = []
this.dialogVisible = false
},
handleSuccess(response, file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
this.listObj[objKeyArr[i]].url = response.files.file
this.listObj[objKeyArr[i]].hasSuccess = true
data() {
return {
dialogVisible: false,
listObj: {},
fileList: []
}
},
methods: {
checkAllSuccess() {
return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
},
handleSubmit() {
const arr = Object.keys(this.listObj).map(v => this.listObj[v])
if (!this.checkAllSuccess()) {
this.$message('请等待所有图片上传成功 或 出现了网络问题,请刷新页面重新上传!')
return
}
}
},
handleRemove(file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
delete this.listObj[objKeyArr[i]]
return
this.$emit('successCBK', arr)
this.listObj = {}
this.fileList = []
this.dialogVisible = false
},
handleSuccess(response, file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
this.listObj[objKeyArr[i]].url = response.files.file
this.listObj[objKeyArr[i]].hasSuccess = true
return
}
}
}
},
beforeUpload(file) {
const _self = this
const _URL = window.URL || window.webkitURL
const fileName = file.uid
this.listObj[fileName] = {}
return new Promise((resolve, reject) => {
const img = new Image()
img.src = _URL.createObjectURL(file)
img.onload = function() {
_self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
},
handleRemove(file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
delete this.listObj[objKeyArr[i]]
return
}
}
resolve(true)
})
},
beforeUpload(file) {
const _self = this
const _URL = window.URL || window.webkitURL
const fileName = file.uid
this.listObj[fileName] = {}
return new Promise((resolve, reject) => {
const img = new Image()
img.src = _URL.createObjectURL(file)
img.onload = function() {
_self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
}
resolve(true)
})
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>

View File

@@ -12,7 +12,8 @@
:before-upload="beforeUpload"
class="editor-slide-upload"
action="https://httpbin.org/post"
list-type="picture-card">
list-type="picture-card"
>
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
<el-button @click="dialogVisible = false"> </el-button>
@@ -24,73 +25,73 @@
<script>
// import { getToken } from 'api/qiniu'
export default {
name: 'EditorSlideUpload',
props: {
color: {
type: String,
default: '#1890ff'
}
},
data() {
return {
dialogVisible: false,
listObj: {},
fileList: []
}
},
methods: {
checkAllSuccess() {
return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
},
handleSubmit() {
const arr = Object.keys(this.listObj).map(v => this.listObj[v])
if (!this.checkAllSuccess()) {
this.$message('请等待所有图片上传成功 或 出现了网络问题,请刷新页面重新上传!')
return
export default {
name: 'EditorSlideUpload',
props: {
color: {
type: String,
default: '#1890ff'
}
this.$emit('successCBK', arr)
this.listObj = {}
this.fileList = []
this.dialogVisible = false
},
handleSuccess(response, file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
this.listObj[objKeyArr[i]].url = response.files.file
this.listObj[objKeyArr[i]].hasSuccess = true
data() {
return {
dialogVisible: false,
listObj: {},
fileList: []
}
},
methods: {
checkAllSuccess() {
return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
},
handleSubmit() {
const arr = Object.keys(this.listObj).map(v => this.listObj[v])
if (!this.checkAllSuccess()) {
this.$message('请等待所有图片上传成功 或 出现了网络问题,请刷新页面重新上传!')
return
}
}
},
handleRemove(file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
delete this.listObj[objKeyArr[i]]
return
this.$emit('successCBK', arr)
this.listObj = {}
this.fileList = []
this.dialogVisible = false
},
handleSuccess(response, file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
this.listObj[objKeyArr[i]].url = response.files.file
this.listObj[objKeyArr[i]].hasSuccess = true
return
}
}
}
},
beforeUpload(file) {
const _self = this
const _URL = window.URL || window.webkitURL
const fileName = file.uid
this.listObj[fileName] = {}
return new Promise((resolve, reject) => {
const img = new Image()
img.src = _URL.createObjectURL(file)
img.onload = function() {
_self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
},
handleRemove(file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
delete this.listObj[objKeyArr[i]]
return
}
}
resolve(true)
})
},
beforeUpload(file) {
const _self = this
const _URL = window.URL || window.webkitURL
const fileName = file.uid
this.listObj[fileName] = {}
return new Promise((resolve, reject) => {
const img = new Image()
img.src = _URL.createObjectURL(file)
img.onload = function() {
_self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
}
resolve(true)
})
}
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>

View File

@@ -1,126 +1,126 @@
<template>
<div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">
<textarea :id="tinymceId" class="tinymce-textarea"/>
<textarea :id="tinymceId" class="tinymce-textarea" />
<div class="editor-custom-btn-container">
<editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK"/>
<editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK" />
</div>
</div>
</template>
<script>
import editorImage from './components/editorImage'
import plugins from './plugins'
import toolbar from './toolbar'
import editorImage from './components/editorImage'
import plugins from './plugins'
import toolbar from './toolbar'
export default {
name: 'Tinymce',
components: { editorImage },
props: {
id: {
type: String,
default: function() {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
toolbar: {
type: Array,
required: false,
default() {
return []
}
},
menubar: {
type: String,
default: 'file edit insert view format table'
},
height: {
type: Number,
required: false,
default: 360
}
},
data() {
return {
hasChange: false,
hasInit: false,
tinymceId: this.id,
fullscreen: false,
languageTypeList: {
'en': 'en',
'zh': 'zh_CN'
}
}
},
computed: {
language() {
return this.languageTypeList[this.$store.getters.language]
}
},
watch: {
value(val) {
if (!this.hasChange && this.hasInit) {
this.$nextTick(() =>
window.tinymce.get(this.tinymceId).setContent(val || ''))
}
},
language() {
this.destroyTinymce()
this.$nextTick(() => this.initTinymce())
}
},
mounted() {
this.initTinymce()
},
activated() {
this.initTinymce()
},
deactivated() {
this.destroyTinymce()
},
destroyed() {
this.destroyTinymce()
},
methods: {
initTinymce() {
const _this = this
window.tinymce.init({
language: this.language,
selector: `#${this.tinymceId}`,
height: this.height,
body_class: 'panel-body ',
object_resizing: false,
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
menubar: this.menubar,
plugins: plugins,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
code_dialog_width: 1000,
advlist_bullet_styles: 'square',
advlist_number_styles: 'default',
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank',
link_title: false,
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
init_instance_callback: editor => {
if (_this.value) {
editor.setContent(_this.value)
}
_this.hasInit = true
editor.on('NodeChange Change KeyUp SetContent', () => {
this.hasChange = true
this.$emit('input', editor.getContent())
})
},
setup(editor) {
editor.on('FullscreenStateChanged', (e) => {
_this.fullscreen = e.state
})
export default {
name: 'Tinymce',
components: { editorImage },
props: {
id: {
type: String,
default: function() {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
toolbar: {
type: Array,
required: false,
default() {
return []
}
},
menubar: {
type: String,
default: 'file edit insert view format table'
},
height: {
type: Number,
required: false,
default: 360
}
},
data() {
return {
hasChange: false,
hasInit: false,
tinymceId: this.id,
fullscreen: false,
languageTypeList: {
'en': 'en',
'zh': 'zh_CN'
}
}
},
computed: {
language() {
return this.languageTypeList[this.$store.getters.language]
}
},
watch: {
value(val) {
if (!this.hasChange && this.hasInit) {
this.$nextTick(() =>
window.tinymce.get(this.tinymceId).setContent(val || ''))
}
},
language() {
this.destroyTinymce()
this.$nextTick(() => this.initTinymce())
}
},
mounted() {
this.initTinymce()
},
activated() {
this.initTinymce()
},
deactivated() {
this.destroyTinymce()
},
destroyed() {
this.destroyTinymce()
},
methods: {
initTinymce() {
const _this = this
window.tinymce.init({
language: this.language,
selector: `#${this.tinymceId}`,
height: this.height,
body_class: 'panel-body ',
object_resizing: false,
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
menubar: this.menubar,
plugins: plugins,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
code_dialog_width: 1000,
advlist_bullet_styles: 'square',
advlist_number_styles: 'default',
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank',
link_title: false,
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
init_instance_callback: editor => {
if (_this.value) {
editor.setContent(_this.value)
}
_this.hasInit = true
editor.on('NodeChange Change KeyUp SetContent', () => {
this.hasChange = true
this.$emit('input', editor.getContent())
})
},
setup(editor) {
editor.on('FullscreenStateChanged', (e) => {
_this.fullscreen = e.state
})
}
// 整合七牛上传
// images_dataimg_filter(img) {
// setTimeout(() => {
@@ -154,32 +154,32 @@ export default {
// console.log(err);
// });
// },
})
},
destroyTinymce() {
const tinymce = window.tinymce.get(this.tinymceId)
if (this.fullscreen) {
tinymce.execCommand('mceFullScreen')
}
})
},
destroyTinymce() {
const tinymce = window.tinymce.get(this.tinymceId)
if (this.fullscreen) {
tinymce.execCommand('mceFullScreen')
}
if (tinymce) {
tinymce.destroy()
if (tinymce) {
tinymce.destroy()
}
},
setContent(value) {
window.tinymce.get(this.tinymceId).setContent(value)
},
getContent() {
window.tinymce.get(this.tinymceId).getContent()
},
imageSuccessCBK(arr) {
const _this = this
arr.forEach(v => {
window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
})
}
},
setContent(value) {
window.tinymce.get(this.tinymceId).setContent(value)
},
getContent() {
window.tinymce.get(this.tinymceId).getContent()
},
imageSuccessCBK(arr) {
const _this = this
arr.forEach(v => {
window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
})
}
}
}
</script>
<style scoped>

View File

@@ -1,6 +1,8 @@
// Here is a list of the toolbar
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
const toolbar = [
'searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample',
'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
export default toolbar

View File

@@ -9,111 +9,111 @@
</template>
<script>
import XLSX from 'xlsx'
import XLSX from 'xlsx'
export default {
props: {
export default {
props: {
beforeUpload: Function, // eslint-disable-line
onSuccess: Function// eslint-disable-line
},
data() {
return {
loading: false,
excelData: {
header: null,
results: null
}
}
},
methods: {
generateData({ header, results }) {
this.excelData.header = header
this.excelData.results = results
this.onSuccess && this.onSuccess(this.excelData)
},
handleDrop(e) {
e.stopPropagation()
e.preventDefault()
if (this.loading) return
const files = e.dataTransfer.files
if (files.length !== 1) {
this.$message.error('Only support uploading one file!')
return
}
const rawFile = files[0] // only use files[0]
if (!this.isExcel(rawFile)) {
this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
return false
}
this.upload(rawFile)
e.stopPropagation()
e.preventDefault()
},
handleDragover(e) {
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
},
handleUpload() {
this.$refs['excel-upload-input'].click()
},
handleClick(e) {
const files = e.target.files
const rawFile = files[0] // only use files[0]
if (!rawFile) return
this.upload(rawFile)
},
upload(rawFile) {
this.$refs['excel-upload-input'].value = null // fix can't select the same excel
if (!this.beforeUpload) {
this.readerData(rawFile)
return
}
const before = this.beforeUpload(rawFile)
if (before) {
this.readerData(rawFile)
}
},
readerData(rawFile) {
this.loading = true
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = e => {
const data = e.target.result
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const header = this.getHeaderRow(worksheet)
const results = XLSX.utils.sheet_to_json(worksheet)
this.generateData({ header, results })
this.loading = false
resolve()
data() {
return {
loading: false,
excelData: {
header: null,
results: null
}
reader.readAsArrayBuffer(rawFile)
})
},
getHeaderRow(sheet) {
const headers = []
const range = XLSX.utils.decode_range(sheet['!ref'])
let C
const R = range.s.r
/* start in the first row */
for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
/* find the cell in the first row */
let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
headers.push(hdr)
}
return headers
},
isExcel(file) {
return /\.(xlsx|xls|csv)$/.test(file.name)
methods: {
generateData({ header, results }) {
this.excelData.header = header
this.excelData.results = results
this.onSuccess && this.onSuccess(this.excelData)
},
handleDrop(e) {
e.stopPropagation()
e.preventDefault()
if (this.loading) return
const files = e.dataTransfer.files
if (files.length !== 1) {
this.$message.error('Only support uploading one file!')
return
}
const rawFile = files[0] // only use files[0]
if (!this.isExcel(rawFile)) {
this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
return false
}
this.upload(rawFile)
e.stopPropagation()
e.preventDefault()
},
handleDragover(e) {
e.stopPropagation()
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
},
handleUpload() {
this.$refs['excel-upload-input'].click()
},
handleClick(e) {
const files = e.target.files
const rawFile = files[0] // only use files[0]
if (!rawFile) return
this.upload(rawFile)
},
upload(rawFile) {
this.$refs['excel-upload-input'].value = null // fix can't select the same excel
if (!this.beforeUpload) {
this.readerData(rawFile)
return
}
const before = this.beforeUpload(rawFile)
if (before) {
this.readerData(rawFile)
}
},
readerData(rawFile) {
this.loading = true
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = e => {
const data = e.target.result
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const header = this.getHeaderRow(worksheet)
const results = XLSX.utils.sheet_to_json(worksheet)
this.generateData({ header, results })
this.loading = false
resolve()
}
reader.readAsArrayBuffer(rawFile)
})
},
getHeaderRow(sheet) {
const headers = []
const range = XLSX.utils.decode_range(sheet['!ref'])
let C
const R = range.s.r
/* start in the first row */
for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
/* find the cell in the first row */
let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
headers.push(hdr)
}
return headers
},
isExcel(file) {
return /\.(xlsx|xls|csv)$/.test(file.name)
}
}
}
}
</script>
<style scoped>