目录
1.1 页面缩放后,滚动条虽然有效,但页面底部会有部分内容被遮挡
1.2 video 标签的 autoplay 属性不起作用,视频无法自动播放
1.3 使用 a 标签在 Android / IOS 上发送短信不兼容
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.2 解决访问不到服务器上的 api-config.js 文件
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. 如何在开发者工具中,筛选给地图发送的消息?
- 打开开发者工具,选择 Network,在 Filter 中筛选 eio:
- 随便点开一个 eio:
- 操作页面,使地图发生变化
- 在 Message 下搜索 给地图发送事件的类型(type):
- 点击数据,即可获得消息相关信息:
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';
}