123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582 |
- // 滑动校验组件
- Vue.component('vue-puzzle-vcode', {
- props: {
- canvasWidth: { type: Number, default: 310 }, // 主canvas的宽
- canvasHeight: { type: Number, default: 160 }, // 主canvas的高
- // 是否出现,由父级控制
- show: { type: Boolean, default: false },
- puzzleScale: { type: Number, default: 1 }, // 拼图块的大小缩放比例
- sliderSize: { type: Number, default: 50 }, // 滑块的大小
- range: { type: Number, default: 10 }, // 允许的偏差值
- // 所有的背景图片
- imgs: {
- type: Array
- },
- successText: {
- type: String,
- default: "验证通过!"
- },
- failText: {
- type: String,
- default: "验证失败,请重试"
- },
- sliderText: {
- type: String,
- default: "拖动滑块完成拼图"
- }
- },
- data() {
- return {
- mouseDown: false, // 鼠标是否在按钮上按下
- startWidth: 50, // 鼠标点下去时父级的width
- startX: 0, // 鼠标按下时的X
- newX: 0, // 鼠标当前的偏移X
- pinX: 0, // 拼图的起始X
- pinY: 0, // 拼图的起始Y
- loading: false, // 是否正在加在中,主要是等图片onload
- isCanSlide: false, // 是否可以拉动滑动条
- error: false, // 图片加在失败会出现这个,提示用户手动刷新
- infoBoxShow: false, // 提示信息是否出现
- infoText: "", // 提示等信息
- infoBoxFail: false, // 是否验证失败
- timer1: null, // setTimout1
- closeDown: false, // 为了解决Mac上的click BUG
- isSuccess: false, // 验证成功
- imgIndex: -1, // 用于自定义图片时不会随机到重复的图片
- isSubmting: false, // 是否正在判定,主要用于判定中不能点击重置按钮
- resetSvg: "el-icon-refresh",
- };
- },
- template: '<div :class="[\'vue-puzzle-vcode\', { show_: show }]"\n' +
- ' @mousedown="onCloseMouseDown"\n' +
- ' @mouseup="onCloseMouseUp"\n' +
- ' @touchstart="onCloseMouseDown"\n' +
- ' @touchend="onCloseMouseUp">\n' +
- ' <div class="vue-auth-box_"\n' +
- ' @mousedown.stop\n' +
- ' @touchstart.stop>\n' +
- ' <div class="auth-body_"\n' +
- ' :style="`height: ${canvasHeight}px`">\n' +
- ' <!-- 主图,有缺口 -->\n' +
- ' <canvas ref="canvas1"\n' +
- ' :width="canvasWidth"\n' +
- ' :height="canvasHeight"\n' +
- ' :style="`width:${canvasWidth}px;height:${canvasHeight}px`" />\n' +
- ' <!-- 成功后显示的完整图 -->\n' +
- ' <canvas ref="canvas3"\n' +
- ' :class="[\'auth-canvas3_\', { show: isSuccess }]"\n' +
- ' :width="canvasWidth"\n' +
- ' :height="canvasHeight"\n' +
- ' :style="`width:${canvasWidth}px;height:${canvasHeight}px`" />\n' +
- ' <!-- 小图 -->\n' +
- ' <canvas :width="puzzleBaseSize"\n' +
- ' class="auth-canvas2_"\n' +
- ' :height="canvasHeight"\n' +
- ' ref="canvas2"\n' +
- ' :style="\n' +
- ' `width:${puzzleBaseSize}px;height:${canvasHeight}px;transform:translateX(${styleWidth -\n' +
- ' sliderBaseSize -\n' +
- ' (puzzleBaseSize - sliderBaseSize) *\n' +
- ' ((styleWidth - sliderBaseSize) /\n' +
- ' (canvasWidth - sliderBaseSize))}px)`\n' +
- ' " />\n' +
- ' <div :class="[\'loading-box_\', { hide_: !loading }]">\n' +
- ' <div class="loading-gif_">\n' +
- ' <span></span>\n' +
- ' <span></span>\n' +
- ' <span></span>\n' +
- ' <span></span>\n' +
- ' <span></span>\n' +
- ' </div>\n' +
- ' </div>\n' +
- ' <div :class="[\'info-box_\', { show: infoBoxShow }, { fail: infoBoxFail }]">\n' +
- ' {{ infoText }}\n' +
- ' </div>\n' +
- ' <div :class="[\'flash_\', { show: isSuccess }]"\n' +
- ' :style="\n' +
- ' `transform: translateX(${\n' +
- ' isSuccess\n' +
- ' ? `${canvasWidth + canvasHeight * 0.578}px`\n' +
- ' : `-${canvasHeight * 0.578}px`\n' +
- ' }) skew(-30deg, 0);`\n' +
- ' "></div>\n' +
- ' <i class="reset_" :class="resetSvg" @click="reset" />\n' +
- ' </div>\n' +
- ' <div class="auth-control_">\n' +
- ' <div class="range-box"\n' +
- ' :style="`height:${sliderBaseSize}px`">\n' +
- ' <div class="range-text">{{ sliderText }}</div>\n' +
- ' <div class="range-slider"\n' +
- ' ref="range-slider"\n' +
- ' :style="`width:${styleWidth}px`">\n' +
- ' <div :class="[\'range-btn\', { isDown: mouseDown }]"\n' +
- ' :style="`width:${sliderBaseSize}px`"\n' +
- ' @mousedown="onRangeMouseDown($event)"\n' +
- ' @touchstart="onRangeMouseDown($event)">\n' +
- ' <div></div>\n' +
- ' <div></div>\n' +
- ' <div></div>\n' +
- ' </div>\n' +
- ' </div>\n' +
- ' </div>\n' +
- ' </div>\n' +
- ' </div>\n' +
- ' </div>',
- mounted: function () {
- document.body.appendChild(this.$el);
- document.addEventListener("mousemove", this.onRangeMouseMove, false);
- document.addEventListener("mouseup", this.onRangeMouseUp, false);
- document.addEventListener("touchmove", this.onRangeMouseMove, {
- passive: false
- });
- document.addEventListener("touchend", this.onRangeMouseUp, false);
- if (this.show) {
- document.body.classList.add("vue-puzzle-overflow");
- this.reset();
- }
- },
- beforeDestroy() {
- clearTimeout(this.timer1);
- document.body.removeChild(this.$el);
- document.removeEventListener("mousemove", this.onRangeMouseMove, false);
- document.removeEventListener("mouseup", this.onRangeMouseUp, false);
- document.removeEventListener("touchmove", this.onRangeMouseMove, {
- passive: false
- });
- document.removeEventListener("touchend", this.onRangeMouseUp, false);
- },
- /** 监听 **/
- watch: {
- show(newV) {
- // 每次出现都应该重新初始化
- if (newV) {
- document.body.classList.add("vue-puzzle-overflow");
- this.reset();
- } else {
- this.isSubmting = false;
- this.isSuccess = false;
- this.infoBoxShow = false;
- document.body.classList.remove("vue-puzzle-overflow");
- }
- }
- },
- /** 计算属性 **/
- computed: {
- // styleWidth是底部用户操作的滑块的父级,就是轨道在鼠标的作用下应该具有的宽度
- styleWidth() {
- const w = this.startWidth + this.newX - this.startX;
- return w < this.sliderBaseSize
- ? this.sliderBaseSize
- : w > this.canvasWidth
- ? this.canvasWidth
- : w;
- },
- // 图中拼图块的60 * 用户设定的缩放比例计算之后的值 0.2~2
- puzzleBaseSize() {
- return Math.round(
- Math.max(Math.min(this.puzzleScale, 2), 0.2) * 52.5 + 6
- );
- },
- // 处理一下sliderSize,弄成整数,以免计算有偏差
- sliderBaseSize() {
- return Math.max(
- Math.min(
- Math.round(this.sliderSize),
- Math.round(this.canvasWidth * 0.5)
- ),
- 10
- );
- }
- },
- /** 方法 **/
- methods: {
- // 关闭
- onClose() {
- if (!this.mouseDown && !this.isSubmting) {
- clearTimeout(this.timer1);
- this.$emit("close");
- }
- },
- onCloseMouseDown() {
- this.closeDown = true;
- },
- onCloseMouseUp() {
- if (this.closeDown) {
- this.onClose();
- }
- this.closeDown = false;
- },
- // 鼠标按下准备拖动
- onRangeMouseDown(e) {
- if (this.isCanSlide) {
- this.mouseDown = true;
- this.startWidth = this.$refs["range-slider"].clientWidth;
- this.newX = e.clientX || e.changedTouches[0].clientX;
- this.startX = e.clientX || e.changedTouches[0].clientX;
- }
- },
- // 鼠标移动
- onRangeMouseMove(e) {
- if (this.mouseDown) {
- e.preventDefault();
- this.newX = e.clientX || e.changedTouches[0].clientX;
- }
- },
- // 鼠标抬起
- onRangeMouseUp() {
- if (this.mouseDown) {
- this.mouseDown = false;
- this.submit();
- }
- },
- /**
- * 开始进行
- * @param withCanvas 是否强制使用canvas随机作图
- */
- init(withCanvas) {
- // 防止重复加载导致的渲染错误
- if(this.loading && !withCanvas){
- return;
- }
- this.loading = true;
- this.isCanSlide = false;
- const c = this.$refs.canvas1;
- const c2 = this.$refs.canvas2;
- const c3 = this.$refs.canvas3;
- const ctx = c.getContext("2d");
- const ctx2 = c2.getContext("2d");
- const ctx3 = c3.getContext("2d");
- const isFirefox = navigator.userAgent.indexOf("Firefox") >= 0 && navigator.userAgent.indexOf("Windows") >= 0; // 是windows版火狐
- const img = document.createElement("img");
- ctx.fillStyle = "rgba(255,255,255,1)";
- ctx3.fillStyle = "rgba(255,255,255,1)";
- ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
- ctx2.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
- // 取一个随机坐标,作为拼图块的位置
- this.pinX = this.getRandom(this.puzzleBaseSize,this.canvasWidth - this.puzzleBaseSize - 20); // 留20的边距
- this.pinY = this.getRandom(20,this.canvasHeight - this.puzzleBaseSize - 20); // 主图高度 - 拼图块自身高度 - 20边距
- img.crossOrigin = "anonymous"; // 匿名,想要获取跨域的图片
- img.onload = () => {
- const [x, y, w, h] = this.makeImgSize(img);
- ctx.save();
- // 先画小图
- this.paintBrick(ctx);
- ctx.closePath();
- if(!isFirefox){
- ctx.shadowOffsetX = 0;
- ctx.shadowOffsetY = 0;
- ctx.shadowColor = "#000";
- ctx.shadowBlur = 3;
- ctx.fill();
- ctx.clip();
- } else {
- ctx.clip();
- ctx.save();
- ctx.shadowOffsetX = 0;
- ctx.shadowOffsetY = 0;
- ctx.shadowColor = "#000";
- ctx.shadowBlur = 3;
- ctx.fill();
- ctx.restore();
- }
- ctx.drawImage(img, x, y, w, h);
- ctx3.fillRect(0,0,this.canvasWidth,this.canvasHeight);
- ctx3.drawImage(img, x, y, w, h);
- // 设置小图的内阴影
- ctx.globalCompositeOperation = "source-atop";
- this.paintBrick(ctx);
- ctx.arc(
- this.pinX + Math.ceil(this.puzzleBaseSize / 2),
- this.pinY + Math.ceil(this.puzzleBaseSize / 2),
- this.puzzleBaseSize * 1.2,
- 0,
- Math.PI * 2,
- true
- );
- ctx.closePath();
- ctx.shadowColor = "rgba(255, 255, 255, .8)";
- ctx.shadowOffsetX = -1;
- ctx.shadowOffsetY = -1;
- ctx.shadowBlur = Math.min(Math.ceil(8 * this.puzzleScale), 12);
- ctx.fillStyle = "#ffffaa";
- ctx.fill();
- // 将小图赋值给ctx2
- const imgData = ctx.getImageData(
- this.pinX - 3, // 为了阴影 是从-3px开始截取,判定的时候要+3px
- this.pinY - 20,
- this.pinX + this.puzzleBaseSize + 5,
- this.pinY + this.puzzleBaseSize + 5
- );
- ctx2.putImageData(imgData, 0, this.pinY - 20);
- // ctx2.drawImage(c, this.pinX - 3,this.pinY - 20,this.pinX + this.puzzleBaseSize + 5,this.pinY + this.puzzleBaseSize + 5,
- // 0, this.pinY - 20, this.pinX + this.puzzleBaseSize + 5, this.pinY + this.puzzleBaseSize + 5);
- // 清理
- ctx.restore();
- ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
- // 画缺口
- ctx.save();
- this.paintBrick(ctx);
- ctx.globalAlpha = 0.8;
- ctx.fillStyle = "#ffffff";
- ctx.fill();
- ctx.restore();
- // 画缺口的内阴影
- ctx.save();
- ctx.globalCompositeOperation = "source-atop";
- this.paintBrick(ctx);
- ctx.arc(
- this.pinX + Math.ceil(this.puzzleBaseSize / 2),
- this.pinY + Math.ceil(this.puzzleBaseSize / 2),
- this.puzzleBaseSize * 1.2,
- 0,
- Math.PI * 2,
- true
- );
- ctx.shadowColor = "#000";
- ctx.shadowOffsetX = 2;
- ctx.shadowOffsetY = 2;
- ctx.shadowBlur = 16;
- ctx.fill();
- ctx.restore();
- // 画整体背景图
- ctx.save();
- ctx.globalCompositeOperation = "destination-over";
- ctx.drawImage(img, x, y, w, h);
- ctx.restore();
- this.loading = false;
- this.isCanSlide = true;
- };
- img.onerror = () => {
- this.init(true); // 如果图片加载错误就重新来,并强制用canvas随机作图
- };
- if (!withCanvas && this.imgs && this.imgs.length) {
- let randomNum = this.getRandom(0, this.imgs.length - 1);
- if (randomNum === this.imgIndex) {
- if (randomNum === this.imgs.length - 1) {
- randomNum = 0;
- } else {
- randomNum++;
- }
- }
- this.imgIndex = randomNum;
- img.src = this.imgs[randomNum];
- } else {
- img.src = this.makeImgWithCanvas();
- }
- },
- // 工具 - 范围随机数
- getRandom(min, max) {
- return Math.ceil(Math.random() * (max - min) + min);
- },
- // 工具 - 设置图片尺寸cover方式贴合canvas尺寸 w/h
- makeImgSize(img) {
- const imgScale = img.width / img.height;
- const canvasScale = this.canvasWidth / this.canvasHeight;
- let x = 0,
- y = 0,
- w = 0,
- h = 0;
- if (imgScale > canvasScale) {
- h = this.canvasHeight;
- w = imgScale * h;
- y = 0;
- x = (this.canvasWidth - w) / 2;
- } else {
- w = this.canvasWidth;
- h = w / imgScale;
- x = 0;
- y = (this.canvasHeight - h) / 2;
- }
- return [x, y, w, h];
- },
- // 绘制拼图块的路径
- paintBrick(ctx) {
- const moveL = Math.ceil(15 * this.puzzleScale); // 直线移动的基础距离
- ctx.beginPath();
- ctx.moveTo(this.pinX, this.pinY);
- ctx.lineTo(this.pinX + moveL, this.pinY);
- ctx.arcTo(
- this.pinX + moveL,
- this.pinY - moveL / 2,
- this.pinX + moveL + moveL / 2,
- this.pinY - moveL / 2,
- moveL / 2
- );
- ctx.arcTo(
- this.pinX + moveL + moveL,
- this.pinY - moveL / 2,
- this.pinX + moveL + moveL,
- this.pinY,
- moveL / 2
- );
- ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY);
- ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY + moveL);
- ctx.arcTo(
- this.pinX + moveL + moveL + moveL + moveL / 2,
- this.pinY + moveL,
- this.pinX + moveL + moveL + moveL + moveL / 2,
- this.pinY + moveL + moveL / 2,
- moveL / 2
- );
- ctx.arcTo(
- this.pinX + moveL + moveL + moveL + moveL / 2,
- this.pinY + moveL + moveL,
- this.pinX + moveL + moveL + moveL,
- this.pinY + moveL + moveL,
- moveL / 2
- );
- ctx.lineTo(
- this.pinX + moveL + moveL + moveL,
- this.pinY + moveL + moveL + moveL
- );
- ctx.lineTo(this.pinX, this.pinY + moveL + moveL + moveL);
- ctx.lineTo(this.pinX, this.pinY + moveL + moveL);
- ctx.arcTo(
- this.pinX + moveL / 2,
- this.pinY + moveL + moveL,
- this.pinX + moveL / 2,
- this.pinY + moveL + moveL / 2,
- moveL / 2
- );
- ctx.arcTo(
- this.pinX + moveL / 2,
- this.pinY + moveL,
- this.pinX,
- this.pinY + moveL,
- moveL / 2
- );
- ctx.lineTo(this.pinX, this.pinY);
- },
- // 用canvas随机生成图片
- makeImgWithCanvas() {
- const canvas = document.createElement("canvas");
- const ctx = canvas.getContext("2d");
- canvas.width = this.canvasWidth;
- canvas.height = this.canvasHeight;
- ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
- 100,
- 255
- )},${this.getRandom(100, 255)})`;
- ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
- // 随机画10个图形
- for (let i = 0; i < 12; i++) {
- ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
- 100,
- 255
- )},${this.getRandom(100, 255)})`;
- ctx.strokeStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
- 100,
- 255
- )},${this.getRandom(100, 255)})`;
- if (this.getRandom(0, 2) > 1) {
- // 矩形
- ctx.save();
- ctx.rotate((this.getRandom(-90, 90) * Math.PI) / 180);
- ctx.fillRect(
- this.getRandom(-20, canvas.width - 20),
- this.getRandom(-20, canvas.height - 20),
- this.getRandom(10, canvas.width / 2 + 10),
- this.getRandom(10, canvas.height / 2 + 10)
- );
- ctx.restore();
- } else {
- // 圆
- ctx.beginPath();
- const ran = this.getRandom(-Math.PI, Math.PI);
- ctx.arc(
- this.getRandom(0, canvas.width),
- this.getRandom(0, canvas.height),
- this.getRandom(10, canvas.height / 2 + 10),
- ran,
- ran + Math.PI * 1.5
- );
- ctx.closePath();
- ctx.fill();
- }
- }
- return canvas.toDataURL("image/png");
- },
- // 开始判定
- submit() {
- this.isSubmting = true;
- // 偏差 x = puzzle的起始X - (用户真滑动的距离) + (puzzle的宽度 - 滑块的宽度) * (用户真滑动的距离/canvas总宽度)
- // 最后+ 的是补上slider和滑块宽度不一致造成的缝隙
- const x = Math.abs(
- this.pinX -
- (this.styleWidth - this.sliderBaseSize) +
- (this.puzzleBaseSize - this.sliderBaseSize) *
- ((this.styleWidth - this.sliderBaseSize) /
- (this.canvasWidth - this.sliderBaseSize)) -
- 3
- );
- if (x < this.range) {
- // 成功
- this.infoText = this.successText;
- this.infoBoxFail = false;
- this.infoBoxShow = true;
- this.isCanSlide = false;
- this.isSuccess = true;
- // 成功后准备关闭
- clearTimeout(this.timer1);
- this.timer1 = setTimeout(() => {
- // 成功的回调
- this.isSubmting = false;
- this.$emit("success", x);
- }, 800);
- } else {
- // 失败
- this.infoText = this.failText;
- this.infoBoxFail = true;
- this.infoBoxShow = true;
- this.isCanSlide = false;
- // 失败的回调
- this.$emit("fail", x);
- // 800ms后重置
- clearTimeout(this.timer1);
- this.timer1 = setTimeout(() => {
- this.isSubmting = false;
- this.reset();
- }, 800);
- }
- },
- // 重置 - 重新设置初始状态
- resetState() {
- this.infoBoxFail = false;
- this.infoBoxShow = false;
- this.isCanSlide = false;
- this.isSuccess = false;
- this.startWidth = this.sliderBaseSize; // 鼠标点下去时父级的width
- this.startX = 0; // 鼠标按下时的X
- this.newX = 0; // 鼠标当前的偏移X
- },
- // 重置
- reset() {
- if(this.isSubmting){
- return;
- }
- this.resetState();
- this.init();
- }
- }
- });
|