ERPTurbo_Client/packages/app-client/src/components/captcha/index.jsx
shenyifei ddf2fe3751 fix(captcha): 修复滑块验证码在桌面端无法使用的问题
- 兼容触摸事件和鼠标事件,支持移动端和桌面端
- 添加鼠标移动、鼠标抬起和鼠标离开事件处理
- 修复坐标获取逻辑,统一处理触摸和鼠标坐标
- 移除调试日志,优化控制台输出
- 添加环境判断,生产环境不加载vConsole
2025-12-24 15:14:57 +08:00

427 lines
12 KiB
JavaScript

import { Component } from "react";
import Taro from "@tarojs/taro";
import { Image, Text, View } from "@tarojs/components";
import auth from "@/services/auth";
import { CSSTransition } from "react-transition-group";
import "./index.css";
import { aesEncrypt, uuid } from "./utils";
class Captcha extends Component {
constructor(props) {
super(props);
this.state = {
blockSize: {
width: "50px",
height: "50px",
},
setSize: {
imgHeight: 155,
imgWidth: 310,
barHeight: 40,
barWidth: 310,
},
backImgBase: "",
blockBackImgBase: "",
backToken: "",
startMoveTime: "",
secretKey: "",
captchaType: "blockPuzzle",
moveBlockBackgroundColor: "rgb(255, 255, 255)",
leftBarBorderColor: "",
iconColor: "",
barAreaLeft: 0,
barAreaOffsetWidth: 0,
startLeft: null,
moveBlockLeft: null,
leftBarWidth: null,
status: false,
isEnd: false,
passFlag: "",
tipWords: "",
text: "向右滑动完成验证",
};
}
componentDidMount() {
this.uuid();
this.init();
// 组件卸载时移除事件监听
Taro.eventCenter.off("touchmove", this.touchMoveHandler);
Taro.eventCenter.off("touchend", this.touchEndHandler);
// 调用 setBarArea 方法
setTimeout(() => {
this.setBarArea(this.bararea);
}, 0);
}
// 初始化 uuid
uuid() {
const s = uuid();
const slider = "slider" + "-" + s;
const point = "point" + "-" + s;
if (!Taro.getStorageSync("slider")) {
Taro.setStorageSync("slider", slider);
}
if (!Taro.getStorageSync("point")) {
Taro.setStorageSync("point", point);
}
}
init() {
this.getData();
}
getData() {
auth.captcha
.get({
captchaType: this.state.captchaType,
clientUid: Taro.getStorageSync("slider"),
ts: Date.now(),
})
.then((res) => {
if (res.data.repCode === "0000") {
console.log(res.data.repData, "res.data.repData");
// 在组件中使用转换后的URL加载图片
// const imgUrl = base64ToURL(res.data.repData.originalImageBase64);
// console.log(imgUrl,'imgUrl');
this.setState({
backImgBase: res.data.repData.originalImageBase64,
blockBackImgBase: res.data.repData.jigsawImageBase64,
backToken: res.data.repData.token,
secretKey: res.data.repData.secretKey,
});
}
// 请求次数超限
if (res.data.repCode === "6201") {
this.setState({
backImgBase: null,
blockBackImgBase: null,
leftBarBorderColor: "#d9534f",
iconColor: "#fff",
// eslint-disable-next-line react/no-unused-state
iconClass: "icon-close",
passFlag: false,
tipWords: res.data.repMsg,
});
setTimeout(() => {
this.setState({
tipWords: "",
});
}, 1000);
}
});
}
refresh = () => {
this.getData();
this.setState({
moveBlockLeft: "",
leftBarWidth: "",
text: "向右滑动完成验证",
moveBlockBackgroundColor: "#fff",
leftBarBorderColor: "#337AB7",
iconColor: "#fff",
status: false,
isEnd: false,
});
};
setBarArea = (barArea) => {
if (barArea) {
Taro.createSelectorQuery()
.select(".verify-bar-area")
.boundingClientRect((rect) => {
console.log("setBarArea", rect.left, rect.width);
if (rect) {
this.setState({
barAreaLeft: rect.left,
barAreaOffsetWidth: rect.width,
});
}
})
.exec();
}
};
// 鼠标按下时的事件处理函数
start = (e) => {
console.log(e, "按下");
this.setState({
startMoveTime: new Date().getTime(), //记录开始滑动的时间
});
if (!this.state.isEnd) {
this.setState({
text: "",
moveBlockBackgroundColor: "#337ab7", //移动块背景颜色
leftBarBorderColor: "#337AB7", //左侧滑块边框颜色
iconColor: "#fff", //图标颜色
status: true, //状态标识为true
});
e.stopPropagation(); //阻止事件冒泡
}
};
// 鼠标/触摸移动时的事件处理函数
move = (e) => {
console.log(e, "滑动滑动");
if (this.state.status && !this.state.isEnd) {
// 兼容触摸事件和鼠标事件
let x = e.touches ? e.touches[0].pageX : e.pageX; // 获取鼠标/触摸点的X坐标
console.log(x);
let bar_area_left = this.state.barAreaLeft; // 滑块区域左侧距离
let move_block_left = x - bar_area_left; // 计算移动块的左侧距离
console.log(
move_block_left >=
this.state.barAreaOffsetWidth -
parseInt(parseInt(this.state.blockSize.width) / 2) -
2,
);
if (
move_block_left >=
this.state.barAreaOffsetWidth -
parseInt(parseInt(this.state.blockSize.width) / 2) -
2
) {
console.log("第一个判断计算");
move_block_left =
this.state.barAreaOffsetWidth -
parseInt(parseInt(this.state.blockSize.width) / 2) -
2;
}
if (move_block_left <= 0) {
console.log("第二个判断计算");
move_block_left = parseInt(this.state.blockSize.width / 2);
}
let moveBlockLeft =
Math.floor(move_block_left - this.state.startLeft) + "px"; //计算移动块的左侧位置并取整
let leftBarWidth =
Math.floor(move_block_left - this.state.startLeft) + "px"; //计算左侧滑块的宽度并取整
console.log(moveBlockLeft, leftBarWidth); //打印移动块左侧位置和左侧滑块宽度
this.setState({
moveBlockLeft: moveBlockLeft, //更新移动块左侧位置
leftBarWidth: leftBarWidth, //更新左侧滑块宽度
});
}
};
end = () => {
console.log("鼠标放下");
let endMoveTime = +new Date();
if (this.state.status && !this.state.isEnd) {
let moveLeftDistance = parseInt(
(this.state.moveBlockLeft || "").replace("px", ""),
);
moveLeftDistance =
(moveLeftDistance * 310) / parseInt(this.state.setSize.imgWidth);
let data = {
captchaType: this.state.captchaType,
pointJson: this.state.secretKey
? aesEncrypt(
JSON.stringify({
x: moveLeftDistance,
y: 5.0,
}),
this.state.secretKey,
)
: JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
token: this.state.backToken,
clientUid: Taro.getStorageSync("slider"),
ts: Date.now(),
};
auth.captcha.check(data).then((res) => {
if (res.data.repCode === "0000") {
this.setState({
isEnd: true,
passFlag: true,
tipWords: `${((endMoveTime - this.state.startMoveTime) / 1000).toFixed(2)}s验证成功`,
});
setTimeout(() => {
this.setState({
tipWords: "",
});
const captchaVerification = this.state.secretKey
? aesEncrypt(
this.state.backToken +
"---" +
JSON.stringify({
x: moveLeftDistance,
y: 5.0,
}),
this.state.secretKey,
)
: this.state.backToken +
"---" +
JSON.stringify({ x: moveLeftDistance, y: 5.0 });
this.props.handleClick(captchaVerification);
this.refresh();
}, 1000);
} else {
this.setState({
isEnd: true,
moveBlockBackgroundColor: "#d9534f",
leftBarBorderColor: "#d9534f",
iconColor: "#fff",
// eslint-disable-next-line react/no-unused-state
iconClass: "icon-close",
passFlag: false,
tipWords: res.data.repMsg || "验证失败",
});
setTimeout(() => {
this.refresh();
this.setState({
tipWords: "",
});
}, 1000);
}
});
this.setState({
status: false,
});
}
};
render() {
const { vSpace, barSize, transitionWidth, finishText, transitionLeft } =
this.props;
return (
<View style={{ position: "relative" }} className="stop-user-select">
<View
className="verify-img-out"
style={{ height: parseInt(this.state.setSize.imgHeight) + vSpace }}
>
<View
className="verify-img-panel"
style={{
width: this.state.setSize.imgWidth,
height: this.state.setSize.imgHeight,
}}
>
<Image
src={"data:image/png;base64," + this.state.backImgBase}
mode="aspectFill"
style={{
width: "100%",
height: "100%",
}}
/>
{/* 其他内容 */}
<CSSTransition
in={this.state.tipWords.length > 0}
timeout={150}
classNames="tips"
unmountOnExit
>
<View
className={
this.state.passFlag
? `${"verify-tips"} ${"suc-bg"}`
: `${"verify-tips"} ${"err-bg"}`
}
>
{this.state.tipWords}
</View>
</CSSTransition>
</View>
</View>
<View
className="verify-bar-area"
style={{
width: this.state.setSize.imgWidth,
height: barSize.height,
lineHeight: barSize.height,
}}
ref={(ref) => (this.bararea = ref)}
>
<Text className="verify-msg">{this.state.text}</Text>
<View
className="verify-left-bar"
style={{
width:
this.state.leftBarWidth !== undefined
? this.state.leftBarWidth
: barSize.height,
height: barSize.height,
borderColor: this.state.leftBarBorderColor,
transaction: transitionWidth,
}}
>
<Text className="verify-msg">{finishText}</Text>
<View
className="verify-move-block"
onTouchStart={this.start}
onMouseDown={this.start}
onTouchMove={this.move}
onMouseMove={this.move}
onTouchEnd={this.end}
onMouseUp={this.end}
onMouseLeave={this.end}
style={{
width: barSize.height,
height: barSize.height,
backgroundColor: this.state.moveBlockBackgroundColor,
left: this.state.moveBlockLeft,
transition: transitionLeft,
}}
>
<Text
className="verify-icon iconfont icon-right"
style={{ color: this.state.iconColor }}
></Text>
<View
className="verify-sub-block"
style={{
width:
Math.floor(
(parseInt(this.state.setSize.imgWidth) * 47) / 310,
) + "px",
height: this.state.setSize.imgHeight,
top:
"-" +
(parseInt(this.state.setSize.imgHeight) + vSpace) +
"px",
backgroundSize:
this.state.setSize.imgWidth +
" " +
this.state.setSize.imgHeight,
}}
>
<Image
src={"data:image/png;base64," + this.state.blockBackImgBase}
alt=""
style={{ width: "100%", height: "100%", display: "block" }}
/>
</View>
</View>
</View>
</View>
</View>
);
}
}
Captcha.defaultProps = {
mode: "fixed",
vSpace: 5,
imgSize: {
width: "310px",
height: "200px",
},
barSize: {
width: "310px",
height: "40px",
},
setSize: {
imgHeight: 200,
imgWidth: 310,
barHeight: 0,
barWidth: 0,
},
};
export default Captcha;