Browse Source

验证码2.0

master
kola-web 3 hours ago
parent
commit
133067f567
  1. 2
      project.private.config.json
  2. 8
      src/app.json
  3. 8
      src/app.ts
  4. BIN
      src/images/defaultBook.png
  5. 4
      src/pages/login/index.json
  6. 51
      src/pages/login/index.ts
  7. 1
      src/pages/login/index.wxml
  8. 350
      src/utils/captcha.ts
  9. 63
      src/utils/network.ts
  10. 1
      typings/index.d.ts

2
project.private.config.json

@ -95,5 +95,5 @@
] ]
} }
}, },
"libVersion": "3.7.7" "libVersion": "3.11.3"
} }

8
src/app.json

@ -56,5 +56,11 @@
"componentFramework": "glass-easel", "componentFramework": "glass-easel",
"sitemapLocation": "sitemap.json", "sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents", "lazyCodeLoading": "requiredComponents",
"requiredBackgroundModes": ["audio"] "requiredBackgroundModes": ["audio"],
"plugins": {
"AliyunCaptcha": {
"version": "3.0.0",
"provider": "wxbe275ff84246f1a4"
}
}
} }

8
src/app.ts

@ -1,8 +1,12 @@
/* eslint-disable perfectionist/sort-imports */
import component from '@/utils/component'; import component from '@/utils/component';
import relativeTime from '@/utils/dayjs/relativeTime.js'; import relativeTime from '@/utils/dayjs/relativeTime.js';
import page from '@/utils/page'; import page from '@/utils/page';
import { request } from '@/utils/request'; import { request } from '@/utils/request';
import { initNetworkMonitor } from './utils/network';
import { parseScene } from './utils/util'; import { parseScene } from './utils/util';
const dayjs = require('dayjs'); const dayjs = require('dayjs');
const licia = require('miniprogram-licia'); const licia = require('miniprogram-licia');
@ -28,12 +32,15 @@ App<IAppOption>({
loginState: '', loginState: '',
isLogin: 0, isLogin: 0,
isAnswer: '0', isAnswer: '0',
sceneId: '',
scene: null, scene: null,
backPath: '', backPath: '',
}, },
onLaunch() { onLaunch() {
// 初始化网络状态监听
initNetworkMonitor();
Page = page as WechatMiniprogram.Page.Constructor; Page = page as WechatMiniprogram.Page.Constructor;
Component = component as WechatMiniprogram.Component.Constructor; Component = component as WechatMiniprogram.Component.Constructor;
@ -50,6 +57,7 @@ App<IAppOption>({
}).then((res) => { }).then((res) => {
this.globalData.loginState = res.loginState; this.globalData.loginState = res.loginState;
this.globalData.isLogin = res.isLogin || 999; this.globalData.isLogin = res.isLogin || 999;
this.globalData.sceneId = res.sceneId;
}); });
}, },
}); });

BIN
src/images/defaultBook.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

4
src/pages/login/index.json

@ -2,6 +2,8 @@
"usingComponents": { "usingComponents": {
"van-icon": "@vant/weapp/icon/index", "van-icon": "@vant/weapp/icon/index",
"van-nav-bar": "@vant/weapp/nav-bar/index", "van-nav-bar": "@vant/weapp/nav-bar/index",
"van-popup": "@vant/weapp/popup/index" "van-popup": "@vant/weapp/popup/index",
"aliyun-captcha": "plugin://AliyunCaptcha/captcha"
} }
} }

51
src/pages/login/index.ts

