Browse Source

🎉 init

晓晓晓晓丶vv 4 năm trước cách đây
commit
77c9f6b751

+ 3 - 0
.browserslistrc

@@ -0,0 +1,3 @@
+> 1%
+last 2 versions
+not dead

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 59 - 0
README.md

@@ -0,0 +1,59 @@
+# Vue3 Slide Verify Code
+> Vue3 滑块验证码
+
+## 参数
+
+| 参数          | 类型                              | 默认值                                 | 说明                                              |
+| ------------- | --------------------------------- | -------------------------------------- | ------------------------------------------------- |
+| containerSize | `{width: number, height: number}` | `{width: 400, height: 250}   `         | 容器大小                                          |
+| slideSize     | `number`                          | `56`                                   | 滑块卡片的大小                                    |
+| slideRadius   | `number`                          | `10`                                   | 滑块卡片的圆圈半径                                |
+| accuracy      | `number`                          | `5`                                    | 机器验证精确度,-1表示不验证是否机器              |
+| barText       | `[string, html]`                  | "<< 按住滑块,向右拖动 >>" | 滑块提示语                                        |
+| resource      | string[]                          | `[]`                                   | 图片资源,默认使用`https://picsum.photos`上的资源 |
+
+## 使用
+```html
+<template>
+  <touch-verify-code @success="onSuccess"
+                     @failed="onFailed"
+                     @refresh="onRefresh" />
+</template>
+
+<script>
+import { defineComponent } from "vue";
+import TouchVerifyCode from "@/components/TouchVerifyCode.vue";
+
+const App = defineComponent({
+  name: "App",
+  components: {
+    TouchVerifyCode,
+  },
+  setup() {
+    const onSuccess = (time: number) => {
+      console.log("验证成功", time);
+    };
+
+    const onFailed = () => {
+      console.log("验证失败");
+    };
+
+    const onRefresh = () => {
+      console.log("图片刷新成功")
+    }
+
+    return { onSuccess, onFailed, onRefresh };
+  },
+});
+
+export default App;
+</script>
+```
+
+## 效果
+
+[效果展示](http://normal-image.xiaovv-web.com/normal/2020-12-08-verify-code-gif.gif)
+
+## TODO
+ - npm
+ - 测试

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

+ 6 - 0
jest.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
+  transform: {
+    '^.+\\.vue$': 'vue-jest'
+  }
+}

+ 27 - 0
package.json

@@ -0,0 +1,27 @@
+{
+  "name": "vue3_touch_code",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "test:unit": "vue-cli-service test:unit"
+  },
+  "dependencies": {
+    "core-js": "^3.6.5",
+    "vue": "^3.0.0"
+  },
+  "devDependencies": {
+    "@types/jest": "^24.0.19",
+    "@vue/cli-plugin-babel": "~4.5.0",
+    "@vue/cli-plugin-typescript": "~4.5.0",
+    "@vue/cli-plugin-unit-jest": "~4.5.0",
+    "@vue/cli-service": "~4.5.0",
+    "@vue/compiler-sfc": "^3.0.0",
+    "@vue/test-utils": "^2.0.0-0",
+    "sass": "^1.26.5",
+    "sass-loader": "^8.0.2",
+    "typescript": "~3.9.3",
+    "vue-jest": "^5.0.0-0"
+  }
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 40 - 0
public/fonts/iconfont.css


+ 14 - 0
public/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+    <link rel="stylesheet" href="<%= BASE_URL %>fonts/iconfont.css" />
+    <title><%= htmlWebpackPlugin.options.title %></title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 29 - 0
src/App.vue

@@ -0,0 +1,29 @@
+<template>
+  <touch-verify-code @success="onSuccess"
+                     @failed="onFailed" />
+</template>
+
+<script lang="ts">
+import { defineComponent } from "vue";
+import TouchVerifyCode from "@/components/TouchVerifyCode.vue";
+
+const App = defineComponent({
+  name: "App",
+  components: {
+    TouchVerifyCode,
+  },
+  setup() {
+    const onSuccess = (time: number) => {
+      console.log("验证成功", time);
+    };
+
+    const onFailed = () => {
+      console.log("验证失败");
+    };
+
+    return { onSuccess, onFailed };
+  },
+});
+
+export default App;
+</script>

