登录添加验证码
This commit is contained in:
parent
37ad56ea54
commit
85905b6015
@ -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": {
|
||||
|
||||
66
src/components/Captcha/index.less
Normal file
66
src/components/Captcha/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
149
src/components/Captcha/index.tsx
Normal file
149
src/components/Captcha/index.tsx
Normal 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;
|
||||
@ -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' }}>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user