@ -1,5 +1,6 @@
import { destroyCaptcha, initCaptcha, showCaptcha } from '@/utils/captcha';
const app = getApp<IAppOption>(); const app = getApp<IAppOption>();
let timer: number | null = 0;
Page({ Page({
data: { data: {
@ -13,17 +14,34 @@ Page({
type: '', type: '',
show: false, show: false,
loadCaptcha: false, // 是否加载验证码
pluginProps: {} as any, // 验证码插件配置
}, },
onLoad(options) { onLoad(options) {
this.setData({ this.setData({
back: options.back === '1', back: options.back === '1',
}); });
app.waitLogin(true).then(() => {
// 初始化验证码插件
const captchaConfig = initCaptcha(this, {
sceneId: app.globalData.sceneId,
sendCodeConfig: {
url: '?r=shizhong/login/send-verify-code',
mobileField: 'mobile',
},
});
this.setData(captchaConfig);
});
}, },
onShow() { onShow() {
wx.hideShareMenu(); wx.hideShareMenu();
}, },
onUnload() {
// 页面卸载时清理验证码资源
destroyCaptcha();
},
getCode() { getCode() {
if (timer) return;
const mobile = this.data.mobile; const mobile = this.data.mobile;
if (!mobile) { if (!mobile) {
wx.showToast({ wx.showToast({
@ -33,39 +51,14 @@ Page({
return; return;
} }
// 验证手机号 // 验证手机号
if (!/^1[3-9,]\d{9}$/.test(mobile)) { if (!/^1[3-9]\d{9}$/.test(mobile)) {
wx.showToast({ wx.showToast({
title: '手机号格式不正确', title: '手机号格式不正确',
icon: 'none', icon: 'none',
}); });
return; return;
} }
wx.ajax({ showCaptcha();
method: 'POST',
url: '?r=shizhong/login/send-verify-code',
data: {
mobile,
},
}).then(() => {
wx.showToast({
icon: 'none',
title: '验证码已发送~',
});
let time = 60;
timer = setInterval(() => {
time--;
this.setData({
codeText: `${time}s后重新发送`,
});
if (time <= 0) {
clearInterval(timer as number);
timer = null;
this.setData({
codeText: '发送验证码',
});
}
}, 1000);
});
}, },
handleSubmit() { handleSubmit() {
const { show, mobile, code, protool } = this.data; const { show, mobile, code, protool } = this.data;

1
src/pages/login/index.wxml

@ -37,6 +37,7 @@
model:value="{{code}}" model:value="{{code}}"
type="number" type="number"
/> />
<aliyun-captcha id="captcha-element" wx:if="{{loadCaptcha}}" props="{{pluginProps}}" />
<view class="btn" bind:tap="getCode">{{codeText}}</view> <view class="btn" bind:tap="getCode">{{codeText}}</view>
</view> </view>
<view class="protool"> <view class="protool">

350
src/utils/captcha.ts

@ -0,0 +1,350 @@
/**
* 2.0
*
*/
// 验证码插件实例
let AliyunCaptchaPluginInterface: any = null;
// 计时器
let timer: number | null = null;
// 页面实例引用
let pageInstance: any = null;
// 是否在发送中(防止重复请求)
let isSending: boolean = false;
// 倒计时剩余秒数
let remainingSeconds: number = 0;
// 发送验证码的接口配置
interface SendCodeConfig {
url: string;
mobileField?: string;
extraData?: Record<string, any>;
}
// 验证码配置选项
interface CaptchaOptions {
sceneId: string;
sendCodeConfig: SendCodeConfig;
onSendSuccess?: () => void;
onSendFail?: (err: any) => void;
countdown?: number;
}
// 当前配置
let currentOptions: CaptchaOptions | null = null;
/**
*
*/
function initPlugin() {
if (!AliyunCaptchaPluginInterface) {
AliyunCaptchaPluginInterface = requirePlugin('AliyunCaptcha');
}
}
/**
*
* @param captchaVerifyParam
*/
async function successCallback(captchaVerifyParam: string) {
if (!pageInstance || !currentOptions) return;
if (isSending) {
wx.showToast({
title: '发送中,请稍候',
icon: 'none',
});
return;
}
const { sendCodeConfig, onSendSuccess, onSendFail, countdown = 60 } = currentOptions;
const mobileField = sendCodeConfig.mobileField || 'mobile';
const mobile = pageInstance.data[mobileField];
if (!mobile) {
wx.showToast({
title: '请输入手机号',
icon: 'none',
});
return;
}
isSending = true;
updateCountdownText('发送中...');
try {
const res = await wx.ajax({
method: 'POST',
url: sendCodeConfig.url,
data: {
[mobileField]: mobile,
captchaVerifyParam,
...sendCodeConfig.extraData,
},
});
wx.showToast({
icon: 'none',
title: '验证码已发送~',
});
// 开始倒计时
startCountdown(countdown);
// 执行成功回调
onSendSuccess?.();
return res;
} catch (err: any) {
// 发送失败,重置状态(包括阿里云插件,以便下次重新验证)
resetCaptchaState(true);
const errorMsg = err.data?.msg || err.message || '发送失败,请重试';
wx.showToast({
title: errorMsg,
icon: 'none',
duration: 2000,
});
onSendFail?.(err);
throw err;
} finally {
isSending = false;
}
}
/**
*
* @param error
*/
function failCallback(error: any) {
console.error('阿里云验证码验证失败:', error);
wx.showToast({
title: '验证失败,请重试',
icon: 'none',
});
}
/**
*
* @param seconds
*/
function startCountdown(seconds: number) {
// 清除之前的计时器
if (timer) {
clearInterval(timer);
timer = null;
}
remainingSeconds = seconds;
updateCountdownText(`${remainingSeconds}s后重新发送`);
timer = setInterval(() => {
remainingSeconds--;
if (remainingSeconds <= 0) {
// 倒计时结束,重置状态(包括阿里云插件,以便下次重新验证)
resetCaptchaState(true);
} else {
updateCountdownText(`${remainingSeconds}s后重新发送`);
}
}, 1000) as unknown as number;
}
/**
*
* 使
*/
function resetCaptchaState(resetPluginToo: boolean = false) {
if (timer) {
clearInterval(timer);
timer = null;
}
remainingSeconds = 0;
isSending = false;
updateCountdownText('发送验证码');
// 如果需要,同时重置阿里云插件状态(接口失败时使用)
if (resetPluginToo) {
refreshCaptchaComponent();
}
}
/**
*
*
*/
function resetAliyunPlugin(): void {
// 销毁插件实例,下次会自动重新初始化
AliyunCaptchaPluginInterface = null;
}
/**
*
*
*/
export function refreshCaptchaComponent(callback?: () => void): void {
if (!pageInstance) return;
// 卸载组件
pageInstance.setData({
loadCaptcha: false,
});
// 重置插件实例
resetAliyunPlugin();
// 延迟重新加载组件,确保卸载完成
setTimeout(() => {
if (!pageInstance || !currentOptions) return;
// 重新初始化配置
const pluginProps = {
SceneId: currentOptions.sceneId,
mode: 'popup',
success: successCallback.bind(pageInstance),
fail: failCallback.bind(pageInstance),
slideStyle: {
width: 540,
height: 60,
},
language: 'cn',
region: 'cn',
};
// 重新加载组件
pageInstance.setData({
loadCaptcha: true,
pluginProps,
});
if (callback) callback();
}, 100);
}
/**
*
* @param text
*/
function updateCountdownText(text: string) {
if (pageInstance) {
pageInstance.setData({ codeText: text });
}
}
/**
*
* @param page
* @param options
* @returns
*/
export function initCaptcha(page: any, options: CaptchaOptions) {
initPlugin();
pageInstance = page;
currentOptions = options;
const pluginProps = {
SceneId: options.sceneId,
mode: 'popup',
success: successCallback.bind(page),
fail: failCallback.bind(page),
slideStyle: {
width: 540,
height: 60,
},
language: 'cn',
region: 'cn',
};
return {
loadCaptcha: true,
pluginProps,
};
}
/**
*
* @returns
*/
export function showCaptcha(): boolean {
if (!AliyunCaptchaPluginInterface) {
initPlugin();
}
if (isCountingDown()) {
return false;
}
wx.getNetworkType({
success: (res) => {
if (res.networkType === 'none') {
wx.showToast({
title: '网络连接不可用,请检查网络设置',
icon: 'none',
});
return;
}
AliyunCaptchaPluginInterface.show();
},
fail: () => {
AliyunCaptchaPluginInterface.show();
},
});
return true;
}
/**
*
* 使
* @returns
*/
export function showCaptchaWithReset(): boolean {
// 重置插件状态,清除已验证的状态
resetAliyunPlugin();
if (isCountingDown()) {
return false;
}
// 重新初始化后显示
initPlugin();
AliyunCaptchaPluginInterface.show();
return true;
}
/**
*
* @returns
*/
export function isCountingDown(): boolean {
return timer !== null;
}
/**
*
*
*/
export function clearCountdown() {
resetCaptchaState();
}
/**
*
* @returns
*/
export function getCaptchaPlugin() {
if (!AliyunCaptchaPluginInterface) {
initPlugin();
}
return AliyunCaptchaPluginInterface;
}
/**
*
*/
export function destroyCaptcha() {
clearCountdown();
pageInstance = null;
currentOptions = null;
}

63
src/utils/network.ts

@ -0,0 +1,63 @@
/**
*
*
*/
interface NetworkStatus {
isConnected: boolean;
networkType: string;
}
let networkListener: WechatMiniprogram.OnNetworkStatusChangeListener | null = null;
let isOfflineShown = false;
let isRecoveryShown = false;
function showOfflineTip() {
if (isOfflineShown) return;
isOfflineShown = true;
wx.showModal({
title: '网络连接失败',
content: '当前网络不可用,请检查网络设置',
showCancel: false,
confirmText: '知道了',
});
}
function showRecoveryTip() {
if (isRecoveryShown) return;
isRecoveryShown = true;
wx.showModal({
title: '网络已恢复',
content: '网络连接已恢复,是否刷新小程序?',
confirmText: '立即刷新',
cancelText: '暂不刷新',
success: (res) => {
if (res.confirm) {
wx.reLaunch({ url: '/pages/home/index' });
}
setTimeout(() => {
isRecoveryShown = false;
}, 3000);
},
});
}
export function initNetworkMonitor(): void {
wx.getNetworkType({
success: (res) => {
if (res.networkType === 'none') showOfflineTip();
},
});
networkListener = (res: NetworkStatus) => {
if (res.isConnected) {
isOfflineShown = false;
showRecoveryTip();
} else {
isRecoveryShown = false;
showOfflineTip();
}
};
wx.onNetworkStatusChange(networkListener);
}

1
typings/index.d.ts vendored

@ -7,6 +7,7 @@ interface IAppOption {
loginState: string; loginState: string;
isLogin: 0 | 1 | 999; isLogin: 0 | 1 | 999;
isAnswer: '0' | '1'; isAnswer: '0' | '1';
sceneId: string;
scene: null | { [key: string]: any }; scene: null | { [key: string]: any };
backPath: string; backPath: string;
}; };

Loading…
Cancel
Save