BIN
src/assets/verify_code_bg.jpg


+ 542 - 0
src/components/TouchVerifyCode.vue

@@ -0,0 +1,542 @@
+<template>
+  <div class="verify-code-wrap"
+       :style="{ width: containerSize.width + 'px' }">
+    <div class="verify-canvas-container">
+      <canvas :width="containerSize.width"
+              :height="containerSize.height"
+              ref="container" />
+      <canvas :width="containerSize.width"
+              :height="containerSize.height"
+              ref="slide"
+              class="verify-slide-block" />
+      <span :class="['verify-refresh', {'verify-refresh__progress': !verifyImageLoaded}]"
+            @click="onRefresh">
+        <i class="icon-shuaxin"></i>
+      </span>
+      <transition name="slide">
+        <div class="verify-time"
+             v-show="verifySuccess">验证成功~本次验证共计{{ verifyUse }}秒</div>
+      </transition>
+      <transition name="fade">
+        <div v-if="!verifyImageLoaded"
+             class="verify-loading-image">图片加载中...请稍等</div>
+      </transition>
+    </div>
+    <div :class="['touch-verify-bar', {'touch-verify-bar__progress': verifying, 'touch-verify-bar__success': verifySuccess, 'touch-verify-bar__failed': verifyFailed}]">
+      <div class="touch-verify-slide-bar"
+           :style="{ width: verifyProgressWidth }">
+        <div class="touch-verify-slide-block"
+             :style="{ left: blockLeftValue }"
+             @mousedown="onBlockTouch">
+          <i v-if="verifySuccess"
+             class="icon-gouxuan"></i>
+          <i v-else-if="verifyFailed"
+             class="icon-guanbi"></i>
+          <i v-else
+             class="icon-youjiantou"></i>
+        </div>
+      </div>
+      <p v-show="showVerifyBarText"
+         v-html="barText" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import {
+  computed,
+  defineComponent,
+  onMounted,
+  onUnmounted,
+  PropType,
+  reactive,
+  ref,
+} from "vue";
+
+import useCanvasApi from "@/hooks/useCanvasApi";
+import useImage from "@/hooks/useImage";
+
+import { calculate, onCreateRandomRange, square } from "@/helper/index";
+import useMouseEvent from "../hooks/useMouseEvent";
+
+interface IContainer {
+  width: number;
+  height: number;
+}
+
+const BLOCK_SIZE = 48;
+
+const TouchVerifyCode = defineComponent({
+  name: "TouchVerifyCode",
+  props: {
+    containerSize: {
+      type: Object as PropType<IContainer>,
+      default: {
+        width: 400,
+        height: 250,
+      },
+    },
+    slideSize: {
+      type: Number,
+      default: 56,
+    },
+    slideRadius: {
+      type: Number,
+      default: 10,
+    },
+    accuracy: {
+      type: Number,
+      default: 5,
+    },
+    barText: {
+      type: String,
+      default: "&lt;&lt; 按住滑块,向右拖动 &gt;&gt;",
+    },
+    resource: {
+      type: Object as PropType<string[]>,
+      default: [],
+    },
+  },
+  emits: ["success", "failed", "after-reset", "refresh"],
+  setup(props, { emit }) {
+    const {
+      resource,
+      slideSize: size,
+      slideRadius: radius,
+      containerSize: { width, height },
+    } = props;
+    const { onDraw } = useCanvasApi(size, radius);
+    const { onImageCreate, onCreateRandomImageByNets } = useImage(
+      resource,
+      width,
+      height
+    );
+
+    // 容器
+    let container = ref<HTMLCanvasElement | null>(null);
+    let slide = ref<HTMLCanvasElement | null>(null);
+    let con_ctx = ref<CanvasRenderingContext2D | null>(null);
+    let slide_ctx = ref<CanvasRenderingContext2D | null>(null);
+
+    // 滑块验证相关
+    let verifyImage = ref<HTMLImageElement | null>(null);
+    let verifyImageLoaded = ref(false);
+    let verifying = ref(false);
+    let verifySuccess = ref(false);
+    let verifyFailed = ref(false);
+    let verifyProgressWidth = ref("0px");
+    let verifyStart = ref(false);
+    let verifyCoordinate = reactive({ x: 0, y: 0 });
+    let verifyTimestamp = ref<number>(0);
+    let verifyUse = computed(() => verifyTimestamp.value / 1000);
+    let verifyTrailArr = ref<number[]>([]);
+    let showVerifyBarText = computed(
+      () => !verifyStart.value && !verifySuccess.value && !verifyFailed.value
+    );
+
+    let blockLeftValue = ref("0px");
+
+    let slideSize = size + radius * 2 + 3;
+
+    const slideOptions = reactive({
+      x: 0,
+      y: 0,
+    });
+
+    const onDomInit = () => {
+      con_ctx.value = <CanvasRenderingContext2D>(
+        container.value?.getContext("2d")
+      );
+      slide_ctx.value = <CanvasRenderingContext2D>slide.value?.getContext("2d");
+    };
+
+    const onImageInit = () => {
+      const image = onImageCreate(() => {
+        verifyImageLoaded.value = true;
+        onSlideDrew();
+        con_ctx.value?.drawImage(image, 0, 0, width, height);
+        slide_ctx.value?.drawImage(image, 0, 0, width, height);
+        const _y = slideOptions.y - radius * 2 - 1;
+        const ImageData = slide_ctx.value!.getImageData(
+          slideOptions.x,
+          _y,
+          slideSize,
+          slideSize
+        );
+        slide.value!.width = slideSize;
+        slide_ctx.value?.putImageData(ImageData, 0, _y);
+      });
+      verifyImage.value = image;
+    };
+
+    // 绘制卡片块
+    const onSlideDrew = () => {
+      const size = slideSize + 10;
+      slideOptions.x = onCreateRandomRange(size, width - size);
+      slideOptions.y = onCreateRandomRange(radius * 2 + 10, height - size);
+
+      onDraw(con_ctx.value!, slideOptions.x, slideOptions.y, "fill");
+      onDraw(slide_ctx.value!, slideOptions.x, slideOptions.y, "clip");
+    };
+
+    // 设置状态,起始坐标
+    const onBlockTouch = (event: MouseEvent) => {
+      if (verifySuccess.value) return false;
+      if (!verifyImageLoaded.value) return false;
+      verifyCoordinate.x = event.clientX;
+      verifyCoordinate.y = event.clientY;
+      verifyTimestamp.value = +new Date();
+      verifyStart.value = true;
+    };
+
+    const onBlockMove = (event: MouseEvent) => {
+      if (!verifyStart.value) return false;
+      const verifyMoveX = event.clientX - verifyCoordinate.x;
+      const verifyMoveY = event.clientY - verifyCoordinate.y;
+      if (verifyMoveX < 0 || verifyMoveX + BLOCK_SIZE >= width) return false;
+      const moveStr = verifyMoveX + "px";
+      const leftValue =
+        ((width - 50 - radius * 2) / (width - 50)) * verifyMoveX;
+      verifying.value = true;
+      slide.value!.style.left = leftValue + "px";
+      blockLeftValue.value = verifyMoveX - 2 + "px";
+      verifyProgressWidth.value = moveStr;
+      verifyTrailArr.value.push(verifyMoveX);
+    };
+
+    // 当离开滑块时进行正确性的判断
+    const onBlockTouchLeave = (event: MouseEvent) => {
+      if (!verifyStart.value) return false;
+      verifyStart.value = false;
+      if (event.clientX === verifyCoordinate.x) return false;
+      verifying.value = false;
+      verifyTimestamp.value = +new Date() - verifyTimestamp.value!;
+
+      const { correct, userOperator } = onVerify();
+
+      if (correct) {
+        // 跳过人机验证
+        if (props.accuracy === -1) return onVerifySuccess();
+        // 如果是人机 则需重新验证
+        if (userOperator) return onVerifySuccess();
+        else return onVerifyFailed();
+      } else onVerifyFailed();
+    };
+
+    // 边界处理 防止鼠标拖动过程中移出滑块之外
+    const onBlockOutside = (event: MouseEvent) => {
+      if (!verifyStart.value) return false;
+      onVerifyFailed();
+    };
+
+    // 成功
+    const onVerifySuccess = () => {
+      verifySuccess.value = true;
+      emit("success", verifyUse.value);
+    };
+
+    // 失败
+    const onVerifyFailed = () => {
+      verifyFailed.value = true;
+      setTimeout(onReset, 1000);
+      emit("failed");
+    };
+
+    // 重置所有滑块相关的状态
+    const onReset = () => {
+      verifyStart.value = false;
+      verifyImageLoaded.value = false;
+      verifying.value = false;
+      verifySuccess.value = false;
+      verifyFailed.value = false;
+      verifyProgressWidth.value = "0px";
+      slide.value!.style.left = "0px";
+      blockLeftValue.value = "0px";
+      con_ctx.value?.clearRect(0, 0, width, height);
+      slide_ctx.value?.clearRect(0, 0, width, height);
+      slide.value!.width = width;
+
+      verifyImage.value!.src = onCreateRandomImageByNets();
+      emit("after-reset");
+    };
+
+    // 验证逻辑
+    const onVerify = () => {
+      const verifyArr = verifyTrailArr.value.slice(0);
+      // 计算平均值
+      const avg = verifyArr.reduce(calculate) / verifyArr.length;
+      // 计算偏差值
+      const deviations = verifyArr.map((x) => x - avg);
+      // 标准偏差值
+      const standard_deviations = Math.sqrt(
+        deviations.map(square).reduce(calculate) / verifyArr.length
+      );
+      const blockMoveX = parseInt(slide.value!.style.left);
+      const accuracy =
+        props.accuracy <= 1 ? 1 : props.accuracy >= 10 ? 10 : props.accuracy;
+
+      return {
+        correct: Math.abs(blockMoveX - slideOptions.x) <= accuracy,
+        userOperator: avg !== standard_deviations,
+      };
+    };
+
+    const onRefresh = () => {
+      onReset();
+      emit("refresh");
+    };
+
+    const { initEvent } = useMouseEvent(onBlockMove, onBlockTouchLeave);
+
+    const init = () => {
+      onDomInit();
+      onImageInit();
+      initEvent();
+    };
+
+    onMounted(init);
+
+    return {
+      container,
+      slide,
+      verifying,
+      verifyStart,
+      verifySuccess,
+      verifyFailed,
+      verifyProgressWidth,
+      verifyImageLoaded,
+      verifyUse,
+      showVerifyBarText,
+      blockLeftValue,
+      onBlockTouch,
+      onBlockMove,
+      onBlockTouchLeave,
+      onBlockOutside,
+      onRefresh,
+    };
+  },
+});
+
+export default TouchVerifyCode;
+</script>
+
+<style lang="scss" scoped>
+.verify-code-wrap {
+  position: relative;
+  font-size: 0;
+
+  .verify-canvas-container {
+    position: relative;
+    border-radius: 4px;
+    overflow: hidden;
+
+    .verify-refresh {
+      position: absolute;
+      right: 5px;
+      top: 5px;
+      width: 30px;
+      height: 30px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      border-radius: 4px;
+      background: rgba(0, 0, 0, 0.3);
+      cursor: pointer;
+      z-index: 99;
+
+      i {
+        font-size: 24px;
+        color: #fff;
+      }
+
+      &__progress {
+        i {
+          animation: rotate 1s linear infinite;
+        }
+      }
+    }
+
+    .verify-time {
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      width: 100%;
+      height: 35px;
+      line-height: 35px;
+      font-size: 12px;
+      font-weight: bold;
+      text-align: center;
+      color: #fff;
+      background: #52ccba;
+    }
+
+    .verify-loading-image {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background: rgba(255, 255, 255, 0.8);
+      z-index: 999;
+      color: #666;
+      font-size: 14px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+  }
+
+  .verify-slide-block {
+    position: absolute;
+    left: 0;
+    top: 0;
+  }
+
+  .touch-verify-bar {
+    margin-top: 10px;
+    position: relative;
+    width: 100%;
+    height: 50px;
+    line-height: 50px;
+    color: #45494c;
+    box-sizing: border-box;
+    border: 1px solid #e4e7eb;
+    border-radius: 2px;
+    background: #f7f9fa;
+    text-align: center;
+
+    .touch-verify-slide-bar {
+      position: absolute;
+      left: 0;
+      top: 0;
+      height: 50px;
+      box-sizing: border-box;
+      border: 0 solid #1991fa;
+      background: #d1e9fe;
+    }
+
+    .touch-verify-slide-block {
+      position: absolute;
+      top: 0px;
+      left: 0px;
+      width: 48px;
+      height: 48px;
+      background: #fff;
+      box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
+      transition: background 0.2s linear;
+      cursor: pointer;
+      z-index: 99;
+
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+      i {
+        font-size: 24px;
+        font-weight: bold;
+      }
+
+      &:hover {
+        background: #1991fa;
+
+        i {
+          color: #fff;
+        }
+      }
+    }
+
+    &__failed {
+      .touch-verify-slide-block {
+        top: -1px;
+        border: 1px solid #f57a7a;
+        background: #f57a7a;
+
+        &:hover {
+          background: #f57a7a;
+        }
+
+        i {
+          color: #fff;
+        }
+      }
+
+      .touch-verify-slide-bar {
+        border-width: 1px;
+        border-color: #f57a7a;
+        background: #fce1e1;
+      }
+    }
+
+    &__success {
+      .touch-verify-slide-block {
+        top: -1px;
+        border: 1px solid #52ccba;
+        background: #52ccba;
+
+        &:hover {
+          background: #52ccba;
+        }
+
+        i {
+          color: #fff;
+        }
+      }
+
+      .touch-verify-slide-bar {
+        border-width: 1px;
+        border-color: #52ccba;
+        background: #d2f4ef;
+      }
+    }
+
+    &__progress {
+      .touch-verify-slide-block {
+        top: -1px;
+        border: 1px solid #1991fa;
+        background: #1991fa;
+      }
+
+      .touch-verify-slide-bar {
+        border-width: 1px;
+      }
+    }
+
+    p {
+      user-select: none;
+      margin: 0;
+      line-height: 50px;
+      font-size: 14px;
+      color: #666;
+    }
+  }
+}
+
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.3s;
+}
+
+.fade-enter-from,
+.fade-leave-active {
+  opacity: 0;
+}
+
+.slide-leave-active,
+.slide-enter-active {
+  transition: all 0.5s;
+}
+
+.slide-enter-from {
+  transform: translateY(100%);
+}
+
+.slide-leave-to {
+  transform: translateY(0%);
+}
+
+@keyframes rotate {
+  to {
+    transform: rotate(360deg);
+  }
+}
+</style>

