diff --git a/package-lock.json b/package-lock.json index 72549f6..dc4fc03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 7ab24b1..62b2419 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/goCaptcha/ClickShapeCapt.js b/src/components/goCaptcha/ClickShapeCapt.js new file mode 100644 index 0000000..2d8a626 --- /dev/null +++ b/src/components/goCaptcha/ClickShapeCapt.js @@ -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 ( + + ); +} + +export default ClickShapeCapt; diff --git a/src/components/goCaptcha/ClickTextCapt.jsx b/src/components/goCaptcha/ClickTextCapt.jsx new file mode 100644 index 0000000..12ad159 --- /dev/null +++ b/src/components/goCaptcha/ClickTextCapt.jsx @@ -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 ( + + ); +} + +export default ClickTextCapt; \ No newline at end of file diff --git a/src/components/goCaptcha/RotateCapt.js b/src/components/goCaptcha/RotateCapt.js new file mode 100644 index 0000000..a5f0ae4 --- /dev/null +++ b/src/components/goCaptcha/RotateCapt.js @@ -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 ( + + ); +} + +export default RotateCapt; diff --git a/src/components/goCaptcha/SlideCapt.js b/src/components/goCaptcha/SlideCapt.js new file mode 100644 index 0000000..4bdb75d --- /dev/null +++ b/src/components/goCaptcha/SlideCapt.js @@ -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 ( + + ); +} + +export default SlideCapt; diff --git a/src/components/goCaptcha/SlideRegionCapt.js b/src/components/goCaptcha/SlideRegionCapt.js new file mode 100644 index 0000000..119cbba --- /dev/null +++ b/src/components/goCaptcha/SlideRegionCapt.js @@ -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 ( + + ); +} + +export default SlideRegionCapt; diff --git a/src/components/goCaptcha/hooks/useClickHandler.js b/src/components/goCaptcha/hooks/useClickHandler.js new file mode 100644 index 0000000..49a44fe --- /dev/null +++ b/src/components/goCaptcha/hooks/useClickHandler.js @@ -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, + } +} diff --git a/src/components/goCaptcha/hooks/useRotateHandler.js b/src/components/goCaptcha/hooks/useRotateHandler.js new file mode 100644 index 0000000..3c31213 --- /dev/null +++ b/src/components/goCaptcha/hooks/useRotateHandler.js @@ -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, + } +} diff --git a/src/components/goCaptcha/hooks/useSlideHandler.js b/src/components/goCaptcha/hooks/useSlideHandler.js new file mode 100644 index 0000000..c0ec588 --- /dev/null +++ b/src/components/goCaptcha/hooks/useSlideHandler.js @@ -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, + } +} diff --git a/src/pages/chat/components/ChatUI.tsx b/src/pages/chat/components/ChatUI.tsx index 7c29efd..3363ad6 100644 --- a/src/pages/chat/components/ChatUI.tsx +++ b/src/pages/chat/components/ChatUI.tsx @@ -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([]); 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(null); diff --git a/src/pages/login/comonents/PhoneLogin.tsx b/src/pages/login/comonents/PhoneLogin.tsx index 4cea270..4e0fe6d 100644 --- a/src/pages/login/comonents/PhoneLogin.tsx +++ b/src/pages/login/comonents/PhoneLogin.tsx @@ -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 = ({ 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 = ({ 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 = ({ handleLoginSuccess }) => { return (
-
+
{/* Logo */} -
- botgroup.chat +
+ + botgroup.chat +
- -
+
仅支持中国大陆手机号登录
- -
+
-
- +86 +
+ +86 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'}`} />
-
-
+
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'}`} />
- - + {/* 弹窗形式的图形验证码 */} + {showCaptcha && ( +
+
+ +
+
+ )} {/* 备案号显示 */} {icpNumber && ( -