refactor(constants): 提取常量并优化国际化配置

- 创建API相关常量文件,包括响应码、HTTP状态码、请求头等
- 将认证相关常量从auth模块提取到独立的常量文件
- 在API客户端中使用新定义的常量替换硬编码值
- 更新认证API和服务中对常量的引用
- 在国际化配置中创建统一的文案常量以减少重复
- 将认证表单验证规则改为使用常量配置
This commit is contained in:
JiaJun
2026-06-02 12:03:29 +08:00
parent 68cf8c0be2
commit 901ad1c30b
30 changed files with 707 additions and 273 deletions

24
src/constants/api.ts Normal file
View File

@@ -0,0 +1,24 @@
/** @description 业务响应成功状态码,响应 envelope.code 等于该值代表成功。 */
export const API_SUCCESS_CODE = 1
/** @description 接口层统一引用的常用 HTTP 状态码集合。 */
export const HTTP_STATUS = {
noContent: 204,
requestTimeout: 408,
unauthorized: 401,
} as const
/** @description 接口请求头字段名集合。 */
export const REQUEST_HEADERS = {
accept: 'Accept',
authToken: 'auth-token',
authorization: 'Authorization',
lang: 'lang',
userToken: 'user-token',
} as const
/** @description Authorization 请求头的 Bearer 前缀。 */
export const AUTHORIZATION_BEARER_PREFIX = 'Bearer '
/** @description JSON 内容类型标识,用于响应体内容类型判定。 */
export const CONTENT_TYPE_JSON = 'application/json'

View File

@@ -34,3 +34,27 @@ export const AUTH_TOKEN_CACHE_SKEW_MS = 30_000
/** @description 认证错误翻译 key 的统一前缀。 */ /** @description 认证错误翻译 key 的统一前缀。 */
export const AUTH_ERROR_KEY_PREFIX = 'auth.' export const AUTH_ERROR_KEY_PREFIX = 'auth.'
/** @description 密码最小长度,用于登录与注册表单校验。 */
export const PASSWORD_MIN_LENGTH = 6
/** @description 密码最大长度,用于登录与注册表单校验。 */
export const PASSWORD_MAX_LENGTH = 32
/** @description 邀请码最大长度,用于注册表单校验。 */
export const INVITE_CODE_MAX_LENGTH = 32
/** @description 短信验证码重发倒计时兜底秒数,后端未返回有效 expiresIn 时使用。 */
export const SMS_CODE_COOLDOWN_FALLBACK_SECONDS = 60
/** @description 发送短信验证码的业务事件类型,当前固定为注册场景。 */
export const SMS_SEND_EVENT_REGISTER = 'user_register'
/** @description 注册邀请码默认值URL 未携带邀请码参数时兜底。 */
export const DEFAULT_REGISTER_INVITE_CODE = 'D97DBC16'
/** @description 注册邀请码在 URL 查询参数中的字段名。 */
export const REGISTER_INVITE_CODE_QUERY_PARAM = 'registerInviteCode'
/** @description 触发登录提示(弹窗/Toast的最小去重间隔单位为毫秒。 */
export const LOGIN_PROMPT_DEDUP_MS = 1200

View File

@@ -240,6 +240,36 @@ export const ENTRY_NOTICE_CONFIRM_INTERVAL_MS = 24 * 60 * 60 * 1000
/** @description 游戏投注记录每页加载条数。 */ /** @description 游戏投注记录每页加载条数。 */
export const GAME_HISTORY_PAGE_SIZE = 20 export const GAME_HISTORY_PAGE_SIZE = 20
/** @description 列表类接口默认分页条数,用于财务、钱包、公告、投注单等列表。 */
export const DEFAULT_LIST_PAGE_SIZE = 20
/** @description 财务配置类查询(充值提现配置、充值档位)缓存新鲜时间,单位为毫秒。 */
export const FINANCE_CONFIG_QUERY_STALE_TIME_MS = 5 * 60 * 1000
/** @description 提现表单默认币种代码。 */
export const DEFAULT_CURRENCY_CODE = 'MYR'
/** @description 自动托管「单次中奖超过」停止规则的默认阈值。 */
export const AUTO_HOSTING_DEFAULT_SINGLE_WIN_THRESHOLD = 50_000
/** @description 全站大奖播报最大保留条数。 */
export const MAX_JACKPOT_BROADCAST_COUNT = 20
/** @description 实时连接延迟信号「优」阈值,单位为毫秒。 */
export const CONNECTION_LATENCY_GOOD_MS = 80
/** @description 实时连接延迟信号「良」阈值,同时用于判定连接是否健康,单位为毫秒。 */
export const CONNECTION_LATENCY_FAIR_MS = 150
/** @description 实时连接延迟信号「中」阈值,单位为毫秒。 */
export const CONNECTION_LATENCY_POOR_MS = 300
/** @description 提现收款邮箱校验正则。 */
export const WITHDRAW_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
/** @description 提现收款手机号校验正则6-20 位数字,允许前导 +。 */
export const WITHDRAW_PHONE_PATTERN = /^\+?\d{6,20}$/
/** @description 提现页快捷法币金额选项。 */ /** @description 提现页快捷法币金额选项。 */
export const QUICK_FIAT_AMOUNTS = [3, 30, 50, 100, 200, 500] as const export const QUICK_FIAT_AMOUNTS = [3, 30, 50, 100, 200, 500] as const

View File

@@ -1,3 +1,4 @@
export * from './api'
export * from './auth' export * from './auth'
export * from './game' export * from './game'
export * from './system' export * from './system'

View File