+ 7 - 0
src/helper/index.ts

@@ -0,0 +1,7 @@
+export const onCreateRandomRange = (start: number, end: number) => {
+  return Math.round(Math.random() * (end - start) + start);
+};
+
+export const calculate = (x: number, y: number) => x + y;
+
+export const square = (x: number) => Math.pow(x, 2);

+ 45 - 0
src/hooks/useCanvasApi.ts

@@ -0,0 +1,45 @@
+const useCanvasApi = (size: number, radius: number) => {
+  const pi = Math.PI;
+
+  const onDraw = (
+    ctx: CanvasRenderingContext2D,
+    x: number,
+    y: number,
+    operator: "clip" | "fill"
+  ) => {
+    ctx.beginPath();
+    ctx.moveTo(x, y);
+    ctx.arc(x + size / 2, y - radius + 2, radius, 0.72 * pi, 2.26 * pi);
+    ctx.lineTo(x + size, y);
+    ctx.arc(x + size + radius - 2, y + size / 2, radius, 1.21 * pi, 2.78 * pi);
+    ctx.lineTo(x + size, y + size);
+    ctx.lineTo(x, y + size);
+    ctx.arc(
+      x + radius - 2,
+      y + size / 2,
+      radius + 0.4,
+      2.76 * pi,
+      1.24 * pi,
+      true
+    );
+    ctx.lineTo(x, y);
+    ctx.lineWidth = 2;
+    ctx.fillStyle = "rgba(255, 255, 255, .5)";
+    ctx.strokeStyle = "rgba(255, 255, 255, .5)";
+    // 制造阴影
+    if (operator === "clip") {
+      ctx.shadowOffsetX = 2;
+      ctx.shadowOffsetY = 2;
+      ctx.shadowBlur = 2;
+      ctx.shadowColor = "rgba(255, 255, 255, .6)";
+    }
+    ctx.stroke();
+    ctx[operator]();
+
+    ctx.globalCompositeOperation = "destination-over";
+  };
+
+  return { onDraw };
+};
+
+export default useCanvasApi;

