增加功能:增加登录验证码 closed #IRWNP
#Issue https://gitee.com/LongbowEnterprise/dashboard/issues?id=IRWNP
This commit is contained in:
parent
0a1b1ff271
commit
296ac82a4c
|
@ -14,6 +14,7 @@
|
|||
<link href="~/lib/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
|
||||
<link href="~/lib/bootstrap-sweetalert/sweetalert.min.css" rel="stylesheet" />
|
||||
</environment>
|
||||
<link href="~/lib/captcha/slidercaptcha.css" rel="stylesheet" />
|
||||
<link href="~/css/theme.css" rel="stylesheet" asp-append-version="true" />
|
||||
<link href="~/css/login.css" rel="stylesheet" asp-append-version="true" />
|
||||
<link href="~/css/login-responsive.css" rel="stylesheet" asp-append-version="true" />
|
||||
|
@ -35,6 +36,7 @@
|
|||
<script src="~/lib/validate/localization/messages_zh.min.js"></script>
|
||||
<script src="~/lib/bootstrap-sweetalert/sweetalert.min.js"></script>
|
||||
</environment>
|
||||
<script src="~/lib/captcha/longbow.slidercaptcha.js"></script>
|
||||
<script src="~/lib/longbow/longbow.common.js"></script>
|
||||
<script src="~/lib/longbow/longbow.validate.js"></script>
|
||||
<script src="~/js/login.js" asp-append-version="true"></script>
|
||||
|
@ -73,6 +75,13 @@
|
|||
<a href="#" data-method="register">申请账号</a>
|
||||
<a href="#" data-method="forgot">忘记密码</a>
|
||||
</div>
|
||||
<div class="slidercaptcha card">
|
||||
<div class="card-header">
|
||||
<span>请完成安全验证</span>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="card-body"><div id="captcha"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
.login-wrap .rememberPwd {
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.login-wrap .rememberPwd i {
|
||||
|
@ -53,3 +54,30 @@
|
|||
.form-control {
|
||||
border-color: #1ca0e9;
|
||||
}
|
||||
|
||||
.slidercaptcha {
|
||||
display: none;
|
||||
position: relative;
|
||||
bottom: 254px;
|
||||
background-color: #00adec;
|
||||
width: 310px;
|
||||
height: 280px;
|
||||
border-radius: 4px;
|
||||
left: -10px;
|
||||
box-shadow: 0 0 10px #fff;
|
||||
}
|
||||
|
||||
.slidercaptcha canvas:first-child {
|
||||
border-radius: 4px;
|
||||
border: solid 1px #0076c9;
|
||||
}
|
||||
|
||||
.slidercaptcha .close {
|
||||
color: #fff;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.slidercaptcha.card .card-header {
|
||||
background-image: none;
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
|
|
@ -51,4 +51,21 @@
|
|||
$rem.val('false');
|
||||
}
|
||||
});
|
||||
|
||||
var $captcha = $('.slidercaptcha');
|
||||
$('.slidercaptcha .close').on('click', function() {
|
||||
$captcha.removeClass('d-block');
|
||||
});
|
||||
|
||||
$('button[type="submit"]').on('click', function(e){
|
||||
if ($.browser.versions.mobile) return true;
|
||||
$captcha.addClass('d-block');
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#captcha').sliderCaptcha({
|
||||
onSuccess: function () {
|
||||
$('form').submit();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,295 @@
|
|||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
var isIE = window.navigator.userAgent.indexOf('Trident') > -1;
|
||||
var L = 63;
|
||||
|
||||
function getRandomNumberByRange(start, end) {
|
||||
return Math.round(Math.random() * (end - start) + start);
|
||||
}
|
||||
|
||||
function createCanvas(width, height) {
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function createImg(w, h, onload) {
|
||||
var img = new Image();
|
||||
img.crossOrigin = "Anonymous";
|
||||
img.onload = onload;
|
||||
img.onerror = function () {
|
||||
img.setSrc(getRandomImgSrc(w, h));
|
||||
}
|
||||
|
||||
img.setSrc = function (src) {
|
||||
if (isIE) { // IE浏览器无法通过img.crossOrigin跨域,使用ajax获取图片blob然后转为dataURL显示
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.onloadend = function (e) {
|
||||
var file = new FileReader(); // FileReader仅支持IE10+
|
||||
file.readAsDataURL(e.target.response);
|
||||
file.onloadend = function (e) {
|
||||
img.src = e.target.result;
|
||||
}
|
||||
}
|
||||
xhr.open('GET', src);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
} else img.src = src;
|
||||
}
|
||||
|
||||
img.setSrc(getRandomImgSrc(w, h));
|
||||
return img;
|
||||
}
|
||||
|
||||
function createElement(tagName, className) {
|
||||
var elment = document.createElement(tagName);
|
||||
elment.className = className;
|
||||
return elment;
|
||||
}
|
||||
|
||||
function addClass(tag, className) {
|
||||
tag.classList.add(className);
|
||||
}
|
||||
|
||||
function removeClass(tag, className) {
|
||||
tag.classList.remove(className);
|
||||
}
|
||||
|
||||
function getRandomImgSrc(w, h) {
|
||||
return '//picsum.photos/' + w + '/' + h + '/?image=' + getRandomNumberByRange(0, 1084);
|
||||
}
|
||||
|
||||
function draw(ctx, x, y, l, r, PI, operation) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y)
|
||||
ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI)
|
||||
ctx.lineTo(x + l, y)
|
||||
ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI)
|
||||
ctx.lineTo(x + l, y + l)
|
||||
ctx.lineTo(x, y + l)
|
||||
ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true)
|
||||
ctx.lineTo(x, y)
|
||||
ctx.lineWidth = 2
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'
|
||||
ctx.stroke()
|
||||
ctx[operation]()
|
||||
ctx.globalCompositeOperation = isIE ? 'xor' : 'overlay'
|
||||
}
|
||||
|
||||
function sum(x, y) {
|
||||
return x + y
|
||||
}
|
||||
|
||||
function square(x) {
|
||||
return x * x
|
||||
}
|
||||
|
||||
var SliderCaptcha = function (element, options) {
|
||||
this.$element = $(element);
|
||||
this.options = $.extend({}, SliderCaptcha.DEFAULTS, options);
|
||||
this.$element.css({ 'position': 'relative', 'width': this.options.width + 'px', 'margin': '0 auto' });
|
||||
|
||||
var L = this.options.sliderL + this.options.sliderR * 2 + 3; // 滑块实际边长
|
||||
};
|
||||
|
||||
SliderCaptcha.VERSION = '1.0';
|
||||
SliderCaptcha.Author = 'argo@163.com';
|
||||
SliderCaptcha.DEFAULTS = {
|
||||
width: 280, // canvas宽度
|
||||
height: 155, // canvas高度
|
||||
PI: Math.PI,
|
||||
sliderL: 42, // 滑块边长
|
||||
sliderR: 9, // 滑块半径
|
||||
loadingText: '正在加载中...',
|
||||
failedText: '再试一次',
|
||||
barText: '向右滑动填充拼图'
|
||||
};
|
||||
|
||||
function Plugin(option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this);
|
||||
var data = $this.data('lgb.SliderCaptcha');
|
||||
var options = typeof option === 'object' && option;
|
||||
|
||||
if (!data && /init|reset/.test(option)) return;
|
||||
if (!data) {
|
||||
$this.data('lgb.SliderCaptcha', data = new SliderCaptcha(this, options));
|
||||
data.init();
|
||||
}
|
||||
if (typeof option === 'string') data[option]();
|
||||
});
|
||||
}
|
||||
|
||||
$.fn.sliderCaptcha = Plugin;
|
||||
$.fn.sliderCaptcha.Constructor = SliderCaptcha;
|
||||
|
||||
var _proto = SliderCaptcha.prototype;
|
||||
_proto.init = function () {
|
||||
this.initDOM()
|
||||
this.initImg()
|
||||
this.bindEvents()
|
||||
};
|
||||
|
||||
_proto.initDOM = function () {
|
||||
var canvas = createCanvas(this.options.width - 2, this.options.height) // 画布
|
||||
var block = canvas.cloneNode(true) // 滑块
|
||||
var sliderContainer = createElement('div', 'sliderContainer');
|
||||
var refreshIcon = createElement('i', 'refreshIcon fa fa-repeat');
|
||||
var sliderMask = createElement('div', 'sliderMask');
|
||||
var sliderbg = createElement('div', 'sliderbg');
|
||||
var slider = createElement('div', 'slider');
|
||||
var sliderIcon = createElement('i', 'fa fa-arrow-right sliderIcon');
|
||||
var text = createElement('span', 'sliderText');
|
||||
|
||||
block.className = 'block'
|
||||
text.innerHTML = this.options.barText;
|
||||
|
||||
var el = this.$element;
|
||||
el.append($(canvas));
|
||||
el.append($(refreshIcon));
|
||||
el.append($(block));
|
||||
slider.appendChild(sliderIcon);
|
||||
sliderMask.appendChild(slider);
|
||||
sliderContainer.appendChild(sliderbg);
|
||||
sliderContainer.appendChild(sliderMask);
|
||||
sliderContainer.appendChild(text);
|
||||
el.append($(sliderContainer));
|
||||
|
||||
Object.assign(this, {
|
||||
canvas,
|
||||
block,
|
||||
sliderContainer,
|
||||
refreshIcon,
|
||||
slider,
|
||||
sliderMask,
|
||||
sliderIcon,
|
||||
text: $(text),
|
||||
canvasCtx: canvas.getContext('2d'),
|
||||
blockCtx: block.getContext('2d')
|
||||
})
|
||||
};
|
||||
|
||||
_proto.initImg = function () {
|
||||
var that = this;
|
||||
var img = createImg(this.options.width, this.options.height, function () {
|
||||
that.draw();
|
||||
that.canvasCtx.drawImage(img, 0, 0, that.options.width - 2, that.options.height);
|
||||
that.blockCtx.drawImage(img, 0, 0, that.options.width - 2, that.options.height);
|
||||
var y = that.y - that.options.sliderR * 2 - 1;
|
||||
var ImageData = that.blockCtx.getImageData(that.x - 3, y, L, L);
|
||||
that.block.width = L;
|
||||
that.blockCtx.putImageData(ImageData, 0, y);
|
||||
that.text.text(that.text.attr('data-text'));
|
||||
})
|
||||
this.text.attr('data-text', this.options.barText);
|
||||
this.text.text(this.options.loadingText);
|
||||
this.img = img
|
||||
};
|
||||
|
||||
_proto.draw = function () {
|
||||
// 随机创建滑块的位置
|
||||
this.x = getRandomNumberByRange(L + 10, this.options.width - (L + 10))
|
||||
this.y = getRandomNumberByRange(10 + this.options.sliderR * 2, this.options.height - (L + 10))
|
||||
draw(this.canvasCtx, this.x, this.y, this.options.sliderL, this.options.sliderR, this.options.PI, 'fill')
|
||||
draw(this.blockCtx, this.x, this.y, this.options.sliderL, this.options.sliderR, this.options.PI, 'clip')
|
||||
};
|
||||
|
||||
_proto.clean = function () {
|
||||
this.canvasCtx.clearRect(0, 0, this.options.width, this.options.height);
|
||||
this.blockCtx.clearRect(0, 0, this.options.width, this.options.height);
|
||||
this.block.width = this.options.width;
|
||||
};
|
||||
|
||||
_proto.bindEvents = function () {
|
||||
var that = this;
|
||||
this.$element.on('selectstart', function () {
|
||||
return false;
|
||||
});
|
||||
|
||||
$(this.refreshIcon).on('click', function () {
|
||||
that.reset();
|
||||
typeof that.onRefresh === 'function' && that.onRefresh()
|
||||
});
|
||||
|
||||
var originX, originY, trail = [],
|
||||
isMouseDown = false
|
||||
|
||||
var handleDragStart = function (e) {
|
||||
originX = e.clientX || e.touches[0].clientX;
|
||||
originY = e.clientY || e.touches[0].clientY;
|
||||
isMouseDown = true;
|
||||
};
|
||||
|
||||
var handleDragMove = function (e) {
|
||||
if (!isMouseDown) return false;
|
||||
var eventX = e.clientX || e.touches[0].clientX;
|
||||
var eventY = e.clientY || e.touches[0].clientY;
|
||||
var moveX = eventX - originX;
|
||||
var moveY = eventY - originY;
|
||||
if (moveX < 0 || moveX + 40 > that.options.width) return false;
|
||||
that.slider.style.left = moveX + 'px';
|
||||
var blockLeft = (that.options.width - 40 - 20) / (that.options.width - 40) * moveX;
|
||||
that.block.style.left = blockLeft + 'px';
|
||||
|
||||
addClass(that.sliderContainer, 'sliderContainer_active');
|
||||
that.sliderMask.style.width = (moveX + 4) + 'px';
|
||||
trail.push(moveY);
|
||||
};
|
||||
|
||||
var handleDragEnd = function (e) {
|
||||
if (!isMouseDown) return false
|
||||
isMouseDown = false
|
||||
var eventX = e.clientX || e.changedTouches[0].clientX
|
||||
if (eventX == originX) return false
|
||||
removeClass(that.sliderContainer, 'sliderContainer_active')
|
||||
that.trail = trail
|
||||
var {
|
||||
spliced,
|
||||
verified
|
||||
} = that.verify()
|
||||
if (spliced && verified) {
|
||||
addClass(that.sliderContainer, 'sliderContainer_success')
|
||||
if ($.isFunction(that.options.onSuccess)) that.options.onSuccess.call(that.$element);
|
||||
} else {
|
||||
addClass(that.sliderContainer, 'sliderContainer_fail')
|
||||
if ($.isFunction(that.options.onFail)) that.options.onFail.call(that.$element);
|
||||
setTimeout(() => {
|
||||
that.text.text(that.options.failedText);
|
||||
that.reset();
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
this.slider.addEventListener('mousedown', handleDragStart);
|
||||
this.slider.addEventListener('touchstart', handleDragStart);
|
||||
document.addEventListener('mousemove', handleDragMove);
|
||||
document.addEventListener('touchmove', handleDragMove);
|
||||
document.addEventListener('mouseup', handleDragEnd);
|
||||
document.addEventListener('touchend', handleDragEnd);
|
||||
};
|
||||
|
||||
_proto.verify = function () {
|
||||
var arr = this.trail // 拖动时y轴的移动距离
|
||||
var average = arr.reduce(sum) / arr.length;
|
||||
var deviations = arr.map(x => x - average);
|
||||
var stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length);
|
||||
var left = parseInt(this.block.style.left);
|
||||
return {
|
||||
spliced: Math.abs(left - this.x) < 10,
|
||||
verified: stddev !== 0, // 简单验证下拖动轨迹,为零时表示Y轴上下没有波动,可能非人为操作
|
||||
}
|
||||
};
|
||||
|
||||
_proto.reset = function () {
|
||||
this.sliderContainer.className = 'sliderContainer'
|
||||
this.slider.style.left = 0
|
||||
this.block.style.left = 0
|
||||
this.sliderMask.style.width = 0
|
||||
this.clean()
|
||||
this.text.attr('data-text', this.text.text());
|
||||
this.text.text(this.options.loadingText);
|
||||
this.img.setSrc(getRandomImgSrc(this.options.width, this.options.height));
|
||||
};
|
||||
})(jQuery);
|
|
@ -0,0 +1,130 @@
|
|||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.block {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sliderContainer {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
background: #f7f9fa;
|
||||
color: #45494c;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.sliderbg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
background-color: #f7f9fa;
|
||||
height: 40px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.sliderContainer_active .slider {
|
||||
top: -1px;
|
||||
border: 1px solid #1991FA;
|
||||
}
|
||||
|
||||
.sliderContainer_active .sliderMask {
|
||||
border-width: 1px 0 1px 1px;
|
||||
}
|
||||
|
||||
.sliderContainer_success .slider {
|
||||
top: -1px;
|
||||
border: 1px solid #52CCBA;
|
||||
background-color: #52CCBA !important;
|
||||
}
|
||||
|
||||
.sliderContainer_success .sliderMask {
|
||||
border: 1px solid #52CCBA;
|
||||
border-width: 1px 0 1px 1px;
|
||||
background-color: #D2F4EF;
|
||||
}
|
||||
|
||||
.sliderContainer_success .sliderIcon:before {
|
||||
content: "\f00c";
|
||||
}
|
||||
|
||||
.sliderContainer_fail .slider {
|
||||
top: -1px;
|
||||
border: 1px solid #f57a7a;
|
||||
background-color: #f57a7a !important;
|
||||
}
|
||||
|
||||
.sliderContainer_fail .sliderMask {
|
||||
border: 1px solid #f57a7a;
|
||||
background-color: #fce1e1;
|
||||
border-width: 1px 0 1px 1px;
|
||||
}
|
||||
|
||||
.sliderContainer_fail .sliderIcon:before {
|
||||
content: "\f00d";
|
||||
}
|
||||
.sliderContainer_active .sliderText, .sliderContainer_success .sliderText, .sliderContainer_fail .sliderText {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sliderMask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 40px;
|
||||
border: 0 solid #1991FA;
|
||||
background: #D1E9FE;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
transition: background .2s linear;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.slider:hover {
|
||||
background: #1991FA;
|
||||
}
|
||||
|
||||
.slider:hover .sliderIcon {
|
||||
background-position: 0 -13px;
|
||||
}
|
||||
|
||||
.sliderText {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sliderIcon {
|
||||
|
||||
}
|
||||
|
||||
.refreshIcon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
margin: 6px;
|
||||
color: #007ec7;
|
||||
font-size: 1rem;
|
||||
z-index: 5;
|
||||
transition: color .3s linear;
|
||||
}
|
||||
|
||||
.refreshIcon:hover {
|
||||
color: #05639a;
|
||||
}
|
Loading…
Reference in New Issue