Vite 构建 Vue3 组件库之路: 行为验证码-滑块拼图组件

1,077 阅读2分钟

示例

image.png

需求分析

在已有滑块组件的基础上,实现拼图验证组件,主要需求如下:

  1. 拼图组件 DOM 结构基于 canvas 实现,具体如下:
    • canvas1 用于绘制背景图,作为拼图的底图,展示完整的图像。
    • canvas2 用于绘制抠图,即拼图中需要拖动拼合的部分。
  2. 拼图组件的属性
    • 背景图的 img :指定背景图的图片路径,用于在 canvas1 上绘制。
    • 抠图的 img :指定抠图的图片路径,在 canvas2 上绘制。
    • 抠图的 width :定义抠图的宽度,默认值为 40,可根据实际需求调整。
    • 抠图的 Y 轴坐标 :确定抠图在背景图中的垂直位置,默认值为 0。
  3. 拼图组件的事件和方法
    • 移动结束的事件 :当用户完成拖动拼图块的操作时触发,传递滑块的状态信息。
    • 重置的方法 :用于将拼图组件恢复到初始状态,如将拼图块移回起始位置等。

实现细节

定义props

export interface PuzzleVerifyProps {
  puzzleImg: string;
  blockImg: string;
  blockY?: number;
}

定义 emits

export interface PuzzleVerifyEmits {
  (e: "moveEnd", state: SliderState, scaleX: number, scaleY: number): void;
}

定义expose

export interface PuzzleVerifyExpose {
  reset: () => void;
}

编写template

<template>
  <div ref="puzzle" :class="classes">
    <canvas ref="puzzleCanvas" class="puzzle"></canvas>
    <canvas ref="blockCanvas" class="block"></canvas>
    <LdSliderVerify
      ref="slide"
      :draggable="true"
      @thumbEnd="thumbEnd"
      @thumbMove="thumbMove"
    />
  </div>
</template>

组件样式

// config
@use "../../styles/variables" as *;

.ld-puzzle-verify {
  position: relative;

  .puzzle {
    width: 100%;
    height: 100%;
  }

  .block {
    position: absolute;
    top: 0;
    left: 0;
  }
}

组件源码

<script lang="ts" setup>
import { computed, onMounted, ref, watch } from "vue";
import classNames from "classnames";
import LdSliderVerify, { type SliderState } from "./SliderVerify.vue";

export interface PuzzleVerifyProps {
  puzzleImg: string;
  blockImg: string;
  blockY?: number;
}

export interface PuzzleVerifyEmits {
  (e: "moveEnd", state: SliderState, scaleX: number, scaleY: number): void;
}

export interface PuzzleVerifyExpose {
  reset: () => void;
}

defineOptions({
  name: "LdPuzzleVerify",
});

const slide = ref<typeof LdSliderVerify | null>(null);
const puzzle = ref<HTMLDivElement | null>(null);
const puzzleCanvas = ref<HTMLCanvasElement | null>(null);
const blockCanvas = ref<HTMLCanvasElement | null>(null);
const puzzleCtx = ref<CanvasRenderingContext2D | null>(null);
const blockCtx = ref<CanvasRenderingContext2D | null>(null);
const scaleX = ref<number>(1);
const scaleY = ref<number>(1);
const props = withDefaults(defineProps<PuzzleVerifyProps>(), {
  blockY: 0,
});
const classes = computed(() => {
  return classNames("ld-puzzle-verify");
});

const emits = defineEmits<PuzzleVerifyEmits>();

const loadImage = (src: string) => {
  return new Promise<HTMLImageElement>((resolve, reject) => {
    const img = new Image();
    img.src = src;
    img.onload = () => resolve(img);
    img.onerror = reject;
  });
};

const drawImage = () => {
  if (
    !puzzleCtx.value ||
    !blockCtx.value ||
    !props.puzzleImg ||
    !props.blockImg
  )
    return;
  Promise.all([loadImage(props.puzzleImg), loadImage(props.blockImg)]).then(
    (res) => {
      const [puzzleImg, blockImg] = res;
      // 宽高比
      const aspectRatio = puzzleImg.width / puzzleImg.height;
      puzzleCanvas.value!.width = puzzle.value!.offsetWidth;
      puzzleCanvas.value!.height = puzzle.value!.offsetWidth / aspectRatio;
      // 缩放比
      scaleX.value = puzzleCanvas.value!.width / puzzleImg.width;
      scaleY.value = puzzleCanvas.value!.height / puzzleImg.height;

      blockCanvas.value!.width = blockImg.width * scaleX.value;
      blockCanvas.value!.height = puzzleCanvas.value!.height;

      puzzleCtx.value!.drawImage(
        puzzleImg,
        0,
        0,
        puzzleImg.width,
        puzzleImg.height,
        0,
        0,
        puzzleCanvas.value!.width,
        puzzleCanvas.value!.height,
      );
      blockCtx.value!.drawImage(
        blockImg,
        0,
        0,
        blockImg.width,
        blockImg.height,
        0,
        props.blockY * scaleY.value,
        blockImg.width * scaleX.value,
        blockImg.height * scaleY.value,
      );
    },
  );
};

const thumbMove = (thumbState: SliderState) => {
  blockCanvas.value!.style.left = thumbState.moveX + "px";
};

const thumbEnd = (thumbState: SliderState) => {
  emits("moveEnd", thumbState, scaleX.value, scaleY.value);
};
const reset = () => {
  blockCanvas.value!.style.left = "0px";
  slide.value!.reset();
};
onMounted(() => {
  puzzleCtx.value = puzzleCanvas.value!.getContext("2d");
  blockCtx.value = blockCanvas.value!.getContext("2d");
  drawImage();
});
watch(() => props.puzzleImg, drawImage);

defineExpose<PuzzleVerifyExpose>({
  reset,
});
</script>

感谢阅读,敬请斧正!