+ 29 - 0
src/hooks/useImage.ts

@@ -0,0 +1,29 @@
+import { onCreateRandomRange } from "@/helper";
+
+const useImage = (resource: string[], width: number, height: number) => {
+  const onImageCreate = (
+    load: ((this: GlobalEventHandlers, ev: Event) => any) | null
+  ) => {
+    const image = document.createElement("img");
+    image.crossOrigin = "Anonymous";
+    image.onload = load;
+    image.onerror = () => (image.src = onCreateRandomImageByNets());
+    image.src = onCreateRandomImageByNets();
+    return image;
+  };
+
+  // 获取随机图片
+  const onCreateRandomImageByNets = () => {
+    let source = "";
+    const resourceLength = resource.length;
+    const IMAGE_RESOURCE = `https://picsum.photos/${width}/${height}/?&image=`;
+    if (resourceLength)
+      source = resource[onCreateRandomRange(0, resourceLength)];
+    else source = IMAGE_RESOURCE + onCreateRandomRange(0, 1024);
+    return source;
+  };
+
+  return { onImageCreate, onCreateRandomImageByNets };
+};
+
+export default useImage;

+ 21 - 0
src/hooks/useMouseEvent.ts

@@ -0,0 +1,21 @@
+import { onUnmounted } from "vue";
+
+interface IMouseEvent {
+  (event: MouseEvent): void;
+}
+
+const useMouseEvent = (move: IMouseEvent, leave: IMouseEvent) => {
+  const initEvent = () => {
+    document.addEventListener("mousemove", move, false);
+    document.addEventListener("mouseup", leave, false);
+  };
+
+  onUnmounted(() => {
+    document.removeEventListener("mousemove", move, false);
+    document.removeEventListener("mouseup", leave, false);
+  });
+
+  return { initEvent };
+};
+
+export default useMouseEvent;

