diff --git a/src/constants/api.ts b/src/constants/api.ts new file mode 100644 index 0000000..20451e4 --- /dev/null +++ b/src/constants/api.ts @@ -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' diff --git a/src/constants/auth.ts b/src/constants/auth.ts index b036330..739bbbf 100644 --- a/src/constants/auth.ts +++ b/src/constants/auth.ts @@ -34,3 +34,27 @@ export const AUTH_TOKEN_CACHE_SKEW_MS = 30_000 /** @description 认证错误翻译 key 的统一前缀。 */ 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 diff --git a/src/constants/game.ts b/src/constants/game.ts index 3b568ce..099a58c 100644 --- a/src/constants/game.ts +++ b/src/constants/game.ts @@ -240,6 +240,36 @@ export const ENTRY_NOTICE_CONFIRM_INTERVAL_MS = 24 * 60 * 60 * 1000 /** @description 游戏投注记录每页加载条数。 */ 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 提现页快捷法币金额选项。 */ export const QUICK_FIAT_AMOUNTS = [3, 30, 50, 100, 200, 500] as const diff --git a/src/constants/index.ts b/src/constants/index.ts index a75d233..806caaa 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,3 +1,4 @@ +export * from './api' export * from './auth' export * from './game' export * from './system' diff --git a/src/constants/system.ts b/src/constants/system.ts index f5cfc32..6ee64ac 100644 --- a/src/constants/system.ts +++ b/src/constants/system.ts @@ -48,6 +48,9 @@ export const QUERY_RETRYABLE_STATUS_CODES = [ /** @description 桌面端布局切换起始断点,单位为像素。 */ export const DESKTOP_LAYOUT_MIN_WIDTH_PX = 1024 +/** @description 移动端布局判定断点(最大宽度),单位为像素。 */ +export const MOBILE_LAYOUT_BREAKPOINT_PX = 768 + /** @description 应用支持的语言代码列表。 */ export const SUPPORTED_LANGUAGES = ['zh-CN', 'en-US', 'ms-MY', 'id-ID'] as const diff --git a/src/features/auth/api/auth-api.ts b/src/features/auth/api/auth-api.ts index 2058220..fd3ae94 100644 --- a/src/features/auth/api/auth-api.ts +++ b/src/features/auth/api/auth-api.ts @@ -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 { ApiError } from '@/lib/api/api-error' import type { AuthSessionInput } from '@/store/auth' @@ -34,7 +39,7 @@ function unwrapEnvelope( response: ApiResponse, fallbackErrorKey = 'auth.errors.requestFailed', ) { - if (response.code === 1) { + if (response.code === API_SUCCESS_CODE) { return response.data } @@ -172,7 +177,7 @@ export async function sendSmsCode( AUTH_ENDPOINTS.sendSmsCode, { json: { - event: 'user_register', + event: SMS_SEND_EVENT_REGISTER, mobile: payload.mobile, }, }, diff --git a/src/features/auth/api/types.ts b/src/features/auth/api/types.ts index 6b17863..496f1fe 100644 --- a/src/features/auth/api/types.ts +++ b/src/features/auth/api/types.ts @@ -1,3 +1,4 @@ +import type { SMS_SEND_EVENT_REGISTER } from '@/constants' import type { AuthSessionInput, AuthUser } from '@/store/auth' export interface AuthApiEnvelope { @@ -70,7 +71,7 @@ export interface RegisterRequestDto { } export interface SendSmsCodeRequestDto { - event: 'user_register' + event: typeof SMS_SEND_EVENT_REGISTER mobile: string } diff --git a/src/features/auth/hooks/use-register-form.ts b/src/features/auth/hooks/use-register-form.ts index b0eb823..84c0b69 100644 --- a/src/features/auth/hooks/use-register-form.ts +++ b/src/features/auth/hooks/use-register-form.ts @@ -1,5 +1,9 @@ import { useMutation } from '@tanstack/react-query' import { useForm } from 'react-hook-form' +import { + DEFAULT_REGISTER_INVITE_CODE, + REGISTER_INVITE_CODE_QUERY_PARAM, +} from '@/constants' import i18n from '@/i18n' import { notify } from '@/lib/notify' import { useAuthStore } from '@/store/auth' @@ -15,9 +19,6 @@ interface UseRegisterFormOptions { onSuccess?: () => void } -const REGISTER_INVITE_CODE_QUERY_PARAM = 'registerInviteCode' -const DEFAULT_REGISTER_INVITE_CODE = 'D97DBC16' - function getInitialRegisterInviteCode() { if (typeof window === 'undefined') { return DEFAULT_REGISTER_INVITE_CODE diff --git a/src/features/auth/hooks/use-send-sms-code.ts b/src/features/auth/hooks/use-send-sms-code.ts index 5af0b36..60ef3c6 100644 --- a/src/features/auth/hooks/use-send-sms-code.ts +++ b/src/features/auth/hooks/use-send-sms-code.ts @@ -1,12 +1,11 @@ import { useMutation } from '@tanstack/react-query' import { useEffect, useState } from 'react' +import { SMS_CODE_COOLDOWN_FALLBACK_SECONDS } from '@/constants' 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({ @@ -24,7 +23,7 @@ export function useSendSmsCode() { setRemainingSeconds( result.expiresIn > 0 ? result.expiresIn - : FALLBACK_SMS_CODE_COOLDOWN_SECONDS, + : SMS_CODE_COOLDOWN_FALLBACK_SECONDS, ) notify.success(i18n.t('auth.register.sms.success')) }, diff --git a/src/features/auth/schema/auth-schema.ts b/src/features/auth/schema/auth-schema.ts index d665e17..d75693b 100644 --- a/src/features/auth/schema/auth-schema.ts +++ b/src/features/auth/schema/auth-schema.ts @@ -1,5 +1,11 @@ import { z } from 'zod' +import { + INVITE_CODE_MAX_LENGTH, + PASSWORD_MAX_LENGTH, + PASSWORD_MIN_LENGTH, +} from '@/constants' + const usernameSchema = z .string() .trim() @@ -12,8 +18,8 @@ const captchaSchema = z const passwordSchema = z .string() - .min(6, 'auth.validation.password.min') - .max(32, 'auth.validation.password.max') + .min(PASSWORD_MIN_LENGTH, 'auth.validation.password.min') + .max(PASSWORD_MAX_LENGTH, 'auth.validation.password.max') export const loginFormSchema = z.object({ password: passwordSchema, @@ -28,7 +34,7 @@ export const registerFormSchema = z .string() .trim() .min(1, 'auth.validation.inviteCode.required') - .max(32, 'auth.validation.inviteCode.max'), + .max(INVITE_CODE_MAX_LENGTH, 'auth.validation.inviteCode.max'), password: passwordSchema, mobile: usernameSchema, }) diff --git a/src/features/game/api/finance-api.ts b/src/features/game/api/finance-api.ts index c1ecf55..9501fba 100644 --- a/src/features/game/api/finance-api.ts +++ b/src/features/game/api/finance-api.ts @@ -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 { ApiError } from '@/lib/api/api-error' import type { ApiResponse } from '@/type' @@ -30,7 +34,7 @@ function unwrapFinanceEnvelope( response: ApiResponse, fallbackMessage = 'Finance request failed', ) { - if (response.code === 1) { + if (response.code === API_SUCCESS_CODE) { return response.data } @@ -194,7 +198,7 @@ function normalizeFinanceOrderList(dto: FinanceOrderListDto): FinanceOrderList { list: (dto.list ?? []).map(normalizeFinanceOrderItem), pagination: { 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, }, } @@ -237,7 +241,8 @@ function normalizeWalletRecordList(dto: WalletRecordListDto): WalletRecordList { list: (dto.list ?? []).map(normalizeWalletRecordItem), pagination: { 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, }, } @@ -301,7 +306,7 @@ export async function getDepositOrderList(params?: { { searchParams: { 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: { 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: { page: String(params?.page ?? 1), - page_size: String(params?.pageSize ?? 20), + page_size: String(params?.pageSize ?? DEFAULT_LIST_PAGE_SIZE), type: params?.type ?? 'payout', }, }, diff --git a/src/features/game/api/game-api.ts b/src/features/game/api/game-api.ts index cc9361f..fbf8879 100644 --- a/src/features/game/api/game-api.ts +++ b/src/features/game/api/game-api.ts @@ -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 { ApiError } from '@/lib/api/api-error' import type { ApiResponse } from '@/type' @@ -51,7 +55,7 @@ function unwrapGameEnvelope( response: ApiResponse, fallbackMessage = 'Game request failed', ) { - if (response.code === 1) { + if (response.code === API_SUCCESS_CODE) { return response.data } @@ -424,7 +428,7 @@ export async function getNoticeList(params?: { const response = await api.get(GAME_API_ENDPOINTS.noticeList, { searchParams: { page: String(params?.page ?? 1), - page_size: String(params?.pageSize ?? 20), + page_size: String(params?.pageSize ?? DEFAULT_LIST_PAGE_SIZE), }, }) const dto = unwrapGameEnvelope( @@ -478,7 +482,7 @@ export async function getGameBetMyOrders(params: { { json: { page: params.page ?? 1, - page_size: params.pageSize ?? 20, + page_size: params.pageSize ?? DEFAULT_LIST_PAGE_SIZE, }, }, ) diff --git a/src/features/game/api/period-history-api.ts b/src/features/game/api/period-history-api.ts index 2b033ce..32ddc3f 100644 --- a/src/features/game/api/period-history-api.ts +++ b/src/features/game/api/period-history-api.ts @@ -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 { ApiError } from '@/lib/api/api-error' import type { ApiResponse } from '@/type' @@ -16,7 +16,7 @@ interface GamePeriodHistoryDto { function unwrapPeriodHistoryEnvelope( response: ApiResponse, ) { - if (response.code === 1) { + if (response.code === API_SUCCESS_CODE) { return response.data } diff --git a/src/features/game/entry/entry-page.tsx b/src/features/game/entry/entry-page.tsx index 9ebac3a..64c5782 100644 --- a/src/features/game/entry/entry-page.tsx +++ b/src/features/game/entry/entry-page.tsx @@ -1,6 +1,7 @@ import { startTransition, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import { MOBILE_LAYOUT_BREAKPOINT_PX } from '@/constants' import { getGameLobbyInit } from '@/features/game' import { EntryNoticeGateModal } from '@/features/game/components' import { MobileEntry } from '@/features/game/entry/mobile-entry.tsx' @@ -74,7 +75,8 @@ export function EntryPage() { return false } - return window.matchMedia('(max-width: 768px)').matches + return window.matchMedia(`(max-width: ${MOBILE_LAYOUT_BREAKPOINT_PX}px)`) + .matches }) useDocumentMetadata({ @@ -189,7 +191,9 @@ export function EntryPage() { return } - const mediaQuery = window.matchMedia('(max-width: 768px)') + const mediaQuery = window.matchMedia( + `(max-width: ${MOBILE_LAYOUT_BREAKPOINT_PX}px)`, + ) const syncLayout = (event?: MediaQueryListEvent) => { setIsMobile(event?.matches ?? mediaQuery.matches) } diff --git a/src/features/game/hooks/use-deposit-tier-list.ts b/src/features/game/hooks/use-deposit-tier-list.ts index fce2fb0..1745a8f 100644 --- a/src/features/game/hooks/use-deposit-tier-list.ts +++ b/src/features/game/hooks/use-deposit-tier-list.ts @@ -1,7 +1,10 @@ import { useQuery } from '@tanstack/react-query' 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' export function useDepositTierList() { @@ -12,6 +15,6 @@ export function useDepositTierList() { return useQuery({ queryKey: ['finance', 'deposit-tier-list', language], queryFn: () => getDepositTierList(), - staleTime: 5 * 60 * 1000, + staleTime: FINANCE_CONFIG_QUERY_STALE_TIME_MS, }) } diff --git a/src/features/game/hooks/use-deposit-withdraw-config.ts b/src/features/game/hooks/use-deposit-withdraw-config.ts index e51d1fc..2cae161 100644 --- a/src/features/game/hooks/use-deposit-withdraw-config.ts +++ b/src/features/game/hooks/use-deposit-withdraw-config.ts @@ -1,7 +1,10 @@ import { useQuery } from '@tanstack/react-query' 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' export function useDepositWithdrawConfig() { @@ -12,6 +15,6 @@ export function useDepositWithdrawConfig() { return useQuery({ queryKey: ['finance', 'deposit-withdraw-config', language], queryFn: () => getDepositWithdrawConfig(), - staleTime: 5 * 60 * 1000, + staleTime: FINANCE_CONFIG_QUERY_STALE_TIME_MS, }) } diff --git a/src/features/game/hooks/use-finance-records-vm.ts b/src/features/game/hooks/use-finance-records-vm.ts index 4f73617..72d4687 100644 --- a/src/features/game/hooks/use-finance-records-vm.ts +++ b/src/features/game/hooks/use-finance-records-vm.ts @@ -2,12 +2,11 @@ import { useInfiniteQuery } from '@tanstack/react-query' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { DEFAULT_LIST_PAGE_SIZE } from '@/constants' import { getDepositOrderList, getWithdrawOrderList } from '@/features/game/api' export type FinanceRecordType = 'deposit' | 'withdraw' -const FINANCE_RECORD_PAGE_SIZE = 20 - const FINANCE_RECORD_TYPE_OPTIONS: Array<{ key: FinanceRecordType labelKey: string @@ -46,11 +45,11 @@ export function useFinanceRecordsVm({ enabled }: { enabled: boolean }) { recordType === 'deposit' ? getDepositOrderList({ page: pageParam, - pageSize: FINANCE_RECORD_PAGE_SIZE, + pageSize: DEFAULT_LIST_PAGE_SIZE, }) : getWithdrawOrderList({ page: pageParam, - pageSize: FINANCE_RECORD_PAGE_SIZE, + pageSize: DEFAULT_LIST_PAGE_SIZE, }), enabled, getNextPageParam: (lastPage) => { diff --git a/src/features/game/hooks/use-header-vm.ts b/src/features/game/hooks/use-header-vm.ts index c49a51d..934df6a 100644 --- a/src/features/game/hooks/use-header-vm.ts +++ b/src/features/game/hooks/use-header-vm.ts @@ -1,4 +1,9 @@ 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 { isDesktopFullscreen, @@ -74,7 +79,7 @@ function resolveSignalPresentation(input: { } satisfies SignalPresentation } - if (input.latencyMs <= 80) { + if (input.latencyMs <= CONNECTION_LATENCY_GOOD_MS) { return { activeBars: 4, latencyLabel: String(input.latencyMs), @@ -82,7 +87,7 @@ function resolveSignalPresentation(input: { } satisfies SignalPresentation } - if (input.latencyMs <= 150) { + if (input.latencyMs <= CONNECTION_LATENCY_FAIR_MS) { return { activeBars: 3, latencyLabel: String(input.latencyMs), @@ -90,7 +95,7 @@ function resolveSignalPresentation(input: { } satisfies SignalPresentation } - if (input.latencyMs <= 300) { + if (input.latencyMs <= CONNECTION_LATENCY_POOR_MS) { return { activeBars: 2, latencyLabel: String(input.latencyMs), diff --git a/src/features/game/hooks/use-wallet-records-vm.ts b/src/features/game/hooks/use-wallet-records-vm.ts index 7c958ee..6827002 100644 --- a/src/features/game/hooks/use-wallet-records-vm.ts +++ b/src/features/game/hooks/use-wallet-records-vm.ts @@ -3,9 +3,9 @@ import dayjs from 'dayjs' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { DEFAULT_LIST_PAGE_SIZE } from '@/constants' import { getWalletRecordList } from '@/features/game/api' -const WALLET_RECORD_PAGE_SIZE = 20 const WALLET_RECORD_TYPE = 'payout' function formatWalletAmount(value: string, locale: string) { @@ -47,7 +47,7 @@ export function useWalletRecordsVm({ enabled }: { enabled: boolean }) { queryFn: ({ pageParam }) => getWalletRecordList({ page: pageParam, - pageSize: WALLET_RECORD_PAGE_SIZE, + pageSize: DEFAULT_LIST_PAGE_SIZE, type: WALLET_RECORD_TYPE, }), enabled, diff --git a/src/features/game/hooks/use-withdraw-vm.ts b/src/features/game/hooks/use-withdraw-vm.ts index 3e560ab..8b279ab 100644 --- a/src/features/game/hooks/use-withdraw-vm.ts +++ b/src/features/game/hooks/use-withdraw-vm.ts @@ -1,7 +1,13 @@ import { useCallback, useEffect, useMemo, useState } from 'react' 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 { useDepositWithdrawConfig } from '@/features/game/hooks/use-deposit-withdraw-config' import { useAuthStore } from '@/store' @@ -47,7 +53,7 @@ function isValidEmail(value: string) { return false } - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()) + return WITHDRAW_EMAIL_PATTERN.test(value.trim()) } function isValidPhone(value: string) { @@ -57,7 +63,7 @@ function isValidPhone(value: string) { return false } - return /^\+?\d{6,20}$/.test(normalized) + return WITHDRAW_PHONE_PATTERN.test(normalized) } export function useWithdrawVm() { @@ -88,7 +94,7 @@ export function useWithdrawVm() { const [amount, setAmountState] = useState(0) const [hasInitializedAmount, setHasInitializedAmount] = useState(false) const [currencyCode, setCurrencyCode] = useState( - config.currencies[0]?.code ?? 'MYR', + config.currencies[0]?.code ?? DEFAULT_CURRENCY_CODE, ) const [paymentChannelCode, setPaymentChannelCode] = useState('') const [bankCode, setBankCode] = useState('') @@ -207,7 +213,7 @@ export function useWithdrawVm() { }, [locale, maxWithdrawAmount, selectedCurrency.code, selectedRate]) const resetForm = useCallback(() => { - const nextCurrencyCode = config.currencies[0]?.code ?? 'MYR' + const nextCurrencyCode = config.currencies[0]?.code ?? DEFAULT_CURRENCY_CODE const nextCurrency = getActiveCurrencyCode( config.currencies, nextCurrencyCode, diff --git a/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx b/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx index 817a274..fa4e9c6 100644 --- a/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx +++ b/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx @@ -5,6 +5,7 @@ import { CenterModal } from '@/components/center-modal.tsx' import { SmartBackground } from '@/components/smart-background.tsx' import { Input } from '@/components/ui/input.tsx' import { Switch } from '@/components/ui/switch.tsx' +import { AUTO_HOSTING_DEFAULT_SINGLE_WIN_THRESHOLD } from '@/constants' import { notify } from '@/lib/notify' import { useModalStore } from '@/store' import { useAuthStore } from '@/store/auth' @@ -51,7 +52,9 @@ function DesktopAutoSettingModal() { const [balanceLimitEnabled, setBalanceLimitEnabled] = useState(false) const [balanceLimitValue, setBalanceLimitValue] = useState('0') 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) function handleClose() { diff --git a/src/features/game/modal/desktop/desktop-userInfo-modal.tsx b/src/features/game/modal/desktop/desktop-userInfo-modal.tsx index 4c443b2..8953807 100644 --- a/src/features/game/modal/desktop/desktop-userInfo-modal.tsx +++ b/src/features/game/modal/desktop/desktop-userInfo-modal.tsx @@ -15,6 +15,7 @@ import userInfoBg from '@/assets/system/userInfo-bg.webp' import { CenterModal } from '@/components/center-modal.tsx' import { SmartBackground } from '@/components/smart-background.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 DesktopFinanceRecordsTab from '@/features/game/modal/desktop/desktop-finance-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) { const url = new URL(window.location.href) diff --git a/src/lib/api/api-client.ts b/src/lib/api/api-client.ts index edcbd8b..f270fd4 100644 --- a/src/lib/api/api-client.ts +++ b/src/lib/api/api-client.ts @@ -2,14 +2,19 @@ import ky, { HTTPError, type Options, TimeoutError } from 'ky' import { ACCESS_TOKEN_REFRESH_SKEW_MS, API_ERROR_MESSAGES, + API_SUCCESS_CODE, AUTH_REFRESH_ATTEMPTED_CONTEXT_KEY, AUTH_REFRESH_ENDPOINT, AUTH_RELOGIN_REQUIRED_CODES, AUTH_SKIP_REFRESH_CONTEXT_KEY, AUTH_TOKEN_CACHE_SKEW_MS, AUTH_TOKEN_ENDPOINT, + AUTHORIZATION_BEARER_PREFIX, + CONTENT_TYPE_JSON, DEFAULT_REQUEST_ACCEPT_HEADER, DEFAULT_REQUEST_TIMEOUT_MS, + HTTP_STATUS, + REQUEST_HEADERS, } from '@/constants' import type { AuthTokenDto } from '@/features/auth/api/types' import { getPreferredLanguage, isSupportedLanguage } from '@/i18n' @@ -65,13 +70,13 @@ function normalizeApiBaseUrl(baseUrl: string | undefined) { } async function parseResponseBody(response: Response) { - if (response.status === 204) { + if (response.status === HTTP_STATUS.noContent) { return null } const contentType = response.headers.get('content-type') ?? '' - if (contentType.includes('application/json')) { + if (contentType.includes(CONTENT_TYPE_JSON)) { return response.json() } @@ -110,7 +115,7 @@ async function toApiError(error: unknown) { if (error instanceof TimeoutError) { return new ApiError({ message: API_ERROR_MESSAGES.timeout, - status: 408, + status: HTTP_STATUS.requestTimeout, }) } @@ -143,14 +148,20 @@ const apiClient = ky.create({ hooks: { beforeRequest: [ ({ request }) => { - request.headers.set('Accept', DEFAULT_REQUEST_ACCEPT_HEADER) - request.headers.set('lang', getRequestLanguage()) + request.headers.set( + REQUEST_HEADERS.accept, + DEFAULT_REQUEST_ACCEPT_HEADER, + ) + request.headers.set(REQUEST_HEADERS.lang, getRequestLanguage()) const token = useAuthStore.getState().accessToken if (token) { - request.headers.set('Authorization', `Bearer ${token}`) - request.headers.set('user-token', token) + request.headers.set( + REQUEST_HEADERS.authorization, + `${AUTHORIZATION_BEARER_PREFIX}${token}`, + ) + request.headers.set(REQUEST_HEADERS.userToken, token) } if (shouldLogRequests) { @@ -222,14 +233,14 @@ function assertValidAuthEnvelope(data: unknown) { throw new ApiError({ data, message: getApiEnvelopeMessage(data), - status: 401, + status: HTTP_STATUS.unauthorized, }) } function unwrapEnvelopeData(response: ApiResponse) { assertValidAuthEnvelope(response) - if (response.code === 1) { + if (response.code === API_SUCCESS_CODE) { return response.data } @@ -333,8 +344,8 @@ function createHeaders(headersInit?: Options['headers']) { async function buildRequestOptions(input: string, options?: Options) { const headers = createHeaders(options?.headers) - if (shouldAttachAuthToken(input) && !headers.has('auth-token')) { - headers.set('auth-token', await fetchAuthToken()) + if (shouldAttachAuthToken(input) && !headers.has(REQUEST_HEADERS.authToken)) { + headers.set(REQUEST_HEADERS.authToken, await fetchAuthToken()) } return { @@ -361,7 +372,7 @@ async function request(input: string, options?: Options) { } catch (error) { if ( error instanceof HTTPError && - error.response.status === 401 && + error.response.status === HTTP_STATUS.unauthorized && input !== AUTH_REFRESH_ENDPOINT && options?.context?.[AUTH_SKIP_REFRESH_CONTEXT_KEY] !== true && options?.context?.[AUTH_REFRESH_ATTEMPTED_CONTEXT_KEY] !== true @@ -379,7 +390,10 @@ async function request(input: string, options?: Options) { } } - if (error instanceof HTTPError && error.response.status === 401) { + if ( + error instanceof HTTPError && + error.response.status === HTTP_STATUS.unauthorized + ) { handleUnauthorizedSession() } diff --git a/src/lib/auth/auth-session.ts b/src/lib/auth/auth-session.ts index 13b5b15..2e1eb58 100644 --- a/src/lib/auth/auth-session.ts +++ b/src/lib/auth/auth-session.ts @@ -1,3 +1,4 @@ +import { LOGIN_PROMPT_DEDUP_MS } from '@/constants' import i18n from '@/i18n' import { notify } from '@/lib/notify' import { queryClient } from '@/lib/query/query-client' @@ -16,8 +17,6 @@ let authInitializationPromise: Promise | null = null let refreshSessionPromise: Promise | null = null let lastLoginPromptAt = 0 -const LOGIN_PROMPT_DEDUP_MS = 1200 - interface ClearAuthenticatedSessionOptions { clearBrowserStorage?: boolean clearQueryCache?: boolean diff --git a/src/locales/en-US/common.ts b/src/locales/en-US/common.ts index 297ea19..e61118c 100644 --- a/src/locales/en-US/common.ts +++ b/src/locales/en-US/common.ts @@ -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 { nav: { home: 'Home', @@ -88,8 +161,8 @@ export default { }, phases: { betting: 'Betting', - locked: 'Locked', - settled: 'Settled', + locked: TEXT_LOCKED, + settled: TEXT_SETTLED, }, roundBettingStart: { title: 'Round {{roundId}}', @@ -99,8 +172,8 @@ export default { unifiedBetHint: 'Unified bet', totalBet: 'Total bet', canBet: 'Can bet', - yes: 'Yes', - no: 'No', + yes: TEXT_YES, + no: TEXT_NO, quickBet: 'Quick bet 08', clearPending: 'Clear pending', autoModeDemo: 'Auto mode demo', @@ -108,16 +181,16 @@ export default { }, modals: { login: { - title: 'Login', + title: TEXT_LOGIN, }, register: { - title: 'Register', + title: TEXT_REGISTER, }, notice: { title: 'Event Notice', 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.', - check: 'View', + check: TEXT_VIEW, }, entryNotice: { title: 'Site Notice', @@ -142,7 +215,7 @@ export default { topup: 'Top Up', }, autoSetting: { - title: 'Auto Spin', + title: TEXT_AUTO_HOSTING, startAutoSpin: 'Start Auto Spin', rows: { stopIfBalanceLowerThan: 'Stop if balance is lower than', @@ -155,8 +228,8 @@ export default { tabs: { profile: 'Profile', financeRecords: 'Top Up / Withdraw Records', - walletRecords: 'Wallet Records', - message: 'Messages', + walletRecords: TEXT_WALLET_RECORDS, + message: TEXT_SITE_MESSAGES, }, profile: { 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.', }, message: { - title: 'Messages', + title: TEXT_SITE_MESSAGES, back: 'Back', loading: 'Loading messages...', loadFailed: 'Failed to load messages. Please try again later.', @@ -178,7 +251,7 @@ export default { unread: 'Unread', eventBonus: '[Top-up Bonus Event] From October 1 to October 7, 2026, claim your rebate rewards...', - check: 'View', + check: TEXT_VIEW, deleteRecords: 'Delete records', }, financeRecords: { @@ -190,9 +263,9 @@ export default { loading: 'Loading records...', loadFailed: 'Failed to load records. Please try again later.', empty: 'No records yet', - page: 'Page {{page}} / {{total}} total', - previous: 'Previous', - next: 'Next', + page: TEXT_PAGE_INDICATOR, + previous: TEXT_PREV_PAGE, + next: TEXT_NEXT_PAGE, }, walletRecords: { amount: 'Amount', @@ -201,12 +274,12 @@ export default { empty: 'No wallet records yet', loadFailed: 'Failed to load wallet records. Please try again later.', loading: 'Loading wallet records...', - next: 'Next', - page: 'Page {{page}} / {{total}} total', - previous: 'Previous', + next: TEXT_NEXT_PAGE, + page: TEXT_PAGE_INDICATOR, + previous: TEXT_PREV_PAGE, remark: 'Remark', - time: 'Time', - type: 'Wallet Records', + time: TEXT_TIME, + type: TEXT_WALLET_RECORDS, }, }, withdrawTopup: { @@ -238,8 +311,8 @@ export default { dialog: { close: 'Close alert', confirm: 'OK', - no: 'No', - yes: 'Yes', + no: TEXT_NO, + yes: TEXT_YES, }, modal: { close: 'Close modal', @@ -256,7 +329,7 @@ export default { inviteLinkCopyFailed: 'Failed to copy invite link. Please copy it manually.', 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', betPlaced: 'Bet placed successfully', noRecentSuccessfulBet: @@ -282,7 +355,7 @@ export default { common: { arrowIconAlt: 'Arrow', actions: { - submitting: 'Submitting...', + submitting: TEXT_SUBMITTING, }, passwordVisibility: { hide: 'Hide password', @@ -295,12 +368,12 @@ export default { }, fields: { username: { - label: 'Mobile:', - placeholder: 'Enter mobile number', + label: TEXT_MOBILE_LABEL, + placeholder: TEXT_MOBILE_PLACEHOLDER, }, password: { - label: 'Password:', - placeholder: 'Enter password', + label: TEXT_PASSWORD_LABEL, + placeholder: TEXT_PASSWORD_PLACEHOLDER, }, }, footer: { @@ -314,20 +387,20 @@ export default { }, register: { actions: { - submit: 'Register', + submit: TEXT_REGISTER, }, fields: { mobile: { - label: 'Mobile:', - placeholder: 'Enter mobile number', + label: TEXT_MOBILE_LABEL, + placeholder: TEXT_MOBILE_PLACEHOLDER, }, captcha: { label: 'Code:', placeholder: 'Enter verification code', }, password: { - label: 'Password:', - placeholder: 'Enter password', + label: TEXT_PASSWORD_LABEL, + placeholder: TEXT_PASSWORD_PLACEHOLDER, }, confirmPassword: { label: 'Confirm Password:', @@ -393,23 +466,23 @@ export default { bgm: 'BGM', id: 'ID', fullscreen: 'Full Screen', - login: 'Login', - register: 'Register', + login: TEXT_LOGIN, + register: TEXT_REGISTER, }, control: { trend: 'Trend', map: 'Map', selected: 'Selected', totalBet: 'Total Bet', - confirm: 'Confirm', + confirm: TEXT_CONFIRM, selectNumbers: 'Select Numbers', insufficientBalance: 'Insufficient Balance', betLimitExceeded: 'Limit Exceeded', - submitting: 'Submitting...', + submitting: TEXT_SUBMITTING, actions: { clear: 'Clear', repeat: 'Repeat', - 'auto-spin': 'Auto Spin', + 'auto-spin': TEXT_AUTO_HOSTING, }, }, status: { @@ -423,7 +496,7 @@ export default { description: '(Accepting Bets)', }, locked: { - label: 'Locked', + label: TEXT_LOCKED, description: '(Betting Closed)', }, revealing: { @@ -445,7 +518,7 @@ export default { }, animal: { insufficientBalanceRecharge: 'Insufficient balance, please top up', - betLimitExceeded: 'Single bet limit exceeded', + betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED, loading: 'Loading', selectionLimitReached: 'Selection limit exceeded', tapToEnter: 'Tap To Enter', @@ -460,29 +533,29 @@ export default { orderNo: 'Order No.', roundId: 'Round ID', numbers: 'Bet Numbers', - createdAt: 'Time', + createdAt: TEXT_TIME, settledAt: 'Settled At', totalPoolAmount: 'Bet Amount', winningResult: 'Winning Result', payout: 'Win Amount', empty: 'No history yet', end: 'No more records', - loading: 'Loading...', - settled: 'Settled', + loading: TEXT_LOADING, + settled: TEXT_SETTLED, }, periodHistory: { title: 'Draw Result History', close: 'Close draw result history', empty: 'No draw results yet', failed: 'Failed to load draw results', - loading: 'Loading...', + loading: TEXT_LOADING, retry: 'Retry', }, topup: { title: 'Top-up Config', platformCoinLabel: 'Platform Coin', - currencyLabel: 'Currency Type', - channelLabel: 'Payment Channel', + currencyLabel: TEXT_CURRENCY_TYPE, + channelLabel: TEXT_PAYMENT_CHANNEL, rateHint: 'Exchange rates are for reference only. The final amount follows the top-up-time rate.', tier: { @@ -524,13 +597,13 @@ export default { feeNotice: 'Transactions between RM10 and RM99.99 will be charged a minimum withdrawal fee of RM 1.', cancel: 'Cancel', - confirm: 'Confirm', + confirm: TEXT_CONFIRM, submitSuccess: 'Withdrawal request submitted', withdrawal: 'Withdrawal', fields: { diamondAmount: 'Withdrawal Diamond Amount', - currencyType: 'Currency Type', - paymentChannel: 'Payment Channel', + currencyType: TEXT_CURRENCY_TYPE, + paymentChannel: TEXT_PAYMENT_CHANNEL, bankCode: 'Bank Code', cardHolderName: 'Card Holder Name', bankAccountNumber: 'Bank Account Number', diff --git a/src/locales/id-ID/common.ts b/src/locales/id-ID/common.ts index ef66e8a..212407c 100644 --- a/src/locales/id-ID/common.ts +++ b/src/locales/id-ID/common.ts @@ -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 { nav: { home: 'Beranda', @@ -87,8 +160,8 @@ export default { }, phases: { betting: 'Betting', - locked: 'Terkunci', - settled: 'Selesai', + locked: TEXT_LOCKED, + settled: TEXT_SETTLED, }, roundBettingStart: { title: 'Ronde {{roundId}}', @@ -98,8 +171,8 @@ export default { unifiedBetHint: 'Bet seragam', totalBet: 'Total bet', canBet: 'Bisa bet', - yes: 'Ya', - no: 'Tidak', + yes: TEXT_YES, + no: TEXT_NO, quickBet: 'Quick bet 08', clearPending: 'Hapus pending', autoModeDemo: 'Demo mode auto', @@ -107,16 +180,16 @@ export default { }, modals: { login: { - title: 'Masuk', + title: TEXT_LOGIN, }, register: { - title: 'Daftar', + title: TEXT_REGISTER, }, notice: { title: 'Pengumuman Acara', 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.', - check: 'Lihat', + check: TEXT_VIEW, }, entryNotice: { title: 'Pengumuman Situs', @@ -141,7 +214,7 @@ export default { topup: 'Isi Ulang', }, autoSetting: { - title: 'Auto Spin', + title: TEXT_AUTO_HOSTING, startAutoSpin: 'Mulai Auto Spin', rows: { stopIfBalanceLowerThan: 'Berhenti jika saldo lebih rendah dari', @@ -154,8 +227,8 @@ export default { tabs: { profile: 'Profil', financeRecords: 'Riwayat Isi Ulang / Penarikan', - walletRecords: 'Riwayat Dompet', - message: 'Pesan', + walletRecords: TEXT_WALLET_RECORDS, + message: TEXT_SITE_MESSAGES, }, profile: { name: 'Nama', @@ -168,7 +241,7 @@ export default { 'Tanda tanganku seunik diriku. Bagian ini nantinya akan menampilkan ringkasan profil yang sebenarnya.', }, message: { - title: 'Pesan', + title: TEXT_SITE_MESSAGES, back: 'Kembali', loading: 'Memuat pesan...', loadFailed: 'Gagal memuat pesan. Silakan coba lagi nanti.', @@ -177,7 +250,7 @@ export default { unread: 'Belum dibaca', eventBonus: '[Event Bonus Isi Ulang] Dari 1 Oktober hingga 7 Oktober 2026, klaim hadiah rebate kamu...', - check: 'Lihat', + check: TEXT_VIEW, deleteRecords: 'Hapus riwayat', }, financeRecords: { @@ -189,9 +262,9 @@ export default { loading: 'Memuat riwayat...', loadFailed: 'Gagal memuat riwayat. Silakan coba lagi nanti.', empty: 'Belum ada riwayat', - page: 'Halaman {{page}} / total {{total}}', - previous: 'Sebelumnya', - next: 'Berikutnya', + page: TEXT_PAGE_INDICATOR, + previous: TEXT_PREV_PAGE, + next: TEXT_NEXT_PAGE, }, walletRecords: { amount: 'Jumlah', @@ -200,12 +273,12 @@ export default { empty: 'Belum ada riwayat dompet', loadFailed: 'Gagal memuat riwayat dompet. Silakan coba lagi nanti.', loading: 'Memuat riwayat dompet...', - next: 'Berikutnya', - page: 'Halaman {{page}} / total {{total}}', - previous: 'Sebelumnya', + next: TEXT_NEXT_PAGE, + page: TEXT_PAGE_INDICATOR, + previous: TEXT_PREV_PAGE, remark: 'Catatan', - time: 'Waktu', - type: 'Riwayat Dompet', + time: TEXT_TIME, + type: TEXT_WALLET_RECORDS, }, }, withdrawTopup: { @@ -237,8 +310,8 @@ export default { dialog: { close: 'Tutup notifikasi', confirm: 'OK', - no: 'Tidak', - yes: 'Ya', + no: TEXT_NO, + yes: TEXT_YES, }, modal: { close: 'Tutup modal', @@ -255,7 +328,7 @@ export default { inviteLinkCopyFailed: 'Gagal menyalin tautan undangan. Silakan salin secara manual.', insufficientBalance: 'Saldo tidak cukup. Silakan sesuaikan taruhan.', - betLimitExceeded: 'Melebihi batas taruhan tunggal', + betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED, betUnavailable: 'Taruhan tidak tersedia untuk ronde ini', betPlaced: 'Taruhan berhasil dikirim', noRecentSuccessfulBet: @@ -282,7 +355,7 @@ export default { common: { arrowIconAlt: 'Panah', actions: { - submitting: 'Mengirim...', + submitting: TEXT_SUBMITTING, }, passwordVisibility: { hide: 'Sembunyikan kata sandi', @@ -291,16 +364,16 @@ export default { }, login: { actions: { - submit: 'Masuk', + submit: TEXT_LOGIN, }, fields: { username: { - label: 'Nomor Ponsel:', - placeholder: 'Masukkan nomor ponsel', + label: TEXT_MOBILE_LABEL, + placeholder: TEXT_MOBILE_PLACEHOLDER, }, password: { - label: 'Kata Sandi:', - placeholder: 'Masukkan kata sandi', + label: TEXT_PASSWORD_LABEL, + placeholder: TEXT_PASSWORD_PLACEHOLDER, }, }, footer: { @@ -314,20 +387,20 @@ export default { }, register: { actions: { - submit: 'Daftar', + submit: TEXT_REGISTER, }, fields: { mobile: { - label: 'Nomor Ponsel:', - placeholder: 'Masukkan nomor ponsel', + label: TEXT_MOBILE_LABEL, + placeholder: TEXT_MOBILE_PLACEHOLDER, }, captcha: { label: 'Kode:', placeholder: 'Masukkan kode verifikasi', }, password: { - label: 'Kata Sandi:', - placeholder: 'Masukkan kata sandi', + label: TEXT_PASSWORD_LABEL, + placeholder: TEXT_PASSWORD_PLACEHOLDER, }, confirmPassword: { label: 'Konfirmasi Kata Sandi:', @@ -352,7 +425,7 @@ export default { submitFailed: 'Gagal mengirim kode. Silakan coba lagi nanti.', }, send: 'Ambil kode', - sending: 'Mengirim...', + sending: TEXT_SUBMITTING, success: 'Kode verifikasi telah dikirim.', }, }, @@ -389,27 +462,27 @@ export default { header: { systemTime: 'Waktu Sistem', rules: 'Aturan', - message: 'Pesan', + message: TEXT_SITE_MESSAGES, bgm: 'BGM', id: 'ID', fullscreen: 'Layar', - login: 'Masuk', - register: 'Daftar', + login: TEXT_LOGIN, + register: TEXT_REGISTER, }, control: { trend: 'Tren', map: 'Peta', selected: 'Dipilih', totalBet: 'Total Bet', - confirm: 'Konfirmasi', + confirm: TEXT_CONFIRM, selectNumbers: 'Pilih Nombor', insufficientBalance: 'Saldo Tidak Cukup', betLimitExceeded: 'Batas Terlampaui', - submitting: 'Mengirim...', + submitting: TEXT_SUBMITTING, actions: { clear: 'Hapus', repeat: 'Ulang', - 'auto-spin': 'Auto Spin', + 'auto-spin': TEXT_AUTO_HOSTING, }, }, status: { @@ -423,7 +496,7 @@ export default { description: '(Menerima Bet)', }, locked: { - label: 'Terkunci', + label: TEXT_LOCKED, description: '(Bet Ditutup)', }, revealing: { @@ -445,7 +518,7 @@ export default { }, animal: { insufficientBalanceRecharge: 'Saldo tidak cukup, silakan isi ulang', - betLimitExceeded: 'Melebihi batas taruhan tunggal', + betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED, loading: 'Memuat', selectionLimitReached: 'Melebihi pilihan yang diizinkan', tapToEnter: 'Ketuk Untuk Masuk', @@ -460,29 +533,29 @@ export default { orderNo: 'No. Order', roundId: 'ID Ronde', numbers: 'Nomor Taruhan', - createdAt: 'Waktu', + createdAt: TEXT_TIME, settledAt: 'Waktu Selesai', totalPoolAmount: 'Jumlah Taruhan', winningResult: 'Hasil Menang', payout: 'Jumlah Menang', empty: 'Belum ada riwayat', end: 'Tidak ada catatan lagi', - loading: 'Memuat...', - settled: 'Selesai', + loading: TEXT_LOADING, + settled: TEXT_SETTLED, }, periodHistory: { title: 'Riwayat Hasil Undian', close: 'Tutup riwayat hasil undian', empty: 'Belum ada hasil undian', failed: 'Gagal memuat hasil undian', - loading: 'Memuat...', + loading: TEXT_LOADING, retry: 'Coba lagi', }, topup: { title: 'Konfigurasi Isi Ulang', platformCoinLabel: 'Koin Platform', - currencyLabel: 'Jenis Mata Uang', - channelLabel: 'Saluran Pembayaran', + currencyLabel: TEXT_CURRENCY_TYPE, + channelLabel: TEXT_PAYMENT_CHANNEL, rateHint: 'Kurs hanya sebagai referensi. Jumlah akhir mengikuti kurs saat isi ulang.', tier: { @@ -525,13 +598,13 @@ export default { feeNotice: 'Transaksi antara RM10 dan RM99.99 akan dikenakan biaya penarikan minimum RM 1.', cancel: 'Batal', - confirm: 'Konfirmasi', + confirm: TEXT_CONFIRM, submitSuccess: 'Permintaan penarikan berhasil dikirim', withdrawal: 'Penarikan', fields: { diamondAmount: 'Jumlah Berlian Penarikan', - currencyType: 'Jenis Mata Uang', - paymentChannel: 'Saluran Pembayaran', + currencyType: TEXT_CURRENCY_TYPE, + paymentChannel: TEXT_PAYMENT_CHANNEL, bankCode: 'Kode Bank', cardHolderName: 'Nama Pemilik Rekening', bankAccountNumber: 'Nomor Rekening Bank', diff --git a/src/locales/ms-MY/common.ts b/src/locales/ms-MY/common.ts index 2c226fa..3e14fc7 100644 --- a/src/locales/ms-MY/common.ts +++ b/src/locales/ms-MY/common.ts @@ -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 { nav: { home: 'Laman Utama', @@ -90,8 +163,8 @@ export default { }, phases: { betting: 'Taruhan', - locked: 'Dikunci', - settled: 'Selesai', + locked: TEXT_LOCKED, + settled: TEXT_SETTLED, }, roundBettingStart: { title: 'Pusingan {{roundId}}', @@ -101,8 +174,8 @@ export default { unifiedBetHint: 'Taruhan seragam', totalBet: 'Jumlah taruhan', canBet: 'Boleh taruhan', - yes: 'Ya', - no: 'Tidak', + yes: TEXT_YES, + no: TEXT_NO, quickBet: 'Taruhan cepat 08', clearPending: 'Kosongkan belum sah', autoModeDemo: 'Demo mod auto', @@ -110,16 +183,16 @@ export default { }, modals: { login: { - title: 'Log Masuk', + title: TEXT_LOGIN, }, register: { - title: 'Daftar', + title: TEXT_REGISTER, }, notice: { title: 'Notis Acara', 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.', - check: 'Semak', + check: TEXT_VIEW, }, entryNotice: { title: 'Notis Laman', @@ -144,7 +217,7 @@ export default { topup: 'Tambah Nilai', }, autoSetting: { - title: 'Putaran Auto', + title: TEXT_AUTO_HOSTING, startAutoSpin: 'Mula Putaran Auto', rows: { stopIfBalanceLowerThan: 'Henti jika baki lebih rendah daripada', @@ -157,8 +230,8 @@ export default { tabs: { profile: 'Profil', financeRecords: 'Rekod Tambah Nilai / Pengeluaran', - walletRecords: 'Rekod Dompet', - message: 'Mesej', + walletRecords: TEXT_WALLET_RECORDS, + message: TEXT_SITE_MESSAGES, }, profile: { name: 'Nama', @@ -171,7 +244,7 @@ export default { 'Tandatangan saya unik seperti personaliti saya. Bahagian ini akan memaparkan ringkasan profil sebenar kemudian.', }, message: { - title: 'Mesej', + title: TEXT_SITE_MESSAGES, back: 'Kembali', loading: 'Memuatkan mesej...', loadFailed: 'Gagal memuatkan mesej. Sila cuba lagi kemudian.', @@ -180,7 +253,7 @@ export default { unread: 'Belum dibaca', eventBonus: '[Acara Bonus Tambah Nilai] Dari 1 Oktober hingga 7 Oktober 2026, tuntut ganjaran rebat anda...', - check: 'Semak', + check: TEXT_VIEW, deleteRecords: 'Padam rekod', }, financeRecords: { @@ -192,9 +265,9 @@ export default { loading: 'Memuatkan rekod...', loadFailed: 'Gagal memuatkan rekod. Sila cuba lagi kemudian.', empty: 'Belum ada rekod', - page: 'Halaman {{page}} / jumlah {{total}}', - previous: 'Sebelumnya', - next: 'Seterusnya', + page: TEXT_PAGE_INDICATOR, + previous: TEXT_PREV_PAGE, + next: TEXT_NEXT_PAGE, }, walletRecords: { amount: 'Jumlah', @@ -203,12 +276,12 @@ export default { empty: 'Belum ada rekod dompet', loadFailed: 'Gagal memuatkan rekod dompet. Sila cuba lagi kemudian.', loading: 'Memuatkan rekod dompet...', - next: 'Seterusnya', - page: 'Halaman {{page}} / jumlah {{total}}', - previous: 'Sebelumnya', + next: TEXT_NEXT_PAGE, + page: TEXT_PAGE_INDICATOR, + previous: TEXT_PREV_PAGE, remark: 'Catatan', - time: 'Masa', - type: 'Rekod Dompet', + time: TEXT_TIME, + type: TEXT_WALLET_RECORDS, }, }, withdrawTopup: { @@ -240,8 +313,8 @@ export default { dialog: { close: 'Tutup notifikasi', confirm: 'OK', - no: 'Tidak', - yes: 'Ya', + no: TEXT_NO, + yes: TEXT_YES, }, modal: { close: 'Tutup modal', @@ -259,7 +332,7 @@ export default { inviteLinkCopyFailed: 'Gagal menyalin pautan jemputan. Sila salin secara manual.', insufficientBalance: 'Baki tidak mencukupi. Sila laraskan taruhan.', - betLimitExceeded: 'Melebihi had taruhan tunggal', + betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED, betUnavailable: 'Taruhan tidak tersedia untuk pusingan ini', betPlaced: 'Taruhan berjaya dihantar', noRecentSuccessfulBet: @@ -287,7 +360,7 @@ export default { common: { arrowIconAlt: 'Anak panah', actions: { - submitting: 'Menghantar...', + submitting: TEXT_SUBMITTING, }, passwordVisibility: { hide: 'Sembunyikan kata laluan', @@ -296,16 +369,16 @@ export default { }, login: { actions: { - submit: 'Log Masuk', + submit: TEXT_LOGIN, }, fields: { username: { - label: 'Nombor Telefon:', - placeholder: 'Masukkan nombor telefon', + label: TEXT_MOBILE_LABEL, + placeholder: TEXT_MOBILE_PLACEHOLDER, }, password: { - label: 'Kata Laluan:', - placeholder: 'Masukkan kata laluan', + label: TEXT_PASSWORD_LABEL, + placeholder: TEXT_PASSWORD_PLACEHOLDER, }, }, footer: { @@ -319,20 +392,20 @@ export default { }, register: { actions: { - submit: 'Daftar', + submit: TEXT_REGISTER, }, fields: { mobile: { - label: 'Nombor Telefon:', - placeholder: 'Masukkan nombor telefon', + label: TEXT_MOBILE_LABEL, + placeholder: TEXT_MOBILE_PLACEHOLDER, }, captcha: { label: 'Kod:', placeholder: 'Masukkan kod pengesahan', }, password: { - label: 'Kata Laluan:', - placeholder: 'Masukkan kata laluan', + label: TEXT_PASSWORD_LABEL, + placeholder: TEXT_PASSWORD_PLACEHOLDER, }, confirmPassword: { label: 'Sahkan Kata Laluan:', @@ -357,7 +430,7 @@ export default { submitFailed: 'Gagal menghantar kod. Sila cuba lagi kemudian.', }, send: 'Dapatkan kod', - sending: 'Menghantar...', + sending: TEXT_SUBMITTING, success: 'Kod pengesahan telah dihantar.', }, }, @@ -394,27 +467,27 @@ export default { header: { systemTime: 'Masa Sistem', rules: 'Peraturan', - message: 'Mesej', + message: TEXT_SITE_MESSAGES, bgm: 'BGM', id: 'ID', fullscreen: 'Skrin', - login: 'Log Masuk', - register: 'Daftar', + login: TEXT_LOGIN, + register: TEXT_REGISTER, }, control: { trend: 'Trend', map: 'Peta', selected: 'Dipilih', totalBet: 'Jumlah Taruhan', - confirm: 'Sahkan', + confirm: TEXT_CONFIRM, selectNumbers: 'Pilih Nombor', insufficientBalance: 'Baki Tidak Mencukupi', betLimitExceeded: 'Melebihi Had', - submitting: 'Menghantar...', + submitting: TEXT_SUBMITTING, actions: { clear: 'Kosongkan', repeat: 'Ulang', - 'auto-spin': 'Putaran Auto', + 'auto-spin': TEXT_AUTO_HOSTING, }, }, status: { @@ -428,7 +501,7 @@ export default { description: '(Menerima Taruhan)', }, locked: { - label: 'Dikunci', + label: TEXT_LOCKED, description: '(Taruhan Ditutup)', }, revealing: { @@ -450,7 +523,7 @@ export default { }, animal: { insufficientBalanceRecharge: 'Baki tidak mencukupi, sila tambah nilai', - betLimitExceeded: 'Melebihi had taruhan tunggal', + betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED, loading: 'Memuatkan', selectionLimitReached: 'Melebihi pilihan aksara yang dibenarkan', tapToEnter: 'Ketik Untuk Masuk', @@ -465,29 +538,29 @@ export default { orderNo: 'No. Pesanan', roundId: 'ID Pusingan', numbers: 'Nombor Pertaruhan', - createdAt: 'Masa', + createdAt: TEXT_TIME, settledAt: 'Masa Selesai', totalPoolAmount: 'Jumlah Pertaruhan', winningResult: 'Keputusan Menang', payout: 'Jumlah Menang', empty: 'Belum ada sejarah', end: 'Tiada lagi rekod', - loading: 'Memuatkan...', - settled: 'Selesai', + loading: TEXT_LOADING, + settled: TEXT_SETTLED, }, periodHistory: { title: 'Sejarah Keputusan Cabutan', close: 'Tutup sejarah keputusan cabutan', empty: 'Belum ada keputusan cabutan', failed: 'Gagal memuatkan keputusan cabutan', - loading: 'Memuatkan...', + loading: TEXT_LOADING, retry: 'Cuba lagi', }, topup: { title: 'Konfigurasi Tambah Nilai', platformCoinLabel: 'Syiling Platform', - currencyLabel: 'Jenis Mata Wang', - channelLabel: 'Saluran Pembayaran', + currencyLabel: TEXT_CURRENCY_TYPE, + channelLabel: TEXT_PAYMENT_CHANNEL, rateHint: 'Kadar pertukaran hanya untuk rujukan. Jumlah akhir tertakluk kepada kadar semasa tambah nilai.', tier: { @@ -529,13 +602,13 @@ export default { feeNotice: 'Transaksi antara RM10 dan RM99.99 akan dikenakan yuran pengeluaran minimum RM 1.', cancel: 'Batal', - confirm: 'Sahkan', + confirm: TEXT_CONFIRM, submitSuccess: 'Permohonan pengeluaran telah dihantar', withdrawal: 'Pengeluaran', fields: { diamondAmount: 'Jumlah Berlian Pengeluaran', - currencyType: 'Jenis Mata Wang', - paymentChannel: 'Saluran Pembayaran', + currencyType: TEXT_CURRENCY_TYPE, + paymentChannel: TEXT_PAYMENT_CHANNEL, bankCode: 'Kod Bank', cardHolderName: 'Nama Pemegang Kad', bankAccountNumber: 'Nombor Akaun Bank', diff --git a/src/locales/zh-CN/common.ts b/src/locales/zh-CN/common.ts index 5d6b8bf..0d8a844 100644 --- a/src/locales/zh-CN/common.ts +++ b/src/locales/zh-CN/common.ts @@ -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 { nav: { home: '首页', @@ -86,8 +159,8 @@ export default { }, phases: { betting: '下注中', - locked: '已封盘', - settled: '已结算', + locked: TEXT_LOCKED, + settled: TEXT_SETTLED, }, roundBettingStart: { title: '{{roundId}}期', @@ -97,8 +170,8 @@ export default { unifiedBetHint: '统一下注额', totalBet: '总下注', canBet: '可下注', - yes: '是', - no: '否', + yes: TEXT_YES, + no: TEXT_NO, quickBet: '快速选中 08', clearPending: '清空未确认', autoModeDemo: '自动托管演示', @@ -106,16 +179,16 @@ export default { }, modals: { login: { - title: '登录', + title: TEXT_LOGIN, }, register: { - title: '注册', + title: TEXT_REGISTER, }, notice: { title: '活动公告', content: '这里后续将接入真实的活动公告内容、图文素材和滚动阅读区域。当前版本先用多语言结构完成桌面端公告弹窗能力。', - check: '查看', + check: TEXT_VIEW, }, entryNotice: { title: '网站公告', @@ -139,7 +212,7 @@ export default { topup: '充值', }, autoSetting: { - title: '自动托管', + title: TEXT_AUTO_HOSTING, startAutoSpin: '开始自动托管', rows: { stopIfBalanceLowerThan: '余额低于时停止', @@ -152,8 +225,8 @@ export default { tabs: { profile: '个人信息', financeRecords: '充值/提现记录', - walletRecords: '钱包流水', - message: '站内消息', + walletRecords: TEXT_WALLET_RECORDS, + message: TEXT_SITE_MESSAGES, }, profile: { name: '用户名', @@ -165,7 +238,7 @@ export default { signature: '我的个性签名和灵魂一样独特,后续这里会接真实资料字段。', }, message: { - title: '站内消息', + title: TEXT_SITE_MESSAGES, back: '返回', loading: '消息加载中...', loadFailed: '消息加载失败,请稍后重试', @@ -173,7 +246,7 @@ export default { read: '已读', unread: '未读', eventBonus: '[充值活动] 10 月 1 日至 10 月 7 日期间可获得返利奖励……', - check: '查看', + check: TEXT_VIEW, deleteRecords: '删除记录', }, financeRecords: { @@ -185,9 +258,9 @@ export default { loading: '记录加载中...', loadFailed: '记录加载失败,请稍后重试', empty: '暂无记录', - page: '第 {{page}} 页 / 共 {{total}} 条', - previous: '上一页', - next: '下一页', + page: TEXT_PAGE_INDICATOR, + previous: TEXT_PREV_PAGE, + next: TEXT_NEXT_PAGE, }, walletRecords: { amount: '变动金额', @@ -196,12 +269,12 @@ export default { empty: '暂无钱包流水', loadFailed: '钱包流水加载失败,请稍后重试', loading: '钱包流水加载中...', - next: '下一页', - page: '第 {{page}} 页 / 共 {{total}} 条', - previous: '上一页', + next: TEXT_NEXT_PAGE, + page: TEXT_PAGE_INDICATOR, + previous: TEXT_PREV_PAGE, remark: '备注', - time: '时间', - type: '钱包流水', + time: TEXT_TIME, + type: TEXT_WALLET_RECORDS, }, }, withdrawTopup: { @@ -210,7 +283,7 @@ export default { }, }, autoSpin: { - eyebrow: '自动托管', + eyebrow: TEXT_AUTO_HOSTING, title: '自动托管运行中', description: '托管态会覆盖主盘面,但目标格子和进度信息仍然保留可见。', runningRounds: '游戏托管中,已经进行 {{count}} 局', @@ -232,8 +305,8 @@ export default { dialog: { close: '关闭提示', confirm: '知道了', - no: '否', - yes: '是', + no: TEXT_NO, + yes: TEXT_YES, }, modal: { close: '关闭弹窗', @@ -249,7 +322,7 @@ export default { inviteLinkCopied: '邀请链接已复制', inviteLinkCopyFailed: '邀请链接复制失败,请手动复制', insufficientBalance: '余额不足,请调整下注金额', - betLimitExceeded: '超过单次投注限额', + betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED, betUnavailable: '当前期不可下注', betPlaced: '下注成功', noRecentSuccessfulBet: '暂无上一局成功下注记录', @@ -270,7 +343,7 @@ export default { common: { arrowIconAlt: '箭头', actions: { - submitting: '提交中...', + submitting: TEXT_SUBMITTING, }, passwordVisibility: { hide: '隐藏密码', @@ -279,16 +352,16 @@ export default { }, login: { actions: { - submit: '登录', + submit: TEXT_LOGIN, }, fields: { username: { - label: '手机号:', - placeholder: '请输入手机号', + label: TEXT_MOBILE_LABEL, + placeholder: TEXT_MOBILE_PLACEHOLDER, }, password: { - label: '密码:', - placeholder: '请输入密码', + label: TEXT_PASSWORD_LABEL, + placeholder: TEXT_PASSWORD_PLACEHOLDER, }, }, footer: { @@ -302,20 +375,20 @@ export default { }, register: { actions: { - submit: '注册', + submit: TEXT_REGISTER, }, fields: { mobile: { - label: '手机号:', - placeholder: '请输入手机号', + label: TEXT_MOBILE_LABEL, + placeholder: TEXT_MOBILE_PLACEHOLDER, }, captcha: { label: '验证码:', placeholder: '请输入验证码', }, password: { - label: '密码:', - placeholder: '请输入密码', + label: TEXT_PASSWORD_LABEL, + placeholder: TEXT_PASSWORD_PLACEHOLDER, }, confirmPassword: { label: '确认密码:', @@ -349,7 +422,7 @@ export default { required: '请输入验证码', }, username: { - required: '请输入手机号', + required: TEXT_MOBILE_PLACEHOLDER, invalidPhone: '请输入正确的手机号', }, password: { @@ -379,23 +452,23 @@ export default { bgm: '音乐', id: '编号', fullscreen: '全屏', - login: '登录', - register: '注册', + login: TEXT_LOGIN, + register: TEXT_REGISTER, }, control: { trend: '走势', map: '地图', selected: '已选', totalBet: '总下注', - confirm: '确认', + confirm: TEXT_CONFIRM, selectNumbers: '请选择号码', insufficientBalance: '余额不足', betLimitExceeded: '超过限额', - submitting: '提交中...', + submitting: TEXT_SUBMITTING, actions: { clear: '清空', repeat: '重复', - 'auto-spin': '自动托管', + 'auto-spin': TEXT_AUTO_HOSTING, }, }, status: { @@ -409,7 +482,7 @@ export default { description: '(接受下注)', }, locked: { - label: '已封盘', + label: TEXT_LOCKED, description: '(停止下注)', }, revealing: { @@ -431,7 +504,7 @@ export default { }, animal: { insufficientBalanceRecharge: '余额不足,请充值', - betLimitExceeded: '超过单次投注限额', + betLimitExceeded: TEXT_BET_LIMIT_EXCEEDED, loading: '加载中', selectionLimitReached: '超过可选择字花', tapToEnter: '点击进入', @@ -446,29 +519,29 @@ export default { orderNo: '订单号', roundId: '期号', numbers: '下注号码', - createdAt: '时间', + createdAt: TEXT_TIME, settledAt: '结算时间', totalPoolAmount: '下注金额', winningResult: '中奖字花', payout: '中奖金额', empty: '暂无历史记录', end: '没有更多记录了', - loading: '加载中...', - settled: '已结算', + loading: TEXT_LOADING, + settled: TEXT_SETTLED, }, periodHistory: { title: '开奖结果历史', close: '关闭开奖结果历史', empty: '暂无开奖结果', failed: '开奖结果加载失败', - loading: '加载中...', + loading: TEXT_LOADING, retry: '重试', }, topup: { title: '充值配置', platformCoinLabel: '平台币', - currencyLabel: '货币类型', - channelLabel: '支付渠道', + currencyLabel: TEXT_CURRENCY_TYPE, + channelLabel: TEXT_PAYMENT_CHANNEL, rateHint: '汇率为参考价格,实际以充值时为准。', tier: { bonus: '赠送', @@ -506,13 +579,13 @@ export default { notice: '注意', feeNotice: 'RM10 - RM99.99 之间的交易将收取最低RM 1的提现手续费', cancel: '取消', - confirm: '确认', + confirm: TEXT_CONFIRM, submitSuccess: '提现申请已提交', withdrawal: '提现', fields: { diamondAmount: '提现钻石数量', - currencyType: '货币类型', - paymentChannel: '支付渠道', + currencyType: TEXT_CURRENCY_TYPE, + paymentChannel: TEXT_PAYMENT_CHANNEL, bankCode: '银行代码', cardHolderName: '持卡人姓名', bankAccountNumber: '银行账号', diff --git a/src/store/game/game-auto-hosting-store.ts b/src/store/game/game-auto-hosting-store.ts index 2c9d1d5..26f1b9f 100644 --- a/src/store/game/game-auto-hosting-store.ts +++ b/src/store/game/game-auto-hosting-store.ts @@ -1,5 +1,6 @@ import { create } from 'zustand' +import { AUTO_HOSTING_DEFAULT_SINGLE_WIN_THRESHOLD } from '@/constants' import type { BetSelection } from '@/features/game/shared' export interface AutoHostingStopRules { @@ -44,7 +45,7 @@ const DEFAULT_AUTO_HOSTING_RULES: AutoHostingStopRules = { enabled: false, }, stopIfSingleWinAbove: { - amount: 50_000, + amount: AUTO_HOSTING_DEFAULT_SINGLE_WIN_THRESHOLD, enabled: false, }, stopOnJackpot: false, diff --git a/src/store/game/game-session-store.ts b/src/store/game/game-session-store.ts index f3c3a0f..7424451 100644 --- a/src/store/game/game-session-store.ts +++ b/src/store/game/game-session-store.ts @@ -1,5 +1,9 @@ import { create } from 'zustand' +import { + CONNECTION_LATENCY_FAIR_MS, + MAX_JACKPOT_BROADCAST_COUNT, +} from '@/constants' import type { AnnouncementState, ConnectionState, @@ -18,8 +22,6 @@ type GameSessionSlice = Pick< 'announcements' | 'connection' | 'dashboard' > -const MAX_JACKPOT_BROADCAST_COUNT = 20 - export interface JackpotBroadcastItem { id: string message: string @@ -173,7 +175,8 @@ export const selectActiveAnnouncement = (state: GameSessionStoreState) => export const selectIsConnectionHealthy = (state: GameSessionStoreState) => 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) => getUnreadAnnouncementCount(state.announcements)