vue-puzzle-vcode.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. // 滑动校验组件
  2. Vue.component('vue-puzzle-vcode', {
  3. props: {
  4. canvasWidth: { type: Number, default: 310 }, // 主canvas的宽
  5. canvasHeight: { type: Number, default: 160 }, // 主canvas的高
  6. // 是否出现,由父级控制
  7. show: { type: Boolean, default: false },
  8. puzzleScale: { type: Number, default: 1 }, // 拼图块的大小缩放比例
  9. sliderSize: { type: Number, default: 50 }, // 滑块的大小
  10. range: { type: Number, default: 10 }, // 允许的偏差值
  11. // 所有的背景图片
  12. imgs: {
  13. type: Array
  14. },
  15. successText: {
  16. type: String,
  17. default: "验证通过!"
  18. },
  19. failText: {
  20. type: String,
  21. default: "验证失败,请重试"
  22. },
  23. sliderText: {
  24. type: String,
  25. default: "拖动滑块完成拼图"
  26. }
  27. },
  28. data() {
  29. return {
  30. mouseDown: false, // 鼠标是否在按钮上按下
  31. startWidth: 50, // 鼠标点下去时父级的width
  32. startX: 0, // 鼠标按下时的X
  33. newX: 0, // 鼠标当前的偏移X
  34. pinX: 0, // 拼图的起始X
  35. pinY: 0, // 拼图的起始Y
  36. loading: false, // 是否正在加在中,主要是等图片onload
  37. isCanSlide: false, // 是否可以拉动滑动条
  38. error: false, // 图片加在失败会出现这个,提示用户手动刷新
  39. infoBoxShow: false, // 提示信息是否出现
  40. infoText: "", // 提示等信息
  41. infoBoxFail: false, // 是否验证失败
  42. timer1: null, // setTimout1
  43. closeDown: false, // 为了解决Mac上的click BUG
  44. isSuccess: false, // 验证成功
  45. imgIndex: -1, // 用于自定义图片时不会随机到重复的图片
  46. isSubmting: false, // 是否正在判定,主要用于判定中不能点击重置按钮
  47. resetSvg: "el-icon-refresh",
  48. };
  49. },
  50. template: '<div :class="[\'vue-puzzle-vcode\', { show_: show }]"\n' +
  51. ' @mousedown="onCloseMouseDown"\n' +
  52. ' @mouseup="onCloseMouseUp"\n' +
  53. ' @touchstart="onCloseMouseDown"\n' +
  54. ' @touchend="onCloseMouseUp">\n' +
  55. ' <div class="vue-auth-box_"\n' +
  56. ' @mousedown.stop\n' +
  57. ' @touchstart.stop>\n' +
  58. ' <div class="auth-body_"\n' +
  59. ' :style="`height: ${canvasHeight}px`">\n' +
  60. ' <!-- 主图,有缺口 -->\n' +
  61. ' <canvas ref="canvas1"\n' +
  62. ' :width="canvasWidth"\n' +
  63. ' :height="canvasHeight"\n' +
  64. ' :style="`width:${canvasWidth}px;height:${canvasHeight}px`" />\n' +
  65. ' <!-- 成功后显示的完整图 -->\n' +
  66. ' <canvas ref="canvas3"\n' +
  67. ' :class="[\'auth-canvas3_\', { show: isSuccess }]"\n' +
  68. ' :width="canvasWidth"\n' +
  69. ' :height="canvasHeight"\n' +
  70. ' :style="`width:${canvasWidth}px;height:${canvasHeight}px`" />\n' +
  71. ' <!-- 小图 -->\n' +
  72. ' <canvas :width="puzzleBaseSize"\n' +
  73. ' class="auth-canvas2_"\n' +
  74. ' :height="canvasHeight"\n' +
  75. ' ref="canvas2"\n' +
  76. ' :style="\n' +
  77. ' `width:${puzzleBaseSize}px;height:${canvasHeight}px;transform:translateX(${styleWidth -\n' +
  78. ' sliderBaseSize -\n' +
  79. ' (puzzleBaseSize - sliderBaseSize) *\n' +
  80. ' ((styleWidth - sliderBaseSize) /\n' +
  81. ' (canvasWidth - sliderBaseSize))}px)`\n' +
  82. ' " />\n' +
  83. ' <div :class="[\'loading-box_\', { hide_: !loading }]">\n' +
  84. ' <div class="loading-gif_">\n' +
  85. ' <span></span>\n' +
  86. ' <span></span>\n' +
  87. ' <span></span>\n' +
  88. ' <span></span>\n' +
  89. ' <span></span>\n' +
  90. ' </div>\n' +
  91. ' </div>\n' +
  92. ' <div :class="[\'info-box_\', { show: infoBoxShow }, { fail: infoBoxFail }]">\n' +
  93. ' {{ infoText }}\n' +
  94. ' </div>\n' +
  95. ' <div :class="[\'flash_\', { show: isSuccess }]"\n' +
  96. ' :style="\n' +
  97. ' `transform: translateX(${\n' +
  98. ' isSuccess\n' +
  99. ' ? `${canvasWidth + canvasHeight * 0.578}px`\n' +
  100. ' : `-${canvasHeight * 0.578}px`\n' +
  101. ' }) skew(-30deg, 0);`\n' +
  102. ' "></div>\n' +
  103. ' <i class="reset_" :class="resetSvg" @click="reset" />\n' +
  104. ' </div>\n' +
  105. ' <div class="auth-control_">\n' +
  106. ' <div class="range-box"\n' +
  107. ' :style="`height:${sliderBaseSize}px`">\n' +
  108. ' <div class="range-text">{{ sliderText }}</div>\n' +
  109. ' <div class="range-slider"\n' +
  110. ' ref="range-slider"\n' +
  111. ' :style="`width:${styleWidth}px`">\n' +
  112. ' <div :class="[\'range-btn\', { isDown: mouseDown }]"\n' +
  113. ' :style="`width:${sliderBaseSize}px`"\n' +
  114. ' @mousedown="onRangeMouseDown($event)"\n' +
  115. ' @touchstart="onRangeMouseDown($event)">\n' +
  116. ' <div></div>\n' +
  117. ' <div></div>\n' +
  118. ' <div></div>\n' +
  119. ' </div>\n' +
  120. ' </div>\n' +
  121. ' </div>\n' +
  122. ' </div>\n' +
  123. ' </div>\n' +
  124. ' </div>',
  125. mounted: function () {
  126. document.body.appendChild(this.$el);
  127. document.addEventListener("mousemove", this.onRangeMouseMove, false);
  128. document.addEventListener("mouseup", this.onRangeMouseUp, false);
  129. document.addEventListener("touchmove", this.onRangeMouseMove, {
  130. passive: false
  131. });
  132. document.addEventListener("touchend", this.onRangeMouseUp, false);
  133. if (this.show) {
  134. document.body.classList.add("vue-puzzle-overflow");
  135. this.reset();
  136. }
  137. },
  138. beforeDestroy() {
  139. clearTimeout(this.timer1);
  140. document.body.removeChild(this.$el);
  141. document.removeEventListener("mousemove", this.onRangeMouseMove, false);
  142. document.removeEventListener("mouseup", this.onRangeMouseUp, false);
  143. document.removeEventListener("touchmove", this.onRangeMouseMove, {
  144. passive: false
  145. });
  146. document.removeEventListener("touchend", this.onRangeMouseUp, false);
  147. },
  148. /** 监听 **/
  149. watch: {
  150. show(newV) {
  151. // 每次出现都应该重新初始化
  152. if (newV) {
  153. document.body.classList.add("vue-puzzle-overflow");
  154. this.reset();
  155. } else {
  156. this.isSubmting = false;
  157. this.isSuccess = false;
  158. this.infoBoxShow = false;
  159. document.body.classList.remove("vue-puzzle-overflow");
  160. }
  161. }
  162. },
  163. /** 计算属性 **/
  164. computed: {
  165. // styleWidth是底部用户操作的滑块的父级,就是轨道在鼠标的作用下应该具有的宽度
  166. styleWidth() {
  167. const w = this.startWidth + this.newX - this.startX;
  168. return w < this.sliderBaseSize
  169. ? this.sliderBaseSize
  170. : w > this.canvasWidth
  171. ? this.canvasWidth
  172. : w;
  173. },
  174. // 图中拼图块的60 * 用户设定的缩放比例计算之后的值 0.2~2
  175. puzzleBaseSize() {
  176. return Math.round(
  177. Math.max(Math.min(this.puzzleScale, 2), 0.2) * 52.5 + 6
  178. );
  179. },
  180. // 处理一下sliderSize,弄成整数,以免计算有偏差
  181. sliderBaseSize() {
  182. return Math.max(
  183. Math.min(
  184. Math.round(this.sliderSize),
  185. Math.round(this.canvasWidth * 0.5)
  186. ),
  187. 10
  188. );
  189. }
  190. },
  191. /** 方法 **/
  192. methods: {
  193. // 关闭
  194. onClose() {
  195. if (!this.mouseDown && !this.isSubmting) {
  196. clearTimeout(this.timer1);
  197. this.$emit("close");
  198. }
  199. },
  200. onCloseMouseDown() {
  201. this.closeDown = true;
  202. },
  203. onCloseMouseUp() {
  204. if (this.closeDown) {
  205. this.onClose();
  206. }
  207. this.closeDown = false;
  208. },
  209. // 鼠标按下准备拖动
  210. onRangeMouseDown(e) {
  211. if (this.isCanSlide) {
  212. this.mouseDown = true;
  213. this.startWidth = this.$refs["range-slider"].clientWidth;
  214. this.newX = e.clientX || e.changedTouches[0].clientX;
  215. this.startX = e.clientX || e.changedTouches[0].clientX;
  216. }
  217. },
  218. // 鼠标移动
  219. onRangeMouseMove(e) {
  220. if (this.mouseDown) {
  221. e.preventDefault();
  222. this.newX = e.clientX || e.changedTouches[0].clientX;
  223. }
  224. },
  225. // 鼠标抬起
  226. onRangeMouseUp() {
  227. if (this.mouseDown) {
  228. this.mouseDown = false;
  229. this.submit();
  230. }
  231. },
  232. /**
  233. * 开始进行
  234. * @param withCanvas 是否强制使用canvas随机作图
  235. */
  236. init(withCanvas) {
  237. // 防止重复加载导致的渲染错误
  238. if(this.loading && !withCanvas){
  239. return;
  240. }
  241. this.loading = true;
  242. this.isCanSlide = false;
  243. const c = this.$refs.canvas1;
  244. const c2 = this.$refs.canvas2;
  245. const c3 = this.$refs.canvas3;
  246. const ctx = c.getContext("2d");
  247. const ctx2 = c2.getContext("2d");
  248. const ctx3 = c3.getContext("2d");
  249. const isFirefox = navigator.userAgent.indexOf("Firefox") >= 0 && navigator.userAgent.indexOf("Windows") >= 0; // 是windows版火狐
  250. const img = document.createElement("img");
  251. ctx.fillStyle = "rgba(255,255,255,1)";
  252. ctx3.fillStyle = "rgba(255,255,255,1)";
  253. ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
  254. ctx2.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
  255. // 取一个随机坐标,作为拼图块的位置
  256. this.pinX = this.getRandom(this.puzzleBaseSize,this.canvasWidth - this.puzzleBaseSize - 20); // 留20的边距
  257. this.pinY = this.getRandom(20,this.canvasHeight - this.puzzleBaseSize - 20); // 主图高度 - 拼图块自身高度 - 20边距
  258. img.crossOrigin = "anonymous"; // 匿名,想要获取跨域的图片
  259. img.onload = () => {
  260. const [x, y, w, h] = this.makeImgSize(img);
  261. ctx.save();
  262. // 先画小图
  263. this.paintBrick(ctx);
  264. ctx.closePath();
  265. if(!isFirefox){
  266. ctx.shadowOffsetX = 0;
  267. ctx.shadowOffsetY = 0;
  268. ctx.shadowColor = "#000";
  269. ctx.shadowBlur = 3;
  270. ctx.fill();
  271. ctx.clip();
  272. } else {
  273. ctx.clip();
  274. ctx.save();
  275. ctx.shadowOffsetX = 0;
  276. ctx.shadowOffsetY = 0;
  277. ctx.shadowColor = "#000";
  278. ctx.shadowBlur = 3;
  279. ctx.fill();
  280. ctx.restore();
  281. }
  282. ctx.drawImage(img, x, y, w, h);
  283. ctx3.fillRect(0,0,this.canvasWidth,this.canvasHeight);
  284. ctx3.drawImage(img, x, y, w, h);
  285. // 设置小图的内阴影
  286. ctx.globalCompositeOperation = "source-atop";
  287. this.paintBrick(ctx);
  288. ctx.arc(
  289. this.pinX + Math.ceil(this.puzzleBaseSize / 2),
  290. this.pinY + Math.ceil(this.puzzleBaseSize / 2),
  291. this.puzzleBaseSize * 1.2,
  292. 0,
  293. Math.PI * 2,
  294. true
  295. );
  296. ctx.closePath();
  297. ctx.shadowColor = "rgba(255, 255, 255, .8)";
  298. ctx.shadowOffsetX = -1;
  299. ctx.shadowOffsetY = -1;
  300. ctx.shadowBlur = Math.min(Math.ceil(8 * this.puzzleScale), 12);
  301. ctx.fillStyle = "#ffffaa";
  302. ctx.fill();
  303. // 将小图赋值给ctx2
  304. const imgData = ctx.getImageData(
  305. this.pinX - 3, // 为了阴影 是从-3px开始截取,判定的时候要+3px
  306. this.pinY - 20,
  307. this.pinX + this.puzzleBaseSize + 5,
  308. this.pinY + this.puzzleBaseSize + 5
  309. );
  310. ctx2.putImageData(imgData, 0, this.pinY - 20);
  311. // ctx2.drawImage(c, this.pinX - 3,this.pinY - 20,this.pinX + this.puzzleBaseSize + 5,this.pinY + this.puzzleBaseSize + 5,
  312. // 0, this.pinY - 20, this.pinX + this.puzzleBaseSize + 5, this.pinY + this.puzzleBaseSize + 5);
  313. // 清理
  314. ctx.restore();
  315. ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
  316. // 画缺口
  317. ctx.save();
  318. this.paintBrick(ctx);
  319. ctx.globalAlpha = 0.8;
  320. ctx.fillStyle = "#ffffff";
  321. ctx.fill();
  322. ctx.restore();
  323. // 画缺口的内阴影
  324. ctx.save();
  325. ctx.globalCompositeOperation = "source-atop";
  326. this.paintBrick(ctx);
  327. ctx.arc(
  328. this.pinX + Math.ceil(this.puzzleBaseSize / 2),
  329. this.pinY + Math.ceil(this.puzzleBaseSize / 2),
  330. this.puzzleBaseSize * 1.2,
  331. 0,
  332. Math.PI * 2,
  333. true
  334. );
  335. ctx.shadowColor = "#000";
  336. ctx.shadowOffsetX = 2;
  337. ctx.shadowOffsetY = 2;
  338. ctx.shadowBlur = 16;
  339. ctx.fill();
  340. ctx.restore();
  341. // 画整体背景图
  342. ctx.save();
  343. ctx.globalCompositeOperation = "destination-over";
  344. ctx.drawImage(img, x, y, w, h);
  345. ctx.restore();
  346. this.loading = false;
  347. this.isCanSlide = true;
  348. };
  349. img.onerror = () => {
  350. this.init(true); // 如果图片加载错误就重新来,并强制用canvas随机作图
  351. };
  352. if (!withCanvas && this.imgs && this.imgs.length) {
  353. let randomNum = this.getRandom(0, this.imgs.length - 1);
  354. if (randomNum === this.imgIndex) {
  355. if (randomNum === this.imgs.length - 1) {
  356. randomNum = 0;
  357. } else {
  358. randomNum++;
  359. }
  360. }
  361. this.imgIndex = randomNum;
  362. img.src = this.imgs[randomNum];
  363. } else {
  364. img.src = this.makeImgWithCanvas();
  365. }
  366. },
  367. // 工具 - 范围随机数
  368. getRandom(min, max) {
  369. return Math.ceil(Math.random() * (max - min) + min);
  370. },
  371. // 工具 - 设置图片尺寸cover方式贴合canvas尺寸 w/h
  372. makeImgSize(img) {
  373. const imgScale = img.width / img.height;
  374. const canvasScale = this.canvasWidth / this.canvasHeight;
  375. let x = 0,
  376. y = 0,
  377. w = 0,
  378. h = 0;
  379. if (imgScale > canvasScale) {
  380. h = this.canvasHeight;
  381. w = imgScale * h;
  382. y = 0;
  383. x = (this.canvasWidth - w) / 2;
  384. } else {
  385. w = this.canvasWidth;
  386. h = w / imgScale;
  387. x = 0;
  388. y = (this.canvasHeight - h) / 2;
  389. }
  390. return [x, y, w, h];
  391. },
  392. // 绘制拼图块的路径
  393. paintBrick(ctx) {
  394. const moveL = Math.ceil(15 * this.puzzleScale); // 直线移动的基础距离
  395. ctx.beginPath();
  396. ctx.moveTo(this.pinX, this.pinY);
  397. ctx.lineTo(this.pinX + moveL, this.pinY);
  398. ctx.arcTo(
  399. this.pinX + moveL,
  400. this.pinY - moveL / 2,
  401. this.pinX + moveL + moveL / 2,
  402. this.pinY - moveL / 2,
  403. moveL / 2
  404. );
  405. ctx.arcTo(
  406. this.pinX + moveL + moveL,
  407. this.pinY - moveL / 2,
  408. this.pinX + moveL + moveL,
  409. this.pinY,
  410. moveL / 2
  411. );
  412. ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY);
  413. ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY + moveL);
  414. ctx.arcTo(
  415. this.pinX + moveL + moveL + moveL + moveL / 2,
  416. this.pinY + moveL,
  417. this.pinX + moveL + moveL + moveL + moveL / 2,
  418. this.pinY + moveL + moveL / 2,
  419. moveL / 2
  420. );
  421. ctx.arcTo(
  422. this.pinX + moveL + moveL + moveL + moveL / 2,
  423. this.pinY + moveL + moveL,
  424. this.pinX + moveL + moveL + moveL,
  425. this.pinY + moveL + moveL,
  426. moveL / 2
  427. );
  428. ctx.lineTo(
  429. this.pinX + moveL + moveL + moveL,
  430. this.pinY + moveL + moveL + moveL
  431. );
  432. ctx.lineTo(this.pinX, this.pinY + moveL + moveL + moveL);
  433. ctx.lineTo(this.pinX, this.pinY + moveL + moveL);
  434. ctx.arcTo(
  435. this.pinX + moveL / 2,
  436. this.pinY + moveL + moveL,
  437. this.pinX + moveL / 2,
  438. this.pinY + moveL + moveL / 2,
  439. moveL / 2
  440. );
  441. ctx.arcTo(
  442. this.pinX + moveL / 2,
  443. this.pinY + moveL,
  444. this.pinX,
  445. this.pinY + moveL,
  446. moveL / 2
  447. );
  448. ctx.lineTo(this.pinX, this.pinY);
  449. },
  450. // 用canvas随机生成图片
  451. makeImgWithCanvas() {
  452. const canvas = document.createElement("canvas");
  453. const ctx = canvas.getContext("2d");
  454. canvas.width = this.canvasWidth;
  455. canvas.height = this.canvasHeight;
  456. ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
  457. 100,
  458. 255
  459. )},${this.getRandom(100, 255)})`;
  460. ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
  461. // 随机画10个图形
  462. for (let i = 0; i < 12; i++) {
  463. ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
  464. 100,
  465. 255
  466. )},${this.getRandom(100, 255)})`;
  467. ctx.strokeStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
  468. 100,
  469. 255
  470. )},${this.getRandom(100, 255)})`;
  471. if (this.getRandom(0, 2) > 1) {
  472. // 矩形
  473. ctx.save();
  474. ctx.rotate((this.getRandom(-90, 90) * Math.PI) / 180);
  475. ctx.fillRect(
  476. this.getRandom(-20, canvas.width - 20),
  477. this.getRandom(-20, canvas.height - 20),
  478. this.getRandom(10, canvas.width / 2 + 10),
  479. this.getRandom(10, canvas.height / 2 + 10)
  480. );
  481. ctx.restore();
  482. } else {
  483. // 圆
  484. ctx.beginPath();
  485. const ran = this.getRandom(-Math.PI, Math.PI);
  486. ctx.arc(
  487. this.getRandom(0, canvas.width),
  488. this.getRandom(0, canvas.height),
  489. this.getRandom(10, canvas.height / 2 + 10),
  490. ran,
  491. ran + Math.PI * 1.5
  492. );
  493. ctx.closePath();
  494. ctx.fill();
  495. }
  496. }
  497. return canvas.toDataURL("image/png");
  498. },
  499. // 开始判定
  500. submit() {
  501. this.isSubmting = true;
  502. // 偏差 x = puzzle的起始X - (用户真滑动的距离) + (puzzle的宽度 - 滑块的宽度) * (用户真滑动的距离/canvas总宽度)
  503. // 最后+ 的是补上slider和滑块宽度不一致造成的缝隙
  504. const x = Math.abs(
  505. this.pinX -
  506. (this.styleWidth - this.sliderBaseSize) +
  507. (this.puzzleBaseSize - this.sliderBaseSize) *
  508. ((this.styleWidth - this.sliderBaseSize) /
  509. (this.canvasWidth - this.sliderBaseSize)) -
  510. 3
  511. );
  512. if (x < this.range) {
  513. // 成功
  514. this.infoText = this.successText;
  515. this.infoBoxFail = false;
  516. this.infoBoxShow = true;
  517. this.isCanSlide = false;
  518. this.isSuccess = true;
  519. // 成功后准备关闭
  520. clearTimeout(this.timer1);
  521. this.timer1 = setTimeout(() => {
  522. // 成功的回调
  523. this.isSubmting = false;
  524. this.$emit("success", x);
  525. }, 800);
  526. } else {
  527. // 失败
  528. this.infoText = this.failText;
  529. this.infoBoxFail = true;
  530. this.infoBoxShow = true;
  531. this.isCanSlide = false;
  532. // 失败的回调
  533. this.$emit("fail", x);
  534. // 800ms后重置
  535. clearTimeout(this.timer1);
  536. this.timer1 = setTimeout(() => {
  537. this.isSubmting = false;
  538. this.reset();
  539. }, 800);
  540. }
  541. },
  542. // 重置 - 重新设置初始状态
  543. resetState() {
  544. this.infoBoxFail = false;
  545. this.infoBoxShow = false;
  546. this.isCanSlide = false;
  547. this.isSuccess = false;
  548. this.startWidth = this.sliderBaseSize; // 鼠标点下去时父级的width
  549. this.startX = 0; // 鼠标按下时的X
  550. this.newX = 0; // 鼠标当前的偏移X
  551. },
  552. // 重置
  553. reset() {
  554. if(this.isSubmting){
  555. return;
  556. }
  557. this.resetState();
  558. this.init();
  559. }
  560. }
  561. });