支持登录验证码
This commit is contained in:
22
package-lock.json
generated
22
package-lock.json
generated
@@ -24,6 +24,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"dom-to-image-more": "^3.5.0",
|
||||
"go-captcha-react": "^1.0.4",
|
||||
"html2canvas": "^1.4.1",
|
||||
"katex": "^0.16.21",
|
||||
"lucide-react": "^0.263.1",
|
||||
@@ -3124,6 +3125,12 @@
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz",
|
||||
@@ -3930,6 +3937,21 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/go-captcha-react": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/go-captcha-react/-/go-captcha-react-1.0.4.tgz",
|
||||
"integrity": "sha512-1pNUoFqU5bt+tAVwqENTT86c5ih3Usppz7oFo2CPcO13o/Pwj60crudwtYdo8BUOghkYvHGL8gNEMMJFNhO0gQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"classnames": "^2.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"dom-to-image-more": "^3.5.0",
|
||||
"go-captcha-react": "^1.0.4",
|
||||
"html2canvas": "^1.4.1",
|
||||
"katex": "^0.16.21",
|
||||
"lucide-react": "^0.263.1",
|
||||
|
||||
33
src/components/goCaptcha/ClickShapeCapt.js
Normal file
33
src/components/goCaptcha/ClickShapeCapt.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, {useRef} from 'react'
|
||||
import GoCaptcha from 'go-captcha-react'
|
||||
// Cache Testing
|
||||
// import GoCaptcha from '../cache'
|
||||
import {useClickHandler} from "../hooks/useClickHandler";
|
||||
|
||||
function ClickShapeCapt() {
|
||||
const domRef = useRef(null)
|
||||
|
||||
const handler = useClickHandler(domRef, {
|
||||
getApi: "/api/go-captcha-data/click-shape",
|
||||
checkApi: "/api/go-captcha-check-data/click-shape"
|
||||
})
|
||||
|
||||
return (
|
||||
<GoCaptcha.Click
|
||||
config={{
|
||||
width: 300,
|
||||
height: 220,
|
||||
dotSize: 24,
|
||||
}}
|
||||
data={handler.data}
|
||||
events={{
|
||||
close: handler.closeEvent,
|
||||
refresh: handler.refreshEvent,
|
||||
confirm: handler.confirmEvent,
|
||||
}}
|
||||
ref={domRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClickShapeCapt;
|
||||
37
src/components/goCaptcha/ClickTextCapt.jsx
Normal file
37
src/components/goCaptcha/ClickTextCapt.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, {useRef} from 'react'
|
||||
import GoCaptcha from 'go-captcha-react'
|
||||
// Cache Testing
|
||||
// import GoCaptcha from '../cache'
|
||||
import {useClickHandler} from "./hooks/useClickHandler";
|
||||
|
||||
function ClickTextCapt({onVisibleChange, extraData, onSuccess}) {
|
||||
const domRef = useRef(null)
|
||||
|
||||
const handler = useClickHandler(domRef, {
|
||||
getApi: "/api/captcha",
|
||||
checkApi: "/api/captcha/check",
|
||||
onVisibleChange,
|
||||
extraData,
|
||||
onSuccess
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
<GoCaptcha.Click
|
||||
config={{
|
||||
width: 300,
|
||||
height: 220,
|
||||
dotSize: 24,
|
||||
}}
|
||||
data={handler.data}
|
||||
events={{
|
||||
close: handler.closeEvent,
|
||||
refresh: handler.refreshEvent,
|
||||
confirm: handler.confirmEvent,
|
||||
}}
|
||||
ref={domRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClickTextCapt;
|
||||
33
src/components/goCaptcha/RotateCapt.js
Normal file
33
src/components/goCaptcha/RotateCapt.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, {useRef} from 'react'
|
||||
import GoCaptcha from 'go-captcha-react'
|
||||
// Cache Testing
|
||||
// import GoCaptcha from '../cache'
|
||||
import {useRotateHandler} from "../hooks/useRotateHandler";
|
||||
|
||||
function RotateCapt() {
|
||||
const domRef = useRef(null)
|
||||
|
||||
const handler = useRotateHandler(domRef, {
|
||||
getApi: "/api/go-captcha-data/rotate-basic",
|
||||
checkApi: "/api/go-captcha-check-data/rotate-basic"
|
||||
})
|
||||
|
||||
return (
|
||||
<GoCaptcha.Rotate
|
||||
config={{
|
||||
width: 300,
|
||||
height: 220,
|
||||
size: 220,
|
||||
}}
|
||||
data={handler.data}
|
||||
events={{
|
||||
close: handler.closeEvent,
|
||||
refresh: handler.refreshEvent,
|
||||
confirm: handler.confirmEvent,
|
||||
}}
|
||||
ref={domRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default RotateCapt;
|
||||
32
src/components/goCaptcha/SlideCapt.js
Normal file
32
src/components/goCaptcha/SlideCapt.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, {useRef} from 'react'
|
||||
import GoCaptcha from 'go-captcha-react'
|
||||
// Cache Testing
|
||||
// import GoCaptcha from '../cache'
|
||||
import {useSlideHandler} from "../hooks/useSlideHandler";
|
||||
|
||||
function SlideCapt() {
|
||||
const domRef = useRef(null)
|
||||
|
||||
const handler = useSlideHandler(domRef, {
|
||||
getApi: "/api/go-captcha-data/slide-basic",
|
||||
checkApi: "/api/go-captcha-check-data/slide-basic"
|
||||
})
|
||||
|
||||
return (
|
||||
<GoCaptcha.Slide
|
||||
config={{
|
||||
width: 300,
|
||||
height: 220,
|
||||
}}
|
||||
data={handler.data}
|
||||
events={{
|
||||
close: handler.closeEvent,
|
||||
refresh: handler.refreshEvent,
|
||||
confirm: handler.confirmEvent,
|
||||
}}
|
||||
ref={domRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SlideCapt;
|
||||
32
src/components/goCaptcha/SlideRegionCapt.js
Normal file
32
src/components/goCaptcha/SlideRegionCapt.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, {useRef} from 'react'
|
||||
import GoCaptcha from 'go-captcha-react'
|
||||
// Cache Testing
|
||||
// import GoCaptcha from '../cache'
|
||||
import {useSlideHandler} from "../hooks/useSlideHandler";
|
||||
|
||||
function SlideRegionCapt() {
|
||||
const domRef = useRef(null)
|
||||
|
||||
const handler = useSlideHandler(domRef, {
|
||||
getApi: "/api/go-captcha-data/slide-region",
|
||||
checkApi: "/api/go-captcha-check-data/slide-region"
|
||||
})
|
||||
|
||||
return (
|
||||
<GoCaptcha.SlideRegion
|
||||
config={{
|
||||
width: 300,
|
||||
height: 220,
|
||||
}}
|
||||
data={handler.data}
|
||||
events={{
|
||||
close: handler.closeEvent,
|
||||
refresh: handler.refreshEvent,
|
||||
confirm: handler.confirmEvent,
|
||||
}}
|
||||
ref={domRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SlideRegionCapt;
|
||||
116
src/components/goCaptcha/hooks/useClickHandler.js
Normal file
116
src/components/goCaptcha/hooks/useClickHandler.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @Author Awen
|
||||
* @Date 2024/05/25
|
||||
* @Email wengaolng@gmail.com
|
||||
**/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from 'sonner';
|
||||
import { request } from '@/utils/request';
|
||||
|
||||
export const useClickHandler = (domRef, config) => {
|
||||
const [state, setState] = useState({ popoverVisible: false })
|
||||
const [data, setData] = useState({})
|
||||
|
||||
const clickEvent = useCallback(() => {
|
||||
setState({ ...state, popoverVisible: true })
|
||||
}, [state])
|
||||
|
||||
const visibleChangeEvent = useCallback((visible) => {
|
||||
setState({ ...state, popoverVisible: visible })
|
||||
if (config.onVisibleChange) {
|
||||
config.onVisibleChange(visible)
|
||||
}
|
||||
}, [state, config])
|
||||
|
||||
const closeEvent = useCallback(() => {
|
||||
setState({ ...state, popoverVisible: false })
|
||||
if (config.onVisibleChange) {
|
||||
config.onVisibleChange(false)
|
||||
}
|
||||
}, [state, config])
|
||||
|
||||
const requestCaptchaData = useCallback(async () => {
|
||||
domRef.current.clear && domRef.current.clear()
|
||||
try {
|
||||
const response = await request(config.getApi)
|
||||
const data = await response.json()
|
||||
|
||||
if (data && (data['code'] || 0) === 0) {
|
||||
setData({
|
||||
image: data['image_base64'] || '',
|
||||
thumb: data['thumb_base64'] || '',
|
||||
captKey: data['captcha_key'] || ''
|
||||
})
|
||||
} else {
|
||||
toast.error('获取验证码失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
toast.error('获取验证码失败')
|
||||
}
|
||||
}, [config.getApi, setData])
|
||||
|
||||
const refreshEvent = useCallback(() => {
|
||||
requestCaptchaData()
|
||||
}, [requestCaptchaData])
|
||||
|
||||
const confirmEvent = useCallback(async (dots, clear) => {
|
||||
const dotArr = []
|
||||
dots.forEach((item) => {
|
||||
dotArr.push(item.x, item.y)
|
||||
})
|
||||
|
||||
try {
|
||||
const formData = new URLSearchParams({
|
||||
dots: dotArr.join(','),
|
||||
key: data.captKey || '',
|
||||
extraData: JSON.stringify(config.extraData),
|
||||
})
|
||||
const response = await request(config.checkApi, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
|
||||
const responseData = await response.json()
|
||||
|
||||
if ((responseData['code'] || 0) === 0) {
|
||||
toast.success(responseData.message)
|
||||
setState({ ...state, popoverVisible: false, type: "success" })
|
||||
if (config.onVisibleChange) {
|
||||
config.onVisibleChange(false)
|
||||
}
|
||||
if (config.onSuccess) {
|
||||
config.onSuccess()
|
||||
}
|
||||
} else {
|
||||
toast.error('验证码验证失败')
|
||||
setState({ ...state, type: "error" })
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
requestCaptchaData()
|
||||
}, 1000)
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
toast.error('验证码验证失败')
|
||||
}
|
||||
}, [data, state, setState, config.checkApi, requestCaptchaData])
|
||||
|
||||
useEffect(() => {
|
||||
requestCaptchaData()
|
||||
}, [requestCaptchaData])
|
||||
|
||||
return {
|
||||
state,
|
||||
data,
|
||||
visibleChangeEvent,
|
||||
clickEvent,
|
||||
closeEvent,
|
||||
refreshEvent,
|
||||
confirmEvent,
|
||||
}
|
||||
}
|
||||
96
src/components/goCaptcha/hooks/useRotateHandler.js
Normal file
96
src/components/goCaptcha/hooks/useRotateHandler.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @Author Awen
|
||||
* @Date 2024/05/25
|
||||
* @Email wengaolng@gmail.com
|
||||
**/
|
||||
|
||||
import {useCallback, useEffect, useRef, useState} from "react";
|
||||
import Lodash from "lodash";
|
||||
import Axios from 'axios'
|
||||
import { Message } from '@arco-design/web-react';
|
||||
import Qs from 'qs'
|
||||
|
||||
export const useRotateHandler = (domRef, config) => {
|
||||
const [state, setState] = useState({})
|
||||
const [data, setData] = useState({})
|
||||
|
||||
const clickEvent = useCallback(() => {
|
||||
setState({...state, popoverVisible: true})
|
||||
}, [state])
|
||||
|
||||
const visibleChangeEvent = useCallback((visible) => {
|
||||
setState({...state, popoverVisible: visible})
|
||||
}, [state])
|
||||
|
||||
const closeEvent = useCallback(() => {
|
||||
setState({...state, popoverVisible: false})
|
||||
}, [state])
|
||||
|
||||
const requestCaptchaData = useCallback(() => {
|
||||
domRef.current.clear && domRef.current.clear()
|
||||
Axios({
|
||||
method: 'get',
|
||||
url: config.getApi,
|
||||
}).then((response)=>{
|
||||
const {data = {}} = response;
|
||||
if (!Lodash.isEmpty(data) && (data['code'] || 0) === 0) {
|
||||
setData({
|
||||
image: data['image_base64'] || '',
|
||||
thumb: data['thumb_base64'] || '',
|
||||
thumbSize: data['thumb_size'] || 0,
|
||||
captKey: data['captcha_key'] || '',
|
||||
})
|
||||
} else {
|
||||
Message.warning(`failed get captcha data`)
|
||||
}
|
||||
}).catch((e)=>{
|
||||
console.warn(e)
|
||||
})
|
||||
}, [setData, config.getApi])
|
||||
|
||||
const refreshEvent = useCallback(() => {
|
||||
requestCaptchaData()
|
||||
}, [requestCaptchaData])
|
||||
|
||||
const confirmEvent = useCallback((angle, clear) => {
|
||||
Axios({
|
||||
method: 'post',
|
||||
url: config.checkApi,
|
||||
data: Qs.stringify({
|
||||
angle: angle,
|
||||
key: data.captKey || ''
|
||||
}),
|
||||
}).then((response)=>{
|
||||
const {data = {}} = response;
|
||||
if ((data['code'] || 0) === 0) {
|
||||
Message.success(`check captcha data success`)
|
||||
setState({...state, popoverVisible: false, type: "success"})
|
||||
} else {
|
||||
Message.warning(`failed check captcha data`)
|
||||
setState({...state, type: "error"})
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
requestCaptchaData()
|
||||
}, 1000)
|
||||
}).catch((e)=>{
|
||||
console.warn(e)
|
||||
})
|
||||
}, [data, state, setState, config.checkApi, requestCaptchaData])
|
||||
|
||||
useEffect(() => {
|
||||
// if (state.popoverVisible) {
|
||||
requestCaptchaData()
|
||||
// }
|
||||
}, [])
|
||||
|
||||
return {
|
||||
state,
|
||||
data,
|
||||
visibleChangeEvent,
|
||||
clickEvent,
|
||||
closeEvent,
|
||||
refreshEvent,
|
||||
confirmEvent,
|
||||
}
|
||||
}
|
||||
99
src/components/goCaptcha/hooks/useSlideHandler.js
Normal file
99
src/components/goCaptcha/hooks/useSlideHandler.js
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* @Author Awen
|
||||
* @Date 2024/05/25
|
||||
* @Email wengaolng@gmail.com
|
||||
**/
|
||||
|
||||
import {useCallback, useEffect, useRef, useState} from "react";
|
||||
import Lodash from "lodash";
|
||||
import Axios from 'axios'
|
||||
import { Message } from '@arco-design/web-react';
|
||||
import Qs from 'qs'
|
||||
|
||||
export const useSlideHandler = (domRef, config) => {
|
||||
const [state, setState] = useState({})
|
||||
const [data, setData] = useState({})
|
||||
|
||||
const clickEvent = useCallback(() => {
|
||||
setState({...state, popoverVisible: true})
|
||||
}, [state])
|
||||
|
||||
const visibleChangeEvent = useCallback((visible) => {
|
||||
setState({...state, popoverVisible: visible})
|
||||
}, [state])
|
||||
|
||||
const closeEvent = useCallback(() => {
|
||||
setState({...state, popoverVisible: false})
|
||||
}, [state])
|
||||
|
||||
const requestCaptchaData = useCallback(() => {
|
||||
domRef.current.clear && domRef.current.clear()
|
||||
Axios({
|
||||
method: 'get',
|
||||
url: config.getApi,
|
||||
}).then((response)=>{
|
||||
const {data = {}} = response;
|
||||
if (!Lodash.isEmpty(data) && (data['code'] || 0) === 0) {
|
||||
setData({
|
||||
image: data['image_base64'] || '',
|
||||
thumb: data['tile_base64'] || '',
|
||||
captKey: data['captcha_key'] || '',
|
||||
thumbX: data['tile_x'] || 0,
|
||||
thumbY: data['tile_y'] || 0,
|
||||
thumbWidth: data['tile_width'] || 0,
|
||||
thumbHeight: data['tile_height'] || 0,
|
||||
})
|
||||
} else {
|
||||
Message.warning(`failed get captcha data`)
|
||||
}
|
||||
}).catch((e)=>{
|
||||
console.warn(e)
|
||||
})
|
||||
}, [setData, config.getApi])
|
||||
|
||||
const refreshEvent = useCallback(() => {
|
||||
requestCaptchaData()
|
||||
}, [requestCaptchaData])
|
||||
|
||||
const confirmEvent = useCallback((point, clear) => {
|
||||
Axios({
|
||||
method: 'post',
|
||||
url: config.checkApi,
|
||||
data: Qs.stringify({
|
||||
point: [point.x, point.y].join(','),
|
||||
key: data.captKey || ''
|
||||
}),
|
||||
}).then((response)=>{
|
||||
const {data = {}} = response;
|
||||
if ((data['code'] || 0) === 0) {
|
||||
Message.success(`check captcha data success`)
|
||||
setState({...state, popoverVisible: false, type: "success"})
|
||||
} else {
|
||||
Message.warning(`failed check captcha data`)
|
||||
setState({...state, type: "error"})
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
requestCaptchaData()
|
||||
}, 1000)
|
||||
}).catch((e)=>{
|
||||
console.warn(e)
|
||||
})
|
||||
}, [data, state, setState, config.checkApi, requestCaptchaData])
|
||||
|
||||
useEffect(() => {
|
||||
// if (state.popoverVisible) {
|
||||
requestCaptchaData()
|
||||
// }
|
||||
}, [])
|
||||
|
||||
return {
|
||||
state,
|
||||
data,
|
||||
visibleChangeEvent,
|
||||
clickEvent,
|
||||
closeEvent,
|
||||
refreshEvent,
|
||||
confirmEvent,
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { MembersManagement } from '@/pages/chat/components/MembersManagement';
|
||||
import Sidebar from './Sidebar';
|
||||
import { AdBanner, AdBannerMobile } from './AdSection';
|
||||
import { useUserStore } from '@/store/userStore';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { getAvatarData } from '@/utils/avatar';
|
||||
|
||||
|
||||
@@ -55,6 +56,7 @@ const KaTeXStyle = () => (
|
||||
|
||||
const ChatUI = () => {
|
||||
const userStore = useUserStore();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
//获取url参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -77,7 +79,14 @@ const ChatUI = () => {
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const [mutedUsers, setMutedUsers] = useState<string[]>([]);
|
||||
const [showPoster, setShowPoster] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false); // 默认关闭,稍后根据设备类型设置
|
||||
|
||||
// 根据设备类型设置侧边栏默认状态
|
||||
useEffect(() => {
|
||||
if (isMobile !== undefined) {
|
||||
setSidebarOpen(!isMobile); // 手机端关闭,PC端开启
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
// 2. 所有的 useRef 声明
|
||||
const currentMessageRef = useRef<number | null>(null);
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { toast } from 'sonner';
|
||||
import { request } from '@/utils/request';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import ClickTextCapt from "@/components/goCaptcha/ClickTextCapt.jsx";
|
||||
|
||||
interface PhoneLoginProps {
|
||||
onLogin: (phone: string, code: string) => void;
|
||||
@@ -13,33 +15,14 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ handleLoginSuccess }) => {
|
||||
const [code, setCode] = useState('');
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showCaptcha, setShowCaptcha] = useState(false); // 新增弹窗状态
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// 获取备案号配置
|
||||
const icpNumber = (window as any).APP_CONFIG?.ICP_NUMBER;
|
||||
|
||||
// 发送验证码
|
||||
const handleSendCode = async () => {
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
toast.error('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await request(`/api/sendcode`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ phone }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('发送验证码失败');
|
||||
}
|
||||
|
||||
toast.success('验证码已发送');
|
||||
|
||||
// 发送验证码成功后倒计时
|
||||
const handleSendCodeSuccess = () => {
|
||||
// 开始倒计时
|
||||
setCountdown(60);
|
||||
const timer = setInterval(() => {
|
||||
@@ -51,15 +34,23 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ handleLoginSuccess }) => {
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error);
|
||||
toast.error('发送验证码失败,请重试');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
// 点击发送验证码按钮
|
||||
const handleSendCode = async () => {
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
toast.error('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
setShowCaptcha(true); // 弹出图形验证码弹窗
|
||||
};
|
||||
|
||||
// 图形验证码通过后的回调
|
||||
// const handleCaptchaSuccess = async () => {
|
||||
// setShowCaptcha(false);
|
||||
// await realSendCode();
|
||||
// };
|
||||
|
||||
// 提交登录
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -91,55 +82,56 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ handleLoginSuccess }) => {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-white flex items-center justify-center">
|
||||
<div className="w-full max-w-md p-8">
|
||||
<div className={`w-full ${isMobile ? 'max-w-sm px-6' : 'max-w-md px-8'} ${isMobile ? 'py-6' : 'py-8'}`}>
|
||||
{/* Logo */}
|
||||
<div className="flex items-center justify-center mb-4">
|
||||
<span style={{fontFamily: 'Audiowide, system-ui', color: '#ff6600'}} className="text-3xl ml-2">botgroup.chat</span>
|
||||
<div className="flex items-center justify-center mb-6">
|
||||
<span
|
||||
style={{fontFamily: 'Audiowide, system-ui', color: '#ff6600'}}
|
||||
className={`${isMobile ? 'text-2xl' : 'text-3xl'} ml-2`}
|
||||
>
|
||||
botgroup.chat
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-gray-500 mb-4 text-center">
|
||||
<div className={`text-gray-500 ${isMobile ? 'mb-6' : 'mb-4'} text-center ${isMobile ? 'text-sm' : 'text-base'}`}>
|
||||
仅支持中国大陆手机号登录
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<form onSubmit={handleSubmit} className={`${isMobile ? 'space-y-4' : 'space-y-6'}`}>
|
||||
<div>
|
||||
<div className="flex items-center border rounded-lg p-3 h-[46px] focus-within:border-[#ff6600]">
|
||||
<span className="text-gray-400 mr-2">+86</span>
|
||||
<div className={`flex items-center border rounded-lg ${isMobile ? 'p-2.5' : 'p-3'} ${isMobile ? 'h-[42px]' : 'h-[46px]'} focus-within:border-[#ff6600]`}>
|
||||
<span className={`text-gray-400 mr-2 ${isMobile ? 'text-sm' : 'text-base'}`}>+86</span>
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="请输入手机号"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
maxLength={11}
|
||||
className="border-none focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none p-0"
|
||||
className={`border-none focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none p-0 ${isMobile ? 'text-sm' : 'text-base'}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex gap-3">
|
||||
<div className={`flex ${isMobile ? 'gap-2' : 'gap-3'}`}>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="请输入验证码"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
maxLength={6}
|
||||
className="border rounded-lg p-3 h-[46px] focus:border-[#ff6600] focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none"
|
||||
className={`border rounded-lg ${isMobile ? 'p-2.5' : 'p-3'} ${isMobile ? 'h-[42px]' : 'h-[46px]'} focus:border-[#ff6600] focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none ${isMobile ? 'text-sm' : 'text-base'}`}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSendCode}
|
||||
disabled={countdown > 0 || isLoading}
|
||||
className="bg-white text-[#ff6600] border border-[#ff6600] hover:bg-[#ff6600] hover:text-white rounded-lg px-6 h-[46px]"
|
||||
className={`bg-white text-[#ff6600] border border-[#ff6600] hover:bg-[#ff6600] hover:text-white rounded-lg ${isMobile ? 'px-3 h-[42px] text-xs' : 'px-6 h-[46px] text-sm'} whitespace-nowrap`}
|
||||
>
|
||||
{countdown > 0 ? `${countdown}秒后重试` : '发送验证码'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-[#ff6600] hover:bg-[#e65c00] text-white rounded-lg py-3"
|
||||
className={`w-full bg-[#ff6600] hover:bg-[#e65c00] text-white rounded-lg ${isMobile ? 'py-2.5 text-sm' : 'py-3 text-base'}`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
@@ -147,10 +139,17 @@ const PhoneLogin: React.FC<PhoneLoginProps> = ({ handleLoginSuccess }) => {
|
||||
) : '登录'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
{/* 弹窗形式的图形验证码 */}
|
||||
{showCaptcha && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
|
||||
<div className={`${isMobile ? 'mx-4' : 'mx-0'}`}>
|
||||
<ClickTextCapt onVisibleChange={setShowCaptcha} onSuccess={handleSendCodeSuccess} extraData={{phone}} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 备案号显示 */}
|
||||
{icpNumber && (
|
||||
<div className="text-center mt-8 text-xs text-gray-400">
|
||||
<div className={`text-center ${isMobile ? 'mt-6' : 'mt-8'} text-xs text-gray-400`}>
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener noreferrer">{icpNumber}</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user