Prechádzať zdrojové kódy

fix: 招商页面调整

周玉环 2 dní pred
rodič
commit
47c0043853

+ 8 - 0
xinkeaboard-promotion-portal/components.d.ts

@@ -18,11 +18,17 @@ declare module 'vue' {
     CountrySelct: typeof import('./src/components/CountrySelct.vue')['default']
     DifficultyBar: typeof import('./src/components/DifficultyBar.vue')['default']
     ElButton: typeof import('element-plus/es')['ElButton']
+    ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDropdown: typeof import('element-plus/es')['ElDropdown']
     ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
     ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
+    ElForm: typeof import('element-plus/es')['ElForm']
+    ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
+    ElOption: typeof import('element-plus/es')['ElOption']
+    ElSelect: typeof import('element-plus/es')['ElSelect']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
@@ -33,6 +39,7 @@ declare module 'vue' {
     Level: typeof import('./src/components/Level.vue')['default']
     LineChart: typeof import('./src/components/competitor/LineChart.vue')['default']
     List: typeof import('./src/components/competitor/list.vue')['default']
+    LoginDialog: typeof import('./src/components/LoginDialog.vue')['default']
     ProductDescription: typeof import('./src/components/ProductDescription.vue')['default']
     RankTable: typeof import('./src/components/competitor/RankTable.vue')['default']
     RecommendTable: typeof import('./src/components/competitor/RecommendTable.vue')['default']
@@ -40,6 +47,7 @@ declare module 'vue' {
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     Search: typeof import('./src/components/keyword/search.vue')['default']
+    SliderVerify: typeof import('./src/components/SliderVerify.vue')['default']
     Table: typeof import('./src/components/keyword/table.vue')['default']
     TopContent: typeof import('./src/components/TopContent.vue')['default']
     TopHeaderContent: typeof import('./src/components/TopHeaderContent.vue')['default']

BIN
xinkeaboard-promotion-portal/src/assets/images/login/Code.png


BIN
xinkeaboard-promotion-portal/src/assets/images/login/Phone.png


BIN
xinkeaboard-promotion-portal/src/assets/images/login/User.png


BIN
xinkeaboard-promotion-portal/src/assets/images/refresh.png


+ 318 - 0
xinkeaboard-promotion-portal/src/components/LoginDialog.vue

@@ -0,0 +1,318 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    class="confirm-dialog"
+    title="信息确认"
+    center
+    destroy-on-close
+    :close-on-click-modal="false"
+    @closed="closed"
+  >
+    <div class="confirm-dialog-tip">
+      <span>每个手机号每月只能生成 3 次出海分析报告。 </span>
+      <span>为确保报告准确送达,请填写以下信息(全程加密处理),获得完整商情报告。</span>
+    </div>
+    <div class="confirm-dialog-content">
+      <el-form ref="ruleFormRef" :model="form" :rules="rules">
+        <el-form-item prop="companyName">
+          <el-input v-model="form.companyName" placeholder="请输入公司名称" clearable>
+            <template #prefix>
+              <img :src="User" />
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item prop="phone">
+          <el-input v-model.number="form.phone" placeholder="请输入手机号" clearable>
+            <template #prefix>
+              <img :src="Phone" />
+            </template>
+          </el-input>
+        </el-form-item>
+        <el-form-item prop="verifyCode">
+          <el-input v-model="form.verifyCode" placeholder="请输入验证码">
+            <template #prefix>
+              <img :src="Code" />
+            </template>
+            <template #suffix>
+              <el-button
+                :disabled="!!countDownNumer"
+                :loading="getVerifyCodeLoading"
+                @click="acceptCode"
+                ><span>{{ codeText }}</span>
+                <span v-if="countDownNumer">{{ `(${countDownNumer})` }}</span></el-button
+              >
+            </template>
+          </el-input>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="submitForm(ruleFormRef)">查看并下载报告</el-button>
+      </div>
+    </template>
+  </el-dialog>
+  <el-dialog
+    title="滑动验证"
+    destroy-on-close
+    width="500px"
+    center
+    modal-class="register-verify-model"
+    v-model="modalVisible"
+  >
+    <SliderVerify
+      :slideVerifyOptions="{ show: false, w: 450, h: 220 }"
+      @onSuccess="verifySuccess"
+      @onFail="verifyFail"
+    />
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, reactive } from 'vue';
+import SliderVerify from '@/components/SliderVerify.vue';
+import { showMessage, startCountdown } from '@/utils/common';
+import User from '@/assets/images/login/User.png';
+import Code from '@/assets/images/login/Code.png';
+import Phone from '@/assets/images/login/Phone.png';
+
+import type { FormInstance, FormRules } from 'element-plus';
+
+interface RuleForm {
+  companyName: string;
+  phone: string;
+  verifyCode: string;
+}
+
+const validatePhone = (rule: any, value: any, callback: any) => {
+  if (value === '') {
+    callback(new Error('请输入手机号'));
+  } else {
+    if (/(1[3-9]\d{9}$)/.test(value)) {
+      callback(new Error('请输入正确的手机号'));
+    } else {
+      callback();
+    }
+  }
+};
+
+const emits = defineEmits(['update:visible']);
+
+const ruleFormRef = ref<FormInstance>();
+// 人机验证弹窗显示标识
+const modalVisible = ref(false);
+
+// 倒计时展示
+const countDownNumer = ref(0);
+// 重新获取验证码标识
+const isReacquireCode = ref(false);
+
+const getVerifyCodeLoading = ref(false);
+
+const props = defineProps(['visible']);
+
+const visible = computed({
+  get() {
+    return props.visible;
+  },
+  set(val) {
+    emits('update:visible', val);
+  }
+});
+
+// 获取验证码的文案
+const codeText = computed(() => {
+  return isReacquireCode.value ? '重新获取' : '获取验证码';
+});
+
+const form = reactive<RuleForm>({
+  companyName: '',
+  phone: '',
+  verifyCode: ''
+});
+
+const rules = reactive<FormRules<RuleForm>>({
+  companyName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
+  phone: [{ validator: validatePhone, trigger: 'blur' }],
+  verifyCode: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
+});
+
+const acceptCode = () => {
+  modalVisible.value = true;
+};
+
+const closed = () => {
+  visible.value = false;
+};
+
+// 获取验证码
+const getVerifyCode = () => {
+  getVerifyCodeLoading.value = true;
+  // 设置倒计时
+  startCountdown(60, (time) => (countDownNumer.value = time));
+  isReacquireCode.value = true;
+  //   post('v3/member/front/active/verification/code', {
+  //     email: email.value,
+  //     source: 1,
+  //     type: 1
+  //   })
+  //     .then((res) => {
+  //       if (res.state === 200) {
+  //         showMessage({
+  //           message: L['register']['验证码已发送'],
+  //           type: 'success'
+  //         });
+  //         // 设置倒计时
+  //         startCountdown(60, (time) => (countDownNumer.value = time));
+  //         isReacquireCode.value = true;
+  //       } else {
+  //         showMessage({
+  //           message: res.msg,
+  //           type: 'warning'
+  //         });
+  //       }
+  //     })
+  //     .finally(() => {
+  //       getVerifyCodeLoading.value = false;
+  //     });
+};
+
+// 人机验证成功
+const verifySuccess = () => {
+  modalVisible.value = false;
+  showMessage({
+    message: '正在发送验证码',
+    type: 'success'
+  });
+  getVerifyCode();
+};
+
+// 人机校验失败
+const verifyFail = () => {
+  showMessage({
+    message: '校验不通过',
+    type: 'warning'
+  });
+};
+
+const submitForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  formEl.validate((valid) => {
+    if (valid) {
+      console.log('submit!');
+    } else {
+      console.log('error submit!');
+    }
+  });
+};
+</script>
+
+<style lang="scss">
+.confirm-dialog {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  // justify-content: center;
+  align-items: center;
+  width: 487px;
+  height: 598px;
+  background-color: #f6f8fa;
+  .el-dialog__header {
+    font-weight: bold;
+    font-size: 32px;
+    color: #282e30;
+  }
+
+  &-tip {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-bottom: 10px;
+    span {
+      font-weight: 400;
+      font-size: 12px;
+      color: rgba(40, 46, 48, 0.6);
+    }
+  }
+
+  &-content {
+    .el-input {
+      width: 431px !important;
+      height: 48px !important;
+      background: #ffffff !important;
+      border: 1px solid #dde1e6 !important;
+      box-shadow: none;
+      border-radius: none;
+    }
+
+    .el-form-item {
+      margin-bottom: 32px;
+    }
+
+    .el-input__wrapper {
+      box-shadow: none !important;
+    }
+
+    .el-input__inner {
+      &::placeholder {
+        position: relative;
+        top: 1px;
+        font-weight: 400;
+        font-size: 16px;
+        color: rgba(40, 46, 48, 0.6);
+      }
+    }
+
+    .el-input__prefix {
+      width: 17px;
+      height: 19px;
+      margin-left: 15px;
+      margin-right: 5px;
+
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+      }
+    }
+
+    .el-input__suffix {
+      .el-button {
+        width: 154px;
+        height: 40px;
+        line-height: 40px;
+        font-weight: bold;
+        font-size: 16px;
+        color: #ffffff;
+        background-color: var(--promotion--color-primary);
+      }
+    }
+
+    .el-form-item__error {
+      font-weight: 400;
+      font-size: 16px;
+      color: #d20000;
+      padding-left: 20px;
+    }
+  }
+
+  .el-dialog__footer {
+    position: absolute;
+    bottom: 80px;
+    width: 100%;
+    .dialog-footer {
+      width: 100%;
+      .el-button {
+        width: 431px;
+        height: 46px;
+        font-weight: bold;
+        font-size: 16px;
+        color: #ffffff;
+        background: var(--promotion--color-primary);
+      }
+    }
+  }
+}
+</style>
+<style lang="scss"></style>

