区域环评项目(Vue3 PC)实现验证码等功能 问题记录

目录

1. HTML、CSS 问题记录

1.1 页面缩放后,滚动条虽然有效,但页面底部会有部分内容被遮挡

1.2 video 标签的 autoplay 属性不起作用,视频无法自动播放

1.3 使用 a 标签在 Android / IOS 上发送短信不兼容

1.4 FIXME、TODO、XXX 的含义

2. TypeScript 补充学习

2.1 通过已有数据类型接口,快速扩展其他数据类型接口

2.2 使用索引访问类型

3. 使用 Vue3 封装验证码组件,防止用户频繁操作

3.1 封装拼图页面 verify-slide.vue

3.2 封装拼图容器 verify-home.vue

3.3 使用验证码组件

4. 微前端相关配置文件介绍

4.1 基座应用里的文件介绍

4.1.1 public/global/config/api-config.js

4.1.2 public/global/config/config-micro-app.js

4.1.3 public/global/config/config-webpack.js

4.1.4 vue.config.js

4.2 解决访问不到服务器上的 api-config.js 文件

5. 如何在开发者工具中,筛选给地图发送的消息?

6. 使用 userAgent 判断设备类型


1. HTML、CSS 问题记录

1.1 页面缩放后,滚动条虽然有效,但页面底部会有部分内容被遮挡

看一下高度是否是 固定数值,把 固定数值 改为 根据页面动态计算高度

.skin-big-screen iframe {
    height: calc(100vh - 240px) !important;
}

1.2 video 标签的 autoplay 属性不起作用,视频无法自动播放

浏览器会拦截自动播放声音的视频,加上 muted 属性即可解决

<video id="video" muted autoplay='true' loop src="./video/v1.mp4"></video>

1.3 使用 a 标签在 Android / IOS 上发送短信不兼容

Android —— <a href="sms:15328656551?body=2955"></a>

IOS —— <a href="sms:15328656551&body=2955"></a>

兼容 Android / IOS —— <a href="sms:15328656551;?&body=2955"></a>

href="'sms:' + smsInfo.target + ';?&body=' + smsInfo.code"

1.4 FIXME、TODO、XXX 的含义

// TODO: 将实现的功能 - 想到哪写到哪,还没完全实现

// FIXME: 如何修复该部分代码问题 - 运行可能存在问题,需要解决 bug

// XXX: 如何改进优化该部分代码 - 功能没问题,但是代码写的不太好,可以优化

2. TypeScript 补充学习

2.1 通过已有数据类型接口,快速扩展其他数据类型接口

  • Pick —— 选择其中的属性
  • Omit —— 排除其中的属性
  • Partial —— 让全部属性变成可选
  • Required —— 让全部属性变成必选

举个栗子~~~

// 以下方数据类型接口为例,进行扩展
interface Test {
    name: string;
    sex: boolean;
    height: number;
}


// Pick —— 选择其中的属性

type PickTest = Pick<Test, 'sex'>;
const a: PickTest = { sex: true };


// Omit —— 排除其中的属性

type OmitTest = Omit<Test, 'sex'>;
const b: OmitTest = { name: 'Lyrelion', height: 188 };


// Partial —— 让全部属性变成可选

type PartialTest = Partial<Test>


Required —— 让全部属性变成必选

type RequiredTest = Required<Test>

2.2 使用索引访问类型

type Person = {
    name: string,
    age: number,
    hobby: [] as string[],
}

type TestOne = Person["name"] // 可以设置一个
type TestMore = Person["name" | "age"] // 可以设置多个

const aaa: TestMore = 'abc' // ok
const bbb: TestMore = 123 // ok 
const ccc: TestMore = [1,2,3] // error

3. 使用 Vue3 封装验证码组件,防止用户频繁操作

3.1 封装拼图页面 verify-slide.vue

<!--
 * @Description: 验证码 - 滑动滑块验证
 * @Author: lyrelion
 * @Date: 2022-08-16 10:34:26
 * @LastEditors: lyrelion
 * @LastEditTime: 2022-10-31 20:43:13
-->

