Merge pull request #60 from tikazyq/develop

Develop
This commit is contained in:
Marvin Zhang
2019-06-11 21:12:03 +08:00
committed by GitHub
27 changed files with 283 additions and 1096 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
.idea
logs
*.log

53
Dockerfile Normal file
View File

@@ -0,0 +1,53 @@
# images
FROM ubuntu:latest
# source files
ADD . /opt/crawlab
# set as non-interactive
ENV DEBIAN_FRONTEND noninteractive
# environment variables
ENV NVM_DIR /usr/local/nvm
ENV NODE_VERSION 8.12.0
ENV WORK_DIR /opt/crawlab
# install pkg
RUN apt-get update \
&& apt-get install -y curl git net-tools iputils-ping ntp gnupg2 nginx redis python python3 python3-pip \
&& apt-get clean \
&& cp $WORK_DIR/crawlab.conf /etc/nginx/conf.d \
&& ln -s /usr/bin/pip3 /usr/local/bin/pip \
&& ln -s /usr/bin/python3 /usr/local/bin/python
# install mongodb
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4 \
&& echo "deb [ arch=amd64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.0.list \
&& apt-get update \
&& apt-get install -y mongodb-org \
&& apt-get clean \
&& mkdir -p /data/db
# install nvm
RUN curl https://raw.githubusercontent.com/creationix/nvm/v0.24.0/install.sh | bash \
&& . $NVM_DIR/nvm.sh \
&& nvm install v$NODE_VERSION \
&& nvm use v$NODE_VERSION \
&& nvm alias default v$NODE_VERSION
ENV NODE_PATH $NVM_DIR/versions/node/v$NODE_VERSION/lib/node_modules
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
# install frontend
RUN npm install -g yarn pm2 --registry=https://registry.npm.taobao.org \
&& cd /opt/crawlab/frontend \
&& yarn install --registry=https://registry.npm.taobao.org
# install backend
RUN pip install -U setuptools -i https://pypi.tuna.tsinghua.edu.cn/simple \
&& pip install -r /opt/crawlab/crawlab/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# start backend
EXPOSE 8080
EXPOSE 8000
WORKDIR /opt/crawlab
ENTRYPOINT ["/bin/sh", "/opt/crawlab/docker_init.sh"]

5
crawlab.conf Normal file
View File

@@ -0,0 +1,5 @@
server {
listen 8080;
root /opt/crawlab/frontend/dist;
index index.html;
}

View File

@@ -1,26 +0,0 @@
# images
#FROM python:latest
FROM ubuntu:latest
# source files
ADD . /opt/crawlab
# add dns
RUN cat /etc/resolv.conf
# install python
RUN apt-get update
RUN apt-get install -y python3 python3-pip net-tools iputils-ping vim ntp
# soft link
RUN ln -s /usr/bin/pip3 /usr/local/bin/pip
RUN ln -s /usr/bin/python3 /usr/local/bin/python
# install required libraries
RUN pip install -U setuptools
RUN pip install -r /opt/crawlab/requirements.txt
# execute apps
WORKDIR /opt/crawlab
CMD python ./bin/run_worker.py
CMD python app.py

View File

@@ -5,15 +5,11 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__fil
# 爬虫源码路径
PROJECT_SOURCE_FILE_FOLDER = os.path.join(BASE_DIR, "spiders")
# 配置python虚拟环境的路径
PYTHON_ENV_PATH = '/Users/yeqing/.pyenv/shims/python'
# 爬虫部署路径
# PROJECT_DEPLOY_FILE_FOLDER = '../deployfile'
PROJECT_DEPLOY_FILE_FOLDER = '/var/crawlab'
# 爬虫日志路径
PROJECT_LOGS_FOLDER = '../deployfile/logs'
PROJECT_LOGS_FOLDER = '/var/log/crawlab'
# 打包临时文件夹
PROJECT_TMP_FOLDER = '/tmp'
@@ -36,11 +32,6 @@ CELERY_TIMEZONE = 'Asia/Shanghai'
# 是否启用UTC
CELERY_ENABLE_UTC = True
# Celery Scheduler Redis URL
CELERY_BEAT_SCHEDULER = 'utils.redisbeat.RedisScheduler'
CELERY_REDIS_SCHEDULER_URL = 'redis://localhost:6379'
CELERY_REDIS_SCHEDULER_KEY = 'celery:beat:order_tasks'
# flower variables
FLOWER_API_ENDPOINT = 'http://localhost:5555/api'