+ 136 - 0
xinkeaboard-promotion-portal/src/components/SliderVerify.vue

@@ -0,0 +1,136 @@
+<template>
+  <div class="verify-content" v-loading="loading">
+    <slide-verify
+      ref="block"
+      v-bind="props.slideVerifyOptions"
+      slider-text="拖动滑块来填充拼图"
+      @again="onAgain"
+      @imageLoad="imageLoad"
+      @success="onSuccess"
+      @fail="onFail"
+    ></slide-verify>
+    <div class="verify-content-action">
+      <span class="verify-content-action__refresh" @click="refresh">
+        <img :src="Refresh" />
+        <span>{{ '刷新' }}</span>
+      </span>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
+import SlideVerify from './vue3-slide-verify';
+import Refresh from '@/assets/images/refresh.png';
+import './vue3-slide-verify/style.css';
+
+const props = defineProps({
+  slideVerifyOptions: {
+    type: Object,
+    default: () => {}
+  }
+});
+
+const emits = defineEmits(['onSuccess', 'onFail']);
+const loading = ref(true);
+
+const block = ref();
+
+const onSuccess = () => {
+  emits('onSuccess');
+};
+
+const refresh = () => {
+  loading.value = true;
+  block.value?.refresh();
+};
+
+const imageLoad = () => {
+  loading.value = false;
+};
+
+const onFail = () => {
+  loading.value = true;
+  block.value?.refresh();
+  emits('onFail');
+};
+
+const onAgain = () => {
+  // 刷新
+  loading.value = true;
+  block.value?.refresh();
+};
+</script>
+<style lang="scss" scoped>
+.verify-content {
+  position: relative;
+  height: 100%;
+
+  :deep(.slide-verify) {
+    canvas {
+      border-radius: 20px;
+    }
+
+    .slide-verify-slider {
+      border-radius: 25px;
+      height: 50px;
+      line-height: 50px;
+      box-sizing: content-box;
+    }
+
+    .slide-verify-slider-mask {
+      width: 50px;
+      height: 50px;
+      border-radius: 25px;
+      background-color: var(--promotion--color-primary);
+      border-color: transparent;
+    }
+
+    .slide-verify-slider-mask-item {
+      width: 50px;
+      height: 50px;
+      border-radius: 100%;
+
+      &:hover {
+        background: #fff;
+
+        .iconfont {
+          color: #303030;
+        }
+      }
+    }
+
+    .slide-verify-slider-text {
+      color: #666;
+    }
+
+    .container-success {
+      .slide-verify-slider-mask-item {
+        background-color: var(--promotion--color-primary) !important;
+
+        .iconfont {
+          color: #fff;
+        }
+      }
+    }
+  }
+
+  &-action {
+    height: 30px;
+    margin-top: 20px;
+    &__refresh {
+      display: flex;
+      // justify-content: center;
+      align-items: center;
+      cursor: pointer;
+      img {
+        width: 20px;
+        margin-right: 5px;
+      }
+
+      span {
+        font-weight: bold;
+      }
+    }
+  }
+}
+</style>