<template>
  <div class="p-relative">
    <!-- 图片部分容器 -->
    <div v-if="isSlideVerify" :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }">
      <!-- 大图片容器 - 相对定位 -->
      <div class="verify-img-panel" :style="{ width: setSize.imgWidth, height: setSize.imgHeight }">
        <!-- 背景图片(缺失拼图的图片) -->
        <img
          :src="'data:image/png;base64,' + backImgBase"
          class="back-img"
        />

        <!-- 刷新验证码图片的按钮 -->
        <!-- <div v-show="showRefresh" class="verify-refresh" @click="refresh">
          <i class="iconfont icon-refresh"></i>
        </div> -->

        <!-- 提示信息(验证成功、验证失败,动态展示) -->
        <transition name="tips">
          <span v-if="tipWords" class="verify-tips" :class="passFlag ? 'suc-bg' : 'err-bg'">{{ tipWords }}</span>
        </transition>
      </div>
    </div>

    <!-- 滑块部分容器 -->
    <div
      class="verify-bar-area"
      :style="{
        width: setSize.imgWidth,
        height: barSize.height,
      }"
    >
      <!-- 用户操作提示信息 - 向右滑动完成验证 -->
      <!-- <span class="verify-msg" v-text="text"></span> -->

      <!-- 可以拖动的滑块容器 -->
      <div
        class="verify-left-bar"
        :style="{
          width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
          height: barSize.height,
          transaction: transitionWidth,
        }"
      >
        <!-- 被拖拽的滑块(实际移动的滑块) -->
        <div
          class="verify-move-block"
          :style="{
            width: blockSize.width,
            height: blockSize.height,
            left: moveBlockLeft,
            transition: transitionLeft,
          }"
          @touchstart="start"
          @mousedown="start"
        >
          <!-- <img src="@/assets/images/login/verify-block.png" alt="" /> -->
          <!-- <i :class="['verify-icon iconfont', iconClass]" :style="{ color: iconColor, 'margin-right': '40px' }">aaaa</i> -->

          <!-- 跟着滑块移动的拼图容器 -->
          <div
            v-if="isSlideVerify"
            class="verify-sub-block"
            :style="{
              width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px',
              height: setSize.imgHeight,
              top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
              'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
            }"
          >
            <!-- 跟着滑块移动的拼图图片 -->
            <img
              :src="'data:image/png;base64,' + blockBackImgBase"
              class="sub-block"
            />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import {
  reactive,
  toRefs,
  defineComponent,
  computed,
  onMounted,
  watch,
  nextTick,
  getCurrentInstance,
} from 'vue';
import codeCase from '@/utils/codeCase';
import { resetSize } from '@/utils/common';
import { reqGet, reqCheck } from '@/services/common/verify';

