【Vue】基于前后端tianaiCaptch验证的滑块组件
代码解释
响应式兼容设计
通过vw和vh,百分比值,动态计算字体大小等响应式动态的滑块组件
@media (max-width: 480px) and (orientation: portrait) {
.container {
padding: 1.2rem;
min-height: 280px;
}
}
@media (max-height: 600px) {
.container {
transform: scale(0.9);
}
}
@media (orientation: landscape) and (max-width: 1000px) {
.container {
width: 70%;
max-width: 360px;
padding: 1.5rem;
}
}
@media (min-width: 1440px) {
.container {
max-width: 450px;
padding: 2.5rem;
}
.title {
font-size: 20px;
}
}
@media (max-width: 768px) {
.container {
width: 95%;
padding: 1.5vh 3vw;
}
.title {
font-size: 14px;
}
.button-group .img-group svg {
width: 12vw;
}
.block {
width: 12vw !important;
}
.slide::before {
font-size: 3vw !important;
}
}
@media (max-width: 480px) {
.container {
min-height: 300px;
width: 95%;
padding: 1vh;
}
.slide::before {
font-size: 12px !important;
}
}
较为美观的设计
自动转换时间格式
避免tianaiCaptch时间格式问题
/*
* @param Date() Web北京标准时间
* @return YYYY-MM-DDThh:mm:ss.sssZ
*/
getCurrentFormatDate(date) {
if (!date) return;
const pad = n => n.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T` + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.` + `${date.getMilliseconds().toString().padStart(3, '0')}Z`;
},
批量记录轨迹坐标
缓存,优化
一些CSS简化和空间内存管理优化等,优化计算
动画帧管理
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId)
}
this.animationFrameId = requestAnimationFrame(() => {
if (e.cancelable && !e.defaultPrevented) e.preventDefault();
this.handleMove(this.getmoveX(e));
});
DOM缓存
//通过缓存并且通过watch,只有在使用才一次性获取,每次只有刷新才获取
watch: {
backgroupImg() {
this.$nextTick(() => {
const bgEl = this.$el.querySelector('.inner-bg-img')
const mvEl = this.$el.querySelector('.inner-mv-img')
this.dimensions = {
bgWidth: bgEl.offsetWidth,
bgHeight: bgEl.offsetHeight,
mvWidth: mvEl.offsetWidth,
mvHeight: mvEl.offsetHeight
}
});
}
},
加速度缓存计算
通过轨迹列表记录最后3点坐标进行简单的加速度计算,进行人机的简单判断,避免异常加速度
/*
* @param trackList[n...n-2] 取轨迹列表最后* 三段计算末速度
* 不过其实可以考虑加入异常方向判断,不过懒的加,你们可以看着加,方法就是根据xy轴进行判断
* @return a 加速度
*/
getAcceleration() {
if (this.trackList.length < 3) return 0;
const [p1, p2, p3] = this.trackList.slice(-3);
const dv1 = (p2.x - p1.x) / ((p2.t - p1.t) || 1) * 0.001;
const dv2 = (p3.x - p2.x) / ((p3.t - p2.t) || 1) * 0.001;
const dt = (p3.t - p1.t) / 2000;
const acceleration = dt !== 0 ? (dv2 - dv1) / dt * 0.0002645833 : 0;
return parseInt(acceleration);
},
logo图片转base64——Bug修复
通过硬编码转换,避免同源策略导致无法显示图片,对于相对路径,需要配合构建工具
/**
* @param URL || FileObject || path(需要配合构建工具)
* @return Base64
*/
isValidUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
},
async handleFileInput() {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
this.logoImag = e.target.result;
resolve();
};
reader.onerror = reject;
reader.readAsDataURL(this.logoImag);
});
},
async handleRemoteUrl() {
const response = await fetch(this.logoImag, {
mode: 'cors',
credentials: 'same-origin'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
return this.handleFileBlob(blob);
},
async handleFileBlob(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
this.logoImag = e.target.result;
resolve();
};
reader.readAsDataURL(blob);
});
},
// 处理相对路径(需要配合构建工具)
async handleLocalPath() {
const module = await import(this.logoImag);
this.logoImag = module.default;
},
async fileToBase64() {
if (!this.logoImag || this.logoImag === logoImg || this.logoImag.startsWith('data:image')) return;
try {
if (this.logoImag instanceof File) {
await this.handleFileInput();
} else if (typeof this.logoImag === 'string') {
if (this.isValidUrl(this.logoImag)) {
await this.handleRemoteUrl();
} else {
await this.handleLocalPath();
}
}
} catch (e) {
this.printLog('fileToBase64', 'debug', `转换失败:${e}`);
}
},
统一的事件处理
移动端触摸,PC端的鼠标都兼容,设备信息获取的兼容
/**
* 统一滑块处理时事件
*/
handleMove(moveX) {
try {
this.maxMove = this.dimensions.bgWidth - this.dimensions.mvWidth + 5;
this.moveX = Math.max(0, Math.min(moveX, this.maxMove))
this.percentage = parseFloat(this.percentager);
const tempTrack = {
x: parseFloat((this.moveX + Math.random() * 2).toFixed(2)),
y: 0.00,
t: parseFloat((Date.now() - this.startTime.getTime()).toFixed(2))
};
if (this.startTime) {
//节流优化,用普通数组替代vue渲染
this._tempTrackList.push(tempTrack);
if (this._tempTrackList.length % 60 === 0) {
this.trackList = [...this._tempTrackList]
}
}
} catch (e) { this.printLog('handleMove', 'debug', e); }
}
使用教程README.md
请根据自行根据图片路径修改 Slider.vue
组件的图片路径
请尽量不要修改其它图片,因为响应式计算是基于图片大小的,修改图片大小会导致响应式计算失效。
- 本组件可传递logo文件对象或者URL,若是必须要相对路径请考虑配合构建工具————加入属性
:logoImag="File || URL"
即可 - 本组件可传递是否输出日志到控制台————加入属性
:log="true"
即可,默认值为true
vue 组件使用教程
vue3选项式API组件使用教程
<script>
import Slider from './Slider.vue'
export default {
components: {
Slider;
},
data() {
return {
show: false;
}
},
methods: {
/**
* @callback 回调函数,调用Slider组件的获取图片方法,请传递请求接口后返回的response.data参数
*/
获取后端图片方法名(callback) {
// 请在此处调用后端接口获取图片数据,并且callback回调函数传递response.data参数
},
/**
* @param id 验证码的唯一标识,Slider组件已提供,调用后台接口带上参数即可
* @param percentage 图片的百分比,Slider组件已提供,调用后台接口带上参数即可
* @param Data tianai captcha的各种数据,Slider组件已提供,调用后台接口带上参数即可
* @callback 回调函数,调用Slider组件的校验图片方法,请传递response.success||response.code参数,返回校验结果
*/
校验图片方法(id,percentage,Data,callback) {
// 请在此处调用校验图片请求接口方法
},
关闭方法() {
// 请在此处调用关闭组件的方法,如清空图片列表等
//类似于代码即可
this.show = false;
}
}
}
</script>
<template>
<!---请用 v-if 控制组件的显示-->
<div v-if="show">
<Slider @getImg="获取后端图片方法名" @validImg="校验图片方法" @close="关闭方法" :logoImag="不支持相对路径,需要的请配合构建工具" :log="true"/>
</div>
</template>
vue3API组合式使用教程
<script>
import Slider from './Slider.vue';
import {ref} from 'vue';
const show=ref(false);
/**
* @callback 回调函数,调用Slider组件的获取图片方法,请传递请求接口后返回的response.data参数
*/
const 获取后端图片方法名=(callback)=> {
// 请在此处调用后端接口获取图片数据,并且callback回调函数传递response.data参数
}
/**
* @param id 验证码的唯一标识,Slider组件已提供,调用后台接口带上参数即可
* @param percentage 图片的百分比,Slider组件已提供,调用后台接口带上参数即可
* @param Data tianai captcha的各种数据,Slider组件已提供,调用后台接口带上参数即可
* @callback 回调函数,调用Slider组件的校验图片方法,请传递response.success||response.code参数,返回校验结果
*/
const 校验图片方法=(id,percentage,Data,callback)=> {
// 请在此处调用校验图片请求接口方法
}
//类似于代码即可
const 关闭方法()=> {
// 请在此处调用关闭组件的方法,如清空图片列表等
this.show = false;
}
</script>
<template>
<!---请用 v-if 控制组件的显示-->
<div v-if="show">
<Slider @getImg="获取后端图片方法名" @validImg="校验图片方法" @close="关闭方法" :logoImag="不支持相对路径,需要的请配合构建工具" :log="true"/>
</div>
</template>
数据传递结构
总代码
<script>
//真要改logo,推荐直接修改这里的字符串
const logoImg = "data:image/png;base64,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";
//获取滑块图片方法
const GET_IMG_FUN = "getImg";
//校验滑块图片方法
const VALID_IMG_FUN = "validImg";
//滑块窗口关闭事件监听
const CLOST_EVENT_FUN = "close";
export default {
data() {
return {
// 临时数据
dimensions: {
bgWidth: 0,
bgHeight: 0,
mvWidth: 0,
mvHeight: 0
},
_tempTrackList: [],
/**相关参数 */
/**划过的百分比 */
percentage: 0,
trackList: [],
startTime: null,
stopTime: null,
/**是否显示提示信息 */
tips: false,
/**滑块背景图片 */
backgroupImg: "",
/**滑块图片 */
moveImg: "",
/**是否已经移动滑块 */
startMove: false,
/**开始滑动的x轴 */
startX: 0,
/**验证码唯一ID */
uuid: "",
/**滑块移动的x轴 */
moveX: 0,
/** 加载遮罩标识 */
loading: true,
/** 请求flag */
result: false,
/**滑动耗时 */
verifyTime: '0',
/** 加速度 */
a: 0,
maxMove: 0,
animationFrameId: null
};
},
props: {
logoImag: {
type: String,
default: logoImg
},
// 是否开启日志, 默认true
log: {
type: Boolean,
default: true
}
},
mounted() {
this.fileToBase64();
this.getImg();
},
computed: {
getAcceleration() {
if (this.trackList.length < 3) return 0;
const [p1, p2, p3] = this.trackList.slice(-3);
const dv1 = (p2.x - p1.x) / ((p2.t - p1.t) || 1) * 0.001;
const dv2 = (p3.x - p2.x) / ((p3.t - p2.t) || 1) * 0.001;
const dt = (p3.t - p1.t) / 2000;
const acceleration = dt !== 0 ? (dv2 - dv1) / dt * 0.0002645833 : 0;
return parseInt(acceleration);
},
percentager() {
return (this.moveX / this.dimensions.bgWidth);
},
},
methods: {
isValidUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
},
async handleFileInput() {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
this.logoImag = e.target.result;
resolve();
};
reader.onerror = reject;
reader.readAsDataURL(this.logoImag);
});
},
async handleRemoteUrl() {
const response = await fetch(this.logoImag, {
mode: 'cors',
credentials: 'same-origin'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
return this.handleFileBlob(blob);
},
async handleFileBlob(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
this.logoImag = e.target.result;
resolve();
};
reader.readAsDataURL(blob);
});
},
// 处理相对路径(需要配合构建工具)
async handleLocalPath() {
const module = await import(this.logoImag);
this.logoImag = module.default;
},
async fileToBase64() {
if (!this.logoImag || this.logoImag === logoImg || this.logoImag.startsWith('data:image')) return;
try {
if (this.logoImag instanceof File) {
await this.handleFileInput();
} else if (typeof this.logoImag === 'string') {
if (this.isValidUrl(this.logoImag)) {
await this.handleRemoteUrl();
} else {
await this.handleLocalPath();
}
}
} catch (e) {
this.printLog('fileToBase64', 'debug', `转换失败:${e}`);
}
},
/**
* 生产环境禁用日志
* 打印日志
*/
printLog(msg, level, ...optionalParams) {
if (!this.log) return;
if (process.env.NODE_ENV === 'production' && level === 'debug') return
if (optionalParams && optionalParams.length > 0) {
optionalParams = (optionalParams.length === 1 ? optionalParams[0] : optionalParams);
if (level === 'error') {
console.error(`滑块验证码[${msg}]`, optionalParams);
} else if (level === 'warn') {
console.warn(`滑块验证码[${msg}]`, optionalParams);
} else if (level === 'debug') {
console.debug(`滑块验证码[${msg}]`, optionalParams);
} else if (level === 'info') {
console.info(`滑块验证码[${msg}]`, optionalParams);
}
}
},
addEventListener() {
window.addEventListener("mousemove", this.move);
window.addEventListener("mouseup", this.up);
window.addEventListener("touchmove", this.move, { passive: true, capture: true });
window.addEventListener("touchend", this.up, { passive: false });
},
removeEventListener() {
window.removeEventListener("mousemove", this.move,);
window.removeEventListener("mouseup", this.up);
window.removeEventListener("touchmove", this.move);
window.removeEventListener("touchend", this.up);
window.removeEventListener("mousedown", this.start);
window.removeEventListener("touchstart", this.start);
},
/**
* 父组件调用请用callback回调函数传入(data响应数据--请看具体传回数据格式)
* 获取滑块图片
*/
getImg() {
try {
this.$emit(GET_IMG_FUN, data => {
this.printLog(GET_IMG_FUN, data);
if (!data) return;
this.backgroupImg = data.captcha.backgroundImage;
if (data.sliderImage === undefined) {
this.moveImg = data.captcha.templateImage;
} else {
this.moveImg = data.captcha.sliderImage;
}
this.uuid = data.id;
this.loading = false;
});
} catch (e) { this.printLog('getImg', 'debug', e); }
},
/**校验图片
* @param id 验证码唯一ID
* @param data 校验数据
* @callback callback 回调函数
* ======旧版本校验=====
* * @param percentage 滑块划过百分比 可以使用但不推荐
* ======新版本校验=====
* ========Track Data========
* * @param bgImageWidth 背景图片宽
* * @param bgImageHeight 背景图片高
* * @param templateImageWidth 模板图片宽
* * @param templateImageHeight 模板图片高
* * @param startTime 滑块开始滑动时间
* * @param stopTime 滑块滑动结束时间
* * @param trackList 滑动轨迹列表
* * @param data 业务数据自定义扩展数据,用户加密等数据,请在组件外调用callback回调函数传入
* ======== Drives Data=====
* * @param userAgent 验证客户端信息
* * @param windowHeight 窗口高度
* * @param windowWidth 窗口宽度
* * @param language 语言
* * @param hasXhr 是否支持xhr请求
* * @param platform 平台信息
* * @param hardwareConcurrency 硬件并发数
* * @param href 页面链接
*/
validImg() {
const isInvaild = Math.abs(this.a) > 10;
if (isInvaild) {
this.printLog('validImg', 'warn', `加速度异常检测:${this.a}`);
this.reset();
return;
}
if (0 === this.percentage || this.percentage >= 1) return;
try {
this.trackList = [...this._tempTrackList];
const Data = {
track: {
bgImageWidth: this.dimensions.bgWidth,
bgImageHeight: this.dimensions.bgHeight,
templateImageWidth: this.dimensions.mvWidth,
templateImageHeight: this.dimensions.mvHeight,
startTime: this.getCurrentFormatDate(this.startTime),
stopTime: this.getCurrentFormatDate(this.stopTime),
trackList: this.trackList
},
drives: {
userAgent: (navigator.userAgent || 'unknown'),
windowHeight: (window.innerHeight || document.documentElement.clientHeight),
windowWidth: (window.innerWidth || document.documentElement.clientWidth),
language: (
navigator.language ||
navigator.userLanguage ||
navigator.browserLanguage ||
navigator.systemLanguage ||
'unknown'
),
hasXhr: (
typeof XMLHttpRequest !== 'undefined' ||
typeof ActiveXObject !== 'undefined'
),
platform: (navigator.platform || 'unknown'),
hardwareConcurrency: (navigator.hardwareConcurrency || 1),
href: window.location.href
}
}
this.printLog(`validImg`, 'debug', { id: this.uuid, percentage: this.percentage, Data });
this.$emit(
VALID_IMG_FUN,
this.uuid,
parseFloat(this.percentage),
Data,
data => {
this.printLog(VALID_IMG_FUN, data);
const flag = (data === false) || (parseInt(data) != 200);
this.$el.querySelector('.tips').style.setProperty('color', flag ? '#ff4d4f' : '#52c41a');
this.tips = true;
if (flag) {
setTimeout(() => this.reset(), 1500);
} else {
this.result = true;
setTimeout(() => this.close(), 1500);
}
});
} catch (e) {
this.printLog('validImg', 'debug', e);
this.reset();
}
},
getCurrentFormatDate(date) {
if (!date) return;
const pad = n => n.toString().padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T` + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.` + `${date.getMilliseconds().toString().padStart(3, '0')}Z`;
},
//获取耗时时间
getTimeDifference(diffeTime) {
if (!diffeTime) return;
if (diffeTime < 1000) {
return `${Math.round(diffeTime, 2)}`;
}
return `${diffeTime / 1000}.${(diffeTime % 1000).toFixed(2)}`;
},
/**
* 重新生成图片
*/
reset() {
Object.assign(this.$data, {
startTime: null,
stopTime: null,
trackList: [],
verifyTime: 0,
tips: false,
moveX: 0,
percentage: 0,
startX: 0,
blcokLeft: 0,
result: false,
loading: true,
a: 0,
});
this.getImg();
},
/**
* 按钮关闭事件
*/
close() {
this.printLog('close', 'debug', "关闭按钮触发");
this.$emit(CLOST_EVENT_FUN);
},
/**
* 统一滑块处理时事件
*/
handleMove(moveX) {
try {
this.maxMove = this.dimensions.bgWidth - this.dimensions.mvWidth + 5;
this.moveX = Math.max(0, Math.min(moveX, this.maxMove))
this.percentage = parseFloat(this.percentager);
const tempTrack = {
x: parseFloat((this.moveX + Math.random() * 2).toFixed(2)),
y: 0.00,
t: parseFloat((Date.now() - this.startTime.getTime()).toFixed(2))
};
if (this.startTime) {
this._tempTrackList.push(tempTrack);
if (this._tempTrackList.length % 60 === 0) {
this.trackList = [...this._tempTrackList]
}
}
} catch (e) { this.printLog('handleMove', 'debug', e); }
},
getmoveX(e) {
return (e.clientX || (e.touches && e.touches[0].clientX) || e.changedTouches[0].clientX) - this.startX;
},
getStartX(e) {
return e.clientX || (e.touches && e.touches[0].clientX) || e.changedTouches[0].clientX;
},
/**
* 开始滑动
*/
start(e) {
try {
this.startTime = new Date();
this.addEventListener();
if (e.type === 'touchstart') {
e.preventDefault();
e.stopPropagation();
}
this.startMove = true;
document.body.style.overflow = 'hidden';
this.startX = this.getStartX(e);
this.trackList.push({
x: parseFloat(this.startX),
y: 0.00,
t: parseFloat(this.startTime.getTime()),
type: 'DOWN'
});
} catch (e) { this.printLog('start', 'debug'.e) }
},
/**
* 滑块滑动事件
*/
move(e) {
if (!this.startMove) return;
this.startTime = new Date();
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId)
}
this.animationFrameId = requestAnimationFrame(() => {
if (e.cancelable && !e.defaultPrevented) e.preventDefault();
this.handleMove(this.getmoveX(e));
});
},
/**
* 滑块抬起事件
*/
up(e) {
try {
this.stopTime = new Date();
this.removeEventListener();
document.body.style.overflow = '';
if (!this.startMove || !this.startTime || !(this.startTime instanceof Date)) {
this.startMove = false;
return;
}
if (this.startTime && this.stopTime) {
this.trackList.push({
x: parseFloat(this.moveX),
y: 0.00,
t: parseFloat(this.stopTime.getTime()),
type: 'UP'
});
}
this.a = this.getAcceleration;
this.startMove = false;
this.verifyTime = this.getTimeDifference(this.stopTime.getTime() - this.startTime.getTime());
this.printLog('up', 'debug', `滑动百分比:${this.percentage}`, `耗时:${this.verifyTime}ms`);
this.validImg();
} catch (e) { this.printLog('up', 'debug', e) }
}
},
watch: {
backgroupImg() {
this.$nextTick(() => {
const bgEl = this.$el.querySelector('.inner-bg-img')
const mvEl = this.$el.querySelector('.inner-mv-img')
this.dimensions = {
bgWidth: bgEl.offsetWidth,
bgHeight: bgEl.offsetHeight,
mvWidth: mvEl.offsetWidth,
mvHeight: mvEl.offsetHeight
}
console.debug('mounted', 'debug', this.dimensions);
});
}
},
/**
* 销毁事件
*/
beforeDestroy() {
this.dimensions = null;
this.trackList = null;
this._tempTrackList = null;
this.$el.parentNode.removeChild(this.$el);
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
document.body.style.overflow = '';
document.body.style.touchAction = '';
document.body.style.overscrollBehavior = '';
this.removeEventListener();
this.startTime = null;
this.stopTime = null;
this.printLog('beforeDestroy', 'debug', "销毁组件");
}
}
</script>
<template>
<div class="slider">
<div class="container">
<div class="title">
<b>请完成下列验证后继续</b>
</div>
<div class="mask">
<div class="loading" v-if="loading">
<img src="../assets/loading.gif" />
</div>
<div class="img">
<div class="backgroup-img">
<img class="inner-bg-img" :src="backgroupImg" />
<div class="tips" v-show="tips">
<span v-if="result">
✅ 验证成功,耗时{{ verifyTime }}ms
</span>
<span v-else>
❌ 验证失败,耗时{{ verifyTime }}ms
</span>
</div>
<div class="move-img" :style="{ transform: `translateX(${moveX}px)` }">
<img class="inner-mv-img" :src="moveImg" />
</div>
</div>
</div>
<div class="slide">
<div class="slider-mask" :style="{ width: `${moveX}px` }">
<div class="block" ref="block" @mousedown="start" @touchstart="start"
:style="{ transform: `translateX(${moveX}px)` }">
<img class="slider-Icon">
</div>
</div>
</div>
</div>
<div class="button-group">
<img :src="logoImag" class="logo" />
<div class="img-group">
<svg @click="reset" viewBox="0 0 1024 1024">
<path
d="M943.8 484.1c-17.5-13.7-42.8-10.7-56.6 6.8-5.7 7.3-8.5 15.8-8.6 24.4h-0.4c-0.6 78.3-26.1 157-78 223.3-124.9 159.2-356 187.1-515.2 62.3-31.7-24.9-58.2-54-79.3-85.9h77.1c22.4 0 40.7-18.3 40.7-40.7v-3c0-22.4-18.3-40.7-40.7-40.7H105.5c-22.4 0-40.7 18.3-40.7 40.7v177.3c0 22.4 18.3 40.7 40.7 40.7h3c22.4 0 40.7-18.3 40.7-40.7v-73.1c24.2 33.3 53 63.1 86 89 47.6 37.3 101 64.2 158.9 79.9 55.9 15.2 113.5 19.3 171.2 12.3 57.7-7 112.7-24.7 163.3-52.8 52.5-29 98-67.9 135.3-115.4 37.3-47.6 64.2-101 79.9-158.9 10.2-37.6 15.4-76 15.6-114.6h-0.1c-0.3-11.6-5.5-23.1-15.5-30.9zM918.7 135.2h-3c-22.4 0-40.7 18.3-40.7 40.7V249c-24.2-33.3-53-63.1-86-89-47.6-37.3-101-64.2-158.9-79.9-55.9-15.2-113.5-19.3-171.2-12.3-57.7 7-112.7 24.7-163.3 52.8-52.5 29-98 67.9-135.3 115.4-37.3 47.5-64.2 101-79.9 158.8-10.2 37.6-15.4 76-15.6 114.6h0.1c0.2 11.7 5.5 23.2 15.4 30.9 17.5 13.7 42.8 10.7 56.6-6.8 5.7-7.3 8.5-15.8 8.6-24.4h0.4c0.6-78.3 26.1-157 78-223.3 124.9-159.2 356-187.1 515.2-62.3 31.7 24.9 58.2 54 79.3 85.9h-77.1c-22.4 0-40.7 18.3-40.7 40.7v3c0 22.4 18.3 40.7 40.7 40.7h177.3c22.4 0 40.7-18.3 40.7-40.7V175.8c0.1-22.3-18.2-40.6-40.6-40.6z"
fill="#5e5c5c"></path>
</svg>
<svg @click="close" viewBox="0 0 1024 1024">
<path
d="M512 42.666667a469.333333 469.333333 0 1 0 469.333333 469.333333A469.333333 469.333333 0 0 0 512 42.666667z m0 864a394.666667 394.666667 0 1 1 394.666667-394.666667 395.146667 395.146667 0 0 1-394.666667 394.666667z"
fill="#5e5c5c"></path>
<path
d="M670.4 300.8l-154.666667 154.666667a5.333333 5.333333 0 0 1-7.573333 0l-154.666667-154.666667a5.333333 5.333333 0 0 0-7.52 0l-45.173333 45.28a5.333333 5.333333 0 0 0 0 7.52l154.666667 154.666667a5.333333 5.333333 0 0 1 0 7.573333l-154.666667 154.666667a5.333333 5.333333 0 0 0 0 7.52l45.28 45.28a5.333333 5.333333 0 0 0 7.52 0l154.666667-154.666667a5.333333 5.333333 0 0 1 7.573333 0l154.666667 154.666667a5.333333 5.333333 0 0 0 7.52 0l45.28-45.28a5.333333 5.333333 0 0 0 0-7.52l-154.666667-154.666667a5.333333 5.333333 0 0 1 0-7.573333l154.666667-154.666667a5.333333 5.333333 0 0 0 0-7.52l-45.28-45.28a5.333333 5.333333 0 0 0-7.626667 0z"
fill="#5e5c5c"></path>
</svg>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.silder-mask {
position: absolute;
border: 1px solid #1991fa;
background: #d1e9fe;
border-radius: 10px;
}
.slider {
inset: 0;
margin: 0 auto;
top: 25%;
width: min(90%, 400px);
position: absolute;
z-index: 999;
.container {
z-index: 999;
display: grid;
grid-template-rows: 18px 1fr 60px;
gap: 0.5rem;
width: min(90%, 400px);
position: absolute;
place-self: center;
min-height: 320px;
height: auto;
padding: 2rem;
background: url(../assets/images/slider-bg.png) center/cover;
border-radius: 10px;
box-shadow: #1991fa 0px 0px 20px 0px;
/* box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); */
.title {
font-family: Arial, Helvetica, sans-serif;
font-size: clamp(16px, 2.5vw, 18px);
color: rgba(155, 4, 255, 1.2);
}
.button-group {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
img,
svg {
aspect-ratio: 1/1;
}
img {
width: clamp(40px, 10vw, 60px);
}
svg {
align-self: center;
&:nth-of-type(1) {
margin-right: 0.2rem;
}
width: clamp(40px, 10vw, 40px);
cursor: pointer;
}
}
.mask {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
position: relative;
.loading {
display: flex;
align-items: center;
position: absolute;
width: 100%;
height: 100%;
z-index: 1003;
img {
margin: 0 auto;
aspect-ratio: 1;
}
}
.move-img {
transition: transform 0.1s ease;
will-change: transform;
z-index: 1000;
position: absolute;
width: calc(110 / 600 * 100%);
height: auto;
backface-visibility: hidden;
}
.img {
box-shadow: #e7c6e4 0px 0px 20px 0px;
aspect-ratio: 16/9;
position: relative;
}
.backgroup-img {
aspect-ratio: 5/3;
width: 100%;
height: 100%;
position: relative;
box-shadow: 0 4px 12px rgba(24, 78, 213, 0.3);
.inner-bg-img {
width: 100%;
height: 100%;
}
.tips {
max-width: 600px;
width: 100%;
background: linear-gradient(145deg, rgba(218, 57, 254, 0.5), rgba(24, 144, 255, 0.5));
font-size: clamp(12px, 2vw, 16px);
bottom: 0;
position: absolute;
text-align: center;
padding: 0.5rem 1rem;
border-radius: 4px;
color: rgb(255, 0, 0);
transition: all 0.3s ease;
z-index: 1002;
}
}
.move-img {
top: 0;
left: 0;
transition: transform 0.1s ease;
will-change: transform;
position: absolute;
height: 100%;
z-index: 1001;
backface-visibility: hidden;
img {
position: absolute;
left: 0;
width: 100%;
aspect-ratio: 11/36;
}
}
.slide {
touch-action: none;
overscroll-behavior: contain;
aspect-ratio: 1/1;
width: 100%;
border-radius: 10px;
background-color: rgba(125, 171, 237, 0.3);
position: relative;
height: clamp(40px, 6vh, 50px);
margin-top: 1rem;
&::before {
inset: 0;
text-align: center;
align-self: center;
position: absolute;
content: "按住左边按钮移动完成上方拼图";
font-size: clamp(10px, 1.5vw, 14px);
color: #999;
}
.slider-mask {
position: absolute;
height: 100%;
border-radius: 10px;
background: linear-gradient(145deg, rgba(218, 57, 254, 0.5), rgba(69, 150, 185, 0.33));
}
.block {
border-radius: 10px;
aspect-ratio: 1/1;
width: clamp(40px, 10vw, 50px) !important;
height: 100%;
transition: transform 0.1s ease;
position: absolute;
cursor: pointer;
will-change: transform;
transform: translateZ(0);
backface-visibility: hidden;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
&:active {
filter: brightness(0.9);
transition: filter 0.1s;
}
.slider-Icon {
border-radius: 10px;
aspect-ratio: 1/1;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: url("../assets/images/btn3.png") center/cover;
}
}
}
}
}
}
@media (max-width: 480px) and (orientation: portrait) {
.container {
padding: 1.2rem;
min-height: 280px;
}
}
@media (max-height: 600px) {
.container {
transform: scale(0.9);
}
}
@media (orientation: landscape) and (max-width: 1000px) {
.container {
width: 70%;
max-width: 360px;
padding: 1.5rem;
}
}
@media (min-width: 1440px) {
.container {
max-width: 450px;
padding: 2.5rem;
}
.title {
font-size: 20px;
}
}
@media (max-width: 768px) {
.container {
width: 95%;
padding: 1.5vh 3vw;
}
.title {
font-size: 14px;
}
.button-group .img-group svg {
width: 12vw;
}
.block {
width: 12vw !important;
}
.slide::before {
font-size: 3vw !important;
}
}
@media (max-width: 480px) {
.container {
min-height: 300px;
width: 95%;
padding: 1vh;
}
.slide::before {
font-size: 12px !important;
}
}
@keyframes shake {
0% { transform: translate(-50%, 0) }
25% { transform: translate(-55%, 0) }
50% { transform: translate(-45%, 0) }
75% { transform: translate(-50%, 0) }
100% { transform: translate(-50%, 0) }
}
</style>