diff --git a/dist.zip b/dist.zip new file mode 100644 index 0000000..33f0589 Binary files /dev/null and b/dist.zip differ diff --git a/package.json b/package.json index f835133..27db6a6 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/components/Captcha/index.less b/src/components/Captcha/index.less new file mode 100644 index 0000000..07d1603 --- /dev/null +++ b/src/components/Captcha/index.less @@ -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; + } +} \ No newline at end of file diff --git a/src/components/Captcha/index.tsx b/src/components/Captcha/index.tsx new file mode 100644 index 0000000..d097844 --- /dev/null +++ b/src/components/Captcha/index.tsx @@ -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 = ({ + width = 120, + height = 40, + onCaptchaChange, + onRefresh, + style, + className, +}) => { + const [captchaCode, setCaptchaCode] = useState(''); + const [imageUrl, setImageUrl] = useState(''); + 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 ( +
+
+ 验证码 + {loading && ( +
+ +
+ )} +
+
+ ); +}; + +export default Captcha; \ No newline at end of file diff --git a/src/pages/User/login/index.tsx b/src/pages/User/login/index.tsx index 8418bb5..a51910c 100644 --- a/src/pages/User/login/index.tsx +++ b/src/pages/User/login/index.tsx @@ -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 = (props) => { const [browser, setBrowser] = useState() // 系统信息 const [systemInfo, setSystemInfo] = useState() + // 验证码相关状态 + const [currentCaptcha, setCurrentCaptcha] = useState('') 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 = (props) => { ]} /> +
+ , + style: { + textTransform: 'uppercase', + fontSize: '18px', + letterSpacing: '3px', + textAlign: 'center', + fontWeight: 'bold' + }, + maxLength: 4, + }} + placeholder='请输入验证码' + rules={[ + { + required: true, + message: '请输入验证码', + }, + { + len: 4, + message: '验证码长度为4位', + }, + ]} + /> + +
+ {/*
diff --git a/src/pages/travelMember/SummaryOfReservation/index.tsx b/src/pages/travelMember/SummaryOfReservation/index.tsx index 814a21a..8ea09e6 100644 --- a/src/pages/travelMember/SummaryOfReservation/index.tsx +++ b/src/pages/travelMember/SummaryOfReservation/index.tsx @@ -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, diff --git a/src/versionEnv.ts b/src/versionEnv.ts index 4054db1..e7e0464 100644 --- a/src/versionEnv.ts +++ b/src/versionEnv.ts @@ -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";