登录添加验证码
This commit is contained in:
parent
37ad56ea54
commit
85905b6015
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ant-design-pro",
|
"name": "ant-design-pro",
|
||||||
"version": "4.5.55",
|
"version": "4.5.59",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "An out-of-box UI solution for enterprise applications",
|
"description": "An out-of-box UI solution for enterprise applications",
|
||||||
"scripts": {
|
"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 {
|
import {
|
||||||
LockFilled,
|
LockFilled,
|
||||||
UserOutlined
|
UserOutlined,
|
||||||
|
SafetyCertificateOutlined
|
||||||
} from '@ant-design/icons';
|
} 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 React, { useEffect, useState } from 'react';
|
||||||
import { LoginForm, ProFormText } from '@ant-design/pro-form';
|
import { LoginForm, ProFormText } from '@ant-design/pro-form';
|
||||||
import { useIntl, connect, Link } from 'umi';
|
import { useIntl, connect, Link } from 'umi';
|
||||||
|
import Captcha from '@/components/Captcha';
|
||||||
|
|
||||||
import type { Dispatch } from 'umi';
|
import type { Dispatch } from 'umi';
|
||||||
import type { StateType } from '@/models/login';
|
import type { StateType } from '@/models/login';
|
||||||
@ -61,15 +63,32 @@ const Login: React.FC<LoginProps> = (props) => {
|
|||||||
const [browser, setBrowser] = useState<any>()
|
const [browser, setBrowser] = useState<any>()
|
||||||
// 系统信息
|
// 系统信息
|
||||||
const [systemInfo, setSystemInfo] = useState<any>()
|
const [systemInfo, setSystemInfo] = useState<any>()
|
||||||
|
// 验证码相关状态
|
||||||
|
const [currentCaptcha, setCurrentCaptcha] = useState<string>('')
|
||||||
|
|
||||||
const handleSubmit = (values: any) => {
|
const handleSubmit = (values: any) => {
|
||||||
|
// 验证验证码是否正确(不区分大小写)
|
||||||
|
if (values.captcha?.toUpperCase() !== currentCaptcha?.toUpperCase()) {
|
||||||
|
message.error('验证码错误,请重新输入');
|
||||||
|
return; // 直接return,阻止表单提交,但不抛出错误
|
||||||
|
}
|
||||||
|
|
||||||
const { dispatch } = props;
|
const { dispatch } = props;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'login/login',
|
type: 'login/login',
|
||||||
payload: { ...values },
|
payload: {
|
||||||
|
...values,
|
||||||
|
// 移除验证码字段,不传给后端
|
||||||
|
captcha: undefined
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 验证码变化回调
|
||||||
|
const handleCaptchaChange = (newCaptchaCode: string) => {
|
||||||
|
setCurrentCaptcha(newCaptchaCode);
|
||||||
|
};
|
||||||
|
|
||||||
function findIP(onNewIP) {
|
function findIP(onNewIP) {
|
||||||
const peerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
const peerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
||||||
const pc = new peerConnection({ iceServers: [] });
|
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>
|
</LoginForm>
|
||||||
{/* <div style={{ textAlign: 'center', width: 328, margin: 'auto' }}>
|
{/* <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 = {
|
const req: any = {
|
||||||
CalcType: 1,
|
CalcType: 1,
|
||||||
@ -252,7 +252,8 @@ const SummaryOfReservation: React.FC<{ currentUser: CurrentUser | undefined }> =
|
|||||||
StartDate: StartDate || "",
|
StartDate: StartDate || "",
|
||||||
EndDate: EndDate || "",
|
EndDate: EndDate || "",
|
||||||
SalebillType: selectPageTab === 1 ? "6000" : "3000,3001,3002",
|
SalebillType: selectPageTab === 1 ? "6000" : "3000,3001,3002",
|
||||||
MerchantId: ""
|
MerchantId: "",
|
||||||
|
MembershipTarget: MEMBERSHIP_TARGET ? MEMBERSHIP_TARGET.toString() : ""
|
||||||
}
|
}
|
||||||
const data = await handeGetGetOnlineOrderSummary(req)
|
const data = await handeGetGetOnlineOrderSummary(req)
|
||||||
console.log('datadatadatadata', data);
|
console.log('datadatadatadata', data);
|
||||||
@ -310,7 +311,7 @@ const SummaryOfReservation: React.FC<{ currentUser: CurrentUser | undefined }> =
|
|||||||
if (values.searchTime && values.searchTime.length > 0) {
|
if (values.searchTime && values.searchTime.length > 0) {
|
||||||
[StartDate, EndDate] = values.searchTime
|
[StartDate, EndDate] = values.searchTime
|
||||||
}
|
}
|
||||||
handleGetTopData(StartDate, EndDate)
|
handleGetTopData(StartDate, EndDate, values.MEMBERSHIP_TARGET || "")
|
||||||
setSearchParams({
|
setSearchParams({
|
||||||
StartDate: StartDate,
|
StartDate: StartDate,
|
||||||
EndDate: EndDate,
|
EndDate: EndDate,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
// 由 scripts/writeVersion.js 自动生成
|
// 由 scripts/writeVersion.js 自动生成
|
||||||
export const VERSION = "4.5.54";
|
export const VERSION = "4.5.59";
|
||||||
export const GIT_HASH = "26ef480";
|
export const GIT_HASH = "37ad56e";
|
||||||
export const BUILD_TIME = "2025-09-10T08:05:17.397Z";
|
export const BUILD_TIME = "2025-09-16T09:37:40.648Z";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user