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} +