+ 4 - 0
src/main.ts

@@ -0,0 +1,4 @@
+import { createApp } from "vue";
+import App from "./App.vue";
+
+createApp(App).mount("#app");

+ 5 - 0
src/shims-vue.d.ts

@@ -0,0 +1,5 @@
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}

+ 12 - 0
tests/unit/example.spec.ts

@@ -0,0 +1,12 @@
+import { shallowMount } from '@vue/test-utils'
+import HelloWorld from '@/components/HelloWorld.vue'
+
+describe('HelloWorld.vue', () => {
+  it('renders props.msg when passed', () => {
+    const msg = 'new message'
+    const wrapper = shallowMount(HelloWorld, {
+      props: { msg }
+    })
+    expect(wrapper.text()).toMatch(msg)
+  })
+})

+ 40 - 0
tsconfig.json

@@ -0,0 +1,40 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "module": "esnext",
+    "strict": true,
+    "jsx": "preserve",
+    "importHelpers": true,
+    "moduleResolution": "node",
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "sourceMap": true,
+    "baseUrl": ".",
+    "types": [
+      "webpack-env",
+      "jest"
+    ],
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    },
+    "lib": [
+      "esnext",
+      "dom",
+      "dom.iterable",
+      "scripthost"
+    ]
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "tests/**/*.ts",
+    "tests/**/*.tsx"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 10206 - 0
yarn.lock