mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-25 17:42:25 +01:00
added setup.py
This commit is contained in:
116
frontend/src/components/BackToTop/index.vue
Normal file
116
frontend/src/components/BackToTop/index.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<transition :name="transitionName">
|
||||
<div v-show="visible" :style="customStyle" class="back-to-ceiling" @click="backToTop">
|
||||
<svg width="16" height="16" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg" class="Icon Icon--backToTopArrow" aria-hidden="true" style="height: 16px; width: 16px;">
|
||||
<title>回到顶部</title>
|
||||
<g>
|
||||
<path d="M12.036 15.59c0 .55-.453.995-.997.995H5.032c-.55 0-.997-.445-.997-.996V8.584H1.03c-1.1 0-1.36-.633-.578-1.416L7.33.29c.39-.39 1.026-.385 1.412 0l6.878 6.88c.782.78.523 1.415-.58 1.415h-3.004v7.004z" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BackToTop',
|
||||
props: {
|
||||
visibilityHeight: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
backPosition: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
customStyle: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {
|
||||
right: '50px',
|
||||
bottom: '50px',
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
'border-radius': '4px',
|
||||
'line-height': '45px',
|
||||
background: '#e7eaf1'
|
||||
}
|
||||
}
|
||||
},
|
||||
transitionName: {
|
||||
type: String,
|
||||
default: 'fade'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
interval: null,
|
||||
isMoving: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleScroll() {
|
||||
this.visible = window.pageYOffset > this.visibilityHeight
|
||||
},
|
||||
backToTop() {
|
||||
if (this.isMoving) return
|
||||
const start = window.pageYOffset
|
||||
let i = 0
|
||||
this.isMoving = true
|
||||
this.interval = setInterval(() => {
|
||||
const next = Math.floor(this.easeInOutQuad(10 * i, start, -start, 500))
|
||||
if (next <= this.backPosition) {
|
||||
window.scrollTo(0, this.backPosition)
|
||||
clearInterval(this.interval)
|
||||
this.isMoving = false
|
||||
} else {
|
||||
window.scrollTo(0, next)
|
||||
}
|
||||
i++
|
||||
}, 16.7)
|
||||
},
|
||||
easeInOutQuad(t, b, c, d) {
|
||||
if ((t /= d / 2) < 1) return c / 2 * t * t + b
|
||||
return -c / 2 * (--t * (t - 2) - 1) + b
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.back-to-ceiling {
|
||||
position: fixed;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.back-to-ceiling:hover {
|
||||
background: #d5dbe7;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity .5s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
.back-to-ceiling .Icon {
|
||||
fill: #9aaabf;
|
||||
background: none;
|
||||
}
|
||||
</style>
|
||||
86
frontend/src/components/Breadcrumb/index.vue
Normal file
86
frontend/src/components/Breadcrumb/index.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb" separator="/">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
|
||||
<span v-if="item.redirect==='noredirect'||index==levelList.length-1"
|
||||
class="no-redirect">{{ item.meta.title }}</span>
|
||||
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import pathToRegexp from 'path-to-regexp'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
levelList: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route () {
|
||||
this.getBreadcrumb()
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getBreadcrumb()
|
||||
},
|
||||
methods: {
|
||||
getBreadcrumb () {
|
||||
let matched = this.$route.matched.filter(item => item.name)
|
||||
|
||||
const first = matched[0]
|
||||
if (first && first.name !== 'Home') {
|
||||
matched = [{ path: '/home', meta: { title: 'Home' } }].concat(matched)
|
||||
}
|
||||
|
||||
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
||||
},
|
||||
pathCompile (path) {
|
||||
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
|
||||
const { params } = this.$route
|
||||
var toPath = pathToRegexp.compile(path)
|
||||
return toPath(params)
|
||||
},
|
||||
handleLink (item) {
|
||||
const { redirect } = item
|
||||
if (redirect) {
|
||||
this.$router.push(redirect)
|
||||
return
|
||||
}
|
||||
this.$router.push(this.getGoToPath(item))
|
||||
},
|
||||
getGoToPath (item) {
|
||||
if (item.path) {
|
||||
var path = item.path
|
||||
var startPos = path.indexOf(':')
|
||||
|
||||
if (startPos !== -1) {
|
||||
var endPos = path.indexOf('/', startPos)
|
||||
var key = path.substring(startPos + 1, endPos)
|
||||
path = path.replace(':' + key, this.$route.params[key])
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return item.redirect || item.path
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.app-breadcrumb.el-breadcrumb {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 50px;
|
||||
margin-left: 10px;
|
||||
|
||||
.no-redirect {
|
||||
color: #97a8be;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
156
frontend/src/components/Charts/keyboard.vue
Normal file
156
frontend/src/components/Charts/keyboard.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div :class="className" :id="id" :style="{height:height,width:width}"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import echarts from 'echarts'
|
||||
import resize from './mixins/resize'
|
||||
|
||||
export default {
|
||||
mixins: [resize],
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (!this.chart) {
|
||||
return
|
||||
}
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.chart = echarts.init(document.getElementById(this.id))
|
||||
|
||||
const xAxisData = []
|
||||
const data = []
|
||||
const data2 = []
|
||||
for (let i = 0; i < 50; i++) {
|
||||
xAxisData.push(i)
|
||||
data.push((Math.sin(i / 5) * (i / 5 - 10) + i / 6) * 5)
|
||||
data2.push((Math.sin(i / 5) * (i / 5 + 10) + i / 6) * 3)
|
||||
}
|
||||
this.chart.setOption(
|
||||
{
|
||||
backgroundColor: '#08263a',
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '5%'
|
||||
},
|
||||
xAxis: [{
|
||||
show: false,
|
||||
data: xAxisData
|
||||
}, {
|
||||
show: false,
|
||||
data: xAxisData
|
||||
}],
|
||||
visualMap: {
|
||||
show: false,
|
||||
min: 0,
|
||||
max: 50,
|
||||
dimension: 0,
|
||||
inRange: {
|
||||
color: ['#4a657a', '#308e92', '#b1cfa5', '#f5d69f', '#f5898b', '#ef5055']
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
textStyle: {
|
||||
color: '#4a657a'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#08263f'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: 'back',
|
||||
type: 'bar',
|
||||
data: data2,
|
||||
z: 1,
|
||||
itemStyle: {
|
||||
normal: {
|
||||
opacity: 0.4,
|
||||
barBorderRadius: 5,
|
||||
shadowBlur: 3,
|
||||
shadowColor: '#111'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
name: 'Simulate Shadow',
|
||||
type: 'line',
|
||||
data,
|
||||
z: 2,
|
||||
showSymbol: false,
|
||||
animationDelay: 0,
|
||||
animationEasing: 'linear',
|
||||
animationDuration: 1200,
|
||||
lineStyle: {
|
||||
normal: {
|
||||
color: 'transparent'
|
||||
}
|
||||
},
|
||||
areaStyle: {
|
||||
normal: {
|
||||
color: '#08263a',
|
||||
shadowBlur: 50,
|
||||
shadowColor: '#000'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
name: 'front',
|
||||
type: 'bar',
|
||||
data,
|
||||
xAxisIndex: 1,
|
||||
z: 3,
|
||||
itemStyle: {
|
||||
normal: {
|
||||
barBorderRadius: 5
|
||||
}
|
||||
}
|
||||
}],
|
||||
animationEasing: 'elasticOut',
|
||||
animationEasingUpdate: 'elasticOut',
|
||||
animationDelay(idx) {
|
||||
return idx * 20
|
||||
},
|
||||
animationDelayUpdate(idx) {
|
||||
return idx * 20
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
227
frontend/src/components/Charts/lineMarker.vue
Normal file
227
frontend/src/components/Charts/lineMarker.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<div :class="className" :id="id" :style="{height:height,width:width}"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import echarts from 'echarts'
|
||||
import resize from './mixins/resize'
|
||||
|
||||
export default {
|
||||
mixins: [resize],
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (!this.chart) {
|
||||
return
|
||||
}
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.chart = echarts.init(document.getElementById(this.id))
|
||||
|
||||
this.chart.setOption({
|
||||
backgroundColor: '#394056',
|
||||
title: {
|
||||
top: 20,
|
||||
text: 'Requests',
|
||||
textStyle: {
|
||||
fontWeight: 'normal',
|
||||
fontSize: 16,
|
||||
color: '#F1F1F3'
|
||||
},
|
||||
left: '1%'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: '#57617B'
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: 20,
|
||||
icon: 'rect',
|
||||
itemWidth: 14,
|
||||
itemHeight: 5,
|
||||
itemGap: 13,
|
||||
data: ['CMCC', 'CTCC', 'CUCC'],
|
||||
right: '4%',
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#F1F1F3'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
top: 100,
|
||||
left: '2%',
|
||||
right: '2%',
|
||||
bottom: '2%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: [{
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#57617B'
|
||||
}
|
||||
},
|
||||
data: ['13:00', '13:05', '13:10', '13:15', '13:20', '13:25', '13:30', '13:35', '13:40', '13:45', '13:50', '13:55']
|
||||
}],
|
||||
yAxis: [{
|
||||
type: 'value',
|
||||
name: '(%)',
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#57617B'
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
margin: 10,
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#57617B'
|
||||
}
|
||||
}
|
||||
}],
|
||||
series: [{
|
||||
name: 'CMCC',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
normal: {
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
areaStyle: {
|
||||
normal: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: 'rgba(137, 189, 27, 0.3)'
|
||||
}, {
|
||||
offset: 0.8,
|
||||
color: 'rgba(137, 189, 27, 0)'
|
||||
}], false),
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: 'rgb(137,189,27)',
|
||||
borderColor: 'rgba(137,189,2,0.27)',
|
||||
borderWidth: 12
|
||||
|
||||
}
|
||||
},
|
||||
data: [220, 182, 191, 134, 150, 120, 110, 125, 145, 122, 165, 122]
|
||||
}, {
|
||||
name: 'CTCC',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
normal: {
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
areaStyle: {
|
||||
normal: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: 'rgba(0, 136, 212, 0.3)'
|
||||
}, {
|
||||
offset: 0.8,
|
||||
color: 'rgba(0, 136, 212, 0)'
|
||||
}], false),
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: 'rgb(0,136,212)',
|
||||
borderColor: 'rgba(0,136,212,0.2)',
|
||||
borderWidth: 12
|
||||
|
||||
}
|
||||
},
|
||||
data: [120, 110, 125, 145, 122, 165, 122, 220, 182, 191, 134, 150]
|
||||
}, {
|
||||
name: 'CUCC',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
normal: {
|
||||
width: 1
|
||||
}
|
||||
},
|
||||
areaStyle: {
|
||||
normal: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
|
||||
offset: 0,
|
||||
color: 'rgba(219, 50, 51, 0.3)'
|
||||
}, {
|
||||
offset: 0.8,
|
||||
color: 'rgba(219, 50, 51, 0)'
|
||||
}], false),
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: 'rgb(219,50,51)',
|
||||
borderColor: 'rgba(219,50,51,0.2)',
|
||||
borderWidth: 12
|
||||
}
|
||||
},
|
||||
data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
271
frontend/src/components/Charts/mixChart.vue
Normal file
271
frontend/src/components/Charts/mixChart.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div :class="className" :id="id" :style="{height:height,width:width}"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import echarts from 'echarts'
|
||||
import resize from './mixins/resize'
|
||||
|
||||
export default {
|
||||
mixins: [resize],
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: 'chart'
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '200px'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (!this.chart) {
|
||||
return
|
||||
}
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
},
|
||||
methods: {
|
||||
initChart() {
|
||||
this.chart = echarts.init(document.getElementById(this.id))
|
||||
const xData = (function() {
|
||||
const data = []
|
||||
for (let i = 1; i < 13; i++) {
|
||||
data.push(i + 'month')
|
||||
}
|
||||
return data
|
||||
}())
|
||||
this.chart.setOption({
|
||||
backgroundColor: '#344b58',
|
||||
title: {
|
||||
text: 'statistics',
|
||||
x: '20',
|
||||
top: '20',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: '22'
|
||||
},
|
||||
subtextStyle: {
|
||||
color: '#90979c',
|
||||
fontSize: '16'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
borderWidth: 0,
|
||||
top: 150,
|
||||
bottom: 95,
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
x: '5%',
|
||||
top: '10%',
|
||||
textStyle: {
|
||||
color: '#90979c'
|
||||
},
|
||||
data: ['female', 'male', 'average']
|
||||
},
|
||||
calculable: true,
|
||||
xAxis: [{
|
||||
type: 'category',
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#90979c'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitArea: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
interval: 0
|
||||
|
||||
},
|
||||
data: xData
|
||||
}],
|
||||
yAxis: [{
|
||||
type: 'value',
|
||||
splitLine: {
|
||||
show: false
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#90979c'
|
||||
}
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
interval: 0
|
||||
},
|
||||
splitArea: {
|
||||
show: false
|
||||
}
|
||||
}],
|
||||
dataZoom: [{
|
||||
show: true,
|
||||
height: 30,
|
||||
xAxisIndex: [
|
||||
0
|
||||
],
|
||||
bottom: 30,
|
||||
start: 10,
|
||||
end: 80,
|
||||
handleIcon: 'path://M306.1,413c0,2.2-1.8,4-4,4h-59.8c-2.2,0-4-1.8-4-4V200.8c0-2.2,1.8-4,4-4h59.8c2.2,0,4,1.8,4,4V413z',
|
||||
handleSize: '110%',
|
||||
handleStyle: {
|
||||
color: '#d3dee5'
|
||||
|
||||
},
|
||||
textStyle: {
|
||||
color: '#fff' },
|
||||
borderColor: '#90979c'
|
||||
|
||||
}, {
|
||||
type: 'inside',
|
||||
show: true,
|
||||
height: 15,
|
||||
start: 1,
|
||||
end: 35
|
||||
}],
|
||||
series: [{
|
||||
name: 'female',
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
barMaxWidth: 35,
|
||||
barGap: '10%',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: 'rgba(255,144,128,1)',
|
||||
label: {
|
||||
show: true,
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
position: 'insideTop',
|
||||
formatter(p) {
|
||||
return p.value > 0 ? p.value : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [
|
||||
709,
|
||||
1917,
|
||||
2455,
|
||||
2610,
|
||||
1719,
|
||||
1433,
|
||||
1544,
|
||||
3285,
|
||||
5208,
|
||||
3372,
|
||||
2484,
|
||||
4078
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
name: 'male',
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: 'rgba(0,191,183,1)',
|
||||
barBorderRadius: 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter(p) {
|
||||
return p.value > 0 ? p.value : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [
|
||||
327,
|
||||
1776,
|
||||
507,
|
||||
1200,
|
||||
800,
|
||||
482,
|
||||
204,
|
||||
1390,
|
||||
1001,
|
||||
951,
|
||||
381,
|
||||
220
|
||||
]
|
||||
}, {
|
||||
name: 'average',
|
||||
type: 'line',
|
||||
stack: 'total',
|
||||
symbolSize: 10,
|
||||
symbol: 'circle',
|
||||
itemStyle: {
|
||||
normal: {
|
||||
color: 'rgba(252,230,48,1)',
|
||||
barBorderRadius: 0,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter(p) {
|
||||
return p.value > 0 ? p.value : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [
|
||||
1036,
|
||||
3693,
|
||||
2962,
|
||||
3810,
|
||||
2519,
|
||||
1915,
|
||||
1748,
|
||||
4675,
|
||||
6209,
|
||||
4323,
|
||||
2865,
|
||||
4298
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
32
frontend/src/components/Charts/mixins/resize.js
Normal file
32
frontend/src/components/Charts/mixins/resize.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { debounce } from '@/utils'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
sidebarElm: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.__resizeHandler = debounce(() => {
|
||||
if (this.chart) {
|
||||
this.chart.resize()
|
||||
}
|
||||
}, 100)
|
||||
window.addEventListener('resize', this.__resizeHandler)
|
||||
|
||||
this.sidebarElm = document.getElementsByClassName('sidebar-container')[0]
|
||||
this.sidebarElm && this.sidebarElm.addEventListener('transitionend', this.sidebarResizeHandler)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.__resizeHandler)
|
||||
|
||||
this.sidebarElm && this.sidebarElm.removeEventListener('transitionend', this.sidebarResizeHandler)
|
||||
},
|
||||
methods: {
|
||||
sidebarResizeHandler(e) {
|
||||
if (e.propertyName === 'width') {
|
||||
this.__resizeHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
165
frontend/src/components/Common/DialogView.vue
Normal file
165
frontend/src/components/Common/DialogView.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="dialog-view">
|
||||
<el-dialog
|
||||
class="deploy-dialog"
|
||||
:title="title"
|
||||
:visible.sync="dialogVisible"
|
||||
width="40%">
|
||||
<!--message-->
|
||||
<label>{{message}}</label>
|
||||
|
||||
<!--selection for node-->
|
||||
<el-select v-if="type === 'node'" v-model="activeSpider._id.$oid">
|
||||
<el-option v-for="op in spiderList" :key="op._id.$oid" :value="op._id.$oid" :label="op.name"></el-option>
|
||||
</el-select>
|
||||
|
||||
<!--selection for spider-->
|
||||
<el-select v-else-if="type === 'spider'" v-model="activeNode._id">
|
||||
<el-option v-for="op in nodeList" :key="op._id" :value="op._id" :label="op.name"></el-option>
|
||||
</el-select>
|
||||
|
||||
<!--action buttons-->
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="onCancel">Cancel</el-button>
|
||||
<el-button type="danger" @click="onConfirm">Confirm</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'DialogView',
|
||||
computed: {
|
||||
...mapState('spider', [
|
||||
'spiderList',
|
||||
'spiderForm'
|
||||
]),
|
||||
...mapState('node', [
|
||||
'nodeList'
|
||||
]),
|
||||
...mapState('dialogView', [
|
||||
'dialogType'
|
||||
]),
|
||||
type () {
|
||||
if (this.dialogType === 'nodeDeploy') {
|
||||
return 'node'
|
||||
} else if (this.dialogType === 'nodeRun') {
|
||||
return 'node'
|
||||
} else if (this.dialogType === 'spiderDeploy') {
|
||||
return 'spider'
|
||||
} else if (this.dialogType === 'spiderRun') {
|
||||
return 'spider'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
activeNode: {
|
||||
get () {
|
||||
return this.$store.state.spider.activeNode
|
||||
},
|
||||
set () {
|
||||
this.$store.commit('spider/SET_ACTIVE_NODE')
|
||||
}
|
||||
},
|
||||
activeSpider: {
|
||||
get () {
|
||||
return this.$store.state.node.activeSpider
|
||||
},
|
||||
set () {
|
||||
this.$store.commit('node/SET_ACTIVE_SPIDER')
|
||||
}
|
||||
},
|
||||
dialogVisible: {
|
||||
get () {
|
||||
return this.$store.state.dialogView.dialogVisible
|
||||
},
|
||||
set (value) {
|
||||
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', value)
|
||||
}
|
||||
},
|
||||
title () {
|
||||
if (this.dialogType === 'nodeDeploy') {
|
||||
return 'Deploy'
|
||||
} else if (this.dialogType === 'nodeRun') {
|
||||
return 'Run'
|
||||
} else if (this.dialogType === 'spiderDeploy') {
|
||||
return 'Deploy'
|
||||
} else if (this.dialogType === 'spiderRun') {
|
||||
return 'Run'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
message () {
|
||||
if (this.dialogType === 'nodeDeploy') {
|
||||
return 'Please select spider you would like to deploy'
|
||||
} else if (this.dialogType === 'nodeRun') {
|
||||
return 'Please select spider you would like to run'
|
||||
} else if (this.dialogType === 'spiderDeploy') {
|
||||
return 'Please select node you would like to deploy'
|
||||
} else if (this.dialogType === 'spiderRun') {
|
||||
return 'Please select node you would like to run'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onCancel () {
|
||||
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', false)
|
||||
},
|
||||
onConfirm () {
|
||||
if (this.dialogType === 'nodeDeploy') {
|
||||
} else if (this.dialogType === 'nodeRun') {
|
||||
} else if (this.dialogType === 'spiderDeploy') {
|
||||
this.$store.dispatch('spider/deploySpider', {
|
||||
id: this.spiderForm._id.$oid,
|
||||
nodeId: this.activeNode._id
|
||||
})
|
||||
.then(() => {
|
||||
this.$message.success(`Spider "${this.spiderForm.name}" has been deployed on node "${this.activeNode._id}" successfully`)
|
||||
})
|
||||
.finally(() => {
|
||||
// get spider deploys
|
||||
this.$store.dispatch('spider/getDeployList', this.$route.params.id)
|
||||
|
||||
// close dialog
|
||||
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', false)
|
||||
})
|
||||
} else if (this.dialogType === 'spiderRun') {
|
||||
this.$store.dispatch('spider/crawlSpider', {
|
||||
id: this.spiderForm._id.$oid,
|
||||
nodeId: this.activeNode._id
|
||||
})
|
||||
.then(() => {
|
||||
this.$message.success(`Spider "${this.spiderForm.name}" started to run on node "${this.activeNode._id}"`)
|
||||
})
|
||||
.finally(() => {
|
||||
// get spider tasks
|
||||
setTimeout(() => {
|
||||
this.$store.dispatch('spider/getTaskList', this.$route.params.id)
|
||||
}, 500)
|
||||
|
||||
// close dialog
|
||||
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', false)
|
||||
})
|
||||
} else {
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (!this.spiderList || !this.spiderList.length) this.$store.dispatch('spider/getSpiderList')
|
||||
if (!this.nodeList || !this.nodeList.length) this.$store.dispatch('node/getNodeList')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
157
frontend/src/components/DndList/index.vue
Normal file
157
frontend/src/components/DndList/index.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="dndList">
|
||||
<div :style="{width:width1}" class="dndList-list">
|
||||
<h3>{{ list1Title }}</h3>
|
||||
<draggable :list="list1" :options="{group:'article'}" class="dragArea">
|
||||
<div v-for="element in list1" :key="element.id" class="list-complete-item">
|
||||
<div class="list-complete-item-handle">{{ element.id }}[{{ element.author }}] {{ element.title }}</div>
|
||||
<div style="position:absolute;right:0px;">
|
||||
<span style="float: right ;margin-top: -20px;margin-right:5px;" @click="deleteEle(element)">
|
||||
<i style="color:#ff4949" class="el-icon-delete"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
<div :style="{width:width2}" class="dndList-list">
|
||||
<h3>{{ list2Title }}</h3>
|
||||
<draggable :list="list2" :options="{group:'article'}" class="dragArea">
|
||||
<div v-for="element in list2" :key="element.id" class="list-complete-item">
|
||||
<div class="list-complete-item-handle2" @click="pushEle(element)">{{ element.id }} [{{ element.author }}] {{ element.title }}</div>
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
name: 'DndList',
|
||||
components: { draggable },
|
||||
props: {
|
||||
list1: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
list2: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
list1Title: {
|
||||
type: String,
|
||||
default: 'list1'
|
||||
},
|
||||
list2Title: {
|
||||
type: String,
|
||||
default: 'list2'
|
||||
},
|
||||
width1: {
|
||||
type: String,
|
||||
default: '48%'
|
||||
},
|
||||
width2: {
|
||||
type: String,
|
||||
default: '48%'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isNotInList1(v) {
|
||||
return this.list1.every(k => v.id !== k.id)
|
||||
},
|
||||
isNotInList2(v) {
|
||||
return this.list2.every(k => v.id !== k.id)
|
||||
},
|
||||
deleteEle(ele) {
|
||||
for (const item of this.list1) {
|
||||
if (item.id === ele.id) {
|
||||
const index = this.list1.indexOf(item)
|
||||
this.list1.splice(index, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (this.isNotInList2(ele)) {
|
||||
this.list2.unshift(ele)
|
||||
}
|
||||
},
|
||||
pushEle(ele) {
|
||||
for (const item of this.list2) {
|
||||
if (item.id === ele.id) {
|
||||
const index = this.list2.indexOf(item)
|
||||
this.list2.splice(index, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (this.isNotInList1(ele)) {
|
||||
this.list1.push(ele)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.dndList {
|
||||
background: #fff;
|
||||
padding-bottom: 40px;
|
||||
&:after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
.dndList-list {
|
||||
float: left;
|
||||
padding-bottom: 30px;
|
||||
&:first-of-type {
|
||||
margin-right: 2%;
|
||||
}
|
||||
.dragArea {
|
||||
margin-top: 15px;
|
||||
min-height: 50px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-complete-item {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
padding: 5px 12px;
|
||||
margin-top: 4px;
|
||||
border: 1px solid #bfcbd9;
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
.list-complete-item-handle {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
||||
.list-complete-item-handle2 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.list-complete-item.sortable-chosen {
|
||||
background: #4AB7BD;
|
||||
}
|
||||
|
||||
.list-complete-item.sortable-ghost {
|
||||
background: #30B08F;
|
||||
}
|
||||
|
||||
.list-complete-enter,
|
||||
.list-complete-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
61
frontend/src/components/DragSelect/index.vue
Normal file
61
frontend/src/components/DragSelect/index.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<el-select ref="dragSelect" v-model="selectVal" v-bind="$attrs" class="drag-select" multiple v-on="$listeners">
|
||||
<slot/>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
export default {
|
||||
name: 'DragSelect',
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectVal: {
|
||||
get() {
|
||||
return [...this.value]
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', [...val])
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setSort()
|
||||
},
|
||||
methods: {
|
||||
setSort() {
|
||||
const el = this.$refs.dragSelect.$el.querySelectorAll('.el-select__tags > span')[0]
|
||||
this.sortable = Sortable.create(el, {
|
||||
ghostClass: 'sortable-ghost', // Class name for the drop placeholder,
|
||||
setData: function(dataTransfer) {
|
||||
dataTransfer.setData('Text', '')
|
||||
// to avoid Firefox bug
|
||||
// Detail see : https://github.com/RubaXa/Sortable/issues/1012
|
||||
},
|
||||
onEnd: evt => {
|
||||
const targetRow = this.value.splice(evt.oldIndex, 1)[0]
|
||||
this.value.splice(evt.newIndex, 0, targetRow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.drag-select >>> .sortable-ghost{
|
||||
opacity: .8;
|
||||
color: #fff!important;
|
||||
background: #42b983!important;
|
||||
}
|
||||
|
||||
.drag-select >>> .el-tag{
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
297
frontend/src/components/Dropzone/index.vue
Normal file
297
frontend/src/components/Dropzone/index.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<div :ref="id" :action="url" :id="id" class="dropzone">
|
||||
<input type="file" name="file">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Dropzone from 'dropzone'
|
||||
import 'dropzone/dist/dropzone.css'
|
||||
// import { getToken } from 'api/qiniu';
|
||||
|
||||
Dropzone.autoDiscover = false
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
defaultMsg: {
|
||||
type: String,
|
||||
default: '上传图片'
|
||||
},
|
||||
acceptedFiles: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
thumbnailHeight: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
thumbnailWidth: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
showRemoveLink: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
maxFilesize: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
maxFiles: {
|
||||
type: Number,
|
||||
default: 3
|
||||
},
|
||||
autoProcessQueue: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
useCustomDropzoneOptions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
defaultImg: {
|
||||
default: '',
|
||||
type: [String, Array]
|
||||
},
|
||||
couldPaste: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dropzone: '',
|
||||
initOnce: true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
defaultImg(val) {
|
||||
if (val.length === 0) {
|
||||
this.initOnce = false
|
||||
return
|
||||
}
|
||||
if (!this.initOnce) return
|
||||
this.initImages(val)
|
||||
this.initOnce = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const element = document.getElementById(this.id)
|
||||
const vm = this
|
||||
this.dropzone = new Dropzone(element, {
|
||||
clickable: this.clickable,
|
||||
thumbnailWidth: this.thumbnailWidth,
|
||||
thumbnailHeight: this.thumbnailHeight,
|
||||
maxFiles: this.maxFiles,
|
||||
maxFilesize: this.maxFilesize,
|
||||
dictRemoveFile: 'Remove',
|
||||
addRemoveLinks: this.showRemoveLink,
|
||||
acceptedFiles: this.acceptedFiles,
|
||||
autoProcessQueue: this.autoProcessQueue,
|
||||
dictDefaultMessage: '<i style="margin-top: 3em;display: inline-block" class="material-icons">' + this.defaultMsg + '</i><br>Drop files here to upload',
|
||||
dictMaxFilesExceeded: '只能一个图',
|
||||
previewTemplate: '<div class="dz-preview dz-file-preview"> <div class="dz-image" style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" ><img style="width:' + this.thumbnailWidth + 'px;height:' + this.thumbnailHeight + 'px" data-dz-thumbnail /></div> <div class="dz-details"><div class="dz-size"><span data-dz-size></span></div> <div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div> <div class="dz-error-message"><span data-dz-errormessage></span></div> <div class="dz-success-mark"> <i class="material-icons">done</i> </div> <div class="dz-error-mark"><i class="material-icons">error</i></div></div>',
|
||||
init() {
|
||||
const val = vm.defaultImg
|
||||
if (!val) return
|
||||
if (Array.isArray(val)) {
|
||||
if (val.length === 0) return
|
||||
val.map((v, i) => {
|
||||
const mockFile = { name: 'name' + i, size: 12345, url: v }
|
||||
this.options.addedfile.call(this, mockFile)
|
||||
this.options.thumbnail.call(this, mockFile, v)
|
||||
mockFile.previewElement.classList.add('dz-success')
|
||||
mockFile.previewElement.classList.add('dz-complete')
|
||||
vm.initOnce = false
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
const mockFile = { name: 'name', size: 12345, url: val }
|
||||
this.options.addedfile.call(this, mockFile)
|
||||
this.options.thumbnail.call(this, mockFile, val)
|
||||
mockFile.previewElement.classList.add('dz-success')
|
||||
mockFile.previewElement.classList.add('dz-complete')
|
||||
vm.initOnce = false
|
||||
}
|
||||
},
|
||||
accept: (file, done) => {
|
||||
/* 七牛*/
|
||||
// const token = this.$store.getters.token;
|
||||
// getToken(token).then(response => {
|
||||
// file.token = response.data.qiniu_token;
|
||||
// file.key = response.data.qiniu_key;
|
||||
// file.url = response.data.qiniu_url;
|
||||
// done();
|
||||
// })
|
||||
done()
|
||||
},
|
||||
sending: (file, xhr, formData) => {
|
||||
// formData.append('token', file.token);
|
||||
// formData.append('key', file.key);
|
||||
vm.initOnce = false
|
||||
}
|
||||
})
|
||||
|
||||
if (this.couldPaste) {
|
||||
document.addEventListener('paste', this.pasteImg)
|
||||
}
|
||||
|
||||
this.dropzone.on('success', file => {
|
||||
vm.$emit('dropzone-success', file, vm.dropzone.element)
|
||||
})
|
||||
this.dropzone.on('addedfile', file => {
|
||||
vm.$emit('dropzone-fileAdded', file)
|
||||
})
|
||||
this.dropzone.on('removedfile', file => {
|
||||
vm.$emit('dropzone-removedFile', file)
|
||||
})
|
||||
this.dropzone.on('error', (file, error, xhr) => {
|
||||
vm.$emit('dropzone-error', file, error, xhr)
|
||||
})
|
||||
this.dropzone.on('successmultiple', (file, error, xhr) => {
|
||||
vm.$emit('dropzone-successmultiple', file, error, xhr)
|
||||
})
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('paste', this.pasteImg)
|
||||
this.dropzone.destroy()
|
||||
},
|
||||
methods: {
|
||||
removeAllFiles() {
|
||||
this.dropzone.removeAllFiles(true)
|
||||
},
|
||||
processQueue() {
|
||||
this.dropzone.processQueue()
|
||||
},
|
||||
pasteImg(event) {
|
||||
const items = (event.clipboardData || event.originalEvent.clipboardData).items
|
||||
if (items[0].kind === 'file') {
|
||||
this.dropzone.addFile(items[0].getAsFile())
|
||||
}
|
||||
},
|
||||
initImages(val) {
|
||||
if (!val) return
|
||||
if (Array.isArray(val)) {
|
||||
val.map((v, i) => {
|
||||
const mockFile = { name: 'name' + i, size: 12345, url: v }
|
||||
this.dropzone.options.addedfile.call(this.dropzone, mockFile)
|
||||
this.dropzone.options.thumbnail.call(this.dropzone, mockFile, v)
|
||||
mockFile.previewElement.classList.add('dz-success')
|
||||
mockFile.previewElement.classList.add('dz-complete')
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
const mockFile = { name: 'name', size: 12345, url: val }
|
||||
this.dropzone.options.addedfile.call(this.dropzone, mockFile)
|
||||
this.dropzone.options.thumbnail.call(this.dropzone, mockFile, val)
|
||||
mockFile.previewElement.classList.add('dz-success')
|
||||
mockFile.previewElement.classList.add('dz-complete')
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dropzone {
|
||||
border: 2px solid #E5E5E5;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
color: #777;
|
||||
transition: background-color .2s linear;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropzone:hover {
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
|
||||
i {
|
||||
color: #CCC;
|
||||
}
|
||||
|
||||
.dropzone .dz-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropzone input[name='file'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-image {
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview:hover .dz-image img {
|
||||
transform: none;
|
||||
-webkit-filter: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-details {
|
||||
bottom: 0px;
|
||||
top: 0px;
|
||||
color: white;
|
||||
background-color: rgba(33, 150, 243, 0.8);
|
||||
transition: opacity .2s linear;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-details .dz-filename:not(:hover) span {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-details .dz-filename:hover span {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-remove {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
color: white;
|
||||
margin-left: 15px;
|
||||
padding: 10px;
|
||||
top: inherit;
|
||||
bottom: 15px;
|
||||
border: 2px white solid;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1.1px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview:hover .dz-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark {
|
||||
margin-left: -40px;
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
.dropzone .dz-preview .dz-success-mark i, .dropzone .dz-preview .dz-error-mark i {
|
||||
color: white;
|
||||
font-size: 5rem;
|
||||
}
|
||||
</style>
|
||||
63
frontend/src/components/ErrorLog/index.vue
Normal file
63
frontend/src/components/ErrorLog/index.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div v-if="errorLogs.length>0">
|
||||
<el-badge :is-dot="true" style="line-height: 25px;margin-top: -5px;" @click.native="dialogTableVisible=true">
|
||||
<el-button style="padding: 8px 10px;" size="small" type="danger">
|
||||
<svg-icon icon-class="bug" />
|
||||
</el-button>
|
||||
</el-badge>
|
||||
|
||||
<el-dialog :visible.sync="dialogTableVisible" title="Error Log" width="80%">
|
||||
<el-table :data="errorLogs" border>
|
||||
<el-table-column label="Message">
|
||||
<template slot-scope="scope">
|
||||
<div>
|
||||
<span class="message-title">Msg:</span>
|
||||
<el-tag type="danger">{{ scope.row.err.message }}</el-tag>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<span class="message-title" style="padding-right: 10px;">Info: </span>
|
||||
<el-tag type="warning">{{ scope.row.vm.$vnode.tag }} error in {{ scope.row.info }}</el-tag>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<span class="message-title" style="padding-right: 16px;">Url: </span>
|
||||
<el-tag type="success">{{ scope.row.url }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Stack">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.err.stack }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ErrorLog',
|
||||
data() {
|
||||
return {
|
||||
dialogTableVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
errorLogs() {
|
||||
return this.$store.getters.errorLogs
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-title {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
padding-right: 8px;
|
||||
}
|
||||
</style>
|
||||
200
frontend/src/components/FileList/FileList.vue
Normal file
200
frontend/src/components/FileList/FileList.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="file-list-container">
|
||||
<div class="top-part">
|
||||
<!--file path-->
|
||||
<div class="file-path-container">
|
||||
<div class="left">
|
||||
<i class="el-icon-back" @click="onBack"></i>
|
||||
<div class="file-path" v-show="!isEdit">{{currentPath}}</div>
|
||||
<el-input class="file-path"
|
||||
v-show="isEdit"
|
||||
v-model="currentPath"
|
||||
@change="onChange"
|
||||
@keypress.enter.native="onChangeSubmit">
|
||||
</el-input>
|
||||
</div>
|
||||
<i class="el-icon-edit" @click="onEdit"></i>
|
||||
</div>
|
||||
<!--action-->
|
||||
<div class="action-container">
|
||||
<el-button type="success" size="mini">Choose Folder</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!--file list-->
|
||||
<template v-if="true">
|
||||
<!--<code-mirror v-model="code"/>-->
|
||||
<ul class="file-list">
|
||||
<li v-for="(item, index) in fileList" :key="index" class="item" @click="onItemClick(item)">
|
||||
<span class="item-icon">
|
||||
<i class="fa" :class="getIcon(item.type)"></i>
|
||||
</span>
|
||||
<span class="item-name">
|
||||
{{item.path}}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-else>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
import path from 'path'
|
||||
// import { codemirror } from 'vue-codemirror-lite'
|
||||
|
||||
export default {
|
||||
name: 'FileList',
|
||||
components: {
|
||||
// CodeMirror: codemirror
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
code: 'var hello = \'world\'',
|
||||
isEdit: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('file', [
|
||||
'fileList'
|
||||
]),
|
||||
currentPath: {
|
||||
set (value) {
|
||||
this.$store.commit('file/SET_CURRENT_PATH', value)
|
||||
},
|
||||
get () {
|
||||
return this.$store.state.file.currentPath
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getIcon (type) {
|
||||
if (type === 1) {
|
||||
return 'fa-file-o'
|
||||
} else if (type === 2) {
|
||||
return 'fa-folder'
|
||||
}
|
||||
},
|
||||
onEdit () {
|
||||
this.isEdit = true
|
||||
},
|
||||
onChange (path) {
|
||||
this.$store.commit('file/SET_CURRENT_PATH', path)
|
||||
},
|
||||
onChangeSubmit () {
|
||||
this.isEdit = false
|
||||
this.$store.dispatch('file/getFileList', this.currentPath)
|
||||
},
|
||||
onItemClick (item) {
|
||||
if (item.type === 2) {
|
||||
this.$store.commit('file/SET_CURRENT_PATH', path.join(this.currentPath, item.path))
|
||||
this.$store.dispatch('file/getFileList', this.currentPath)
|
||||
}
|
||||
},
|
||||
onBack () {
|
||||
const sep = '/'
|
||||
let arr = this.currentPath.split(sep)
|
||||
arr.splice(arr.length - 1, 1)
|
||||
const path = arr.join(sep)
|
||||
this.$store.commit('file/SET_CURRENT_PATH', path)
|
||||
this.$store.dispatch('file/getFileList', this.currentPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.file-list-container {
|
||||
height: 100%;
|
||||
|
||||
.top-part {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.file-path-container {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
margin: 0 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(48, 65, 86, 0.4);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.left {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
.el-icon-back {
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
/*height: 22px;*/
|
||||
width: 100%;
|
||||
line-height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon-edit {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.action-container {
|
||||
text-align: right;
|
||||
padding: 1px 5px;
|
||||
height: 24px;
|
||||
|
||||
.el-button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
height: 450px;
|
||||
overflow-y: auto;
|
||||
|
||||
.item {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
color: #303133;
|
||||
|
||||
.item-icon {
|
||||
.fa-folder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background-color: rgba(48, 65, 86, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.file-path >>> .el-input__inner {
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
height: 18px;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-bottom: 2px solid #409EFF;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-line {
|
||||
padding-right: 20px;
|
||||
}
|
||||
</style>
|
||||
51
frontend/src/components/GithubCorner/index.vue
Normal file
51
frontend/src/components/GithubCorner/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<a href="https://github.com/PanJiaChen/vue-element-admin" target="_blank" class="github-corner" aria-label="View source on Github">
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 250 250"
|
||||
style="fill:#40c9c6; color:#fff;"
|
||||
aria-hidden="true">
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"/>
|
||||
<path
|
||||
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
|
||||
fill="currentColor"
|
||||
style="transform-origin: 130px 106px;"
|
||||
class="octo-arm"/>
|
||||
<path
|
||||
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
|
||||
fill="currentColor"
|
||||
class="octo-body"/>
|
||||
</svg>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.github-corner:hover .octo-arm {
|
||||
animation: octocat-wave 560ms ease-in-out
|
||||
}
|
||||
|
||||
@keyframes octocat-wave {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0)
|
||||
}
|
||||
20%,
|
||||
60% {
|
||||
transform: rotate(-25deg)
|
||||
}
|
||||
40%,
|
||||
80% {
|
||||
transform: rotate(10deg)
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:500px) {
|
||||
.github-corner:hover .octo-arm {
|
||||
animation: none
|
||||
}
|
||||
.github-corner .octo-arm {
|
||||
animation: octocat-wave 560ms ease-in-out
|
||||
}
|
||||
}
|
||||
</style>
|
||||
42
frontend/src/components/Hamburger/index.vue
Normal file
42
frontend/src/components/Hamburger/index.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<svg
|
||||
:class="{'is-active':isActive}"
|
||||
class="hamburger"
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="64"
|
||||
height="64"
|
||||
@click="toggleClick">
|
||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Hamburger',
|
||||
props: {
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
toggleClick: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hamburger {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.hamburger.is-active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
187
frontend/src/components/HeaderSearch/index.vue
Normal file
187
frontend/src/components/HeaderSearch/index.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div :class="{'show':show}" class="header-search">
|
||||
<svg-icon class-name="search-icon" icon-class="search" @click="click" />
|
||||
<el-select
|
||||
ref="headerSearchSelect"
|
||||
v-model="search"
|
||||
:remote-method="querySearch"
|
||||
filterable
|
||||
default-first-option
|
||||
remote
|
||||
placeholder="Search"
|
||||
class="header-search-select"
|
||||
@change="change">
|
||||
<el-option v-for="item in options" :key="item.path" :value="item" :label="item.title.join(' > ')"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Fuse from 'fuse.js'
|
||||
import path from 'path'
|
||||
import i18n from '@/lang'
|
||||
|
||||
export default {
|
||||
name: 'HeaderSearch',
|
||||
data() {
|
||||
return {
|
||||
search: '',
|
||||
options: [],
|
||||
searchPool: [],
|
||||
show: false,
|
||||
fuse: undefined
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
routers() {
|
||||
return this.$store.getters.permission_routers
|
||||
},
|
||||
lang() {
|
||||
return this.$store.getters.language
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
lang() {
|
||||
this.searchPool = this.generateRouters(this.routers)
|
||||
},
|
||||
routers() {
|
||||
this.searchPool = this.generateRouters(this.routers)
|
||||
},
|
||||
searchPool(list) {
|
||||
this.initFuse(list)
|
||||
},
|
||||
show(value) {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', this.close)
|
||||
} else {
|
||||
document.body.removeEventListener('click', this.close)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.searchPool = this.generateRouters(this.routers)
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
this.show = !this.show
|
||||
if (this.show) {
|
||||
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
|
||||
}
|
||||
},
|
||||
close() {
|
||||
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
|
||||
this.options = []
|
||||
this.show = false
|
||||
},
|
||||
change(val) {
|
||||
this.$router.push(val.path)
|
||||
this.search = ''
|
||||
this.options = []
|
||||
this.$nextTick(() => {
|
||||
this.show = false
|
||||
})
|
||||
},
|
||||
initFuse(list) {
|
||||
this.fuse = new Fuse(list, {
|
||||
shouldSort: true,
|
||||
threshold: 0.4,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: [{
|
||||
name: 'title',
|
||||
weight: 0.7
|
||||
}, {
|
||||
name: 'path',
|
||||
weight: 0.3
|
||||
}]
|
||||
})
|
||||
},
|
||||
// Filter out the routes that can be displayed in the sidebar
|
||||
// And generate the internationalized title
|
||||
generateRouters(routers, basePath = '/', prefixTitle = []) {
|
||||
let res = []
|
||||
|
||||
for (const router of routers) {
|
||||
// skip hidden router
|
||||
if (router.hidden) { continue }
|
||||
|
||||
const data = {
|
||||
path: path.resolve(basePath, router.path),
|
||||
title: [...prefixTitle]
|
||||
}
|
||||
|
||||
if (router.meta && router.meta.title) {
|
||||
// generate internationalized title
|
||||
const i18ntitle = i18n.t(`route.${router.meta.title}`)
|
||||
|
||||
data.title = [...data.title, i18ntitle]
|
||||
|
||||
if (router.redirect !== 'noredirect') {
|
||||
// only push the routes with title
|
||||
// special case: need to exclude parent router without redirect
|
||||
res.push(data)
|
||||
}
|
||||
}
|
||||
|
||||
// recursive child routers
|
||||
if (router.children) {
|
||||
const tempRouters = this.generateRouters(router.children, data.path, data.title)
|
||||
if (tempRouters.length >= 1) {
|
||||
res = [...res, ...tempRouters]
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
},
|
||||
querySearch(query) {
|
||||
if (query !== '') {
|
||||
this.options = this.fuse.search(query)
|
||||
} else {
|
||||
this.options = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-search {
|
||||
font-size: 0 !important;
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-search-select {
|
||||
font-size: 18px;
|
||||
transition: width 0.2s;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
/deep/ .el-input__inner {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
box-shadow: none !important;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.show {
|
||||
.header-search-select {
|
||||
width: 210px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1420
frontend/src/components/ImageCropper/index.vue
Normal file
1420
frontend/src/components/ImageCropper/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
19
frontend/src/components/ImageCropper/utils/data2blob.js
Executable file
19
frontend/src/components/ImageCropper/utils/data2blob.js
Executable file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* database64文件格式转换为2进制
|
||||
*
|
||||
* @param {[String]} data dataURL 的格式为 “data:image/png;base64,****”,逗号之前都是一些说明性的文字,我们只需要逗号之后的就行了
|
||||
* @param {[String]} mime [description]
|
||||
* @return {[blob]} [description]
|
||||
*/
|
||||
export default function(data, mime) {
|
||||
data = data.split(',')[1]
|
||||
data = window.atob(data)
|
||||
var ia = new Uint8Array(data.length)
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
ia[i] = data.charCodeAt(i)
|
||||
}
|
||||
// canvas.toDataURL 返回的默认格式就是 image/png
|
||||
return new Blob([ia], {
|
||||
type: mime
|
||||
})
|
||||
}
|
||||
39
frontend/src/components/ImageCropper/utils/effectRipple.js
Executable file
39
frontend/src/components/ImageCropper/utils/effectRipple.js
Executable file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 点击波纹效果
|
||||
*
|
||||
* @param {[event]} e [description]
|
||||
* @param {[Object]} arg_opts [description]
|
||||
* @return {[bollean]} [description]
|
||||
*/
|
||||
export default function(e, arg_opts) {
|
||||
var opts = Object.assign({
|
||||
ele: e.target, // 波纹作用元素
|
||||
type: 'hit', // hit点击位置扩散center中心点扩展
|
||||
bgc: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
|
||||
}, arg_opts)
|
||||
var target = opts.ele
|
||||
if (target) {
|
||||
var rect = target.getBoundingClientRect()
|
||||
var ripple = target.querySelector('.e-ripple')
|
||||
if (!ripple) {
|
||||
ripple = document.createElement('span')
|
||||
ripple.className = 'e-ripple'
|
||||
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
|
||||
target.appendChild(ripple)
|
||||
} else {
|
||||
ripple.className = 'e-ripple'
|
||||
}
|
||||
switch (opts.type) {
|
||||
case 'center':
|
||||
ripple.style.top = (rect.height / 2 - ripple.offsetHeight / 2) + 'px'
|
||||
ripple.style.left = (rect.width / 2 - ripple.offsetWidth / 2) + 'px'
|
||||
break
|
||||
default:
|
||||
ripple.style.top = (e.pageY - rect.top - ripple.offsetHeight / 2 - document.body.scrollTop) + 'px'
|
||||
ripple.style.left = (e.pageX - rect.left - ripple.offsetWidth / 2 - document.body.scrollLeft) + 'px'
|
||||
}
|
||||
ripple.style.backgroundColor = opts.bgc
|
||||
ripple.className = 'e-ripple z-active'
|
||||
return false
|
||||
}
|
||||
}
|
||||
232
frontend/src/components/ImageCropper/utils/language.js
Executable file
232
frontend/src/components/ImageCropper/utils/language.js
Executable file
@@ -0,0 +1,232 @@
|
||||
export default {
|
||||
zh: {
|
||||
hint: '点击,或拖动图片至此处',
|
||||
loading: '正在上传……',
|
||||
noSupported: '浏览器不支持该功能,请使用IE10以上或其他现在浏览器!',
|
||||
success: '上传成功',
|
||||
fail: '图片上传失败',
|
||||
preview: '头像预览',
|
||||
btn: {
|
||||
off: '取消',
|
||||
close: '关闭',
|
||||
back: '上一步',
|
||||
save: '保存'
|
||||
},
|
||||
error: {
|
||||
onlyImg: '仅限图片格式',
|
||||
outOfSize: '单文件大小不能超过 ',
|
||||
lowestPx: '图片最低像素为(宽*高):'
|
||||
}
|
||||
},
|
||||
'zh-tw': {
|
||||
hint: '點擊,或拖動圖片至此處',
|
||||
loading: '正在上傳……',
|
||||
noSupported: '瀏覽器不支持該功能,請使用IE10以上或其他現代瀏覽器!',
|
||||
success: '上傳成功',
|
||||
fail: '圖片上傳失敗',
|
||||
preview: '頭像預覽',
|
||||
btn: {
|
||||
off: '取消',
|
||||
close: '關閉',
|
||||
back: '上一步',
|
||||
save: '保存'
|
||||
},
|
||||
error: {
|
||||
onlyImg: '僅限圖片格式',
|
||||
outOfSize: '單文件大小不能超過 ',
|
||||
lowestPx: '圖片最低像素為(寬*高):'
|
||||
}
|
||||
},
|
||||
en: {
|
||||
hint: 'Click or drag the file here to upload',
|
||||
loading: 'Uploading…',
|
||||
noSupported: 'Browser is not supported, please use IE10+ or other browsers',
|
||||
success: 'Upload success',
|
||||
fail: 'Upload failed',
|
||||
preview: 'Preview',
|
||||
btn: {
|
||||
off: 'Cancel',
|
||||
close: 'Close',
|
||||
back: 'Back',
|
||||
save: 'Save'
|
||||
},
|
||||
error: {
|
||||
onlyImg: 'Image only',
|
||||
outOfSize: 'Image exceeds size limit: ',
|
||||
lowestPx: 'Image\'s size is too low. Expected at least: '
|
||||
}
|
||||
},
|
||||
ro: {
|
||||
hint: 'Atinge sau trage fișierul aici',
|
||||
loading: 'Se încarcă',
|
||||
noSupported: 'Browser-ul tău nu suportă acest feature. Te rugăm încearcă cu alt browser.',
|
||||
success: 'S-a încărcat cu succes',
|
||||
fail: 'A apărut o problemă la încărcare',
|
||||
preview: 'Previzualizează',
|
||||
|
||||
btn: {
|
||||
off: 'Anulează',
|
||||
close: 'Închide',
|
||||
back: 'Înapoi',
|
||||
save: 'Salvează'
|
||||
},
|
||||
|
||||
error: {
|
||||
onlyImg: 'Doar imagini',
|
||||
outOfSize: 'Imaginea depășește limita de: ',
|
||||
loewstPx: 'Imaginea este prea mică; Minim: '
|
||||
}
|
||||
},
|
||||
ru: {
|
||||
hint: 'Нажмите, или перетащите файл в это окно',
|
||||
loading: 'Загружаю……',
|
||||
noSupported: 'Ваш браузер не поддерживается, пожалуйста, используйте IE10 + или другие браузеры',
|
||||
success: 'Загрузка выполнена успешно',
|
||||
fail: 'Ошибка загрузки',
|
||||
preview: 'Предпросмотр',
|
||||
btn: {
|
||||
off: 'Отменить',
|
||||
close: 'Закрыть',
|
||||
back: 'Назад',
|
||||
save: 'Сохранить'
|
||||
},
|
||||
error: {
|
||||
onlyImg: 'Только изображения',
|
||||
outOfSize: 'Изображение превышает предельный размер: ',
|
||||
lowestPx: 'Минимальный размер изображения: '
|
||||
}
|
||||
},
|
||||
'pt-br': {
|
||||
hint: 'Clique ou arraste o arquivo aqui para carregar',
|
||||
loading: 'Carregando…',
|
||||
noSupported: 'Browser não suportado, use o IE10+ ou outro browser',
|
||||
success: 'Sucesso ao carregar imagem',
|
||||
fail: 'Falha ao carregar imagem',
|
||||
preview: 'Pré-visualizar',
|
||||
btn: {
|
||||
off: 'Cancelar',
|
||||
close: 'Fechar',
|
||||
back: 'Voltar',
|
||||
save: 'Salvar'
|
||||
},
|
||||
error: {
|
||||
onlyImg: 'Apenas imagens',
|
||||
outOfSize: 'A imagem excede o limite de tamanho: ',
|
||||
lowestPx: 'O tamanho da imagem é muito pequeno. Tamanho mínimo: '
|
||||
}
|
||||
},
|
||||
fr: {
|
||||
hint: 'Cliquez ou glissez le fichier ici.',
|
||||
loading: 'Téléchargement…',
|
||||
noSupported: 'Votre navigateur n\'est pas supporté. Utilisez IE10 + ou un autre navigateur s\'il vous plaît.',
|
||||
success: 'Téléchargement réussit',
|
||||
fail: 'Téléchargement echoué',
|
||||
preview: 'Aperçu',
|
||||
btn: {
|
||||
off: 'Annuler',
|
||||
close: 'Fermer',
|
||||
back: 'Retour',
|
||||
save: 'Enregistrer'
|
||||
},
|
||||
error: {
|
||||
onlyImg: 'Image uniquement',
|
||||
outOfSize: 'L\'image sélectionnée dépasse la taille maximum: ',
|
||||
lowestPx: 'L\'image sélectionnée est trop petite. Dimensions attendues: '
|
||||
}
|
||||
},
|
||||
nl: {
|
||||
hint: 'Klik hier of sleep een afbeelding in dit vlak',
|
||||
loading: 'Uploaden…',
|
||||
noSupported: 'Je browser wordt helaas niet ondersteund. Gebruik IE10+ of een andere browser.',
|
||||
success: 'Upload succesvol',
|
||||
fail: 'Upload mislukt',
|
||||
preview: 'Voorbeeld',
|
||||
btn: {
|
||||
off: 'Annuleren',
|
||||
close: 'Sluiten',
|
||||
back: 'Terug',
|
||||
save: 'Opslaan'
|
||||
},
|
||||
error: {
|
||||
onlyImg: 'Alleen afbeeldingen',
|
||||
outOfSize: 'De afbeelding is groter dan: ',
|
||||
lowestPx: 'De afbeelding is te klein! Minimale afmetingen: '
|
||||
}
|
||||
},
|
||||
tr: {
|
||||
hint: 'Tıkla veya yüklemek istediğini buraya sürükle',
|
||||
loading: 'Yükleniyor…',
|
||||
noSupported: 'Tarayıcı desteklenmiyor, lütfen IE10+ veya farklı tarayıcı kullanın',
|
||||
success: 'Yükleme başarılı',
|
||||
fail: 'Yüklemede hata oluştu',
|
||||
preview: 'Önizle',
|
||||
btn: {
|
||||
off: 'İptal',
|
||||
close: 'Kapat',
|
||||
back: 'Geri',
|
||||
save: 'Kaydet'
|
||||
},
|
||||
error: {
|
||||
onlyImg: 'Sadece resim',
|
||||
outOfSize: 'Resim yükleme limitini aşıyor: ',
|
||||
lowestPx: 'Resmin boyutu çok küçük. En az olması gereken: '
|
||||
}
|
||||
},
|
||||
'es-MX': {
|
||||
hint: 'Selecciona o arrastra una imagen',
|
||||
loading: 'Subiendo...',
|
||||
noSupported: 'Tu navegador no es soportado, porfavor usa IE10+ u otros navegadores mas recientes',
|
||||
success: 'Subido exitosamente',
|
||||
fail: 'Sucedió un error',
|
||||
preview: 'Vista previa',
|
||||
btn: {
|
||||
off: 'Cancelar',
|
||||
close: 'Cerrar',
|
||||
back: 'Atras',
|
||||
save: 'Guardar'
|
||||
},
|
||||
error: {
|
||||
onlyImg: 'Unicamente imagenes',
|
||||
outOfSize: 'La imagen excede el tamaño maximo:',
|
||||
lowestPx: 'La imagen es demasiado pequeño. Se espera por lo menos:'
|
||||
}
|
||||
},
|
||||
de: {
|
||||
hint: 'Klick hier oder zieh eine Datei hier rein zum Hochladen',
|
||||
loading: 'Hochladen…',
|
||||
noSupported: 'Browser wird nicht unterstützt, bitte verwende IE10+ oder andere Browser',
|
||||
success: 'Upload erfolgreich',
|
||||
fail: 'Upload fehlgeschlagen',
|
||||
preview: 'Vorschau',
|
||||
btn: {
|
||||
off: 'Abbrechen',
|
||||
close: 'Schließen',
|
||||
back: 'Zurück',
|
||||
save: 'Speichern'
|
||||
},
|
||||
error: {
|
||||
onlyImg: 'Nur Bilder',
|
||||
outOfSize: 'Das Bild ist zu groß: ',
|
||||
lowestPx: 'Das Bild ist zu klein. Mindestens: '
|
||||
}
|
||||
},
|
||||
ja: {
|
||||
hint: 'クリック・ドラッグしてファイルをアップロード',
|
||||
loading: 'アップロード中...',
|
||||
noSupported: 'このブラウザは対応されていません。IE10+かその他の主要ブラウザをお使いください。',
|
||||
success: 'アップロード成功',
|
||||
fail: 'アップロード失敗',
|
||||
preview: 'プレビュー',
|
||||
btn: {
|
||||
off: 'キャンセル',
|
||||
close: '閉じる',
|
||||
back: '戻る',
|
||||
save: '保存'
|
||||
},
|
||||
error: {
|
||||
onlyImg: '画像のみ',
|
||||
outOfSize: '画像サイズが上限を超えています。上限: ',
|
||||
lowestPx: '画像が小さすぎます。最小サイズ: '
|
||||
}
|
||||
}
|
||||
}
|
||||
7
frontend/src/components/ImageCropper/utils/mimes.js
Executable file
7
frontend/src/components/ImageCropper/utils/mimes.js
Executable file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
'jpg': 'image/jpeg',
|
||||
'png': 'image/png',
|
||||
'gif': 'image/gif',
|
||||
'svg': 'image/svg+xml',
|
||||
'psd': 'image/photoshop'
|
||||
}
|
||||
73
frontend/src/components/InfoView/NodeInfoView.vue
Normal file
73
frontend/src/components/InfoView/NodeInfoView.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="info-view">
|
||||
<el-row>
|
||||
<el-form label-width="150px"
|
||||
:model="nodeForm"
|
||||
ref="nodeForm"
|
||||
class="node-form"
|
||||
label-position="right">
|
||||
<el-form-item label="Node Name">
|
||||
<el-input v-model="nodeForm.name" placeholder="Node Name" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Node IP" prop="ip" required>
|
||||
<el-input v-model="nodeForm.ip" placeholder="Node IP" :disabled="isView"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Node Port" prop="port" required>
|
||||
<el-input v-model="nodeForm.port" placeholder="Node Port" :disabled="isView"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Description">
|
||||
<el-input type="textarea" v-model="nodeForm.description" placeholder="Description" :disabled="isView">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-row class="button-container" v-if="!isView">
|
||||
<el-button type="success" @click="onSave">Save</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'NodeInfoView',
|
||||
props: {
|
||||
isView: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('node', [
|
||||
'nodeForm'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
onSave () {
|
||||
this.$refs.nodeForm.validate(valid => {
|
||||
if (valid) {
|
||||
this.$store.dispatch('node/editNode')
|
||||
.then(() => {
|
||||
this.$message.success('Node has been saved successfully')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.node-form {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
112
frontend/src/components/InfoView/SpiderInfoView.vue
Normal file
112
frontend/src/components/InfoView/SpiderInfoView.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="info-view">
|
||||
<el-row>
|
||||
<el-form label-width="150px"
|
||||
:model="spiderForm"
|
||||
ref="spiderForm"
|
||||
class="spider-form"
|
||||
label-position="right">
|
||||
<el-form-item label="Spider ID">
|
||||
<el-input v-model="spiderForm._id.$oid" placeholder="Spider ID" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Spider Name">
|
||||
<el-input v-model="spiderForm.name" placeholder="Spider Name" :disabled="isView"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Source Folder">
|
||||
<el-input v-model="spiderForm.src" placeholder="Source Folder" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Execute Command" prop="cmd" :rule="cmdRule" required>
|
||||
<el-input v-model="spiderForm.cmd" placeholder="Execute Command"
|
||||
:disabled="isView"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Spider Type">
|
||||
<el-select v-model="spiderForm.type" placeholder="Select Spider Type" :disabled="isView" clearable>
|
||||
<el-option value="scrapy" label="Scrapy"></el-option>
|
||||
<el-option value="pyspider" label="PySpider"></el-option>
|
||||
<el-option value="webmagic" label="WebMagic"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Language">
|
||||
<el-select v-model="spiderForm.lang" placeholder="Select Language" :disabled="isView" clearable>
|
||||
<el-option value="python" label="Python"></el-option>
|
||||
<el-option value="javascript" label="JavaScript"></el-option>
|
||||
<el-option value="java" label="Java"></el-option>
|
||||
<el-option value="go" label="Go"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-row class="button-container" v-if="!isView">
|
||||
<el-button type="success" @click="onRun">Run</el-button>
|
||||
<el-button type="primary" @click="onDeploy">Deploy</el-button>
|
||||
<el-button type="success" @click="onSave">Save</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'SpiderInfoView',
|
||||
props: {
|
||||
isView: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
cmdRule: [
|
||||
{ message: 'Execute Command should not be empty', required: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('spider', [
|
||||
'spiderForm'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
onRun () {
|
||||
this.$refs['spiderForm'].validate(res => {
|
||||
if (res) {
|
||||
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', true)
|
||||
this.$store.commit('dialogView/SET_DIALOG_TYPE', 'spiderRun')
|
||||
}
|
||||
})
|
||||
},
|
||||
onDeploy () {
|
||||
this.$store.commit('dialogView/SET_DIALOG_VISIBLE', true)
|
||||
this.$store.commit('dialogView/SET_DIALOG_TYPE', 'spiderDeploy')
|
||||
},
|
||||
onSave () {
|
||||
this.$refs['spiderForm'].validate(res => {
|
||||
if (res) {
|
||||
this.$store.dispatch('spider/editSpider')
|
||||
.then(() => {
|
||||
this.$message.success('Spider info has been saved successfully')
|
||||
})
|
||||
.catch(error => {
|
||||
this.$message.error(error)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.spider-form {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
89
frontend/src/components/InfoView/TaskInfoView.vue
Normal file
89
frontend/src/components/InfoView/TaskInfoView.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="info-view">
|
||||
<el-row>
|
||||
<el-form label-width="150px"
|
||||
:model="taskForm"
|
||||
ref="nodeForm"
|
||||
class="node-form"
|
||||
label-position="right">
|
||||
<el-form-item label="Task ID">
|
||||
<el-input v-model="taskForm._id" placeholder="Task ID" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Status">
|
||||
<el-tag type="success" v-if="taskForm.status === 'SUCCESS'">SUCCESS</el-tag>
|
||||
<el-tag type="warning" v-else-if="taskForm.status === 'PENDING'">PENDING</el-tag>
|
||||
<el-tag type="danger" v-else-if="taskForm.status === 'FAILURE'">FAILURE</el-tag>
|
||||
<el-tag type="info" v-else>{{taskForm.status}}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="Spider Version">
|
||||
<el-input v-model="taskForm.spider_version" placeholder="Log File Path" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Log File Path">
|
||||
<el-input v-model="taskForm.log_file_path" placeholder="Log File Path" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Create Timestamp">
|
||||
<el-input v-model="taskForm.create_ts" placeholder="Create Timestamp" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Finish Timestamp">
|
||||
<el-input v-model="taskForm.finish_ts" placeholder="Finish Timestamp" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Duration (sec)">
|
||||
<el-input v-model="taskForm.duration" placeholder="Duration" disabled></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Error Message" v-if="taskForm.status === 'FAILURE'">
|
||||
<div class="error-message">
|
||||
{{taskForm.result}}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<el-row class="button-container">
|
||||
<el-button type="danger" @click="onRestart">Restart</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'NodeInfoView',
|
||||
computed: {
|
||||
...mapState('task', [
|
||||
'taskForm'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
Restart () {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.node-form {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: rgba(245, 108, 108, .1);
|
||||
color: #f56c6c;
|
||||
border: 1px solid rgba(245, 108, 108, .2);
|
||||
border-radius: 4px;
|
||||
line-height: 18px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
</style>
|
||||
72
frontend/src/components/JsonEditor/index.vue
Normal file
72
frontend/src/components/JsonEditor/index.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="json-editor">
|
||||
<textarea ref="textarea"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CodeMirror from 'codemirror'
|
||||
import 'codemirror/addon/lint/lint.css'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/theme/rubyblue.css'
|
||||
require('script-loader!jsonlint')
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/addon/lint/lint'
|
||||
import 'codemirror/addon/lint/json-lint'
|
||||
|
||||
export default {
|
||||
name: 'JsonEditor',
|
||||
/* eslint-disable vue/require-prop-types */
|
||||
props: ['value'],
|
||||
data() {
|
||||
return {
|
||||
jsonEditor: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(value) {
|
||||
const editor_value = this.jsonEditor.getValue()
|
||||
if (value !== editor_value) {
|
||||
this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.jsonEditor = CodeMirror.fromTextArea(this.$refs.textarea, {
|
||||
lineNumbers: true,
|
||||
mode: 'application/json',
|
||||
gutters: ['CodeMirror-lint-markers'],
|
||||
theme: 'rubyblue',
|
||||
lint: true
|
||||
})
|
||||
|
||||
this.jsonEditor.setValue(JSON.stringify(this.value, null, 2))
|
||||
this.jsonEditor.on('change', cm => {
|
||||
this.$emit('changed', cm.getValue())
|
||||
this.$emit('input', cm.getValue())
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
getValue() {
|
||||
return this.jsonEditor.getValue()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.json-editor{
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.json-editor >>> .CodeMirror {
|
||||
height: auto;
|
||||
min-height: 300px;
|
||||
}
|
||||
.json-editor >>> .CodeMirror-scroll{
|
||||
min-height: 300px;
|
||||
}
|
||||
.json-editor >>> .cm-s-rubyblue span.cm-string {
|
||||
color: #F08047;
|
||||
}
|
||||
</style>
|
||||
89
frontend/src/components/Kanban/index.vue
Normal file
89
frontend/src/components/Kanban/index.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="board-column">
|
||||
<div class="board-column-header">
|
||||
{{ headerText }}
|
||||
</div>
|
||||
<draggable
|
||||
:list="list"
|
||||
:options="options"
|
||||
class="board-column-content">
|
||||
<div v-for="element in list" :key="element.id" class="board-item">
|
||||
{{ element.name }} {{ element.id }}
|
||||
</div>
|
||||
</draggable>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
name: 'DragKanbanDemo',
|
||||
components: {
|
||||
draggable
|
||||
},
|
||||
props: {
|
||||
headerText: {
|
||||
type: String,
|
||||
default: 'Header'
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
list: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.board-column {
|
||||
min-width: 300px;
|
||||
min-height: 100px;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
border-radius: 3px;
|
||||
|
||||
.board-column-header {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
overflow: hidden;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
|
||||
.board-column-content {
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
border: 10px solid transparent;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.board-item {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
margin: 5px 0;
|
||||
background-color: #fff;
|
||||
text-align: left;
|
||||
line-height: 54px;
|
||||
padding: 5px 10px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 1px 3px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
32
frontend/src/components/LangSelect/index.vue
Normal file
32
frontend/src/components/LangSelect/index.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown trigger="click" class="international" @command="handleSetLanguage">
|
||||
<div>
|
||||
<svg-icon class-name="international-icon" icon-class="language" />
|
||||
</div>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :disabled="language==='zh'" command="zh">中文</el-dropdown-item>
|
||||
<el-dropdown-item :disabled="language==='en'" command="en">English</el-dropdown-item>
|
||||
<el-dropdown-item :disabled="language==='es'" command="es">Español</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
language() {
|
||||
return this.$store.getters.language
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSetLanguage(lang) {
|
||||
this.$i18n.locale = lang
|
||||
this.$store.dispatch('setLanguage', lang)
|
||||
this.$message({
|
||||
message: 'Switch Language Success',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
354
frontend/src/components/MDinput/index.vue
Normal file
354
frontend/src/components/MDinput/index.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div :class="computedClasses" class="material-input__component">
|
||||
<div :class="{iconClass:icon}">
|
||||
<i v-if="icon" :class="['el-icon-' + icon]" class="el-input__icon material-input__icon"/>
|
||||
<input
|
||||
v-if="type === 'email'"
|
||||
:name="name"
|
||||
:placeholder="fillPlaceHolder"
|
||||
v-model="currentValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autoComplete="autoComplete"
|
||||
:required="required"
|
||||
type="email"
|
||||
class="material-input"
|
||||
@focus="handleMdFocus"
|
||||
@blur="handleMdBlur"
|
||||
@input="handleModelInput">
|
||||
<input
|
||||
v-if="type === 'url'"
|
||||
:name="name"
|
||||
:placeholder="fillPlaceHolder"
|
||||
v-model="currentValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autoComplete="autoComplete"
|
||||
:required="required"
|
||||
type="url"
|
||||
class="material-input"
|
||||
@focus="handleMdFocus"
|
||||
@blur="handleMdBlur"
|
||||
@input="handleModelInput">
|
||||
<input
|
||||
v-if="type === 'number'"
|
||||
:name="name"
|
||||
:placeholder="fillPlaceHolder"
|
||||
v-model="currentValue"
|
||||
:step="step"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autoComplete="autoComplete"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:minlength="minlength"
|
||||
:maxlength="maxlength"
|
||||
:required="required"
|
||||
type="number"
|
||||
class="material-input"
|
||||
@focus="handleMdFocus"
|
||||
@blur="handleMdBlur"
|
||||
@input="handleModelInput">
|
||||
<input
|
||||
v-if="type === 'password'"
|
||||
:name="name"
|
||||
:placeholder="fillPlaceHolder"
|
||||
v-model="currentValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autoComplete="autoComplete"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:required="required"
|
||||
type="password"
|
||||
class="material-input"
|
||||
@focus="handleMdFocus"
|
||||
@blur="handleMdBlur"
|
||||
@input="handleModelInput">
|
||||
<input
|
||||
v-if="type === 'tel'"
|
||||
:name="name"
|
||||
:placeholder="fillPlaceHolder"
|
||||
v-model="currentValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autoComplete="autoComplete"
|
||||
:required="required"
|
||||
type="tel"
|
||||
class="material-input"
|
||||
@focus="handleMdFocus"
|
||||
@blur="handleMdBlur"
|
||||
@input="handleModelInput">
|
||||
<input
|
||||
v-if="type === 'text'"
|
||||
:name="name"
|
||||
:placeholder="fillPlaceHolder"
|
||||
v-model="currentValue"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:autoComplete="autoComplete"
|
||||
:minlength="minlength"
|
||||
:maxlength="maxlength"
|
||||
:required="required"
|
||||
type="text"
|
||||
class="material-input"
|
||||
@focus="handleMdFocus"
|
||||
@blur="handleMdBlur"
|
||||
@input="handleModelInput">
|
||||
<span class="material-input-bar"/>
|
||||
<label class="material-label">
|
||||
<slot/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// source:https://github.com/wemake-services/vue-material-input/blob/master/src/components/MaterialInput.vue
|
||||
|
||||
export default {
|
||||
name: 'MdInput',
|
||||
props: {
|
||||
/* eslint-disable */
|
||||
icon: String,
|
||||
name: String,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
value: [String, Number],
|
||||
placeholder: String,
|
||||
readonly: Boolean,
|
||||
disabled: Boolean,
|
||||
min: String,
|
||||
max: String,
|
||||
step: String,
|
||||
minlength: Number,
|
||||
maxlength: Number,
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoComplete: {
|
||||
type: String,
|
||||
default: 'off'
|
||||
},
|
||||
validateEvent: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: this.value,
|
||||
focus: false,
|
||||
fillPlaceHolder: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedClasses() {
|
||||
return {
|
||||
'material--active': this.focus,
|
||||
'material--disabled': this.disabled,
|
||||
'material--raised': Boolean(this.focus || this.currentValue) // has value
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.currentValue = newValue
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleModelInput(event) {
|
||||
const value = event.target.value
|
||||
this.$emit('input', value)
|
||||
if (this.$parent.$options.componentName === 'ElFormItem') {
|
||||
if (this.validateEvent) {
|
||||
this.$parent.$emit('el.form.change', [value])
|
||||
}
|
||||
}
|
||||
this.$emit('change', value)
|
||||
},
|
||||
handleMdFocus(event) {
|
||||
this.focus = true
|
||||
this.$emit('focus', event)
|
||||
if (this.placeholder && this.placeholder !== '') {
|
||||
this.fillPlaceHolder = this.placeholder
|
||||
}
|
||||
},
|
||||
handleMdBlur(event) {
|
||||
this.focus = false
|
||||
this.$emit('blur', event)
|
||||
this.fillPlaceHolder = null
|
||||
if (this.$parent.$options.componentName === 'ElFormItem') {
|
||||
if (this.validateEvent) {
|
||||
this.$parent.$emit('el.form.blur', [this.currentValue])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
// Fonts:
|
||||
$font-size-base: 16px;
|
||||
$font-size-small: 18px;
|
||||
$font-size-smallest: 12px;
|
||||
$font-weight-normal: normal;
|
||||
$font-weight-bold: bold;
|
||||
$apixel: 1px;
|
||||
// Utils
|
||||
$spacer: 12px;
|
||||
$transition: 0.2s ease all;
|
||||
$index: 0px;
|
||||
$index-has-icon: 30px;
|
||||
// Theme:
|
||||
$color-white: white;
|
||||
$color-grey: #9E9E9E;
|
||||
$color-grey-light: #E0E0E0;
|
||||
$color-blue: #2196F3;
|
||||
$color-red: #F44336;
|
||||
$color-black: black;
|
||||
// Base clases:
|
||||
%base-bar-pseudo {
|
||||
content: '';
|
||||
height: 1px;
|
||||
width: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
transition: $transition;
|
||||
}
|
||||
|
||||
// Mixins:
|
||||
@mixin slided-top() {
|
||||
top: - ($font-size-base + $spacer);
|
||||
left: 0;
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
// Component:
|
||||
.material-input__component {
|
||||
margin-top: 36px;
|
||||
position: relative;
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.iconClass {
|
||||
.material-input__icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
line-height: $font-size-base;
|
||||
color: $color-blue;
|
||||
top: $spacer;
|
||||
width: $index-has-icon;
|
||||
height: $font-size-base;
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-normal;
|
||||
pointer-events: none;
|
||||
}
|
||||
.material-label {
|
||||
left: $index-has-icon;
|
||||
}
|
||||
.material-input {
|
||||
text-indent: $index-has-icon;
|
||||
}
|
||||
}
|
||||
.material-input {
|
||||
font-size: $font-size-base;
|
||||
padding: $spacer $spacer $spacer - $apixel * 10 $spacer / 2;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: none;
|
||||
line-height: 1;
|
||||
border-radius: 0;
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid transparent; // fixes the height issue
|
||||
}
|
||||
}
|
||||
.material-label {
|
||||
font-weight: $font-weight-normal;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: $index;
|
||||
top: 0;
|
||||
transition: $transition;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
.material-input-bar {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
&:before {
|
||||
@extend %base-bar-pseudo;
|
||||
left: 50%;
|
||||
}
|
||||
&:after {
|
||||
@extend %base-bar-pseudo;
|
||||
right: 50%;
|
||||
}
|
||||
}
|
||||
// Disabled state:
|
||||
&.material--disabled {
|
||||
.material-input {
|
||||
border-bottom-style: dashed;
|
||||
}
|
||||
}
|
||||
// Raised state:
|
||||
&.material--raised {
|
||||
.material-label {
|
||||
@include slided-top();
|
||||
}
|
||||
}
|
||||
// Active state:
|
||||
&.material--active {
|
||||
.material-input-bar {
|
||||
&:before,
|
||||
&:after {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.material-input__component {
|
||||
background: $color-white;
|
||||
.material-input {
|
||||
background: none;
|
||||
color: $color-black;
|
||||
text-indent: $index;
|
||||
border-bottom: 1px solid $color-grey-light;
|
||||
}
|
||||
.material-label {
|
||||
color: $color-grey;
|
||||
}
|
||||
.material-input-bar {
|
||||
&:before,
|
||||
&:after {
|
||||
background: $color-blue;
|
||||
}
|
||||
}
|
||||
// Active state:
|
||||
&.material--active {
|
||||
.material-label {
|
||||
color: $color-blue;
|
||||
}
|
||||
}
|
||||
// Errors:
|
||||
&.material--has-errors {
|
||||
&.material--active .material-label {
|
||||
color: $color-red;
|
||||
}
|
||||
.material-input-bar {
|
||||
&:before,
|
||||
&:after {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
31
frontend/src/components/MarkdownEditor/defaultOptions.js
Normal file
31
frontend/src/components/MarkdownEditor/defaultOptions.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// doc: https://nhnent.github.io/tui.editor/api/latest/ToastUIEditor.html#ToastUIEditor
|
||||
export default {
|
||||
minHeight: '200px',
|
||||
previewStyle: 'vertical',
|
||||
useCommandShortcut: true,
|
||||
useDefaultHTMLSanitizer: true,
|
||||
usageStatistics: false,
|
||||
hideModeSwitch: false,
|
||||
toolbarItems: [
|
||||
'heading',
|
||||
'bold',
|
||||
'italic',
|
||||
'strike',
|
||||
'divider',
|
||||
'hr',
|
||||
'quote',
|
||||
'divider',
|
||||
'ul',
|
||||
'ol',
|
||||
'task',
|
||||
'indent',
|
||||
'outdent',
|
||||
'divider',
|
||||
'table',
|
||||
'image',
|
||||
'link',
|
||||
'divider',
|
||||
'code',
|
||||
'codeblock'
|
||||
]
|
||||
}
|
||||
118
frontend/src/components/MarkdownEditor/index.vue
Normal file
118
frontend/src/components/MarkdownEditor/index.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div :id="id"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// deps for editor
|
||||
import 'codemirror/lib/codemirror.css' // codemirror
|
||||
import 'tui-editor/dist/tui-editor.css' // editor ui
|
||||
import 'tui-editor/dist/tui-editor-contents.css' // editor content
|
||||
|
||||
import Editor from 'tui-editor'
|
||||
import defaultOptions from './defaultOptions'
|
||||
|
||||
export default {
|
||||
name: 'MarddownEditor',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
default() {
|
||||
return 'markdown-editor-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
|
||||
}
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default() {
|
||||
return defaultOptions
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'markdown'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '300px'
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'en_US' // https://github.com/nhnent/tui.editor/tree/master/src/js/langs
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editor: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
editorOptions() {
|
||||
const options = Object.assign({}, defaultOptions, this.options)
|
||||
options.initialEditType = this.mode
|
||||
options.height = this.height
|
||||
options.language = this.language
|
||||
return options
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newValue, preValue) {
|
||||
if (newValue !== preValue && newValue !== this.editor.getValue()) {
|
||||
this.editor.setValue(newValue)
|
||||
}
|
||||
},
|
||||
language(val) {
|
||||
this.destroyEditor()
|
||||
this.initEditor()
|
||||
},
|
||||
height(newValue) {
|
||||
this.editor.height(newValue)
|
||||
},
|
||||
mode(newValue) {
|
||||
this.editor.changeMode(newValue)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initEditor()
|
||||
},
|
||||
destroyed() {
|
||||
this.destroyEditor()
|
||||
},
|
||||
methods: {
|
||||
initEditor() {
|
||||
this.editor = new Editor({
|
||||
el: document.getElementById(this.id),
|
||||
...this.editorOptions
|
||||
})
|
||||
if (this.value) {
|
||||
this.editor.setValue(this.value)
|
||||
}
|
||||
this.editor.on('change', () => {
|
||||
this.$emit('input', this.editor.getValue())
|
||||
})
|
||||
},
|
||||
destroyEditor() {
|
||||
if (!this.editor) return
|
||||
this.editor.off('change')
|
||||
this.editor.remove()
|
||||
},
|
||||
setValue(value) {
|
||||
this.editor.setValue(value)
|
||||
},
|
||||
getValue() {
|
||||
return this.editor.getValue()
|
||||
},
|
||||
setHtml(value) {
|
||||
this.editor.setHtml(value)
|
||||
},
|
||||
getHtml() {
|
||||
return this.editor.getHtml()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
55
frontend/src/components/Overview/NodeOverview.vue
Normal file
55
frontend/src/components/Overview/NodeOverview.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<!--last tasks-->
|
||||
<el-row>
|
||||
<task-table-view title="Latest Tasks"/>
|
||||
</el-row>
|
||||
|
||||
<!--last deploys-->
|
||||
<el-row>
|
||||
<deploy-table-view title="Latest Deploys"/>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<!--basic info-->
|
||||
<node-info-view/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
import DeployTableView from '../TableView/DeployTableView'
|
||||
import TaskTableView from '../TableView/TaskTableView'
|
||||
import NodeInfoView from '../InfoView/NodeInfoView'
|
||||
|
||||
export default {
|
||||
name: 'NodeOverview',
|
||||
components: {
|
||||
NodeInfoView,
|
||||
DeployTableView,
|
||||
TaskTableView
|
||||
},
|
||||
computed: {
|
||||
id () {
|
||||
return this.$route.params.id
|
||||
},
|
||||
...mapState('node', [
|
||||
'nodeForm'
|
||||
])
|
||||
},
|
||||
methods: {},
|
||||
created () {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
margin: 10px 0 3px 0;
|
||||
}
|
||||
</style>
|
||||
63
frontend/src/components/Overview/SpiderOverview.vue
Normal file
63
frontend/src/components/Overview/SpiderOverview.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<!--last tasks-->
|
||||
<el-row>
|
||||
<task-table-view title="Latest Tasks"/>
|
||||
</el-row>
|
||||
|
||||
<!--last deploys-->
|
||||
<el-row>
|
||||
<deploy-table-view title="Latest Deploys"/>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<!--basic info-->
|
||||
<spider-info-view/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
import DeployTableView from '../TableView/DeployTableView'
|
||||
import TaskTableView from '../TableView/TaskTableView'
|
||||
import SpiderInfoView from '../InfoView/SpiderInfoView'
|
||||
|
||||
export default {
|
||||
name: 'SpiderOverview',
|
||||
components: {
|
||||
SpiderInfoView,
|
||||
DeployTableView,
|
||||
TaskTableView
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// spiderForm: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
id () {
|
||||
return this.$route.params.id
|
||||
},
|
||||
...mapState('spider', [
|
||||
'spiderForm'
|
||||
]),
|
||||
...mapState('deploy', [
|
||||
'deployList'
|
||||
])
|
||||
},
|
||||
methods: {},
|
||||
created () {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
margin: 10px 0 3px 0;
|
||||
}
|
||||
</style>
|
||||
89
frontend/src/components/Overview/TaskOverview.vue
Normal file
89
frontend/src/components/Overview/TaskOverview.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<el-row>
|
||||
<el-col :span="12" style="padding-right: 20px;">
|
||||
<el-row>
|
||||
<h4 class="title">Task Info</h4>
|
||||
<task-info-view/>
|
||||
</el-row>
|
||||
<el-row style="border-bottom:1px solid #e4e7ed;margin:0 0 20px 0;padding-bottom:20px;"/>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-row>
|
||||
<h4 class="title spider-title" @click="onClickSpiderTitle">Spider Info</h4>
|
||||
<spider-info-view :is-view="true"/>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<h4 class="title node-title" @click="onClickNodeTitle">Node Info</h4>
|
||||
<node-info-view :is-view="true"/>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
import SpiderInfoView from '../InfoView/SpiderInfoView'
|
||||
import NodeInfoView from '../InfoView/NodeInfoView'
|
||||
import TaskInfoView from '../InfoView/TaskInfoView'
|
||||
|
||||
export default {
|
||||
name: 'SpiderOverview',
|
||||
components: {
|
||||
NodeInfoView,
|
||||
SpiderInfoView,
|
||||
TaskInfoView
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
// spiderForm: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('node', [
|
||||
'nodeForm'
|
||||
]),
|
||||
...mapState('spider', [
|
||||
'spiderForm'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
onClickNodeTitle () {
|
||||
this.$router.push(`/nodes/${this.nodeForm._id}`)
|
||||
},
|
||||
onClickSpiderTitle () {
|
||||
this.$router.push(`/spiders/${this.spiderForm._id.$oid}`)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
margin: 10px 0 3px 0;
|
||||
}
|
||||
|
||||
.spider-form {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.node-title,
|
||||
.spider-title {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
100
frontend/src/components/Pagination/index.vue
Normal file
100
frontend/src/components/Pagination/index.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div :class="{'hidden':hidden}" class="pagination-container">
|
||||
<el-pagination
|
||||
:background="background"
|
||||
:current-page.sync="currentPage"
|
||||
:page-size.sync="pageSize"
|
||||
:layout="layout"
|
||||
:page-sizes="pageSizes"
|
||||
:total="total"
|
||||
v-bind="$attrs"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { scrollTo } from '@/utils/scrollTo'
|
||||
|
||||
export default {
|
||||
name: 'Pagination',
|
||||
props: {
|
||||
total: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
pageSizes: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [10, 20, 30, 50]
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'total, sizes, prev, pager, next, jumper'
|
||||
},
|
||||
background: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoScroll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hidden: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentPage: {
|
||||
get() {
|
||||
return this.page
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:page', val)
|
||||
}
|
||||
},
|
||||
pageSize: {
|
||||
get() {
|
||||
return this.limit
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:limit', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSizeChange(val) {
|
||||
this.$emit('pagination', { page: this.currentPage, limit: val })
|
||||
if (this.autoScroll) {
|
||||
scrollTo(0, 800)
|
||||
}
|
||||
},
|
||||
handleCurrentChange(val) {
|
||||
this.$emit('pagination', { page: val, limit: this.pageSize })
|
||||
if (this.autoScroll) {
|
||||
scrollTo(0, 800)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pagination-container {
|
||||
background: #fff;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
.pagination-container.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
140
frontend/src/components/PanThumb/index.vue
Normal file
140
frontend/src/components/PanThumb/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
|
||||
<div class="pan-info">
|
||||
<div class="pan-info-roles-container">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
<img :src="image" class="pan-thumb">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PanThumb',
|
||||
props: {
|
||||
image: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '150px'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '150px'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pan-item {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: default;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.pan-info-roles-container {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pan-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
transform-origin: 95% 40%;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.pan-thumb:after {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
top: 40%;
|
||||
left: 95%;
|
||||
margin: -4px 0 0 -4px;
|
||||
background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
|
||||
box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.pan-info {
|
||||
position: absolute;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.pan-info h3 {
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
letter-spacing: 2px;
|
||||
font-size: 18px;
|
||||
margin: 0 60px;
|
||||
padding: 22px 0 0 0;
|
||||
height: 85px;
|
||||
font-family: 'Open Sans', Arial, sans-serif;
|
||||
text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.pan-info p {
|
||||
color: #fff;
|
||||
padding: 10px 5px;
|
||||
font-style: italic;
|
||||
margin: 0 30px;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.pan-info p a {
|
||||
display: block;
|
||||
color: #333;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
font-size: 9px;
|
||||
letter-spacing: 1px;
|
||||
padding-top: 24px;
|
||||
margin: 7px auto 0;
|
||||
font-family: 'Open Sans', Arial, sans-serif;
|
||||
opacity: 0;
|
||||
transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
|
||||
transform: translateX(60px) rotate(90deg);
|
||||
}
|
||||
|
||||
.pan-info p a:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.pan-item:hover .pan-thumb {
|
||||
transform: rotate(-110deg);
|
||||
}
|
||||
|
||||
.pan-item:hover .pan-info p a {
|
||||
opacity: 1;
|
||||
transform: translateX(0px) rotate(0deg);
|
||||
}
|
||||
</style>
|
||||
51
frontend/src/components/Screenfull/index.vue
Normal file
51
frontend/src/components/Screenfull/index.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import screenfull from 'screenfull'
|
||||
|
||||
export default {
|
||||
name: 'Screenfull',
|
||||
data() {
|
||||
return {
|
||||
isFullscreen: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
if (!screenfull.enabled) {
|
||||
this.$message({
|
||||
message: 'you browser can not work',
|
||||
type: 'warning'
|
||||
})
|
||||
return false
|
||||
}
|
||||
screenfull.toggle()
|
||||
},
|
||||
init() {
|
||||
if (screenfull.enabled) {
|
||||
screenfull.on('change', () => {
|
||||
this.isFullscreen = screenfull.isFullscreen
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.screenfull-svg {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
fill: #5a5e66;;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: 10px;
|
||||
}
|
||||
</style>
|
||||
81
frontend/src/components/ScrollPane/index.vue
Normal file
81
frontend/src/components/ScrollPane/index.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
|
||||
<slot/>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const tagAndTagSpacing = 4 // tagAndTagSpacing
|
||||
|
||||
export default {
|
||||
name: 'ScrollPane',
|
||||
data () {
|
||||
return {
|
||||
left: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleScroll (e) {
|
||||
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
||||
const $scrollWrapper = this.$refs.scrollContainer.$refs.wrap
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
||||
},
|
||||
moveToTarget (currentTag) {
|
||||
const $container = this.$refs.scrollContainer.$el
|
||||
const $containerWidth = $container.offsetWidth
|
||||
const $scrollWrapper = this.$refs.scrollContainer.$refs.wrap
|
||||
const tagList = this.$parent.$refs.tag
|
||||
|
||||
let firstTag = null
|
||||
let lastTag = null
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagList.length > 0) {
|
||||
firstTag = tagList[0]
|
||||
lastTag = tagList[tagList.length - 1]
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
|
||||
} else {
|
||||
// find preTag and nextTag
|
||||
const currentIndex = tagList.findIndex(item => item === currentTag)
|
||||
const prevTag = tagList[currentIndex - 1]
|
||||
const nextTag = tagList[currentIndex + 1]
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
|
||||
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.scroll-container {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
/deep/ {
|
||||
.el-scrollbar__bar {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100
frontend/src/components/Share/dropdownMenu.vue
Normal file
100
frontend/src/components/Share/dropdownMenu.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div :class="{active:isActive}" class="share-dropdown-menu">
|
||||
<div class="share-dropdown-menu-wrapper">
|
||||
<span class="share-dropdown-menu-title" @click.self="clickTitle">{{ title }}</span>
|
||||
<div v-for="(item,index) of items" :key="index" class="share-dropdown-menu-item">
|
||||
<a v-if="item.href" :href="item.href" target="_blank">{{ item.title }}</a>
|
||||
<span v-else>{{ item.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: function() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'vue'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isActive: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickTitle() {
|
||||
this.isActive = !this.isActive
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" >
|
||||
$n: 8; //和items.length 相同
|
||||
$t: .1s;
|
||||
.share-dropdown-menu {
|
||||
width: 250px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
&-title {
|
||||
width: 100%;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
background: black;
|
||||
color: white;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
transform: translate3d(0,0,0);
|
||||
}
|
||||
&-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
&-item {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: #e0e0e0;
|
||||
line-height: 60px;
|
||||
height: 60px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
opacity: 1;
|
||||
transition: transform 0.28s ease;
|
||||
&:hover {
|
||||
background: black;
|
||||
color: white;
|
||||
}
|
||||
@for $i from 1 through $n {
|
||||
&:nth-of-type(#{$i}) {
|
||||
z-index: -1;
|
||||
transition-delay: $i*$t;
|
||||
transform: translate3d(0, -60px, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
.share-dropdown-menu-wrapper {
|
||||
z-index: 1;
|
||||
}
|
||||
.share-dropdown-menu-item {
|
||||
@for $i from 1 through $n {
|
||||
&:nth-of-type(#{$i}) {
|
||||
transition-delay: ($n - $i)*$t;
|
||||
transform: translate3d(0, ($i - 1)*60px, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
55
frontend/src/components/SizeSelect/index.vue
Normal file
55
frontend/src/components/SizeSelect/index.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleSetSize">
|
||||
<div>
|
||||
<svg-icon class-name="size-icon" icon-class="size" />
|
||||
</div>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">{{
|
||||
item.label }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
sizeOptions: [
|
||||
{ label: 'Default', value: 'default' },
|
||||
{ label: 'Medium', value: 'medium' },
|
||||
{ label: 'Small', value: 'small' },
|
||||
{ label: 'Mini', value: 'mini' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
size() {
|
||||
return this.$store.getters.size
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSetSize(size) {
|
||||
this.$ELEMENT.size = size
|
||||
this.$store.dispatch('setSize', size)
|
||||
this.refreshView()
|
||||
this.$message({
|
||||
message: 'Switch Size Success',
|
||||
type: 'success'
|
||||
})
|
||||
},
|
||||
refreshView() {
|
||||
// In order to make the cached page re-rendered
|
||||
this.$store.dispatch('delAllCachedViews', this.$route)
|
||||
|
||||
const { fullPath } = this.$route
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$router.replace({
|
||||
path: '/redirect' + fullPath
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
88
frontend/src/components/Sticky/index.vue
Normal file
88
frontend/src/components/Sticky/index.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div :style="{height:height+'px',zIndex:zIndex}">
|
||||
<div :class="className" :style="{top:stickyTop+'px',zIndex:zIndex,position:position,width:width,height:height+'px'}">
|
||||
<slot>
|
||||
<div>sticky</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Sticky',
|
||||
props: {
|
||||
stickyTop: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: false,
|
||||
position: '',
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
isSticky: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.height = this.$el.getBoundingClientRect().height
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
window.addEventListener('resize', this.handleReize)
|
||||
},
|
||||
activated() {
|
||||
this.handleScroll()
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
window.removeEventListener('resize', this.handleReize)
|
||||
},
|
||||
methods: {
|
||||
sticky() {
|
||||
if (this.active) {
|
||||
return
|
||||
}
|
||||
this.position = 'fixed'
|
||||
this.active = true
|
||||
this.width = this.width + 'px'
|
||||
this.isSticky = true
|
||||
},
|
||||
handleReset() {
|
||||
if (!this.active) {
|
||||
return
|
||||
}
|
||||
this.reset()
|
||||
},
|
||||
reset() {
|
||||
this.position = ''
|
||||
this.width = 'auto'
|
||||
this.active = false
|
||||
this.isSticky = false
|
||||
},
|
||||
handleScroll() {
|
||||
const width = this.$el.getBoundingClientRect().width
|
||||
this.width = width || 'auto'
|
||||
const offsetTop = this.$el.getBoundingClientRect().top
|
||||
if (offsetTop < this.stickyTop) {
|
||||
this.sticky()
|
||||
return
|
||||
}
|
||||
this.handleReset()
|
||||
},
|
||||
handleReize() {
|
||||
if (this.isSticky) {
|
||||
this.width = this.$el.getBoundingClientRect().width + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
43
frontend/src/components/SvgIcon/index.vue
Normal file
43
frontend/src/components/SvgIcon/index.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<svg :class="svgClass" aria-hidden="true" v-on="$listeners">
|
||||
<use :xlink:href="iconName"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SvgIcon',
|
||||
props: {
|
||||
iconClass: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconName () {
|
||||
return `#icon-${this.iconClass}`
|
||||
},
|
||||
svgClass () {
|
||||
if (this.className) {
|
||||
return 'svg-icon ' + this.className
|
||||
} else {
|
||||
return 'svg-icon'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
73
frontend/src/components/TableView/DeployTableView.vue
Normal file
73
frontend/src/components/TableView/DeployTableView.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="deploy-table-view">
|
||||
<el-row class="title-wrapper">
|
||||
<h5 class="title">{{title}}</h5>
|
||||
<el-button type="success" plain class="small-btn" size="mini" icon="fa fa-refresh" @click="onRefresh"></el-button>
|
||||
</el-row>
|
||||
<el-table border height="240px" :data="deployList">
|
||||
<el-table-column property="version" label="Ver" width="40" align="center"></el-table-column>
|
||||
<el-table-column property="node" label="Node" width="220" align="center">
|
||||
<template slot-scope="scope">
|
||||
<a class="a-tag" @click="onClickNode(scope.row)">{{scope.row.node_id}}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="spider_name" label="Spider" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<a class="a-tag" @click="onClickSpider(scope.row)">{{scope.row.spider_name}}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="finish_ts" label="Finish Time" width="auto" align="center"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'DeployTableView',
|
||||
props: {
|
||||
title: String
|
||||
},
|
||||
computed: {
|
||||
...mapState('spider', [
|
||||
'spiderForm'
|
||||
]),
|
||||
...mapState('deploy', [
|
||||
'deployList'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
onClickSpider (row) {
|
||||
this.$router.push(`/spiders/${row.spider_id.$oid}`)
|
||||
},
|
||||
onClickNode (row) {
|
||||
this.$router.push(`/nodes/${row.node_id}`)
|
||||
},
|
||||
onRefresh () {
|
||||
this.$store.dispatch('deploy/getDeployList', this.spiderForm._id.$oid)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-table .a-tag {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.title {
|
||||
float: left;
|
||||
margin: 10px 0 3px 0;
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
float: right;
|
||||
width: 24px;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
94
frontend/src/components/TableView/TaskTableView.vue
Normal file
94
frontend/src/components/TableView/TaskTableView.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="task-table-view">
|
||||
<el-row class="title-wrapper">
|
||||
<h5 class="title">{{title}}</h5>
|
||||
<el-button type="success" plain class="small-btn" size="mini" icon="fa fa-refresh" @click="onRefresh"></el-button>
|
||||
</el-row>
|
||||
<el-table border height="240px" :data="taskList">
|
||||
<el-table-column property="node" label="Node" width="220" align="center">
|
||||
<template slot-scope="scope">
|
||||
<a class="a-tag" @click="onClickNode(scope.row)">{{scope.row.node_id}}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="spider_name" label="Spider" width="80" align="center">
|
||||
<template slot-scope="scope">
|
||||
<a class="a-tag" @click="onClickSpider(scope.row)">{{scope.row.spider_name}}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Status"
|
||||
align="center"
|
||||
width="100">
|
||||
<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['status']}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!--<el-table-column property="create_ts" label="Create Time" width="auto" align="center"></el-table-column>-->
|
||||
<el-table-column property="create_ts" label="Create Time" width="auto" align="center">
|
||||
<template slot-scope="scope">
|
||||
<a href="javascript:" class="a-tag" @click="onClickTask(scope.row)">{{scope.row.create_ts}}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
mapState
|
||||
} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'TaskTableView',
|
||||
props: {
|
||||
title: String
|
||||
},
|
||||
computed: {
|
||||
...mapState('spider', [
|
||||
'spiderForm'
|
||||
]),
|
||||
...mapState('task', [
|
||||
'taskList'
|
||||
])
|
||||
},
|
||||
methods: {
|
||||
onClickSpider (row) {
|
||||
this.$router.push(`/spiders/${row.spider_id.$oid}`)
|
||||
},
|
||||
onClickNode (row) {
|
||||
this.$router.push(`/nodes/${row.node_id}`)
|
||||
},
|
||||
onClickTask (row) {
|
||||
this.$router.push(`/tasks/${row._id}`)
|
||||
},
|
||||
onRefresh () {
|
||||
this.$store.dispatch('spider/getTaskList', this.spiderForm._id.$oid)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-table-view {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.el-table .a-tag {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 10px 0 3px 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
float: right;
|
||||
width: 24px;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
113
frontend/src/components/TextHoverEffect/Mallki.vue
Normal file
113
frontend/src/components/TextHoverEffect/Mallki.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<a :class="className" class="link--mallki" href="#">
|
||||
{{ text }}
|
||||
<span :data-letters="text"/>
|
||||
<span :data-letters="text"/>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: 'vue-element-admin'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Mallki */
|
||||
|
||||
.link--mallki {
|
||||
font-weight: 800;
|
||||
color: #4dd9d5;
|
||||
font-family: 'Dosis', sans-serif;
|
||||
-webkit-transition: color 0.5s 0.25s;
|
||||
transition: color 0.5s 0.25s;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link--mallki:hover {
|
||||
-webkit-transition: none;
|
||||
transition: none;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.link--mallki::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
margin: -3px 0 0 0;
|
||||
background: #3888fa;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
-webkit-transform: translate3d(-100%, 0, 0);
|
||||
transform: translate3d(-100%, 0, 0);
|
||||
-webkit-transition: -webkit-transform 0.4s;
|
||||
transition: transform 0.4s;
|
||||
-webkit-transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
|
||||
transition-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
|
||||
}
|
||||
|
||||
.link--mallki:hover::before {
|
||||
-webkit-transform: translate3d(100%, 0, 0);
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
|
||||
.link--mallki span {
|
||||
position: absolute;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link--mallki span::before {
|
||||
content: attr(data-letters);
|
||||
color: red;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
color: #3888fa;
|
||||
-webkit-transition: -webkit-transform 0.5s;
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
.link--mallki span:nth-child(2) {
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.link--mallki span:first-child::before {
|
||||
top: 0;
|
||||
-webkit-transform: translate3d(0, 100%, 0);
|
||||
transform: translate3d(0, 100%, 0);
|
||||
}
|
||||
|
||||
.link--mallki span:nth-child(2)::before {
|
||||
bottom: 0;
|
||||
-webkit-transform: translate3d(0, -100%, 0);
|
||||
transform: translate3d(0, -100%, 0);
|
||||
}
|
||||
|
||||
.link--mallki:hover span::before {
|
||||
-webkit-transition-delay: 0.3s;
|
||||
transition-delay: 0.3s;
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
|
||||
transition-timing-function: cubic-bezier(0.2, 1, 0.3, 1);
|
||||
}
|
||||
</style>
|
||||
148
frontend/src/components/ThemePicker/index.vue
Normal file
148
frontend/src/components/ThemePicker/index.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<el-color-picker
|
||||
v-model="theme"
|
||||
class="theme-picker"
|
||||
popper-class="theme-picker-dropdown"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
const version = require('element-ui/package.json').version // element-ui version from node_modules
|
||||
const ORIGINAL_THEME = '#409EFF' // default color
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
chalk: '', // content of theme-chalk css
|
||||
theme: ORIGINAL_THEME
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
theme(val) {
|
||||
const oldVal = this.theme
|
||||
if (typeof val !== 'string') return
|
||||
const themeCluster = this.getThemeCluster(val.replace('#', ''))
|
||||
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
|
||||
console.log(themeCluster, originalCluster)
|
||||
const getHandler = (variable, id) => {
|
||||
return () => {
|
||||
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
|
||||
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
|
||||
|
||||
let styleTag = document.getElementById(id)
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style')
|
||||
styleTag.setAttribute('id', id)
|
||||
document.head.appendChild(styleTag)
|
||||
}
|
||||
styleTag.innerText = newStyle
|
||||
}
|
||||
}
|
||||
|
||||
const chalkHandler = getHandler('chalk', 'chalk-style')
|
||||
|
||||
if (!this.chalk) {
|
||||
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
|
||||
this.getCSSString(url, chalkHandler, 'chalk')
|
||||
} else {
|
||||
chalkHandler()
|
||||
}
|
||||
|
||||
const styles = [].slice.call(document.querySelectorAll('style'))
|
||||
.filter(style => {
|
||||
const text = style.innerText
|
||||
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
|
||||
})
|
||||
styles.forEach(style => {
|
||||
const { innerText } = style
|
||||
if (typeof innerText !== 'string') return
|
||||
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
|
||||
})
|
||||
this.$message({
|
||||
message: '换肤成功',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateStyle(style, oldCluster, newCluster) {
|
||||
let newStyle = style
|
||||
oldCluster.forEach((color, index) => {
|
||||
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
|
||||
})
|
||||
return newStyle
|
||||
},
|
||||
|
||||
getCSSString(url, callback, variable) {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
|
||||
callback()
|
||||
}
|
||||
}
|
||||
xhr.open('GET', url)
|
||||
xhr.send()
|
||||
},
|
||||
|
||||
getThemeCluster(theme) {
|
||||
const tintColor = (color, tint) => {
|
||||
let red = parseInt(color.slice(0, 2), 16)
|
||||
let green = parseInt(color.slice(2, 4), 16)
|
||||
let blue = parseInt(color.slice(4, 6), 16)
|
||||
|
||||
if (tint === 0) { // when primary color is in its rgb space
|
||||
return [red, green, blue].join(',')
|
||||
} else {
|
||||
red += Math.round(tint * (255 - red))
|
||||
green += Math.round(tint * (255 - green))
|
||||
blue += Math.round(tint * (255 - blue))
|
||||
|
||||
red = red.toString(16)
|
||||
green = green.toString(16)
|
||||
blue = blue.toString(16)
|
||||
|
||||
return `#${red}${green}${blue}`
|
||||
}
|
||||
}
|
||||
|
||||
const shadeColor = (color, shade) => {
|
||||
let red = parseInt(color.slice(0, 2), 16)
|
||||
let green = parseInt(color.slice(2, 4), 16)
|
||||
let blue = parseInt(color.slice(4, 6), 16)
|
||||
|
||||
red = Math.round((1 - shade) * red)
|
||||
green = Math.round((1 - shade) * green)
|
||||
blue = Math.round((1 - shade) * blue)
|
||||
|
||||
red = red.toString(16)
|
||||
green = green.toString(16)
|
||||
blue = blue.toString(16)
|
||||
|
||||
return `#${red}${green}${blue}`
|
||||
}
|
||||
|
||||
const clusters = [theme]
|
||||
for (let i = 0; i <= 9; i++) {
|
||||
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
|
||||
}
|
||||
clusters.push(shadeColor(theme, 0.1))
|
||||
return clusters
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.theme-picker .el-color-picker__trigger {
|
||||
margin-top: 12px;
|
||||
height: 26px!important;
|
||||
width: 26px!important;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.theme-picker-dropdown .el-color-dropdown__link-btn {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
103
frontend/src/components/Tinymce/components/editorImage.vue
Normal file
103
frontend/src/components/Tinymce/components/editorImage.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">上传图片
|
||||
</el-button>
|
||||
<el-dialog :visible.sync="dialogVisible">
|
||||
<el-upload
|
||||
:multiple="true"
|
||||
:file-list="fileList"
|
||||
:show-file-list="true"
|
||||
:on-remove="handleRemove"
|
||||
:on-success="handleSuccess"
|
||||
:before-upload="beforeUpload"
|
||||
class="editor-slide-upload"
|
||||
action="https://httpbin.org/post"
|
||||
list-type="picture-card">
|
||||
<el-button size="small" type="primary">点击上传</el-button>
|
||||
</el-upload>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import { getToken } from 'api/qiniu'
|
||||
|
||||
export default {
|
||||
name: 'EditorSlideUpload',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '#1890ff'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
listObj: {},
|
||||
fileList: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkAllSuccess() {
|
||||
return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
|
||||
},
|
||||
handleSubmit() {
|
||||
const arr = Object.keys(this.listObj).map(v => this.listObj[v])
|
||||
if (!this.checkAllSuccess()) {
|
||||
this.$message('请等待所有图片上传成功 或 出现了网络问题,请刷新页面重新上传!')
|
||||
return
|
||||
}
|
||||
this.$emit('successCBK', arr)
|
||||
this.listObj = {}
|
||||
this.fileList = []
|
||||
this.dialogVisible = false
|
||||
},
|
||||
handleSuccess(response, file) {
|
||||
const uid = file.uid
|
||||
const objKeyArr = Object.keys(this.listObj)
|
||||
for (let i = 0, len = objKeyArr.length; i < len; i++) {
|
||||
if (this.listObj[objKeyArr[i]].uid === uid) {
|
||||
this.listObj[objKeyArr[i]].url = response.files.file
|
||||
this.listObj[objKeyArr[i]].hasSuccess = true
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
handleRemove(file) {
|
||||
const uid = file.uid
|
||||
const objKeyArr = Object.keys(this.listObj)
|
||||
for (let i = 0, len = objKeyArr.length; i < len; i++) {
|
||||
if (this.listObj[objKeyArr[i]].uid === uid) {
|
||||
delete this.listObj[objKeyArr[i]]
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const _self = this
|
||||
const _URL = window.URL || window.webkitURL
|
||||
const fileName = file.uid
|
||||
this.listObj[fileName] = {}
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.src = _URL.createObjectURL(file)
|
||||
img.onload = function() {
|
||||
_self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
|
||||
}
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.editor-slide-upload {
|
||||
margin-bottom: 20px;
|
||||
/deep/ .el-upload--picture-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
210
frontend/src/components/Tinymce/index.vue
Normal file
210
frontend/src/components/Tinymce/index.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">
|
||||
<textarea :id="tinymceId" class="tinymce-textarea"/>
|
||||
<div class="editor-custom-btn-container">
|
||||
<editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import editorImage from './components/editorImage'
|
||||
import plugins from './plugins'
|
||||
import toolbar from './toolbar'
|
||||
|
||||
export default {
|
||||
name: 'Tinymce',
|
||||
components: { editorImage },
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
|
||||
}
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
toolbar: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
menubar: {
|
||||
type: String,
|
||||
default: 'file edit insert view format table'
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 360
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasChange: false,
|
||||
hasInit: false,
|
||||
tinymceId: this.id,
|
||||
fullscreen: false,
|
||||
languageTypeList: {
|
||||
'en': 'en',
|
||||
'zh': 'zh_CN'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.languageTypeList[this.$store.getters.language]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
if (!this.hasChange && this.hasInit) {
|
||||
this.$nextTick(() =>
|
||||
window.tinymce.get(this.tinymceId).setContent(val || ''))
|
||||
}
|
||||
},
|
||||
language() {
|
||||
this.destroyTinymce()
|
||||
this.$nextTick(() => this.initTinymce())
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initTinymce()
|
||||
},
|
||||
activated() {
|
||||
this.initTinymce()
|
||||
},
|
||||
deactivated() {
|
||||
this.destroyTinymce()
|
||||
},
|
||||
destroyed() {
|
||||
this.destroyTinymce()
|
||||
},
|
||||
methods: {
|
||||
initTinymce() {
|
||||
const _this = this
|
||||
window.tinymce.init({
|
||||
language: this.language,
|
||||
selector: `#${this.tinymceId}`,
|
||||
height: this.height,
|
||||
body_class: 'panel-body ',
|
||||
object_resizing: false,
|
||||
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
|
||||
menubar: this.menubar,
|
||||
plugins: plugins,
|
||||
end_container_on_empty_block: true,
|
||||
powerpaste_word_import: 'clean',
|
||||
code_dialog_height: 450,
|
||||
code_dialog_width: 1000,
|
||||
advlist_bullet_styles: 'square',
|
||||
advlist_number_styles: 'default',
|
||||
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
|
||||
default_link_target: '_blank',
|
||||
link_title: false,
|
||||
nonbreaking_force_tab: true, // inserting nonbreaking space need Nonbreaking Space Plugin
|
||||
init_instance_callback: editor => {
|
||||
if (_this.value) {
|
||||
editor.setContent(_this.value)
|
||||
}
|
||||
_this.hasInit = true
|
||||
editor.on('NodeChange Change KeyUp SetContent', () => {
|
||||
this.hasChange = true
|
||||
this.$emit('input', editor.getContent())
|
||||
})
|
||||
},
|
||||
setup(editor) {
|
||||
editor.on('FullscreenStateChanged', (e) => {
|
||||
_this.fullscreen = e.state
|
||||
})
|
||||
}
|
||||
// 整合七牛上传
|
||||
// images_dataimg_filter(img) {
|
||||
// setTimeout(() => {
|
||||
// const $image = $(img);
|
||||
// $image.removeAttr('width');
|
||||
// $image.removeAttr('height');
|
||||
// if ($image[0].height && $image[0].width) {
|
||||
// $image.attr('data-wscntype', 'image');
|
||||
// $image.attr('data-wscnh', $image[0].height);
|
||||
// $image.attr('data-wscnw', $image[0].width);
|
||||
// $image.addClass('wscnph');
|
||||
// }
|
||||
// }, 0);
|
||||
// return img
|
||||
// },
|
||||
// images_upload_handler(blobInfo, success, failure, progress) {
|
||||
// progress(0);
|
||||
// const token = _this.$store.getters.token;
|
||||
// getToken(token).then(response => {
|
||||
// const url = response.data.qiniu_url;
|
||||
// const formData = new FormData();
|
||||
// formData.append('token', response.data.qiniu_token);
|
||||
// formData.append('key', response.data.qiniu_key);
|
||||
// formData.append('file', blobInfo.blob(), url);
|
||||
// upload(formData).then(() => {
|
||||
// success(url);
|
||||
// progress(100);
|
||||
// })
|
||||
// }).catch(err => {
|
||||
// failure('出现未知问题,刷新页面,或者联系程序员')
|
||||
// console.log(err);
|
||||
// });
|
||||
// },
|
||||
})
|
||||
},
|
||||
destroyTinymce() {
|
||||
const tinymce = window.tinymce.get(this.tinymceId)
|
||||
if (this.fullscreen) {
|
||||
tinymce.execCommand('mceFullScreen')
|
||||
}
|
||||
|
||||
if (tinymce) {
|
||||
tinymce.destroy()
|
||||
}
|
||||
},
|
||||
setContent(value) {
|
||||
window.tinymce.get(this.tinymceId).setContent(value)
|
||||
},
|
||||
getContent() {
|
||||
window.tinymce.get(this.tinymceId).getContent()
|
||||
},
|
||||
imageSuccessCBK(arr) {
|
||||
const _this = this
|
||||
arr.forEach(v => {
|
||||
window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tinymce-container {
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
}
|
||||
.tinymce-container>>>.mce-fullscreen {
|
||||
z-index: 10000;
|
||||
}
|
||||
.tinymce-textarea {
|
||||
visibility: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
.editor-custom-btn-container {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
/*z-index: 2005;*/
|
||||
}
|
||||
.fullscreen .editor-custom-btn-container {
|
||||
z-index: 10000;
|
||||
position: fixed;
|
||||
}
|
||||
.editor-upload-btn {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
7
frontend/src/components/Tinymce/plugins.js
Normal file
7
frontend/src/components/Tinymce/plugins.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// Any plugins you want to use has to be imported
|
||||
// Detail plugins list see https://www.tinymce.com/docs/plugins/
|
||||
// Custom builds see https://www.tinymce.com/download/custom-builds/
|
||||
|
||||
const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
|
||||
|
||||
export default plugins
|
||||
6
frontend/src/components/Tinymce/toolbar.js
Normal file
6
frontend/src/components/Tinymce/toolbar.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Here is a list of the toolbar
|
||||
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
|
||||
|
||||
const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
|
||||
|
||||
export default toolbar
|
||||
29
frontend/src/components/TreeTable/eval.js
Normal file
29
frontend/src/components/TreeTable/eval.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @Author: jianglei
|
||||
* @Date: 2017-10-12 12:06:49
|
||||
*/
|
||||
'use strict'
|
||||
import Vue from 'vue'
|
||||
export default function treeToArray(data, expandAll, parent = null, level = null) {
|
||||
let tmp = []
|
||||
Array.from(data).forEach(function(record) {
|
||||
if (record._expanded === undefined) {
|
||||
Vue.set(record, '_expanded', expandAll)
|
||||
}
|
||||
let _level = 1
|
||||
if (level !== undefined && level !== null) {
|
||||
_level = level + 1
|
||||
}
|
||||
Vue.set(record, '_level', _level)
|
||||
// 如果有父元素
|
||||
if (parent) {
|
||||
Vue.set(record, 'parent', parent)
|
||||
}
|
||||
tmp.push(record)
|
||||
if (record.children && record.children.length > 0) {
|
||||
const children = treeToArray(record.children, expandAll, record, _level)
|
||||
tmp = tmp.concat(children)
|
||||
}
|
||||
})
|
||||
return tmp
|
||||
}
|
||||
127
frontend/src/components/TreeTable/index.vue
Normal file
127
frontend/src/components/TreeTable/index.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<el-table :data="formatData" :row-style="showRow" v-bind="$attrs">
|
||||
<el-table-column v-if="columns.length===0" width="150">
|
||||
<template slot-scope="scope">
|
||||
<span v-for="space in scope.row._level" :key="space" class="ms-tree-space"/>
|
||||
<span v-if="iconShow(0,scope.row)" class="tree-ctrl" @click="toggleExpanded(scope.$index)">
|
||||
<i v-if="!scope.row._expanded" class="el-icon-plus"/>
|
||||
<i v-else class="el-icon-minus"/>
|
||||
</span>
|
||||
{{ scope.$index }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="(column, index) in columns" v-else :key="column.value" :label="column.text" :width="column.width">
|
||||
<template slot-scope="scope">
|
||||
<!-- Todo -->
|
||||
<!-- eslint-disable-next-line vue/no-confusing-v-for-v-if -->
|
||||
<span v-for="space in scope.row._level" v-if="index === 0" :key="space" class="ms-tree-space"/>
|
||||
<span v-if="iconShow(index,scope.row)" class="tree-ctrl" @click="toggleExpanded(scope.$index)">
|
||||
<i v-if="!scope.row._expanded" class="el-icon-plus"/>
|
||||
<i v-else class="el-icon-minus"/>
|
||||
</span>
|
||||
{{ scope.row[column.value] }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<slot/>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
Auth: Lei.j1ang
|
||||
Created: 2018/1/19-13:59
|
||||
*/
|
||||
import treeToArray from './eval'
|
||||
export default {
|
||||
name: 'TreeTable',
|
||||
props: {
|
||||
/* eslint-disable */
|
||||
data: {
|
||||
type: [Array, Object],
|
||||
required: true
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
evalFunc: Function,
|
||||
evalArgs: Array,
|
||||
expandAll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 格式化数据源
|
||||
formatData: function() {
|
||||
let tmp
|
||||
if (!Array.isArray(this.data)) {
|
||||
tmp = [this.data]
|
||||
} else {
|
||||
tmp = this.data
|
||||
}
|
||||
const func = this.evalFunc || treeToArray
|
||||
const args = this.evalArgs ? Array.concat([tmp, this.expandAll], this.evalArgs) : [tmp, this.expandAll]
|
||||
return func.apply(null, args)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showRow: function(row) {
|
||||
const show = (row.row.parent ? (row.row.parent._expanded && row.row.parent._show) : true)
|
||||
row.row._show = show
|
||||
return show ? 'animation:treeTableShow 1s;-webkit-animation:treeTableShow 1s;' : 'display:none;'
|
||||
},
|
||||
// 切换下级是否展开
|
||||
toggleExpanded: function(trIndex) {
|
||||
const record = this.formatData[trIndex]
|
||||
record._expanded = !record._expanded
|
||||
},
|
||||
// 图标显示
|
||||
iconShow(index, record) {
|
||||
return (index === 0 && record.children && record.children.length > 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style rel="stylesheet/css">
|
||||
@keyframes treeTableShow {
|
||||
from {opacity: 0;}
|
||||
to {opacity: 1;}
|
||||
}
|
||||
@-webkit-keyframes treeTableShow {
|
||||
from {opacity: 0;}
|
||||
to {opacity: 1;}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" rel="stylesheet/scss" scoped>
|
||||
$color-blue: #2196F3;
|
||||
$space-width: 18px;
|
||||
.ms-tree-space {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
width: $space-width;
|
||||
height: 14px;
|
||||
&::before {
|
||||
content: ""
|
||||
}
|
||||
}
|
||||
.processContainer{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
table td {
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.tree-ctrl{
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
color: $color-blue;
|
||||
margin-left: -$space-width;
|
||||
}
|
||||
</style>
|
||||
89
frontend/src/components/TreeTable/readme.md
Normal file
89
frontend/src/components/TreeTable/readme.md
Normal file
@@ -0,0 +1,89 @@
|
||||
## 写在前面
|
||||
此组件仅提供一个创建TreeTable的解决思路
|
||||
|
||||
## prop说明
|
||||
#### *data*
|
||||
**必填**
|
||||
|
||||
原始数据,要求是一个数组或者对象
|
||||
```javascript
|
||||
[{
|
||||
key1: value1,
|
||||
key2: value2,
|
||||
children: [{
|
||||
key1: value1
|
||||
},
|
||||
{
|
||||
key1: value1
|
||||
}]
|
||||
},
|
||||
{
|
||||
key1: value1
|
||||
}]
|
||||
```
|
||||
或者
|
||||
```javascript
|
||||
{
|
||||
key1: value1,
|
||||
key2: value2,
|
||||
children: [{
|
||||
key1: value1
|
||||
},
|
||||
{
|
||||
key1: value1
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### columns
|
||||
列属性,要求是一个数组
|
||||
|
||||
1. text: 显示在表头的文字
|
||||
2. value: 对应data的key。treeTable将显示相应的value
|
||||
3. width: 每列的宽度,为一个数字(可选)
|
||||
|
||||
如果你想要每个字段都有自定义的样式或者嵌套其他组件,columns可不提供,直接像在el-table一样写即可,如果没有自定义内容,提供columns将更加的便捷方便
|
||||
|
||||
如果你有几个字段是需要自定义的,几个不需要,那么可以将不需要自定义的字段放入columns,将需要自定义的内容放入到slot中,详情见后文
|
||||
```javascript
|
||||
[{
|
||||
value:string,
|
||||
text:string,
|
||||
width:number
|
||||
},{
|
||||
value:string,
|
||||
text:string,
|
||||
width:number
|
||||
}]
|
||||
```
|
||||
|
||||
#### expandAll
|
||||
是否默认全部展开,boolean值,默认为false
|
||||
|
||||
#### evalFunc
|
||||
解析函数,function,非必须
|
||||
|
||||
如果不提供,将使用默认的[evalFunc](./eval.js)
|
||||
|
||||
如果提供了evalFunc,那么会用提供的evalFunc去解析data,并返回treeTable渲染所需要的值。如何编写一个evalFunc,请参考[*eval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/components/TreeTable/eval.js)或[*customEval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customEval.js)
|
||||
|
||||
#### evalArgs
|
||||
解析函数的参数,是一个数组
|
||||
|
||||
**请注意,自定义的解析函数参数第一个为this.data,第二个参数为, this.expandAll,你不需要在evalArgs填写。一定记住,这两个参数是强制性的,并且位置不可颠倒** *this.data为需要解析的数据,this.expandAll为是否默认展开*
|
||||
|
||||
如你的解析函数需要的参数为`(this.data, this.expandAll,1,2,3,4)`,那么你只需要将`[1,2,3,4]`赋值给`evalArgs`就可以了
|
||||
|
||||
如果你的解析函数参数只有`(this.data, this.expandAll)`,那么就可以不用填写evalArgs了
|
||||
|
||||
具体可参考[*customEval.js*](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customEval.js)的函数参数和[customTreeTable](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customTreeTable.vue)的`evalArgs`属性值
|
||||
|
||||
## slot
|
||||
这是一个自定义列的插槽。
|
||||
|
||||
默认情况下,treeTable只有一行行展示数据的功能。但是一般情况下,我们会要给行加上一个操作按钮或者根据当行数据展示不同的样式,这时我们就需要自定义列了。请参考[customTreeTable](https://github.com/PanJiaChen/vue-element-admin/blob/master/src/views/table/treeTable/customTreeTable.vue),[实例效果](https://panjiachen.github.io/vue-element-admin/#/table/tree-table)
|
||||
|
||||
`slot`和`columns属性`可同时存在,columns里面的数据列会在slot自定义列的左边展示
|
||||
|
||||
## 其他
|
||||
如果有其他的需求,请参考[el-table](http://element-cn.eleme.io/#/en-US/component/table)的api自行修改index.vue
|
||||
132
frontend/src/components/Upload/singleImage.vue
Normal file
132
frontend/src/components/Upload/singleImage.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
:data="dataObj"
|
||||
:multiple="false"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
class="image-uploader"
|
||||
drag
|
||||
action="https://httpbin.org/post">
|
||||
<i class="el-icon-upload"/>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<div class="image-preview">
|
||||
<div v-show="imageUrl.length>1" class="image-preview-wrapper">
|
||||
<img :src="imageUrl+'?imageView2/1/w/200/h/200'">
|
||||
<div class="image-preview-action">
|
||||
<i class="el-icon-delete" @click="rmImage"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 预览效果见付费文章
|
||||
import { getToken } from '@/api/qiniu'
|
||||
|
||||
export default {
|
||||
name: 'SingleImageUpload',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tempUrl: '',
|
||||
dataObj: { token: '', key: '' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imageUrl() {
|
||||
return this.value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rmImage() {
|
||||
this.emitInput('')
|
||||
},
|
||||
emitInput(val) {
|
||||
this.$emit('input', val)
|
||||
},
|
||||
handleImageSuccess() {
|
||||
this.emitInput(this.tempUrl)
|
||||
},
|
||||
beforeUpload() {
|
||||
const _self = this
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key
|
||||
const token = response.data.qiniu_token
|
||||
_self._data.dataObj.token = token
|
||||
_self._data.dataObj.key = key
|
||||
this.tempUrl = response.data.qiniu_url
|
||||
resolve(true)
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
reject(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
@import "src/styles/mixin.scss";
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@include clearfix;
|
||||
.image-uploader {
|
||||
width: 60%;
|
||||
float: left;
|
||||
}
|
||||
.image-preview {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
border: 1px dashed #d9d9d9;
|
||||
float: left;
|
||||
margin-left: 50px;
|
||||
.image-preview-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.image-preview-action {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
font-size: 20px;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
transition: opacity .3s;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
line-height: 200px;
|
||||
.el-icon-delete {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.image-preview-action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
127
frontend/src/components/Upload/singleImage2.vue
Normal file
127
frontend/src/components/Upload/singleImage2.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="singleImageUpload2 upload-container">
|
||||
<el-upload
|
||||
:data="dataObj"
|
||||
:multiple="false"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
class="image-uploader"
|
||||
drag
|
||||
action="https://httpbin.org/post">
|
||||
<i class="el-icon-upload"/>
|
||||
<div class="el-upload__text">Drag或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<div v-show="imageUrl.length>0" class="image-preview">
|
||||
<div v-show="imageUrl.length>1" class="image-preview-wrapper">
|
||||
<img :src="imageUrl">
|
||||
<div class="image-preview-action">
|
||||
<i class="el-icon-delete" @click="rmImage"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getToken } from '@/api/qiniu'
|
||||
|
||||
export default {
|
||||
name: 'SingleImageUpload2',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tempUrl: '',
|
||||
dataObj: { token: '', key: '' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imageUrl() {
|
||||
return this.value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rmImage() {
|
||||
this.emitInput('')
|
||||
},
|
||||
emitInput(val) {
|
||||
this.$emit('input', val)
|
||||
},
|
||||
handleImageSuccess() {
|
||||
this.emitInput(this.tempUrl)
|
||||
},
|
||||
beforeUpload() {
|
||||
const _self = this
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key
|
||||
const token = response.data.qiniu_token
|
||||
_self._data.dataObj.token = token
|
||||
_self._data.dataObj.key = key
|
||||
this.tempUrl = response.data.qiniu_url
|
||||
resolve(true)
|
||||
}).catch(() => {
|
||||
reject(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.image-uploader {
|
||||
height: 100%;
|
||||
}
|
||||
.image-preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
.image-preview-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.image-preview-action {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
font-size: 20px;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
transition: opacity .3s;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
line-height: 200px;
|
||||
.el-icon-delete {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.image-preview-action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
154
frontend/src/components/Upload/singleImage3.vue
Normal file
154
frontend/src/components/Upload/singleImage3.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
:data="dataObj"
|
||||
:multiple="false"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
class="image-uploader"
|
||||
drag
|
||||
action="https://httpbin.org/post">
|
||||
<i class="el-icon-upload"/>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
</el-upload>
|
||||
<div class="image-preview image-app-preview">
|
||||
<div v-show="imageUrl.length>1" class="image-preview-wrapper">
|
||||
<img :src="imageUrl">
|
||||
<div class="image-preview-action">
|
||||
<i class="el-icon-delete" @click="rmImage"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-preview">
|
||||
<div v-show="imageUrl.length>1" class="image-preview-wrapper">
|
||||
<img :src="imageUrl">
|
||||
<div class="image-preview-action">
|
||||
<i class="el-icon-delete" @click="rmImage"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getToken } from '@/api/qiniu'
|
||||
|
||||
export default {
|
||||
name: 'SingleImageUpload3',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tempUrl: '',
|
||||
dataObj: { token: '', key: '' }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imageUrl() {
|
||||
return this.value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rmImage() {
|
||||
this.emitInput('')
|
||||
},
|
||||
emitInput(val) {
|
||||
this.$emit('input', val)
|
||||
},
|
||||
handleImageSuccess(file) {
|
||||
this.emitInput(file.files.file)
|
||||
},
|
||||
beforeUpload() {
|
||||
const _self = this
|
||||
return new Promise((resolve, reject) => {
|
||||
getToken().then(response => {
|
||||
const key = response.data.qiniu_key
|
||||
const token = response.data.qiniu_token
|
||||
_self._data.dataObj.token = token
|
||||
_self._data.dataObj.key = key
|
||||
this.tempUrl = response.data.qiniu_url
|
||||
resolve(true)
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
reject(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
@import "~@/styles/mixin.scss";
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@include clearfix;
|
||||
.image-uploader {
|
||||
width: 35%;
|
||||
float: left;
|
||||
}
|
||||
.image-preview {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
border: 1px dashed #d9d9d9;
|
||||
float: left;
|
||||
margin-left: 50px;
|
||||
.image-preview-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.image-preview-action {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
opacity: 0;
|
||||
font-size: 20px;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
transition: opacity .3s;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
line-height: 200px;
|
||||
.el-icon-delete {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.image-preview-action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.image-app-preview {
|
||||
width: 320px;
|
||||
height: 180px;
|
||||
position: relative;
|
||||
border: 1px dashed #d9d9d9;
|
||||
float: left;
|
||||
margin-left: 50px;
|
||||
.app-fake-conver {
|
||||
height: 44px;
|
||||
position: absolute;
|
||||
width: 100%; // background: rgba(0, 0, 0, .1);
|
||||
text-align: center;
|
||||
line-height: 64px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
136
frontend/src/components/UploadExcel/index.vue
Normal file
136
frontend/src/components/UploadExcel/index.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div>
|
||||
<input ref="excel-upload-input" class="excel-upload-input" type="file" accept=".xlsx, .xls" @change="handleClick">
|
||||
<div class="drop" @drop="handleDrop" @dragover="handleDragover" @dragenter="handleDragover">
|
||||
Drop excel file here or
|
||||
<el-button :loading="loading" style="margin-left:16px;" size="mini" type="primary" @click="handleUpload">Browse</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import XLSX from 'xlsx'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
beforeUpload: Function, // eslint-disable-line
|
||||
onSuccess: Function// eslint-disable-line
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
excelData: {
|
||||
header: null,
|
||||
results: null
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
generateData({ header, results }) {
|
||||
this.excelData.header = header
|
||||
this.excelData.results = results
|
||||
this.onSuccess && this.onSuccess(this.excelData)
|
||||
},
|
||||
handleDrop(e) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
if (this.loading) return
|
||||
const files = e.dataTransfer.files
|
||||
if (files.length !== 1) {
|
||||
this.$message.error('Only support uploading one file!')
|
||||
return
|
||||
}
|
||||
const rawFile = files[0] // only use files[0]
|
||||
|
||||
if (!this.isExcel(rawFile)) {
|
||||
this.$message.error('Only supports upload .xlsx, .xls, .csv suffix files')
|
||||
return false
|
||||
}
|
||||
this.upload(rawFile)
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
},
|
||||
handleDragover(e) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
},
|
||||
handleUpload() {
|
||||
this.$refs['excel-upload-input'].click()
|
||||
},
|
||||
handleClick(e) {
|
||||
const files = e.target.files
|
||||
const rawFile = files[0] // only use files[0]
|
||||
if (!rawFile) return
|
||||
this.upload(rawFile)
|
||||
},
|
||||
upload(rawFile) {
|
||||
this.$refs['excel-upload-input'].value = null // fix can't select the same excel
|
||||
|
||||
if (!this.beforeUpload) {
|
||||
this.readerData(rawFile)
|
||||
return
|
||||
}
|
||||
const before = this.beforeUpload(rawFile)
|
||||
if (before) {
|
||||
this.readerData(rawFile)
|
||||
}
|
||||
},
|
||||
readerData(rawFile) {
|
||||
this.loading = true
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = e => {
|
||||
const data = e.target.result
|
||||
const workbook = XLSX.read(data, { type: 'array' })
|
||||
const firstSheetName = workbook.SheetNames[0]
|
||||
const worksheet = workbook.Sheets[firstSheetName]
|
||||
const header = this.getHeaderRow(worksheet)
|
||||
const results = XLSX.utils.sheet_to_json(worksheet)
|
||||
this.generateData({ header, results })
|
||||
this.loading = false
|
||||
resolve()
|
||||
}
|
||||
reader.readAsArrayBuffer(rawFile)
|
||||
})
|
||||
},
|
||||
getHeaderRow(sheet) {
|
||||
const headers = []
|
||||
const range = XLSX.utils.decode_range(sheet['!ref'])
|
||||
let C
|
||||
const R = range.s.r
|
||||
/* start in the first row */
|
||||
for (C = range.s.c; C <= range.e.c; ++C) { /* walk every column in the range */
|
||||
const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]
|
||||
/* find the cell in the first row */
|
||||
let hdr = 'UNKNOWN ' + C // <-- replace with your desired default
|
||||
if (cell && cell.t) hdr = XLSX.utils.format_cell(cell)
|
||||
headers.push(hdr)
|
||||
}
|
||||
return headers
|
||||
},
|
||||
isExcel(file) {
|
||||
return /\.(xlsx|xls|csv)$/.test(file.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.excel-upload-input{
|
||||
display: none;
|
||||
z-index: -9999;
|
||||
}
|
||||
.drop{
|
||||
border: 2px dashed #bbb;
|
||||
width: 600px;
|
||||
height: 160px;
|
||||
line-height: 160px;
|
||||
margin: 0 auto;
|
||||
font-size: 24px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user