mirror of
https://github.com/crawlab-team/crawlab.git
synced 2026-01-22 17:31:03 +01:00
added login page
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"crawlab/constants"
|
||||
"crawlab/database"
|
||||
"crawlab/model"
|
||||
"crawlab/services"
|
||||
"crawlab/utils"
|
||||
"github.com/apex/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"github.com/pkg/errors"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
@@ -14,7 +18,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetSpiderList(c *gin.Context) {
|
||||
@@ -342,3 +348,123 @@ func PostSpiderFile(c *gin.Context) {
|
||||
Message: "success",
|
||||
})
|
||||
}
|
||||
|
||||
func GetSpiderStats(c *gin.Context) {
|
||||
type Overview struct {
|
||||
TaskCount int `json:"task_count" bson:"task_count"`
|
||||
ResultCount int `json:"result_count" bson:"result_count"`
|
||||
SuccessCount int `json:"success_count" bson:"success_count"`
|
||||
SuccessRate float64 `json:"success_rate"`
|
||||
TotalWaitDuration float64 `json:"wait_duration" bson:"wait_duration"`
|
||||
TotalRuntimeDuration float64 `json:"runtime_duration" bson:"runtime_duration"`
|
||||
AvgWaitDuration float64 `json:"avg_wait_duration"`
|
||||
AvgRuntimeDuration float64 `json:"avg_runtime_duration"`
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
Overview Overview `json:"overview"`
|
||||
Daily []model.TaskDailyItem `json:"daily"`
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
spider, err := model.GetSpider(bson.ObjectIdHex(id))
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
HandleError(http.StatusInternalServerError, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
s, col := database.GetCol("tasks")
|
||||
defer s.Close()
|
||||
|
||||
// 起始日期
|
||||
startDate := time.Now().Add(-time.Hour * 24 * 30)
|
||||
endDate := time.Now()
|
||||
|
||||
// match
|
||||
op1 := bson.M{
|
||||
"$match": bson.M{
|
||||
"spider_id": spider.Id,
|
||||
"create_ts": bson.M{
|
||||
"$gte": startDate,
|
||||
"$lt": endDate,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// project
|
||||
op2 := bson.M{
|
||||
"$project": bson.M{
|
||||
"success_count": bson.M{
|
||||
"$cond": []interface{}{
|
||||
bson.M{
|
||||
"$eq": []string{
|
||||
"$status",
|
||||
constants.StatusFinished,
|
||||
},
|
||||
},
|
||||
1,
|
||||
0,
|
||||
},
|
||||
},
|
||||
"result_count": "$result_count",
|
||||
"wait_duration": "$wait_duration",
|
||||
"runtime_duration": "$runtime_duration",
|
||||
},
|
||||
}
|
||||
|
||||
// group
|
||||
op3 := bson.M{
|
||||
"$group": bson.M{
|
||||
"_id": nil,
|
||||
"task_count": bson.M{"$sum": 1},
|
||||
"success_count": bson.M{"$sum": "$success_count"},
|
||||
"result_count": bson.M{"$sum": "$result_count"},
|
||||
"wait_duration": bson.M{"$sum": "$wait_duration"},
|
||||
"runtime_duration": bson.M{"$sum": "$runtime_duration"},
|
||||
},
|
||||
}
|
||||
|
||||
// run aggregation pipeline
|
||||
var overview Overview
|
||||
if err := col.Pipe([]bson.M{op1, op2, op3}).One(&overview); err != nil {
|
||||
if err == mgo.ErrNotFound {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Status: "ok",
|
||||
Message: "success",
|
||||
Data: Data{
|
||||
Overview: overview,
|
||||
Daily: []model.TaskDailyItem{},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Errorf(err.Error())
|
||||
HandleError(http.StatusInternalServerError, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 后续处理
|
||||
successCount, _ := strconv.ParseFloat(strconv.Itoa(overview.SuccessCount), 64)
|
||||
taskCount, _ := strconv.ParseFloat(strconv.Itoa(overview.TaskCount), 64)
|
||||
overview.SuccessRate = successCount / taskCount
|
||||
overview.AvgWaitDuration = overview.TotalWaitDuration / taskCount
|
||||
overview.AvgRuntimeDuration = overview.TotalRuntimeDuration / taskCount
|
||||
|
||||
items, err := model.GetDailyTaskStats(bson.M{"spider_id": spider.Id})
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
HandleError(http.StatusInternalServerError, c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Status: "ok",
|
||||
Message: "success",
|
||||
Data: Data{
|
||||
Overview: overview,
|
||||
Daily: items,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -235,5 +235,20 @@ export default {
|
||||
'Spider info has been saved successfully': '爬虫信息已成功保存',
|
||||
'Do you allow us to collect some statistics to improve Crawlab?': '您允许我们收集统计数据以更好地优化Crawlab?',
|
||||
'Saved file successfully': '成功保存文件',
|
||||
'An error happened when fetching the data': '请求数据时出错'
|
||||
'An error happened when fetching the data': '请求数据时出错',
|
||||
|
||||
// 登录
|
||||
'Sign in': '登录',
|
||||
'Sign-in': '登录',
|
||||
'Sign out': '退出登录',
|
||||
'Sign-out': '退出登录',
|
||||
'Sign up': '注册',
|
||||
'Sign-up': '注册',
|
||||
'Forgot Password': '忘记密码',
|
||||
'Has Account': '已有账号',
|
||||
'New to Crawlab': 'Crawlab新用户',
|
||||
'Initial Username/Password': '初始用户名/密码',
|
||||
'Username': '用户名',
|
||||
'Password': '密码',
|
||||
'Confirm Password': '确认密码'
|
||||
}
|
||||
|
||||
@@ -1,38 +1,65 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<canvas id="canvas"></canvas>
|
||||
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form" auto-complete="on"
|
||||
label-position="left">
|
||||
<h3 class="title">Crawlab</h3>
|
||||
<el-form-item prop="username">
|
||||
<span class="svg-container">
|
||||
<svg-icon icon-class="user"/>
|
||||
</span>
|
||||
<el-input v-model="loginForm.username" name="username" type="text" auto-complete="on"
|
||||
placeholder="username"/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<span class="svg-container">
|
||||
<svg-icon icon-class="password"/>
|
||||
</span>
|
||||
<h3 class="title">
|
||||
Crawlab
|
||||
</h3>
|
||||
<el-form-item prop="username" style="margin-bottom: 28px;">
|
||||
<el-input
|
||||
:type="pwdType"
|
||||
v-model="loginForm.password"
|
||||
name="password"
|
||||
auto-complete="on"
|
||||
placeholder="password"
|
||||
@keyup.enter.native="handleLogin"/>
|
||||
<span class="show-pwd" @click="showPwd">
|
||||
<svg-icon :icon-class="pwdType === 'password' ? 'eye' : 'eye-open'"/>
|
||||
</span>
|
||||
v-model="loginForm.username"
|
||||
name="username"
|
||||
type="text"
|
||||
auto-complete="on"
|
||||
:placeholder="$t('Username')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button :loading="loading" type="primary" style="width:100%;" @click.native.prevent="handleLogin">
|
||||
Sign in
|
||||
<el-form-item prop="password" style="margin-bottom: 28px;">
|
||||
<el-input
|
||||
:type="pwdType"
|
||||
v-model="loginForm.password"
|
||||
name="password"
|
||||
auto-complete="on"
|
||||
:placeholder="$t('Password')"
|
||||
@keyup.enter.native="isSignUp ? handleSignup : handleLogin"/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isSignUp" prop="confirmPassword" style="margin-bottom: 28px;">
|
||||
<el-input
|
||||
:type="pwdType"
|
||||
v-model="loginForm.confirmPassword"
|
||||
name="password"
|
||||
auto-complete="on"
|
||||
:placeholder="$t('Confirm Password')"
|
||||
@keyup.enter.native="isSignUp ? handleSignup : handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item style="border: none">
|
||||
<el-button v-if="isSignUp" :loading="loading" type="primary" style="width:100%;"
|
||||
@click.native.prevent="handleSignup">
|
||||
{{$t('Sign up')}}
|
||||
</el-button>
|
||||
<el-button v-if="!isSignUp" :loading="loading" type="primary" style="width:100%;"
|
||||
@click.native.prevent="handleLogin">
|
||||
{{$t('Sign in')}}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<div class="alternatives">
|
||||
<div class="left">
|
||||
<span v-if="!isSignUp" class="forgot-password">{{$t('Forgot Password')}}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span v-if="isSignUp">{{$t('Has Account')}}, </span>
|
||||
<span v-if="isSignUp" class="sign-in" @click="isSignUp=false">{{$t('Sign-in')}} ></span>
|
||||
<span v-if="!isSignUp">{{$t('New to Crawlab')}}, </span>
|
||||
<span v-if="!isSignUp" class="sign-up" @click="isSignUp=true">{{$t('Sign-up')}} ></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tips">
|
||||
<span style="margin-right:20px;">username: admin</span>
|
||||
<span> password: admin</span>
|
||||
<span>{{$t('Initial Username/Password')}}: admin/admin</span>
|
||||
<a href="https://github.com/tikazyq/crawlab" target="_blank" style="float:right">
|
||||
<img src="https://img.shields.io/badge/github-crawlab-blue">
|
||||
</a>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -60,8 +87,9 @@ export default {
|
||||
}
|
||||
return {
|
||||
loginForm: {
|
||||
username: 'admin',
|
||||
password: 'admin'
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
loginRules: {
|
||||
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
|
||||
@@ -69,17 +97,10 @@ export default {
|
||||
},
|
||||
loading: false,
|
||||
pwdType: 'password',
|
||||
redirect: undefined
|
||||
redirect: undefined,
|
||||
isSignUp: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// $route: {
|
||||
// handler: function (route) {
|
||||
// this.redirect = route.query && route.query.redirect
|
||||
// },
|
||||
// immediate: true
|
||||
// }
|
||||
},
|
||||
methods: {
|
||||
showPwd () {
|
||||
if (this.pwdType === 'password') {
|
||||
@@ -105,7 +126,130 @@ export default {
|
||||
// return false
|
||||
// }
|
||||
// })
|
||||
},
|
||||
handleSignup () {
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
initCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
const initCanvas = () => {
|
||||
var canvas = document.getElementById('canvas')
|
||||
var ctx = canvas.getContext('2d')
|
||||
|
||||
resize()
|
||||
window.onresize = resize
|
||||
|
||||
function resize () {
|
||||
canvas.width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
canvas.height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
|
||||
}
|
||||
|
||||
var RAF = (function () {
|
||||
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
|
||||
window.setTimeout(callback, 1000 / 60)
|
||||
}
|
||||
})()
|
||||
|
||||
// 鼠标活动时,获取鼠标坐标
|
||||
var warea = { x: null, y: null, max: 20000 }
|
||||
// window.onmousemove = function (e) {
|
||||
// e = e || window.event
|
||||
//
|
||||
// warea.x = e.clientX
|
||||
// warea.y = e.clientY
|
||||
// }
|
||||
// window.onmouseout = function (e) {
|
||||
// warea.x = null
|
||||
// warea.y = null
|
||||
// }
|
||||
|
||||
// 添加粒子
|
||||
// x,y为粒子坐标,xa, ya为粒子xy轴加速度,max为连线的最大距离
|
||||
var dots = []
|
||||
for (var i = 0; i < 300; i++) {
|
||||
var x = Math.random() * canvas.width
|
||||
var y = Math.random() * canvas.height
|
||||
var xa = Math.random() * 2 - 1
|
||||
var ya = Math.random() * 2 - 1
|
||||
|
||||
dots.push({
|
||||
x: x,
|
||||
y: y,
|
||||
xa: xa,
|
||||
ya: ya,
|
||||
max: 6000
|
||||
})
|
||||
}
|
||||
|
||||
// 延迟100秒开始执行动画,如果立即执行有时位置计算会出错
|
||||
setTimeout(function () {
|
||||
animate()
|
||||
}, 100)
|
||||
|
||||
// 每一帧循环的逻辑
|
||||
function animate () {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 将鼠标坐标添加进去,产生一个用于比对距离的点数组
|
||||
var ndots = [warea].concat(dots)
|
||||
|
||||
dots.forEach(function (dot) {
|
||||
// 粒子位移
|
||||
dot.x += dot.xa
|
||||
dot.y += dot.ya
|
||||
|
||||
// 遇到边界将加速度反向
|
||||
dot.xa *= (dot.x > canvas.width || dot.x < 0) ? -1 : 1
|
||||
dot.ya *= (dot.y > canvas.height || dot.y < 0) ? -1 : 1
|
||||
|
||||
// 绘制点
|
||||
ctx.fillRect(dot.x - 0.5, dot.y - 0.5, 1, 1)
|
||||
|
||||
// 循环比对粒子间的距离
|
||||
for (var i = 0; i < ndots.length; i++) {
|
||||
var d2 = ndots[i]
|
||||
|
||||
if (dot === d2 || d2.x === null || d2.y === null) continue
|
||||
|
||||
var xc = dot.x - d2.x
|
||||
var yc = dot.y - d2.y
|
||||
|
||||
// 两个粒子之间的距离
|
||||
var dis = xc * xc + yc * yc
|
||||
|
||||
// 距离比
|
||||
var ratio
|
||||
|
||||
// 如果两个粒子之间的距离小于粒子对象的max值,则在两个粒子间画线
|
||||
if (dis < d2.max) {
|
||||
// 如果是鼠标,则让粒子向鼠标的位置移动
|
||||
if (d2 === warea && dis > (d2.max / 2)) {
|
||||
dot.x -= xc * 0.03
|
||||
dot.y -= yc * 0.03
|
||||
}
|
||||
|
||||
// 计算距离比
|
||||
ratio = (d2.max - dis) / d2.max
|
||||
|
||||
// 画线
|
||||
ctx.beginPath()
|
||||
ctx.lineWidth = ratio / 2
|
||||
// 线条颜色
|
||||
ctx.strokeStyle = 'rgba(64,158,255,' + (ratio + 0.1) + ')'
|
||||
ctx.moveTo(dot.x, dot.y)
|
||||
ctx.lineTo(d2.x, d2.y)
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
// 将已经计算过的粒子从数组中删除
|
||||
ndots.splice(ndots.indexOf(dot), 1)
|
||||
})
|
||||
|
||||
RAF(animate)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -118,39 +262,51 @@ export default {
|
||||
.login-container {
|
||||
.el-input {
|
||||
display: inline-block;
|
||||
height: 47px;
|
||||
width: 85%;
|
||||
width: calc(100% - 44px);
|
||||
margin-left: 22px;
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: 0px;
|
||||
border: 0;
|
||||
-webkit-appearance: none;
|
||||
border-radius: 0px;
|
||||
border-radius: 0;
|
||||
padding: 12px 5px 12px 15px;
|
||||
color: $light_gray;
|
||||
height: 47px;
|
||||
|
||||
&:-webkit-autofill {
|
||||
-webkit-box-shadow: 0 0 0px 1000px $bg inset !important;
|
||||
-webkit-text-fill-color: #fff !important;
|
||||
}
|
||||
color: #666;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ddd;
|
||||
background: #fff;
|
||||
border-radius: 22px;
|
||||
color: #454545;
|
||||
height: 44px;
|
||||
/*margin-bottom: 28px;*/
|
||||
|
||||
.el-form-item__content {
|
||||
line-height: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
height: 44px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
$bg: #2d3a4b;
|
||||
$bg: transparent;
|
||||
$dark_gray: #889aa4;
|
||||
$light_gray: #eee;
|
||||
$light_gray: #aaa;
|
||||
.login-container {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
@@ -158,10 +314,11 @@ export default {
|
||||
background-color: $bg;
|
||||
|
||||
.login-form {
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 520px;
|
||||
width: 480px;
|
||||
max-width: 100%;
|
||||
padding: 35px 35px 15px 35px;
|
||||
margin: 120px auto;
|
||||
@@ -169,12 +326,13 @@ export default {
|
||||
|
||||
.tips {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
color: #666;
|
||||
margin-bottom: 10px;
|
||||
background: transparent;
|
||||
|
||||
span {
|
||||
&:first-of-type {
|
||||
margin-right: 16px;
|
||||
margin-right: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,12 +346,11 @@ export default {
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 26px;
|
||||
font-weight: 400;
|
||||
color: $light_gray;
|
||||
margin: 0px auto 40px auto;
|
||||
font-size: 32px;
|
||||
color: #666;
|
||||
margin: 0px auto 20px auto;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.show-pwd {
|
||||
@@ -205,5 +362,27 @@ export default {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.alternatives {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 400;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
.forgot-password {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sign-in,
|
||||
.sign-up {
|
||||
cursor: pointer;
|
||||
color: #409EFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user