From 68cf8c0be2ae53bfb4fe80c96475c3e811e6ffc6 Mon Sep 17 00:00:00 2001 From: JiaJun <2394389886@qq.com> Date: Tue, 2 Jun 2026 11:09:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=88=87=E6=8D=A2=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E6=96=B9=E5=BC=8F=E4=B8=BA=E6=89=8B=E6=9C=BA=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E5=B9=B6=E6=B7=BB=E5=8A=A0=E5=AE=A2=E6=9C=8D?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将注册表单从用户名改为手机号输入 - 集成短信验证码功能,添加验证码输入字段 - 实现发送短信验证码的API调用和倒计时逻辑 - 更新国际化配置以支持验证码相关文案 - 添加桌面端客服聊天弹窗功能 - 调整注册表单UI布局和样式优化 - 修改认证相关常量和类型定义以适配新流程 --- src/constants/auth.ts | 10 +- src/constants/system.ts | 2 + src/features/auth/api/auth-api.ts | 31 ++++- src/features/auth/api/types.ts | 30 ++++- .../components/desktop-register-form-view.tsx | 123 +++++++++++++----- .../auth/components/desktop-register-form.tsx | 23 +++- src/features/auth/hooks/use-register-form.ts | 3 +- src/features/auth/hooks/use-send-sms-code.ts | 51 ++++++++ src/features/auth/schema/auth-schema.ts | 8 +- .../components/desktop/desktop-header.tsx | 19 +++ src/features/game/entry/entry-page.tsx | 3 + .../modal/desktop/desktop-support-modal.tsx | 75 +++++++++++ src/locales/en-US/common.ts | 26 +++- src/locales/id-ID/common.ts | 26 +++- src/locales/ms-MY/common.ts | 26 +++- src/locales/zh-CN/common.ts | 26 +++- 16 files changed, 410 insertions(+), 72 deletions(-) create mode 100644 src/features/auth/hooks/use-send-sms-code.ts create mode 100644 src/features/game/modal/desktop/desktop-support-modal.tsx diff --git a/src/constants/auth.ts b/src/constants/auth.ts index 925d488..b036330 100644 --- a/src/constants/auth.ts +++ b/src/constants/auth.ts @@ -8,17 +8,11 @@ export const AUTH_ENDPOINTS = { profile: 'api/user/profile', refreshToken: 'api/user/refreshToken', register: 'api/user/register', + sendSmsCode: 'api/user/sendSmsCode', } as const -/** @description 后端返回该 code 表示登录态 token 无效或已过期。 */ -export const AUTH_INVALID_TOKEN_CODE = 1101 - /** @description 后端返回这些 code 时需要清空当前状态并触发用户重新登录。 */ -export const AUTH_RELOGIN_REQUIRED_CODES: readonly number[] = [ - AUTH_INVALID_TOKEN_CODE, - 1103, - 303, -] +export const AUTH_RELOGIN_REQUIRED_CODES: readonly number[] = [1101, 1103, 303] /** @description 获取接口鉴权 auth-token 时使用的接口地址。 */ export const AUTH_TOKEN_ENDPOINT = 'api/v1/authToken' diff --git a/src/constants/system.ts b/src/constants/system.ts index 0b2d59f..f5cfc32 100644 --- a/src/constants/system.ts +++ b/src/constants/system.ts @@ -113,6 +113,7 @@ export const MODAL_KEYS = [ 'desktopProcedures', 'desktopWithdrawTopup', 'desktopPeriodHistory', + 'desktopSupport', ] as const /** @description 全局弹窗默认可见状态。 */ @@ -127,4 +128,5 @@ export const INITIAL_MODAL_VISIBILITY = { desktopProcedures: false, desktopWithdrawTopup: false, desktopPeriodHistory: false, + desktopSupport: false, } as const diff --git a/src/features/auth/api/auth-api.ts b/src/features/auth/api/auth-api.ts index 9cc480a..2058220 100644 --- a/src/features/auth/api/auth-api.ts +++ b/src/features/auth/api/auth-api.ts @@ -15,6 +15,10 @@ import type { RefreshTokenRequestDto, RegisterPayload, RegisterRequestDto, + SendSmsCodeDto, + SendSmsCodePayload, + SendSmsCodeRequestDto, + SendSmsCodeResult, } from './types' import { mergeAuthUsers, @@ -140,10 +144,11 @@ export async function registerWithPassword( AUTH_ENDPOINTS.register, { json: { + captcha: payload.captcha, device_id: getAuthDeviceId(), invite_code: payload.inviteCode, password: payload.password, - username: payload.username, + username: payload.mobile, }, }, ) @@ -160,6 +165,30 @@ export async function registerWithPassword( return session } +export async function sendSmsCode( + payload: SendSmsCodePayload, +): Promise { + const response = await api.post( + AUTH_ENDPOINTS.sendSmsCode, + { + json: { + event: 'user_register', + mobile: payload.mobile, + }, + }, + ) + + const data = unwrapEnvelope( + response as ApiResponse, + 'auth.register.sms.errors.submitFailed', + ) + + return { + expiresIn: data.expires_in, + messageId: data.message_id, + } +} + export async function getCurrentUserProfile() { const response = await api.post(AUTH_ENDPOINTS.profile) diff --git a/src/features/auth/api/types.ts b/src/features/auth/api/types.ts index 21cca30..6b17863 100644 --- a/src/features/auth/api/types.ts +++ b/src/features/auth/api/types.ts @@ -61,8 +61,22 @@ export interface LogoutRequestDto { username: string } -export interface RegisterRequestDto extends LoginRequestDto { +export interface RegisterRequestDto { + captcha: string + device_id?: string invite_code: string + password: string + username: string +} + +export interface SendSmsCodeRequestDto { + event: 'user_register' + mobile: string +} + +export interface SendSmsCodeDto { + expires_in: number + message_id: string } export interface RefreshTokenRequestDto { @@ -76,8 +90,20 @@ export interface LoginPayload { export type LogoutPayload = LoginPayload -export interface RegisterPayload extends LoginPayload { +export interface RegisterPayload { + captcha: string inviteCode: string + mobile: string + password: string +} + +export interface SendSmsCodePayload { + mobile: string +} + +export interface SendSmsCodeResult { + expiresIn: number + messageId: string } export function normalizeAuthUser(dto: AuthUserDto): AuthUser { diff --git a/src/features/auth/components/desktop-register-form-view.tsx b/src/features/auth/components/desktop-register-form-view.tsx index ca00e6f..44bdc8e 100644 --- a/src/features/auth/components/desktop-register-form-view.tsx +++ b/src/features/auth/components/desktop-register-form-view.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import loginBg from '@/assets/system/login-bg.webp' import { SmartBackground } from '@/components/smart-background.tsx' import { Input } from '@/components/ui/input.tsx' +import { cn } from '@/lib/utils' import { DesktopAuthFieldRow, DesktopAuthInputError, @@ -11,40 +12,56 @@ import { interface DesktopRegisterFormViewProps { errors: { + captcha?: string confirmPassword?: string inviteCode?: string + mobile?: string password?: string - username?: string } + captcha: string inviteCode: string + isSendingSmsCode: boolean isSubmitting: boolean + mobile: string + onCaptchaChange: (value: string) => void onConfirmPasswordChange: (value: string) => void onInviteCodeChange: (value: string) => void + onMobileChange: (value: string) => void onPasswordChange: (value: string) => void + onSendSmsCode: () => Promise onSubmit: () => void onSwitchToLogin: () => void - onUsernameChange: (value: string) => void + smsCodeCanSend: boolean + smsCodeRemainingSeconds: number password: string confirmPassword: string - username: string } export function DesktopRegisterFormView({ + captcha, confirmPassword, errors, inviteCode, + isSendingSmsCode, isSubmitting, + mobile, + onCaptchaChange, onConfirmPasswordChange, onInviteCodeChange, + onMobileChange, onPasswordChange, + onSendSmsCode, onSubmit, onSwitchToLogin, - onUsernameChange, + smsCodeCanSend, + smsCodeRemainingSeconds, password, - username, }: DesktopRegisterFormViewProps) { const { t } = useTranslation() const prefersReducedMotion = useReducedMotion() + const compactInputClassName = + 'h-design-54 border border-transparent bg-[linear-gradient(180deg,rgba(23,98,105,0.72),rgba(11,62,68,0.82))] py-design-11 text-left text-design-21 shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(129,239,243,0.05)]' + const compactErrorClassName = 'relative h-design-26 overflow-hidden' return (
-
+
- + onUsernameChange(event.target.value)} - placeholder={t('auth.register.fields.username.placeholder')} - aria-describedby="desktop-register-username-error" - aria-invalid={Boolean(errors.username)} - className={ - 'h-design-58 border border-transparent bg-[linear-gradient(180deg,rgba(23,98,105,0.72),rgba(11,62,68,0.82))] text-left shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(129,239,243,0.05)]' - } + value={mobile} + onChange={(event) => onMobileChange(event.target.value)} + placeholder={t('auth.register.fields.mobile.placeholder')} + aria-describedby="desktop-register-mobile-error" + aria-invalid={Boolean(errors.mobile)} + className={compactInputClassName} />
+
+
+
+ + +
+ onCaptchaChange(event.target.value)} + placeholder={t('auth.register.fields.captcha.placeholder')} + aria-describedby="desktop-register-captcha-error" + aria-invalid={Boolean(errors.captcha)} + className={compactInputClassName} + /> + +
+
+
+
@@ -106,13 +167,11 @@ export function DesktopRegisterFormView({ placeholder={t('auth.register.fields.password.placeholder')} aria-describedby="desktop-register-password-error" aria-invalid={Boolean(errors.password)} - className={ - 'h-design-58 border border-transparent bg-[linear-gradient(180deg,rgba(23,98,105,0.72),rgba(11,62,68,0.82))] text-left shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(129,239,243,0.05)]' - } + className={compactInputClassName} />
+ + setModalOpen('desktopSupport', true)} + whileTap={{ + scale: 0.95, + }} + whileHover={{ scale: 1.05 }} + > + +
{authStatus === 'authenticated' ? ( diff --git a/src/features/game/entry/entry-page.tsx b/src/features/game/entry/entry-page.tsx index 6332ef8..9ebac3a 100644 --- a/src/features/game/entry/entry-page.tsx +++ b/src/features/game/entry/entry-page.tsx @@ -14,6 +14,7 @@ import { DesktopPeriodHistoryDrawer } from '@/features/game/modal/desktop/deskto import DesktopProceduresModal from '@/features/game/modal/desktop/desktop-procedures-modal.tsx' import DesktopRegisterModal from '@/features/game/modal/desktop/desktop-register-modal.tsx' import DesktopRulesModal from '@/features/game/modal/desktop/desktop-rules-modal.tsx' +import DesktopSupportModal from '@/features/game/modal/desktop/desktop-support-modal.tsx' import DesktopUserInfoModal from '@/features/game/modal/desktop/desktop-userInfo-modal.tsx' import DesktopWithdrawTopupModal from '@/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx' import { useDocumentMetadata } from '@/lib/head/document-metadata' @@ -42,6 +43,8 @@ function EntryModalHost() { {/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */} + {/* 桌面端客服弹窗:承载在线客服 iframe */} + {/* 强制弹窗 */} {/* 历史开奖信息弹窗 */} diff --git a/src/features/game/modal/desktop/desktop-support-modal.tsx b/src/features/game/modal/desktop/desktop-support-modal.tsx new file mode 100644 index 0000000..ec2cd9e --- /dev/null +++ b/src/features/game/modal/desktop/desktop-support-modal.tsx @@ -0,0 +1,75 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { CenterModal } from '@/components/center-modal.tsx' +import { DataLoadingIndicator } from '@/components/ui/data-loading-indicator' +import { useModalStore } from '@/store' + +const SUPPORT_CHAT_URL = + 'https://tawk.to/chat/6a1d23d9e29f411c2ce86772/1jq0t82lu' +const IFRAME_READY_DELAY_MS = 2_000 + +function DesktopSupportModal() { + const [isLoading, setIsLoading] = useState(true) + const readyTimerRef = useRef(null) + const open = useModalStore((state) => state.modals.desktopSupport) + const setModalOpen = useModalStore((state) => state.setModalOpen) + + const clearReadyTimer = useCallback(() => { + if (readyTimerRef.current === null) { + return + } + + window.clearTimeout(readyTimerRef.current) + readyTimerRef.current = null + }, []) + + const handleClose = () => { + setModalOpen('desktopSupport', false) + } + + const handleLoaded = () => { + clearReadyTimer() + readyTimerRef.current = window.setTimeout(() => { + setIsLoading(false) + readyTimerRef.current = null + }, IFRAME_READY_DELAY_MS) + } + + useEffect(() => { + if (open) { + clearReadyTimer() + setIsLoading(true) + } + + return clearReadyTimer + }, [clearReadyTimer, open]) + + return ( + 在线客服
} + className="h-design-760 w-design-980" + > +
+
+ {isLoading ? ( +
+ +
+ ) : null} +