View File

@@ -509,7 +509,10 @@ class SpiderApi(BaseApi):
}, r.status_code
# get html parse tree
sel = etree.HTML(r.content)
try:
sel = etree.HTML(r.content.decode('utf-8'))
except Exception as err:
sel = etree.HTML(r.content)
# remove unnecessary tags
unnecessary_tags = [
@@ -550,6 +553,7 @@ class SpiderApi(BaseApi):
'下页',
'next page',
'next',
'>'
]
for tag in sel.iter():
if tag.text is not None and tag.text.lower().strip() in next_page_text_list:
@@ -654,19 +658,24 @@ class SpiderApi(BaseApi):
# get list item selector
item_selector = None
item_selector_type = 'css'
if max_tag.get('id') is not None:
item_selector = f'#{max_tag.get("id")} > {self._get_children(max_tag)[0].tag}'
elif max_tag.get('class') is not None:
cls_str = '.'.join([x for x in max_tag.get("class").split(' ') if x != ''])
if len(sel.cssselect(f'.{cls_str}')) == 1:
item_selector = f'.{cls_str} > {self._get_children(max_tag)[0].tag}'
else:
item_selector = max_tag.getroottree().getpath(max_tag)
item_selector_type = 'xpath'
# get list fields
fields = []
if item_selector is not None:
first_tag = self._get_children(max_tag)[0]
for i, tag in enumerate(self._get_text_child_tags(first_tag)):
if len(first_tag.cssselect(f'{tag.tag}')) == 1:
el_list = first_tag.cssselect(f'{tag.tag}')
if len(el_list) == 1:
fields.append({
'name': f'field{i + 1}',
'type': 'css',
@@ -682,6 +691,15 @@ class SpiderApi(BaseApi):
'extract_type': 'text',
'query': f'{tag.tag}.{cls_str}',
})
else:
for j, el in enumerate(el_list):
if tag == el:
fields.append({
'name': f'field{i + 1}',
'type': 'css',
'extract_type': 'text',
'query': f'{tag.tag}:nth-of-type({j + 1})',
})
for i, tag in enumerate(self._get_a_child_tags(self._get_children(max_tag)[0])):
# if the tag is <a...></a>, extract its href
@@ -707,6 +725,7 @@ class SpiderApi(BaseApi):
return {
'status': 'ok',
'item_selector': item_selector,
'item_selector_type': item_selector_type,
'pagination_selector': pagination_selector,
'fields': fields
}

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import os
import sys
from urllib.parse import urlparse
from urllib.parse import urlparse, urljoin
import scrapy
@@ -72,8 +72,8 @@ def get_next_url(response):
# found next url
if next_url is not None:
if not next_url.startswith('http') and not next_url.startswith('//'):
u = urlparse(response.url)
next_url = f'{u.scheme}://{u.netloc}{next_url}'
return urljoin(response.url, next_url)
else:
return next_url
return None

View File

@@ -6,7 +6,7 @@ from time import sleep
from bson import ObjectId
from pymongo import ASCENDING, DESCENDING
from config import PROJECT_DEPLOY_FILE_FOLDER, PROJECT_LOGS_FOLDER, PYTHON_ENV_PATH, MONGO_HOST, MONGO_PORT, MONGO_DB
from config import PROJECT_DEPLOY_FILE_FOLDER, PROJECT_LOGS_FOLDER, MONGO_HOST, MONGO_PORT, MONGO_DB
from constants.task import TaskStatus
from db.manager import db_manager
from .celery import celery_app

View File

@@ -1,11 +1,11 @@
version: '3.3' # 表示该 Docker-Compose 文件使用的是 Version 2 file
services:
web: # 指定服务名称
app: # 指定服务名称
build: . # 指定 Dockerfile 所在路径
ports: # 指定端口映射
- "5001:5000"
task:
image: crawlab:v3
image: crawlab:latest
db:
image: mongo
restart: always

