feat(auth): 切换注册方式为手机验证码并添加客服功能

- 将注册表单从用户名改为手机号输入
- 集成短信验证码功能,添加验证码输入字段
- 实现发送短信验证码的API调用和倒计时逻辑
- 更新国际化配置以支持验证码相关文案
- 添加桌面端客服聊天弹窗功能
- 调整注册表单UI布局和样式优化
- 修改认证相关常量和类型定义以适配新流程
This commit is contained in:
JiaJun
2026-06-02 11:09:24 +08:00
parent 522b8a1f28
commit 68cf8c0be2
16 changed files with 410 additions and 72 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -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<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() {
const response = await api.post<AuthUserProfileDto>(AUTH_ENDPOINTS.profile)

View File

@@ -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 {

View File

@@ -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<unknown>
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 (
<form
@@ -53,7 +70,7 @@ export function DesktopRegisterFormView({
onSubmit()
}}
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
@@ -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-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="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>
<DesktopAuthFieldRow label={t('auth.register.fields.username.label')}>
<DesktopAuthFieldRow label={t('auth.register.fields.mobile.label')}>
<Input
id="desktop-register-username"
name="username"
id="desktop-register-mobile"
name="mobile"
autoComplete="username"
inputMode="tel"
spellCheck={false}
value={username}
onChange={(event) => 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}
/>
<div
id="desktop-register-username-error"
className="relative h-design-30 overflow-hidden"
id="desktop-register-mobile-error"
className={compactErrorClassName}
>
<div className="absolute inset-x-0 top-0">
<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>
@@ -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}
/>
<div
id="desktop-register-password-error"
className="relative h-design-30 overflow-hidden"
className={compactErrorClassName}
>
<div className="absolute inset-x-0 top-0">
<DesktopAuthInputError
@@ -136,13 +195,11 @@ export function DesktopRegisterFormView({
)}
aria-describedby="desktop-register-confirm-password-error"
aria-invalid={Boolean(errors.confirmPassword)}
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}
/>
<div
id="desktop-register-confirm-password-error"
className="relative h-design-30 overflow-hidden"
className={compactErrorClassName}
>
<div className="absolute inset-x-0 top-0">
<DesktopAuthInputError
@@ -170,13 +227,11 @@ export function DesktopRegisterFormView({
placeholder={t('auth.register.fields.inviteCode.placeholder')}
aria-describedby="desktop-register-invite-code-error"
aria-invalid={Boolean(errors.inviteCode)}
className={
'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)]'
}
className={cn(compactInputClassName, 'max-w-design-520')}
/>
<div
id="desktop-register-invite-code-error"
className="relative h-design-30 overflow-hidden"
className={compactErrorClassName}
>
<div className="absolute inset-x-0 top-0">
<DesktopAuthInputError
@@ -189,7 +244,7 @@ export function DesktopRegisterFormView({
<div className="mt-auto flex flex-col">
<motion.div
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))]" />
<button

View File

@@ -1,6 +1,7 @@
import { useController } from 'react-hook-form'
import { useModalStore } from '@/store'
import { useRegisterForm } from '../hooks/use-register-form'
import { useSendSmsCode } from '../hooks/use-send-sms-code'
import { DesktopRegisterFormView } from './desktop-register-form-view'
interface DesktopRegisterFormProps {
@@ -11,10 +12,15 @@ export function DesktopRegisterForm({ onSuccess }: DesktopRegisterFormProps) {
const { form, isSubmitting, onSubmit } = useRegisterForm({
onSuccess,
})
const smsCode = useSendSmsCode()
const openExclusiveModal = useModalStore((state) => state.openExclusiveModal)
const usernameField = useController({
const mobileField = useController({
control: form.control,
name: 'username',
name: 'mobile',
})
const captchaField = useController({
control: form.control,
name: 'captcha',
})
const passwordField = useController({
control: form.control,
@@ -35,23 +41,30 @@ export function DesktopRegisterForm({ onSuccess }: DesktopRegisterFormProps) {
return (
<DesktopRegisterFormView
username={usernameField.field.value ?? ''}
mobile={mobileField.field.value ?? ''}
captcha={captchaField.field.value ?? ''}
password={passwordField.field.value ?? ''}
confirmPassword={confirmPasswordField.field.value ?? ''}
inviteCode={inviteCodeField.field.value ?? ''}
errors={{
captcha: form.formState.errors.captcha?.message,
confirmPassword: form.formState.errors.confirmPassword?.message,
inviteCode: form.formState.errors.inviteCode?.message,
mobile: form.formState.errors.mobile?.message,
password: form.formState.errors.password?.message,
username: form.formState.errors.username?.message,
}}
isSubmitting={isSubmitting}
isSendingSmsCode={smsCode.isSending}
onConfirmPasswordChange={confirmPasswordField.field.onChange}
onCaptchaChange={captchaField.field.onChange}
onInviteCodeChange={inviteCodeField.field.onChange}
onSendSmsCode={() => smsCode.send(mobileField.field.value ?? '')}
onPasswordChange={passwordField.field.onChange}
onSubmit={onSubmit}
onSwitchToLogin={handleSwitchToLogin}
onUsernameChange={usernameField.field.onChange}
onMobileChange={mobileField.field.onChange}
smsCodeCanSend={smsCode.canSend}
smsCodeRemainingSeconds={smsCode.remainingSeconds}
/>
)
}

View File

@@ -34,10 +34,11 @@ export function useRegisterForm({ onSuccess }: UseRegisterFormOptions = {}) {
const startSession = useAuthStore((state) => state.startSession)
const form = useForm<RegisterFormValues>({
defaultValues: {
captcha: '',
confirmPassword: '',
inviteCode: getInitialRegisterInviteCode(),
mobile: '',
password: '',
username: '',
},
mode: 'onBlur',
resolver: createZodResolver(registerFormSchema),

View 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,
}
}

View File

@@ -5,6 +5,11 @@ const usernameSchema = z
.trim()
.min(1, 'auth.validation.username.required')
const captchaSchema = z
.string()
.trim()
.min(1, 'auth.validation.captcha.required')
const passwordSchema = z
.string()
.min(6, 'auth.validation.password.min')
@@ -17,6 +22,7 @@ export const loginFormSchema = z.object({
export const registerFormSchema = z
.object({
captcha: captchaSchema,
confirmPassword: passwordSchema,
inviteCode: z
.string()
@@ -24,7 +30,7 @@ export const registerFormSchema = z
.min(1, 'auth.validation.inviteCode.required')
.max(32, 'auth.validation.inviteCode.max'),
password: passwordSchema,
username: usernameSchema,
mobile: usernameSchema,
})
.refine((value) => value.password === value.confirmPassword, {
message: 'auth.validation.confirmPassword.mismatch',

View File

@@ -8,9 +8,11 @@ import {
Volume2,
VolumeX,
} from 'lucide-react'
import { motion } from 'motion/react'
import { useTranslation } from 'react-i18next'
import add from '@/assets/game/add.webp'
import avatar from '@/assets/system/avatar.webp'
import chatImage from '@/assets/system/chat.webp'
import diamond from '@/assets/system/diamond.webp'
import logo from '@/assets/system/logo.webp'
import { SmartImage } from '@/components/smart-image.tsx'
@@ -18,6 +20,7 @@ import {
useHeaderClockLabel,
useHeaderVm,
} from '@/features/game/hooks/use-header-vm'
import { useModalStore } from '@/store'
function HeaderClock() {
const systemTimeLabel = useHeaderClockLabel()
@@ -59,6 +62,7 @@ function SignalBars({
export function DesktopHeader() {
const { t } = useTranslation()
const setModalOpen = useModalStore((state) => state.setModalOpen)
const {
authStatus,
currentLanguageLabel,
@@ -173,6 +177,21 @@ export function DesktopHeader() {
)}
<div>{t('gameDesktop.header.fullscreen')}</div>
</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>
{authStatus === 'authenticated' ? (

View File

@@ -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() {
<DesktopProceduresModal />
{/* 桌面端充值/提现业务弹窗:承载具体的充值或提现内容 */}
<DesktopWithdrawTopupModal />
{/* 桌面端客服弹窗:承载在线客服 iframe */}
<DesktopSupportModal />
{/* 强制弹窗 */}
<EntryNoticeGateModal />
{/* 历史开奖信息弹窗 */}

View 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

View File

@@ -295,8 +295,8 @@ export default {
},
fields: {
username: {
label: 'Account / Phone:',
placeholder: 'Enter account or mobile number',
label: 'Mobile:',
placeholder: 'Enter mobile number',
},
password: {
label: 'Password:',
@@ -317,9 +317,13 @@ export default {
submit: 'Register',
},
fields: {
username: {
label: 'Account / Phone:',
placeholder: 'Enter account or mobile number',
mobile: {
label: 'Mobile:',
placeholder: 'Enter mobile number',
},
captcha: {
label: 'Code:',
placeholder: 'Enter verification code',
},
password: {
label: 'Password:',
@@ -342,8 +346,20 @@ export default {
submitFailed: 'Registration failed. 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: {
captcha: {
required: 'Please enter the verification code.',
},
username: {
required: 'Please enter your mobile number.',
invalidPhone: 'Please enter a valid mobile number.',

View File

@@ -295,8 +295,8 @@ export default {
},
fields: {
username: {
label: 'Akun / Telepon:',
placeholder: 'Masukkan akun atau nomor ponsel',
label: 'Nomor Ponsel:',
placeholder: 'Masukkan nomor ponsel',
},
password: {
label: 'Kata Sandi:',
@@ -317,9 +317,13 @@ export default {
submit: 'Daftar',
},
fields: {
username: {
label: 'Akun / Telepon:',
placeholder: 'Masukkan akun atau nomor ponsel',
mobile: {
label: 'Nomor Ponsel:',
placeholder: 'Masukkan nomor ponsel',
},
captcha: {
label: 'Kode:',
placeholder: 'Masukkan kode verifikasi',
},
password: {
label: 'Kata Sandi:',
@@ -342,8 +346,20 @@ export default {
submitFailed: 'Pendaftaran gagal. 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: {
captcha: {
required: 'Silakan masukkan kode verifikasi.',
},
username: {
required: 'Silakan masukkan nomor ponsel.',
invalidPhone: 'Silakan masukkan nomor ponsel yang valid.',

View File

@@ -300,8 +300,8 @@ export default {
},
fields: {
username: {
label: 'Akaun / Telefon:',
placeholder: 'Masukkan akaun atau nombor telefon',
label: 'Nombor Telefon:',
placeholder: 'Masukkan nombor telefon',
},
password: {
label: 'Kata Laluan:',
@@ -322,9 +322,13 @@ export default {
submit: 'Daftar',
},
fields: {
username: {
label: 'Akaun / Telefon:',
placeholder: 'Masukkan akaun atau nombor telefon',
mobile: {
label: 'Nombor Telefon:',
placeholder: 'Masukkan nombor telefon',
},
captcha: {
label: 'Kod:',
placeholder: 'Masukkan kod pengesahan',
},
password: {
label: 'Kata Laluan:',
@@ -347,8 +351,20 @@ export default {
submitFailed: 'Pendaftaran gagal. 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: {
captcha: {
required: 'Sila masukkan kod pengesahan.',
},
username: {
required: 'Sila masukkan nombor telefon anda.',
invalidPhone: 'Sila masukkan nombor telefon yang sah.',

View File

@@ -283,8 +283,8 @@ export default {
},
fields: {
username: {
label: '账号/电话:',
placeholder: '请输入账号或手机号',
label: '手机号:',
placeholder: '请输入手机号',
},
password: {
label: '密码:',
@@ -305,9 +305,13 @@ export default {
submit: '注册',
},
fields: {
username: {
label: '账号/电话:',
placeholder: '请输入账号或手机号',
mobile: {
label: '手机号:',
placeholder: '请输入手机号',
},
captcha: {
label: '验证码:',
placeholder: '请输入验证码',
},
password: {
label: '密码:',
@@ -330,8 +334,20 @@ export default {
submitFailed: '注册失败,请稍后重试',
unauthorized: '注册未授权,请稍后重试',
},
sms: {
countdown: '{{seconds}}秒',
errors: {
submitFailed: '验证码发送失败,请稍后重试',
},
send: '获取验证码',
sending: '发送中...',
success: '验证码已发送',
},
},
validation: {
captcha: {
required: '请输入验证码',
},
username: {
required: '请输入手机号',
invalidPhone: '请输入正确的手机号',