+ 11 - 6
xinkeaboard-promotion-portal/src/components/TopContent.vue

@@ -30,20 +30,22 @@
         >
       </div>
     </div>
+    <LoginDialog :visible="visible" @update:visible="(val) => (visible = val)"></LoginDialog>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { ref, computed, nextTick } from 'vue';
+import { ref, computed, reactive } from 'vue';
 import { useMainStore } from '../store';
-
 import { useRouter } from 'vue-router';
 import CountrySelct from '@/components/CountrySelct.vue';
 import ProductDescription from '@/components/ProductDescription.vue';
 import CompetitorWebsite from '@/components/CompetitorWebsite.vue';
+import LoginDialog from '@/components/LoginDialog.vue';
 import Logo from '@/assets/images/logo.png';
 
 import { showMessage } from '@/utils/common';
+
 const mainStore = useMainStore();
 const router = useRouter();
 
@@ -52,6 +54,7 @@ const currentStep = computed(() => mainStore.getCurrentStep);
 const CountrySelctRef = ref();
 const ProductDescriptionRef = ref();
 const CompetitorWebsiteRef = ref();
+const visible = ref<boolean>(false);
 
 const btnLoading = ref<boolean>(false);
 
@@ -123,7 +126,6 @@ const validateCompetitorWebsite = (site: string) => {
 };
 
 const acceptRecod = async () => {
-  btnLoading.value = true;
   const formData = getFormData();
   const { competitorWebsite } = formData;
   const result = validateCompetitorWebsite(competitorWebsite);
@@ -136,12 +138,15 @@ const acceptRecod = async () => {
     btnLoading.value = false;
     return;
   }
-  mainStore.setFormData(formData);
+  visible.value = true;
+  return;
+  btnLoading.value = true;
+
+  // mainStore.setFormData(formData);
 
-  router.push('/record');
+  // router.push('/record');
 };
 </script>