17
docker_init.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/sh
case $1 in
master)
cd /opt/crawlab/frontend \
&& npm run build:prod \
&& service nginx start \
&& mongod --fork --logpath /var/log/mongod.log
redis-server >> /var/log/redis-server.log 2>&1 &
python $WORK_DIR/crawlab/flower.py >> /opt/crawlab/flower.log 2>&1 &
python $WORK_DIR/crawlab/worker.py >> /opt/crawlab/worker.log 2>&1 &
python $WORK_DIR/crawlab/app.py
;;
worker)
python $WORK_DIR/crawlab/app.py >> /opt/crawlab/app.log 2>&1 &
python $WORK_DIR/crawlab/worker.py
;;
esac

36
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
# images
FROM node:8.12
# source files
ADD . /opt/crawlab/frontend
#更新apt-get源 使用163的源
#RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak
#COPY sources.list /etc/apt/sources.list
# environment variables
#ENV NVM_DIR /usr/local/nvm
#ENV NODE_VERSION 8.12.0
#ENV WORK_DIR /opt/crawlab/frontend
# install git curl
RUN apt-get update && apt-get install -y nginx
#RUN apt-get install -y git curl
# install nvm
#RUN curl https://raw.githubusercontent.com/creationix/nvm/v0.24.0/install.sh | bash \
# && . $NVM_DIR/nvm.sh \
# && nvm install v$NODE_VERSION \
# && nvm use v$NODE_VERSION \
# && nvm alias default v$NODE_VERSION
#ENV NODE_PATH $NVM_DIR/versions/node/v$NODE_VERSION/lib/node_modules
#ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
# install frontend
RUN npm install -g yarn pm2 --registry=https://registry.npm.taobao.org
RUN cd /opt/crawlab/frontend && yarn install --registry=https://registry.npm.taobao.org
# nginx config & start frontend
RUN cp $WORK_DIR/conf/crawlab.conf /etc/nginx/conf.d && service nginx reload
CMD ["npm", "run", "build:prod"]

View File

@@ -0,0 +1,5 @@
server {
listen 8080;
root /opt/crawlab/frontend/dist;
index index.html;
}

View File

@@ -8,8 +8,8 @@ http {
default_type application/octet-stream;
server {
listen 8888;
root /Users/yeqing/projects/crawlab-frontend/dist;
listen 8080;
root /opt/dist;
index index.html;
location ~ .*\.(js|css)?$ {

View File

@@ -27,6 +27,7 @@
"vue-codemirror-lite": "^1.0.4",
"vue-i18n": "^8.9.0",
"vue-router": "^3.0.1",
"vue-virtual-scroll-list": "^1.3.9",
"vuex": "^3.0.1"
},
"devDependencies": {

4
frontend/sources.list Normal file
View File

@@ -0,0 +1,4 @@
deb http://mirrors.aliyun.com/debian/ jessie main non-free contrib
deb http://mirrors.aliyun.com/debian/ jessie-proposed-updates main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ jessie main non-free contrib
deb-src http://mirrors.aliyun.com/debian/ jessie-proposed-updates main non-free contrib

View File

@@ -1,156 +0,0 @@
<template>
<div :class="className" :id="id" :style="{height:height,width:width}"/>
</template>
<script>
import echarts from 'echarts'
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
id: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '200px'
},
height: {
type: String,
default: '200px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(document.getElementById(this.id))
const xAxisData = []
const data = []
const data2 = []
for (let i = 0; i < 50; i++) {
xAxisData.push(i)
data.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5)
data2.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 3)
}
this.chart.setOption(
{
backgroundColor: '#08263a',
grid: {
left: '5%',
right: '5%'
},
xAxis: [{
show: false,
data: xAxisData
}, {
show: false,
data: xAxisData
}],
visualMap: {
show: false,
min: 0,
max: 50,
dimension: 0,
inRange: {
color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
}
},
yAxis: {
axisLine: {
show: false
},
axisLabel: {
textStyle: {
color: '#4a657a'
}
},
splitLine: {
show: true,
lineStyle: {
color: '#08263f'
}
},
axisTick: {
show: false
}
},
series: [{
name: 'back',
type: 'bar',
data: data2,
z: 1,
itemStyle: {
normal: {
opacity: 0.4,
barBorderRadius: 5,
shadowBlur: 3,
shadowColor: '#111'
}
}
}, {
name: 'Simulate Shadow',
type: 'line',
data,
z: 2,
showSymbol: false,
animationDelay: 0,
animationEasing: 'linear',
animationDuration: 1200,
lineStyle: {
normal: {
color: 'transparent'
}
},
areaStyle: {
normal: {
color: '#08263a',
shadowBlur: 50,
shadowColor: '#000'
}
}
}, {
name: 'front',
type: 'bar',
data,
xAxisIndex: 1,
z: 3,
itemStyle: {
normal: {
barBorderRadius: 5
}
}
}],
animationEasing: 'elasticOut',
animationEasingUpdate: 'elasticOut',
animationDelay(idx) {
return idx * 20
},
animationDelayUpdate(idx) {
return idx * 20
}
})
}
}
}
</script>

