mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-29 18:00:51 +01:00
added setup.py
This commit is contained in:
228
frontend/src/views/404.vue
Normal file
228
frontend/src/views/404.vue
Normal 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>
|
||||
32
frontend/src/views/dashboard/index.vue
Normal file
32
frontend/src/views/dashboard/index.vue
Normal 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>
|
||||
15
frontend/src/views/deploy/DeployDetail.vue
Normal file
15
frontend/src/views/deploy/DeployDetail.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="">
|
||||
NodeDetail
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NodeDetail'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
181
frontend/src/views/deploy/DeployList.vue
Normal file
181
frontend/src/views/deploy/DeployList.vue
Normal 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>
|
||||
84
frontend/src/views/form/index.vue
Normal file
84
frontend/src/views/form/index.vue
Normal 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>
|
||||
160
frontend/src/views/home/Home.vue
Normal file
160
frontend/src/views/home/Home.vue
Normal 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>
|
||||
79
frontend/src/views/layout/Layout.vue
Normal file
79
frontend/src/views/layout/Layout.vue
Normal 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>
|
||||
29
frontend/src/views/layout/components/AppMain.vue
Normal file
29
frontend/src/views/layout/components/AppMain.vue
Normal 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>
|
||||
99
frontend/src/views/layout/components/Navbar.vue
Normal file
99
frontend/src/views/layout/components/Navbar.vue
Normal 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>
|
||||
34
frontend/src/views/layout/components/Sidebar/Item.vue
Normal file
34
frontend/src/views/layout/components/Sidebar/Item.vue
Normal 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>
|
||||
35
frontend/src/views/layout/components/Sidebar/Link.vue
Normal file
35
frontend/src/views/layout/components/Sidebar/Link.vue
Normal 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>
|
||||
103
frontend/src/views/layout/components/Sidebar/SidebarItem.vue
Normal file
103
frontend/src/views/layout/components/Sidebar/SidebarItem.vue
Normal 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>
|
||||
44
frontend/src/views/layout/components/Sidebar/index.vue
Normal file
44
frontend/src/views/layout/components/Sidebar/index.vue
Normal 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>
|
||||
294
frontend/src/views/layout/components/TagsView.vue
Normal file
294
frontend/src/views/layout/components/TagsView.vue
Normal 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>
|
||||
4
frontend/src/views/layout/components/index.js
Normal file
4
frontend/src/views/layout/components/index.js
Normal 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'
|
||||
41
frontend/src/views/layout/mixin/ResizeHandler.js
Normal file
41
frontend/src/views/layout/mixin/ResizeHandler.js
Normal 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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
209
frontend/src/views/login/index.vue
Normal file
209
frontend/src/views/login/index.vue
Normal 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>
|
||||
7
frontend/src/views/nested/menu1/index.vue
Normal file
7
frontend/src/views/nested/menu1/index.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template >
|
||||
<div style="padding:30px;">
|
||||
<el-alert :closable="false" title="menu 1">
|
||||
<router-view />
|
||||
</el-alert>
|
||||
</div>
|
||||
</template>
|
||||
7
frontend/src/views/nested/menu1/menu1-1/index.vue
Normal file
7
frontend/src/views/nested/menu1/menu1-1/index.vue
Normal 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>
|
||||
7
frontend/src/views/nested/menu1/menu1-2/index.vue
Normal file
7
frontend/src/views/nested/menu1/menu1-2/index.vue
Normal 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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template functional>
|
||||
<div style="padding:30px;">
|
||||
<el-alert :closable="false" title="menu 1-2-1" type="warning" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template functional>
|
||||
<div style="padding:30px;">
|
||||
<el-alert :closable="false" title="menu 1-2-2" type="warning" />
|
||||
</div>
|
||||
</template>
|
||||
5
frontend/src/views/nested/menu1/menu1-3/index.vue
Normal file
5
frontend/src/views/nested/menu1/menu1-3/index.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template functional>
|
||||
<div style="padding:30px;">
|
||||
<el-alert :closable="false" title="menu 1-3" type="success" />
|
||||
</div>
|
||||
</template>
|
||||
5
frontend/src/views/nested/menu2/index.vue
Normal file
5
frontend/src/views/nested/menu2/index.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div style="padding:30px;">
|
||||
<el-alert :closable="false" title="menu 2" />
|
||||
</div>
|
||||
</template>
|
||||
93
frontend/src/views/node/NodeDetail.vue
Normal file
93
frontend/src/views/node/NodeDetail.vue
Normal 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>
|
||||
220
frontend/src/views/node/NodeList.vue
Normal file
220
frontend/src/views/node/NodeList.vue
Normal 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>
|
||||
95
frontend/src/views/result/ResultDetail.vue
Normal file
95
frontend/src/views/result/ResultDetail.vue
Normal 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>
|
||||
296
frontend/src/views/result/ResultList.vue
Normal file
296
frontend/src/views/result/ResultList.vue
Normal 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>
|
||||
94
frontend/src/views/spider/SpiderDetail.vue
Normal file
94
frontend/src/views/spider/SpiderDetail.vue
Normal 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>
|
||||
319
frontend/src/views/spider/SpiderList.vue
Normal file
319
frontend/src/views/spider/SpiderList.vue
Normal 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>
|
||||
78
frontend/src/views/table/index.vue
Normal file
78
frontend/src/views/table/index.vue
Normal 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>
|
||||
75
frontend/src/views/task/TaskDetail.vue
Normal file
75
frontend/src/views/task/TaskDetail.vue
Normal 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>
|
||||
238
frontend/src/views/task/TaskList.vue
Normal file
238
frontend/src/views/task/TaskList.vue
Normal 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>
|
||||
77
frontend/src/views/tree/index.vue
Normal file
77
frontend/src/views/tree/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user