@@ -48,6 +48,9 @@ export const QUERY_RETRYABLE_STATUS_CODES = [
/** @description 桌面端布局切换起始断点,单位为像素。 */ /** @description 桌面端布局切换起始断点,单位为像素。 */
export const DESKTOP_LAYOUT_MIN_WIDTH_PX = 1024 export const DESKTOP_LAYOUT_MIN_WIDTH_PX = 1024
/** @description 移动端布局判定断点(最大宽度),单位为像素。 */
export const MOBILE_LAYOUT_BREAKPOINT_PX = 768
/** @description 应用支持的语言代码列表。 */ /** @description 应用支持的语言代码列表。 */
export const SUPPORTED_LANGUAGES = ['zh-CN', 'en-US', 'ms-MY', 'id-ID'] as const export const SUPPORTED_LANGUAGES = ['zh-CN', 'en-US', 'ms-MY', 'id-ID'] as const

View File

@@ -1,4 +1,9 @@
import { AUTH_ENDPOINTS, AUTH_SKIP_REFRESH_CONTEXT_KEY } from '@/constants' import {
API_SUCCESS_CODE,
AUTH_ENDPOINTS,
AUTH_SKIP_REFRESH_CONTEXT_KEY,
SMS_SEND_EVENT_REGISTER,
} from '@/constants'
import { api } from '@/lib/api/api-client' import { api } from '@/lib/api/api-client'
import { ApiError } from '@/lib/api/api-error' import { ApiError } from '@/lib/api/api-error'
import type { AuthSessionInput } from '@/store/auth' import type { AuthSessionInput } from '@/store/auth'
@@ -34,7 +39,7 @@ function unwrapEnvelope<T>(
response: ApiResponse<T>, response: ApiResponse<T>,
fallbackErrorKey = 'auth.errors.requestFailed', fallbackErrorKey = 'auth.errors.requestFailed',
) { ) {
if (response.code === 1) { if (response.code === API_SUCCESS_CODE) {
return response.data return response.data
} }
@@ -172,7 +177,7 @@ export async function sendSmsCode(
AUTH_ENDPOINTS.sendSmsCode, AUTH_ENDPOINTS.sendSmsCode,
{ {
json: { json: {
event: 'user_register', event: SMS_SEND_EVENT_REGISTER,
mobile: payload.mobile, mobile: payload.mobile,
}, },
}, },

View File

@@ -1,3 +1,4 @@
import type { SMS_SEND_EVENT_REGISTER } from '@/constants'
import type { AuthSessionInput, AuthUser } from '@/store/auth' import type { AuthSessionInput, AuthUser } from '@/store/auth'
export interface AuthApiEnvelope<T> { export interface AuthApiEnvelope<T> {
@@ -70,7 +71,7 @@ export interface RegisterRequestDto {
} }
export interface SendSmsCodeRequestDto { export interface SendSmsCodeRequestDto {
event: 'user_register' event: typeof SMS_SEND_EVENT_REGISTER
mobile: string mobile: string
} }

View File

@@ -1,5 +1,9 @@
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import {
DEFAULT_REGISTER_INVITE_CODE,
REGISTER_INVITE_CODE_QUERY_PARAM,
} from '@/constants'
import i18n from '@/i18n' import i18n from '@/i18n'
import { notify } from '@/lib/notify' import { notify } from '@/lib/notify'
import { useAuthStore } from '@/store/auth' import { useAuthStore } from '@/store/auth'
@@ -15,9 +19,6 @@ interface UseRegisterFormOptions {
onSuccess?: () => void onSuccess?: () => void
} }
const REGISTER_INVITE_CODE_QUERY_PARAM = 'registerInviteCode'
const DEFAULT_REGISTER_INVITE_CODE = 'D97DBC16'
function getInitialRegisterInviteCode() { function getInitialRegisterInviteCode() {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return DEFAULT_REGISTER_INVITE_CODE return DEFAULT_REGISTER_INVITE_CODE

View File

@@ -1,12 +1,11 @@
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { SMS_CODE_COOLDOWN_FALLBACK_SECONDS } from '@/constants'
import i18n from '@/i18n' import i18n from '@/i18n'
import { notify } from '@/lib/notify' import { notify } from '@/lib/notify'
import { sendSmsCode } from '../api/auth-api' import { sendSmsCode } from '../api/auth-api'
import { toAuthSubmitErrorKey } from './auth-error-key' import { toAuthSubmitErrorKey } from './auth-error-key'
const FALLBACK_SMS_CODE_COOLDOWN_SECONDS = 60
export function useSendSmsCode() { export function useSendSmsCode() {
const [remainingSeconds, setRemainingSeconds] = useState(0) const [remainingSeconds, setRemainingSeconds] = useState(0)
const mutation = useMutation({ const mutation = useMutation({
@@ -24,7 +23,7 @@ export function useSendSmsCode() {
setRemainingSeconds( setRemainingSeconds(
result.expiresIn > 0 result.expiresIn > 0
? result.expiresIn ? result.expiresIn
: FALLBACK_SMS_CODE_COOLDOWN_SECONDS, : SMS_CODE_COOLDOWN_FALLBACK_SECONDS,
) )
notify.success(i18n.t('auth.register.sms.success')) notify.success(i18n.t('auth.register.sms.success'))
}, },

View File

@@ -1,5 +1,11 @@
import { z } from 'zod' import { z } from 'zod'
import {
INVITE_CODE_MAX_LENGTH,
PASSWORD_MAX_LENGTH,
PASSWORD_MIN_LENGTH,
} from '@/constants'
const usernameSchema = z const usernameSchema = z
.string() .string()
.trim() .trim()
@@ -12,8 +18,8 @@ const captchaSchema = z
const passwordSchema = z const passwordSchema = z
.string() .string()
.min(6, 'auth.validation.password.min') .min(PASSWORD_MIN_LENGTH, 'auth.validation.password.min')
.max(32, 'auth.validation.password.max') .max(PASSWORD_MAX_LENGTH, 'auth.validation.password.max')
export const loginFormSchema = z.object({ export const loginFormSchema = z.object({
password: passwordSchema, password: passwordSchema,
@@ -28,7 +34,7 @@ export const registerFormSchema = z
.string() .string()
.trim() .trim()
.min(1, 'auth.validation.inviteCode.required') .min(1, 'auth.validation.inviteCode.required')
.max(32, 'auth.validation.inviteCode.max'), .max(INVITE_CODE_MAX_LENGTH, 'auth.validation.inviteCode.max'),
password: passwordSchema, password: passwordSchema,
mobile: usernameSchema, mobile: usernameSchema,
}) })

View File

@@ -1,4 +1,8 @@
import { FINANCE_API_ENDPOINTS } from '@/constants' import {
API_SUCCESS_CODE,
DEFAULT_LIST_PAGE_SIZE,
FINANCE_API_ENDPOINTS,
} from '@/constants'
import { api } from '@/lib/api/api-client' import { api } from '@/lib/api/api-client'
import { ApiError } from '@/lib/api/api-error' import { ApiError } from '@/lib/api/api-error'
import type { ApiResponse } from '@/type' import type { ApiResponse } from '@/type'
@@ -30,7 +34,7 @@ function unwrapFinanceEnvelope<T>(
response: ApiResponse<T>, response: ApiResponse<T>,
fallbackMessage = 'Finance request failed', fallbackMessage = 'Finance request failed',
) { ) {
if (response.code === 1) { if (response.code === API_SUCCESS_CODE) {
return response.data return response.data
} }
@@ -194,7 +198,7 @@ function normalizeFinanceOrderList(dto: FinanceOrderListDto): FinanceOrderList {
list: (dto.list ?? []).map(normalizeFinanceOrderItem), list: (dto.list ?? []).map(normalizeFinanceOrderItem),
pagination: { pagination: {
page: dto.pagination?.page ?? 1, page: dto.pagination?.page ?? 1,
page_size: dto.pagination?.page_size ?? 20, page_size: dto.pagination?.page_size ?? DEFAULT_LIST_PAGE_SIZE,
total: dto.pagination?.total ?? 0, total: dto.pagination?.total ?? 0,
}, },
} }
@@ -237,7 +241,8 @@ function normalizeWalletRecordList(dto: WalletRecordListDto): WalletRecordList {
list: (dto.list ?? []).map(normalizeWalletRecordItem), list: (dto.list ?? []).map(normalizeWalletRecordItem),
pagination: { pagination: {
page: dto.pagination?.page ?? dto.page ?? 1, page: dto.pagination?.page ?? dto.page ?? 1,
page_size: dto.pagination?.page_size ?? dto.page_size ?? 20, page_size:
dto.pagination?.page_size ?? dto.page_size ?? DEFAULT_LIST_PAGE_SIZE,
total: dto.pagination?.total ?? dto.total ?? 0, total: dto.pagination?.total ?? dto.total ?? 0,
}, },
} }
@@ -301,7 +306,7 @@ export async function getDepositOrderList(params?: {
{ {
searchParams: { searchParams: {
page: String(params?.page ?? 1), page: String(params?.page ?? 1),
page_size: String(params?.pageSize ?? 20), page_size: String(params?.pageSize ?? DEFAULT_LIST_PAGE_SIZE),
}, },
}, },
) )
@@ -322,7 +327,7 @@ export async function getWithdrawOrderList(params?: {
{ {
searchParams: { searchParams: {
page: String(params?.page ?? 1), page: String(params?.page ?? 1),
page_size: String(params?.pageSize ?? 20), page_size: String(params?.pageSize ?? DEFAULT_LIST_PAGE_SIZE),
}, },
}, },
) )
@@ -344,7 +349,7 @@ export async function getWalletRecordList(params?: {
{ {
searchParams: { searchParams: {
page: String(params?.page ?? 1), page: String(params?.page ?? 1),
page_size: String(params?.pageSize ?? 20), page_size: String(params?.pageSize ?? DEFAULT_LIST_PAGE_SIZE),
type: params?.type ?? 'payout', type: params?.type ?? 'payout',
}, },
}, },

View File

@@ -1,4 +1,8 @@
import { GAME_API_ENDPOINTS } from '@/constants' import {
API_SUCCESS_CODE,
DEFAULT_LIST_PAGE_SIZE,
GAME_API_ENDPOINTS,
} from '@/constants'
import { api } from '@/lib/api/api-client' import { api } from '@/lib/api/api-client'
import { ApiError } from '@/lib/api/api-error' import { ApiError } from '@/lib/api/api-error'
import type { ApiResponse } from '@/type' import type { ApiResponse } from '@/type'
@@ -51,7 +55,7 @@ function unwrapGameEnvelope<T>(
response: ApiResponse<T>, response: ApiResponse<T>,
fallbackMessage = 'Game request failed', fallbackMessage = 'Game request failed',
) { ) {
if (response.code === 1) { if (response.code === API_SUCCESS_CODE) {
return response.data return response.data
} }
@@ -424,7 +428,7 @@ export async function getNoticeList(params?: {
const response = await api.get<NoticeListDto>(GAME_API_ENDPOINTS.noticeList, { const response = await api.get<NoticeListDto>(GAME_API_ENDPOINTS.noticeList, {
searchParams: { searchParams: {
page: String(params?.page ?? 1), page: String(params?.page ?? 1),
page_size: String(params?.pageSize ?? 20), page_size: String(params?.pageSize ?? DEFAULT_LIST_PAGE_SIZE),
}, },
}) })
const dto = unwrapGameEnvelope( const dto = unwrapGameEnvelope(
@@ -478,7 +482,7 @@ export async function getGameBetMyOrders(params: {
{ {
json: { json: {
page: params.page ?? 1, page: params.page ?? 1,
page_size: params.pageSize ?? 20, page_size: params.pageSize ?? DEFAULT_LIST_PAGE_SIZE,
}, },
}, },
) )

View File

@@ -1,4 +1,4 @@
import { GAME_API_ENDPOINTS } from '@/constants' import { API_SUCCESS_CODE, GAME_API_ENDPOINTS } from '@/constants'
import { api } from '@/lib/api/api-client' import { api } from '@/lib/api/api-client'
import { ApiError } from '@/lib/api/api-error' import { ApiError } from '@/lib/api/api-error'
import type { ApiResponse } from '@/type' import type { ApiResponse } from '@/type'
@@ -16,7 +16,7 @@ interface GamePeriodHistoryDto {
function unwrapPeriodHistoryEnvelope( function unwrapPeriodHistoryEnvelope(
response: ApiResponse<GamePeriodHistoryDto>, response: ApiResponse<GamePeriodHistoryDto>,
) { ) {
if (response.code === 1) { if (response.code === API_SUCCESS_CODE) {
return response.data return response.data
} }

View File

@@ -1,6 +1,7 @@
import { startTransition, useEffect, useState } from 'react' import { startTransition, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { MOBILE_LAYOUT_BREAKPOINT_PX } from '@/constants'
import { getGameLobbyInit } from '@/features/game' import { getGameLobbyInit } from '@/features/game'
import { EntryNoticeGateModal } from '@/features/game/components' import { EntryNoticeGateModal } from '@/features/game/components'
import { MobileEntry } from '@/features/game/entry/mobile-entry.tsx' import { MobileEntry } from '@/features/game/entry/mobile-entry.tsx'
@@ -74,7 +75,8 @@ export function EntryPage() {
return false return false
} }
return window.matchMedia('(max-width: 768px)').matches return window.matchMedia(`(max-width: ${MOBILE_LAYOUT_BREAKPOINT_PX}px)`)
.matches
}) })
useDocumentMetadata({ useDocumentMetadata({
@@ -189,7 +191,9 @@ export function EntryPage() {
return return
} }
const mediaQuery = window.matchMedia('(max-width: 768px)') const mediaQuery = window.matchMedia(
`(max-width: ${MOBILE_LAYOUT_BREAKPOINT_PX}px)`,
)
const syncLayout = (event?: MediaQueryListEvent) => { const syncLayout = (event?: MediaQueryListEvent) => {
setIsMobile(event?.matches ?? mediaQuery.matches) setIsMobile(event?.matches ?? mediaQuery.matches)
} }

View File

@@ -1,7 +1,10 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DEFAULT_APP_LANGUAGE } from '@/constants' import {
DEFAULT_APP_LANGUAGE,
FINANCE_CONFIG_QUERY_STALE_TIME_MS,
} from '@/constants'
import { getDepositTierList } from '@/features/game/api' import { getDepositTierList } from '@/features/game/api'
export function useDepositTierList() { export function useDepositTierList() {
@@ -12,6 +15,6 @@ export function useDepositTierList() {
return useQuery({ return useQuery({
queryKey: ['finance', 'deposit-tier-list', language], queryKey: ['finance', 'deposit-tier-list', language],
queryFn: () => getDepositTierList(), queryFn: () => getDepositTierList(),
staleTime: 5 * 60 * 1000, staleTime: FINANCE_CONFIG_QUERY_STALE_TIME_MS,
}) })
} }

View File

@@ -1,7 +1,10 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DEFAULT_APP_LANGUAGE } from '@/constants' import {
DEFAULT_APP_LANGUAGE,
FINANCE_CONFIG_QUERY_STALE_TIME_MS,
} from '@/constants'
import { getDepositWithdrawConfig } from '@/features/game/api' import { getDepositWithdrawConfig } from '@/features/game/api'
export function useDepositWithdrawConfig() { export function useDepositWithdrawConfig() {
@@ -12,6 +15,6 @@ export function useDepositWithdrawConfig() {
return useQuery({ return useQuery({
queryKey: ['finance', 'deposit-withdraw-config', language], queryKey: ['finance', 'deposit-withdraw-config', language],
queryFn: () => getDepositWithdrawConfig(), queryFn: () => getDepositWithdrawConfig(),
staleTime: 5 * 60 * 1000, staleTime: FINANCE_CONFIG_QUERY_STALE_TIME_MS,
}) })
} }

View File

@@ -2,12 +2,11 @@ import { useInfiniteQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DEFAULT_LIST_PAGE_SIZE } from '@/constants'
import { getDepositOrderList, getWithdrawOrderList } from '@/features/game/api' import { getDepositOrderList, getWithdrawOrderList } from '@/features/game/api'
export type FinanceRecordType = 'deposit' | 'withdraw' export type FinanceRecordType = 'deposit' | 'withdraw'
const FINANCE_RECORD_PAGE_SIZE = 20
const FINANCE_RECORD_TYPE_OPTIONS: Array<{ const FINANCE_RECORD_TYPE_OPTIONS: Array<{
key: FinanceRecordType key: FinanceRecordType
labelKey: string labelKey: string
@@ -46,11 +45,11 @@ export function useFinanceRecordsVm({ enabled }: { enabled: boolean }) {
recordType === 'deposit' recordType === 'deposit'
? getDepositOrderList({ ? getDepositOrderList({
page: pageParam, page: pageParam,
pageSize: FINANCE_RECORD_PAGE_SIZE, pageSize: DEFAULT_LIST_PAGE_SIZE,
}) })
: getWithdrawOrderList({ : getWithdrawOrderList({
page: pageParam, page: pageParam,
pageSize: FINANCE_RECORD_PAGE_SIZE, pageSize: DEFAULT_LIST_PAGE_SIZE,
}), }),
enabled, enabled,
getNextPageParam: (lastPage) => { getNextPageParam: (lastPage) => {

View File

@@ -1,4 +1,9 @@
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import {
CONNECTION_LATENCY_FAIR_MS,
CONNECTION_LATENCY_GOOD_MS,
CONNECTION_LATENCY_POOR_MS,
} from '@/constants'
import { useAppLanguage } from '@/features/game/hooks/use-app-language' import { useAppLanguage } from '@/features/game/hooks/use-app-language'
import { import {
isDesktopFullscreen, isDesktopFullscreen,
@@ -74,7 +79,7 @@ function resolveSignalPresentation(input: {
} satisfies SignalPresentation } satisfies SignalPresentation
} }
if (input.latencyMs <= 80) { if (input.latencyMs <= CONNECTION_LATENCY_GOOD_MS) {
return { return {
activeBars: 4, activeBars: 4,
latencyLabel: String(input.latencyMs), latencyLabel: String(input.latencyMs),
@@ -82,7 +87,7 @@ function resolveSignalPresentation(input: {
} satisfies SignalPresentation } satisfies SignalPresentation
} }
if (input.latencyMs <= 150) { if (input.latencyMs <= CONNECTION_LATENCY_FAIR_MS) {
return { return {
activeBars: 3, activeBars: 3,
latencyLabel: String(input.latencyMs), latencyLabel: String(input.latencyMs),
@@ -90,7 +95,7 @@ function resolveSignalPresentation(input: {
} satisfies SignalPresentation } satisfies SignalPresentation
} }
if (input.latencyMs <= 300) { if (input.latencyMs <= CONNECTION_LATENCY_POOR_MS) {
return { return {
activeBars: 2, activeBars: 2,
latencyLabel: String(input.latencyMs), latencyLabel: String(input.latencyMs),

View File

@@ -3,9 +3,9 @@ import dayjs from 'dayjs'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DEFAULT_LIST_PAGE_SIZE } from '@/constants'
import { getWalletRecordList } from '@/features/game/api' import { getWalletRecordList } from '@/features/game/api'
const WALLET_RECORD_PAGE_SIZE = 20
const WALLET_RECORD_TYPE = 'payout' const WALLET_RECORD_TYPE = 'payout'
function formatWalletAmount(value: string, locale: string) { function formatWalletAmount(value: string, locale: string) {
@@ -47,7 +47,7 @@ export function useWalletRecordsVm({ enabled }: { enabled: boolean }) {
queryFn: ({ pageParam }) => queryFn: ({ pageParam }) =>
getWalletRecordList({ getWalletRecordList({
page: pageParam, page: pageParam,
pageSize: WALLET_RECORD_PAGE_SIZE, pageSize: DEFAULT_LIST_PAGE_SIZE,
type: WALLET_RECORD_TYPE, type: WALLET_RECORD_TYPE,
}), }),
enabled, enabled,

View File

@@ -1,7 +1,13 @@
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DEFAULT_WITHDRAW_CONFIG, QUICK_FIAT_AMOUNTS } from '@/constants' import {
DEFAULT_CURRENCY_CODE,
DEFAULT_WITHDRAW_CONFIG,
QUICK_FIAT_AMOUNTS,
WITHDRAW_EMAIL_PATTERN,
WITHDRAW_PHONE_PATTERN,
} from '@/constants'
import type { DepositWithdrawConfig } from '@/features/game/api' import type { DepositWithdrawConfig } from '@/features/game/api'
import { useDepositWithdrawConfig } from '@/features/game/hooks/use-deposit-withdraw-config' import { useDepositWithdrawConfig } from '@/features/game/hooks/use-deposit-withdraw-config'
import { useAuthStore } from '@/store' import { useAuthStore } from '@/store'
@@ -47,7 +53,7 @@ function isValidEmail(value: string) {
return false return false
} }
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()) return WITHDRAW_EMAIL_PATTERN.test(value.trim())
} }
function isValidPhone(value: string) { function isValidPhone(value: string) {
@@ -57,7 +63,7 @@ function isValidPhone(value: string) {
return false return false
} }
return /^\+?\d{6,20}$/.test(normalized) return WITHDRAW_PHONE_PATTERN.test(normalized)
} }
export function useWithdrawVm() { export function useWithdrawVm() {
@@ -88,7 +94,7 @@ export function useWithdrawVm() {
const [amount, setAmountState] = useState(0) const [amount, setAmountState] = useState(0)
const [hasInitializedAmount, setHasInitializedAmount] = useState(false) const [hasInitializedAmount, setHasInitializedAmount] = useState(false)
const [currencyCode, setCurrencyCode] = useState( const [currencyCode, setCurrencyCode] = useState(
config.currencies[0]?.code ?? 'MYR', config.currencies[0]?.code ?? DEFAULT_CURRENCY_CODE,
) )
const [paymentChannelCode, setPaymentChannelCode] = useState('') const [paymentChannelCode, setPaymentChannelCode] = useState('')
const [bankCode, setBankCode] = useState('') const [bankCode, setBankCode] = useState('')
@@ -207,7 +213,7 @@ export function useWithdrawVm() {
}, [locale, maxWithdrawAmount, selectedCurrency.code, selectedRate]) }, [locale, maxWithdrawAmount, selectedCurrency.code, selectedRate])
const resetForm = useCallback(() => { const resetForm = useCallback(() => {
const nextCurrencyCode = config.currencies[0]?.code ?? 'MYR' const nextCurrencyCode = config.currencies[0]?.code ?? DEFAULT_CURRENCY_CODE
const nextCurrency = getActiveCurrencyCode( const nextCurrency = getActiveCurrencyCode(
config.currencies, config.currencies,
nextCurrencyCode, nextCurrencyCode,

View File

@@ -5,6 +5,7 @@ import { CenterModal } from '@/components/center-modal.tsx'
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 { Switch } from '@/components/ui/switch.tsx' import { Switch } from '@/components/ui/switch.tsx'
import { AUTO_HOSTING_DEFAULT_SINGLE_WIN_THRESHOLD } from '@/constants'
import { notify } from '@/lib/notify' import { notify } from '@/lib/notify'
import { useModalStore } from '@/store' import { useModalStore } from '@/store'
import { useAuthStore } from '@/store/auth' import { useAuthStore } from '@/store/auth'
@@ -51,7 +52,9 @@ function DesktopAutoSettingModal() {
const [balanceLimitEnabled, setBalanceLimitEnabled] = useState(false) const [balanceLimitEnabled, setBalanceLimitEnabled] = useState(false)
const [balanceLimitValue, setBalanceLimitValue] = useState('0') const [balanceLimitValue, setBalanceLimitValue] = useState('0')
const [singleWinLimitEnabled, setSingleWinLimitEnabled] = useState(false) const [singleWinLimitEnabled, setSingleWinLimitEnabled] = useState(false)
const [singleWinLimitValue, setSingleWinLimitValue] = useState('50000') const [singleWinLimitValue, setSingleWinLimitValue] = useState(
String(AUTO_HOSTING_DEFAULT_SINGLE_WIN_THRESHOLD),
)
const [jackpotStopEnabled, setJackpotStopEnabled] = useState(false) const [jackpotStopEnabled, setJackpotStopEnabled] = useState(false)
function handleClose() { function handleClose() {

View File

@@ -15,6 +15,7 @@ import userInfoBg from '@/assets/system/userInfo-bg.webp'
import { CenterModal } from '@/components/center-modal.tsx' import { CenterModal } from '@/components/center-modal.tsx'
import { SmartBackground } from '@/components/smart-background.tsx' import { SmartBackground } from '@/components/smart-background.tsx'
import { SmartImage } from '@/components/smart-image.tsx' import { SmartImage } from '@/components/smart-image.tsx'
import { REGISTER_INVITE_CODE_QUERY_PARAM } from '@/constants'
import { logoutWithPassword } from '@/features/auth/api/auth-api' import { logoutWithPassword } from '@/features/auth/api/auth-api'
import DesktopFinanceRecordsTab from '@/features/game/modal/desktop/desktop-finance-records-tab' import DesktopFinanceRecordsTab from '@/features/game/modal/desktop/desktop-finance-records-tab'
import DesktopWalletRecordsTab from '@/features/game/modal/desktop/desktop-wallet-records-tab' import DesktopWalletRecordsTab from '@/features/game/modal/desktop/desktop-wallet-records-tab'
@@ -47,8 +48,6 @@ const USER_INFO_TABS: Array<{
}, },
] ]
const REGISTER_INVITE_CODE_QUERY_PARAM = 'registerInviteCode'
function createRegisterInviteUrl(inviteCode: string) { function createRegisterInviteUrl(inviteCode: string) {
const url = new URL(window.location.href) const url = new URL(window.location.href)

View File

@@ -2,14 +2,19 @@ import ky, { HTTPError, type Options, TimeoutError } from 'ky'
import { import {
ACCESS_TOKEN_REFRESH_SKEW_MS, ACCESS_TOKEN_REFRESH_SKEW_MS,
API_ERROR_MESSAGES, API_ERROR_MESSAGES,
API_SUCCESS_CODE,
AUTH_REFRESH_ATTEMPTED_CONTEXT_KEY, AUTH_REFRESH_ATTEMPTED_CONTEXT_KEY,
AUTH_REFRESH_ENDPOINT, AUTH_REFRESH_ENDPOINT,
AUTH_RELOGIN_REQUIRED_CODES, AUTH_RELOGIN_REQUIRED_CODES,
AUTH_SKIP_REFRESH_CONTEXT_KEY, AUTH_SKIP_REFRESH_CONTEXT_KEY,
AUTH_TOKEN_CACHE_SKEW_MS, AUTH_TOKEN_CACHE_SKEW_MS,
AUTH_TOKEN_ENDPOINT, AUTH_TOKEN_ENDPOINT,
AUTHORIZATION_BEARER_PREFIX,
CONTENT_TYPE_JSON,
DEFAULT_REQUEST_ACCEPT_HEADER, DEFAULT_REQUEST_ACCEPT_HEADER,
DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS,
HTTP_STATUS,
REQUEST_HEADERS,
} from '@/constants' } from '@/constants'
import type { AuthTokenDto } from '@/features/auth/api/types' import type { AuthTokenDto } from '@/features/auth/api/types'
import { getPreferredLanguage, isSupportedLanguage } from '@/i18n' import { getPreferredLanguage, isSupportedLanguage } from '@/i18n'
@@ -65,13 +70,13 @@ function normalizeApiBaseUrl(baseUrl: string | undefined) {
} }
async function parseResponseBody(response: Response) { async function parseResponseBody(response: Response) {
if (response.status === 204) { if (response.status === HTTP_STATUS.noContent) {
return null return null
} }
const contentType = response.headers.get('content-type') ?? '' const contentType = response.headers.get('content-type') ?? ''
if (contentType.includes('application/json')) { if (contentType.includes(CONTENT_TYPE_JSON)) {
return response.json() return response.json()
} }
@@ -110,7 +115,7 @@ async function toApiError(error: unknown) {
if (error instanceof TimeoutError) { if (error instanceof TimeoutError) {
return new ApiError({ return new ApiError({
message: API_ERROR_MESSAGES.timeout, message: API_ERROR_MESSAGES.timeout,
status: 408, status: HTTP_STATUS.requestTimeout,
}) })
} }
@@ -143,14 +148,20 @@ const apiClient = ky.create({
hooks: { hooks: {
beforeRequest: [ beforeRequest: [
({ request }) => { ({ request }) => {
request.headers.set('Accept', DEFAULT_REQUEST_ACCEPT_HEADER) request.headers.set(
request.headers.set('lang', getRequestLanguage()) REQUEST_HEADERS.accept,
DEFAULT_REQUEST_ACCEPT_HEADER,
)
request.headers.set(REQUEST_HEADERS.lang, getRequestLanguage())
const token = useAuthStore.getState().accessToken const token = useAuthStore.getState().accessToken
if (token) { if (token) {
request.headers.set('Authorization', `Bearer ${token}`) request.headers.set(
request.headers.set('user-token', token) REQUEST_HEADERS.authorization,
`${AUTHORIZATION_BEARER_PREFIX}${token}`,
)
request.headers.set(REQUEST_HEADERS.userToken, token)
} }
if (shouldLogRequests) { if (shouldLogRequests) {
@@ -222,14 +233,14 @@ function assertValidAuthEnvelope(data: unknown) {
throw new ApiError({ throw new ApiError({
data, data,
message: getApiEnvelopeMessage(data), message: getApiEnvelopeMessage(data),
status: 401, status: HTTP_STATUS.unauthorized,
}) })
} }
function unwrapEnvelopeData<T>(response: ApiResponse<T>) { function unwrapEnvelopeData<T>(response: ApiResponse<T>) {
assertValidAuthEnvelope(response) assertValidAuthEnvelope(response)
if (response.code === 1) { if (response.code === API_SUCCESS_CODE) {
return response.data return response.data
} }
@@ -333,8 +344,8 @@ function createHeaders(headersInit?: Options['headers']) {
async function buildRequestOptions(input: string, options?: Options) { async function buildRequestOptions(input: string, options?: Options) {
const headers = createHeaders(options?.headers) const headers = createHeaders(options?.headers)
if (shouldAttachAuthToken(input) && !headers.has('auth-token')) { if (shouldAttachAuthToken(input) && !headers.has(REQUEST_HEADERS.authToken)) {
headers.set('auth-token', await fetchAuthToken()) headers.set(REQUEST_HEADERS.authToken, await fetchAuthToken())
} }
return { return {
@@ -361,7 +372,7 @@ async function request<TResponse>(input: string, options?: Options) {
} catch (error) { } catch (error) {
if ( if (
error instanceof HTTPError && error instanceof HTTPError &&
error.response.status === 401 && error.response.status === HTTP_STATUS.unauthorized &&
input !== AUTH_REFRESH_ENDPOINT && input !== AUTH_REFRESH_ENDPOINT &&
options?.context?.[AUTH_SKIP_REFRESH_CONTEXT_KEY] !== true && options?.context?.[AUTH_SKIP_REFRESH_CONTEXT_KEY] !== true &&
options?.context?.[AUTH_REFRESH_ATTEMPTED_CONTEXT_KEY] !== true options?.context?.[AUTH_REFRESH_ATTEMPTED_CONTEXT_KEY] !== true
@@ -379,7 +390,10 @@ async function request<TResponse>(input: string, options?: Options) {
} }
} }
if (error instanceof HTTPError && error.response.status === 401) { if (
error instanceof HTTPError &&
error.response.status === HTTP_STATUS.unauthorized
) {
handleUnauthorizedSession() handleUnauthorizedSession()
} }

View File

@@ -1,3 +1,4 @@
import { LOGIN_PROMPT_DEDUP_MS } from '@/constants'
import i18n from '@/i18n' import i18n from '@/i18n'
import { notify } from '@/lib/notify' import { notify } from '@/lib/notify'
import { queryClient } from '@/lib/query/query-client' import { queryClient } from '@/lib/query/query-client'
@@ -16,8 +17,6 @@ let authInitializationPromise: Promise<void> | null = null
let refreshSessionPromise: Promise<boolean> | null = null let refreshSessionPromise: Promise<boolean> | null = null
let lastLoginPromptAt = 0 let lastLoginPromptAt = 0
const LOGIN_PROMPT_DEDUP_MS = 1200
interface ClearAuthenticatedSessionOptions { interface ClearAuthenticatedSessionOptions {
clearBrowserStorage?: boolean clearBrowserStorage?: boolean
clearQueryCache?: boolean clearQueryCache?: boolean

View File

@@ -1,3 +1,76 @@
/* 以下为多语言中重复出现的文案,统一声明一次后在下方各 key 复用,避免同一文案多处声明。 */
/** @description 登录的统一文案。 */
const TEXT_LOGIN = 'Login'
/** @description 注册的统一文案。 */
const TEXT_REGISTER = 'Register'
/** @description “是”的统一文案。 */
const TEXT_YES = 'Yes'
/** @description “否”的统一文案。 */
const TEXT_NO = 'No'
/** @description “查看”的统一文案。 */
const TEXT_VIEW = 'View'
/** @description 自动托管的统一文案。 */
const TEXT_AUTO_HOSTING = 'Auto Spin'
/** @description 钱包流水的统一文案。 */
const TEXT_WALLET_RECORDS = 'Wallet Records'
/** @description 站内消息的统一文案。 */
const TEXT_SITE_MESSAGES = 'Messages'
/** @description 分页“第x页/共x条”的统一文案。 */
const TEXT_PAGE_INDICATOR = 'Page {{page}} / {{total}} total'
/** @description 上一页的统一文案。 */
const TEXT_PREV_PAGE = 'Previous'
/** @description 下一页的统一文案。 */
const TEXT_NEXT_PAGE = 'Next'
/** @description “时间”的统一文案。 */
const TEXT_TIME = 'Time'
/** @description 超过单次投注限额的统一文案。 */
const TEXT_BET_LIMIT_EXCEEDED = 'Single bet limit exceeded'
/** @description “提交中...”的统一文案。 */
const TEXT_SUBMITTING = 'Submitting...'
/** @description 手机号字段标签的统一文案。 */
const TEXT_MOBILE_LABEL = 'Mobile:'
/** @description 手机号输入占位的统一文案。 */
const TEXT_MOBILE_PLACEHOLDER = 'Enter mobile number'
/** @description 密码字段标签的统一文案。 */
const TEXT_PASSWORD_LABEL = 'Password:'
/** @description 密码输入占位的统一文案。 */
const TEXT_PASSWORD_PLACEHOLDER = 'Enter password'
/** @description “确认”按钮的统一文案。 */
const TEXT_CONFIRM = 'Confirm'
/** @description “加载中...”的统一文案。 */
const TEXT_LOADING = 'Loading...'
/** @description 货币类型的统一文案。 */
const TEXT_CURRENCY_TYPE = 'Currency Type'
/** @description 支付渠道的统一文案。 */
const TEXT_PAYMENT_CHANNEL = 'Payment Channel'
/** @description “已封盘”的统一文案。 */
const TEXT_LOCKED = 'Locked'
/** @description “已结算”的统一文案。 */
const TEXT_SETTLED = 'Settled'
export default { export default {
nav: { nav: {
home: 'Home', home: 'Home',
@@ -88,8 +161,8 @@ export default {
}, },
phases: { phases: {
betting: 'Betting', betting: 'Betting',
locked: 'Locked', locked: TEXT_LOCKED,
settled: 'Settled', settled: TEXT_SETTLED,
}, },
roundBettingStart: { roundBettingStart: {
title: 'Round {{roundId}}', title: 'Round {{roundId}}',
@@ -99,8 +172,8 @@ export default {
unifiedBetHint: 'Unified bet', unifiedBetHint: 'Unified bet',
totalBet: 'Total bet', totalBet: 'Total bet',
canBet: 'Can bet', canBet: 'Can bet',
yes: 'Yes', yes: TEXT_YES,
no: 'No', no: TEXT_NO,
quickBet: 'Quick bet 08', quickBet: 'Quick bet 08',
clearPending: 'Clear pending', clearPending: 'Clear pending',
autoModeDemo: 'Auto mode demo', autoModeDemo: 'Auto mode demo',
@@ -108,16 +181,16 @@ export default {
}, },
modals: { modals: {
login: { login: {
title: 'Login', title: TEXT_LOGIN,
}, },
register: { register: {
title: 'Register', title: TEXT_REGISTER,
}, },
notice: { notice: {
title: 'Event Notice', title: 'Event Notice',
content: content:
'This area will later load the real event announcement body, rich media, and a longer scrollable message. The current version focuses on shared multilingual modal wiring.', 'This area will later load the real event announcement body, rich media, and a longer scrollable message. The current version focuses on shared multilingual modal wiring.',
check: 'View', check: TEXT_VIEW,
}, },
entryNotice: { entryNotice: {
title: 'Site Notice', title: 'Site Notice',
@@ -142,7 +215,7 @@ export default {
topup: 'Top Up', topup: 'Top Up',
}, },
autoSetting: { autoSetting: {
title: 'Auto Spin', title: TEXT_AUTO_HOSTING,
startAutoSpin: 'Start Auto Spin', startAutoSpin: 'Start Auto Spin',
rows: { rows: {
stopIfBalanceLowerThan: 'Stop if balance is lower than', stopIfBalanceLowerThan: 'Stop if balance is lower than',
@@ -155,8 +228,8 @@ export default {
tabs: { tabs: {
profile: 'Profile', profile: 'Profile',
financeRecords: 'Top Up / Withdraw Records', financeRecords: 'Top Up / Withdraw Records',
walletRecords: 'Wallet Records', walletRecords: TEXT_WALLET_RECORDS,
message: 'Messages', message: TEXT_SITE_MESSAGES,
}, },
profile: { profile: {
name: 'Name', name: 'Name',
@@ -169,7 +242,7 @@ export default {
'My signature is as unique as my personality. This area will later display the real profile summary.', 'My signature is as unique as my personality. This area will later display the real profile summary.',
}, },
message: { message: {
title: 'Messages', title: TEXT_SITE_MESSAGES,
back: 'Back', back: 'Back',
loading: 'Loading messages...', loading: 'Loading messages...',
loadFailed: 'Failed to load messages. Please try again later.', loadFailed: 'Failed to load messages. Please try again later.',
@@ -178,7 +251,7 @@ export default {
unread: 'Unread', unread: 'Unread',
eventBonus: eventBonus:
'[Top-up Bonus Event] From October 1 to October 7, 2026, claim your rebate rewards...', '[Top-up Bonus Event] From October 1 to October 7, 2026, claim your rebate rewards...',
check: 'View', check: TEXT_VIEW,
deleteRecords: 'Delete records', deleteRecords: 'Delete records',
}, },
financeRecords: { financeRecords: {
@@ -190,9 +263,9 @@ export default {
loading: 'Loading records...', loading: 'Loading records...',
loadFailed: 'Failed to load records. Please try again later.', loadFailed: 'Failed to load records. Please try again later.',
empty: 'No records yet', empty: 'No records yet',
page: 'Page {{page}} / {{total}} total', page: TEXT_PAGE_INDICATOR,
previous: 'Previous', previous: TEXT_PREV_PAGE,
next: 'Next', next: TEXT_NEXT_PAGE,
}, },
walletRecords: { walletRecords: {
amount: 'Amount', amount: 'Amount',
@@ -201,12 +274,12 @@ export default {
empty: 'No wallet records yet', empty: 'No wallet records yet',
loadFailed: 'Failed to load wallet records. Please try again later.', loadFailed: 'Failed to load wallet records. Please try again later.',
loading: 'Loading wallet records...', loading: 'Loading wallet records...',
next: 'Next', next: TEXT_NEXT_PAGE,
page: 'Page {{page}} / {{total}} total', page: TEXT_PAGE_INDICATOR,
previous: 'Previous', previous: TEXT_PREV_PAGE,
remark: 'Remark', remark: 'Remark',
time: 'Time', time: TEXT_TIME,
type: 'Wallet Records', type: TEXT_WALLET_RECORDS,
}, },
}, },
withdrawTopup: { withdrawTopup: {
@@ -238,8 +311,8 @@ export default {
dialog: { dialog: {
close: 'Close alert', close: 'Close alert',
confirm: 'OK', confirm: 'OK',
no: 'No', no: TEXT_NO,
yes: 'Yes', yes: TEXT_YES,
}, },
modal: { modal: {
close: 'Close modal', close: 'Close modal',
@@ -256,7 +329,7 @@ export default {
inviteLinkCopyFailed: inviteLinkCopyFailed:
'Failed to copy invite link. Please copy it manually.', 'Failed to copy invite link. Please copy it manually.',
insufficientBalance: 'Insufficient balance. Please adjust your bet.', insufficientBalance: 'Insufficient balance. Please adjust your bet.',
betLimitExceeded: 'Single bet limit exceeded', betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
betUnavailable: 'Betting is not available for this round', betUnavailable: 'Betting is not available for this round',
betPlaced: 'Bet placed successfully', betPlaced: 'Bet placed successfully',
noRecentSuccessfulBet: noRecentSuccessfulBet:
@@ -282,7 +355,7 @@ export default {
common: { common: {
arrowIconAlt: 'Arrow', arrowIconAlt: 'Arrow',
actions: { actions: {
submitting: 'Submitting...', submitting: TEXT_SUBMITTING,
}, },
passwordVisibility: { passwordVisibility: {
hide: 'Hide password', hide: 'Hide password',
@@ -295,12 +368,12 @@ export default {
}, },
fields: { fields: {
username: { username: {
label: 'Mobile:', label: TEXT_MOBILE_LABEL,
placeholder: 'Enter mobile number', placeholder: TEXT_MOBILE_PLACEHOLDER,
}, },
password: { password: {
label: 'Password:', label: TEXT_PASSWORD_LABEL,
placeholder: 'Enter password', placeholder: TEXT_PASSWORD_PLACEHOLDER,
}, },
}, },
footer: { footer: {
@@ -314,20 +387,20 @@ export default {
}, },
register: { register: {
actions: { actions: {
submit: 'Register', submit: TEXT_REGISTER,
}, },
fields: { fields: {
mobile: { mobile: {
label: 'Mobile:', label: TEXT_MOBILE_LABEL,
placeholder: 'Enter mobile number', placeholder: TEXT_MOBILE_PLACEHOLDER,
}, },
captcha: { captcha: {
label: 'Code:', label: 'Code:',
placeholder: 'Enter verification code', placeholder: 'Enter verification code',
}, },
password: { password: {
label: 'Password:', label: TEXT_PASSWORD_LABEL,
placeholder: 'Enter password', placeholder: TEXT_PASSWORD_PLACEHOLDER,
}, },
confirmPassword: { confirmPassword: {
label: 'Confirm Password:', label: 'Confirm Password:',
@@ -393,23 +466,23 @@ export default {
bgm: 'BGM', bgm: 'BGM',
id: 'ID', id: 'ID',
fullscreen: 'Full Screen', fullscreen: 'Full Screen',
login: 'Login', login: TEXT_LOGIN,
register: 'Register', register: TEXT_REGISTER,
}, },
control: { control: {
trend: 'Trend', trend: 'Trend',
map: 'Map', map: 'Map',
selected: 'Selected', selected: 'Selected',
totalBet: 'Total Bet', totalBet: 'Total Bet',
confirm: 'Confirm', confirm: TEXT_CONFIRM,
selectNumbers: 'Select Numbers', selectNumbers: 'Select Numbers',
insufficientBalance: 'Insufficient Balance', insufficientBalance: 'Insufficient Balance',
betLimitExceeded: 'Limit Exceeded', betLimitExceeded: 'Limit Exceeded',
submitting: 'Submitting...', submitting: TEXT_SUBMITTING,
actions: { actions: {
clear: 'Clear', clear: 'Clear',
repeat: 'Repeat', repeat: 'Repeat',
'auto-spin': 'Auto Spin', 'auto-spin': TEXT_AUTO_HOSTING,
}, },
}, },
status: { status: {
@@ -423,7 +496,7 @@ export default {
description: '(Accepting Bets)', description: '(Accepting Bets)',
}, },
locked: { locked: {
label: 'Locked', label: TEXT_LOCKED,
description: '(Betting Closed)', description: '(Betting Closed)',
}, },
revealing: { revealing: {
@@ -445,7 +518,7 @@ export default {
}, },
animal: { animal: {
insufficientBalanceRecharge: 'Insufficient balance, please top up', insufficientBalanceRecharge: 'Insufficient balance, please top up',
betLimitExceeded: 'Single bet limit exceeded', betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
loading: 'Loading', loading: 'Loading',
selectionLimitReached: 'Selection limit exceeded', selectionLimitReached: 'Selection limit exceeded',
tapToEnter: 'Tap To Enter', tapToEnter: 'Tap To Enter',
@@ -460,29 +533,29 @@ export default {
orderNo: 'Order No.', orderNo: 'Order No.',
roundId: 'Round ID', roundId: 'Round ID',
numbers: 'Bet Numbers', numbers: 'Bet Numbers',
createdAt: 'Time', createdAt: TEXT_TIME,
settledAt: 'Settled At', settledAt: 'Settled At',
totalPoolAmount: 'Bet Amount', totalPoolAmount: 'Bet Amount',
winningResult: 'Winning Result', winningResult: 'Winning Result',
payout: 'Win Amount', payout: 'Win Amount',
empty: 'No history yet', empty: 'No history yet',
end: 'No more records', end: 'No more records',
loading: 'Loading...', loading: TEXT_LOADING,
settled: 'Settled', settled: TEXT_SETTLED,
}, },
periodHistory: { periodHistory: {
title: 'Draw Result History', title: 'Draw Result History',
close: 'Close draw result history', close: 'Close draw result history',
empty: 'No draw results yet', empty: 'No draw results yet',
failed: 'Failed to load draw results', failed: 'Failed to load draw results',
loading: 'Loading...', loading: TEXT_LOADING,
retry: 'Retry', retry: 'Retry',
}, },
topup: { topup: {
title: 'Top-up Config', title: 'Top-up Config',
platformCoinLabel: 'Platform Coin', platformCoinLabel: 'Platform Coin',
currencyLabel: 'Currency Type', currencyLabel: TEXT_CURRENCY_TYPE,
channelLabel: 'Payment Channel', channelLabel: TEXT_PAYMENT_CHANNEL,
rateHint: rateHint:
'Exchange rates are for reference only. The final amount follows the top-up-time rate.', 'Exchange rates are for reference only. The final amount follows the top-up-time rate.',
tier: { tier: {
@@ -524,13 +597,13 @@ export default {
feeNotice: feeNotice:
'Transactions between RM10 and RM99.99 will be charged a minimum withdrawal fee of RM 1.', 'Transactions between RM10 and RM99.99 will be charged a minimum withdrawal fee of RM 1.',
cancel: 'Cancel', cancel: 'Cancel',
confirm: 'Confirm', confirm: TEXT_CONFIRM,
submitSuccess: 'Withdrawal request submitted', submitSuccess: 'Withdrawal request submitted',
withdrawal: 'Withdrawal', withdrawal: 'Withdrawal',
fields: { fields: {
diamondAmount: 'Withdrawal Diamond Amount', diamondAmount: 'Withdrawal Diamond Amount',
currencyType: 'Currency Type', currencyType: TEXT_CURRENCY_TYPE,
paymentChannel: 'Payment Channel', paymentChannel: TEXT_PAYMENT_CHANNEL,
bankCode: 'Bank Code', bankCode: 'Bank Code',
cardHolderName: 'Card Holder Name', cardHolderName: 'Card Holder Name',
bankAccountNumber: 'Bank Account Number', bankAccountNumber: 'Bank Account Number',

View File

@@ -1,3 +1,76 @@
/* 以下为多语言中重复出现的文案,统一声明一次后在下方各 key 复用,避免同一文案多处声明。 */
/** @description 登录的统一文案。 */
const TEXT_LOGIN = 'Masuk'
/** @description 注册的统一文案。 */
const TEXT_REGISTER = 'Daftar'
/** @description “是”的统一文案。 */
const TEXT_YES = 'Ya'
/** @description “否”的统一文案。 */
const TEXT_NO = 'Tidak'
/** @description “查看”的统一文案。 */
const TEXT_VIEW = 'Lihat'
/** @description 自动托管的统一文案。 */
const TEXT_AUTO_HOSTING = 'Auto Spin'
/** @description 钱包流水的统一文案。 */
const TEXT_WALLET_RECORDS = 'Riwayat Dompet'
/** @description 站内消息的统一文案。 */
const TEXT_SITE_MESSAGES = 'Pesan'
/** @description 分页“第x页/共x条”的统一文案。 */
const TEXT_PAGE_INDICATOR = 'Halaman {{page}} / total {{total}}'
/** @description 上一页的统一文案。 */
const TEXT_PREV_PAGE = 'Sebelumnya'
/** @description 下一页的统一文案。 */
const TEXT_NEXT_PAGE = 'Berikutnya'
/** @description “时间”的统一文案。 */
const TEXT_TIME = 'Waktu'
/** @description 超过单次投注限额的统一文案。 */
const TEXT_BET_LIMIT_EXCEEDED = 'Melebihi batas taruhan tunggal'
/** @description “提交中...”的统一文案。 */
const TEXT_SUBMITTING = 'Mengirim...'
/** @description 手机号字段标签的统一文案。 */
const TEXT_MOBILE_LABEL = 'Nomor Ponsel:'
/** @description 手机号输入占位的统一文案。 */
const TEXT_MOBILE_PLACEHOLDER = 'Masukkan nomor ponsel'
/** @description 密码字段标签的统一文案。 */
const TEXT_PASSWORD_LABEL = 'Kata Sandi:'
/** @description 密码输入占位的统一文案。 */
const TEXT_PASSWORD_PLACEHOLDER = 'Masukkan kata sandi'
/** @description “确认”按钮的统一文案。 */
const TEXT_CONFIRM = 'Konfirmasi'
/** @description “加载中...”的统一文案。 */
const TEXT_LOADING = 'Memuat...'
/** @description 货币类型的统一文案。 */
const TEXT_CURRENCY_TYPE = 'Jenis Mata Uang'
/** @description 支付渠道的统一文案。 */
const TEXT_PAYMENT_CHANNEL = 'Saluran Pembayaran'
/** @description “已封盘”的统一文案。 */
const TEXT_LOCKED = 'Terkunci'
/** @description “已结算”的统一文案。 */
const TEXT_SETTLED = 'Selesai'
export default { export default {
nav: { nav: {
home: 'Beranda', home: 'Beranda',
@@ -87,8 +160,8 @@ export default {
}, },
phases: { phases: {
betting: 'Betting', betting: 'Betting',
locked: 'Terkunci', locked: TEXT_LOCKED,
settled: 'Selesai', settled: TEXT_SETTLED,
}, },
roundBettingStart: { roundBettingStart: {
title: 'Ronde {{roundId}}', title: 'Ronde {{roundId}}',
@@ -98,8 +171,8 @@ export default {
unifiedBetHint: 'Bet seragam', unifiedBetHint: 'Bet seragam',
totalBet: 'Total bet', totalBet: 'Total bet',
canBet: 'Bisa bet', canBet: 'Bisa bet',
yes: 'Ya', yes: TEXT_YES,
no: 'Tidak', no: TEXT_NO,
quickBet: 'Quick bet 08', quickBet: 'Quick bet 08',
clearPending: 'Hapus pending', clearPending: 'Hapus pending',
autoModeDemo: 'Demo mode auto', autoModeDemo: 'Demo mode auto',
@@ -107,16 +180,16 @@ export default {
}, },
modals: { modals: {
login: { login: {
title: 'Masuk', title: TEXT_LOGIN,
}, },
register: { register: {
title: 'Daftar', title: TEXT_REGISTER,
}, },
notice: { notice: {
title: 'Pengumuman Acara', title: 'Pengumuman Acara',
content: content:
'Bagian ini nantinya akan memuat konten pengumuman acara yang sebenarnya, materi visual, dan pesan panjang yang dapat digulir. Versi saat ini fokus pada sambungan modal multibahasa.', 'Bagian ini nantinya akan memuat konten pengumuman acara yang sebenarnya, materi visual, dan pesan panjang yang dapat digulir. Versi saat ini fokus pada sambungan modal multibahasa.',
check: 'Lihat', check: TEXT_VIEW,
}, },
entryNotice: { entryNotice: {
title: 'Pengumuman Situs', title: 'Pengumuman Situs',
@@ -141,7 +214,7 @@ export default {
topup: 'Isi Ulang', topup: 'Isi Ulang',
}, },
autoSetting: { autoSetting: {
title: 'Auto Spin', title: TEXT_AUTO_HOSTING,
startAutoSpin: 'Mulai Auto Spin', startAutoSpin: 'Mulai Auto Spin',
rows: { rows: {
stopIfBalanceLowerThan: 'Berhenti jika saldo lebih rendah dari', stopIfBalanceLowerThan: 'Berhenti jika saldo lebih rendah dari',
@@ -154,8 +227,8 @@ export default {
tabs: { tabs: {
profile: 'Profil', profile: 'Profil',
financeRecords: 'Riwayat Isi Ulang / Penarikan', financeRecords: 'Riwayat Isi Ulang / Penarikan',
walletRecords: 'Riwayat Dompet', walletRecords: TEXT_WALLET_RECORDS,
message: 'Pesan', message: TEXT_SITE_MESSAGES,
}, },
profile: { profile: {
name: 'Nama', name: 'Nama',
@@ -168,7 +241,7 @@ export default {
'Tanda tanganku seunik diriku. Bagian ini nantinya akan menampilkan ringkasan profil yang sebenarnya.', 'Tanda tanganku seunik diriku. Bagian ini nantinya akan menampilkan ringkasan profil yang sebenarnya.',
}, },
message: { message: {
title: 'Pesan', title: TEXT_SITE_MESSAGES,
back: 'Kembali', back: 'Kembali',
loading: 'Memuat pesan...', loading: 'Memuat pesan...',
loadFailed: 'Gagal memuat pesan. Silakan coba lagi nanti.', loadFailed: 'Gagal memuat pesan. Silakan coba lagi nanti.',
@@ -177,7 +250,7 @@ export default {
unread: 'Belum dibaca', unread: 'Belum dibaca',
eventBonus: eventBonus:
'[Event Bonus Isi Ulang] Dari 1 Oktober hingga 7 Oktober 2026, klaim hadiah rebate kamu...', '[Event Bonus Isi Ulang] Dari 1 Oktober hingga 7 Oktober 2026, klaim hadiah rebate kamu...',
check: 'Lihat', check: TEXT_VIEW,
deleteRecords: 'Hapus riwayat', deleteRecords: 'Hapus riwayat',
}, },
financeRecords: { financeRecords: {
@@ -189,9 +262,9 @@ export default {
loading: 'Memuat riwayat...', loading: 'Memuat riwayat...',
loadFailed: 'Gagal memuat riwayat. Silakan coba lagi nanti.', loadFailed: 'Gagal memuat riwayat. Silakan coba lagi nanti.',
empty: 'Belum ada riwayat', empty: 'Belum ada riwayat',
page: 'Halaman {{page}} / total {{total}}', page: TEXT_PAGE_INDICATOR,
previous: 'Sebelumnya', previous: TEXT_PREV_PAGE,
next: 'Berikutnya', next: TEXT_NEXT_PAGE,
}, },
walletRecords: { walletRecords: {
amount: 'Jumlah', amount: 'Jumlah',
@@ -200,12 +273,12 @@ export default {
empty: 'Belum ada riwayat dompet', empty: 'Belum ada riwayat dompet',
loadFailed: 'Gagal memuat riwayat dompet. Silakan coba lagi nanti.', loadFailed: 'Gagal memuat riwayat dompet. Silakan coba lagi nanti.',
loading: 'Memuat riwayat dompet...', loading: 'Memuat riwayat dompet...',
next: 'Berikutnya', next: TEXT_NEXT_PAGE,
page: 'Halaman {{page}} / total {{total}}', page: TEXT_PAGE_INDICATOR,
previous: 'Sebelumnya', previous: TEXT_PREV_PAGE,
remark: 'Catatan', remark: 'Catatan',
time: 'Waktu', time: TEXT_TIME,
type: 'Riwayat Dompet', type: TEXT_WALLET_RECORDS,
}, },
}, },
withdrawTopup: { withdrawTopup: {
@@ -237,8 +310,8 @@ export default {
dialog: { dialog: {
close: 'Tutup notifikasi', close: 'Tutup notifikasi',
confirm: 'OK', confirm: 'OK',
no: 'Tidak', no: TEXT_NO,
yes: 'Ya', yes: TEXT_YES,
}, },
modal: { modal: {
close: 'Tutup modal', close: 'Tutup modal',
@@ -255,7 +328,7 @@ export default {
inviteLinkCopyFailed: inviteLinkCopyFailed:
'Gagal menyalin tautan undangan. Silakan salin secara manual.', 'Gagal menyalin tautan undangan. Silakan salin secara manual.',
insufficientBalance: 'Saldo tidak cukup. Silakan sesuaikan taruhan.', insufficientBalance: 'Saldo tidak cukup. Silakan sesuaikan taruhan.',
betLimitExceeded: 'Melebihi batas taruhan tunggal', betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
betUnavailable: 'Taruhan tidak tersedia untuk ronde ini', betUnavailable: 'Taruhan tidak tersedia untuk ronde ini',
betPlaced: 'Taruhan berhasil dikirim', betPlaced: 'Taruhan berhasil dikirim',
noRecentSuccessfulBet: noRecentSuccessfulBet:
@@ -282,7 +355,7 @@ export default {
common: { common: {
arrowIconAlt: 'Panah', arrowIconAlt: 'Panah',
actions: { actions: {
submitting: 'Mengirim...', submitting: TEXT_SUBMITTING,
}, },
passwordVisibility: { passwordVisibility: {
hide: 'Sembunyikan kata sandi', hide: 'Sembunyikan kata sandi',
@@ -291,16 +364,16 @@ export default {
}, },
login: { login: {
actions: { actions: {
submit: 'Masuk', submit: TEXT_LOGIN,
}, },
fields: { fields: {
username: { username: {
label: 'Nomor Ponsel:', label: TEXT_MOBILE_LABEL,
placeholder: 'Masukkan nomor ponsel', placeholder: TEXT_MOBILE_PLACEHOLDER,
}, },
password: { password: {
label: 'Kata Sandi:', label: TEXT_PASSWORD_LABEL,
placeholder: 'Masukkan kata sandi', placeholder: TEXT_PASSWORD_PLACEHOLDER,
}, },
}, },
footer: { footer: {
@@ -314,20 +387,20 @@ export default {
}, },
register: { register: {
actions: { actions: {
submit: 'Daftar', submit: TEXT_REGISTER,
}, },
fields: { fields: {
mobile: { mobile: {
label: 'Nomor Ponsel:', label: TEXT_MOBILE_LABEL,
placeholder: 'Masukkan nomor ponsel', placeholder: TEXT_MOBILE_PLACEHOLDER,
}, },
captcha: { captcha: {
label: 'Kode:', label: 'Kode:',
placeholder: 'Masukkan kode verifikasi', placeholder: 'Masukkan kode verifikasi',
}, },
password: { password: {
label: 'Kata Sandi:', label: TEXT_PASSWORD_LABEL,
placeholder: 'Masukkan kata sandi', placeholder: TEXT_PASSWORD_PLACEHOLDER,
}, },
confirmPassword: { confirmPassword: {
label: 'Konfirmasi Kata Sandi:', label: 'Konfirmasi Kata Sandi:',
@@ -352,7 +425,7 @@ export default {
submitFailed: 'Gagal mengirim kode. Silakan coba lagi nanti.', submitFailed: 'Gagal mengirim kode. Silakan coba lagi nanti.',
}, },
send: 'Ambil kode', send: 'Ambil kode',
sending: 'Mengirim...', sending: TEXT_SUBMITTING,
success: 'Kode verifikasi telah dikirim.', success: 'Kode verifikasi telah dikirim.',
}, },
}, },
@@ -389,27 +462,27 @@ export default {
header: { header: {
systemTime: 'Waktu Sistem', systemTime: 'Waktu Sistem',
rules: 'Aturan', rules: 'Aturan',
message: 'Pesan', message: TEXT_SITE_MESSAGES,
bgm: 'BGM', bgm: 'BGM',
id: 'ID', id: 'ID',
fullscreen: 'Layar', fullscreen: 'Layar',
login: 'Masuk', login: TEXT_LOGIN,
register: 'Daftar', register: TEXT_REGISTER,
}, },
control: { control: {
trend: 'Tren', trend: 'Tren',
map: 'Peta', map: 'Peta',
selected: 'Dipilih', selected: 'Dipilih',
totalBet: 'Total Bet', totalBet: 'Total Bet',
confirm: 'Konfirmasi', confirm: TEXT_CONFIRM,
selectNumbers: 'Pilih Nombor', selectNumbers: 'Pilih Nombor',
insufficientBalance: 'Saldo Tidak Cukup', insufficientBalance: 'Saldo Tidak Cukup',
betLimitExceeded: 'Batas Terlampaui', betLimitExceeded: 'Batas Terlampaui',
submitting: 'Mengirim...', submitting: TEXT_SUBMITTING,
actions: { actions: {
clear: 'Hapus', clear: 'Hapus',
repeat: 'Ulang', repeat: 'Ulang',
'auto-spin': 'Auto Spin', 'auto-spin': TEXT_AUTO_HOSTING,
}, },
}, },
status: { status: {
@@ -423,7 +496,7 @@ export default {
description: '(Menerima Bet)', description: '(Menerima Bet)',
}, },
locked: { locked: {
label: 'Terkunci', label: TEXT_LOCKED,
description: '(Bet Ditutup)', description: '(Bet Ditutup)',
}, },
revealing: { revealing: {
@@ -445,7 +518,7 @@ export default {
}, },
animal: { animal: {
insufficientBalanceRecharge: 'Saldo tidak cukup, silakan isi ulang', insufficientBalanceRecharge: 'Saldo tidak cukup, silakan isi ulang',
betLimitExceeded: 'Melebihi batas taruhan tunggal', betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
loading: 'Memuat', loading: 'Memuat',
selectionLimitReached: 'Melebihi pilihan yang diizinkan', selectionLimitReached: 'Melebihi pilihan yang diizinkan',
tapToEnter: 'Ketuk Untuk Masuk', tapToEnter: 'Ketuk Untuk Masuk',
@@ -460,29 +533,29 @@ export default {
orderNo: 'No. Order', orderNo: 'No. Order',
roundId: 'ID Ronde', roundId: 'ID Ronde',
numbers: 'Nomor Taruhan', numbers: 'Nomor Taruhan',
createdAt: 'Waktu', createdAt: TEXT_TIME,
settledAt: 'Waktu Selesai', settledAt: 'Waktu Selesai',
totalPoolAmount: 'Jumlah Taruhan', totalPoolAmount: 'Jumlah Taruhan',
winningResult: 'Hasil Menang', winningResult: 'Hasil Menang',
payout: 'Jumlah Menang', payout: 'Jumlah Menang',
empty: 'Belum ada riwayat', empty: 'Belum ada riwayat',
end: 'Tidak ada catatan lagi', end: 'Tidak ada catatan lagi',
loading: 'Memuat...', loading: TEXT_LOADING,
settled: 'Selesai', settled: TEXT_SETTLED,
}, },
periodHistory: { periodHistory: {
title: 'Riwayat Hasil Undian', title: 'Riwayat Hasil Undian',
close: 'Tutup riwayat hasil undian', close: 'Tutup riwayat hasil undian',
empty: 'Belum ada hasil undian', empty: 'Belum ada hasil undian',
failed: 'Gagal memuat hasil undian', failed: 'Gagal memuat hasil undian',
loading: 'Memuat...', loading: TEXT_LOADING,
retry: 'Coba lagi', retry: 'Coba lagi',
}, },
topup: { topup: {
title: 'Konfigurasi Isi Ulang', title: 'Konfigurasi Isi Ulang',
platformCoinLabel: 'Koin Platform', platformCoinLabel: 'Koin Platform',
currencyLabel: 'Jenis Mata Uang', currencyLabel: TEXT_CURRENCY_TYPE,
channelLabel: 'Saluran Pembayaran', channelLabel: TEXT_PAYMENT_CHANNEL,
rateHint: rateHint:
'Kurs hanya sebagai referensi. Jumlah akhir mengikuti kurs saat isi ulang.', 'Kurs hanya sebagai referensi. Jumlah akhir mengikuti kurs saat isi ulang.',
tier: { tier: {
@@ -525,13 +598,13 @@ export default {
feeNotice: feeNotice:
'Transaksi antara RM10 dan RM99.99 akan dikenakan biaya penarikan minimum RM 1.', 'Transaksi antara RM10 dan RM99.99 akan dikenakan biaya penarikan minimum RM 1.',
cancel: 'Batal', cancel: 'Batal',
confirm: 'Konfirmasi', confirm: TEXT_CONFIRM,
submitSuccess: 'Permintaan penarikan berhasil dikirim', submitSuccess: 'Permintaan penarikan berhasil dikirim',
withdrawal: 'Penarikan', withdrawal: 'Penarikan',
fields: { fields: {
diamondAmount: 'Jumlah Berlian Penarikan', diamondAmount: 'Jumlah Berlian Penarikan',
currencyType: 'Jenis Mata Uang', currencyType: TEXT_CURRENCY_TYPE,
paymentChannel: 'Saluran Pembayaran', paymentChannel: TEXT_PAYMENT_CHANNEL,
bankCode: 'Kode Bank', bankCode: 'Kode Bank',
cardHolderName: 'Nama Pemilik Rekening', cardHolderName: 'Nama Pemilik Rekening',
bankAccountNumber: 'Nomor Rekening Bank', bankAccountNumber: 'Nomor Rekening Bank',

View File

@@ -1,3 +1,76 @@
/* 以下为多语言中重复出现的文案,统一声明一次后在下方各 key 复用,避免同一文案多处声明。 */
/** @description 登录的统一文案。 */
const TEXT_LOGIN = 'Log Masuk'
/** @description 注册的统一文案。 */
const TEXT_REGISTER = 'Daftar'
/** @description “是”的统一文案。 */
const TEXT_YES = 'Ya'
/** @description “否”的统一文案。 */
const TEXT_NO = 'Tidak'
/** @description “查看”的统一文案。 */
const TEXT_VIEW = 'Semak'
/** @description 自动托管的统一文案。 */
const TEXT_AUTO_HOSTING = 'Putaran Auto'
/** @description 钱包流水的统一文案。 */
const TEXT_WALLET_RECORDS = 'Rekod Dompet'
/** @description 站内消息的统一文案。 */
const TEXT_SITE_MESSAGES = 'Mesej'
/** @description 分页“第x页/共x条”的统一文案。 */
const TEXT_PAGE_INDICATOR = 'Halaman {{page}} / jumlah {{total}}'
/** @description 上一页的统一文案。 */
const TEXT_PREV_PAGE = 'Sebelumnya'
/** @description 下一页的统一文案。 */
const TEXT_NEXT_PAGE = 'Seterusnya'
/** @description “时间”的统一文案。 */
const TEXT_TIME = 'Masa'
/** @description 超过单次投注限额的统一文案。 */
const TEXT_BET_LIMIT_EXCEEDED = 'Melebihi had taruhan tunggal'
/** @description “提交中...”的统一文案。 */
const TEXT_SUBMITTING = 'Menghantar...'
/** @description 手机号字段标签的统一文案。 */
const TEXT_MOBILE_LABEL = 'Nombor Telefon:'
/** @description 手机号输入占位的统一文案。 */
const TEXT_MOBILE_PLACEHOLDER = 'Masukkan nombor telefon'
/** @description 密码字段标签的统一文案。 */
const TEXT_PASSWORD_LABEL = 'Kata Laluan:'
/** @description 密码输入占位的统一文案。 */
const TEXT_PASSWORD_PLACEHOLDER = 'Masukkan kata laluan'
/** @description “确认”按钮的统一文案。 */
const TEXT_CONFIRM = 'Sahkan'
/** @description “加载中...”的统一文案。 */
const TEXT_LOADING = 'Memuatkan...'
/** @description 货币类型的统一文案。 */
const TEXT_CURRENCY_TYPE = 'Jenis Mata Wang'
/** @description 支付渠道的统一文案。 */
const TEXT_PAYMENT_CHANNEL = 'Saluran Pembayaran'
/** @description “已封盘”的统一文案。 */
const TEXT_LOCKED = 'Dikunci'
/** @description “已结算”的统一文案。 */
const TEXT_SETTLED = 'Selesai'
export default { export default {
nav: { nav: {
home: 'Laman Utama', home: 'Laman Utama',
@@ -90,8 +163,8 @@ export default {
}, },
phases: { phases: {
betting: 'Taruhan', betting: 'Taruhan',
locked: 'Dikunci', locked: TEXT_LOCKED,
settled: 'Selesai', settled: TEXT_SETTLED,
}, },
roundBettingStart: { roundBettingStart: {
title: 'Pusingan {{roundId}}', title: 'Pusingan {{roundId}}',
@@ -101,8 +174,8 @@ export default {
unifiedBetHint: 'Taruhan seragam', unifiedBetHint: 'Taruhan seragam',
totalBet: 'Jumlah taruhan', totalBet: 'Jumlah taruhan',
canBet: 'Boleh taruhan', canBet: 'Boleh taruhan',
yes: 'Ya', yes: TEXT_YES,
no: 'Tidak', no: TEXT_NO,
quickBet: 'Taruhan cepat 08', quickBet: 'Taruhan cepat 08',
clearPending: 'Kosongkan belum sah', clearPending: 'Kosongkan belum sah',
autoModeDemo: 'Demo mod auto', autoModeDemo: 'Demo mod auto',
@@ -110,16 +183,16 @@ export default {
}, },
modals: { modals: {
login: { login: {
title: 'Log Masuk', title: TEXT_LOGIN,
}, },
register: { register: {
title: 'Daftar', title: TEXT_REGISTER,
}, },
notice: { notice: {
title: 'Notis Acara', title: 'Notis Acara',
content: content:
'Bahagian ini akan memuatkan kandungan notis acara sebenar, bahan visual, dan mesej boleh skrol yang lebih panjang. Versi semasa memfokuskan sambungan modal pelbagai bahasa.', 'Bahagian ini akan memuatkan kandungan notis acara sebenar, bahan visual, dan mesej boleh skrol yang lebih panjang. Versi semasa memfokuskan sambungan modal pelbagai bahasa.',
check: 'Semak', check: TEXT_VIEW,
}, },
entryNotice: { entryNotice: {
title: 'Notis Laman', title: 'Notis Laman',
@@ -144,7 +217,7 @@ export default {
topup: 'Tambah Nilai', topup: 'Tambah Nilai',
}, },
autoSetting: { autoSetting: {
title: 'Putaran Auto', title: TEXT_AUTO_HOSTING,
startAutoSpin: 'Mula Putaran Auto', startAutoSpin: 'Mula Putaran Auto',
rows: { rows: {
stopIfBalanceLowerThan: 'Henti jika baki lebih rendah daripada', stopIfBalanceLowerThan: 'Henti jika baki lebih rendah daripada',
@@ -157,8 +230,8 @@ export default {
tabs: { tabs: {
profile: 'Profil', profile: 'Profil',
financeRecords: 'Rekod Tambah Nilai / Pengeluaran', financeRecords: 'Rekod Tambah Nilai / Pengeluaran',
walletRecords: 'Rekod Dompet', walletRecords: TEXT_WALLET_RECORDS,
message: 'Mesej', message: TEXT_SITE_MESSAGES,
}, },
profile: { profile: {
name: 'Nama', name: 'Nama',
@@ -171,7 +244,7 @@ export default {
'Tandatangan saya unik seperti personaliti saya. Bahagian ini akan memaparkan ringkasan profil sebenar kemudian.', 'Tandatangan saya unik seperti personaliti saya. Bahagian ini akan memaparkan ringkasan profil sebenar kemudian.',
}, },
message: { message: {
title: 'Mesej', title: TEXT_SITE_MESSAGES,
back: 'Kembali', back: 'Kembali',
loading: 'Memuatkan mesej...', loading: 'Memuatkan mesej...',
loadFailed: 'Gagal memuatkan mesej. Sila cuba lagi kemudian.', loadFailed: 'Gagal memuatkan mesej. Sila cuba lagi kemudian.',
@@ -180,7 +253,7 @@ export default {
unread: 'Belum dibaca', unread: 'Belum dibaca',
eventBonus: eventBonus:
'[Acara Bonus Tambah Nilai] Dari 1 Oktober hingga 7 Oktober 2026, tuntut ganjaran rebat anda...', '[Acara Bonus Tambah Nilai] Dari 1 Oktober hingga 7 Oktober 2026, tuntut ganjaran rebat anda...',
check: 'Semak', check: TEXT_VIEW,
deleteRecords: 'Padam rekod', deleteRecords: 'Padam rekod',
}, },
financeRecords: { financeRecords: {
@@ -192,9 +265,9 @@ export default {
loading: 'Memuatkan rekod...', loading: 'Memuatkan rekod...',
loadFailed: 'Gagal memuatkan rekod. Sila cuba lagi kemudian.', loadFailed: 'Gagal memuatkan rekod. Sila cuba lagi kemudian.',
empty: 'Belum ada rekod', empty: 'Belum ada rekod',
page: 'Halaman {{page}} / jumlah {{total}}', page: TEXT_PAGE_INDICATOR,
previous: 'Sebelumnya', previous: TEXT_PREV_PAGE,
next: 'Seterusnya', next: TEXT_NEXT_PAGE,
}, },
walletRecords: { walletRecords: {
amount: 'Jumlah', amount: 'Jumlah',
@@ -203,12 +276,12 @@ export default {
empty: 'Belum ada rekod dompet', empty: 'Belum ada rekod dompet',
loadFailed: 'Gagal memuatkan rekod dompet. Sila cuba lagi kemudian.', loadFailed: 'Gagal memuatkan rekod dompet. Sila cuba lagi kemudian.',
loading: 'Memuatkan rekod dompet...', loading: 'Memuatkan rekod dompet...',
next: 'Seterusnya', next: TEXT_NEXT_PAGE,
page: 'Halaman {{page}} / jumlah {{total}}', page: TEXT_PAGE_INDICATOR,
previous: 'Sebelumnya', previous: TEXT_PREV_PAGE,
remark: 'Catatan', remark: 'Catatan',
time: 'Masa', time: TEXT_TIME,
type: 'Rekod Dompet', type: TEXT_WALLET_RECORDS,
}, },
}, },
withdrawTopup: { withdrawTopup: {
@@ -240,8 +313,8 @@ export default {
dialog: { dialog: {
close: 'Tutup notifikasi', close: 'Tutup notifikasi',
confirm: 'OK', confirm: 'OK',
no: 'Tidak', no: TEXT_NO,
yes: 'Ya', yes: TEXT_YES,
}, },
modal: { modal: {
close: 'Tutup modal', close: 'Tutup modal',
@@ -259,7 +332,7 @@ export default {
inviteLinkCopyFailed: inviteLinkCopyFailed:
'Gagal menyalin pautan jemputan. Sila salin secara manual.', 'Gagal menyalin pautan jemputan. Sila salin secara manual.',
insufficientBalance: 'Baki tidak mencukupi. Sila laraskan taruhan.', insufficientBalance: 'Baki tidak mencukupi. Sila laraskan taruhan.',
betLimitExceeded: 'Melebihi had taruhan tunggal', betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
betUnavailable: 'Taruhan tidak tersedia untuk pusingan ini', betUnavailable: 'Taruhan tidak tersedia untuk pusingan ini',
betPlaced: 'Taruhan berjaya dihantar', betPlaced: 'Taruhan berjaya dihantar',
noRecentSuccessfulBet: noRecentSuccessfulBet:
@@ -287,7 +360,7 @@ export default {
common: { common: {
arrowIconAlt: 'Anak panah', arrowIconAlt: 'Anak panah',
actions: { actions: {
submitting: 'Menghantar...', submitting: TEXT_SUBMITTING,
}, },
passwordVisibility: { passwordVisibility: {
hide: 'Sembunyikan kata laluan', hide: 'Sembunyikan kata laluan',
@@ -296,16 +369,16 @@ export default {
}, },
login: { login: {
actions: { actions: {
submit: 'Log Masuk', submit: TEXT_LOGIN,
}, },
fields: { fields: {
username: { username: {
label: 'Nombor Telefon:', label: TEXT_MOBILE_LABEL,
placeholder: 'Masukkan nombor telefon', placeholder: TEXT_MOBILE_PLACEHOLDER,
}, },
password: { password: {
label: 'Kata Laluan:', label: TEXT_PASSWORD_LABEL,
placeholder: 'Masukkan kata laluan', placeholder: TEXT_PASSWORD_PLACEHOLDER,
}, },
}, },
footer: { footer: {
@@ -319,20 +392,20 @@ export default {
}, },
register: { register: {
actions: { actions: {
submit: 'Daftar', submit: TEXT_REGISTER,
}, },
fields: { fields: {
mobile: { mobile: {
label: 'Nombor Telefon:', label: TEXT_MOBILE_LABEL,
placeholder: 'Masukkan nombor telefon', placeholder: TEXT_MOBILE_PLACEHOLDER,
}, },
captcha: { captcha: {
label: 'Kod:', label: 'Kod:',
placeholder: 'Masukkan kod pengesahan', placeholder: 'Masukkan kod pengesahan',
}, },
password: { password: {
label: 'Kata Laluan:', label: TEXT_PASSWORD_LABEL,
placeholder: 'Masukkan kata laluan', placeholder: TEXT_PASSWORD_PLACEHOLDER,
}, },
confirmPassword: { confirmPassword: {
label: 'Sahkan Kata Laluan:', label: 'Sahkan Kata Laluan:',
@@ -357,7 +430,7 @@ export default {
submitFailed: 'Gagal menghantar kod. Sila cuba lagi kemudian.', submitFailed: 'Gagal menghantar kod. Sila cuba lagi kemudian.',
}, },
send: 'Dapatkan kod', send: 'Dapatkan kod',
sending: 'Menghantar...', sending: TEXT_SUBMITTING,
success: 'Kod pengesahan telah dihantar.', success: 'Kod pengesahan telah dihantar.',
}, },
}, },
@@ -394,27 +467,27 @@ export default {
header: { header: {
systemTime: 'Masa Sistem', systemTime: 'Masa Sistem',
rules: 'Peraturan', rules: 'Peraturan',
message: 'Mesej', message: TEXT_SITE_MESSAGES,
bgm: 'BGM', bgm: 'BGM',
id: 'ID', id: 'ID',
fullscreen: 'Skrin', fullscreen: 'Skrin',
login: 'Log Masuk', login: TEXT_LOGIN,
register: 'Daftar', register: TEXT_REGISTER,
}, },
control: { control: {
trend: 'Trend', trend: 'Trend',
map: 'Peta', map: 'Peta',
selected: 'Dipilih', selected: 'Dipilih',
totalBet: 'Jumlah Taruhan', totalBet: 'Jumlah Taruhan',
confirm: 'Sahkan', confirm: TEXT_CONFIRM,
selectNumbers: 'Pilih Nombor', selectNumbers: 'Pilih Nombor',
insufficientBalance: 'Baki Tidak Mencukupi', insufficientBalance: 'Baki Tidak Mencukupi',
betLimitExceeded: 'Melebihi Had', betLimitExceeded: 'Melebihi Had',
submitting: 'Menghantar...', submitting: TEXT_SUBMITTING,
actions: { actions: {
clear: 'Kosongkan', clear: 'Kosongkan',
repeat: 'Ulang', repeat: 'Ulang',
'auto-spin': 'Putaran Auto', 'auto-spin': TEXT_AUTO_HOSTING,
}, },
}, },
status: { status: {
@@ -428,7 +501,7 @@ export default {
description: '(Menerima Taruhan)', description: '(Menerima Taruhan)',
}, },
locked: { locked: {
label: 'Dikunci', label: TEXT_LOCKED,
description: '(Taruhan Ditutup)', description: '(Taruhan Ditutup)',
}, },
revealing: { revealing: {
@@ -450,7 +523,7 @@ export default {
}, },
animal: { animal: {
insufficientBalanceRecharge: 'Baki tidak mencukupi, sila tambah nilai', insufficientBalanceRecharge: 'Baki tidak mencukupi, sila tambah nilai',
betLimitExceeded: 'Melebihi had taruhan tunggal', betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
loading: 'Memuatkan', loading: 'Memuatkan',
selectionLimitReached: 'Melebihi pilihan aksara yang dibenarkan', selectionLimitReached: 'Melebihi pilihan aksara yang dibenarkan',
tapToEnter: 'Ketik Untuk Masuk', tapToEnter: 'Ketik Untuk Masuk',
@@ -465,29 +538,29 @@ export default {
orderNo: 'No. Pesanan', orderNo: 'No. Pesanan',
roundId: 'ID Pusingan', roundId: 'ID Pusingan',
numbers: 'Nombor Pertaruhan', numbers: 'Nombor Pertaruhan',
createdAt: 'Masa', createdAt: TEXT_TIME,
settledAt: 'Masa Selesai', settledAt: 'Masa Selesai',
totalPoolAmount: 'Jumlah Pertaruhan', totalPoolAmount: 'Jumlah Pertaruhan',
winningResult: 'Keputusan Menang', winningResult: 'Keputusan Menang',
payout: 'Jumlah Menang', payout: 'Jumlah Menang',
empty: 'Belum ada sejarah', empty: 'Belum ada sejarah',
end: 'Tiada lagi rekod', end: 'Tiada lagi rekod',
loading: 'Memuatkan...', loading: TEXT_LOADING,
settled: 'Selesai', settled: TEXT_SETTLED,
}, },
periodHistory: { periodHistory: {
title: 'Sejarah Keputusan Cabutan', title: 'Sejarah Keputusan Cabutan',
close: 'Tutup sejarah keputusan cabutan', close: 'Tutup sejarah keputusan cabutan',
empty: 'Belum ada keputusan cabutan', empty: 'Belum ada keputusan cabutan',
failed: 'Gagal memuatkan keputusan cabutan', failed: 'Gagal memuatkan keputusan cabutan',
loading: 'Memuatkan...', loading: TEXT_LOADING,
retry: 'Cuba lagi', retry: 'Cuba lagi',
}, },
topup: { topup: {
title: 'Konfigurasi Tambah Nilai', title: 'Konfigurasi Tambah Nilai',
platformCoinLabel: 'Syiling Platform', platformCoinLabel: 'Syiling Platform',
currencyLabel: 'Jenis Mata Wang', currencyLabel: TEXT_CURRENCY_TYPE,
channelLabel: 'Saluran Pembayaran', channelLabel: TEXT_PAYMENT_CHANNEL,
rateHint: rateHint:
'Kadar pertukaran hanya untuk rujukan. Jumlah akhir tertakluk kepada kadar semasa tambah nilai.', 'Kadar pertukaran hanya untuk rujukan. Jumlah akhir tertakluk kepada kadar semasa tambah nilai.',
tier: { tier: {
@@ -529,13 +602,13 @@ export default {
feeNotice: feeNotice:
'Transaksi antara RM10 dan RM99.99 akan dikenakan yuran pengeluaran minimum RM 1.', 'Transaksi antara RM10 dan RM99.99 akan dikenakan yuran pengeluaran minimum RM 1.',
cancel: 'Batal', cancel: 'Batal',
confirm: 'Sahkan', confirm: TEXT_CONFIRM,
submitSuccess: 'Permohonan pengeluaran telah dihantar', submitSuccess: 'Permohonan pengeluaran telah dihantar',
withdrawal: 'Pengeluaran', withdrawal: 'Pengeluaran',
fields: { fields: {
diamondAmount: 'Jumlah Berlian Pengeluaran', diamondAmount: 'Jumlah Berlian Pengeluaran',
currencyType: 'Jenis Mata Wang', currencyType: TEXT_CURRENCY_TYPE,
paymentChannel: 'Saluran Pembayaran', paymentChannel: TEXT_PAYMENT_CHANNEL,
bankCode: 'Kod Bank', bankCode: 'Kod Bank',
cardHolderName: 'Nama Pemegang Kad', cardHolderName: 'Nama Pemegang Kad',
bankAccountNumber: 'Nombor Akaun Bank', bankAccountNumber: 'Nombor Akaun Bank',

View File

@@ -1,3 +1,76 @@
/* 以下为多语言中重复出现的文案,统一声明一次后在下方各 key 复用,避免同一文案多处声明。 */
/** @description 登录的统一文案。 */
const TEXT_LOGIN = '登录'
/** @description 注册的统一文案。 */
const TEXT_REGISTER = '注册'
/** @description “是”的统一文案。 */
const TEXT_YES = '是'
/** @description “否”的统一文案。 */
const TEXT_NO = '否'
/** @description “查看”的统一文案。 */
const TEXT_VIEW = '查看'
/** @description 自动托管的统一文案。 */
const TEXT_AUTO_HOSTING = '自动托管'
/** @description 钱包流水的统一文案。 */
const TEXT_WALLET_RECORDS = '钱包流水'
/** @description 站内消息的统一文案。 */
const TEXT_SITE_MESSAGES = '站内消息'
/** @description 分页“第x页/共x条”的统一文案。 */
const TEXT_PAGE_INDICATOR = '第 {{page}} 页 / 共 {{total}} 条'
/** @description 上一页的统一文案。 */
const TEXT_PREV_PAGE = '上一页'
/** @description 下一页的统一文案。 */
const TEXT_NEXT_PAGE = '下一页'
/** @description “时间”的统一文案。 */
const TEXT_TIME = '时间'
/** @description 超过单次投注限额的统一文案。 */
const TEXT_BET_LIMIT_EXCEEDED = '超过单次投注限额'
/** @description “提交中...”的统一文案。 */
const TEXT_SUBMITTING = '提交中...'
/** @description 手机号字段标签的统一文案。 */
const TEXT_MOBILE_LABEL = '手机号:'
/** @description 手机号输入占位的统一文案。 */
const TEXT_MOBILE_PLACEHOLDER = '请输入手机号'
/** @description 密码字段标签的统一文案。 */
const TEXT_PASSWORD_LABEL = '密码:'
/** @description 密码输入占位的统一文案。 */
const TEXT_PASSWORD_PLACEHOLDER = '请输入密码'
/** @description “确认”按钮的统一文案。 */
const TEXT_CONFIRM = '确认'
/** @description “加载中...”的统一文案。 */
const TEXT_LOADING = '加载中...'
/** @description 货币类型的统一文案。 */
const TEXT_CURRENCY_TYPE = '货币类型'
/** @description 支付渠道的统一文案。 */
const TEXT_PAYMENT_CHANNEL = '支付渠道'
/** @description “已封盘”的统一文案。 */
const TEXT_LOCKED = '已封盘'
/** @description “已结算”的统一文案。 */
const TEXT_SETTLED = '已结算'
export default { export default {
nav: { nav: {
home: '首页', home: '首页',
@@ -86,8 +159,8 @@ export default {
}, },
phases: { phases: {
betting: '下注中', betting: '下注中',
locked: '已封盘', locked: TEXT_LOCKED,
settled: '已结算', settled: TEXT_SETTLED,
}, },
roundBettingStart: { roundBettingStart: {
title: '{{roundId}}期', title: '{{roundId}}期',
@@ -97,8 +170,8 @@ export default {
unifiedBetHint: '统一下注额', unifiedBetHint: '统一下注额',
totalBet: '总下注', totalBet: '总下注',
canBet: '可下注', canBet: '可下注',
yes: '是', yes: TEXT_YES,
no: '否', no: TEXT_NO,
quickBet: '快速选中 08', quickBet: '快速选中 08',
clearPending: '清空未确认', clearPending: '清空未确认',
autoModeDemo: '自动托管演示', autoModeDemo: '自动托管演示',
@@ -106,16 +179,16 @@ export default {
}, },
modals: { modals: {
login: { login: {
title: '登录', title: TEXT_LOGIN,
}, },
register: { register: {
title: '注册', title: TEXT_REGISTER,
}, },
notice: { notice: {
title: '活动公告', title: '活动公告',
content: content:
'这里后续将接入真实的活动公告内容、图文素材和滚动阅读区域。当前版本先用多语言结构完成桌面端公告弹窗能力。', '这里后续将接入真实的活动公告内容、图文素材和滚动阅读区域。当前版本先用多语言结构完成桌面端公告弹窗能力。',
check: '查看', check: TEXT_VIEW,
}, },
entryNotice: { entryNotice: {
title: '网站公告', title: '网站公告',
@@ -139,7 +212,7 @@ export default {
topup: '充值', topup: '充值',
}, },
autoSetting: { autoSetting: {
title: '自动托管', title: TEXT_AUTO_HOSTING,
startAutoSpin: '开始自动托管', startAutoSpin: '开始自动托管',
rows: { rows: {
stopIfBalanceLowerThan: '余额低于时停止', stopIfBalanceLowerThan: '余额低于时停止',
@@ -152,8 +225,8 @@ export default {
tabs: { tabs: {
profile: '个人信息', profile: '个人信息',
financeRecords: '充值/提现记录', financeRecords: '充值/提现记录',
walletRecords: '钱包流水', walletRecords: TEXT_WALLET_RECORDS,
message: '站内消息', message: TEXT_SITE_MESSAGES,
}, },
profile: { profile: {
name: '用户名', name: '用户名',
@@ -165,7 +238,7 @@ export default {
signature: '我的个性签名和灵魂一样独特,后续这里会接真实资料字段。', signature: '我的个性签名和灵魂一样独特,后续这里会接真实资料字段。',
}, },
message: { message: {
title: '站内消息', title: TEXT_SITE_MESSAGES,
back: '返回', back: '返回',
loading: '消息加载中...', loading: '消息加载中...',
loadFailed: '消息加载失败,请稍后重试', loadFailed: '消息加载失败,请稍后重试',
@@ -173,7 +246,7 @@ export default {
read: '已读', read: '已读',
unread: '未读', unread: '未读',
eventBonus: '[充值活动] 10 月 1 日至 10 月 7 日期间可获得返利奖励……', eventBonus: '[充值活动] 10 月 1 日至 10 月 7 日期间可获得返利奖励……',
check: '查看', check: TEXT_VIEW,
deleteRecords: '删除记录', deleteRecords: '删除记录',
}, },
financeRecords: { financeRecords: {
@@ -185,9 +258,9 @@ export default {
loading: '记录加载中...', loading: '记录加载中...',
loadFailed: '记录加载失败,请稍后重试', loadFailed: '记录加载失败,请稍后重试',
empty: '暂无记录', empty: '暂无记录',
page: '第 {{page}} 页 / 共 {{total}} 条', page: TEXT_PAGE_INDICATOR,
previous: '上一页', previous: TEXT_PREV_PAGE,
next: '下一页', next: TEXT_NEXT_PAGE,
}, },
walletRecords: { walletRecords: {
amount: '变动金额', amount: '变动金额',
@@ -196,12 +269,12 @@ export default {
empty: '暂无钱包流水', empty: '暂无钱包流水',
loadFailed: '钱包流水加载失败,请稍后重试', loadFailed: '钱包流水加载失败,请稍后重试',
loading: '钱包流水加载中...', loading: '钱包流水加载中...',
next: '下一页', next: TEXT_NEXT_PAGE,
page: '第 {{page}} 页 / 共 {{total}} 条', page: TEXT_PAGE_INDICATOR,
previous: '上一页', previous: TEXT_PREV_PAGE,
remark: '备注', remark: '备注',
time: '时间', time: TEXT_TIME,
type: '钱包流水', type: TEXT_WALLET_RECORDS,
}, },
}, },
withdrawTopup: { withdrawTopup: {
@@ -210,7 +283,7 @@ export default {
}, },
}, },
autoSpin: { autoSpin: {
eyebrow: '自动托管', eyebrow: TEXT_AUTO_HOSTING,
title: '自动托管运行中', title: '自动托管运行中',
description: '托管态会覆盖主盘面,但目标格子和进度信息仍然保留可见。', description: '托管态会覆盖主盘面,但目标格子和进度信息仍然保留可见。',
runningRounds: '游戏托管中,已经进行 {{count}} 局', runningRounds: '游戏托管中,已经进行 {{count}} 局',
@@ -232,8 +305,8 @@ export default {
dialog: { dialog: {
close: '关闭提示', close: '关闭提示',
confirm: '知道了', confirm: '知道了',
no: '否', no: TEXT_NO,
yes: '是', yes: TEXT_YES,
}, },
modal: { modal: {
close: '关闭弹窗', close: '关闭弹窗',
@@ -249,7 +322,7 @@ export default {
inviteLinkCopied: '邀请链接已复制', inviteLinkCopied: '邀请链接已复制',
inviteLinkCopyFailed: '邀请链接复制失败,请手动复制', inviteLinkCopyFailed: '邀请链接复制失败,请手动复制',
insufficientBalance: '余额不足,请调整下注金额', insufficientBalance: '余额不足,请调整下注金额',
betLimitExceeded: '超过单次投注限额', betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
betUnavailable: '当前期不可下注', betUnavailable: '当前期不可下注',
betPlaced: '下注成功', betPlaced: '下注成功',
noRecentSuccessfulBet: '暂无上一局成功下注记录', noRecentSuccessfulBet: '暂无上一局成功下注记录',
@@ -270,7 +343,7 @@ export default {
common: { common: {
arrowIconAlt: '箭头', arrowIconAlt: '箭头',
actions: { actions: {
submitting: '提交中...', submitting: TEXT_SUBMITTING,
}, },
passwordVisibility: { passwordVisibility: {
hide: '隐藏密码', hide: '隐藏密码',
@@ -279,16 +352,16 @@ export default {
}, },
login: { login: {
actions: { actions: {
submit: '登录', submit: TEXT_LOGIN,
}, },
fields: { fields: {
username: { username: {
label: '手机号:', label: TEXT_MOBILE_LABEL,
placeholder: '请输入手机号', placeholder: TEXT_MOBILE_PLACEHOLDER,
}, },
password: { password: {
label: '密码:', label: TEXT_PASSWORD_LABEL,
placeholder: '请输入密码', placeholder: TEXT_PASSWORD_PLACEHOLDER,
}, },
}, },
footer: { footer: {
@@ -302,20 +375,20 @@ export default {
}, },
register: { register: {
actions: { actions: {
submit: '注册', submit: TEXT_REGISTER,
}, },
fields: { fields: {
mobile: { mobile: {
label: '手机号:', label: TEXT_MOBILE_LABEL,
placeholder: '请输入手机号', placeholder: TEXT_MOBILE_PLACEHOLDER,
}, },
captcha: { captcha: {
label: '验证码:', label: '验证码:',
placeholder: '请输入验证码', placeholder: '请输入验证码',
}, },
password: { password: {
label: '密码:', label: TEXT_PASSWORD_LABEL,
placeholder: '请输入密码', placeholder: TEXT_PASSWORD_PLACEHOLDER,
}, },
confirmPassword: { confirmPassword: {
label: '确认密码:', label: '确认密码:',
@@ -349,7 +422,7 @@ export default {
required: '请输入验证码', required: '请输入验证码',
}, },
username: { username: {
required: '请输入手机号', required: TEXT_MOBILE_PLACEHOLDER,
invalidPhone: '请输入正确的手机号', invalidPhone: '请输入正确的手机号',
}, },
password: { password: {
@@ -379,23 +452,23 @@ export default {
bgm: '音乐', bgm: '音乐',
id: '编号', id: '编号',
fullscreen: '全屏', fullscreen: '全屏',
login: '登录', login: TEXT_LOGIN,
register: '注册', register: TEXT_REGISTER,
}, },
control: { control: {
trend: '走势', trend: '走势',
map: '地图', map: '地图',
selected: '已选', selected: '已选',
totalBet: '总下注', totalBet: '总下注',
confirm: '确认', confirm: TEXT_CONFIRM,
selectNumbers: '请选择号码', selectNumbers: '请选择号码',
insufficientBalance: '余额不足', insufficientBalance: '余额不足',
betLimitExceeded: '超过限额', betLimitExceeded: '超过限额',
submitting: '提交中...', submitting: TEXT_SUBMITTING,
actions: { actions: {
clear: '清空', clear: '清空',
repeat: '重复', repeat: '重复',
'auto-spin': '自动托管', 'auto-spin': TEXT_AUTO_HOSTING,
}, },
}, },
status: { status: {
@@ -409,7 +482,7 @@ export default {
description: '(接受下注)', description: '(接受下注)',
}, },
locked: { locked: {
label: '已封盘', label: TEXT_LOCKED,
description: '(停止下注)', description: '(停止下注)',
}, },
revealing: { revealing: {
@@ -431,7 +504,7 @@ export default {
}, },
animal: { animal: {
insufficientBalanceRecharge: '余额不足,请充值', insufficientBalanceRecharge: '余额不足,请充值',
betLimitExceeded: '超过单次投注限额', betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED,
loading: '加载中', loading: '加载中',
selectionLimitReached: '超过可选择字花', selectionLimitReached: '超过可选择字花',
tapToEnter: '点击进入', tapToEnter: '点击进入',
@@ -446,29 +519,29 @@ export default {
orderNo: '订单号', orderNo: '订单号',
roundId: '期号', roundId: '期号',
numbers: '下注号码', numbers: '下注号码',
createdAt: '时间', createdAt: TEXT_TIME,
settledAt: '结算时间', settledAt: '结算时间',
totalPoolAmount: '下注金额', totalPoolAmount: '下注金额',
winningResult: '中奖字花', winningResult: '中奖字花',
payout: '中奖金额', payout: '中奖金额',
empty: '暂无历史记录', empty: '暂无历史记录',
end: '没有更多记录了', end: '没有更多记录了',
loading: '加载中...', loading: TEXT_LOADING,
settled: '已结算', settled: TEXT_SETTLED,
}, },
periodHistory: { periodHistory: {
title: '开奖结果历史', title: '开奖结果历史',
close: '关闭开奖结果历史', close: '关闭开奖结果历史',
empty: '暂无开奖结果', empty: '暂无开奖结果',
failed: '开奖结果加载失败', failed: '开奖结果加载失败',
loading: '加载中...', loading: TEXT_LOADING,
retry: '重试', retry: '重试',
}, },
topup: { topup: {
title: '充值配置', title: '充值配置',
platformCoinLabel: '平台币', platformCoinLabel: '平台币',
currencyLabel: '货币类型', currencyLabel: TEXT_CURRENCY_TYPE,
channelLabel: '支付渠道', channelLabel: TEXT_PAYMENT_CHANNEL,
rateHint: '汇率为参考价格,实际以充值时为准。', rateHint: '汇率为参考价格,实际以充值时为准。',
tier: { tier: {
bonus: '赠送', bonus: '赠送',
@@ -506,13 +579,13 @@ export default {
notice: '注意', notice: '注意',
feeNotice: 'RM10 - RM99.99 之间的交易将收取最低RM 1的提现手续费', feeNotice: 'RM10 - RM99.99 之间的交易将收取最低RM 1的提现手续费',
cancel: '取消', cancel: '取消',
confirm: '确认', confirm: TEXT_CONFIRM,
submitSuccess: '提现申请已提交', submitSuccess: '提现申请已提交',
withdrawal: '提现', withdrawal: '提现',
fields: { fields: {
diamondAmount: '提现钻石数量', diamondAmount: '提现钻石数量',
currencyType: '货币类型', currencyType: TEXT_CURRENCY_TYPE,
paymentChannel: '支付渠道', paymentChannel: TEXT_PAYMENT_CHANNEL,
bankCode: '银行代码', bankCode: '银行代码',
cardHolderName: '持卡人姓名', cardHolderName: '持卡人姓名',
bankAccountNumber: '银行账号', bankAccountNumber: '银行账号',

View File

@@ -1,5 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { AUTO_HOSTING_DEFAULT_SINGLE_WIN_THRESHOLD } from '@/constants'
import type { BetSelection } from '@/features/game/shared' import type { BetSelection } from '@/features/game/shared'
export interface AutoHostingStopRules { export interface AutoHostingStopRules {
@@ -44,7 +45,7 @@ const DEFAULT_AUTO_HOSTING_RULES: AutoHostingStopRules = {
enabled: false, enabled: false,
}, },
stopIfSingleWinAbove: { stopIfSingleWinAbove: {
amount: 50_000, amount: AUTO_HOSTING_DEFAULT_SINGLE_WIN_THRESHOLD,
enabled: false, enabled: false,
}, },
stopOnJackpot: false, stopOnJackpot: false,

View File

@@ -1,5 +1,9 @@
import { create } from 'zustand' import { create } from 'zustand'
import {
CONNECTION_LATENCY_FAIR_MS,
MAX_JACKPOT_BROADCAST_COUNT,
} from '@/constants'
import type { import type {
AnnouncementState, AnnouncementState,
ConnectionState, ConnectionState,
@@ -18,8 +22,6 @@ type GameSessionSlice = Pick<
'announcements' | 'connection' | 'dashboard' 'announcements' | 'connection' | 'dashboard'
> >
const MAX_JACKPOT_BROADCAST_COUNT = 20
export interface JackpotBroadcastItem { export interface JackpotBroadcastItem {
id: string id: string
message: string message: string
@@ -173,7 +175,8 @@ export const selectActiveAnnouncement = (state: GameSessionStoreState) =>
export const selectIsConnectionHealthy = (state: GameSessionStoreState) => export const selectIsConnectionHealthy = (state: GameSessionStoreState) =>
state.connection.status === 'connected' && state.connection.status === 'connected' &&
(state.connection.latencyMs === null || state.connection.latencyMs < 150) (state.connection.latencyMs === null ||
state.connection.latencyMs < CONNECTION_LATENCY_FAIR_MS)
export const selectUnreadAnnouncementCount = (state: GameSessionStoreState) => export const selectUnreadAnnouncementCount = (state: GameSessionStoreState) =>
getUnreadAnnouncementCount(state.announcements) getUnreadAnnouncementCount(state.announcements)