View File

@@ -1,227 +0,0 @@
<template>
<div :class="className" :id="id" :style="{height:height,width:width}"/>
</template>
<script>
import echarts from 'echarts'
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
id: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '200px'
},
height: {
type: String,
default: '200px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(document.getElementById(this.id))
this.chart.setOption({
backgroundColor: '#394056',
title: {
top: 20,
text: 'Requests',
textStyle: {
fontWeight: 'normal',
fontSize: 16,
color: '#F1F1F3'
},
left: '1%'
},
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
color: '#57617B'
}
}
},
legend: {
top: 20,
icon: 'rect',
itemWidth: 14,
itemHeight: 5,
itemGap: 13,
data: ['CMCC', 'CTCC', 'CUCC'],
right: '4%',
textStyle: {
fontSize: 12,
color: '#F1F1F3'
}
},
grid: {
top: 100,
left: '2%',
right: '2%',
bottom: '2%',
containLabel: true
},
xAxis: [{
type: 'category',
boundaryGap: false,
axisLine: {
lineStyle: {
color: '#57617B'
}
},
data: ['13:00', '13:05', '13:10', '13:15', '13:20', '13:25', '13:30', '13:35', '13:40', '13:45', '13:50', '13:55']
}],
yAxis: [{
type: 'value',
name: '(%)',
axisTick: {
show: false
},
axisLine: {
lineStyle: {
color: '#57617B'
}
},
axisLabel: {
margin: 10,
textStyle: {
fontSize: 14
}
},
splitLine: {
lineStyle: {
color: '#57617B'
}
}
}],
series: [{
name: 'CMCC',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
showSymbol: false,
lineStyle: {
normal: {
width: 1
}
},
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(137, 189, 27, 0.3)'
}, {
offset: 0.8,
color: 'rgba(137, 189, 27, 0)'
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: 'rgb(137,189,27)',
borderColor: 'rgba(137,189,2,0.27)',
borderWidth: 12
}
},
data: [220, 182, 191, 134, 150, 120, 110, 125, 145, 122, 165, 122]
}, {
name: 'CTCC',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
showSymbol: false,
lineStyle: {
normal: {
width: 1
}
},
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(0, 136, 212, 0.3)'
}, {
offset: 0.8,
color: 'rgba(0, 136, 212, 0)'
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: 'rgb(0,136,212)',
borderColor: 'rgba(0,136,212,0.2)',
borderWidth: 12
}
},
data: [120, 110, 125, 145, 122, 165, 122, 220, 182, 191, 134, 150]
}, {
name: 'CUCC',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
showSymbol: false,
lineStyle: {
normal: {
width: 1
}
},
areaStyle: {
normal: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 0,
color: 'rgba(219, 50, 51, 0.3)'
}, {
offset: 0.8,
color: 'rgba(219, 50, 51, 0)'
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 10
}
},
itemStyle: {
normal: {
color: 'rgb(219,50,51)',
borderColor: 'rgba(219,50,51,0.2)',
borderWidth: 12
}
},
data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
}]
})
}
}
}
</script>

View File

@@ -1,271 +0,0 @@
<template>
<div :class="className" :id="id" :style="{height:height,width:width}"/>
</template>
<script>
import echarts from 'echarts'
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
id: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '200px'
},
height: {
type: String,
default: '200px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.initChart()
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(document.getElementById(this.id))
const xData = (function() {
const data = []
for (let i = 1; i < 13; i++) {
data.push(i + 'month')
}
return data
}())
this.chart.setOption({
backgroundColor: '#344b58',
title: {
text: 'statistics',
x: '20',
top: '20',
textStyle: {
color: '#fff',
fontSize: '22'
},
subtextStyle: {
color: '#90979c',
fontSize: '16'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
textStyle: {
color: '#fff'
}
}
},
grid: {
left: '5%',
right: '5%',
borderWidth: 0,
top: 150,
bottom: 95,
textStyle: {
color: '#fff'
}
},
legend: {
x: '5%',
top: '10%',
textStyle: {
color: '#90979c'
},
data: ['female', 'male', 'average']
},
calculable: true,
xAxis: [{
type: 'category',
axisLine: {
lineStyle: {
color: '#90979c'
}
},
splitLine: {
show: false
},
axisTick: {
show: false
},
splitArea: {
show: false
},
axisLabel: {
interval: 0
},
data: xData
}],
yAxis: [{
type: 'value',
splitLine: {
show: false
},
axisLine: {
lineStyle: {
color: '#90979c'
}
},
axisTick: {
show: false
},
axisLabel: {
interval: 0
},
splitArea: {
show: false
}
}],
dataZoom: [{
show: true,
height: 30,
xAxisIndex: [
0
],
bottom: 30,
start: 10,
end: 80,
handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
handleSize: '110%',
handleStyle: {
color: '#d3dee5'
},
textStyle: {
color: '#fff' },
borderColor: '#90979c'
}, {
type: 'inside',
show: true,
height: 15,
start: 1,
end: 35
}],
series: [{
name: 'female',
type: 'bar',
stack: 'total',
barMaxWidth: 35,
barGap: '10%',
itemStyle: {
normal: {
color: 'rgba(255,144,128,1)',
label: {
show: true,
textStyle: {
color: '#fff'
},
position: 'insideTop',
formatter(p) {
return p.value > 0 ? p.value : ''
}
}
}
},
data: [
709,
1917,
2455,
2610,
1719,
1433,
1544,
3285,
5208,
3372,
2484,
4078
]
},
{
name: 'male',
type: 'bar',
stack: 'total',
itemStyle: {
normal: {
color: 'rgba(0,191,183,1)',
barBorderRadius: 0,
label: {
show: true,
position: 'top',
formatter(p) {
return p.value > 0 ? p.value : ''
}
}
}
},
data: [
327,
1776,
507,
1200,
800,
482,
204,
1390,
1001,
951,
381,
220
]
}, {
name: 'average',
type: 'line',
stack: 'total',
symbolSize: 10,
symbol: 'circle',
itemStyle: {
normal: {
color: 'rgba(252,230,48,1)',
barBorderRadius: 0,
label: {
show: true,
position: 'top',
formatter(p) {
return p.value > 0 ? p.value : ''
}
}
}
},
data: [
1036,
3693,
2962,
3810,
2519,
1915,
1748,
4675,
6209,
4323,
2865,
4298
]
}
]
})
}
}
}
</script>

View File

@@ -1,32 +0,0 @@
import { debounce } from '@/utils'
export default {
data() {
return {
sidebarElm: null
}
},
mounted() {
this.__resizeHandler = debounce(() => {
if (this.chart) {
this.chart.resize()
}
}, 100)
window.addEventListener('resize', this.__resizeHandler)
this.sidebarElm = document.getElementsByClassName('sidebar-container')[0]
this.sidebarElm && this.sidebarElm.addEventListener('transitionend', this.sidebarResizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this.__resizeHandler)
this.sidebarElm && this.sidebarElm.removeEventListener('transitionend', this.sidebarResizeHandler)
},
methods: {
sidebarResizeHandler(e) {
if (e.propertyName === 'width') {
this.__resizeHandler()
}
}
}
}

View File

