added setup.py

This commit is contained in:
Marvin Zhang
2019-03-03 10:48:04 +08:00
parent d26f43e09e
commit 8361c04de9
151 changed files with 21609 additions and 10 deletions

228
frontend/src/views/404.vue Normal file
View File

@@ -0,0 +1,228 @@
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
</div>
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__info">版权所有
<a class="link-type" href="https://wallstreetcn.com" target="_blank">华尔街见闻</a>
</div>
<div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">请检查您输入的网址是否正确请点击以下按钮返回主页或者发送错误报告</div>
<a href="" class="bullshit__return-home">返回首页</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Page404',
computed: {
message () {
return '网管说这个页面你不能进......'
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.wscn-http404-container{
transform: translate(-50%,-50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>

View File

@@ -0,0 +1,32 @@
<template>
<div class="dashboard-container">
<div class="dashboard-text">name:{{ name }}</div>
<div class="dashboard-text">roles:<span v-for="role in roles" :key="role">{{ role }}</span></div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Dashboard',
computed: {
...mapGetters([
'name',
'roles'
])
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.dashboard {
&-container {
margin: 30px;
}
&-text {
font-size: 30px;
line-height: 46px;
}
}
</style>

View File

@@ -0,0 +1,15 @@
<template>
<div class="">
NodeDetail
</div>
</template>
<script>
export default {
name: 'NodeDetail'
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,181 @@
<template>
<div class="app-container">
<!--filter-->
<div class="filter">
<el-input prefix-icon="el-icon-search"
placeholder="Search"
class="filter-search"
v-model="filter.keyword"
@change="onSearch">
</el-input>
<div class="right">
<el-button type="success"
icon="el-icon-refresh"
class="refresh"
@click="onRefresh">
Refresh
</el-button>
</div>
</div>
<!--table list-->
<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="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>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'node_id'"
:key="col.name"
:label="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>
</template>
</el-table-column>
<el-table-column v-else
:key="col.name"
:property="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
</el-table-column>
</template>
<el-table-column label="Action" align="center" width="160">
<template slot-scope="scope">
<el-tooltip content="View" placement="top">
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)"></el-button>
</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>
</div>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
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: 'Finish 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
})
}
},
methods: {
onSearch (value) {
console.log(value)
},
onRefresh () {
this.$store.dispatch('deploy/getDeployList')
},
onView (row) {
this.$router.push(`/deploys/${row._id}`)
},
onClickSpider (row) {
this.$router.push(`/spiders/${row.spider_id.$oid}`)
},
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">
.filter {
display: flex;
justify-content: space-between;
.filter-search {
width: 240px;
}
.add {
}
}
.table {
margin-top: 20px;
border-radius: 5px;
}
.delete-confirm {
background-color: red;
}
.el-table .el-button {
padding: 7px;
}
.el-table .a-tag {
text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<div class="app-container">
<el-form ref="form" :model="form" label-width="120px">
<el-form-item label="Activity name">
<el-input v-model="form.name"/>
</el-form-item>
<el-form-item label="Activity zone">
<el-select v-model="form.region" placeholder="please select your zone">
<el-option label="Zone one" value="shanghai"/>
<el-option label="Zone two" value="beijing"/>
</el-select>
</el-form-item>
<el-form-item label="Activity time">
<el-col :span="11">
<el-date-picker v-model="form.date1" type="date" placeholder="Pick a date" style="width: 100%;"/>
</el-col>
<el-col :span="2" class="line">-</el-col>
<el-col :span="11">
<el-time-picker v-model="form.date2" type="fixed-time" placeholder="Pick a time" style="width: 100%;"/>
</el-col>
</el-form-item>
<el-form-item label="Instant delivery">
<el-switch v-model="form.delivery"/>
</el-form-item>
<el-form-item label="Activity type">
<el-checkbox-group v-model="form.type">
<el-checkbox label="Online activities" name="type"/>
<el-checkbox label="Promotion activities" name="type"/>
<el-checkbox label="Offline activities" name="type"/>
<el-checkbox label="Simple brand exposure" name="type"/>
</el-checkbox-group>
</el-form-item>
<el-form-item label="Resources">
<el-radio-group v-model="form.resource">
<el-radio label="Sponsor"/>
<el-radio label="Venue"/>
</el-radio-group>
</el-form-item>
<el-form-item label="Activity form">
<el-input v-model="form.desc" type="textarea"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">Create</el-button>
<el-button @click="onCancel">Cancel</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
data () {
return {
form: {
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: ''
}
}
},
methods: {
onSubmit () {
this.$message('submit!')
},
onCancel () {
this.$message({
message: 'cancel!',
type: 'warning'
})
}
}
}
</script>
<style scoped>
.line{
text-align: center;
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<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">
<el-card class="metric-card" shadow="hover">
<el-col :span="6" class="icon-col">
<i :class="m.icon" :style="{color:m.color}"></i>
</el-col>
<el-col :span="18" class="text-col">
<el-row>
<label class="label">{{m.label}}</label>
</el-row>
<el-row>
<div class="value">{{overviewStats[m.name]}}</div>
</el-row>
</el-col>
</el-card>
</li>
</ul>
</el-row>
<el-row>
<el-card shadow="hover">
<h4 class="title">Daily New Tasks</h4>
<div id="echarts-daily-tasks" class="echarts-box"></div>
</el-card>
</el-row>
</div>
</template>
<script>
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-play', color: '#f56c6c', path: 'tasks' },
{ name: 'spider_count', label: 'Spiders', icon: 'fa fa-bug', color: '#67c23a', path: 'spiders' },
{ name: 'node_count', label: 'Active Nodes', icon: 'fa fa-server', color: '#409EFF', path: 'nodes' },
{ name: 'deploy_count', label: 'Total Deploys', icon: 'fa fa-cloud', color: '#409EFF', path: 'deploys' }
]
}
},
methods: {
initEchartsDailyTasks () {
const option = {
xAxis: {
type: 'category',
data: this.dailyTasks.map(d => d.date)
},
yAxis: {
type: 'value'
},
series: [{
data: this.dailyTasks.map(d => d.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}`)
}
},
created () {
request.get('/stats/get_home_stats')
.then(response => {
// overview stats
this.overviewStats = response.data.overview_stats
// daily tasks
this.dailyTasks = response.data.daily_tasks
this.initEchartsDailyTasks()
})
}
}
</script>
<style scoped lang="scss">
.metric-list {
margin-top: 0;
padding-left: 0;
list-style: none;
display: flex;
font-size: 16px;
.metric-item:last-child .metric-card {
margin-right: 0;
}
.metric-item {
flex-basis: 25%;
.metric-card:hover {
}
.metric-card {
margin-right: 30px;
cursor: pointer;
.icon-col {
text-align: right;
i {
margin-bottom: 15px;
font-size: 56px;
}
}
.text-col {
padding-left: 20px;
height: 76px;
text-align: center;
.label {
cursor: pointer;
font-size: 16px;
display: block;
height: 24px;
color: grey;
font-weight: 900;
}
.value {
font-size: 24px;
display: block;
height: 32px;
}
}
}
}
}
.title {
padding: 0;
margin: 0;
}
#echarts-daily-tasks {
height: 360px;
width: 100%;
}
.el-card {
/*border: 1px solid lightgrey;*/
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"></div>
<sidebar class="sidebar-container"/>
<div class="main-container">
<navbar/>
<!--<tags-view/>-->
<app-main/>
</div>
</div>
</template>
<script>
import {
Navbar,
Sidebar,
AppMain
// TagsView
} from './components'
import ResizeMixin from './mixin/ResizeHandler'
export default {
name: 'Layout',
components: {
Navbar,
Sidebar,
// TagsView,
AppMain
},
mixins: [ResizeMixin],
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'
}
}
},
methods: {
handleClickOutside () {
this.$store.dispatch('CloseSideBar', { withoutAnimation: false })
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
@import "../../../src/styles/mixin.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
</style>

View File

@@ -0,0 +1,29 @@
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<!-- or name="fade" -->
<router-view :key="key"></router-view>
<!--<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()
}
}
}
</script>
<style scoped>
.app-main {
/*50 = navbar */
min-height: calc(100vh - 50px);
position: relative;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,99 @@
<template>
<div class="navbar">
<hamburger :toggle-click="toggleSideBar" :is-active="sidebar.opened" class="hamburger-container"/>
<breadcrumb/>
<el-dropdown class="avatar-container" trigger="click">
<div class="avatar-wrapper">
<img src="https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif?imageView2/1/w/80/h/80"
class="user-avatar">
<i class="el-icon-caret-bottom"/>
</div>
<el-dropdown-menu slot="dropdown" class="user-dropdown">
<!--<router-link class="inlineBlock" to="/">-->
<!--<el-dropdown-item>-->
<!--Home-->
<!--</el-dropdown-item>-->
<!--</router-link>-->
<el-dropdown-item divided>
<span style="display:block;" @click="logout">LogOut</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
export default {
components: {
Breadcrumb,
Hamburger
},
computed: {
...mapGetters([
'sidebar',
'avatar'
])
},
methods: {
toggleSideBar () {
this.$store.dispatch('ToggleSideBar')
},
logout () {
this.$router.push('/login')
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.navbar {
height: 50px;
line-height: 50px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
.hamburger-container {
line-height: 58px;
height: 50px;
float: left;
padding: 0 10px;
}
.screenfull {
position: absolute;
right: 90px;
top: 16px;
color: red;
}
.avatar-container {
height: 50px;
display: inline-block;
position: absolute;
right: 35px;
.avatar-wrapper {
cursor: pointer;
margin-top: 5px;
position: relative;
line-height: initial;
.user-avatar {
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
</style>

View File

@@ -0,0 +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
}
vnodes.push(<span class={icon} style={style}/>)
}
if (title) {
vnodes.push(<span class="title" slot='title'>{(title)}</span>)
}
return vnodes
}
}
</script>

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
<template>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:show-timeout="200"
:default-active="routeLevel1"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:active-text-color="variables.menuActiveText"
mode="vertical"
>
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path"/>
</el-menu>
</el-scrollbar>
</template>
<script>
import { mapGetters } from 'vuex'
import variables from '@/styles/variables.scss'
import SidebarItem from './SidebarItem'
export default {
components: { SidebarItem },
computed: {
...mapGetters([
'sidebar'
]),
routeLevel1 () {
let pathArray = this.$route.path.split('/')
return `/${pathArray[1]}`
},
routes () {
// console.log(this.$router.options.routes.filter(d => !d.hidden))
return this.$router.options.routes
},
variables () {
return variables
},
isCollapse () {
return !this.sidebar.opened
}
}
}
</script>

View File

@@ -0,0 +1,294 @@
<template>
<div class="tags-view-container">
<scroll-pane ref="scrollPane" class="tags-view-wrapper">
<router-link
v-for="tag in visitedViews"
ref="tag"
:class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
:key="tag.path"
tag="span"
class="tags-view-item"
@click.middle.native="closeSelectedTag(tag)"
@contextmenu.prevent.native="openMenu(tag,$event)">
{{ generateTitle(tag.title) }}
<span v-if="!tag.meta.affix" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)"/>
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">{{ $t('tagsView.refresh') }}</li>
<li v-if="!(selectedTag.meta&&selectedTag.meta.affix)" @click="closeSelectedTag(selectedTag)">{{
$t('tagsView.close') }}
</li>
<li @click="closeOthersTags">{{ $t('tagsView.closeOthers') }}</li>
<li @click="closeAllTags(selectedTag)">{{ $t('tagsView.closeAll') }}</li>
</ul>
</div>
</template>
<script>
import ScrollPane from '@/components/ScrollPane'
import { generateTitle } from '@/utils/i18n'
import path from 'path'
export default {
components: { ScrollPane },
data () {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: []
}
},
computed: {
visitedViews () {
return this.$store.state.tagsView.visitedViews
},
routers () {
return this.$store.state.permission.routers
}
},
watch: {
$route () {
this.addTags()
this.moveToCurrentTag()
},
visible (value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
mounted () {
this.initTags()
this.addTags()
},
methods: {
generateTitle, // generateTitle by vue-i18n
isActive (route) {
return route.path === this.$route.path
},
filterAffixTags (routes, basePath = '/') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
tags.push({
path: path.resolve(basePath, route.path),
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
},
initTags () {
const affixTags = this.affixTags = this.filterAffixTags(this.routers)
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('addVisitedView', tag)
}
}
},
addTags () {
const { name } = this.$route
if (name) {
this.$store.dispatch('addView', this.$route)
}
return false
},
moveToCurrentTag () {
const tags = this.$refs.tag
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('updateVisitedView', this.$route)
}
break
}
}
})
},
refreshSelectedTag (view) {
this.$store.dispatch('delCachedView', view).then(() => {
const { fullPath } = view
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
})
},
closeSelectedTag (view) {
this.$store.dispatch('delView', view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews)
}
})
},
closeOthersTags () {
this.$router.push(this.selectedTag)
this.$store.dispatch('delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags (view) {
this.$store.dispatch('delAllViews').then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === view.path)) {
return
}
this.toLastView(visitedViews)
})
},
toLastView (visitedViews) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView)
} else {
// You can set another route
this.$router.push('/')
}
},
openMenu (tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
this.left = maxLeft
} else {
this.left = left
}
this.top = e.clientY
this.visible = true
this.selectedTag = tag
},
closeMenu () {
this.visible = false
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
}
}
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 100;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
</style>
<style rel="stylesheet/scss" lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
.tags-view-item {
.el-icon-close {
width: 16px;
height: 16px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all .3s cubic-bezier(.645, .045, .355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
}
</style>

View File

@@ -0,0 +1,4 @@
export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
export { default as TagsView } from './TagsView'
export { default as AppMain } from './AppMain'

View File

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

View File

@@ -0,0 +1,209 @@
<template>
<div class="login-container">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on"
label-position="left">
<h3 class="title">Crawlab</h3>
<el-form-item prop="username">
<span class="svg-container">
<svg-icon icon-class="user"/>
</span>
<el-input v-model="loginForm.username" name="username" type="text" auto-complete="on"
placeholder="username"/>
</el-form-item>
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password"/>
</span>
<el-input
:type="pwdType"
v-model="loginForm.password"
name="password"
auto-complete="on"
placeholder="password"
@keyup.enter.native="handleLogin"/>
<span class="show-pwd" @click="showPwd">
<svg-icon :icon-class="pwdType === 'password' ? 'eye' : 'eye-open'"/>
</span>
</el-form-item>
<el-form-item>
<el-button :loading="loading" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
Sign in
</el-button>
</el-form-item>
<div class="tips">
<span style="margin-right:20px;">username: admin</span>
<span> password: admin</span>
</div>
</el-form>
</div>
</template>
<script>
import { isvalidUsername } from '@/utils/validate'
export default {
name: 'Login',
data () {
const validateUsername = (rule, value, callback) => {
if (!isvalidUsername(value)) {
callback(new Error('请输入正确的用户名'))
} else {
callback()
}
}
const validatePass = (rule, value, callback) => {
if (value.length < 5) {
callback(new Error('密码不能小于5位'))
} else {
callback()
}
}
return {
loginForm: {
username: 'admin',
password: 'admin'
},
loginRules: {
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
password: [{ required: true, trigger: 'blur', validator: validatePass }]
},
loading: false,
pwdType: 'password',
redirect: undefined
}
},
watch: {
// $route: {
// handler: function (route) {
// this.redirect = route.query && route.query.redirect
// },
// immediate: true
// }
},
methods: {
showPwd () {
if (this.pwdType === 'password') {
this.pwdType = ''
} else {
this.pwdType = 'password'
}
},
handleLogin () {
this.$router.push('/')
// this.$refs.loginForm.validate(valid => {
// if (valid) {
// this.loading = true
// this.$store.dispatch('Login', this.loginForm).then(() => {
// this.loading = false
// this.$router.push({ path: this.redirect || '/' })
// }).catch(() => {
// this.loading = false
// })
// } else {
// console.log('error submit!!')
// return false
// }
// })
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss">
$bg: #2d3a4b;
$light_gray: #eee;
/* reset element-ui css */
.login-container {
.el-input {
display: inline-block;
height: 47px;
width: 85%;
input {
background: transparent;
border: 0px;
-webkit-appearance: none;
border-radius: 0px;
padding: 12px 5px 12px 15px;
color: $light_gray;
height: 47px;
&:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px $bg inset !important;
-webkit-text-fill-color: #fff !important;
}
}
}
.el-form-item {
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.1);
border-radius: 5px;
color: #454545;
}
}
</style>
<style rel="stylesheet/scss" lang="scss" scoped>
$bg: #2d3a4b;
$dark_gray: #889aa4;
$light_gray: #eee;
.login-container {
position: fixed;
height: 100%;
width: 100%;
background-color: $bg;
.login-form {
position: absolute;
left: 0;
right: 0;
width: 520px;
max-width: 100%;
padding: 35px 35px 15px 35px;
margin: 120px auto;
}
.tips {
font-size: 14px;
color: #fff;
margin-bottom: 10px;
span {
&:first-of-type {
margin-right: 16px;
}
}
}
.svg-container {
padding: 6px 5px 6px 15px;
color: $dark_gray;
vertical-align: middle;
width: 30px;
display: inline-block;
}
.title {
font-size: 26px;
font-weight: 400;
color: $light_gray;
margin: 0px auto 40px auto;
text-align: center;
font-weight: bold;
}
.show-pwd {
position: absolute;
right: 10px;
top: 7px;
font-size: 16px;
color: $dark_gray;
cursor: pointer;
user-select: none;
}
}
</style>

View File

@@ -0,0 +1,7 @@
<template >
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1">
<router-view />
</el-alert>
</div>
</template>

View File

@@ -0,0 +1,7 @@
<template >
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1-1" type="success">
<router-view />
</el-alert>
</div>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1-2" type="success">
<router-view />
</el-alert>
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template functional>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1-2-1" type="warning" />
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template functional>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1-2-2" type="warning" />
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template functional>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 1-3" type="success" />
</div>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<div style="padding:30px;">
<el-alert :closable="false" title="menu 2" />
</div>
</template>

View File

@@ -0,0 +1,93 @@
<template>
<div class="app-container">
<!--selector-->
<div class="selector">
<label class="label">Node: </label>
<el-select v-model="nodeForm._id" @change="onNodeChange">
<el-option v-for="op in nodeList" :key="op._id" :value="op._id" :label="op.name"></el-option>
</el-select>
</div>
<!--tabs-->
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="card">
<el-tab-pane label="Overview" name="overview">
<node-overview></node-overview>
</el-tab-pane>
<el-tab-pane label="Deployed Spiders" name="spiders">
Deployed Spiders
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import NodeOverview from '../../components/Overview/NodeOverview'
export default {
name: 'NodeDetail',
components: {
NodeOverview
},
data () {
return {
activeTabName: 'overview'
}
},
computed: {
...mapState('node', [
'nodeList',
'nodeForm'
])
},
methods: {
onTabClick () {
},
onNodeChange (id) {
this.$router.push(`/nodes/${id}`)
}
},
created () {
// get list of nodes
this.$store.dispatch('node/getNodeList')
// get node basic info
this.$store.dispatch('node/getNodeData', this.$route.params.id)
// get node deploy list
this.$store.dispatch('node/getDeployList', this.$route.params.id)
// get node task list
this.$store.dispatch('node/getTaskList', this.$route.params.id)
}
}
</script>
<style scoped>
.selector {
display: flex;
align-items: center;
position: absolute;
right: 20px;
margin-top: -7px;
/*float: right;*/
z-index: 999;
}
.selector .el-select {
padding-left: 10px;
}
</style>
<style lang="scss">
.selector {
.el-select {
.el-input {
.el-input_inner {
height: 26px;
}
}
}
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<div class="app-container">
<!--filter-->
<div class="filter">
<el-input prefix-icon="el-icon-search"
placeholder="Search"
class="filter-search"
v-model="filter.keyword"
@change="onSearch">
</el-input>
<div class="right">
<el-button type="success"
icon="el-icon-refresh"
class="refresh"
@click="onRefresh">
Refresh
</el-button>
</div>
</div>
<!--table list-->
<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 === 'status'"
:key="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
<template slot-scope="scope">
<el-tag type="info" v-if="scope.row.status === 'offline'">Offline</el-tag>
<el-tag type="success" v-else-if="scope.row.status === 'online'">Online</el-tag>
<el-tag type="danger" v-else>Unavailable</el-tag>
</template>
</el-table-column>
<el-table-column v-else
:key="col.name"
:property="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
</el-table-column>
</template>
<el-table-column label="Action" align="center" width="160">
<template slot-scope="scope">
<el-tooltip content="View" placement="top">
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="Edit" placement="top">
<el-button type="warning" icon="el-icon-edit" size="mini" @click="onView(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="Edit" placement="top">
<el-button type="danger" icon="el-icon-delete" size="mini" @click="onRemove(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
@current-change="onPageChange"
@size-change="onPageChange"
:current-page.sync="pagination.pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size.sync="pagination.pageSize"
layout="sizes, prev, pager, next"
:total="nodeList.length">
</el-pagination>
</div>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
export default {
name: 'NodeList',
data () {
return {
pagination: {
pageNum: 0,
pageSize: 10
},
isEditMode: false,
dialogVisible: false,
filter: {
keyword: ''
},
// tableData,
columns: [
{ name: 'name', label: 'Name', width: '220' },
{ name: 'ip', label: 'IP', width: '160' },
{ name: 'port', label: 'Port', width: '80' },
{ name: 'status', label: 'Status', width: '120', sortable: true },
{ name: 'description', label: 'Description', width: 'auto' }
],
nodeFormRules: {
name: [{ required: true, message: 'Required Field', trigger: 'change' }]
}
}
},
computed: {
...mapState('node', [
'nodeList',
'nodeForm'
]),
filteredTableData () {
return this.nodeList.filter(d => {
if (!this.filter.keyword) return true
for (let i = 0; i < this.columns.length; i++) {
const colName = this.columns[i].name
if (d[colName] && d[colName].toLowerCase().indexOf(this.filter.keyword.toLowerCase()) > -1) {
return true
}
}
return false
})
}
},
methods: {
onSearch (value) {
console.log(value)
},
onAdd () {
this.$store.commit('node/SET_NODE_FORM', [])
this.isEditMode = false
this.dialogVisible = true
},
onRefresh () {
this.$store.dispatch('node/getNodeList')
},
onSubmit () {
const vm = this
const formName = 'nodeForm'
this.$refs[formName].validate((valid) => {
if (valid) {
if (this.isEditMode) {
vm.$store.dispatch('node/editNode')
} else {
vm.$store.dispatch('node/addNode')
}
vm.dialogVisible = false
} else {
return false
}
})
},
onCancel () {
this.$store.commit('node/SET_NODE_FORM', {})
this.dialogVisible = false
},
onDialogClose () {
this.$store.commit('node/SET_NODE_FORM', {})
this.dialogVisible = false
},
onEdit (row) {
console.log(row)
this.isEditMode = true
this.$store.commit('node/SET_NODE_FORM', row)
this.dialogVisible = true
},
onRemove (row) {
this.$confirm('Are you sure to delete this node?', 'Notification', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$store.dispatch('node/deleteNode', row._id)
.then(() => {
this.$message({
type: 'success',
message: 'Deleted successfully'
})
})
})
},
onView (row) {
this.$router.push(`/nodes/${row._id}`)
},
onPageChange () {
this.$store.dispatch('node/getNodeList')
}
},
created () {
this.$store.dispatch('node/getNodeList')
}
}
</script>
<style scoped lang="scss">
.filter {
display: flex;
justify-content: space-between;
.filter-search {
width: 240px;
}
.add {
}
}
.table {
margin-top: 20px;
border-radius: 5px;
}
.delete-confirm {
background-color: red;
}
.el-table .el-button {
padding: 7px;
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<div class="app-container">
<!--selector-->
<div class="selector">
<label class="label">Spider: </label>
<el-select v-model="spiderForm._id.$oid" @change="onSpiderChange">
<el-option v-for="op in spiderList" :key="op._id.$oid" :value="op._id.$oid" :label="op.name"></el-option>
</el-select>
</div>
<!--tabs-->
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="card">
<el-tab-pane label="Overview" name="overview">
<spider-overview/>
</el-tab-pane>
<el-tab-pane label="Files" name="files">
<file-list/>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import FileList from '../../components/FileList/FileList'
import SpiderOverview from '../../components/Overview/SpiderOverview'
export default {
name: 'NodeDetail',
components: {
FileList,
SpiderOverview
},
data () {
return {
activeTabName: 'overview'
}
},
computed: {
...mapState('spider', [
'spiderList',
'spiderForm'
]),
...mapState('file', [
'currentPath'
]),
...mapState('deploy', [
'deployList'
])
},
methods: {
onTabClick () {
},
onSpiderChange (id) {
this.$router.push(`/spiders/${id}`)
}
},
created () {
// get the list of the spiders
this.$store.dispatch('spider/getSpiderList')
// get spider basic info
this.$store.dispatch('spider/getSpiderData', this.$route.params.id)
.then(() => {
// get spider file info
this.$store.dispatch('file/getFileList', this.spiderForm.src)
})
// get spider deploys
this.$store.dispatch('spider/getDeployList', this.$route.params.id)
// get spider tasks
this.$store.dispatch('spider/getTaskList', this.$route.params.id)
}
}
</script>
<style scoped>
.selector {
display: flex;
align-items: center;
position: absolute;
right: 20px;
/*float: right;*/
z-index: 999;
margin-top: -7px;
}
.selector .el-select {
padding-left: 10px;
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<div class="app-container">
<!--add popup-->
<el-dialog
:title="isEditMode ? 'Edit Spider' : 'Add Spider'"
:visible.sync="dialogVisible"
width="60%"
:before-close="onDialogClose">
<el-form label-width="150px"
:model="spiderForm"
:rules="spiderFormRules"
ref="spiderForm"
label-position="right">
<el-form-item label="Spider Name">
<el-input v-model="spiderForm.name" placeholder="Spider Name"></el-input>
</el-form-item>
<el-form-item label="Source Folder">
<el-input v-model="spiderForm.src" placeholder="Source Folder"></el-input>
</el-form-item>
<el-form-item label="Execute Command">
<el-input v-model="spiderForm.cmd" placeholder="Execute Command"></el-input>
</el-form-item>
<el-form-item label="Spider Type">
<el-select v-model="spiderForm.type" placeholder="Select Spider Type">
<el-option :value="1" label="Scrapy"></el-option>
<el-option :value="2" label="PySpider"></el-option>
<el-option :value="3" label="WebMagic"></el-option>
</el-select>
</el-form-item>
<el-form-item label="Language">
<el-select v-model="spiderForm.lang" placeholder="Select Language">
<el-option :value="1" label="Python"></el-option>
<el-option :value="2" label="Nodejs"></el-option>
<el-option :value="3" label="Java"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="onCancel">Cancel</el-button>
<el-button type="primary" @click="onSubmit">Submit</el-button>
</span>
</el-dialog>
<!--filter-->
<div class="filter">
<el-input prefix-icon="el-icon-search"
placeholder="Search"
class="filter-search"
v-model="filter.keyword"
@change="onSearch">
</el-input>
<div class="right">
<el-button type="success"
icon="el-icon-refresh"
class="refresh"
@click="onRefresh">
Refresh
</el-button>
<el-button type="primary"
v-if="false"
icon="el-icon-plus"
class="add"
@click="onAdd">
Add Spider
</el-button>
</div>
</div>
<!--table list-->
<el-table :data="filteredTableData"
class="table"
height="500"
:header-cell-style="{background:'rgb(48, 65, 86)',color:'white'}"
border>
<template v-for="col in columns">
<el-table-column v-if="col.name === 'type'"
:key="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
<template slot-scope="scope">
<el-tag v-if="scope.row.type === 'scrapy'">Scrapy</el-tag>
<el-tag type="warning" v-else-if="scope.row.type === 'pyspider'">PySpider</el-tag>
<el-tag type="info" v-else-if="scope.row.type === 'webmagic'">WebMagic</el-tag>
<el-tag type="success" v-else>Other</el-tag>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'lang'"
:key="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
<template slot-scope="scope">
<el-tag type="warning" v-if="scope.row.lang === 'python'">Python</el-tag>
<el-tag type="warning" v-else-if="scope.row.lang === 'javascript'">JavaScript</el-tag>
<el-tag type="info" v-else-if="scope.row.lang === 'java'">Java</el-tag>
<el-tag type="danger" v-else-if="scope.row.lang === 'go'">Go</el-tag>
<el-tag type="success" v-else>Other</el-tag>
</template>
</el-table-column>
<el-table-column v-else
:key="col.name"
:property="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
</el-table-column>
</template>
<el-table-column label="Action" align="center" width="250">
<template slot-scope="scope">
<el-tooltip content="View" placement="top">
<el-button type="info" icon="el-icon-search" size="mini" @click="onView(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="Edit" placement="top">
<el-button type="warning" icon="el-icon-edit" size="mini" @click="onView(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="Remove" placement="top">
<el-button type="danger" icon="el-icon-delete" size="mini" @click="onRemove(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="Deploy" placement="top">
<el-button type="primary" icon="fa fa-cloud" size="mini" @click="onDeploy(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="Run" placement="top">
<el-button type="success" icon="fa fa-bug" size="mini" @click="onCrawl(scope.row)"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
export default {
name: 'SpiderList',
data () {
// let tableData = []
// for (let i = 0; i < 50; i++) {
// tableData.push({
// spider_name: `Spider ${Math.floor(Math.random() * 100)}`,
// spider_ip: '127.0.0.1:8888',
// 'spider_description': `The ID of the spider is ${Math.random().toString().replace('0.', '')}`,
// status: Math.floor(Math.random() * 100) % 2
// })
// }
return {
isEditMode: false,
dialogVisible: false,
filter: {
keyword: ''
},
// tableData,
columns: [
{ name: 'name', label: 'Name', width: 'auto' },
{ name: 'type', label: 'Spider Type', width: '160', sortable: true },
{ name: 'lang', label: 'Language', width: '160', sortable: true },
{ name: 'status', label: 'Status', width: '160' }
],
spiderFormRules: {
name: [{ required: true, message: 'Required Field', trigger: 'change' }]
}
}
},
computed: {
...mapState('spider', [
'spiderList',
'spiderForm'
]),
filteredTableData () {
return this.spiderList.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
})
}
},
methods: {
onSearch (value) {
console.log(value)
},
onAdd () {
this.$store.commit('spider/SET_SPIDER_FORM', {})
this.isEditMode = false
this.dialogVisible = true
},
onRefresh () {
this.$store.dispatch('spider/getSpiderList')
},
onSubmit () {
const vm = this
const formName = 'spiderForm'
this.$refs[formName].validate((valid) => {
if (valid) {
if (this.isEditMode) {
vm.$store.dispatch('spider/editSpider')
} else {
vm.$store.dispatch('spider/addSpider')
}
vm.dialogVisible = false
} else {
return false
}
})
},
onCancel () {
this.$store.commit('spider/SET_SPIDER_FORM', {})
this.dialogVisible = false
},
onDialogClose () {
this.$store.commit('spider/SET_SPIDER_FORM', {})
this.dialogVisible = false
},
onEdit (row) {
console.log(row)
this.isEditMode = true
this.$store.commit('spider/SET_SPIDER_FORM', row)
this.dialogVisible = true
},
onRemove (row) {
this.$confirm('Are you sure to delete this spider?', 'Notification', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$store.dispatch('spider/deleteSpider', row._id.$oid)
.then(() => {
this.$message({
type: 'success',
message: 'Deleted successfully'
})
})
})
},
onDeploy (row) {
this.$store.dispatch('spider/getSpiderData', row._id.$oid)
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', true)
this.$store.commit('dialogView/SET_DIALOG_TYPE', 'spiderDeploy')
},
onCrawl (row) {
this.$store.dispatch('spider/getSpiderData', row._id.$oid)
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', true)
this.$store.commit('dialogView/SET_DIALOG_TYPE', 'spiderRun')
},
onView (row) {
this.$router.push(`/spiders/${row._id.$oid}`)
}
},
created () {
this.$store.dispatch('spider/getSpiderList')
}
}
</script>
<style scoped lang="scss">
.el-dialog {
.el-select {
width: 100%;
}
}
.filter {
display: flex;
justify-content: space-between;
.filter-search {
width: 240px;
}
.add {
}
}
.table {
margin-top: 20px;
border-radius: 5px;
.el-button {
padding: 7px;
}
}
.delete-confirm {
background-color: red;
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<div class="app-container">
<!--selector-->
<div class="selector">
<label class="label">Spider: </label>
<el-select v-model="spiderForm._id.$oid" @change="onSpiderChange">
<el-option v-for="op in spiderList" :key="op._id.$oid" :value="op._id.$oid" :label="op.name"></el-option>
</el-select>
</div>
<!--tabs-->
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="card">
<el-tab-pane label="Overview" name="overview">
<spider-overview/>
</el-tab-pane>
<el-tab-pane label="Files" name="files">
<file-list/>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import FileList from '../../components/FileList/FileList'
import SpiderOverview from '../../components/Overview/SpiderOverview'
export default {
name: 'NodeDetail',
components: {
FileList,
SpiderOverview
},
data () {
return {
activeTabName: 'overview'
}
},
computed: {
...mapState('spider', [
'spiderList',
'spiderForm'
]),
...mapState('file', [
'currentPath'
]),
...mapState('deploy', [
'deployList'
])
},
methods: {
onTabClick () {
},
onSpiderChange (id) {
this.$router.push(`/spiders/${id}`)
}
},
created () {
// get the list of the spiders
this.$store.dispatch('spider/getSpiderList')
// get spider basic info
this.$store.dispatch('spider/getSpiderData', this.$route.params.id)
.then(() => {
// get spider file info
this.$store.dispatch('file/getFileList', this.spiderForm.src)
})
// get spider deploys
this.$store.dispatch('spider/getDeployList', this.$route.params.id)
// get spider tasks
this.$store.dispatch('spider/getTaskList', this.$route.params.id)
}
}
</script>
<style scoped>
.selector {
display: flex;
align-items: center;
position: absolute;
right: 20px;
/*float: right;*/
z-index: 999;
margin-top: -7px;
}
.selector .el-select {
padding-left: 10px;
}
</style>

View File

@@ -0,0 +1,319 @@
<template>
<div class="app-container">
<!--add popup-->
<el-dialog
title="Import Spider"
:visible.sync="dialogVisible"
width="60%"
:before-close="onDialogClose">
<el-form label-width="150px"
:model="importForm"
ref="spiderForm"
label-position="right">
<el-form-item label="Source URL" prop="url" required>
<el-input v-model="importForm.url" placeholder="Source URL"></el-input>
</el-form-item>
<el-form-item label="Source Type" prop="type" required>
<el-select v-model="importForm.type" placeholder="Source Type">
<el-option value="github"></el-option>
<el-option value="gitlab"></el-option>
<el-option value="svn"></el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="onCancel">Cancel</el-button>
<el-button type="primary" @click="onImport">Import</el-button>
</span>
</el-dialog>
<!--filter-->
<div class="filter">
<el-input prefix-icon="el-icon-search"
placeholder="Search"
class="filter-search"
v-model="filter.keyword"
@change="onSearch">
</el-input>
<div class="right">
<el-dropdown class="btn">
<el-button type="primary" icon="el-icon-upload">
Import Spiders
<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>
<div @click="openImportDialog('github')">Github</div>
</el-dropdown-item>
<el-dropdown-item disabled>
<span @click="openImportDialog('gitlab')">Gitlab</span>
</el-dropdown-item>
<el-dropdown-item disabled>
<span @click="openImportDialog('svn')">SVN</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-button type="success"
icon="el-icon-refresh"
class="btn refresh"
@click="onRefresh">
Refresh
</el-button>
<el-button type="primary"
v-if="false"
icon="el-icon-plus"
class="add"
@click="onAdd">
Add Spider
</el-button>
</div>
</div>
<!--table list-->
<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 === 'type'"
:key="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
<template slot-scope="scope">
<el-tag v-if="scope.row.type === 'scrapy'">Scrapy</el-tag>
<el-tag type="warning" v-else-if="scope.row.type === 'pyspider'">PySpider</el-tag>
<el-tag type="info" v-else-if="scope.row.type === 'webmagic'">WebMagic</el-tag>
<el-tag type="success" v-else-if="scope.row.type">{{scope.row.type}}</el-tag>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'lang'"
:key="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
<template slot-scope="scope">
<el-tag type="warning" v-if="scope.row.lang === 'python'">Python</el-tag>
<el-tag type="primary" v-else-if="scope.row.lang === 'javascript'">JavaScript</el-tag>
<el-tag type="info" v-else-if="scope.row.lang === 'java'">Java</el-tag>
<el-tag type="danger" v-else-if="scope.row.lang === 'go'">Go</el-tag>
<el-tag type="success" v-else-if="scope.row.lang">{{scope.row.lang}}</el-tag>
</template>
</el-table-column>
<el-table-column v-else
:key="col.name"
:property="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
</el-table-column>
</template>
<el-table-column label="Action" align="center" width="250">
<template slot-scope="scope">
<el-tooltip content="View" placement="top">
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="Edit" placement="top">
<el-button type="warning" icon="el-icon-edit" size="mini" @click="onView(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="Remove" placement="top">
<el-button type="danger" icon="el-icon-delete" size="mini" @click="onRemove(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="Deploy" placement="top">
<el-button type="primary" icon="fa fa-cloud" size="mini" @click="onDeploy(scope.row)"></el-button>
</el-tooltip>
<el-tooltip content="Run" placement="top">
<el-button type="success" icon="fa fa-bug" size="mini" @click="onCrawl(scope.row)"></el-button>
</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="spiderList.length">
</el-pagination>
</div>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
export default {
name: 'SpiderList',
data () {
return {
pagination: {
pageNum: 0,
pageSize: 10
},
isEditMode: false,
dialogVisible: false,
filter: {
keyword: ''
},
// tableData,
columns: [
{ name: 'name', label: 'Name', width: 'auto' },
{ name: 'type', label: 'Spider Type', width: '160', sortable: true },
{ name: 'lang', label: 'Language', width: '160', sortable: true },
{ name: 'update_ts', label: 'Last Update', width: '120' }
],
spiderFormRules: {
name: [{ required: true, message: 'Required Field', trigger: 'change' }]
}
}
},
computed: {
...mapState('spider', [
'importForm',
'spiderList',
'spiderForm'
]),
filteredTableData () {
return this.spiderList.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
})
}
},
methods: {
onSearch (value) {
console.log(value)
},
onAdd () {
this.$store.commit('spider/SET_SPIDER_FORM', {})
this.isEditMode = false
this.dialogVisible = true
},
onRefresh () {
this.$store.dispatch('spider/getSpiderList')
},
onSubmit () {
const vm = this
const formName = 'spiderForm'
this.$refs[formName].validate((valid) => {
if (valid) {
if (this.isEditMode) {
vm.$store.dispatch('spider/editSpider')
} else {
vm.$store.dispatch('spider/addSpider')
}
vm.dialogVisible = false
} else {
return false
}
})
},
onCancel () {
this.$store.commit('spider/SET_SPIDER_FORM', {})
this.dialogVisible = false
},
onDialogClose () {
this.$store.commit('spider/SET_SPIDER_FORM', {})
this.dialogVisible = false
},
onEdit (row) {
console.log(row)
this.isEditMode = true
this.$store.commit('spider/SET_SPIDER_FORM', row)
this.dialogVisible = true
},
onRemove (row) {
this.$confirm('Are you sure to delete this spider?', 'Notification', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$store.dispatch('spider/deleteSpider', row._id.$oid)
.then(() => {
this.$message({
type: 'success',
message: 'Deleted successfully'
})
})
})
},
onDeploy (row) {
this.$store.dispatch('spider/getSpiderData', row._id.$oid)
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', true)
this.$store.commit('dialogView/SET_DIALOG_TYPE', 'spiderDeploy')
},
onCrawl (row) {
this.$store.dispatch('spider/getSpiderData', row._id.$oid)
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', true)
this.$store.commit('dialogView/SET_DIALOG_TYPE', 'spiderRun')
},
onView (row) {
this.$router.push(`/spiders/${row._id.$oid}`)
},
onPageChange () {
this.$store.dispatch('spider/getSpiderList')
},
onImport () {
this.dialogVisible = false
},
openImportDialog (type) {
this.$store.commit('spider/SET_IMPORT_FORM', { type })
this.dialogVisible = true
}
},
created () {
this.$store.dispatch('spider/getSpiderList')
}
}
</script>
<style scoped lang="scss">
.el-dialog {
.el-select {
width: 100%;
}
}
.filter {
display: flex;
justify-content: space-between;
.filter-search {
width: 240px;
}
.right {
.btn {
margin-left: 10px;
}
}
}
.table {
margin-top: 20px;
border-radius: 5px;
.el-button {
padding: 7px;
}
}
.delete-confirm {
background-color: red;
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div class="app-container">
<el-table
v-loading="listLoading"
:data="list"
element-loading-text="Loading"
border
fit
highlight-current-row>
<el-table-column align="center" label="ID" width="95">
<template slot-scope="scope">
{{ scope.$index }}
</template>
</el-table-column>
<el-table-column label="Title">
<template slot-scope="scope">
{{ scope.row.title }}
</template>
</el-table-column>
<el-table-column label="Author" width="110" align="center">
<template slot-scope="scope">
<span>{{ scope.row.author }}</span>
</template>
</el-table-column>
<el-table-column label="Pageviews" width="110" align="center">
<template slot-scope="scope">
{{ scope.row.pageviews }}
</template>
</el-table-column>
<el-table-column class-name="status-col" label="Status" width="110" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status | statusFilter">{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" prop="created_at" label="Display_time" width="200">
<template slot-scope="scope">
<i class="el-icon-time"/>
<span>{{ scope.row.display_time }}</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { getList } from '@/api/table'
export default {
filters: {
statusFilter (status) {
const statusMap = {
published: 'success',
draft: 'gray',
deleted: 'danger'
}
return statusMap[status]
}
},
data () {
return {
list: null,
listLoading: true
}
},
created () {
this.fetchData()
},
methods: {
fetchData () {
this.listLoading = true
getList(this.listQuery).then(response => {
this.list = response.data.items
this.listLoading = false
})
}
}
}
</script>

View File

@@ -0,0 +1,75 @@
<template>
<div class="app-container">
<!--tabs-->
<el-tabs v-model="activeTabName" @tab-click="onTabClick" type="card">
<el-tab-pane label="Overview" name="overview">
<task-overview/>
</el-tab-pane>
<el-tab-pane label="Log" name="log">
<div class="log-view">
<pre>
{{taskLog}}
</pre>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import TaskOverview from '../../components/Overview/TaskOverview'
export default {
name: 'TaskDetail',
components: {
TaskOverview
},
data () {
return {
activeTabName: 'overview'
}
},
computed: {
...mapState('task', [
'taskLog'
]),
...mapState('file', [
'currentPath'
]),
...mapState('deploy', [
'deployList'
])
},
methods: {
onTabClick () {
},
onSpiderChange (id) {
this.$router.push(`/spiders/${id}`)
}
},
created () {
this.$store.dispatch('task/getTaskData', this.$route.params.id)
this.$store.dispatch('task/getTaskLog', this.$route.params.id)
}
}
</script>
<style scoped>
.selector {
display: flex;
align-items: center;
position: absolute;
right: 20px;
/*float: right;*/
z-index: 999;
margin-top: -7px;
}
.selector .el-select {
padding-left: 10px;
}
</style>

View File

@@ -0,0 +1,238 @@
<template>
<div class="app-container">
<!--filter-->
<div class="filter">
<el-input prefix-icon="el-icon-search"
placeholder="Search"
class="filter-search"
v-model="filter.keyword"
@change="onSearch">
</el-input>
<div class="right">
<el-button type="success"
icon="el-icon-refresh"
class="refresh"
@click="onRefresh">
Search
</el-button>
</div>
</div>
<!--table list-->
<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="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
<template slot-scope="scope">
<a href="javascript:" class="a-tag" @click="onClickSpider(scope.row)">{{scope.row[col.name]}}</a>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'node_id'"
:key="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
<template slot-scope="scope">
<a href="javascript:" class="a-tag" @click="onClickNode(scope.row)">{{scope.row[col.name]}}</a>
</template>
</el-table-column>
<el-table-column v-else-if="col.name === 'status'"
:key="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
<template slot-scope="scope">
<el-tag type="success" v-if="scope.row.status === 'SUCCESS'">SUCCESS</el-tag>
<el-tag type="warning" v-else-if="scope.row.status === 'PENDING'">PENDING</el-tag>
<el-tag type="danger" v-else-if="scope.row.status === 'FAILURE'">FAILURE</el-tag>
<el-tag type="info" v-else>{{scope.row[col.name]}}</el-tag>
</template>
</el-table-column>
<el-table-column v-else
:key="col.name"
:property="col.name"
:label="col.label"
:sortable="col.sortable"
align="center"
:width="col.width">
</el-table-column>
</template>
<el-table-column label="Action" align="center" width="180">
<template slot-scope="scope">
<el-tooltip content="View" placement="top">
<el-button type="primary" icon="el-icon-search" size="mini" @click="onView(scope.row)"></el-button>
</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="taskList.length">
</el-pagination>
</div>
</div>
</template>
<script>
import {
mapState
} from 'vuex'
import dayjs from 'dayjs'
export default {
name: 'TaskList',
data () {
return {
pagination: {
pageNum: 0,
pageSize: 10
},
isEditMode: false,
dialogVisible: false,
filter: {
keyword: ''
},
// tableData,
columns: [
{ name: 'create_ts', label: 'Create Date', width: '150' },
{ name: 'finish_ts', label: 'Finish Date', width: '150' },
{ name: 'spider_name', label: 'Spider', width: '160' },
{ name: 'node_id', label: 'Node', width: 'auto' },
{ name: 'status', label: 'Status', width: '160', sortable: true }
]
}
},
computed: {
...mapState('task', [
'taskList',
'taskForm'
]),
filteredTableData () {
return this.taskList
.map(d => {
if (d.create_ts) d.create_ts = dayjs(d.create_ts.$date).format('YYYY-MM-DD HH:mm:ss')
if (d.finish_ts) d.finish_ts = dayjs(d.finish_ts.$date).format('YYYY-MM-DD HH:mm:ss')
try {
d.spider_id = d.spider_id.$oid
} catch (e) {
if (d.spider_id) d.spider_id = d.spider_id.toString()
}
return d
})
.sort((a, b) => a.create_ts < b.create_ts ? 1 : -1)
.filter(d => {
// keyword
if (!this.filter.keyword) return true
for (let i = 0; i < this.columns.length; i++) {
const colName = this.columns[i].name
if (d[colName] && d[colName].toLowerCase().indexOf(this.filter.keyword.toLowerCase()) > -1) {
return true
}
}
return false
})
.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('task/getTaskList')
},
onRemove (row) {
this.$confirm('Are you sure to delete this task?', 'Notification', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
this.$store.dispatch('task/deleteTask', row._id)
.then(() => {
this.$message({
type: 'success',
message: 'Deleted successfully'
})
})
})
},
onView (row) {
this.$router.push(`/tasks/${row._id}`)
},
onClickSpider (row) {
this.$router.push(`/spiders/${row.spider_id}`)
},
onClickNode (row) {
this.$router.push(`/nodes/${row.node_id}`)
},
onPageChange () {
this.$store.dispatch('task/getTaskList')
}
},
created () {
this.$store.dispatch('task/getTaskList')
}
}
</script>
<style scoped lang="scss">
.el-dialog {
.el-select {
width: 100%;
}
}
.filter {
display: flex;
justify-content: space-between;
.filter-search {
width: 240px;
}
.add {
}
}
.table {
margin-top: 20px;
border-radius: 5px;
.el-button {
padding: 7px;
}
}
.delete-confirm {
background-color: red;
}
.el-table .a-tag {
text-decoration: underline;
}
.pagination {
margin-top: 10px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="app-container">
<el-input v-model="filterText" placeholder="Filter keyword" style="margin-bottom:30px;"/>
<el-tree
ref="tree2"
:data="data2"
:props="defaultProps"
:filter-node-method="filterNode"
class="filter-tree"
default-expand-all
/>
</div>
</template>
<script>
export default {
data () {
return {
filterText: '',
data2: [{
id: 1,
label: 'Level one 1',
children: [{
id: 4,
label: 'Level two 1-1',
children: [{
id: 9,
label: 'Level three 1-1-1'
}, {
id: 10,
label: 'Level three 1-1-2'
}]
}]
}, {
id: 2,
label: 'Level one 2',
children: [{
id: 5,
label: 'Level two 2-1'
}, {
id: 6,
label: 'Level two 2-2'
}]
}, {
id: 3,
label: 'Level one 3',
children: [{
id: 7,
label: 'Level two 3-1'
}, {
id: 8,
label: 'Level two 3-2'
}]
}],
defaultProps: {
children: 'children',
label: 'label'
}
}
},
watch: {
filterText (val) {
this.$refs.tree2.filter(val)
}
},
methods: {
filterNode (value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
}
}
}
</script>