演示项目-图书馆
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

477 lines
17 KiB

2 years ago
  1. <template>
  2. <div class="slide-verify" :style="{width: w + 'px'}" id="slideVerify" onselectstart="return false;">
  3. <!-- 图片加载遮蔽罩 -->
  4. <div :class="{'slider-verify-loading': loadBlock}"></div>
  5. <canvas :width="w" :height="h" ref="canvas" ></canvas>
  6. <div v-if="show" @click="refresh" class="slide-verify-refresh-icon"></div>
  7. <canvas :width="w" :height="h" ref="block" class="slide-verify-block"></canvas>
  8. <!-- container -->
  9. <div class="slide-verify-slider" :class="{'container-active': containerActive, 'container-success': containerSuccess, 'container-fail': containerFail}">
  10. <div class="slide-verify-slider-mask" :style="{width: sliderMaskWidth}">
  11. <!-- slider -->
  12. <div @mousedown="sliderDown"
  13. @touchstart="touchStartEvent"
  14. @touchmove="touchMoveEvent"
  15. @touchend="touchEndEvent"
  16. class="slide-verify-slider-mask-item"
  17. :style="{left: sliderLeft}">
  18. <div class="slide-verify-slider-mask-item-icon"></div>
  19. </div>
  20. </div>
  21. <span class="slide-verify-slider-text">{{sliderText}}</span>
  22. </div>
  23. </div>
  24. </template>
  25. <script>
  26. const PI = Math.PI;
  27. function sum(x, y) {
  28. return x + y
  29. }
  30. function square(x) {
  31. return x * x
  32. }
  33. export default {
  34. name: 'SlideVerify',
  35. props: {
  36. // block length
  37. l: {
  38. type: Number,
  39. default: 42,
  40. },
  41. // block radius
  42. r: {
  43. type: Number,
  44. default: 10,
  45. },
  46. // canvas width
  47. w: {
  48. type: Number,
  49. default: 310,
  50. },
  51. // canvas height
  52. h: {
  53. type: Number,
  54. default: 155,
  55. },
  56. sliderText: {
  57. type: String,
  58. default: '向右滑动滑块填充拼图',
  59. },
  60. accuracy: {
  61. type: Number,
  62. default: 5, // 若为 -1 则不进行机器判断
  63. },
  64. show: {
  65. type: Boolean,
  66. default: true,
  67. },
  68. imgs: {
  69. type: Array,
  70. default: () => [],
  71. },
  72. },
  73. data() {
  74. return {
  75. containerActive: false, // container active class
  76. containerSuccess: false, // container success class
  77. containerFail: false, // container fail class
  78. canvasCtx: null,
  79. blockCtx: null,
  80. block: null,
  81. block_x: undefined, // container random position
  82. block_y: undefined,
  83. L: this.l + this.r * 2 + 3, // block real lenght
  84. img: undefined,
  85. originX: undefined,
  86. originY: undefined,
  87. isMouseDown: false,
  88. trail: [],
  89. sliderLeft: 0, // block right offset
  90. sliderMaskWidth: 0, // mask width,
  91. success: false, // Bug Fixes 修复了验证成功后还能滑动
  92. loadBlock: true, // Features 图片加载提示,防止图片没加载完就开始验证
  93. timestamp: null,
  94. }
  95. },
  96. mounted() {
  97. this.init()
  98. },
  99. methods: {
  100. init() {
  101. this.initDom()
  102. this.initImg()
  103. this.bindEvents()
  104. },
  105. initDom() {
  106. this.block = this.$refs.block;
  107. this.canvasCtx = this.$refs.canvas.getContext('2d')
  108. this.blockCtx = this.block.getContext('2d')
  109. },
  110. initImg() {
  111. const img = this.createImg(() => {
  112. // 图片加载完关闭遮蔽罩
  113. this.loadBlock = false;
  114. this.drawBlock()
  115. this.canvasCtx.drawImage(img, 0, 0, this.w, this.h)
  116. this.blockCtx.drawImage(img, 0, 0, this.w, this.h)
  117. let {
  118. block_x: x,
  119. block_y: y,
  120. r,
  121. L
  122. } = this
  123. let _y = y - r * 2 - 1
  124. let ImageData = this.blockCtx.getImageData(x, _y, L, L);
  125. this.block.width = L;
  126. this.blockCtx.putImageData(ImageData, 0, _y)
  127. });
  128. this.img = img;
  129. },
  130. drawBlock() {
  131. this.block_x = this.getRandomNumberByRange(this.L + 10, this.w - (this.L + 10))
  132. this.block_y = this.getRandomNumberByRange(10 + this.r * 2, this.h - (this.L + 10))
  133. this.draw(this.canvasCtx, this.block_x, this.block_y, 'fill')
  134. this.draw(this.blockCtx, this.block_x, this.block_y, 'clip')
  135. },
  136. draw(ctx, x, y, operation) {
  137. let {
  138. l,
  139. r
  140. } = this;
  141. ctx.beginPath()
  142. ctx.moveTo(x, y)
  143. ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI)
  144. ctx.lineTo(x + l, y)
  145. ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI)
  146. ctx.lineTo(x + l, y + l)
  147. ctx.lineTo(x, y + l)
  148. ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true)
  149. ctx.lineTo(x, y)
  150. ctx.lineWidth = 2
  151. ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
  152. ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)'
  153. ctx.stroke()
  154. ctx[operation]()
  155. // Bug Fixes 修复了火狐和ie显示问题
  156. ctx.globalCompositeOperation = "destination-over"
  157. },
  158. createImg(onload) {
  159. const img = document.createElement('img');
  160. img.crossOrigin = "Anonymous";
  161. img.onload = onload;
  162. img.onerror = () => {
  163. img.src = this.getRandomImg()
  164. }
  165. img.src = this.getRandomImg()
  166. return img;
  167. },
  168. // 随机生成img src
  169. getRandomImg() {
  170. // return require('../assets/img.jpg')
  171. const len = this.imgs.length;
  172. return len > 0 ?
  173. this.imgs[this.getRandomNumberByRange(0, len)] :
  174. 'https://picsum.photos/300/150/?image=' + this.getRandomNumberByRange(0, 1084);
  175. },
  176. getRandomNumberByRange(start, end) {
  177. return Math.round(Math.random() * (end - start) + start)
  178. },
  179. refresh() {
  180. this.reset()
  181. this.$emit('refresh')
  182. },
  183. sliderDown(event) {
  184. if (this.success) return;
  185. this.originX = event.clientX;
  186. this.originY = event.clientY;
  187. this.isMouseDown = true;
  188. this.timestamp = + new Date();
  189. },
  190. touchStartEvent(e) {
  191. if (this.success) return;
  192. this.originX = e.changedTouches[0].pageX;
  193. this.originY = e.changedTouches[0].pageY;
  194. this.isMouseDown = true;
  195. this.timestamp = + new Date();
  196. },
  197. bindEvents() {
  198. document.addEventListener('mousemove', (e) => {
  199. if (!this.isMouseDown) return false;
  200. const moveX = e.clientX - this.originX;
  201. const moveY = e.clientY - this.originY;
  202. if (moveX < 0 || moveX + 38 >= this.w) return false;
  203. this.sliderLeft = moveX + 'px';
  204. let blockLeft = (this.w - 40 - 20) / (this.w - 40) * moveX;
  205. this.block.style.left = blockLeft + 'px';
  206. this.containerActive = true; // add active
  207. this.sliderMaskWidth = moveX + 'px';
  208. this.trail.push(moveY);
  209. });
  210. document.addEventListener('mouseup', (e) => {
  211. if (!this.isMouseDown) return false
  212. this.isMouseDown = false
  213. if (e.clientX === this.originX) return false;
  214. this.containerActive = false; // remove active
  215. this.timestamp = + new Date() - this.timestamp;
  216. const {
  217. spliced,
  218. TuringTest
  219. } = this.verify();
  220. if (spliced) {
  221. if(this.accuracy === -1) {
  222. this.containerSuccess = true;
  223. this.success = true;
  224. this.$emit('success', this.timestamp);
  225. return;
  226. }
  227. if (TuringTest) {
  228. // succ
  229. this.containerSuccess = true;
  230. this.success = true;
  231. this.$emit('success', this.timestamp)
  232. } else {
  233. this.containerFail = true;
  234. this.$emit('again')
  235. }
  236. } else {
  237. this.containerFail = true;
  238. this.$emit('fail')
  239. setTimeout(() => {
  240. this.reset()
  241. }, 1000)
  242. }
  243. })
  244. },
  245. touchMoveEvent(e) {
  246. if (!this.isMouseDown) return false;
  247. const moveX = e.changedTouches[0].pageX - this.originX;
  248. const moveY = e.changedTouches[0].pageY - this.originY;
  249. if (moveX < 0 || moveX + 38 >= this.w) return false;
  250. this.sliderLeft = moveX + 'px';
  251. let blockLeft = (this.w - 40 - 20) / (this.w - 40) * moveX;
  252. this.block.style.left = blockLeft + 'px';
  253. this.containerActive = true;
  254. this.sliderMaskWidth = moveX + 'px';
  255. this.trail.push(moveY);
  256. },
  257. touchEndEvent(e) {
  258. if (!this.isMouseDown) return false
  259. this.isMouseDown = false
  260. if (e.changedTouches[0].pageX === this.originX) return false;
  261. this.containerActive = false;
  262. this.timestamp = + new Date() - this.timestamp;
  263. const {
  264. spliced,
  265. TuringTest
  266. } = this.verify();
  267. if (spliced) {
  268. if(this.accuracy === -1) {
  269. this.containerSuccess = true;
  270. this.success = true;
  271. this.$emit('success', this.timestamp);
  272. return;
  273. }
  274. if (TuringTest) {
  275. // succ
  276. this.containerSuccess = true;
  277. this.success = true;
  278. this.$emit('success', this.timestamp)
  279. } else {
  280. this.containerFail = true;
  281. this.$emit('again')
  282. }
  283. } else {
  284. this.containerFail = true;
  285. this.$emit('fail')
  286. setTimeout(() => {
  287. this.reset()
  288. }, 1000)
  289. }
  290. },
  291. verify() {
  292. const arr = this.trail // drag y move distance
  293. const average = arr.reduce(sum) / arr.length // average
  294. const deviations = arr.map(x => x - average) // deviation array
  295. const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length) // standard deviation
  296. const left = parseInt(this.block.style.left)
  297. const accuracy = this.accuracy <= 1 ? 1 : this.accuracy > 10 ? 10 : this.accuracy;
  298. return {
  299. spliced: Math.abs(left - this.block_x) <= accuracy,
  300. TuringTest: average !== stddev, // equal => not person operate
  301. }
  302. },
  303. reset() {
  304. this.success = false;
  305. this.containerActive = false;
  306. this.containerSuccess = false;
  307. this.containerFail = false;
  308. this.sliderLeft = 0;
  309. this.block.style.left = 0;
  310. this.sliderMaskWidth = 0;
  311. // canvas
  312. let {
  313. w,
  314. h
  315. } = this;
  316. this.canvasCtx.clearRect(0, 0, w, h)
  317. this.blockCtx.clearRect(0, 0, w, h)
  318. this.block.width = w
  319. // generate img
  320. this.img.src = this.getRandomImg();
  321. this.$emit('fulfilled')
  322. },
  323. }
  324. }
  325. </script>
  326. <style scoped>
  327. .slide-verify {
  328. position: relative;
  329. }
  330. /* 图片加载样式 */
  331. .slider-verify-loading{
  332. position: absolute;
  333. top: 0;
  334. right: 0;
  335. left: 0;
  336. bottom: 0;
  337. background: rgba(255, 255, 255, 0.9);
  338. z-index: 999;
  339. animation: loading 1.5s infinite;
  340. }
  341. @keyframes loading {
  342. 0%{
  343. opacity: .7;
  344. }
  345. 100% {
  346. opacity: 9;
  347. }
  348. }
  349. .slide-verify-block {
  350. position: absolute;
  351. left: 0;
  352. top: 0
  353. }
  354. .slide-verify-refresh-icon {
  355. position: absolute;
  356. right: 0;
  357. top: 0;
  358. width: 34px;
  359. height: 34px;
  360. cursor: pointer;
  361. background: url("../../assets/images/login/icon_light.png") 0 -437px;
  362. background-size: 34px 471px
  363. }
  364. .slide-verify-slider {
  365. position: relative;
  366. text-align: center;
  367. width: 100%;
  368. height: 40px;
  369. line-height: 40px;
  370. margin-top: 15px;
  371. background: #f7f9fa;
  372. color: #45494c;
  373. border: 1px solid #e4e7eb
  374. }
  375. .slide-verify-slider-mask {
  376. position: absolute;
  377. left: 0;
  378. top: 0;
  379. height: 40px;
  380. border: 0 solid #1991FA;
  381. background: #D1E9FE
  382. }
  383. .slide-verify-slider-mask-item {
  384. position: absolute;
  385. top: 0;
  386. left: 0;
  387. width: 40px;
  388. height: 40px;
  389. background: #fff;
  390. box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
  391. cursor: pointer;
  392. transition: background .2s linear
  393. }
  394. .slide-verify-slider-mask-item:hover {
  395. background: #1991FA
  396. }
  397. .slide-verify-slider-mask-item:hover .slide-verify-slider-mask-item-icon {
  398. background-position: 0 -13px
  399. }
  400. .slide-verify-slider-mask-item-icon {
  401. position: absolute;
  402. top: 15px;
  403. left: 13px;
  404. width: 14px;
  405. height: 12px;
  406. background: url("../../assets/images/login/icon_light.png") 0 -26px;
  407. background-size: 34px 471px
  408. }
  409. .container-active .slide-verify-slider-mask-item {
  410. height: 38px;
  411. top: -1px;
  412. border: 1px solid #1991FA;
  413. }
  414. .container-active .slide-verify-slider-mask {
  415. height: 38px;
  416. border-width: 1px;
  417. }
  418. .container-success .slide-verify-slider-mask-item {
  419. height: 38px;
  420. top: -1px;
  421. border: 1px solid #52CCBA;
  422. background-color: #52CCBA !important;
  423. }
  424. .container-success .slide-verify-slider-mask {
  425. height: 38px;
  426. border: 1px solid #52CCBA;
  427. background-color: #D2F4EF;
  428. }
  429. .container-success .slide-verify-slider-mask-item-icon {
  430. background-position: 0 0 !important;
  431. }
  432. .container-fail .slide-verify-slider-mask-item {
  433. height: 38px;
  434. top: -1px;
  435. border: 1px solid #f57a7a;
  436. background-color: #f57a7a !important;
  437. }
  438. .container-fail .slide-verify-slider-mask {
  439. height: 38px;
  440. border: 1px solid #f57a7a;
  441. background-color: #fce1e1;
  442. }
  443. .container-fail .slide-verify-slider-mask-item-icon {
  444. top: 14px;
  445. background-position: 0 -82px !important;
  446. }
  447. .container-active .slide-verify-slider-text,
  448. .container-success .slide-verify-slider-text,
  449. .container-fail .slide-verify-slider-text {
  450. display: none;
  451. }
  452. </style>