-
 <style lang="scss" scoped>
 .head-content {
   width: 100%;

+ 380 - 0
xinkeaboard-promotion-portal/src/components/vue3-slide-verify/index.js

@@ -0,0 +1,380 @@
+import { reactive, ref, defineComponent, onMounted, onBeforeUnmount, openBlock, createElementBlock, normalizeStyle, createElementVNode, normalizeClass, createCommentVNode, toDisplayString } from "vue";
+const PI = Math.PI;
+function sum(x, y) {
+  return x + y;
+}
+function square(x) {
+  return x * x;
+}
+function draw(ctx, x, y, l, r, operation) {
+  ctx.beginPath();
+  ctx.moveTo(x, y);
+  ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);
+  ctx.lineTo(x + l, y);
+  ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);
+  ctx.lineTo(x + l, y + l);
+  ctx.lineTo(x, y + l);
+  ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);
+  ctx.lineTo(x, y);
+  ctx.lineWidth = 2;
+  ctx.fillStyle = "rgba(255, 255, 255, 0.7)";
+  ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
+  ctx.stroke();
+  ctx[operation]();
+  ctx.globalCompositeOperation = "destination-over";
+}
+function createImg(imgs, onload) {
+  const img = document.createElement("img");
+  img.crossOrigin = "Anonymous";
+  img.onload = onload;
+  img.onerror = () => {
+    img.src = getRandomImg(imgs);
+  };
+  img.src = getRandomImg(imgs);
+  return img;
+}
+function getRandomNumberByRange(start, end) {
+  return Math.round(Math.random() * (end - start) + start);
+}
+function getRandomImg(imgs) {
+  const len = imgs.length;
+  return len > 0 ? imgs[getRandomNumberByRange(0, len - 1)] : "https://picsum.photos/300/150?image=" + getRandomNumberByRange(0, 1084);
+}
+function throttle(fn, interval, options = { leading: true, trailing: true }) {
+  const { leading, trailing, resultCallback } = options;
+  let lastTime = 0;
+  let timer = null;
+  const _throttle = function(...args) {
+    return new Promise((resolve, reject) => {
+      const nowTime = new Date().getTime();
+      if (!lastTime && !leading)
+        lastTime = nowTime;
+      const remainTime = interval - (nowTime - lastTime);
+      if (remainTime <= 0) {
+        if (timer) {
+          clearTimeout(timer);
+          timer = null;
+        }
+        const result = fn.apply(this, args);
+        if (resultCallback)
+          resultCallback(result);
+        resolve(result);
+        lastTime = nowTime;
+        return;
+      }
+      if (trailing && !timer) {
+        timer = setTimeout(() => {
+          timer = null;
+          lastTime = !leading ? 0 : new Date().getTime();
+          const result = fn.apply(this, args);
+          if (resultCallback)
+            resultCallback(result);
+          resolve(result);
+        }, remainTime);
+      }
+    });
+  };
+  _throttle.cancel = function() {
+    if (timer)
+      clearTimeout(timer);
+    timer = null;
+    lastTime = 0;
+  };
+  return _throttle;
+}
+function useSlideAction() {
+  const origin = reactive({
+    x: 0,
+    y: 0
+  });
+  const success = ref(false);
+  const isMouseDown = ref(false);
+  const timestamp = ref(0);
+  const trail = ref([]);
+  const start = (e) => {
+    if (success.value)
+      return;
+    if (e instanceof MouseEvent) {
+      origin.x = e.clientX;
+      origin.y = e.clientY;
+    } else {
+      origin.x = e.changedTouches[0].pageX;
+      origin.y = e.changedTouches[0].pageY;
+    }
+    isMouseDown.value = true;
+    timestamp.value = Date.now();
+  };
+  const move = (w, e, cb) => {
+    if (!isMouseDown.value)
+      return false;
+    let moveX = 0;
+    let moveY = 0;
+    if (e instanceof MouseEvent) {
+      moveX = e.clientX - origin.x;
+      moveY = e.clientY - origin.y;
+    } else {
+      moveX = e.changedTouches[0].pageX - origin.x;
+      moveY = e.changedTouches[0].pageY - origin.y;
+    }
+    if (moveX < 0 || moveX + 38 >= w)
+      return false;
+    cb(moveX);
+    trail.value.push(moveY);
+  };
+  const verify = (left, blockX, accuracy) => {
+    const arr = trail.value;
+    const average = arr.reduce(sum) / arr.length;
+    const deviations = arr.map((x) => x - average);
+    const stddev = Math.sqrt(deviations.map(square).reduce(sum) / arr.length);
+    const leftNum = parseInt(left);
+    accuracy = accuracy <= 1 ? 1 : accuracy > 10 ? 10 : accuracy;
+    return {
+      spliced: Math.abs(leftNum - blockX) <= accuracy,
+      TuringTest: average !== stddev
+    };
+  };
+  const end = (e, cb) => {
+    if (!isMouseDown.value)
+      return false;
+    isMouseDown.value = false;
+    const moveX = e instanceof MouseEvent ? e.clientX : e.changedTouches[0].pageX;
+    if (moveX === origin.x)
+      return false;
+    timestamp.value = Date.now() - timestamp.value;
+    cb(timestamp.value);
+  };
+  return { origin, success, isMouseDown, timestamp, trail, start, move, end, verify };
+}
+var slideVerify_vue_vue_type_style_index_0_scoped_true_lang = "";
+var _export_sfc = (sfc, props) => {
+  const target = sfc.__vccOpts || sfc;
+  for (const [key, val] of props) {
+    target[key] = val;
+  }
+  return target;
+};
+const _sfc_main = defineComponent({
+  name: "SlideVerify",
+  props: {
+    l: {
+      type: Number,
+      default: 42
+    },
+    r: {
+      type: Number,
+      default: 10
+    },
+    w: {
+      type: Number,
+      default: 310
+    },
+    h: {
+      type: Number,
+      default: 155
+    },
+    sliderText: {
+      type: String,
+      default: "Slide filled right"
+    },
+    accuracy: {
+      type: Number,
+      default: 5
+    },
+    show: {
+      type: Boolean,
+      default: true
+    },
+    imgs: {
+      type: Array,
+      default: () => []
+    },
+    interval: {
+      type: Number,
+      default: 50
+    }
+  },
+  emits: ["success", "again", "fail", "refresh", "imageLoad"],
+  setup(props, { emit }) {
+    const { imgs, l, r, w, h, accuracy, interval } = props;
+    const loadBlock = ref(true);
+    const blockX = ref(0);
+    const blockY = ref(0);
+    const containerCls = reactive({
+      containerActive: false,
+      containerSuccess: false,
+      containerFail: false
+    });
+    const sliderBox = reactive({
+      iconCls: "arrow-right",
+      width: "0",
+      left: "0"
+    });
+    const block = ref();
+    const blockCtx = ref();
+    const canvas = ref();
+    const canvasCtx = ref();
+    let img;
+    const { success, start, move, end, verify } = useSlideAction();
+    const reset = () => {
+      var _a, _b;
+      success.value = false;
+      containerCls.containerActive = false;
+      containerCls.containerSuccess = false;
+      containerCls.containerFail = false;
+      sliderBox.iconCls = "arrow-right";
+      sliderBox.left = "0";
+      sliderBox.width = "0";
+      block.value.style.left = "0";
+      (_a = canvasCtx.value) == null ? void 0 : _a.clearRect(0, 0, w, h);
+      (_b = blockCtx.value) == null ? void 0 : _b.clearRect(0, 0, w, h);
+      block.value.width = w;
+      img.src = getRandomImg(imgs);
+    };
+    const refresh = () => {
+      reset();
+      emit("refresh");
+    };
+    function moveCb(moveX) {
+      sliderBox.left = moveX + "px";
+      let blockLeft = (w - 40 - 20) / (w - 40) * moveX;
+      block.value.style.left = blockLeft + "px";
+      containerCls.containerActive = true;
+      sliderBox.width = moveX + "px";
+    }
+    function endCb(timestamp) {
+      const { spliced, TuringTest } = verify(block.value.style.left, blockX.value, accuracy);
+      if (spliced) {
+        if (accuracy === -1) {
+          containerCls.containerSuccess = true;
+          sliderBox.iconCls = "success";
+          success.value = true;
+          emit("success", timestamp);
+          return;
+        }
+        if (TuringTest) {
+          containerCls.containerSuccess = true;
+          sliderBox.iconCls = "success";
+          success.value = true;
+          emit("success", timestamp);
+        } else {
+          containerCls.containerFail = true;
+          sliderBox.iconCls = "fail";
+          emit("again");
+        }
+      } else {
+        containerCls.containerFail = true;
+        sliderBox.iconCls = "fail";
+        emit("fail");
+      }
+    }
+    const touchMoveEvent = throttle((e) => {
+      move(w, e, moveCb);
+    }, interval);
+    const touchEndEvent = (e) => {
+      end(e, endCb);
+    };
+    onMounted(() => {
+      var _a, _b;
+      const _canvasCtx = (_a = canvas.value) == null ? void 0 : _a.getContext("2d");
+      const _blockCtx = (_b = block.value) == null ? void 0 : _b.getContext("2d", { willReadFrequently: true });
+      canvasCtx.value = _canvasCtx;
+      blockCtx.value = _blockCtx;
+      img = createImg(imgs, () => {
+        loadBlock.value = false;
+        const L = l + r * 2 + 3;
+        blockX.value = getRandomNumberByRange(L + 10, w - (L + 10));
+        blockY.value = getRandomNumberByRange(10 + r * 2, h - (L + 10));
+        if (_canvasCtx && _blockCtx) {
+          draw(_canvasCtx, blockX.value, blockY.value, l, r, "fill");
+          draw(_blockCtx, blockX.value, blockY.value, l, r, "clip");
+          _canvasCtx.drawImage(img, 0, 0, w, h);
+          _blockCtx.drawImage(img, 0, 0, w, h);
+          const _y = blockY.value - r * 2 - 1;
+          const imgData = _blockCtx.getImageData(blockX.value, _y, L, L);
+          block.value.width = L;
+          _blockCtx.putImageData(imgData, 0, _y);
+        }
+        emit('imageLoad')
+      });
+      document.addEventListener("mousemove", touchMoveEvent);
+      document.addEventListener("mouseup", touchEndEvent);
+    });
+    onBeforeUnmount(() => {
+      document.removeEventListener("mousemove", touchMoveEvent);
+      document.removeEventListener("mouseup", touchEndEvent);
+    });
+    return {
+      block,
+      canvas,
+      loadBlock,
+      containerCls,
+      sliderBox,
+      refresh,
+      sliderDown: start,
+      touchStartEvent: start,
+      touchMoveEvent,
+      touchEndEvent
+    };
+  }
+});
+const _hoisted_1 = ["width", "height"];
+const _hoisted_2 = ["width", "height"];
+const _hoisted_3 = { class: "slide-verify-slider-text" };
+function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
+  return openBlock(), createElementBlock("div", {
+    id: "slideVerify",
+    class: "slide-verify",
+    style: normalizeStyle({ width: _ctx.w + "px" }),
+    onselectstart: "return false;"
+  }, [
+    createElementVNode("div", {
+      class: normalizeClass({ "slider-verify-loading": _ctx.loadBlock })
+    }, null, 2),
+    createElementVNode("canvas", {
+      ref: "canvas",
+      width: _ctx.w,
+      height: _ctx.h
+    }, null, 8, _hoisted_1),
+    _ctx.show ? (openBlock(), createElementBlock("div", {
+      key: 0,
+      class: "slide-verify-refresh-icon",
+      onClick: _cache[0] || (_cache[0] = (...args) => _ctx.refresh && _ctx.refresh(...args))
+    }, _cache[5] || (_cache[5] = [
+      createElementVNode("i", { class: "iconfont icon-refresh" }, null, -1)
+    ]))) : createCommentVNode("", true),
+    createElementVNode("canvas", {
+      ref: "block",
+      width: _ctx.w,
+      height: _ctx.h,
+      class: "slide-verify-block"
+    }, null, 8, _hoisted_2),
+    createElementVNode("div", {
+      class: normalizeClass(["slide-verify-slider", {
+        "container-active": _ctx.containerCls.containerActive,
+        "container-success": _ctx.containerCls.containerSuccess,
+        "container-fail": _ctx.containerCls.containerFail
+      }])
+    }, [
+      createElementVNode("div", {
+        class: "slide-verify-slider-mask",
+        style: normalizeStyle({ width: _ctx.sliderBox.width })
+      }, [
+        createElementVNode("div", {
+          class: "slide-verify-slider-mask-item",
+          style: normalizeStyle({ left: _ctx.sliderBox.left }),
+          onMousedown: _cache[1] || (_cache[1] = (...args) => _ctx.sliderDown && _ctx.sliderDown(...args)),
+          onTouchstart: _cache[2] || (_cache[2] = (...args) => _ctx.touchStartEvent && _ctx.touchStartEvent(...args)),
+          onTouchmove: _cache[3] || (_cache[3] = (...args) => _ctx.touchMoveEvent && _ctx.touchMoveEvent(...args)),
+          onTouchend: _cache[4] || (_cache[4] = (...args) => _ctx.touchEndEvent && _ctx.touchEndEvent(...args))
+        }, [
+          createElementVNode("i", {
+            class: normalizeClass(["slide-verify-slider-mask-item-icon", "iconfont", `icon-${_ctx.sliderBox.iconCls}`])
+          }, null, 2)
+        ], 36)
+      ], 4),
+      createElementVNode("span", _hoisted_3, toDisplayString(_ctx.sliderText), 1)
+    ], 2)
+  ], 4);
+}
+var SlideVerify = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-3f647794"]]);
+export { SlideVerify as default };

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
xinkeaboard-promotion-portal/src/components/vue3-slide-verify/style.css


+ 25 - 0
xinkeaboard-promotion-portal/src/utils/common.ts

@@ -25,3 +25,28 @@ export const safeJsonParse = (val: string, defaultValue?: any) => {
     return defaultValue ?? {};
   }
 };
+
+/**
+ * [倒计时]
+ *
+ * @return  {[params]}  [return description]
+ */
+export function startCountdown(
+  duration: number,
+  onTick?: (args: number) => void,
+  onComplete?: () => void
+) {
+  let time = duration;
+  const timer = setInterval(() => {
+    time--;
+    if (typeof onTick === "function") {
+      onTick(time);
+    }
+    if (time <= 0) {
+      clearInterval(timer);
+      if (typeof onComplete === "function") {
+        onComplete();
+      }
+    }
+  }, 1000);
+}

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov