CaptchaCanvas.vue 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. <template>
  2. <div class="cap-wrap" @click="refresh" :title="'点击刷新验证码'">
  3. <canvas ref="cv"></canvas>
  4. </div>
  5. </template>
  6. <script>
  7. export default {
  8. name: "CaptchaCanvas",
  9. props: { value: String },
  10. data() { return { code: "" }; },
  11. mounted() {
  12. this.refresh();
  13. if (typeof ResizeObserver !== "undefined") {
  14. this.ro = new ResizeObserver(() => this.draw());
  15. this.ro.observe(this.$el);
  16. }
  17. },
  18. beforeDestroy() {
  19. if (this.ro) { this.ro.disconnect(); this.ro = null; }
  20. },
  21. methods: {
  22. randChar() {
  23. const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
  24. return chars[Math.floor(Math.random() * chars.length)];
  25. },
  26. refresh() {
  27. this.code = Array.from({ length: 4 }).map(() => this.randChar()).join("");
  28. this.$emit("input", this.code);
  29. this.draw();
  30. },
  31. draw() {
  32. const cv = this.$refs.cv;
  33. if (!cv) return;
  34. const wrap = this.$el;
  35. const w = wrap.clientWidth || 120;
  36. const h = wrap.clientHeight || 60;
  37. const dpr = window.devicePixelRatio || 1;
  38. cv.width = Math.round(w * dpr);
  39. cv.height = Math.round(h * dpr);
  40. cv.style.width = w + "px";
  41. cv.style.height = h + "px";
  42. const ctx = cv.getContext("2d");
  43. ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  44. // 背景
  45. ctx.clearRect(0, 0, w, h);
  46. const g = ctx.createLinearGradient(0, 0, w, h);
  47. g.addColorStop(0, "rgba(14,45,120,0.65)");
  48. g.addColorStop(1, "rgba(8,20,55,0.85)");
  49. ctx.fillStyle = g;
  50. ctx.fillRect(0, 0, w, h);
  51. // 干扰线
  52. for (let i = 0; i < 5; i++) {
  53. ctx.strokeStyle = `rgba(60,180,255,${0.15 + Math.random() * 0.25})`;
  54. ctx.beginPath();
  55. ctx.moveTo(Math.random() * w, Math.random() * h);
  56. ctx.lineTo(Math.random() * w, Math.random() * h);
  57. ctx.stroke();
  58. }
  59. // 字符:按容器宽高等分布点 + 字号随高度缩放
  60. const n = this.code.length;
  61. const fontSize = Math.max(12, Math.round(h * 0.55));
  62. ctx.font = `bold ${fontSize}px Arial`;
  63. ctx.textBaseline = "middle";
  64. ctx.textAlign = "center";
  65. for (let i = 0; i < n; i++) {
  66. const x = (w * (i + 0.5)) / n;
  67. const y = h / 2 + (Math.random() * 6 - 4);
  68. ctx.save();
  69. ctx.translate(x, y);
  70. ctx.rotate(((Math.random() * 14 - 7) * Math.PI) / 180);
  71. ctx.fillStyle = "rgba(210,245,255,0.92)";
  72. ctx.fillText(this.code[i], 0, 0);
  73. ctx.restore();
  74. }
  75. }
  76. }
  77. };
  78. </script>
  79. <style scoped>
  80. .cap-wrap {
  81. width: 100%;
  82. height: 100%;
  83. border: 1px solid #3D6EB8;
  84. border-radius: 8px;
  85. overflow: hidden;
  86. cursor: pointer;
  87. box-sizing: border-box;
  88. }
  89. canvas { display: block; }
  90. </style>