export default defineComponent({
  name: 'VerifySlide',
  props: {
    // 验证码类型(滑块拼图、点击文字)
    captchaType: {
      type: String,
      default: 'blockPuzzle',
    },
    // 验证码类型是否为滑块拼图
    isSlideVerify: {
      type: Boolean,
      default: true,
    },
    // 验证码的显示方式,弹出式 - pop,固定 - fixed,默认为 mode: 'pop'
    mode: {
      type: String,
      default: 'pop',
    },
    // 验证码图片和移动条容器的间隔,默认单位是px。如:间隔为5px,默认:vSpace:5
    vSpace: {
      type: Number,
      default: 5,
    },
    // 滑动条内的提示,不设置默认是:'向右滑动完成验证'
    explain: {
      type: String,
      default: '向右滑动完成验证',
    },
    // 图片的大小对象, 有默认值 { width: '310px', height: '155px' }, 可省略
    imgSize: {
      type: Object,
      default() {
        return {
          width: '310px',
          height: '155px',
        };
      },
    },
    // 下方滑块的大小对象, 有默认值 { width: '310px', height: '50px' }, 可省略
    barSize: {
      type: Object,
      default() {
        return {
          width: '310px',
          height: '40px',
        };
      },
    },
    blockSize: {
      type: Object,
      default() {
        return {
          width: '50px',
          height: '50px',
        };
      },
    },
  },
  emits: ['init-failed', 'success', 'error'],
  setup(props: any, { emit }) {
    const { proxy } = getCurrentInstance() as any;

    // 响应式变量
    const state = reactive({
      // 后端返回的aes加密秘钥
      secretKey: '',
      // 是否通过的标识
      passFlag: false,
      // 验证码背景图片
      backImgBase: '',
      // 验证滑块的背景图片
      blockBackImgBase: '',
      // 后端返回的唯一token值
      backToken: '',
      // 移动开始的时间
      startMoveTime: 0,
      // 移动结束的时间
      endMovetime: 0,
      // 提示词的背景颜色
      tipsBackColor: '',
      // 提示词
      tipWords: '',
      // 滑动条内的提示
      text: '',
      setSize: {
        imgHeight: '0px',
        imgWidth: '0px',
        barHeight: '0px',
        barWidth: '0px',
      },
      top: 0,
      left: 0,
      moveBlockLeft: '0px',
      leftBarWidth: '0px',
      // 移动中样式
      iconColor: '',
      iconClass: 'icon-right',
      // 鼠标状态
      status: false,
      // 是否验证完成
      isEnd: false,
      showRefresh: true,
      transitionLeft: '',
      transitionWidth: '',
      startLeft: 0,
    });

    const barArea = computed(() => proxy.$el.querySelector('.verify-bar-area'));

    /**
     * 请求背景图片和验证图片
     */
    const getPictrue = () => {
      const data = {
        captchaType: props.captchaType,
      };
      reqGet(data).then((res: any) => {
        console.log('请求背景图片和验证图片 ===', res);
        if (res.data.repCode === '0000') {
          state.backImgBase = res.data.repData.originalImageBase64;
          state.blockBackImgBase = res.data.repData.jigsawImageBase64;
          state.backToken = res.data.repData.token;
          state.secretKey = res.data.repData.secretKey;
        } else {
          state.tipWords = res.data.repMsg;
          emit('init-failed', res);
        }
      });
    };

    /**
     * 刷新
     */
    const refresh = () => {
      state.showRefresh = true;

      state.transitionLeft = 'left .3s';
      state.moveBlockLeft = '0px';

      state.leftBarWidth = '0px';
      state.transitionWidth = 'width .3s';

      state.iconColor = '#000';
      state.iconClass = 'icon-right';
      state.isEnd = false;
      // 请求背景图片和验证图片
      getPictrue();
      setTimeout(() => {
        state.transitionWidth = '';
        state.transitionLeft = '';
        state.text = props.explain;
      }, 300);
    };

    /**
     * 鼠标按下
     */
    const start = (e: any) => {
      // eslint-disable-next-line no-param-reassign
      e = e || window.event;
      let x: any;
      if (!e.touches) {
        // 兼容PC端
        x = e.clientX;
      } else {
        // 兼容移动端
        x = e.touches[0].pageX;
      }
      console.log('barArea ===', barArea);
      state.startLeft = Math.floor(x - barArea.value.getBoundingClientRect().left);
      // 开始滑动的时间
      state.startMoveTime = +new Date();
      if (!state.isEnd) {
        state.text = '';
        state.iconColor = '#fff';
        e.stopPropagation();
        state.status = true;
      }
    };

    /**
     * 鼠标移动
     */
    const move = (e: any) => {
      // eslint-disable-next-line no-param-reassign
      e = e || window.event;
      let x: any;
      if (state.status && !state.isEnd) {
        if (!e.touches) {
          // 兼容PC端
          x = e.clientX;
        } else {
          // 兼容移动端
          x = e.touches[0].pageX;
        }
        // eslint-disable-next-line camelcase
        const bar_area_left = barArea.value.getBoundingClientRect().left;
        // 小方块相对于父元素的left值
        // eslint-disable-next-line camelcase
        let move_block_left = x - bar_area_left;

        // 计算 props 中的 blockSize 滑块尺寸
        const blockSizeWidthNum = parseInt(props.blockSize.width, 10); // 50

        // eslint-disable-next-line camelcase
        if (move_block_left >= barArea.value.offsetWidth - blockSizeWidthNum / 2 - 2) {
          // eslint-disable-next-line camelcase
          move_block_left = barArea.value.offsetWidth - blockSizeWidthNum / 2 - 2;
        }

        // eslint-disable-next-line camelcase
        if (move_block_left <= 0) {
          // eslint-disable-next-line camelcase
          move_block_left = blockSizeWidthNum / 2;
        }

        // 拖动后小方块的left值
        // eslint-disable-next-line camelcase
        state.moveBlockLeft = move_block_left - state.startLeft + 'px';
        // eslint-disable-next-line camelcase
        state.leftBarWidth = move_block_left - state.startLeft + 'px';
      }
    };

    /**
     * 鼠标松开
     */
    const end = () => {
      state.endMovetime = +new Date();
      // 判断是否重合
      if (state.status && !state.isEnd) {
        let moveLeftDistance: any;
        moveLeftDistance = parseInt((state.moveBlockLeft || '').replace('px', ''), 10);
        moveLeftDistance = (moveLeftDistance * 310) / parseInt(state.setSize.imgWidth, 10);
        const data = {
          captchaType: props.captchaType,
          pointJson: state.secretKey
            ? codeCase.verifyAesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), state.secretKey)
            : JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
          token: state.backToken,
        };
        reqCheck(data).then((res: any) => {
          console.log('校验验证码图片 ===', res.data);
          if (res.data.repCode === '0000') {
            state.iconColor = '#fff';
            state.iconClass = 'icon-check';
            state.showRefresh = false;
            state.isEnd = true;
            if (props.mode === 'pop') {
              setTimeout(() => {
                proxy.$parent.clickShow = false;
                refresh();
              }, 1500);
            }
            state.passFlag = true;
            state.tipWords = `${((state.endMovetime - state.startMoveTime) / 1000).toFixed(2)}s验证成功`;
            const captchaVerification = state.secretKey
              ? codeCase.verifyAesEncrypt(
                state.backToken + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
                state.secretKey,
              )
              : state.backToken + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 });
            setTimeout(() => {
              state.tipWords = '';
              proxy.$parent.closeBox();
              // proxy.$parent.$emit('success', { captchaVerification });
              emit('success', { captchaVerification });
            }, 1000);
          } else {
            state.iconColor = '#fff';
            state.iconClass = 'icon-close';
            state.passFlag = false;
            setTimeout(() => {
              refresh();
            }, 1000);
            // proxy.$parent.$emit('error', proxy);
            emit('error', proxy);
            state.tipWords = '验证失败';
            setTimeout(() => {
              state.tipWords = '';
            }, 1000);
          }
        });
        state.status = false;
      }
    };

    /**
     * 页面初始化
     */
    const init = () => {
      state.text = props.explain;
      // 请求背景图片和验证图片
      getPictrue();
      // 重新设置相关组件尺寸
      nextTick(() => {
        const { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy);
        state.setSize.imgHeight = imgHeight;
        state.setSize.imgWidth = imgWidth;
        state.setSize.barHeight = barHeight;
        state.setSize.barWidth = barWidth;
        // console.log('proxy templete 里第一行不能加注释,否则会导致获取不到 proxy.$el ===', proxy);
        proxy.$parent.$emit('ready', proxy);
      });

      // 移除已有监听器
      window.removeEventListener('touchmove', (e) => {
        move(e);
      });
      window.removeEventListener('mousemove', (e) => {
        move(e);
      });
      window.removeEventListener('touchend', () => {
        end();
      });
      window.removeEventListener('mouseup', () => {
        end();
      });
      // 添加新的监听器
      window.addEventListener('touchmove', (e) => {
        move(e);
      });
      window.addEventListener('mousemove', (e) => {
        move(e);
      });
      window.addEventListener('touchend', () => {
        end();
      });
      window.addEventListener('mouseup', () => {
        end();
      });
    };

    onMounted(() => {
      // 页面初始化
      init();
      // 对象开始选中时触发(此处含义为禁止选中、拖拽)
      proxy.$el.onselectstart = () => false;
    });

    watch(
      () => props.isSlideVerify,
      () => {
        // 页面初始化
        init();
      },
    );

    return {
      ...toRefs(state),
      barArea,
      refresh,
      start,
    };
  },
});
</script>

