added login page

This commit is contained in:
Marvin Zhang
2019-07-26 20:04:20 +08:00
parent 946bb7ceba
commit ffb8cc5ef9
3 changed files with 382 additions and 62 deletions

View File

@@ -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,
},
})
}

View File

@@ -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': '确认密码'
}

View File

@@ -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
// }
// 添加粒子
// xy为粒子坐标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>