feat(auth): 切换注册方式为手机验证码并添加客服功能
- 将注册表单从用户名改为手机号输入 - 集成短信验证码功能,添加验证码输入字段 - 实现发送短信验证码的API调用和倒计时逻辑 - 更新国际化配置以支持验证码相关文案 - 添加桌面端客服聊天弹窗功能 - 调整注册表单UI布局和样式优化 - 修改认证相关常量和类型定义以适配新流程
This commit is contained in:
@@ -8,17 +8,11 @@ export const AUTH_ENDPOINTS = {
|
|||||||
profile: 'api/user/profile',
|
profile: 'api/user/profile',
|
||||||
refreshToken: 'api/user/refreshToken',
|
refreshToken: 'api/user/refreshToken',
|
||||||
register: 'api/user/register',
|
register: 'api/user/register',
|
||||||
|
sendSmsCode: 'api/user/sendSmsCode',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** @description 后端返回该 code 表示登录态 token 无效或已过期。 */
|
|
||||||
export const AUTH_INVALID_TOKEN_CODE = 1101
|
|
||||||
|
|
||||||
/** @description 后端返回这些 code 时需要清空当前状态并触发用户重新登录。 */
|
/** @description 后端返回这些 code 时需要清空当前状态并触发用户重新登录。 */
|
||||||
export const AUTH_RELOGIN_REQUIRED_CODES: readonly number[] = [
|
export const AUTH_RELOGIN_REQUIRED_CODES: readonly number[] = [1101, 1103, 303]
|
||||||
AUTH_INVALID_TOKEN_CODE,
|
|
||||||
1103,
|
|
||||||
303,
|
|
||||||
]
|
|
||||||
|
|
||||||
/** @description 获取接口鉴权 auth-token 时使用的接口地址。 */
|
/** @description 获取接口鉴权 auth-token 时使用的接口地址。 */
|
||||||
export const AUTH_TOKEN_ENDPOINT = 'api/v1/authToken'
|
export const AUTH_TOKEN_ENDPOINT = 'api/v1/authToken'
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export const MODAL_KEYS = [
|
|||||||
'desktopProcedures',
|
'desktopProcedures',
|
||||||
'desktopWithdrawTopup',
|
'desktopWithdrawTopup',
|
||||||
'desktopPeriodHistory',
|
'desktopPeriodHistory',
|
||||||
|
'desktopSupport',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
/** @description 全局弹窗默认可见状态。 */
|
/** @description 全局弹窗默认可见状态。 */
|
||||||
@@ -127,4 +128,5 @@ export const INITIAL_MODAL_VISIBILITY = {
|
|||||||
desktopProcedures: false,
|
desktopProcedures: false,
|
||||||
desktopWithdrawTopup: false,
|
desktopWithdrawTopup: false,
|
||||||
desktopPeriodHistory: false,
|
desktopPeriodHistory: false,
|
||||||
|
desktopSupport: false,
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import type {
|
|||||||
RefreshTokenRequestDto,
|
RefreshTokenRequestDto,
|
||||||
RegisterPayload,
|
RegisterPayload,
|
||||||
RegisterRequestDto,
|
RegisterRequestDto,
|
||||||
|
SendSmsCodeDto,
|
||||||
|
SendSmsCodePayload,
|
||||||
|
SendSmsCodeRequestDto,
|
||||||
|
SendSmsCodeResult,
|
||||||
} from './types'
|
} from './types'
|
||||||
import {
|
import {
|
||||||
mergeAuthUsers,
|
mergeAuthUsers,
|
||||||
@@ -140,10 +144,11 @@ export async function registerWithPassword(
|
|||||||
AUTH_ENDPOINTS.register,
|
AUTH_ENDPOINTS.register,
|
||||||
{
|
{
|
||||||
json: {
|
json: {
|
||||||
|
captcha: payload.captcha,
|
||||||
device_id: getAuthDeviceId(),
|
device_id: getAuthDeviceId(),
|
||||||
invite_code: payload.inviteCode,
|
invite_code: payload.inviteCode,
|
||||||
password: payload.password,
|
password: payload.password,
|
||||||
username: payload.username,
|
username: payload.mobile,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -160,6 +165,30 @@ export async function registerWithPassword(
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendSmsCode(
|
||||||
|
payload: SendSmsCodePayload,
|
||||||
|
): Promise<SendSmsCodeResult> {
|
||||||
|
const response = await api.post<SendSmsCodeDto, SendSmsCodeRequestDto>(
|
||||||
|
AUTH_ENDPOINTS.sendSmsCode,
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
event: 'user_register',
|
||||||
|
mobile: payload.mobile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = unwrapEnvelope(
|
||||||
|
response as ApiResponse<SendSmsCodeDto>,
|
||||||
|
'auth.register.sms.errors.submitFailed',
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
messageId: data.message_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCurrentUserProfile() {
|
export async function getCurrentUserProfile() {
|
||||||
const response = await api.post<AuthUserProfileDto>(AUTH_ENDPOINTS.profile)
|
const response = await api.post<AuthUserProfileDto>(AUTH_ENDPOINTS.profile)
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,22 @@ export interface LogoutRequestDto {
|
|||||||
username: string
|
username: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterRequestDto extends LoginRequestDto {
|
export interface RegisterRequestDto {
|
||||||
|
captcha: string
|
||||||
|
device_id?: string
|
||||||
invite_code: 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 {
|
export interface RefreshTokenRequestDto {
|
||||||
@@ -76,8 +90,20 @@ export interface LoginPayload {
|
|||||||
|
|
||||||
export type LogoutPayload = LoginPayload
|
export type LogoutPayload = LoginPayload
|
||||||
|
|
||||||
export interface RegisterPayload extends LoginPayload {
|
export interface RegisterPayload {
|
||||||
|
captcha: string
|
||||||
inviteCode: 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 {
|
export function normalizeAuthUser(dto: AuthUserDto): AuthUser {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import loginBg from '@/assets/system/login-bg.webp'
|
import loginBg from '@/assets/system/login-bg.webp'
|
||||||
import { SmartBackground } from '@/components/smart-background.tsx'
|
import { SmartBackground } from '@/components/smart-background.tsx'
|
||||||
import { Input } from '@/components/ui/input.tsx'
|
import { Input } from '@/components/ui/input.tsx'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
DesktopAuthFieldRow,
|
DesktopAuthFieldRow,
|
||||||
DesktopAuthInputError,
|
DesktopAuthInputError,
|
||||||
@@ -11,40 +12,56 @@ import {
|
|||||||
|
|
||||||
interface DesktopRegisterFormViewProps {
|
interface DesktopRegisterFormViewProps {
|
||||||
errors: {
|
errors: {
|
||||||
|
captcha?: string
|
||||||
confirmPassword?: string
|
confirmPassword?: string
|
||||||
inviteCode?: string
|
inviteCode?: string
|
||||||
|
mobile?: string
|
||||||
password?: string
|
password?: string
|
||||||
username?: string
|
|
||||||
}
|
}
|
||||||
|
captcha: string
|
||||||
inviteCode: string
|
inviteCode: string
|
||||||
|
isSendingSmsCode: boolean
|
||||||
isSubmitting: boolean
|
isSubmitting: boolean
|
||||||
|
mobile: string
|
||||||
|
onCaptchaChange: (value: string) => void
|
||||||
onConfirmPasswordChange: (value: string) => void
|
onConfirmPasswordChange: (value: string) => void
|
||||||
onInviteCodeChange: (value: string) => void
|
onInviteCodeChange: (value: string) => void
|
||||||
|
onMobileChange: (value: string) => void
|
||||||
onPasswordChange: (value: string) => void
|
onPasswordChange: (value: string) => void
|
||||||
|
onSendSmsCode: () => Promise<unknown>
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
onSwitchToLogin: () => void
|
onSwitchToLogin: () => void
|
||||||
onUsernameChange: (value: string) => void
|
smsCodeCanSend: boolean
|
||||||
|
smsCodeRemainingSeconds: number
|
||||||
password: string
|
password: string
|
||||||
confirmPassword: string
|
confirmPassword: string
|
||||||
username: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DesktopRegisterFormView({
|
export function DesktopRegisterFormView({
|
||||||
|
captcha,
|
||||||
confirmPassword,
|
confirmPassword,
|
||||||
errors,
|
errors,
|
||||||
inviteCode,
|
inviteCode,
|
||||||
|
isSendingSmsCode,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
|
mobile,
|
||||||
|
onCaptchaChange,
|
||||||
onConfirmPasswordChange,
|
onConfirmPasswordChange,
|
||||||
onInviteCodeChange,
|
onInviteCodeChange,
|
||||||
|
onMobileChange,
|
||||||
onPasswordChange,
|
onPasswordChange,
|
||||||
|
onSendSmsCode,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onSwitchToLogin,
|
onSwitchToLogin,
|
||||||
onUsernameChange,
|
smsCodeCanSend,
|
||||||
|
smsCodeRemainingSeconds,
|
||||||
password,
|
password,
|
||||||
username,
|
|
||||||
}: DesktopRegisterFormViewProps) {
|
}: DesktopRegisterFormViewProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prefersReducedMotion = useReducedMotion()
|
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 (
|
return (
|
||||||
<form
|
<form
|
||||||
@@ -53,7 +70,7 @@ export function DesktopRegisterFormView({
|
|||||||
onSubmit()
|
onSubmit()
|
||||||
}}
|
}}
|
||||||
className={
|
className={
|
||||||
'relative isolate flex flex-col px-design-30 mt-design-10 box-border h-design-700 !overflow-hidden'
|
'relative isolate mt-design-8 box-border flex h-design-700 flex-col px-design-30 !overflow-hidden'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -63,34 +80,78 @@ export function DesktopRegisterFormView({
|
|||||||
>
|
>
|
||||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(93,211,218,0.16),transparent_30%),radial-gradient(circle_at_bottom_left,rgba(63,109,137,0.22),transparent_34%)]" />
|
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(93,211,218,0.16),transparent_30%),radial-gradient(circle_at_bottom_left,rgba(63,109,137,0.22),transparent_34%)]" />
|
||||||
<div className="pointer-events-none absolute inset-x-0 top-0 h-design-140 bg-[linear-gradient(180deg,rgba(87,196,201,0.12),transparent)]" />
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-design-140 bg-[linear-gradient(180deg,rgba(87,196,201,0.12),transparent)]" />
|
||||||
<div className="relative flex flex-col gap-design-20 p-design-50">
|
<div className="relative flex flex-col gap-design-12 p-design-38">
|
||||||
<div className="flex items-center gap-design-14">
|
<div className="flex items-center gap-design-14">
|
||||||
<div className="h-design-10 w-design-10 rounded-full bg-[#6EE4E6] shadow-[0_0_calc(var(--design-unit)*12)_rgba(110,228,230,0.75)]" />
|
<div className="h-design-10 w-design-10 rounded-full bg-[#6EE4E6] shadow-[0_0_calc(var(--design-unit)*12)_rgba(110,228,230,0.75)]" />
|
||||||
<div className="h-px flex-1 bg-[linear-gradient(90deg,rgba(110,228,230,0.5),rgba(110,228,230,0))]" />
|
<div className="h-px flex-1 bg-[linear-gradient(90deg,rgba(110,228,230,0.5),rgba(110,228,230,0))]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DesktopAuthFieldRow label={t('auth.register.fields.username.label')}>
|
<DesktopAuthFieldRow label={t('auth.register.fields.mobile.label')}>
|
||||||
<Input
|
<Input
|
||||||
id="desktop-register-username"
|
id="desktop-register-mobile"
|
||||||
name="username"
|
name="mobile"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
|
inputMode="tel"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
value={username}
|
value={mobile}
|
||||||
onChange={(event) => onUsernameChange(event.target.value)}
|
onChange={(event) => onMobileChange(event.target.value)}
|
||||||
placeholder={t('auth.register.fields.username.placeholder')}
|
placeholder={t('auth.register.fields.mobile.placeholder')}
|
||||||
aria-describedby="desktop-register-username-error"
|
aria-describedby="desktop-register-mobile-error"
|
||||||
aria-invalid={Boolean(errors.username)}
|
aria-invalid={Boolean(errors.mobile)}
|
||||||
className={
|
className={compactInputClassName}
|
||||||
'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)]'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
id="desktop-register-username-error"
|
id="desktop-register-mobile-error"
|
||||||
className="relative h-design-30 overflow-hidden"
|
className={compactErrorClassName}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-x-0 top-0">
|
<div className="absolute inset-x-0 top-0">
|
||||||
<DesktopAuthInputError
|
<DesktopAuthInputError
|
||||||
message={errors.username ? t(errors.username) : undefined}
|
message={errors.mobile ? t(errors.mobile) : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DesktopAuthFieldRow>
|
||||||
|
|
||||||
|
<DesktopAuthFieldRow label={t('auth.register.fields.captcha.label')}>
|
||||||
|
<div className="flex gap-design-12">
|
||||||
|
<Input
|
||||||
|
id="desktop-register-captcha"
|
||||||
|
name="captcha"
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
inputMode="numeric"
|
||||||
|
spellCheck={false}
|
||||||
|
value={captcha}
|
||||||
|
onChange={(event) => onCaptchaChange(event.target.value)}
|
||||||
|
placeholder={t('auth.register.fields.captcha.placeholder')}
|
||||||
|
aria-describedby="desktop-register-captcha-error"
|
||||||
|
aria-invalid={Boolean(errors.captcha)}
|
||||||
|
className={compactInputClassName}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void onSendSmsCode()}
|
||||||
|
disabled={!mobile.trim() || !smsCodeCanSend}
|
||||||
|
className={cn(
|
||||||
|
'h-design-54 w-design-166 shrink-0 cursor-pointer rounded-md border border-[#3F8E93] bg-[linear-gradient(180deg,rgba(37,116,122,0.9),rgba(16,75,82,0.9))] px-design-12 text-design-19 font-semibold text-[#E8FFFF] transition duration-200 ease-out hover:brightness-110 focus-visible:outline-none focus-visible:ring-[calc(var(--design-unit)*1.5)] focus-visible:ring-[rgba(110,255,255,0.35)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-55',
|
||||||
|
smsCodeRemainingSeconds > 0 && 'text-[#9AC7CA]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{smsCodeRemainingSeconds > 0
|
||||||
|
? t('auth.register.sms.countdown', {
|
||||||
|
seconds: smsCodeRemainingSeconds,
|
||||||
|
})
|
||||||
|
: isSendingSmsCode
|
||||||
|
? t('auth.register.sms.sending')
|
||||||
|
: t('auth.register.sms.send')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="desktop-register-captcha-error"
|
||||||
|
className={compactErrorClassName}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-x-0 top-0">
|
||||||
|
<DesktopAuthInputError
|
||||||
|
message={errors.captcha ? t(errors.captcha) : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,13 +167,11 @@ export function DesktopRegisterFormView({
|
|||||||
placeholder={t('auth.register.fields.password.placeholder')}
|
placeholder={t('auth.register.fields.password.placeholder')}
|
||||||
aria-describedby="desktop-register-password-error"
|
aria-describedby="desktop-register-password-error"
|
||||||
aria-invalid={Boolean(errors.password)}
|
aria-invalid={Boolean(errors.password)}
|
||||||
className={
|
className={compactInputClassName}
|
||||||
'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)]'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
id="desktop-register-password-error"
|
id="desktop-register-password-error"
|
||||||
className="relative h-design-30 overflow-hidden"
|
className={compactErrorClassName}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-x-0 top-0">
|
<div className="absolute inset-x-0 top-0">
|
||||||
<DesktopAuthInputError
|
<DesktopAuthInputError
|
||||||
@@ -136,13 +195,11 @@ export function DesktopRegisterFormView({
|
|||||||
)}
|
)}
|
||||||
aria-describedby="desktop-register-confirm-password-error"
|
aria-describedby="desktop-register-confirm-password-error"
|
||||||
aria-invalid={Boolean(errors.confirmPassword)}
|
aria-invalid={Boolean(errors.confirmPassword)}
|
||||||
className={
|
className={compactInputClassName}
|
||||||
'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)]'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
id="desktop-register-confirm-password-error"
|
id="desktop-register-confirm-password-error"
|
||||||
className="relative h-design-30 overflow-hidden"
|
className={compactErrorClassName}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-x-0 top-0">
|
<div className="absolute inset-x-0 top-0">
|
||||||
<DesktopAuthInputError
|
<DesktopAuthInputError
|
||||||
@@ -170,13 +227,11 @@ export function DesktopRegisterFormView({
|
|||||||
placeholder={t('auth.register.fields.inviteCode.placeholder')}
|
placeholder={t('auth.register.fields.inviteCode.placeholder')}
|
||||||
aria-describedby="desktop-register-invite-code-error"
|
aria-describedby="desktop-register-invite-code-error"
|
||||||
aria-invalid={Boolean(errors.inviteCode)}
|
aria-invalid={Boolean(errors.inviteCode)}
|
||||||
className={
|
className={cn(compactInputClassName, 'max-w-design-520')}
|
||||||
'h-design-58 max-w-design-520 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)]'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
id="desktop-register-invite-code-error"
|
id="desktop-register-invite-code-error"
|
||||||
className="relative h-design-30 overflow-hidden"
|
className={compactErrorClassName}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-x-0 top-0">
|
<div className="absolute inset-x-0 top-0">
|
||||||
<DesktopAuthInputError
|
<DesktopAuthInputError
|
||||||
@@ -189,7 +244,7 @@ export function DesktopRegisterFormView({
|
|||||||
<div className="mt-auto flex flex-col">
|
<div className="mt-auto flex flex-col">
|
||||||
<motion.div
|
<motion.div
|
||||||
whileTap={prefersReducedMotion ? undefined : { scale: 0.98 }}
|
whileTap={prefersReducedMotion ? undefined : { scale: 0.98 }}
|
||||||
className="flex items-center justify-center gap-design-12 text-center text-design-20 text-[#6DB5B9]"
|
className="flex items-center justify-center gap-design-12 text-center text-design-18 text-[#6DB5B9]"
|
||||||
>
|
>
|
||||||
<div className="h-px w-design-90 bg-[linear-gradient(90deg,rgba(109,181,185,0),rgba(109,181,185,0.7))]" />
|
<div className="h-px w-design-90 bg-[linear-gradient(90deg,rgba(109,181,185,0),rgba(109,181,185,0.7))]" />
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useController } from 'react-hook-form'
|
import { useController } from 'react-hook-form'
|
||||||
import { useModalStore } from '@/store'
|
import { useModalStore } from '@/store'
|
||||||
import { useRegisterForm } from '../hooks/use-register-form'
|
import { useRegisterForm } from '../hooks/use-register-form'
|
||||||
|
import { useSendSmsCode } from '../hooks/use-send-sms-code'
|
||||||
import { DesktopRegisterFormView } from './desktop-register-form-view'
|
import { DesktopRegisterFormView } from './desktop-register-form-view'
|
||||||
|
|
||||||
interface DesktopRegisterFormProps {
|
interface DesktopRegisterFormProps {
|
||||||
@@ -11,10 +12,15 @@ export function DesktopRegisterForm({ onSuccess }: DesktopRegisterFormProps) {
|
|||||||
const { form, isSubmitting, onSubmit } = useRegisterForm({
|
const { form, isSubmitting, onSubmit } = useRegisterForm({
|
||||||
onSuccess,
|
onSuccess,
|
||||||
})
|
})
|
||||||
|
const smsCode = useSendSmsCode()
|
||||||
const openExclusiveModal = useModalStore((state) => state.openExclusiveModal)
|
const openExclusiveModal = useModalStore((state) => state.openExclusiveModal)
|
||||||
const usernameField = useController({
|
const mobileField = useController({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
name: 'username',
|
name: 'mobile',
|
||||||
|
})
|
||||||
|
const captchaField = useController({
|
||||||
|
control: form.control,
|
||||||
|
name: 'captcha',
|
||||||
})
|
})
|
||||||
const passwordField = useController({
|
const passwordField = useController({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
@@ -35,23 +41,30 @@ export function DesktopRegisterForm({ onSuccess }: DesktopRegisterFormProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DesktopRegisterFormView
|
<DesktopRegisterFormView
|
||||||
username={usernameField.field.value ?? ''}
|
mobile={mobileField.field.value ?? ''}
|
||||||
|
captcha={captchaField.field.value ?? ''}
|
||||||
password={passwordField.field.value ?? ''}
|
password={passwordField.field.value ?? ''}
|
||||||
confirmPassword={confirmPasswordField.field.value ?? ''}
|
confirmPassword={confirmPasswordField.field.value ?? ''}
|
||||||
inviteCode={inviteCodeField.field.value ?? ''}
|
inviteCode={inviteCodeField.field.value ?? ''}
|
||||||
errors={{
|
errors={{
|
||||||
|
captcha: form.formState.errors.captcha?.message,
|
||||||
confirmPassword: form.formState.errors.confirmPassword?.message,
|
confirmPassword: form.formState.errors.confirmPassword?.message,
|
||||||
inviteCode: form.formState.errors.inviteCode?.message,
|
inviteCode: form.formState.errors.inviteCode?.message,
|
||||||
|
mobile: form.formState.errors.mobile?.message,
|
||||||
password: form.formState.errors.password?.message,
|
password: form.formState.errors.password?.message,
|
||||||
username: form.formState.errors.username?.message,
|
|
||||||
}}
|
}}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
|
isSendingSmsCode={smsCode.isSending}
|
||||||
onConfirmPasswordChange={confirmPasswordField.field.onChange}
|
onConfirmPasswordChange={confirmPasswordField.field.onChange}
|
||||||
|
onCaptchaChange={captchaField.field.onChange}
|
||||||
onInviteCodeChange={inviteCodeField.field.onChange}
|
onInviteCodeChange={inviteCodeField.field.onChange}
|
||||||
|
onSendSmsCode={() => smsCode.send(mobileField.field.value ?? '')}
|
||||||
onPasswordChange={passwordField.field.onChange}
|
onPasswordChange={passwordField.field.onChange}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onSwitchToLogin={handleSwitchToLogin}
|
onSwitchToLogin={handleSwitchToLogin}
|
||||||
onUsernameChange={usernameField.field.onChange}
|
onMobileChange={mobileField.field.onChange}
|
||||||
|
smsCodeCanSend={smsCode.canSend}
|
||||||
|
smsCodeRemainingSeconds={smsCode.remainingSeconds}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,10 +34,11 @@ export function useRegisterForm({ onSuccess }: UseRegisterFormOptions = {}) {
|
|||||||
const startSession = useAuthStore((state) => state.startSession)
|
const startSession = useAuthStore((state) => state.startSession)
|
||||||
const form = useForm<RegisterFormValues>({
|
const form = useForm<RegisterFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
captcha: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
inviteCode: getInitialRegisterInviteCode(),
|
inviteCode: getInitialRegisterInviteCode(),
|
||||||
|
mobile: '',
|
||||||
password: '',
|
password: '',
|
||||||
username: '',
|
|
||||||
},
|
},
|
||||||
mode: 'onBlur',
|
mode: 'onBlur',
|
||||||
resolver: createZodResolver(registerFormSchema),
|
resolver: createZodResolver(registerFormSchema),
|
||||||
|
|||||||
51
src/features/auth/hooks/use-send-sms-code.ts
Normal file
51
src/features/auth/hooks/use-send-sms-code.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import i18n from '@/i18n'
|
||||||
|
import { notify } from '@/lib/notify'
|
||||||
|
import { sendSmsCode } from '../api/auth-api'
|
||||||
|
import { toAuthSubmitErrorKey } from './auth-error-key'
|
||||||
|
|
||||||
|
const FALLBACK_SMS_CODE_COOLDOWN_SECONDS = 60
|
||||||
|
|
||||||
|
export function useSendSmsCode() {
|
||||||
|
const [remainingSeconds, setRemainingSeconds] = useState(0)
|
||||||
|
const mutation = useMutation({
|
||||||
|
mutationFn: (mobile: string) => sendSmsCode({ mobile: mobile.trim() }),
|
||||||
|
onError: (error) => {
|
||||||
|
const errorKey = toAuthSubmitErrorKey(error, 'register')
|
||||||
|
|
||||||
|
notify.error(
|
||||||
|
errorKey
|
||||||
|
? i18n.t(errorKey)
|
||||||
|
: i18n.t('auth.register.sms.errors.submitFailed'),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSuccess: (result) => {
|
||||||
|
setRemainingSeconds(
|
||||||
|
result.expiresIn > 0
|
||||||
|
? result.expiresIn
|
||||||
|
: FALLBACK_SMS_CODE_COOLDOWN_SECONDS,
|
||||||
|
)
|
||||||
|
notify.success(i18n.t('auth.register.sms.success'))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (remainingSeconds <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = window.setTimeout(() => {
|
||||||
|
setRemainingSeconds((value) => Math.max(0, value - 1))
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timer)
|
||||||
|
}, [remainingSeconds])
|
||||||
|
|
||||||
|
return {
|
||||||
|
canSend: !mutation.isPending && remainingSeconds <= 0,
|
||||||
|
isSending: mutation.isPending,
|
||||||
|
remainingSeconds,
|
||||||
|
send: mutation.mutateAsync,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,11 @@ const usernameSchema = z
|
|||||||
.trim()
|
.trim()
|
||||||
.min(1, 'auth.validation.username.required')
|
.min(1, 'auth.validation.username.required')
|
||||||
|
|
||||||
|
const captchaSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, 'auth.validation.captcha.required')
|
||||||
|
|
||||||
const passwordSchema = z
|
const passwordSchema = z
|
||||||
.string()
|
.string()
|
||||||
.min(6, 'auth.validation.password.min')
|
.min(6, 'auth.validation.password.min')
|
||||||
@@ -17,6 +22,7 @@ export const loginFormSchema = z.object({
|
|||||||
|
|
||||||
export const registerFormSchema = z
|
export const registerFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
captcha: captchaSchema,
|
||||||
confirmPassword: passwordSchema,
|
confirmPassword: passwordSchema,
|
||||||
inviteCode: z
|
inviteCode: z
|
||||||
.string()
|
.string()
|
||||||
@@ -24,7 +30,7 @@ export const registerFormSchema = z
|
|||||||
.min(1, 'auth.validation.inviteCode.required')
|
.min(1, 'auth.validation.inviteCode.required')
|
||||||
.max(32, 'auth.validation.inviteCode.max'),
|
.max(32, 'auth.validation.inviteCode.max'),
|
||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
username: usernameSchema,
|
mobile: usernameSchema,
|
||||||
})
|
})
|
||||||
.refine((value) => value.password === value.confirmPassword, {
|
.refine((value) => value.password === value.confirmPassword, {
|
||||||
message: 'auth.validation.confirmPassword.mismatch',
|
message: 'auth.validation.confirmPassword.mismatch',
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import {
|
|||||||
Volume2,
|
Volume2,
|
||||||
VolumeX,
|
VolumeX,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { motion } from 'motion/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import add from '@/assets/game/add.webp'
|
import add from '@/assets/game/add.webp'
|
||||||
import avatar from '@/assets/system/avatar.webp'
|
import avatar from '@/assets/system/avatar.webp'
|
||||||
|
import chatImage from '@/assets/system/chat.webp'
|
||||||
import diamond from '@/assets/system/diamond.webp'
|
import diamond from '@/assets/system/diamond.webp'
|
||||||
import logo from '@/assets/system/logo.webp'
|
import logo from '@/assets/system/logo.webp'
|
||||||
import { SmartImage } from '@/components/smart-image.tsx'
|
import { SmartImage } from '@/components/smart-image.tsx'
|
||||||
@@ -18,6 +20,7 @@ import {
|
|||||||
useHeaderClockLabel,
|
useHeaderClockLabel,
|
||||||
useHeaderVm,
|
useHeaderVm,
|
||||||
} from '@/features/game/hooks/use-header-vm'
|
} from '@/features/game/hooks/use-header-vm'
|
||||||
|
import { useModalStore } from '@/store'
|
||||||
|
|
||||||
function HeaderClock() {
|
function HeaderClock() {
|
||||||
const systemTimeLabel = useHeaderClockLabel()
|
const systemTimeLabel = useHeaderClockLabel()
|
||||||
@@ -59,6 +62,7 @@ function SignalBars({
|
|||||||
|
|
||||||
export function DesktopHeader() {
|
export function DesktopHeader() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||||
const {
|
const {
|
||||||
authStatus,
|
authStatus,
|
||||||
currentLanguageLabel,
|
currentLanguageLabel,
|
||||||
@@ -173,6 +177,21 @@ export function DesktopHeader() {
|
|||||||
)}
|
)}
|
||||||
<div>{t('gameDesktop.header.fullscreen')}</div>
|
<div>{t('gameDesktop.header.fullscreen')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setModalOpen('desktopSupport', true)}
|
||||||
|
whileTap={{
|
||||||
|
scale: 0.95,
|
||||||
|
}}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
>
|
||||||
|
<SmartImage
|
||||||
|
className={'h-design-30 w-design-30 cursor-pointer'}
|
||||||
|
alt={'chatImage'}
|
||||||
|
src={chatImage}
|
||||||
|
/>
|
||||||
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{authStatus === 'authenticated' ? (
|
{authStatus === 'authenticated' ? (
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { DesktopPeriodHistoryDrawer } from '@/features/game/modal/desktop/deskto
|
|||||||
import DesktopProceduresModal from '@/features/game/modal/desktop/desktop-procedures-modal.tsx'
|
import DesktopProceduresModal from '@/features/game/modal/desktop/desktop-procedures-modal.tsx'
|
||||||
import DesktopRegisterModal from '@/features/game/modal/desktop/desktop-register-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 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 DesktopUserInfoModal from '@/features/game/modal/desktop/desktop-userInfo-modal.tsx'
|
||||||
import DesktopWithdrawTopupModal from '@/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx'
|
import DesktopWithdrawTopupModal from '@/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx'
|
||||||
import { useDocumentMetadata } from '@/lib/head/document-metadata'
|
import { useDocumentMetadata } from '@/lib/head/document-metadata'
|
||||||
@@ -42,6 +43,8 @@ function EntryModalHost() {
|
|||||||
<DesktopProceduresModal />
|
<DesktopProceduresModal />
|
||||||
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
|
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
|
||||||
<DesktopWithdrawTopupModal />
|
<DesktopWithdrawTopupModal />
|
||||||
|
{/* 桌面端客服弹窗:承载在线客服 iframe */}
|
||||||
|
<DesktopSupportModal />
|
||||||
{/* 强制弹窗 */}
|
{/* 强制弹窗 */}
|
||||||
<EntryNoticeGateModal />
|
<EntryNoticeGateModal />
|
||||||
{/* 历史开奖信息弹窗 */}
|
{/* 历史开奖信息弹窗 */}
|
||||||
|
|||||||
75
src/features/game/modal/desktop/desktop-support-modal.tsx
Normal file
75
src/features/game/modal/desktop/desktop-support-modal.tsx
Normal file
@@ -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<number | null>(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 (
|
||||||
|
<CenterModal
|
||||||
|
open={open}
|
||||||
|
isNormalBg={true}
|
||||||
|
onClose={handleClose}
|
||||||
|
titleAlign="left"
|
||||||
|
title={<div className="modal-title-glow text-design-30">在线客服</div>}
|
||||||
|
className="h-design-760 w-design-980"
|
||||||
|
>
|
||||||
|
<div className="h-full px-design-24 pb-design-40 pt-design-10">
|
||||||
|
<div className="relative h-full overflow-hidden rounded-[calc(var(--design-unit)*14)] border border-[#2A6D73] bg-[linear-gradient(180deg,rgba(5,22,31,0.98),rgba(2,10,17,0.98))] shadow-[inset_0_0_calc(var(--design-unit)*22)_rgba(88,205,218,0.13),0_0_calc(var(--design-unit)*18)_rgba(31,156,174,0.14)]">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="absolute inset-0 z-10 flex items-center justify-center bg-[radial-gradient(circle_at_center,rgba(20,92,105,0.38),rgba(2,10,17,0.98)_58%)]">
|
||||||
|
<DataLoadingIndicator label="客服连线中" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<iframe
|
||||||
|
title="customer-service-chat"
|
||||||
|
src={SUPPORT_CHAT_URL}
|
||||||
|
onLoad={handleLoaded}
|
||||||
|
className="h-full w-full bg-[linear-gradient(180deg,#061923,#020A11)]"
|
||||||
|
allow="microphone; camera; clipboard-read; clipboard-write"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CenterModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DesktopSupportModal
|
||||||
@@ -295,8 +295,8 @@ export default {
|
|||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
username: {
|
username: {
|
||||||
label: 'Account / Phone:',
|
label: 'Mobile:',
|
||||||
placeholder: 'Enter account or mobile number',
|
placeholder: 'Enter mobile number',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
label: 'Password:',
|
label: 'Password:',
|
||||||
@@ -317,9 +317,13 @@ export default {
|
|||||||
submit: 'Register',
|
submit: 'Register',
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
username: {
|
mobile: {
|
||||||
label: 'Account / Phone:',
|
label: 'Mobile:',
|
||||||
placeholder: 'Enter account or mobile number',
|
placeholder: 'Enter mobile number',
|
||||||
|
},
|
||||||
|
captcha: {
|
||||||
|
label: 'Code:',
|
||||||
|
placeholder: 'Enter verification code',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
label: 'Password:',
|
label: 'Password:',
|
||||||
@@ -342,8 +346,20 @@ export default {
|
|||||||
submitFailed: 'Registration failed. Please try again later.',
|
submitFailed: 'Registration failed. Please try again later.',
|
||||||
unauthorized: 'Registration is not authorized. Please try again later.',
|
unauthorized: 'Registration is not authorized. Please try again later.',
|
||||||
},
|
},
|
||||||
|
sms: {
|
||||||
|
countdown: '{{seconds}}s',
|
||||||
|
errors: {
|
||||||
|
submitFailed: 'Failed to send code. Please try again later.',
|
||||||
|
},
|
||||||
|
send: 'Get code',
|
||||||
|
sending: 'Sending...',
|
||||||
|
success: 'Verification code sent.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
|
captcha: {
|
||||||
|
required: 'Please enter the verification code.',
|
||||||
|
},
|
||||||
username: {
|
username: {
|
||||||
required: 'Please enter your mobile number.',
|
required: 'Please enter your mobile number.',
|
||||||
invalidPhone: 'Please enter a valid mobile number.',
|
invalidPhone: 'Please enter a valid mobile number.',
|
||||||
|
|||||||
@@ -295,8 +295,8 @@ export default {
|
|||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
username: {
|
username: {
|
||||||
label: 'Akun / Telepon:',
|
label: 'Nomor Ponsel:',
|
||||||
placeholder: 'Masukkan akun atau nomor ponsel',
|
placeholder: 'Masukkan nomor ponsel',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
label: 'Kata Sandi:',
|
label: 'Kata Sandi:',
|
||||||
@@ -317,9 +317,13 @@ export default {
|
|||||||
submit: 'Daftar',
|
submit: 'Daftar',
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
username: {
|
mobile: {
|
||||||
label: 'Akun / Telepon:',
|
label: 'Nomor Ponsel:',
|
||||||
placeholder: 'Masukkan akun atau nomor ponsel',
|
placeholder: 'Masukkan nomor ponsel',
|
||||||
|
},
|
||||||
|
captcha: {
|
||||||
|
label: 'Kode:',
|
||||||
|
placeholder: 'Masukkan kode verifikasi',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
label: 'Kata Sandi:',
|
label: 'Kata Sandi:',
|
||||||
@@ -342,8 +346,20 @@ export default {
|
|||||||
submitFailed: 'Pendaftaran gagal. Silakan coba lagi nanti.',
|
submitFailed: 'Pendaftaran gagal. Silakan coba lagi nanti.',
|
||||||
unauthorized: 'Pendaftaran tidak diizinkan. Silakan coba lagi nanti.',
|
unauthorized: 'Pendaftaran tidak diizinkan. Silakan coba lagi nanti.',
|
||||||
},
|
},
|
||||||
|
sms: {
|
||||||
|
countdown: '{{seconds}}d',
|
||||||
|
errors: {
|
||||||
|
submitFailed: 'Gagal mengirim kode. Silakan coba lagi nanti.',
|
||||||
|
},
|
||||||
|
send: 'Ambil kode',
|
||||||
|
sending: 'Mengirim...',
|
||||||
|
success: 'Kode verifikasi telah dikirim.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
|
captcha: {
|
||||||
|
required: 'Silakan masukkan kode verifikasi.',
|
||||||
|
},
|
||||||
username: {
|
username: {
|
||||||
required: 'Silakan masukkan nomor ponsel.',
|
required: 'Silakan masukkan nomor ponsel.',
|
||||||
invalidPhone: 'Silakan masukkan nomor ponsel yang valid.',
|
invalidPhone: 'Silakan masukkan nomor ponsel yang valid.',
|
||||||
|
|||||||
@@ -300,8 +300,8 @@ export default {
|
|||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
username: {
|
username: {
|
||||||
label: 'Akaun / Telefon:',
|
label: 'Nombor Telefon:',
|
||||||
placeholder: 'Masukkan akaun atau nombor telefon',
|
placeholder: 'Masukkan nombor telefon',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
label: 'Kata Laluan:',
|
label: 'Kata Laluan:',
|
||||||
@@ -322,9 +322,13 @@ export default {
|
|||||||
submit: 'Daftar',
|
submit: 'Daftar',
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
username: {
|
mobile: {
|
||||||
label: 'Akaun / Telefon:',
|
label: 'Nombor Telefon:',
|
||||||
placeholder: 'Masukkan akaun atau nombor telefon',
|
placeholder: 'Masukkan nombor telefon',
|
||||||
|
},
|
||||||
|
captcha: {
|
||||||
|
label: 'Kod:',
|
||||||
|
placeholder: 'Masukkan kod pengesahan',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
label: 'Kata Laluan:',
|
label: 'Kata Laluan:',
|
||||||
@@ -347,8 +351,20 @@ export default {
|
|||||||
submitFailed: 'Pendaftaran gagal. Sila cuba lagi kemudian.',
|
submitFailed: 'Pendaftaran gagal. Sila cuba lagi kemudian.',
|
||||||
unauthorized: 'Pendaftaran tidak dibenarkan. Sila cuba lagi kemudian.',
|
unauthorized: 'Pendaftaran tidak dibenarkan. Sila cuba lagi kemudian.',
|
||||||
},
|
},
|
||||||
|
sms: {
|
||||||
|
countdown: '{{seconds}}s',
|
||||||
|
errors: {
|
||||||
|
submitFailed: 'Gagal menghantar kod. Sila cuba lagi kemudian.',
|
||||||
|
},
|
||||||
|
send: 'Dapatkan kod',
|
||||||
|
sending: 'Menghantar...',
|
||||||
|
success: 'Kod pengesahan telah dihantar.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
|
captcha: {
|
||||||
|
required: 'Sila masukkan kod pengesahan.',
|
||||||
|
},
|
||||||
username: {
|
username: {
|
||||||
required: 'Sila masukkan nombor telefon anda.',
|
required: 'Sila masukkan nombor telefon anda.',
|
||||||
invalidPhone: 'Sila masukkan nombor telefon yang sah.',
|
invalidPhone: 'Sila masukkan nombor telefon yang sah.',
|
||||||
|
|||||||
@@ -283,8 +283,8 @@ export default {
|
|||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
username: {
|
username: {
|
||||||
label: '账号/电话:',
|
label: '手机号:',
|
||||||
placeholder: '请输入账号或手机号',
|
placeholder: '请输入手机号',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
label: '密码:',
|
label: '密码:',
|
||||||
@@ -305,9 +305,13 @@ export default {
|
|||||||
submit: '注册',
|
submit: '注册',
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
username: {
|
mobile: {
|
||||||
label: '账号/电话:',
|
label: '手机号:',
|
||||||
placeholder: '请输入账号或手机号',
|
placeholder: '请输入手机号',
|
||||||
|
},
|
||||||
|
captcha: {
|
||||||
|
label: '验证码:',
|
||||||
|
placeholder: '请输入验证码',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
label: '密码:',
|
label: '密码:',
|
||||||
@@ -330,8 +334,20 @@ export default {
|
|||||||
submitFailed: '注册失败,请稍后重试',
|
submitFailed: '注册失败,请稍后重试',
|
||||||
unauthorized: '注册未授权,请稍后重试',
|
unauthorized: '注册未授权,请稍后重试',
|
||||||
},
|
},
|
||||||
|
sms: {
|
||||||
|
countdown: '{{seconds}}秒',
|
||||||
|
errors: {
|
||||||
|
submitFailed: '验证码发送失败,请稍后重试',
|
||||||
|
},
|
||||||
|
send: '获取验证码',
|
||||||
|
sending: '发送中...',
|
||||||
|
success: '验证码已发送',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
|
captcha: {
|
||||||
|
required: '请输入验证码',
|
||||||
|
},
|
||||||
username: {
|
username: {
|
||||||
required: '请输入手机号',
|
required: '请输入手机号',
|
||||||
invalidPhone: '请输入正确的手机号',
|
invalidPhone: '请输入正确的手机号',
|
||||||
|
|||||||
Reference in New Issue
Block a user