<style lang="scss" scoped>
/* 验证码父容器,相对定位标志 */
.p-relative {
  position: relative !important;
}

/* 背景图片(缺失拼图的图片) */
.back-img {
  display: block;
  width: 100%;
  height: 100%;
}

/* 跟着滑块移动的拼图图片 */
.sub-block {
  display: block;
  width: 100%;
  height: 100%;
  -webkit-user-drag: none;
}

/* 提示信息(验证成功、验证失败,动态展示) */
.verify-tips {
  position: absolute;
  bottom: 0;
  left: 0;
  box-sizing: border-box;
  width: 100%;
  height: 30px;
  padding: 0 0 0 10px;
  color: #FFF;
  font-size: 14px;
  line-height: 30px;
}

.suc-bg {
  background-color: rgba(92, 184, 92, 0.5);
  filter: progid:dximagetransform.microsoft.gradient(startcolorstr=#7f5CB85C, endcolorstr=#7f5CB85C);
}

.err-bg {
  background-color: rgba(217, 83, 79, 0.5);
  filter: progid:dximagetransform.microsoft.gradient(startcolorstr=#7fD9534F, endcolorstr=#7fD9534F);
}

.tips-enter,
.tips-leave-to {
  bottom: -30px;
}

.tips-enter-active,
.tips-leave-active {
  transition: bottom 0.5s;
}

/* 常规验证码 */
.verify-code {
  margin-bottom: 5px;
  border: 1px solid #DDD;
  font-size: 20px;
  text-align: center;
  cursor: pointer;
}

.cerify-code-panel {
  overflow: hidden;
  height: 100%;
}

.verify-code-area {
  float: left;
}

.verify-input-area {
  float: left;
  width: 60%;
  padding-right: 10px;
}

.verify-change-area {
  float: left;
  line-height: 30px;
}

.varify-input-code {
  display: inline-block;
  width: 100%;
  height: 25px;
}

.verify-change-code {
  color: #337AB7;
  cursor: pointer;
}

.verify-btn {
  width: 200px;
  height: 30px;
  margin-top: 10px;
  border: 0;
  background-color: #337AB7;
  color: #FFF;
}

/* 滑块部分容器 */
.verify-bar-area {
  position: relative;
  border: 0;
  border-radius: 10px;
  text-align: center;
  &::before {
    content: ' ';
    position: absolute;
    width: 100%;
    height: 16px;
    left: 0;
    top: 50%;
    background: #D8D8D8;
    border-radius: 10px;
    transform: translate(0, -50%);
  }
}

/* 被拖拽的滑块(实际移动的滑块) */
.verify-bar-area .verify-move-block {
  position: absolute;
  top: 0;
  left: 0;
  // background: #00A870;
  background: var(--theme-color);
  border-radius: 50px;
  cursor: pointer;
  &::before {
    content: ' ';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 26px;
    height: 20px;
    background: url(../../../assets/images/login/verify-block-center.png) no-repeat left center/ cover;
    transform: translate(-50%, -50%);
  }
}

/* 可以拖动的滑块容器 */
.verify-bar-area .verify-left-bar {
  position: absolute;
  // top: -1px;
  // left: -1px;
  border: 0;
  cursor: pointer;
  &::before {
    content: ' ';
    position: absolute;
    width: 100%;
    height: 16px;
    left: 0;
    top: 50%;
    background: #D8D8D8;
    border-top-left-radius: 10px;
    border-bottom-left-radius: 10px;
    transform: translate(0, -50%);
  }
}

/* 大图片容器 - 相对定位 */
.verify-img-panel {
  position: relative;
  box-sizing: content-box;
  margin: 0;
  border-radius: 4px;
}

/* 刷新验证码图片的按钮 */
.verify-img-panel .verify-refresh {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 2;
  width: 25px;
  height: 25px;
  padding: 5px;
  text-align: center;
  cursor: pointer;
}

.verify-img-panel .icon-refresh {
  color: #FFF;
  font-size: 14px;
}

.verify-img-panel .verify-gap {
  position: relative;
  z-index: 2;
  border: 1px solid #FFF;
  background-color: #FFF;
}

/* 跟着滑块移动的拼图容器 */
.verify-bar-area .verify-move-block .verify-sub-block {
  position: absolute;
  z-index: 3;
  text-align: center;
}

.verify-bar-area .verify-move-block .verify-icon {
  font-size: 14px;
}

/* 用户操作提示信息 - 向右滑动完成验证 */
.verify-msg {
  font-size: 14px;
}

.verify-bar-area .verify-msg {
  z-index: 3;
}

/* 字体图标 - 这里我隐藏了,也没有引入字体图标 */
.iconfont {
  font-style: normal;
  font-size: 16px;
  font-family: 'iconfont' !important;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-check:before,
.icon-close:before,
.icon-right:before,
.icon-refresh:before {
  content: ' ';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 9999;
  display: block;
  width: 16px;
  height: 16px;
  margin: auto;
  background-size: contain;
}
</style>

3.2 封装拼图容器 verify-home.vue

<!--
 * @Description: 验证码校验弹框
 * @Author: lyrelion
 * @Date: 2022-08-16 11:35:55
 * @LastEditors: lyrelion
 * @LastEditTime: 2022-11-01 14:14:47
-->

<template>
  <div v-show="showBox" :class="{ 'mask' : mode === modeEnum.pop }">
    <!-- 验证码容器 -->
    <div
      :class="{ 'verifybox': mode === modeEnum.pop }"
      :style="{ 'max-width': `${parseInt(imgSize.width)}px` }"
    >
      <!-- 弹框顶部标题 及 关闭按钮 -->
      <div v-if="mode === modeEnum.pop" class="verifybox-top">
        <img alt="安全验证" draggable="true" src="@/assets/images/login/verify-check.png" />
        安全验证
        <span class="verifybox-close" @click="closeBox"> ✕ </span>
      </div>

      <!-- 验证码组件容器 -->
      <div class="verifybox-bottom" :style="{ padding: mode === modeEnum.pop ? '15px 0 0' : '0' }">
        <!-- 验证码组件 -->
        <component
          :is="componentName"
          v-if="componentName"
          ref="verifyComponentDOM"
          :captcha-type="captchaType"
          :is-slide-verify="captchaType === captchaTypeEnum.blockPuzzle"
          :mode="mode"
          :v-space="vSpace"
          :explain="explain"
          :img-size="imgSize"
          :block-size="blockSize"
          :bar-size="barSize"
          @init-failed="handleInitFailed"
          @success="handleSuccess"
          @error="handleError"
        ></component>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import {
  reactive,
  toRefs,
  defineComponent,
  computed,
  ref,
  watchEffect,
} from 'vue';
// 滑块拼图验证码
import VerifySlide from '@/components/verify/verify-type/verify-slide.vue';

// 验证码类型(滑块拼图、点击文字)
export enum captchaTypeEnum {
  blockPuzzle = 'blockPuzzle', // 滑块拼图
  clickWord = 'clickWord', // 点击文字
}

// 验证码的显示方式,弹出式 - pop,固定 - fixed,默认为 mode: 'pop'
export enum modeEnum {
  pop = 'pop', // 弹出式
  fixed = 'fixed', // 固定
}

export default defineComponent({
  name: 'VerifyHome',
  components: {
    VerifySlide, // 滑块拼图验证码
  },
  props: {
    // 验证码类型(滑块拼图、点击文字)
    captchaType: {
      type: String,
      default: captchaTypeEnum.blockPuzzle,
    },
    // 验证码的显示方式,弹出式 - pop,固定 - fixed,默认为 mode: 'pop'
    mode: {
      type: String,
      default: modeEnum.pop,
    },
    // 验证码图片和移动条容器的间隔,默认单位是px。如:间隔为5px,默认:vSpace:5
    vSpace: {
      type: Number,
      default: 16,
    },
    // 滑动条内的提示,不设置默认是:'向右滑动完成验证'
    explain: {
      type: String,
      default: '向右滑动完成验证',
    },
    // 图片的大小对象, 有默认值 { width: '310px', height: '155px' }, 可省略
    imgSize: {
      type: Object,
      default: () => ({
        width: '600px',
        height: '320px',
      }),
    },
    // 下方滑块的大小对象, 有默认值 { width: '310px', height: '50px' }, 可省略
    barSize: {
      type: Object,
      default: () => ({
        width: '310px',
        height: '34px',
      }),
    },
    blockSize: {
      type: Object,
      default: () => ({
        width: '64px',
        height: '34px',
      }),
    },
  },
  emits: ['init-failed', 'success', 'error'],
  setup(props, { emit }) {
    // 响应式变量
    const state = reactive({});

    // 控制弹窗的显隐(不要随便改这个变量名,因为在子组件里,有直接修改到它)
    const clickShow = ref(false);
    // 验证码类型对应的组件名称
    const componentName = ref('');

    // 验证码组件DOM
    const verifyComponentDOM = ref<HTMLFormElement | null>(null);

    /**
     * 刷新验证码
     */
    const refreshVerify = () => {
      // console.log('验证码组件 DOM 实例 ===', verifyComponentDOM.value);
      if (verifyComponentDOM.value && verifyComponentDOM.value.refresh) {
        verifyComponentDOM.value.refresh();
      }
    };

    /**
     * 展示弹框 - 组件内部调用
     */
    const showBox = computed(() => {
      if (props.mode === modeEnum.pop) {
        return clickShow.value;
      }
      return true;
    });

    /**
     * 展示弹框 - 组件外部调用
     */
    const showDialogOutSide = () => {
      if (props.mode === modeEnum.pop) {
        clickShow.value = true;
      }
    };

    /**
     * 关闭弹窗
     */
    const closeBox = () => {
      clickShow.value = false;
      // 刷新验证码
      refreshVerify();
    };

    /**
     * 验证码初始化失败
     */
    const handleInitFailed = (res: any) => {
      // console.log('验证码初始化失败 ===', res);
      emit('init-failed', res);
    };

    /**
     * 验证码校验成功
     */
    const handleSuccess = (res: any) => {
      // console.log('验证码校验成功 ===', res);
      emit('success', res);
    };

    /**
     * 验证码校验失败
     */
    const handleError = (res: any) => {
      // console.log('验证码校验失败 ===', res);
      emit('error', res);
    };

    /**
     * 根据验证码类型,确定验证码组件
     */
    watchEffect(() => {
      // 验证码类型
      if (props.captchaType === captchaTypeEnum.blockPuzzle) {
        componentName.value = 'VerifySlide';
      } else {
        componentName.value = 'VerifyPoints';
      }
    });

    /*
     * onMounted(() => {
     *   proxy.$on('success', (data: any) => {
     *     handleSuccess(data);
     *   });
     *   proxy.$on('error', (data: any) => {
     *     handleError(data);
     *   });
     * });
     */

    /*
     * onUnmounted(() => {
     *   proxy.$off('success');
     *   proxy.$off('error');
     * });
     */

    return {
      ...toRefs(state),
      modeEnum,
      componentName,
      captchaTypeEnum,
      verifyComponentDOM,
      showBox,
      closeBox,
      showDialogOutSide,
      handleInitFailed,
      handleSuccess,
      handleError,
    };
  },
});
</script>

<style lang="scss" scoped>
/* 验证码容器 */
.verifybox {
  position: relative;
  top: 50%;
  left: 50%;
  padding: 8px 16px 24px;
  background-color: #FFF;
  // border: 1px solid red;
  border-radius: 4px;
  transform: translate(-50%, -50%);
  /* 弹框顶部标题 及 关闭按钮 */
  &-top {
    position: relative;
    display: flex;
    justify-content: flex-start;
    align-items: center;
    height: 48px;
    line-height: 16px;
    padding: 0;
    border-bottom: 1px solid #D8D8D8;
    font-size: 16px;
    color: rgba(0, 0, 0, 0.9);
    img {
      width: 16px;
      height: 16px;
      margin-right: 4px;
    }
  }
  /* 关闭按钮 */
  &-close {
    position: absolute;
    top: 50%;
    right: 0;
    font-size: 16px;
    font-weight: 700;
    color: rgba(0, 0, 0, 0.6);
    cursor: pointer;
    transform: translate(-50%, -50%);
  }
  /* 验证码组件容器 */
  &-bottom {
    box-sizing: border-box;
    padding: 0;
  }
}

/* 遮罩 */
.mask {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10001;
  width: 100%;
  height: 100vh;
  background: rgba(0, 0, 0, 0.6);
  transition: all 0.5s;
}
</style>

3.3 使用验证码组件

场景:

  • 首次进入系统,点击表格项,无需弹出验证码
  • 30s 内再次点击表格项,需要弹出验证码
  • 30s 内刷新页面,点击表格项,仍需弹出验证码;30s 外则不需要

实现思路:

  • 点击列表后,将列表项信息存到页面中
  • 如果验证码初始化失败,则不进行跳转,无法查看列表项,并提示用户
  • 从本地存储 localStorage 中获取上次点击列表项的时间戳
  • 如果没有,则证明是第一次点击列表项,直接执行打开表单详情操作即可,并存储当前点击列表项的时间
  • 如果有时间戳,则要与现在的时间进行对比;如果超过 30s,则执行打开表单详情操作即可,并存储当前点击列表项的时间;如果没超过 30s,则调用验证码 DOM 实例的方法,展示验证码
  • 用户验证码校验成功,才能进入表单查看页面,并存储当前点击列表项的时间;用户校验失败,则无法进入表单查看页面,不用存储此次点击时间
  <verify-home
    ref="verifyDOM"
    @success="handleSuccess"
    @error="handleError"
    @init-failed="handleInitFailed"
  ></verify-home>


    // 验证码组件
    import VerifyHome from '@/components/verify/verify-home.vue';

    // 验证码组件 DOM
    const verifyDOM = ref<HTMLFormElement | null>(null);

    const state = reactive({
      // 是否初始化成功
      verifyInitSuccess: true,
    })

    /**
     * 处理表格查看事件
     */
    const handleView = (row: ListFillingListItemInf) => {
      // 切换当前编辑/查看的项目id
      state.enterId = row.enterId;
      state.enterRowInfo = row;

      if (!state.verifyInitSuccess) {
        ElMessage.error('验证码初始化失败,无法查看企业信息');
        return;
      }

      // 从 localstorage 中获取上一次查看企业的时间
      const vsTime = localStorage.getItem('verifySapceTime');
      // 如果没有时间,证明为第一次查看企业信息,则直接展示即可
      if (!vsTime) {
        // 展示列表详情页
        state.viewVisible = true;
        // 新建时间戳
        localStorage.setItem('verifySapceTime', Number(new Date()).toString());
        console.log('没有时间间隔');
        // 如果有时间,则弹出验证码
      } else {
        // 如果时间间隔超过 30s,则直接展示表单,并且重置时间间隔
        // eslint-disable-next-line no-lonely-if
        if (Number(new Date()) - Number(vsTime) > 1000 * 30) {
          console.log('时间间隔超过 30s', Number(new Date()) - Number(vsTime));
          // 展示列表详情页
          state.viewVisible = true;
          // 新建时间戳
          localStorage.setItem('verifySapceTime', Number(new Date()).toString());
          // 如果时间间隔小于 30s,则展示验证码,防止频繁操作
        } else {
          console.log('时间间隔不到 30s', Number(new Date()) - Number(vsTime));
          // console.log('verifyDOM ===', verifyDOM.value);
          if (verifyDOM.value) {
            // 将验证码弹框,挂载到页面中
            (verifyDOM.value as any)!.showDialogOutSide();
          }
        }
      }
    };


    /**
     * 验证码初始化失败
     */
    const handleInitFailed = (res: any) => {
      ElMessage.error(`请联系维护人员!${res.data.repMsg}`);
      console.log('验证码初始化失败 ===', res);
      // 标识验证码初始化失败
      state.verifyInitSuccess = false;
    };


    /**
     * 验证码校验成功
     */
    const handleSuccess = async (res: any) => {
      console.log('验证码校验成功 ===', res);
      // 如果 验证码接口 没有返回二次校验参数
      if (!res.captchaVerification) {
        ElMessage.error(`验证码获取失败,请联系管理员 res.captchaVerification ${res.captchaVerification}`);
        return;
      }
      // 展示列表详情页
      state.viewVisible = true;
      // 新建时间戳
      localStorage.setItem('verifySapceTime', Number(new Date()).toString());
    };


    /**
     * 验证码校验失败
     */
    const handleError = (res: any) => {
      console.log('验证码校验失败 ===', res);
      ElMessage.error('验证码校验失败,请重试');
    };

4. 微前端相关配置文件介绍

4.1 基座应用里的文件介绍

4.1.1 public/global/config/api-config.js

此步骤打包前、后修改都可以

api-config.js 中存放的是:项目中所有微应用需要的各种接口的公共部分(例如 ip+port)和 加载微应用 没有任何关系

api-config.js 中的内容即使在打包后,再进行修改也是生效的,所以在打包前、打包后修改都行

api-config.js 目前没有分开发环境或部署环境,所以部署完项目访问系统的时候,这个文件里写的是什么,读取的就是什么

api-config.js 是通过基座中 public > global > config > config-webpack.js 加载到每个微应用中的

4.1.2 public/global/config/config-micro-app.js

此步骤打包前、后修改都可以

config-micro-app.js 中存放的是:微应用的信息,和 加载微应用 有关系,所以在不同环境部署的时候,这个文件内容必须修改

config-micro-app.js 中的内容即使在打包后,再进行修改也是生效的,所以在打包前、打包后修改都行

config-micro-app.js 只有在基座中有,微应用里都没有这个文件

4.1.3 public/global/config/config-webpack.js

此步骤 打包前修改CDN链接 和 打包后修改CDN链接 的方式不一样

config-webpack.js 中存放的是:项目中所有应用的公共依赖(所谓的公共依赖,其实最后访问的都是基座应用中 public/global/libs 下的资源)

config-webpack.js 在 所有应用(基座应用、微应用)打包前、打包时 都使用到了,唯独 打包后 没有用

config-webpack.js 中的内容,最后都会通过 webpack 插件,动态添加到 public/index.html 中去,所以修改CDN链接有种方式:

  • 打包前,就把对应的资源路径修改好,再进行打包【推荐】
  • 打包后,直接修改 dist/public/index.html 中的资源的路径,而不是改 config-webpack.js 中的路径

4.1.4 vue.config.js

此步骤 打包前修改CDN链接 和 打包后修改CDN链接 的方式不一样

基座中的 vue.config.js 一般不需要修改,他用于存放 webpack 打包配置 和 vue 项目基本配置

vue.config.js 的内容会在 打包前、打包时 使用到了,打包后 没有用

下图中的 ${name} 对应的是 package.json 中的 name ,也对应部署环境的上下文访问地址(例如:http://localhost:9160/idp-base-app/,package.json 中的 name 和 应用的上下文访问地址 对应的都是 idp-base-app)

如果需要修改应用上下文的话,有两种方式:

  • 打包前修改 package.json 中的 name,然后重新打包【推荐】
  • 打包好的 dist 文件夹里全局搜索并替换(因为有一些压缩文件中的也需要修改)

4.2 解决访问不到服务器上的 api-config.js 文件

有以下几种可能:

  • 服务器跨域
  • 访问了服务器上的 api-config.js ,但是服务器上的加密了,没解密成功
  • 解密失败还有可能是使用了 es6语法中的 const,目前仅支持 var

解决方案:

  • 修改微应用中的 webOnline,换成本地基座文件 http://localhost:8080...
  • 同时,还要修改基座中的 config-webpack.js ,把里面的 api-config.js 改成本地地址

5. 如何在开发者工具中,筛选给地图发送的消息?

  1. 打开开发者工具,选择 Network,在 Filter 中筛选 eio
  2. 随便点开一个 eio:
  3. 操作页面,使地图发生变化
  4. 在 Message 下搜索 给地图发送事件的类型(type):
  5. 点击数据,即可获得消息相关信息:

6. 使用 userAgent 判断设备类型

      // 设备 0-PC,1-移动端
      let dev = '0';
      // eslint-disable-next-line max-len
      if ((navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i))) {
        dev = '1';
      } else {
        dev = '0';
      }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lyrelion

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值