@wiajs/ui
Version:
wia app ui packages
484 lines (483 loc) • 17.2 kB
JavaScript
/** @jsx-x jsx */ /** @jsxImportSource @wiajs/core */ import { jsx as _jsx, jsxs as _jsxs } from "@wiajs/core/jsx-runtime";
import { Event } from '@wiajs/core';
import * as css from './index.less';
// const css = styles['wiaui-verify']
// console.log('verify', {styles});
/**
* @typedef {Object} OptType
* @prop {string} el 容器
* @prop {string} lang 语言
* @prop {string} url 图片下载网址
* @prop {number} count 图片数量,随机数获取
* @prop {string} tip 滑块半径
* @prop {number} sr 滑块半径
* @prop {number} sw 滑块边长
* @prop {number} srw 滑块实际边长
* @prop {number} width 实际宽度,iPhone会缩小
* @prop {number} ratio 服务器图片与手机实际宽度缩小比
* @prop {number} cw canvas宽度
* @prop {number} ch canvas高度
* @prop {number} len
* @prop {number} x
* @prop {number} y
* @prop {()=>*} onSuccess
* @prop {()=>*} onFail
* @prop {()=>*} onRefresh
* @prop {string} mobile
* @prop {{refresh: string, slider: string, succ: string, fail: string}} icon
*/ const def = {
el: '.wiaui_verify',
lang: 'zh',
tip: '请向右滑动填充拼图',
// url: 'https://cos.wia.pub/wiajs/img/verify', // 图片下载网址
url: 'https://test.lianlian.pub/auth',
count: 39,
sr: 7.5,
sw: 42,
srw: 42,
cw: 300,
ch: 200,
len: 0,
x: 0,
y: 0,
icon: {
refresh: '',
slider: '',
succ: '',
fail: ''
}
};
/** @enum {number} */ const Status = {
fail: -1,
null: 0,
succ: 1,
ready: 2,
over: 3,
smsMax: 4
};
/**
* 滑动条验证
*/ export default class Verify extends Event {
/**
* 构造函数
* @param {*} page Page 实例
* @param {*} opts
*/ constructor(page, opts = {}){
super(opts, [
page
]), this.status = Status.null;
const _ = this;
try {
if (opts.lang === 'en') opts.tip = opts.tip ?? 'please slide to fill';
/** @type {OptType} */ const opt = {
...def,
...opts
};
const { width } = opt;
// 匹配iPhone屏幕避免页面左右滑动,iPhone 13 屏幕实际宽度340,原因不明
if (width && width < opt.cw) {
opt.ratio = opt.cw / width;
opt.cw = width;
opt.ch = Math.round(opt.ch / opt.ratio);
} else opt.ratio = 1;
opt.srw = Math.round((opt.sw + opt.sr * 2 + 9) / opt.ratio) // 滑块实际边长
;
_.opt = opt;
_.el = $(opt.el);
if (opt.mobile) _.mobile = opt.mobile;
if (opt.onSuccess) _.onSuccess = opt.onSuccess;
if (opt.onFail) _.onFail = opt.onFail;
if (opt.onRefresh) _.onRefresh = opt.onRefresh;
_.init();
} catch (e) {
console.error(`constructor exp:${e.message}`);
}
}
/**
* 初始化
*/ async init() {
const _ = this;
const { opt } = _;
let { el } = _;
try {
const { tip, ch, cw, srw } = opt;
const html = /*#__PURE__*/ _jsxs("div", {
class: css.wiaui_verify,
style: `width: ${cw}px`,
children: [
/*#__PURE__*/ _jsx("canvas", {
width: cw,
height: ch
}),
/*#__PURE__*/ _jsx("div", {
class: css.refreshIcon,
children: /*#__PURE__*/ _jsx("i", {
class: "wiaicon",
children: opt.icon.refresh
})
}),
/*#__PURE__*/ _jsx("canvas", {
class: css.piece,
width: srw,
height: ch
}),
/*#__PURE__*/ _jsxs("div", {
class: css.sliderContainer,
style: `width: ${cw}px`,
children: [
/*#__PURE__*/ _jsx("div", {
class: css.sliderMask,
children: /*#__PURE__*/ _jsx("div", {
class: css.slider,
children: /*#__PURE__*/ _jsx("span", {
class: css.sliderIcon,
children: /*#__PURE__*/ _jsx("i", {
class: "wiaicon rot-90",
children: opt.icon.slider
})
})
})
}),
/*#__PURE__*/ _jsx("span", {
class: css.sliderText,
children: tip
})
]
})
]
});
// 替换当前节点
const prev = el.before(html).prev();
el.remove();
el = prev;
$.el = prev;
// 所有直接子元素节点
const child = el.dom.children;
[_.canvas, _.refreshIcon, _.block, _.sliderContainer] = child;
[_.sliderMask, _.text] = _.sliderContainer.children;
[_.slider] = _.sliderMask.children;
[_.sliderIcon] = _.slider.children;
_.backCtx = _.canvas.getContext('2d');
_.blockCtx = _.block.getContext('2d');
scale(_.backCtx);
scale(_.blockCtx);
_.trail = [];
_.bind();
} catch (e) {
console.error(`init exp:${e.message}`);
}
}
/**
* 加载图片
* @param {string} mobile
*/ async loadImg(mobile) {
if (!mobile || !/1\d{10}/.test(mobile)) return;
const _ = this;
const { opt } = _;
const { cw, ch, srw } = opt;
try {
_.mobile = mobile;
const rs = await _.getImg();
if (rs) {
const imgs = await Promise.all([
loadImg(rs.back),
loadImg(rs.block)
]);
if (imgs) {
_.backCtx.drawImage(imgs[0], 0, 0, cw, ch) // 背景
;
_.blockCtx.drawImage(imgs[1], 0, 0, srw, ch) // 背景
;
_.status = Status.ready;
}
}
} catch (e) {
console.log(`loadImg exp:${e.message}`);
}
}
/**
* 清除
*/ clean() {
const _ = this;
const { opt } = _;
const { cw, ch, srw } = opt;
_.backCtx.clearRect(0, 0, cw, ch);
_.blockCtx.clearRect(0, 0, srw, ch);
// _.block.width = cw;
}
/**
* 绑定事件
*/ bind() {
const _ = this;
const { opt } = _;
const { cw, icon } = opt;
// _.el.dom.onselectstart = () => false;
$(_.refreshIcon).click(async (ev)=>{
await _.reload();
if (_.onRefresh) _.onRefresh();
});
/** {number} */ let originX = 0;
/** {number} */ let originY = 0;
/** {number[]} */ const trail = [];
/** {boolean} */ let isMouseDown = false;
/**
* 滑动开始
* @param {MouseEvent | TouchEvent} ev
*/ function handleStart(ev) {
if (ev instanceof MouseEvent) {
originX = ev.clientX;
originY = ev.clientY;
} else if (ev instanceof TouchEvent) {
originX = ev.touches[0].clientX;
originY = ev.touches[0].clientY;
}
isMouseDown = true;
}
/**
* 滑动
* @param {MouseEvent | TouchEvent} ev
*/ function handleMove(ev) {
if (!isMouseDown) return false;
let eventX = 0;
let eventY = 0;
if (ev instanceof MouseEvent) {
eventX = ev.clientX;
eventY = ev.clientY;
} else if (ev instanceof TouchEvent) {
eventX = ev.touches[0].clientX;
eventY = ev.touches[0].clientY;
}
const moveX = eventX - originX;
const moveY = eventY - originY;
if (moveX < 0 || moveX + 58 >= cw) return false;
_.slider.style.left = `${moveX}px`;
// const blockLeft = (w - 40 - 20) / (w - 40) * moveX
const blockLeft = moveX;
_.block.style.left = `${blockLeft}px`;
$(_.sliderContainer).addClass(css.sliderContainer_active);
_.sliderMask.style.width = `${moveX + 12}px`;
trail.push(moveY);
}
/**
*
* @param {MouseEvent | TouchEvent} ev
*/ async function handleEnd(ev) {
if (!isMouseDown) return false;
isMouseDown = false;
let eventX = 0;
if (ev instanceof MouseEvent) {
eventX = ev.clientX;
} else if (ev instanceof TouchEvent) {
eventX = ev.changedTouches[0].clientX;
}
if (eventX === originX) return false;
$(_.sliderContainer).removeClass(css.sliderContainer_active);
_.trail = trail;
if (_.status !== Status.over) _.status = await _.verify();
switch(_.status){
case Status.succ:
$(_.sliderIcon).html(`<i class="wiaicon">${icon.succ}</i>`);
// $(_.sliderIcon).html(`<i class="fas fa-check" aria-hidden="true"></i>`);
$(_.sliderContainer).addClass(css.sliderContainer_success);
if (_.onSuccess) _.onSuccess();
break;
case Status.ready:
$(_.sliderContainer).addClass(css.sliderContainer_fail);
$(_.text).html('差点成功,请再试一次');
_.reset();
break;
case Status.over:
$(_.sliderContainer).addClass(css.sliderContainer_fail);
$(_.text).html('请点击右上角刷新图标');
_.reset();
alert('请点击右上角刷新图标重新匹配!');
break;
case Status.smsMax:
$(_.sliderContainer).addClass(css.sliderContainer_fail);
$(_.text).html('登录次数太多,请联系管理员!');
alert('登录次数太多,请联系管理员!');
if (_.onFail) _.onFail(Status.smsMax);
break;
default:
{
$(_.sliderIcon).html(`<i class="wiaicon">${icon.fail}</i>`);
$(_.sliderContainer).addClass(css.sliderContainer_fail);
if (_.onFail) _.onFail(Status.fail);
setTimeout(()=>{
_.reset();
}, 1000);
}
}
}
_.slider.addEventListener('mousedown', handleStart);
_.slider.addEventListener('touchstart', handleStart);
_.block.addEventListener('mousedown', handleStart);
_.block.addEventListener('touchstart', handleStart);
document.addEventListener('mousemove', handleMove);
document.addEventListener('touchmove', handleMove);
document.addEventListener('mouseup', handleEnd);
document.addEventListener('touchend', handleEnd);
}
/**
* 核验
* @returns {Promise<Status>}
*/ async verify() {
let R = Status.fail;
const _ = this;
const { mobile, trail, opt } = _;
const { url, ratio } = opt;
try {
const left = Math.round(parseInt(_.block.style.left.replace('px', '')) * ratio);
const rs = await $.post(`${url}/verify`, {
left,
mobile,
trail
});
console.log('verify', {
rs
});
if (rs?.code === 200) R = Status.succ;
else if (rs?.code === 4039) R = Status.over;
else if (rs?.code === 4041) R = Status.ready;
else if (rs?.code === 4028) R = Status.smsMax;
} catch (e) {
console.error(`verify exp:${e.message}`);
}
return R;
}
reset() {
const _ = this;
const { opt } = _;
_.sliderContainer.className = css.sliderContainer;
$(_.sliderIcon).html(`<i class="wiaicon rot-90">${opt.icon.slider}</i>`);
// $(_.sliderIcon).html(`<i class="fas fa-bars fa-rotate-90" aria-hidden="true"></i>`);
_.slider.style.left = '0';
_.block.style.left = '0';
_.sliderMask.style.width = '0';
}
async reload() {
const _ = this;
const { opt } = _;
try {
const { tip } = opt;
_.status = Status.null;
_.reset();
_.clean();
await _.loadImg(_.mobile);
$(_.text).html(tip);
} catch (e) {
console.error(`reload exp:${e.message}`);
}
}
/**
* 获取随机图片网址
* @returns {Promise<{back: string, block: string, time: number}>}
*/ async getImg() {
let R;
const _ = this;
const { mobile, opt } = _;
const { url, count } = opt;
try {
// R = `${url}/${random(1, count)}.jpg`;
const rs = await $.post(`${url}/getVerify`, {
mobile
});
if (rs?.code === 200 && rs.data) {
console.log('getImg', {
rs
});
// {
// back: 'https://img.wia.pub/lianlian/verify/b2f300ffbf949460037368c18a0be637.jpg';
// block: 'https://img.wia.pub/lianlian/verify/b2f300ffbf949460037368c18a0be637.png';
// time: 120;
// }
R = rs.data;
} else if (rs?.code === 4039) alert('登录次数太多,请联系管理员!');
} catch (e) {
console.log(`getImg exp:${e.message}`);
}
return R;
}
destroy() {}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @returns
*/ function getRatio(ctx) {
let R = 1;
try {
// 设备像素比,高清屏上,一个图片像素点对应屏幕2-4个像素点
// 如果图片密度不够,就会模糊,高清屏需要高密度图片
const dpr = window.devicePixelRatio || 1;
// 存储像素比,浏览器在渲染 canvas 之前会用几个像素存储画布信息
const bsr = ctx.backingStorePixelRatio || ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || 1;
// 设备与屏幕像素比例
R = dpr / bsr;
console.log(`getRatio:${R}`);
} catch (e) {
console.log(`getRatio exp:${e.message}`);
}
return R;
}
/**
* 放大画布,避免模糊
* 场景需同比例放大,放大后,相对显示区域的坐标、字体、尺寸会自动放大
* 画板不变,画布字段缩小到画板展示时,像素密度更大,边界更细腻。
* @param {*} ctx
* @returns
*/ function scale(ctx) {
const cv = ctx.canvas;
const { width: cw, height: ch } = cv;
const ratio = getRatio(ctx);
// 放大画布,放大的画布最后缩小绘制到屏幕,单位面积的像素点更多,匹配高清屏
cv.width = Math.floor(cw * ratio);
cv.height = Math.floor(ch * ratio);
// 画布显示尺寸不变,画布缩小到现实尺寸
cv.style.width = `${cw}px`;
cv.style.height = `${ch}px`;
// 按缩小比例进行绘制
ctx.scale(ratio, ratio);
ctx.lineWidth = 1 // 修改线条宽度的值,要求为实际像素值的一半
;
}
/**
*
* @param {number} x
* @param {number} y
* @returns
*/ function sum(x, y) {
return x + y;
}
/**
*
* @param {number} x
* @returns
*/ function square(x) {
return x * x;
}
/**
* 指定范围随机数
* @param {number} min 起始 >= min
* @param {number} max 终止 < max
* @returns
*/ function random(min, max) {
return Math.round(Math.random() * (max - min) + min);
}
/**
* 设置 img src
* @param {string} src
* @returns {Promise<HTMLImageElement>}
*/ function loadImg(src) {
return new Promise((res, rej)=>{
// 不能使用页面中的img,页面中的img会压缩图片,得不到图片真实大小!
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = src;
if (img.complete) {
res(img);
} else {
img.onload = ()=>res(img);
}
});
}