* 增加Docker开发环境

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

View File

@@ -1,5 +1,13 @@
[*.{js,jsx,ts,tsx,vue}]
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@@ -1,20 +1,254 @@
module.exports = {
root: true,
env: {
node: true
browser: true,
node: true,
es6: true
},
'extends': [
'plugin:vue/essential',
'@vue/standard'
extends: ['plugin:vue/recommended', 'eslint:recommended'],
overrides: [
{
files: [
'*.vue'
],
rules: {
indent: 'off'
}
},
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
],
rules: {
'vue/max-attributes-per-line': [
2, {
'singleline': 10,
'multiline': {
'max': 1,
'allowFirstLine': false
}
}],
'vue/singleline-html-element-content-newline': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/name-property-casing': ['error', 'PascalCase'],
'vue/no-v-html': 'off',
'vue/no-template-shadow': 'off',
'vue/this-in-template': 'off',
'vue/script-indent': [
'error', 2, {
'baseIndent': 1,
'switchCase': 0,
'ignores': []
}],
'accessor-pairs': 2,
'arrow-spacing': [
2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [
2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [
0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [
2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
'generator-star-spacing': [
2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [
2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [
2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [
2, {
'before': true,
'after': true
}],
'new-cap': [
2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [
2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [
2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [
2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [
2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [
2, {
'initialized': 'never'
}],
'operator-linebreak': [
2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [
2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [
2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [
2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [
2, 'always', {
'markers': [
'global',
'globals',
'eslint',
'eslint-disable',
'*package',
'!',
',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [
2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
},
parserOptions: {
parser: 'babel-eslint'
parser: 'babel-eslint',
sourceType: 'module'
},
globals: {
'_hmt': 1
'_hmt': 'readonly'
}
}

View File

@@ -1,5 +1,5 @@
module.exports = {
presets: [
'@vue/app'
'@vue/cli-plugin-babel/preset'
]
}

View File

@@ -5,19 +5,25 @@ module.exports = {
'json',
'vue'
],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
testURL: 'http://localhost/'
testURL: 'http://localhost/',
preset: '@vue/cli-plugin-unit-jest'
}

9
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

View File

@@ -4,60 +4,64 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve --ip=0.0.0.0 --mode=development",
"serve:prod": "vue-cli-service serve --mode=production --ip=0.0.0.0",
"config": "vue ui",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint",
"build:dev": "vue-cli-service build --mode development",
"build:prod": "vue-cli-service build --mode production",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit"
"config": "vue ui",
"serve:prod": "vue-cli-service serve --mode=production --ip=0.0.0.0"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.19",
"@fortawesome/free-brands-svg-icons": "^5.9.0",
"@fortawesome/free-regular-svg-icons": "^5.9.0",
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"@fortawesome/vue-fontawesome": "^0.1.6",
"@tinymce/tinymce-vue": "^2.0.0",
"ansi-to-html": "^0.6.13",
"axios": "0.18.0",
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-brands-svg-icons": "^5.13.0",
"@fortawesome/free-regular-svg-icons": "^5.13.0",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/vue-fontawesome": "^0.1.9",
"@tinymce/tinymce-vue": "^3.2.2",
"ansi-to-html": "^0.6.14",
"axios": "^0.19.2",
"babel-polyfill": "^6.26.0",
"cross-env": "^5.2.0",
"dayjs": "^1.8.6",
"echarts": "^4.1.0",
"element-ui": "2.13.0",
"core-js": "^3.6.5",
"cross-env": "^7.0.2",
"dayjs": "^1.8.28",
"echarts": "^4.8.0",
"element-ui": "^2.13.2",
"font-awesome": "^4.7.0",
"github-markdown-css": "^3.0.1",
"js-cookie": "2.2.0",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"github-markdown-css": "^4.0.0",
"js-cookie": "^2.2.1",
"normalize.css": "^8.0.1",
"npm": "^6.14.5",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"showdown": "^1.9.1",
"vcrontab": "^0.3.3",
"vue": "^2.5.22",
"vue-ba": "^1.2.5",
"vcrontab": "^0.3.5",
"vue": "^2.6.11",
"vue-ba": "^1.2.8",
"vue-codemirror": "^4.0.6",
"vue-codemirror-lite": "^1.0.4",
"vue-github-button": "^1.1.2",
"vue-i18n": "^8.9.0",
"vue-router": "^3.0.1",
"vue-tour": "^1.2.0",
"vue-virtual-scroll-list": "^1.3.9",
"vuex": "^3.0.1"
"vue-github-button": "^1.2.0",
"vue-i18n": "^8.18.1",
"vue-router": "^3.3.2",
"vue-tour": "^1.4.0",
"vue-virtual-scroll-list": "^2.2.6",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.4.0",
"@vue/cli-plugin-eslint": "^3.4.0",
"@vue/cli-plugin-unit-jest": "^3.4.0",
"@vue/cli-service": "^3.4.0",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "^1.0.0-beta.20",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"eslint": "^5.8.0",
"eslint-plugin-vue": "^5.0.0",
"node-sass": "^4.9.0",
"sass-loader": "^7.1.0",
"vue-template-compiler": "^2.5.21"
"@babel/core": "^7.10.2",
"@babel/register": "^7.10.1",
"@vue/cli-plugin-babel": "~4.4.0",
"@vue/cli-plugin-eslint": "^4.4.1",
"@vue/cli-plugin-unit-jest": "~4.4.0",
"@vue/cli-service": "^4.4.1",
"@vue/test-utils": "^1.0.3",
"autoprefixer": "^9.5.1",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.0.1",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^6.2.2",
"node-sass": "^4.14.1",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.11"
}
}

View File

@@ -1,66 +1,67 @@
<template>
<div id="app">
<dialog-view/>
<router-view/>
<dialog-view />
<router-view />
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import DialogView from './components/Common/DialogView'
import {
mapState
} from 'vuex'
import DialogView from './components/Common/DialogView'
import { getToken } from '@/utils/auth'
export default {
name: 'App',
data () {
return {
msgPopup: undefined
}
},
components: {
DialogView
},
computed: {
...mapState('setting', ['setting']),
useStats () {
return localStorage.getItem('useStats')
export default {
name: 'App',
components: {
DialogView
},
uid () {
return localStorage.getItem('uid')
data() {
return {
msgPopup: undefined
}
},
sid () {
return sessionStorage.getItem('sid')
}
},
methods: {},
async mounted () {
// set uid if first visit
if (this.uid === undefined || this.uid === null) {
localStorage.setItem('uid', this.$utils.encrypt.UUID())
}
computed: {
...mapState('setting', ['setting']),
useStats() {
return localStorage.getItem('useStats')
},
uid() {
return localStorage.getItem('uid')
},
sid() {
return sessionStorage.getItem('sid')
}
},
async mounted() {
// set uid if first visit
if (this.uid === undefined || this.uid === null) {
localStorage.setItem('uid', this.$utils.encrypt.UUID())
}
// set session id if starting a session
if (this.sid === undefined || this.sid === null) {
sessionStorage.setItem('sid', this.$utils.encrypt.UUID())
}
// set session id if starting a session
if (this.sid === undefined || this.sid === null) {
sessionStorage.setItem('sid', this.$utils.encrypt.UUID())
}
// get latest version
await this.$store.dispatch('version/getLatestRelease')
// get latest version
await this.$store.dispatch('version/getLatestRelease')
if (getToken()) {
// get user info
await this.$store.dispatch('user/getInfo')
// remove loading-placeholder
const elLoading = document.querySelector('#loading-placeholder')
elLoading.remove()
// get user info
await this.$store.dispatch('user/getInfo')
// remove loading-placeholder
const elLoading = document.querySelector('#loading-placeholder')
elLoading.remove()
// send visit event
await this.$request.put('/actions', {
type: 'visit'
})
// send visit event
await this.$request.put('/actions', {
type: 'visit'
})
}
},
methods: {}
}
}
</script>
<style>

View File

@@ -1,72 +1,27 @@
import axios from 'axios'
import router from '../router'
import { Message } from 'element-ui'
import service from '@/utils/request'
// 根据 VUE_APP_BASE_URL 生成 baseUrl
let baseUrl = process.env.VUE_APP_BASE_URL ? process.env.VUE_APP_BASE_URL : 'http://localhost:8000'
if (!baseUrl.match(/^https?/i)) {
baseUrl = `${window.location.protocol}//${window.location.host}${process.env.VUE_APP_BASE_URL}`
}
// 如果 Docker 中设置了 CRAWLAB_API_ADDRESS 这个环境变量,则会将 baseUrl 覆盖
const CRAWLAB_API_ADDRESS = '###CRAWLAB_API_ADDRESS###'
if (!CRAWLAB_API_ADDRESS.match('CRAWLAB_API_ADDRESS')) {
baseUrl = CRAWLAB_API_ADDRESS
}
const request = (method, path, params, data, others = {}) => {
const url = baseUrl + path
const headers = {
'Authorization': window.localStorage.getItem('token')
}
return axios({
method,
url,
params,
data,
headers,
...others
}).then((response) => {
if (response.status === 200) {
return Promise.resolve(response)
}
return Promise.reject(response)
}).catch((e) => {
let response = e.response
if (!response) {
return e
}
if (response.status === 400) {
Message.error(response.data.error)
}
if (response.status === 401 && router.currentRoute.path !== '/login') {
router.push('/login')
}
if (response.status === 500) {
Message.error(response.data.error)
}
return response
const get = (path, params) => {
return service.get(path, {
params
})
}
const get = (path, params) => {
return request('GET', path, params)
}
const post = (path, data) => {
return request('POST', path, {}, data)
return service.post(path, data)
}
const put = (path, data) => {
return request('PUT', path, {}, data)
return service.put(path, data)
}
const del = (path, data) => {
return request('DELETE', path, {}, data)
return service.delete(path, {
params: data
})
}
const request = service.request
export default {
baseUrl,
request,
get,
post,

View File

@@ -1,14 +1,14 @@
<svg width="300" height="300" xmlns="http://www.w3.org/2000/svg">
<circle cx="150" cy="150" r="150" fill="#409eff">
</circle>
<circle cx="150" cy="150" r="110" fill="#fff">
</circle>
<circle cx="150" cy="150" r="70" fill="#409eff">
</circle>
<path d="
<circle cx="150" cy="150" r="150" fill="#409eff">
</circle>
<circle cx="150" cy="150" r="110" fill="#fff">
</circle>
<circle cx="150" cy="150" r="70" fill="#409eff">
</circle>
<path d="
M 150,150
L 280,225
A 150,150 90 0 0 280,75
" fill="#409eff">
</path>
</path>
</svg>

Before

Width:  |  Height:  |  Size: 393 B

After

Width:  |  Height:  |  Size: 377 B

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>

View File

@@ -6,5 +6,11 @@ You cannot add nodes directly on the web interface in Crawlab.
Adding a node is quite simple. The only thing you have to do is to run a Crawlab service on your target machine.
For details, please refer to the [Multi-Node Deployment Documentation](https://docs.crawlab.cn/Installation/MultiNode.html).
`
`,
auth: {
login_expired_message: 'You have been logged out, you can cancel to stay on this page, or log in again',
login_expired_title: 'Confirm logout',
login_expired_confirm: 'Confirm',
login_expired_cancel: 'Cancel'
}
}

View File

@@ -37,7 +37,6 @@ export default {
Running: '进行中',
Finished: '已完成',
Error: '错误',
Errors: '错误',
NA: '未知',
Cancelled: '已取消',
Abnormal: '异常',
@@ -417,8 +416,6 @@ export default {
'Disclaimer': '免责声明',
'Please search dependencies': '请搜索依赖',
'No Data': '暂无数据',
'No data available': '暂无数据',
'No data available. Please check whether your spiders are missing dependencies or no spiders created.': '暂无数据请检查您的爬虫是否缺少依赖或者没有创建爬虫',
'Show installed': '查看已安装',
'Installing dependency successful': '安装依赖成功',
'Installing dependency failed': '安装依赖失败',
@@ -541,7 +538,12 @@ export default {
// Cron Format: [second] [minute] [hour] [day of month] [month] [day of week]
cron_format: 'Cron 格式: [] [] [小时] [] [] []'
},
auth: {
login_expired_message: '您已注销可以取消以保留在该页面上或者再次登录',
login_expired_title: '确认登出',
login_expired_confirm: '确认',
login_expired_cancel: '取消'
},
// 内容
addNodeInstruction: `
您不能在 Crawlab 的 Web 界面直接添加节点。

View File

@@ -13,7 +13,11 @@ import { library } from '@fortawesome/fontawesome-svg-core'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { far } from '@fortawesome/free-regular-svg-icons'
import { FontAwesomeIcon, FontAwesomeLayers, FontAwesomeLayersText } from '@fortawesome/vue-fontawesome'
import {
FontAwesomeIcon,
FontAwesomeLayers,
FontAwesomeLayersText
} from '@fortawesome/vue-fontawesome'
import 'codemirror/lib/codemirror.js'
import { codemirror } from 'vue-codemirror-lite'
@@ -57,10 +61,10 @@ Vue.config.productionTip = false
// 百度统计
if (localStorage.getItem('useStats') !== '0') {
window._hmt = window._hmt || [];
(function () {
let hm = document.createElement('script')
(function() {
const hm = document.createElement('script')
hm.src = 'https://hm.baidu.com/hm.js?c35e3a563a06caee2524902c81975add'
let s = document.getElementsByTagName('script')[0]
const s = document.getElementsByTagName('script')[0]
s.parentNode.insertBefore(hm, s)
})()
}

View File

@@ -27,8 +27,16 @@ Vue.use(Router)
}
**/
export const constantRouterMap = [
{ path: '/login', component: () => import('../views/login/index'), hidden: true },
{ path: '/signup', component: () => import('../views/login/index'), hidden: true },
{
path: '/login',
component: () => import('../views/login/index'),
hidden: true
},
{
path: '/signup',
component: () => import('../views/login/index'),
hidden: true
},
{ path: '/404', component: () => import('../views/404'), hidden: true },
{ path: '/', redirect: '/home' },
@@ -180,7 +188,6 @@ export const constantRouterMap = [
title: 'Disclaimer',
icon: 'fa fa-exclamation-triangle'
},
hidden: true,
children: [
{
path: '',
@@ -200,7 +207,6 @@ export const constantRouterMap = [
title: 'ChallengeList',
icon: 'fa fa-flash'
},
hidden: true,
children: [
{
path: '',
@@ -220,7 +226,6 @@ export const constantRouterMap = [
title: 'Feedback',
icon: 'fa fa-commenting-o'
},
hidden: true,
children: [
{
path: '',
@@ -300,7 +305,7 @@ router.beforeEach((to, from, next) => {
}
})
router.afterEach(async (to, from, next) => {
router.afterEach(async(to, from, next) => {
if (to.path) {
await store.dispatch('setting/getSetting')
const res = await request.get('/version')

View File

@@ -31,10 +31,10 @@ const app = {
ToggleSideBar: ({ commit }) => {
commit('TOGGLE_SIDEBAR')
},
CloseSideBar ({ commit }, { withoutAnimation }) {
CloseSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
ToggleDevice ({ commit }, device) {
ToggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
}
}

View File

@@ -7,13 +7,13 @@ const state = {
const getters = {}
const mutations = {
SET_DEPLOY_LIST (state, value) {
SET_DEPLOY_LIST(state, value) {
state.deployList = value
}
}
const actions = {
getDeployList ({ state, commit }) {
getDeployList({ state, commit }) {
request.get('/deploys')
.then(response => {
commit('SET_DEPLOY_LIST', response.data.items.map(d => {

View File

@@ -6,10 +6,10 @@ const dialogView = {
},
getters: {},
mutations: {
SET_DIALOG_TYPE (state, value) {
SET_DIALOG_TYPE(state, value) {
state.dialogType = value
},
SET_DIALOG_VISIBLE (state, value) {
SET_DIALOG_VISIBLE(state, value) {
state.dialogVisible = value
}
},

View File

@@ -7,13 +7,13 @@ const state = {
const getters = {}
const mutations = {
SET_DOC_DATA (state, value) {
SET_DOC_DATA(state, value) {
state.docData = value
}
}
const actions = {
async getDocData ({ commit }) {
async getDocData({ commit }) {
const res = await request.get('/docs')
const data = JSON.parse(res.data.data.string)
@@ -22,8 +22,8 @@ const actions = {
const cache = {}
// iterate paths
for (let path in data) {
if (data.hasOwnProperty(path)) {
for (const path in data) {
if (Object.prototype.hasOwnProperty.call(data, path)) {
const d = data[path]
if (path.match(/\/$/)) {
cache[path] = d

View File

@@ -9,19 +9,20 @@ const state = {
const getters = {}
const mutations = {
SET_CURRENT_PATH (state, value) {
SET_CURRENT_PATH(state, value) {
state.currentPath = value
},
SET_FILE_LIST (state, value) {
SET_FILE_LIST(state, value) {
state.fileList = value
},
SET_FILE_CONTENT (state, value) {
SET_FILE_CONTENT(state, value) {
state.fileContent = value
}
}
const actions = {
getFileList ({ commit, rootState }, payload) {
getFileList({ commit, rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
commit('SET_CURRENT_PATH', path)
@@ -36,7 +37,7 @@ const actions = {
)
})
},
getFileContent ({ commit, rootState }, payload) {
getFileContent({ commit, rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
return request.get(`/spiders/${spiderId}/file`, { path })
@@ -44,30 +45,32 @@ const actions = {
commit('SET_FILE_CONTENT', response.data.data)
})
},
saveFileContent ({ state, rootState }, payload) {
saveFileContent({ state, rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
return request.post(`/spiders/${spiderId}/file`, { path, content: state.fileContent })
return request.post(`/spiders/${spiderId}/file`,
{ path, content: state.fileContent })
},
addFile ({ rootState }, payload) {
addFile({ rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
return request.put(`/spiders/${spiderId}/file`, { path })
},
addDir ({ rootState }, payload) {
addDir({ rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
return request.put(`/spiders/${spiderId}/dir`, { path })
},
deleteFile ({ rootState }, payload) {
deleteFile({ rootState }, payload) {
const { path } = payload
const spiderId = rootState.spider.spiderForm._id
return request.delete(`/spiders/${spiderId}/file`, { path })
},
renameFile ({ rootState }, payload) {
renameFile({ rootState }, payload) {
const { path, newPath } = payload
const spiderId = rootState.spider.spiderForm._id
return request.post(`/spiders/${spiderId}/file/rename`, { path, new_path: newPath })
return request.post(`/spiders/${spiderId}/file/rename`,
{ path, new_path: newPath })
}
}

View File

@@ -3,7 +3,7 @@ const state = {
}
const getters = {
lang (state) {
lang(state) {
if (state.lang === 'en') {
return 'English'
} else if (state.lang === 'zh') {
@@ -15,7 +15,7 @@ const getters = {
}
const mutations = {
SET_LANG (state, value) {
SET_LANG(state, value) {
state.lang = value
}
}

View File

@@ -12,16 +12,16 @@ const state = {
const getters = {}
const mutations = {
SET_NODE_FORM (state, value) {
SET_NODE_FORM(state, value) {
state.nodeForm = value
},
SET_NODE_LIST (state, value) {
SET_NODE_LIST(state, value) {
state.nodeList = value
},
SET_ACTIVE_SPIDER (state, value) {
SET_ACTIVE_SPIDER(state, value) {
state.activeSpider = value
},
SET_NODE_SYSTEM_INFO (state, payload) {
SET_NODE_SYSTEM_INFO(state, payload) {
const { id, systemInfo } = payload
for (let i = 0; i < state.nodeList.length; i++) {
if (state.nodeList[i]._id === id) {
@@ -33,54 +33,48 @@ const mutations = {
}
const actions = {
getNodeList ({ state, commit }) {
request.get('/nodes', {})
.then(response => {
commit('SET_NODE_LIST', response.data.data.map(d => {
d.systemInfo = {
os: '',
arch: '',
num_cpu: '',
executables: []
}
return d
}))
})
},
editNode ({ state, dispatch }) {
request.post(`/nodes/${state.nodeForm._id}`, state.nodeForm)
.then(() => {
dispatch('getNodeList')
})
},
deleteNode ({ state, dispatch }, id) {
request.delete(`/nodes/${id}`)
.then(() => {
dispatch('getNodeList')
})
},
getNodeData ({ state, commit }, id) {
request.get(`/nodes/${id}`)
.then(response => {
commit('SET_NODE_FORM', response.data.data)
})
},
getTaskList ({ state, commit }, id) {
return request.get(`/nodes/${id}/tasks`)
.then(response => {
if (response.data.data) {
commit('task/SET_TASK_LIST',
response.data.data.map(d => d)
.sort((a, b) => a.create_ts < b.create_ts ? 1 : -1),
{ root: true })
getNodeList({ state, commit }) {
request.get('/nodes', {}).then(response => {
commit('SET_NODE_LIST', response.data.data.map(d => {
d.systemInfo = {
os: '',
arch: '',
num_cpu: '',
executables: []
}
})
return d
}))
})
},
getNodeSystemInfo ({ state, commit }, id) {
return request.get(`/nodes/${id}/system`)
.then(response => {
commit('SET_NODE_SYSTEM_INFO', { id, systemInfo: response.data.data })
})
editNode({ state, dispatch }) {
request.post(`/nodes/${state.nodeForm._id}`, state.nodeForm).then(() => {
dispatch('getNodeList')
})
},
deleteNode({ state, dispatch }, id) {
request.delete(`/nodes/${id}`).then(() => {
dispatch('getNodeList')
})
},
getNodeData({ state, commit }, id) {
request.get(`/nodes/${id}`).then(response => {
commit('SET_NODE_FORM', response.data.data)
})
},
getTaskList({ state, commit }, id) {
return request.get(`/nodes/${id}/tasks`).then(response => {
if (response.data.data) {
commit('task/SET_TASK_LIST',
response.data.data.map(d => d)
.sort((a, b) => a.create_ts < b.create_ts ? 1 : -1),
{ root: true })
}
})
},
getNodeSystemInfo({ state, commit }, id) {
return request.get(`/nodes/${id}/system`).then(response => {
commit('SET_NODE_SYSTEM_INFO', { id, systemInfo: response.data.data })
})
}
}

View File

@@ -21,7 +21,7 @@ const mutations = {
}
const actions = {
getProjectList ({ state, commit }, payload) {
getProjectList({ state, commit }, payload) {
return request.get('/projects', payload)
.then(response => {
if (response.data.data) {
@@ -32,7 +32,7 @@ const actions = {
}
})
},
getProjectTags ({ state, commit }) {
getProjectTags({ state, commit }) {
return request.get('/projects/tags')
.then(response => {
if (response.data.data) {
@@ -40,13 +40,13 @@ const actions = {
}
})
},
addProject ({ state }) {
addProject({ state }) {
return request.put('/projects', state.projectForm)
},
editProject ({ state }, id) {
editProject({ state }, id) {
return request.post(`/projects/${id}`, state.projectForm)
},
removeProject ({ state }, id) {
removeProject({ state }, id) {
return request.delete(`/projects/${id}`)
}
}

View File

@@ -1,4 +1,5 @@
import request from '../../api/request'
const state = {
scheduleList: [],
scheduleForm: {
@@ -9,16 +10,16 @@ const state = {
const getters = {}
const mutations = {
SET_SCHEDULE_LIST (state, value) {
SET_SCHEDULE_LIST(state, value) {
state.scheduleList = value
},
SET_SCHEDULE_FORM (state, value) {
SET_SCHEDULE_FORM(state, value) {
state.scheduleForm = value
}
}
const actions = {
getScheduleList ({ state, commit }) {
getScheduleList({ state, commit }) {
request.get('/schedules')
.then(response => {
if (response.data.data) {
@@ -31,19 +32,19 @@ const actions = {
}
})
},
addSchedule ({ state }) {
addSchedule({ state }) {
request.put('/schedules', state.scheduleForm)
},
editSchedule ({ state }, id) {
editSchedule({ state }, id) {
request.post(`/schedules/${id}`, state.scheduleForm)
},
removeSchedule ({ state }, id) {
removeSchedule({ state }, id) {
request.delete(`/schedules/${id}`)
},
enableSchedule ({ state, dispatch }, id) {
enableSchedule({ state, dispatch }, id) {
return request.post(`/schedules/${id}/enable`)
},
disableSchedule ({ state, dispatch }, id) {
disableSchedule({ state, dispatch }, id) {
return request.post(`/schedules/${id}/disable`)
}
}

View File

@@ -7,13 +7,13 @@ const state = {
const getters = {}
const mutations = {
SET_SETTING (state, value) {
SET_SETTING(state, value) {
state.setting = value
}
}
const actions = {
async getSetting ({ commit }) {
async getSetting({ commit }) {
const res = await request.get('/setting')
commit('SET_SETTING', res.data.data)

View File

@@ -26,37 +26,37 @@ const state = {
const getters = {}
const mutations = {
SET_KEYWORD (state, value) {
SET_KEYWORD(state, value) {
state.keyword = value
},
SET_SITE_LIST (state, value) {
SET_SITE_LIST(state, value) {
state.siteList = value
},
SET_PAGE_NUM (state, value) {
SET_PAGE_NUM(state, value) {
state.pageNum = value
},
SET_PAGE_SIZE (state, value) {
SET_PAGE_SIZE(state, value) {
state.pageSize = value
},
SET_TOTAL_COUNT (state, value) {
SET_TOTAL_COUNT(state, value) {
state.totalCount = value
},
SET_MAIN_CATEGORY_LIST (state, value) {
SET_MAIN_CATEGORY_LIST(state, value) {
state.mainCategoryList = value
},
SET_CATEGORY_LIST (state, value) {
SET_CATEGORY_LIST(state, value) {
state.categoryList = value
}
}
const actions = {
editSite ({ state, dispatch }, payload) {
editSite({ state, dispatch }, payload) {
const { id, category } = payload
return request.post(`/sites/${id}`, {
category
})
},
getSiteList ({ state, commit }) {
getSiteList({ state, commit }) {
return request.get('/sites', {
page_num: state.pageNum,
page_size: state.pageSize,
@@ -71,13 +71,13 @@ const actions = {
commit('SET_TOTAL_COUNT', response.data.total_count)
})
},
getMainCategoryList ({ state, commit }) {
getMainCategoryList({ state, commit }) {
return request.get('/sites/get/get_main_category_list')
.then(response => {
commit('SET_MAIN_CATEGORY_LIST', response.data.items)
})
},
getCategoryList ({ state, commit }) {
getCategoryList({ state, commit }) {
return request.get('/sites/get/get_category_list', {
'main_category': state.filter.mainCategory || undefined
})

View File

@@ -19,9 +19,6 @@ const state = {
// spider scrapy pipelines
spiderScrapyPipelines: [],
// scrapy errors
spiderScrapyErrors: {},
// node to deploy/run
activeNode: {},
@@ -62,110 +59,94 @@ const state = {
const getters = {}
const mutations = {
SET_SPIDER_TOTAL (state, value) {
SET_SPIDER_TOTAL(state, value) {
state.spiderTotal = value
},
SET_SPIDER_FORM (state, value) {
SET_SPIDER_FORM(state, value) {
state.spiderForm = value
},
SET_SPIDER_LIST (state, value) {
SET_SPIDER_LIST(state, value) {
state.spiderList = value
},
SET_ACTIVE_NODE (state, value) {
SET_ACTIVE_NODE(state, value) {
state.activeNode = value
},
SET_IMPORT_FORM (state, value) {
SET_IMPORT_FORM(state, value) {
state.importForm = value
},
SET_OVERVIEW_STATS (state, value) {
SET_OVERVIEW_STATS(state, value) {
state.overviewStats = value
},
SET_STATUS_STATS (state, value) {
SET_STATUS_STATS(state, value) {
state.statusStats = value
},
SET_DAILY_STATS (state, value) {
SET_DAILY_STATS(state, value) {
state.dailyStats = value
},
SET_NODE_STATS (state, value) {
SET_NODE_STATS(state, value) {
state.nodeStats = value
},
SET_FILTER_SITE (state, value) {
SET_FILTER_SITE(state, value) {
state.filterSite = value
},
SET_PREVIEW_CRAWL_DATA (state, value) {
SET_PREVIEW_CRAWL_DATA(state, value) {
state.previewCrawlData = value
},
SET_SPIDER_FORM_CONFIG_SETTINGS (state, payload) {
SET_SPIDER_FORM_CONFIG_SETTINGS(state, payload) {
const settings = {}
payload.forEach(row => {
settings[row.name] = row.value
})
Vue.set(state.spiderForm.config, 'settings', settings)
},
SET_TEMPLATE_LIST (state, value) {
SET_TEMPLATE_LIST(state, value) {
state.templateList = value
},
SET_FILE_TREE (state, value) {
SET_FILE_TREE(state, value) {
state.fileTree = value
},
SET_SPIDER_SCRAPY_SETTINGS (state, value) {
SET_SPIDER_SCRAPY_SETTINGS(state, value) {
state.spiderScrapySettings = value
},
SET_SPIDER_SCRAPY_ITEMS (state, value) {
SET_SPIDER_SCRAPY_ITEMS(state, value) {
state.spiderScrapyItems = value
},
SET_SPIDER_SCRAPY_PIPELINES (state, value) {
SET_SPIDER_SCRAPY_PIPELINES(state, value) {
state.spiderScrapyPipelines = value
},
SET_CONFIG_LIST_TS (state, value) {
SET_CONFIG_LIST_TS(state, value) {
state.configListTs = value
},
SET_SPIDER_SCRAPY_ERRORS (state, value) {
for (let key in value) {
if (value.hasOwnProperty(key)) {
Vue.set(state.spiderScrapyErrors, key, value[key])
}
}
}
}
const actions = {
getSpiderList ({ state, commit }, params = {}) {
getSpiderList({ state, commit }, params = {}) {
return request.get('/spiders', params)
.then(response => {
commit('SET_SPIDER_LIST', response.data.data.list)
commit('SET_SPIDER_TOTAL', response.data.data.total)
})
},
editSpider ({ state, dispatch }) {
editSpider({ state, dispatch }) {
return request.post(`/spiders/${state.spiderForm._id}`, state.spiderForm)
},
deleteSpider ({ state, dispatch }, id) {
deleteSpider({ state, dispatch }, id) {
return request.delete(`/spiders/${id}`)
},
getSpiderData ({ state, commit }, id) {
getSpiderData({ state, commit }, id) {
return request.get(`/spiders/${id}`)
.then(response => {
let data = response.data.data
const data = response.data.data
commit('SET_SPIDER_FORM', data)
})
},
async getSpiderScrapySpiders ({ state, commit }, id) {
async getSpiderScrapySpiders({ state, commit }, id) {
const res = await request.get(`/spiders/${id}/scrapy/spiders`)
if (res.data.error) {
commit('SET_SPIDER_SCRAPY_ERRORS', { spiders: res.data.error })
return
}
state.spiderForm.spider_names = res.data.data
commit('SET_SPIDER_FORM', state.spiderForm)
commit('SET_SPIDER_SCRAPY_ERRORS', { spiders: '' })
},
async getSpiderScrapySettings ({ state, commit }, id) {
async getSpiderScrapySettings({ state, commit }, id) {
const res = await request.get(`/spiders/${id}/scrapy/settings`)
if (res.data.error) {
commit('SET_SPIDER_SCRAPY_ERRORS', { settings: res.data.error })
return
}
commit('SET_SPIDER_SCRAPY_SETTINGS', res.data.data.map(d => {
const key = d.key
const value = d.value
@@ -183,17 +164,13 @@ const actions = {
type
}
}))
commit('SET_SPIDER_SCRAPY_ERRORS', { settings: '' })
},
async saveSpiderScrapySettings ({ state }, id) {
return request.post(`/spiders/${id}/scrapy/settings`, state.spiderScrapySettings)
async saveSpiderScrapySettings({ state }, id) {
return request.post(`/spiders/${id}/scrapy/settings`,
state.spiderScrapySettings)
},
async getSpiderScrapyItems ({ state, commit }, id) {
async getSpiderScrapyItems({ state, commit }, id) {
const res = await request.get(`/spiders/${id}/scrapy/items`)
if (res.data.error) {
commit('SET_SPIDER_SCRAPY_ERRORS', { items: res.data.error })
return
}
let nodeId = 0
commit('SET_SPIDER_SCRAPY_ITEMS', res.data.data.map(d => {
d.id = nodeId++
@@ -210,36 +187,33 @@ const actions = {
})
return d
}))
commit('SET_SPIDER_SCRAPY_ERRORS', { items: '' })
},
async saveSpiderScrapyItems ({ state }, id) {
return request.post(`/spiders/${id}/scrapy/items`, state.spiderScrapyItems.map(d => {
d.name = d.label
d.fields = d.children.map(f => f.label)
return d
}))
async saveSpiderScrapyItems({ state }, id) {
return request.post(`/spiders/${id}/scrapy/items`,
state.spiderScrapyItems.map(d => {
d.name = d.label
d.fields = d.children.map(f => f.label)
return d
}))
},
async getSpiderScrapyPipelines ({ state, commit }, id) {
async getSpiderScrapyPipelines({ state, commit }, id) {
const res = await request.get(`/spiders/${id}/scrapy/pipelines`)
if (res.data.error) {
commit('SET_SPIDER_SCRAPY_ERRORS', { pipelines: res.data.error })
return
}
commit('SET_SPIDER_SCRAPY_PIPELINES', res.data.data)
commit('SET_SPIDER_SCRAPY_ERRORS', { pipelines: '' })
},
async saveSpiderScrapyPipelines ({ state }, id) {
return request.post(`/spiders/${id}/scrapy/pipelines`, state.spiderScrapyPipelines)
async saveSpiderScrapyPipelines({ state }, id) {
return request.post(`/spiders/${id}/scrapy/pipelines`,
state.spiderScrapyPipelines)
},
async getSpiderScrapySpiderFilepath ({ state, commit }, payload) {
async getSpiderScrapySpiderFilepath({ state, commit }, payload) {
const { id, spiderName } = payload
return request.get(`/spiders/${id}/scrapy/spider/filepath`, { spider_name: spiderName })
return request.get(`/spiders/${id}/scrapy/spider/filepath`,
{ spider_name: spiderName })
},
addSpiderScrapySpider ({ state }, payload) {
addSpiderScrapySpider({ state }, payload) {
const { id, form } = payload
return request.put(`/spiders/${id}/scrapy/spiders`, form)
},
crawlSpider ({ state, dispatch }, payload) {
crawlSpider({ state, dispatch }, payload) {
const { spiderId, runType, nodeIds, param } = payload
return request.put(`/tasks`, {
spider_id: spiderId,
@@ -248,7 +222,7 @@ const actions = {
param: param
})
},
crawlSelectedSpiders ({ state, dispatch }, payload) {
crawlSelectedSpiders({ state, dispatch }, payload) {
const { taskParams, runType, nodeIds } = payload
return request.post(`/spiders-run`, {
task_params: taskParams,
@@ -256,7 +230,7 @@ const actions = {
node_ids: nodeIds
})
},
getTaskList ({ state, commit }, id) {
getTaskList({ state, commit }, id) {
return request.get(`/spiders/${id}/tasks`)
.then(response => {
commit('task/SET_TASK_LIST',
@@ -266,18 +240,18 @@ const actions = {
{ root: true })
})
},
getDir ({ state, commit }, path) {
getDir({ state, commit }, path) {
const id = state.spiderForm._id
return request.get(`/spiders/${id}/dir`)
.then(response => {
commit('')
})
},
importGithub ({ state }) {
importGithub({ state }) {
const url = state.importForm.url
return request.post('/spiders/import/github', { url })
},
getSpiderStats ({ state, commit }) {
getSpiderStats({ state, commit }) {
return request.get(`/spiders/${state.spiderForm._id}/stats`)
.then(response => {
commit('SET_OVERVIEW_STATS', response.data.data.overview)
@@ -286,33 +260,35 @@ const actions = {
// commit('SET_NODE_STATS', response.data.task_count_by_node)
})
},
getPreviewCrawlData ({ state, commit }) {
getPreviewCrawlData({ state, commit }) {
return request.post(`/spiders/${state.spiderForm._id}/preview_crawl`)
.then(response => {
commit('SET_PREVIEW_CRAWL_DATA', response.data.items)
})
},
extractFields ({ state, commit }) {
extractFields({ state, commit }) {
return request.post(`/spiders/${state.spiderForm._id}/extract_fields`)
},
postConfigSpiderConfig ({ state }) {
return request.post(`/config_spiders/${state.spiderForm._id}/config`, state.spiderForm.config)
postConfigSpiderConfig({ state }) {
return request.post(`/config_spiders/${state.spiderForm._id}/config`,
state.spiderForm.config)
},
saveConfigSpiderSpiderfile ({ state, rootState }) {
saveConfigSpiderSpiderfile({ state, rootState }) {
const content = rootState.file.fileContent
return request.post(`/config_spiders/${state.spiderForm._id}/spiderfile`, { content })
return request.post(`/config_spiders/${state.spiderForm._id}/spiderfile`,
{ content })
},
addConfigSpider ({ state }) {
addConfigSpider({ state }) {
return request.put(`/config_spiders`, state.spiderForm)
},
addSpider ({ state }) {
addSpider({ state }) {
return request.put(`/spiders`, state.spiderForm)
},
async getTemplateList ({ state, commit }) {
async getTemplateList({ state, commit }) {
const res = await request.get(`/config_spiders_templates`)
commit('SET_TEMPLATE_LIST', res.data.data)
},
async getScheduleList ({ state, commit }, payload) {
async getScheduleList({ state, commit }, payload) {
const { id } = payload
const res = await request.get(`/spiders/${id}/schedules`)
let data = res.data.data
@@ -326,7 +302,7 @@ const actions = {
}
commit('schedule/SET_SCHEDULE_LIST', data, { root: true })
},
async getFileTree ({ state, commit }, payload) {
async getFileTree({ state, commit }, payload) {
const id = payload ? payload.id : state.spiderForm._id
const res = await request.get(`/spiders/${id}/file/tree`)
commit('SET_FILE_TREE', res.data.data)

View File

@@ -1,6 +1,6 @@
const state = {}
const getters = {
useStats () {
useStats() {
return localStorage.getItem('useStats')
}
}

View File

@@ -72,18 +72,18 @@ const tagsView = {
},
actions: {
addView ({ dispatch }, view) {
addView({ dispatch }, view) {
dispatch('addVisitedView', view)
dispatch('addCachedView', view)
},
addVisitedView ({ commit }, view) {
addVisitedView({ commit }, view) {
commit('ADD_VISITED_VIEW', view)
},
addCachedView ({ commit }, view) {
addCachedView({ commit }, view) {
commit('ADD_CACHED_VIEW', view)
},
delView ({ dispatch, state }, view) {
delView({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delVisitedView', view)
dispatch('delCachedView', view)
@@ -93,20 +93,20 @@ const tagsView = {
})
})
},
delVisitedView ({ commit, state }, view) {
delVisitedView({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_VISITED_VIEW', view)
resolve([...state.visitedViews])
})
},
delCachedView ({ commit, state }, view) {
delCachedView({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_CACHED_VIEW', view)
resolve([...state.cachedViews])
})
},
delOthersViews ({ dispatch, state }, view) {
delOthersViews({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delOthersVisitedViews', view)
dispatch('delOthersCachedViews', view)
@@ -116,20 +116,20 @@ const tagsView = {
})
})
},
delOthersVisitedViews ({ commit, state }, view) {
delOthersVisitedViews({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_OTHERS_VISITED_VIEWS', view)
resolve([...state.visitedViews])
})
},
delOthersCachedViews ({ commit, state }, view) {
delOthersCachedViews({ commit, state }, view) {
return new Promise(resolve => {
commit('DEL_OTHERS_CACHED_VIEWS', view)
resolve([...state.cachedViews])
})
},
delAllViews ({ dispatch, state }, view) {
delAllViews({ dispatch, state }, view) {
return new Promise(resolve => {
dispatch('delAllVisitedViews', view)
dispatch('delAllCachedViews', view)
@@ -139,20 +139,20 @@ const tagsView = {
})
})
},
delAllVisitedViews ({ commit, state }) {
delAllVisitedViews({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_VISITED_VIEWS')
resolve([...state.visitedViews])
})
},
delAllCachedViews ({ commit, state }) {
delAllCachedViews({ commit, state }) {
return new Promise(resolve => {
commit('DEL_ALL_CACHED_VIEWS')
resolve([...state.cachedViews])
})
},
updateVisitedView ({ commit }, view) {
updateVisitedView({ commit }, view) {
commit('UPDATE_VISITED_VIEW', view)
}
}

View File

@@ -36,20 +36,20 @@ const state = {
}
const getters = {
taskResultsColumns (state) {
taskResultsColumns(state) {
if (!state.taskResultsData || !state.taskResultsData.length) {
return []
}
const keys = []
const item = state.taskResultsData[0]
for (const key in item) {
if (item.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(item, key)) {
keys.push(key)
}
}
return keys
},
logData (state) {
logData(state) {
const data = state.taskLog
.map((d, i) => {
return {
@@ -71,7 +71,7 @@ const getters = {
}
return data
},
errorLogData (state, getters) {
errorLogData(state, getters) {
const idxList = getters.logData.map(d => d._id)
return state.errorLogData.map(d => {
const idx = idxList.indexOf(d._id)
@@ -82,82 +82,82 @@ const getters = {
}
const mutations = {
SET_TASK_FORM (state, value) {
SET_TASK_FORM(state, value) {
state.taskForm = value
},
SET_TASK_LIST (state, value) {
SET_TASK_LIST(state, value) {
state.taskList = value
},
SET_TASK_LOG (state, value) {
SET_TASK_LOG(state, value) {
state.taskLog = value
},
SET_TASK_LOG_TOTAL (state, value) {
SET_TASK_LOG_TOTAL(state, value) {
state.taskLogTotal = value
},
SET_CURRENT_LOG_INDEX (state, value) {
SET_CURRENT_LOG_INDEX(state, value) {
state.currentLogIndex = value
},
SET_TASK_RESULTS_DATA (state, value) {
SET_TASK_RESULTS_DATA(state, value) {
state.taskResultsData = value
},
SET_TASK_RESULTS_COLUMNS (state, value) {
SET_TASK_RESULTS_COLUMNS(state, value) {
state.taskResultsColumns = value
},
SET_PAGE_NUM (state, value) {
SET_PAGE_NUM(state, value) {
state.pageNum = value
},
SET_PAGE_SIZE (state, value) {
SET_PAGE_SIZE(state, value) {
state.pageSize = value
},
SET_TASK_LIST_TOTAL_COUNT (state, value) {
SET_TASK_LIST_TOTAL_COUNT(state, value) {
state.taskListTotalCount = value
},
SET_RESULTS_PAGE_NUM (state, value) {
SET_RESULTS_PAGE_NUM(state, value) {
state.resultsPageNum = value
},
SET_RESULTS_PAGE_SIZE (state, value) {
SET_RESULTS_PAGE_SIZE(state, value) {
state.resultsPageSize = value
},
SET_TASK_RESULTS_TOTAL_COUNT (state, value) {
SET_TASK_RESULTS_TOTAL_COUNT(state, value) {
state.taskResultsTotalCount = value
},
SET_LOG_KEYWORD (state, value) {
SET_LOG_KEYWORD(state, value) {
state.logKeyword = value
},
SET_ERROR_LOG_DATA (state, value) {
SET_ERROR_LOG_DATA(state, value) {
state.errorLogData = value
},
SET_TASK_LOG_PAGE (state, value) {
SET_TASK_LOG_PAGE(state, value) {
state.taskLogPage = value
},
SET_TASK_LOG_PAGE_SIZE (state, value) {
SET_TASK_LOG_PAGE_SIZE(state, value) {
state.taskLogPageSize = value
},
SET_IS_LOG_AUTO_SCROLL (state, value) {
SET_IS_LOG_AUTO_SCROLL(state, value) {
state.isLogAutoScroll = value
},
SET_IS_LOG_AUTO_FETCH (state, value) {
SET_IS_LOG_AUTO_FETCH(state, value) {
state.isLogAutoFetch = value
},
SET_IS_LOG_FETCH_LOADING (state, value) {
SET_IS_LOG_FETCH_LOADING(state, value) {
state.isLogFetchLoading = value
},
SET_ACTIVE_ERROR_LOG_ITEM (state, value) {
SET_ACTIVE_ERROR_LOG_ITEM(state, value) {
state.activeErrorLogItem = value
}
}
const actions = {
getTaskData ({ state, dispatch, commit }, id) {
getTaskData({ state, dispatch, commit }, id) {
return request.get(`/tasks/${id}`)
.then(response => {
let data = response.data.data
const data = response.data.data
commit('SET_TASK_FORM', data)
dispatch('spider/getSpiderData', data.spider_id, { root: true })
dispatch('node/getNodeData', data.node_id, { root: true })
})
},
getTaskList ({ state, commit }) {
getTaskList({ state, commit }) {
return request.get('/tasks', {
page_num: state.pageNum,
page_size: state.pageSize,
@@ -171,24 +171,24 @@ const actions = {
commit('SET_TASK_LIST_TOTAL_COUNT', response.data.total)
})
},
deleteTask ({ state, dispatch }, id) {
deleteTask({ state, dispatch }, id) {
return request.delete(`/tasks/${id}`)
.then(() => {
dispatch('getTaskList')
})
},
deleteTaskMultiple ({ state }, ids) {
deleteTaskMultiple({ state }, ids) {
return request.delete(`/tasks`, {
ids: ids
})
},
restartTask ({ state, dispatch }, id) {
restartTask({ state, dispatch }, id) {
return request.post(`/tasks/${id}/restart`)
.then(() => {
dispatch('getTaskList')
})
},
getTaskLog ({ state, commit }, { id, keyword }) {
getTaskLog({ state, commit }, { id, keyword }) {
return request.get(`/tasks/${id}/log`, {
keyword,
page_num: state.taskLogPage,
@@ -199,18 +199,20 @@ const actions = {
commit('SET_TASK_LOG_TOTAL', response.data.total || 0)
// auto switch to next page if not reaching the end
if (state.isLogAutoScroll && state.taskLogTotal > (state.taskLogPage * state.taskLogPageSize)) {
commit('SET_TASK_LOG_PAGE', Math.ceil(state.taskLogTotal / state.taskLogPageSize))
if (state.isLogAutoScroll && state.taskLogTotal >
(state.taskLogPage * state.taskLogPageSize)) {
commit('SET_TASK_LOG_PAGE',
Math.ceil(state.taskLogTotal / state.taskLogPageSize))
}
})
},
getTaskErrorLog ({ state, commit }, id) {
getTaskErrorLog({ state, commit }, id) {
return request.get(`/tasks/${id}/error-log`, {})
.then(response => {
commit('SET_ERROR_LOG_DATA', response.data.data || [])
})
},
getTaskResults ({ state, commit }, id) {
getTaskResults({ state, commit }, id) {
return request.get(`/tasks/${id}/results`, {
page_num: state.resultsPageNum,
page_size: state.resultsPageSize
@@ -221,10 +223,11 @@ const actions = {
commit('SET_TASK_RESULTS_TOTAL_COUNT', response.data.total)
})
},
async getTaskResultExcel ({ state, commit }, id) {
const { data } = await request.request('GET', '/tasks/' + id + '/results/download', {}, {
responseType: 'blob' // important
})
async getTaskResultExcel({ state, commit }, id) {
const { data } = await request.request('GET',
'/tasks/' + id + '/results/download', {}, {
responseType: 'blob' // important
})
const downloadUrl = window.URL.createObjectURL(new Blob([data]))
const link = document.createElement('a')
@@ -237,7 +240,7 @@ const actions = {
link.click()
link.remove()
},
cancelTask ({ state, dispatch }, id) {
cancelTask({ state, dispatch }, id) {
return new Promise(resolve => {
request.post(`/tasks/${id}/cancel`)
.then(res => {

View File

@@ -1,10 +1,11 @@
import request from '../../api/request'
import { getToken, setToken, removeToken } from '@/utils/auth'
const user = {
namespaced: true,
state: {
// token: getToken(),
token: getToken(),
name: '',
avatar: '',
roles: [],
@@ -22,13 +23,13 @@ const user = {
},
getters: {
userInfo (state) {
userInfo(state) {
if (state.userInfo) return state.userInfo
const userInfoStr = window.localStorage.getItem('user_info')
if (!userInfoStr) return {}
return JSON.parse(userInfoStr)
},
token () {
token() {
return window.localStorage.getItem('token')
}
},
@@ -71,43 +72,41 @@ const user = {
actions: {
// 登录
async login ({ commit }, userInfo) {
async login({ commit }, userInfo) {
const username = userInfo.username.trim()
let res
res = await request.post('/login', { username, password: userInfo.password })
const res = await request.post('/login',
{ username, password: userInfo.password })
if (res.status === 200) {
const token = res.data.data
commit('SET_TOKEN', token)
window.localStorage.setItem('token', token)
setToken(token)
}
return res
},
// 获取用户信息
getInfo ({ commit, state }) {
return request.get('/me')
.then(response => {
// ensure compatibility
if (!response.data.data.setting.max_error_log) {
response.data.data.setting.max_error_log = 1000
}
if (!response.data.data.setting.log_expire_duration) {
response.data.data.setting.log_expire_duration = 3600 * 24
}
commit('SET_USER_INFO', response.data.data)
window.localStorage.setItem('user_info', JSON.stringify(response.data.data))
})
async getInfo({ commit, state }) {
const response = await request.get('/me')
// ensure compatibility
if (!response.data.data.setting.max_error_log) {
response.data.data.setting.max_error_log = 1000
}
commit('SET_USER_INFO', response.data.data)
window.localStorage.setItem('user_info',
JSON.stringify(response.data.data))
},
// 修改用户信息
postInfo ({ commit }, form) {
postInfo({ commit }, form) {
return request.post('/me', form)
},
// 注册
register ({ dispatch, commit, state }, userInfo) {
register({ dispatch, commit, state }, userInfo) {
return new Promise((resolve, reject) => {
request.put('/users', { username: userInfo.username, password: userInfo.password })
request.put('/users',
{ username: userInfo.username, password: userInfo.password })
.then(() => {
resolve()
})
@@ -118,7 +117,7 @@ const user = {
},
// 登出
logout ({ commit, state }) {
logout({ commit, state }) {
return new Promise((resolve, reject) => {
window.localStorage.removeItem('token')
window.localStorage.removeItem('user_info')
@@ -128,9 +127,13 @@ const user = {
resolve()
})
},
async resetToken({ commit }) {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
},
// 获取用户列表
getUserList ({ commit, state }) {
getUserList({ commit, state }) {
return new Promise((resolve, reject) => {
request.get('/users', {
page_num: state.pageNum,
@@ -144,34 +147,34 @@ const user = {
},
// 删除用户
deleteUser ({ state }, id) {
deleteUser({ state }, id) {
return request.delete(`/users/${id}`)
},
// 编辑用户
editUser ({ state }) {
editUser({ state }) {
return request.post(`/users/${state.userForm._id}`, state.userForm)
},
// 添加用户
addUser ({ dispatch, commit, state }) {
addUser({ dispatch, commit, state }) {
return request.put('/users-add', state.userForm)
},
// 新增全局变量
addGlobalVariable ({ commit, state }) {
addGlobalVariable({ commit, state }) {
return request.put(`/variable`, state.globalVariableForm)
.then(() => {
state.globalVariableForm = {}
})
},
// 获取全局变量列表
getGlobalVariable ({ commit, state }) {
getGlobalVariable({ commit, state }) {
request.get('/variables').then((response) => {
commit('SET_GLOBAL_VARIABLE_LIST', response.data.data)
})
},
// 删除全局变量
deleteGlobalVariable ({ commit, state }, id) {
deleteGlobalVariable({ commit, state }, id) {
return request.delete(`/variable/${id}`)
}
}

View File

@@ -20,7 +20,7 @@ const mutations = {
}
const actions = {
async getLatestRelease ({ commit }) {
async getLatestRelease({ commit }) {
const res = await request.get('/releases/latest')
if (!res.data.error) {
commit('SET_LATEST_RELEASE', res.data.data)

View File

@@ -1,15 +1,13 @@
import Cookies from 'js-cookie'
const TokenKey = 'token'
const TokenKey = 'Admin-Token'
export function getToken () {
return Cookies.get(TokenKey)
export function getToken() {
return window.localStorage.getItem(TokenKey)
}
export function setToken (token) {
return Cookies.set(TokenKey, token)
export function setToken(token) {
return window.localStorage.setItem(TokenKey, token)
}
export function removeToken () {
return Cookies.remove(TokenKey)
export function removeToken() {
return window.localStorage.removeItem(TokenKey)
}

View File

@@ -1,7 +1,7 @@
export default {
UUID: () => {
let s = []
let hexDigits = '0123456789abcdef'
const s = []
const hexDigits = '0123456789abcdef'
for (let i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
}

View File

@@ -1,6 +1,6 @@
export default {
htmlEscape: text => {
return text.replace(/[<>"&]/g, function (match, pos, originalText) {
return text.replace(/[<>"&]/g, function(match, pos, originalText) {
switch (match) {
case '<':
return '&lt;'

View File

@@ -1,5 +1,5 @@
// translate router.meta.title, be used in breadcrumb sidebar tagsview
export function generateTitle (title) {
export function generateTitle(title) {
const hasKey = this.$te('route.' + title)
if (hasKey) {

View File

@@ -4,7 +4,6 @@ import tour from './tour'
import log from './log'
import scrapy from './scrapy'
import doc from './doc'
import html from './html'
export default {
stats,
@@ -12,6 +11,5 @@ export default {
tour,
log,
scrapy,
doc,
html
doc
}

View File

@@ -2,7 +2,9 @@ const regexToken = ' :,.'
export default {
// errorRegex: new RegExp(`(?:[${regexToken}]|^)((?:error|exception|traceback)s?)(?:[${regexToken}]|$)`, 'gi')
errorRegex: new RegExp(`(?:[${regexToken}]|^)((?:error|exception|traceback)s?)(?:[${regexToken}]|$)`, 'gi'),
errorRegex: new RegExp(
`(?:[${regexToken}]|^)((?:error|exception|traceback)s?)(?:[${regexToken}]|$)`,
'gi'),
errorWhitelist: [
'log_count/ERROR'
]

View File

@@ -0,0 +1,123 @@
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
import i18n from '@/i18n'
import router from '@/router'
const codeMessage = {
200: '服务器成功返回请求的数据',
201: '新建或修改数据成功',
202: '一个请求已经进入后台排队异步任务',
204: '删除数据成功',
400: '发出的请求有错误服务器没有进行新建或修改数据的操作',
401: '用户没有权限令牌用户名密码错误',
403: '用户得到授权但是访问是被禁止的',
404: '发出的请求针对的是不存在的记录服务器没有进行操作',
406: '请求的格式不可得',
410: '请求的资源被永久删除且不会再得到的',
422: '当创建一个对象时发生一个验证错误',
500: '服务器发生错误请检查服务器',
502: '网关错误',
503: '服务不可用服务器暂时过载或维护',
504: '网关超时'
}
/**
* 异常处理程序
*/
const errorHandler = (error) => {
const { response } = error
const routePath = router.currentRoute.path
if (response && response.status) {
const errorText = codeMessage[response.status] || response.statusText
const { status } = response
Message({
message: `请求错误 ${status}: ${response.request.responseURL},${errorText}`,
type: 'error',
duration: 5 * 1000
})
switch (status) {
case 401:
if (routePath !== '/login' && routePath !== '/') {
MessageBox.confirm(
i18n.t('auth.login_expired_message'),
i18n.t('auth.login_expired_title'), {
confirmButtonText: i18n.t('auth.login_expired_confirm'),
cancelButtonText: i18n.t('auth.login_expired_cancel'),
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
break
default:
}
} else if (!response) {
Message({
message: `您的网络发生异常,无法连接服务器`,
type: 'error',
duration: 5 * 1000
})
}
return response
}
// 根据 VUE_APP_BASE_URL 生成 baseUrl
let baseUrl = process.env.VUE_APP_BASE_URL
? process.env.VUE_APP_BASE_URL
: 'http://localhost:8000'
if (!baseUrl.match(/^https?/i)) {
baseUrl = `${window.location.protocol}//${window.location.host}${process.env.VUE_APP_BASE_URL}`
}
// 如果 Docker 中设置了 CRAWLAB_API_ADDRESS 这个环境变量,则会将 baseUrl 覆盖
const CRAWLAB_API_ADDRESS = '###CRAWLAB_API_ADDRESS###'
if (!CRAWLAB_API_ADDRESS.match('CRAWLAB_API_ADDRESS')) {
baseUrl = CRAWLAB_API_ADDRESS
}
// create an axios instance
const service = axios.create({
baseURL: baseUrl, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['Authorization'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
return response
},
errorHandler
)
export default service

View File

@@ -1,6 +1,6 @@
import axios from 'axios'
const sendEvCrawlab = async (eventCategory, eventAction, eventLabel) => {
const sendEvCrawlab = async(eventCategory, eventAction, eventLabel) => {
await axios.get(process.env.VUE_APP_CRAWLAB_BASE_URL + '/track', {
params: {
uid: localStorage.getItem('uid'),
@@ -14,13 +14,13 @@ const sendEvCrawlab = async (eventCategory, eventAction, eventLabel) => {
}
export default {
sendPv (page) {
sendPv(page) {
if (localStorage.getItem('useStats') !== '0') {
window._hmt.push(['_trackPageview', page])
sendEvCrawlab('访问页面', page, '')
}
},
sendEv (category, eventName, optLabel, optValue) {
sendEv(category, eventName, optLabel, optValue) {
if (localStorage.getItem('useStats') !== '0') {
window._hmt.push(['_trackEvent', category, eventName, optLabel, optValue])
sendEvCrawlab(category, eventName, optLabel)

View File

@@ -2,7 +2,7 @@
* Created by jiachenpan on 16/11/18.
*/
export function isValidUsername (str) {
export function isValidUsername(str) {
if (!str) return false
if (str.length > 100) return false
return true
@@ -10,6 +10,6 @@ export function isValidUsername (str) {
// return validMap.indexOf(str.trim()) >= 0
}
export function isExternal (path) {
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More