@@ -255,7 +255,9 @@ export default {
.then(response => {
if (response.data.item_selector) {
this.$set(this.spiderForm, 'item_selector', response.data.item_selector)
this.$set(this.spiderForm, 'item_selector_type', 'css')
}
if (response.data.item_selector_type) {
this.$set(this.spiderForm, 'item_selector_type', response.data.item_selector_type)
}
if (response.data.fields && response.data.fields.length) {

View File

@@ -1,61 +0,0 @@
<template>
<el-select ref="dragSelect" v-model="selectVal" v-bind="$attrs" class="drag-select" multiple v-on="$listeners">
<slot/>
</el-select>
</template>
<script>
import Sortable from 'sortablejs'
export default {
name: 'DragSelect',
props: {
value: {
type: Array,
required: true
}
},
computed: {
selectVal: {
get() {
return [...this.value]
},
set(val) {
this.$emit('input', [...val])
}
}
},
mounted() {
this.setSort()
},
methods: {
setSort() {
const el = this.$refs.dragSelect.$el.querySelectorAll('.el-select__tags > span')[0]
this.sortable = Sortable.create(el, {
ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
setData: function(dataTransfer) {
dataTransfer.setData('Text', '')
// to avoid Firefox bug
// Detail see : https://github.com/RubaXa/Sortable/issues/1012
},
onEnd: evt => {
const targetRow = this.value.splice(evt.oldIndex, 1)[0]
this.value.splice(evt.newIndex, 0, targetRow)
}
})
}
}
}
</script>
<style scoped>
.drag-select >>> .sortable-ghost{
opacity: .8;
color: #fff!important;
background: #42b983!important;
}
.drag-select >>> .el-tag{
cursor: pointer;
}
</style>

View File

@@ -1,297 +0,0 @@
<template>
<div :ref="id" :action="url" :id="id" class="dropzone">
<input type="file" name="file">
</div>
</template>
<script>
import Dropzone from 'dropzone'
import 'dropzone/dist/dropzone.css'
// import { getToken } from 'api/qiniu';
Dropzone.autoDiscover = false
export default {
props: {
id: {
type: String,
required: true
},
url: {
type: String,
required: true
},
clickable: {
type: Boolean,
default: true
},
defaultMsg: {
type: String,
default: '上传图片'
},
acceptedFiles: {
type: String,
default: ''
},
thumbnailHeight: {
type: Number,
default: 200
},
thumbnailWidth: {
type: Number,
default: 200
},
showRemoveLink: {
type: Boolean,
default: true
},
maxFilesize: {
type: Number,
default: 2
},
maxFiles: {
type: Number,
default: 3
},
autoProcessQueue: {
type: Boolean,
default: true
},
useCustomDropzoneOptions: {
type: Boolean,
default: false
},
defaultImg: {
default: '',
type: [String, Array]
},
couldPaste: {
type: Boolean,
default: false
}
},
data() {
return {
dropzone: '',
initOnce: true
}
},
watch: {
defaultImg(val) {
if (val.length === 0) {
this.initOnce = false
return
}
if (!this.initOnce) return
this.initImages(val)
this.initOnce = false
}
},
mounted() {
const element = document.getElementById(this.id)
const vm = this
this.dropzone = new Dropzone(element, {
clickable: this.clickable,
thumbnailWidth: this.thumbnailWidth,
thumbnailHeight: this.thumbnailHeight,
maxFiles: this.maxFiles,
maxFilesize: this.maxFilesize,
dictRemoveFile: 'Remove',
addRemoveLinks: this.showRemoveLink,
acceptedFiles: this.acceptedFiles,
autoProcessQueue: this.autoProcessQueue,
dictDefaultMessage: '<i style="margin-top: 3em;display: inline-block" class="material-icons">' + this.defaultMsg + '</i><br>Drop files here to upload',
dictMaxFilesExceeded: '只能一个图',
previewTemplate: '<div class="dz-preview dz-file-preview"> <div class="dz-image" style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" ><img style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" data-dz-thumbnail /></div> <div class="dz-details"><div class="dz-size"><span data-dz-size></span></div> <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div> <div class="dz-error-message"><span data-dz-errormessage></span></div> <div class="dz-success-mark"> <i class="material-icons">done</i> </div> <div class="dz-error-mark"><i class="material-icons">error</i></div></div>',
init() {
const val = vm.defaultImg
if (!val) return
if (Array.isArray(val)) {
if (val.length === 0) return
val.map((v, i) => {
const mockFile = { name: 'name' + i, size: 12345, url: v }
this.options.addedfile.call(this, mockFile)
this.options.thumbnail.call(this, mockFile, v)
mockFile.previewElement.classList.add('dz-success')
mockFile.previewElement.classList.add('dz-complete')
vm.initOnce = false
return true
})
} else {
const mockFile = { name: 'name', size: 12345, url: val }
this.options.addedfile.call(this, mockFile)
this.options.thumbnail.call(this, mockFile, val)
mockFile.previewElement.classList.add('dz-success')
mockFile.previewElement.classList.add('dz-complete')
vm.initOnce = false
}
},
accept: (file, done) => {
/* 七牛*/
// const token = this.$store.getters.token;
// getToken(token).then(response => {
// file.token = response.data.qiniu_token;
// file.key = response.data.qiniu_key;
// file.url = response.data.qiniu_url;
// done();
// })
done()
},
sending: (file, xhr, formData) => {
// formData.append('token', file.token);
// formData.append('key', file.key);
vm.initOnce = false
}
})
if (this.couldPaste) {
document.addEventListener('paste', this.pasteImg)
}
this.dropzone.on('success', file => {
vm.$emit('dropzone-success', file, vm.dropzone.element)
})
this.dropzone.on('addedfile', file => {
vm.$emit('dropzone-fileAdded', file)
})
this.dropzone.on('removedfile', file => {
vm.$emit('dropzone-removedFile', file)
})
this.dropzone.on('error', (file, error, xhr) => {
vm.$emit('dropzone-error', file, error, xhr)
})
this.dropzone.on('successmultiple', (file, error, xhr) => {
vm.$emit('dropzone-successmultiple', file, error, xhr)
})
},
destroyed() {
document.removeEventListener('paste', this.pasteImg)
this.dropzone.destroy()
},
methods: {
removeAllFiles() {
this.dropzone.removeAllFiles(true)
},
processQueue() {
this.dropzone.processQueue()
},
pasteImg(event) {
const items = (event.clipboardData || event.originalEvent.clipboardData).items
if (items[0].kind === 'file') {
this.dropzone.addFile(items[0].getAsFile())
}
},
initImages(val) {
if (!val) return
if (Array.isArray(val)) {
val.map((v, i) => {
const mockFile = { name: 'name' + i, size: 12345, url: v }
this.dropzone.options.addedfile.call(this.dropzone, mockFile)
this.dropzone.options.thumbnail.call(this.dropzone, mockFile, v)
mockFile.previewElement.classList.add('dz-success')
mockFile.previewElement.classList.add('dz-complete')
return true
})
} else {
const mockFile = { name: 'name', size: 12345, url: val }
this.dropzone.options.addedfile.call(this.dropzone, mockFile)
this.dropzone.options.thumbnail.call(this.dropzone, mockFile, val)
mockFile.previewElement.classList.add('dz-success')
mockFile.previewElement.classList.add('dz-complete')
}
}
}
}
</script>
<style scoped>
.dropzone {
border: 2px solid #E5E5E5;
font-family: 'Roboto', sans-serif;
color: #777;
transition: background-color .2s linear;
padding: 5px;
}
.dropzone:hover {
background-color: #F6F6F6;
}
i {
color: #CCC;
}
.dropzone .dz-image img {
width: 100%;
height: 100%;
}
.dropzone input[name='file'] {
display: none;
}
.dropzone .dz-preview .dz-image {
border-radius: 0px;
}
.dropzone .dz-preview:hover .dz-image img {
transform: none;
-webkit-filter: none;
width: 100%;
height: 100%;
}
.dropzone .dz-preview .dz-details {
bottom: 0px;
top: 0px;
color: white;
background-color: rgba(33, 150, 243, 0.8);
transition: opacity .2s linear;
text-align: left;
}
.dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
background-color: transparent;
}
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
border: none;
}
.dropzone .dz-preview .dz-details .dz-filename:hover span {
background-color: transparent;
border: none;
}
.dropzone .dz-preview .dz-remove {
position: absolute;
z-index: 30;
color: white;
margin-left: 15px;
padding: 10px;
top: inherit;
bottom: 15px;
border: 2px white solid;
text-decoration: none;
text-transform: uppercase;
font-size: 0.8rem;
font-weight: 800;
letter-spacing: 1.1px;
opacity: 0;
}
.dropzone .dz-preview:hover .dz-remove {
opacity: 1;
}
.dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
margin-left: -40px;
margin-top: -50px;
}
.dropzone .dz-preview .dz-success-mark i, .dropzone .dz-preview .dz-error-mark i {
color: white;
font-size: 5rem;
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="log-item">
<div class="line-no">{{index}}</div>
<div class="line-content">{{data}}</div>
</div>
</template>
<script>
export default {
name: 'LogItem',
props: {
index: {
type: Number,
default: 1
},
data: {
type: String,
default: ''
}
}
}
</script>
<style scoped>
.log-item {
display: flex;
}
.log-item .line-no {
margin-right: 10px;
text-align: right;
flex-basis: 40px;
}
.log-item .line-content {
display: inline-block;
flex-basis: calc(100% - 50px);
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<virtual-list
:size="18"
:remain="100"
:item="item"
:itemcount="logData.length"
:itemprops="getItemProps"
>
</virtual-list>
</template>
<script>
import LogItem from './LogItem'
import VirtualList from 'vue-virtual-scroll-list'
export default {
name: 'LogView',
components: {
VirtualList
},
props: {
data: {
type: String,
default: ''
}
},
data () {
return {
item: LogItem
}
},
computed: {
logData () {
return this.data.split('\n')
.map((d, i) => {
return {
index: i + 1,
data: d
}
})
}
},
methods: {
getItemProps (index) {
const logItem = this.logData[index]
return {
// <item/> will render with itemProps.
// https://vuejs.org/v2/guide/render-function.html#createElement-Arguments
props: {
index: logItem.index,
data: logItem.data
}
}
}
},
mounted () {
}
}
</script>
<style scoped>
.log-view {
min-height: 100%;
overflow-y: scroll;
list-style: none;
}
.log-view .log-line {
display: flex;
}
.log-view .log-line:nth-child(odd) {
}
.log-view .log-line:nth-child(even) {
}
</style>

View File

@@ -159,6 +159,7 @@ export const constantRouterMap = [
name: 'Deploy',
path: '/deploys',
component: Layout,
hidden: true,
meta: {
title: 'Deploy',
icon: 'fa fa-cloud'

View File

@@ -7,11 +7,7 @@
</el-tab-pane>
<el-tab-pane :label="$t('Log')" name="log">
<el-card>
<div class="log-view">
<pre>
{{taskLog}}
</pre>
</div>
<log-view :data="taskLog"/>
</el-card>
</el-tab-pane>
<el-tab-pane :label="$t('Results')" name="results">
@@ -37,10 +33,12 @@ import {
} from 'vuex'
import TaskOverview from '../../components/Overview/TaskOverview'
import GeneralTableView from '../../components/TableView/GeneralTableView'
import LogView from '../../components/ScrollView/LogView'
export default {
name: 'TaskDetail',
components: {
LogView,
GeneralTableView,
TaskOverview
},

View File

@@ -8493,6 +8493,11 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.8.2:
version "1.8.2"
resolved "http://registry.npm.taobao.org/vue-template-es2015-compiler/download/vue-template-es2015-compiler-1.8.2.tgz#dd73e80ba58bb65dd7a8aa2aeef6089cf6116f2a"
vue-virtual-scroll-list@^1.3.9:
version "1.3.9"
resolved "https://registry.npm.taobao.org/vue-virtual-scroll-list/download/vue-virtual-scroll-list-1.3.9.tgz#ba3ce6425374fb323ea83ab33daa2727117808ed"
integrity sha1-ujzmQlN0+zI+qDqzPaonJxF4CO0=
vue@^2.3.3:
version "2.6.10"
resolved "https://registry.npm.taobao.org/vue/download/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"