登录添加验证码

This commit is contained in:
ylj20011123 2025-09-16 17:46:06 +08:00
parent 37ad56ea54
commit 85905b6015
7 changed files with 280 additions and 10 deletions

BIN
dist.zip Normal file

Binary file not shown.

View File

@ -1,6 +1,6 @@
{
"name": "ant-design-pro",
"version": "4.5.55",
"version": "4.5.59",
"private": true,
"description": "An out-of-box UI solution for enterprise applications",
"scripts": {

View File

@ -0,0 +1,66 @@
.captcha {
display: inline-block;
.imageContainer {
position: relative;
display: inline-block;
border: 2px solid #d9d9d9;
border-radius: 6px;
overflow: hidden;
background: #fff;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
border-color: #40a9ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
}
.loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.9);
color: #1890ff;
font-size: 14px;
}
}
.refreshBtn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: #1890ff;
background: #f0f6ff;
border: 1px solid #d6e4ff;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
&:hover {
background: #e6f7ff;
border-color: #91d5ff;
color: #096dd9;
transform: rotate(180deg);
}
&:active {
transform: rotate(180deg) scale(0.95);
}
}
.refreshText {
color: #666;
font-size: 12px;
margin-left: 4px;
user-select: none;
}
}

View File

@ -0,0 +1,149 @@
/*
* @Author: cclu
* @Date: 2025-09-16 10:00:00
* @Description:
*/
import React, { useEffect, useState } from 'react';
import { Image } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import styles from './index.less';
export interface CaptchaProps {
width?: number;
height?: number;
onCaptchaChange?: (captchaCode: string) => void;
onRefresh?: () => void;
style?: React.CSSProperties;
className?: string;
}
const Captcha: React.FC<CaptchaProps> = ({
width = 120,
height = 40,
onCaptchaChange,
onRefresh,
style,
className,
}) => {
const [captchaCode, setCaptchaCode] = useState<string>('');
const [imageUrl, setImageUrl] = useState<string>('');
const [loading, setLoading] = useState(false);
// 生成随机验证码文本
const generateCaptchaCode = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
for (let i = 0; i < 4; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
};
// 刷新验证码
const refreshCaptcha = () => {
setLoading(true);
// 生成新的验证码文本
const newCaptchaCode = generateCaptchaCode();
setCaptchaCode(newCaptchaCode);
// 使用Canvas绘制验证码图片
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (ctx) {
// 清空画布并设置渐变背景
const gradient = ctx.createLinearGradient(0, 0, width, height);
gradient.addColorStop(0, '#fafafa');
gradient.addColorStop(1, '#f0f0f0');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
// 绘制验证码文字
ctx.font = `bold ${height * 0.7}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 为每个字符设置随机颜色和位置
const colors = ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2'];
for (let i = 0; i < newCaptchaCode.length; i++) {
ctx.fillStyle = colors[Math.floor(Math.random() * colors.length)];
const x = (width / 4) * i + width / 8;
const y = height / 2 + (Math.random() - 0.5) * 6;
ctx.save();
ctx.translate(x, y);
ctx.rotate((Math.random() - 0.5) * 0.25);
// 添加文字阴影效果
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 2;
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 1;
ctx.fillText(newCaptchaCode[i], 0, 0);
ctx.restore();
}
// 添加更美观的干扰线
for (let i = 0; i < 2; i++) {
ctx.strokeStyle = `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.3)`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(Math.random() * width, Math.random() * height);
ctx.bezierCurveTo(
Math.random() * width, Math.random() * height,
Math.random() * width, Math.random() * height,
Math.random() * width, Math.random() * height
);
ctx.stroke();
}
// 添加装饰性干扰点
for (let i = 0; i < 20; i++) {
ctx.fillStyle = `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.4)`;
ctx.beginPath();
ctx.arc(Math.random() * width, Math.random() * height, Math.random() * 2 + 1, 0, 2 * Math.PI);
ctx.fill();
}
setImageUrl(canvas.toDataURL());
}
setLoading(false);
onCaptchaChange?.(newCaptchaCode);
onRefresh?.();
};
useEffect(() => {
refreshCaptcha();
}, []);
return (
<div
className={`${styles.captcha} ${className || ''}`}
style={style}
>
<div className={styles.imageContainer} onClick={refreshCaptcha}>
<Image
src={imageUrl}
alt="验证码"
width={width}
height={height}
preview={false}
placeholder="验证码加载中..."
style={{ cursor: 'pointer', display: 'block' }}
/>
{loading && (
<div className={styles.loading}>
<ReloadOutlined spin />
</div>
)}
</div>
</div>
);
};
export default Captcha;

View File

@ -8,12 +8,14 @@
*/
import {
LockFilled,
UserOutlined
UserOutlined,
SafetyCertificateOutlined
} from '@ant-design/icons';
import { Alert, Col, Divider, Row, Space, Typography } from 'antd';
import { Alert, Col, Divider, Row, Space, Typography, message } from 'antd';
import React, { useEffect, useState } from 'react';
import { LoginForm, ProFormText } from '@ant-design/pro-form';
import { useIntl, connect, Link } from 'umi';
import Captcha from '@/components/Captcha';
import type { Dispatch } from 'umi';
import type { StateType } from '@/models/login';
@ -61,15 +63,32 @@ const Login: React.FC<LoginProps> = (props) => {
const [browser, setBrowser] = useState<any>()
// 系统信息
const [systemInfo, setSystemInfo] = useState<any>()
// 验证码相关状态
const [currentCaptcha, setCurrentCaptcha] = useState<string>('')
const handleSubmit = (values: any) => {
// 验证验证码是否正确(不区分大小写)
if (values.captcha?.toUpperCase() !== currentCaptcha?.toUpperCase()) {
message.error('验证码错误,请重新输入');
return; // 直接return阻止表单提交但不抛出错误
}
const { dispatch } = props;
dispatch({
type: 'login/login',
payload: { ...values },
payload: {
...values,
// 移除验证码字段,不传给后端
captcha: undefined
},
});
};
// 验证码变化回调
const handleCaptchaChange = (newCaptchaCode: string) => {
setCurrentCaptcha(newCaptchaCode);
};
function findIP(onNewIP) {
const peerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
const pc = new peerConnection({ iceServers: [] });
@ -327,6 +346,41 @@ const Login: React.FC<LoginProps> = (props) => {
]}
/>
<div style={{ display: 'flex', gap: '16px', alignItems: 'flex-start', justifyContent: 'flex-start' }}>
<ProFormText
name="captcha"
style={{ width: '200px' }}
fieldProps={{
size: 'large',
prefix: <SafetyCertificateOutlined className={styles.prefixIcon} />,
style: {
textTransform: 'uppercase',
fontSize: '18px',
letterSpacing: '3px',
textAlign: 'center',
fontWeight: 'bold'
},
maxLength: 4,
}}
placeholder='请输入验证码'
rules={[
{
required: true,
message: '请输入验证码',
},
{
len: 4,
message: '验证码长度为4位',
},
]}
/>
<Captcha
width={120}
height={40}
onCaptchaChange={handleCaptchaChange}
/>
</div>
</LoginForm>
{/* <div style={{ textAlign: 'center', width: 328, margin: 'auto' }}>

View File

@ -244,7 +244,7 @@ const SummaryOfReservation: React.FC<{ currentUser: CurrentUser | undefined }> =
]
// 获取顶部的数据
const handleGetTopData = async (StartDate?: string, EndDate?: string) => {
const handleGetTopData = async (StartDate?: string, EndDate?: string, MEMBERSHIP_TARGET?: string) => {
const req: any = {
CalcType: 1,
@ -252,7 +252,8 @@ const SummaryOfReservation: React.FC<{ currentUser: CurrentUser | undefined }> =
StartDate: StartDate || "",
EndDate: EndDate || "",
SalebillType: selectPageTab === 1 ? "6000" : "3000,3001,3002",
MerchantId: ""
MerchantId: "",
MembershipTarget: MEMBERSHIP_TARGET ? MEMBERSHIP_TARGET.toString() : ""
}
const data = await handeGetGetOnlineOrderSummary(req)
console.log('datadatadatadata', data);
@ -310,7 +311,7 @@ const SummaryOfReservation: React.FC<{ currentUser: CurrentUser | undefined }> =
if (values.searchTime && values.searchTime.length > 0) {
[StartDate, EndDate] = values.searchTime
}
handleGetTopData(StartDate, EndDate)
handleGetTopData(StartDate, EndDate, values.MEMBERSHIP_TARGET || "")
setSearchParams({
StartDate: StartDate,
EndDate: EndDate,

View File

@ -1,4 +1,4 @@
// 由 scripts/writeVersion.js 自动生成
export const VERSION = "4.5.54";
export const GIT_HASH = "26ef480";
export const BUILD_TIME = "2025-09-10T08:05:17.397Z";
export const VERSION = "4.5.59";
export const GIT_HASH = "37ad56e";
export const BUILD_TIME = "2025-09-16T09:37:40.648Z";