feat(game): 优化界面组件
- 在国际化文件中添加钱包流水相关翻译项 - 在用户个人资料页面添加复制邀请链接功能 - 优化桌面端动物组件的视觉效果和动画参数 - 添加虚拟滚动功能到财务记录标签页提升性能 - 为桌面端控制面板添加投注数量调节按钮 - 更新消息模态框为通知列表和详情展示 - 在头部余额显示旁添加充值图标入口
BIN
figma/img.png
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 236 KiB |
BIN
figma/img_1.png
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 2.3 MiB |
BIN
src/assets/game/pc-streak.webp
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 745 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
@@ -159,6 +159,7 @@ export const FINANCE_API_ENDPOINTS = {
|
|||||||
depositTierList: 'api/finance/depositTierList',
|
depositTierList: 'api/finance/depositTierList',
|
||||||
depositWithdrawConfig: 'api/finance/depositWithdrawConfig',
|
depositWithdrawConfig: 'api/finance/depositWithdrawConfig',
|
||||||
legacyCashierConfig: 'api/finance/cashierConfig',
|
legacyCashierConfig: 'api/finance/cashierConfig',
|
||||||
|
walletRecordList: 'api/wallet/recordList',
|
||||||
withdrawCreate: 'api/finance/withdrawCreate',
|
withdrawCreate: 'api/finance/withdrawCreate',
|
||||||
withdrawList: 'api/finance/withdrawList',
|
withdrawList: 'api/finance/withdrawList',
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -15,12 +15,26 @@ interface UseRegisterFormOptions {
|
|||||||
onSuccess?: () => void
|
onSuccess?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REGISTER_INVITE_CODE_QUERY_PARAM = 'registerInviteCode'
|
||||||
|
|
||||||
|
function getInitialRegisterInviteCode() {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
new URLSearchParams(window.location.search)
|
||||||
|
.get(REGISTER_INVITE_CODE_QUERY_PARAM)
|
||||||
|
?.trim() ?? ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function useRegisterForm({ onSuccess }: UseRegisterFormOptions = {}) {
|
export function useRegisterForm({ onSuccess }: UseRegisterFormOptions = {}) {
|
||||||
const startSession = useAuthStore((state) => state.startSession)
|
const startSession = useAuthStore((state) => state.startSession)
|
||||||
const form = useForm<RegisterFormValues>({
|
const form = useForm<RegisterFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
inviteCode: '',
|
inviteCode: getInitialRegisterInviteCode(),
|
||||||
password: '',
|
password: '',
|
||||||
username: '',
|
username: '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ import type {
|
|||||||
FinanceRateConfigDto,
|
FinanceRateConfigDto,
|
||||||
FinanceWithdrawBankDto,
|
FinanceWithdrawBankDto,
|
||||||
FinanceWithdrawConfigDto,
|
FinanceWithdrawConfigDto,
|
||||||
|
WalletRecordItemDto,
|
||||||
|
WalletRecordList,
|
||||||
|
WalletRecordListDto,
|
||||||
|
WalletRecordType,
|
||||||
WithdrawCreateRequestDto,
|
WithdrawCreateRequestDto,
|
||||||
WithdrawCreateResponseDto,
|
WithdrawCreateResponseDto,
|
||||||
} from './finance-types'
|
} from './finance-types'
|
||||||
@@ -196,6 +200,49 @@ function normalizeFinanceOrderList(dto: FinanceOrderListDto): FinanceOrderList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stringifyNullableValue(value: unknown) {
|
||||||
|
return value === null || value === undefined ? '' : String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWalletRecordItem(dto: WalletRecordItemDto, index: number) {
|
||||||
|
const createdAt = dto.created_at ?? dto.create_time ?? dto.time ?? null
|
||||||
|
const id =
|
||||||
|
dto.id ??
|
||||||
|
dto.record_id ??
|
||||||
|
dto.wallet_record_id ??
|
||||||
|
dto.order_no ??
|
||||||
|
`${createdAt ?? 'wallet-record'}-${index + 1}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount: stringifyNullableValue(
|
||||||
|
dto.amount ?? dto.change_amount ?? dto.coin ?? '',
|
||||||
|
),
|
||||||
|
balanceAfter: stringifyNullableValue(
|
||||||
|
dto.balance_after ?? dto.after_balance ?? dto.balance ?? '',
|
||||||
|
),
|
||||||
|
balanceBefore: stringifyNullableValue(
|
||||||
|
dto.balance_before ?? dto.before_balance ?? '',
|
||||||
|
),
|
||||||
|
createdAt,
|
||||||
|
id: String(id),
|
||||||
|
remark: stringifyNullableValue(dto.remark ?? dto.description ?? dto.memo),
|
||||||
|
type: stringifyNullableValue(
|
||||||
|
dto.type ?? dto.change_type ?? dto.biz_type ?? dto.scene,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWalletRecordList(dto: WalletRecordListDto): WalletRecordList {
|
||||||
|
return {
|
||||||
|
list: (dto.list ?? []).map(normalizeWalletRecordItem),
|
||||||
|
pagination: {
|
||||||
|
page: dto.pagination?.page ?? dto.page ?? 1,
|
||||||
|
page_size: dto.pagination?.page_size ?? dto.page_size ?? 20,
|
||||||
|
total: dto.pagination?.total ?? dto.total ?? 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getDepositWithdrawConfig() {
|
export async function getDepositWithdrawConfig() {
|
||||||
const response = await api.post<DepositWithdrawConfigDto>(
|
const response = await api.post<DepositWithdrawConfigDto>(
|
||||||
FINANCE_API_ENDPOINTS.depositWithdrawConfig,
|
FINANCE_API_ENDPOINTS.depositWithdrawConfig,
|
||||||
@@ -287,6 +334,29 @@ export async function getWithdrawOrderList(params?: {
|
|||||||
return normalizeFinanceOrderList(dto)
|
return normalizeFinanceOrderList(dto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getWalletRecordList(params?: {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
type?: WalletRecordType
|
||||||
|
}) {
|
||||||
|
const response = await api.get<WalletRecordListDto>(
|
||||||
|
FINANCE_API_ENDPOINTS.walletRecordList,
|
||||||
|
{
|
||||||
|
searchParams: {
|
||||||
|
page: String(params?.page ?? 1),
|
||||||
|
page_size: String(params?.pageSize ?? 20),
|
||||||
|
type: params?.type ?? 'payout',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const dto = unwrapFinanceEnvelope(
|
||||||
|
response as ApiResponse<WalletRecordListDto>,
|
||||||
|
'Failed to load wallet record list',
|
||||||
|
)
|
||||||
|
|
||||||
|
return normalizeWalletRecordList(dto)
|
||||||
|
}
|
||||||
|
|
||||||
export async function createWithdraw(payload: WithdrawCreateRequestDto) {
|
export async function createWithdraw(payload: WithdrawCreateRequestDto) {
|
||||||
const response = await api.post<
|
const response = await api.post<
|
||||||
WithdrawCreateResponseDto,
|
WithdrawCreateResponseDto,
|
||||||
|
|||||||
@@ -190,6 +190,56 @@ export interface FinanceOrderList {
|
|||||||
pagination: FinanceOrderPaginationDto
|
pagination: FinanceOrderPaginationDto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WalletRecordType = 'payout' | (string & {})
|
||||||
|
|
||||||
|
export interface WalletRecordItemDto {
|
||||||
|
after_balance?: number | string | null
|
||||||
|
amount?: number | string | null
|
||||||
|
balance?: number | string | null
|
||||||
|
balance_after?: number | string | null
|
||||||
|
balance_before?: number | string | null
|
||||||
|
before_balance?: number | string | null
|
||||||
|
biz_type?: string | null
|
||||||
|
change_amount?: number | string | null
|
||||||
|
change_type?: string | null
|
||||||
|
coin?: number | string | null
|
||||||
|
create_time?: number | string | null
|
||||||
|
created_at?: number | string | null
|
||||||
|
description?: string | null
|
||||||
|
id?: number | string | null
|
||||||
|
memo?: string | null
|
||||||
|
order_no?: string | null
|
||||||
|
record_id?: number | string | null
|
||||||
|
remark?: string | null
|
||||||
|
scene?: string | null
|
||||||
|
time?: number | string | null
|
||||||
|
type?: string | null
|
||||||
|
wallet_record_id?: number | string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletRecordListDto {
|
||||||
|
list: WalletRecordItemDto[]
|
||||||
|
pagination?: FinanceOrderPaginationDto
|
||||||
|
page?: number
|
||||||
|
page_size?: number
|
||||||
|
total?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletRecordItem {
|
||||||
|
amount: string
|
||||||
|
balanceAfter: string
|
||||||
|
balanceBefore: string
|
||||||
|
createdAt: number | string | null
|
||||||
|
id: string
|
||||||
|
remark: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletRecordList {
|
||||||
|
list: WalletRecordItem[]
|
||||||
|
pagination: FinanceOrderPaginationDto
|
||||||
|
}
|
||||||
|
|
||||||
export interface WithdrawCreateRequestDto {
|
export interface WithdrawCreateRequestDto {
|
||||||
bank_code: string
|
bank_code: string
|
||||||
channel_code: string
|
channel_code: string
|
||||||
|
|||||||
@@ -288,10 +288,12 @@ export interface GameBetOrdersDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface GamePlaceBetRequestDto {
|
export interface GamePlaceBetRequestDto {
|
||||||
|
bet_amount?: string
|
||||||
bet_id: number
|
bet_id: number
|
||||||
idempotency_key: string
|
idempotency_key: string
|
||||||
numbers: string
|
numbers: string
|
||||||
period_no: string
|
period_no: string
|
||||||
|
single_bet_amount?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GamePlaceBetDto {
|
export interface GamePlaceBetDto {
|
||||||
|
|||||||
@@ -298,9 +298,9 @@ export function DesktopAnimal({
|
|||||||
isMarqueeActive &&
|
isMarqueeActive &&
|
||||||
'border-[rgba(121,255,250,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(85,255,247,0.98),0_0_calc(var(--design-unit)*34)_rgba(39,245,255,0.88),inset_0_0_calc(var(--design-unit)*26)_rgba(112,255,248,0.34)]',
|
'border-[rgba(121,255,250,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(85,255,247,0.98),0_0_calc(var(--design-unit)*34)_rgba(39,245,255,0.88),inset_0_0_calc(var(--design-unit)*26)_rgba(112,255,248,0.34)]',
|
||||||
isRevealRunning &&
|
isRevealRunning &&
|
||||||
'border-[rgba(104,255,249,0.9)] shadow-[0_0_calc(var(--design-unit)*12)_rgba(68,244,255,0.68),0_0_calc(var(--design-unit)*26)_rgba(37,214,255,0.42),inset_0_0_calc(var(--design-unit)*18)_rgba(115,255,247,0.24)] brightness-125 saturate-150',
|
'border-[rgba(104,255,249,0.76)] shadow-[0_0_calc(var(--design-unit)*10)_rgba(68,244,255,0.46),0_0_calc(var(--design-unit)*22)_rgba(37,214,255,0.28),inset_0_0_calc(var(--design-unit)*16)_rgba(115,255,247,0.18)] brightness-115 saturate-125',
|
||||||
isRevealWinner &&
|
isRevealWinner &&
|
||||||
'shadow-[0_0_calc(var(--design-unit)*14)_rgba(81,248,255,0.72),0_0_calc(var(--design-unit)*24)_rgba(30,199,255,0.42),inset_0_0_calc(var(--design-unit)*18)_rgba(125,255,249,0.34)] brightness-125 saturate-150',
|
'border-[rgba(121,255,250,0.72)] shadow-[0_0_calc(var(--design-unit)*12)_rgba(81,248,255,0.54),0_0_calc(var(--design-unit)*22)_rgba(30,199,255,0.32),inset_0_0_calc(var(--design-unit)*18)_rgba(125,255,249,0.24)] brightness-110 saturate-120',
|
||||||
showCellWarning &&
|
showCellWarning &&
|
||||||
'border-[rgba(255,92,92,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(255,88,88,0.56),0_0_calc(var(--design-unit)*28)_rgba(255,44,44,0.32),inset_0_0_calc(var(--design-unit)*18)_rgba(255,126,126,0.3)]',
|
'border-[rgba(255,92,92,1)] shadow-[0_0_calc(var(--design-unit)*18)_rgba(255,88,88,0.56),0_0_calc(var(--design-unit)*28)_rgba(255,44,44,0.32),inset_0_0_calc(var(--design-unit)*18)_rgba(255,126,126,0.3)]',
|
||||||
!showStandbyState && !hasPlacedSelection && 'opacity-95',
|
!showStandbyState && !hasPlacedSelection && 'opacity-95',
|
||||||
@@ -341,9 +341,9 @@ export function DesktopAnimal({
|
|||||||
isMarqueeActive &&
|
isMarqueeActive &&
|
||||||
'bg-[radial-gradient(circle_at_center,rgba(129,255,250,0.48)_0%,rgba(94,255,247,0.18)_38%,rgba(43,236,255,0.08)_56%,transparent_76%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(119,255,249,0.98),0_0_calc(var(--design-unit)*28)_rgba(53,246,255,0.9),0_0_calc(var(--design-unit)*44)_rgba(37,241,255,0.58),inset_0_0_calc(var(--design-unit)*20)_rgba(163,255,250,0.52)]',
|
'bg-[radial-gradient(circle_at_center,rgba(129,255,250,0.48)_0%,rgba(94,255,247,0.18)_38%,rgba(43,236,255,0.08)_56%,transparent_76%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(119,255,249,0.98),0_0_calc(var(--design-unit)*28)_rgba(53,246,255,0.9),0_0_calc(var(--design-unit)*44)_rgba(37,241,255,0.58),inset_0_0_calc(var(--design-unit)*20)_rgba(163,255,250,0.52)]',
|
||||||
isRevealRunning &&
|
isRevealRunning &&
|
||||||
'bg-[radial-gradient(circle_at_center,rgba(128,255,250,0.5)_0%,rgba(77,244,255,0.24)_40%,rgba(27,183,255,0.1)_68%,transparent_88%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*16)_rgba(95,249,255,0.72),inset_0_0_calc(var(--design-unit)*22)_rgba(151,255,250,0.4)]',
|
'bg-[radial-gradient(circle_at_center,rgba(128,255,250,0.36)_0%,rgba(77,244,255,0.16)_40%,rgba(27,183,255,0.07)_68%,transparent_88%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(95,249,255,0.46),inset_0_0_calc(var(--design-unit)*18)_rgba(151,255,250,0.24)]',
|
||||||
isRevealWinner &&
|
isRevealWinner &&
|
||||||
'bg-[radial-gradient(circle_at_center,rgba(128,255,250,0.5)_0%,rgba(67,226,255,0.24)_38%,rgba(25,131,255,0.1)_58%,transparent_76%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*14)_rgba(92,248,255,0.58),inset_0_0_calc(var(--design-unit)*20)_rgba(126,255,250,0.4)]',
|
'bg-[radial-gradient(circle_at_center,rgba(128,255,250,0.34)_0%,rgba(67,226,255,0.16)_38%,rgba(25,131,255,0.07)_58%,transparent_76%)] opacity-100 shadow-[0_0_calc(var(--design-unit)*12)_rgba(92,248,255,0.42),inset_0_0_calc(var(--design-unit)*18)_rgba(126,255,250,0.24)]',
|
||||||
showCellWarning &&
|
showCellWarning &&
|
||||||
'bg-[radial-gradient(circle_at_center,rgba(255,106,106,0.34)_0%,rgba(255,58,58,0.18)_42%,rgba(108,0,0,0.2)_78%,transparent_100%)] opacity-100',
|
'bg-[radial-gradient(circle_at_center,rgba(255,106,106,0.34)_0%,rgba(255,58,58,0.18)_42%,rgba(108,0,0,0.2)_78%,transparent_100%)] opacity-100',
|
||||||
)}
|
)}
|
||||||
@@ -360,9 +360,9 @@ export function DesktopAnimal({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'absolute left-[1.5%] right-[1.5%] top-[2.9%] bottom-[2.9%] z-10 overflow-hidden rounded-[calc(var(--design-unit)*14)]',
|
'absolute left-[1.5%] right-[1.5%] top-[2.9%] bottom-[2.9%] z-10 overflow-hidden rounded-[calc(var(--design-unit)*14)]',
|
||||||
isRevealRunning &&
|
isRevealRunning &&
|
||||||
'brightness-125 saturate-150 drop-shadow-[0_0_calc(var(--design-unit)*10)_rgba(101,250,255,0.62)]',
|
'brightness-115 saturate-125 drop-shadow-[0_0_calc(var(--design-unit)*8)_rgba(101,250,255,0.42)]',
|
||||||
isRevealWinner &&
|
isRevealWinner &&
|
||||||
'brightness-140 saturate-150 drop-shadow-[0_0_calc(var(--design-unit)*12)_rgba(106,250,255,0.72)]',
|
'brightness-110 saturate-120 drop-shadow-[0_0_calc(var(--design-unit)*9)_rgba(106,250,255,0.44)]',
|
||||||
imageClassName,
|
imageClassName,
|
||||||
)}
|
)}
|
||||||
imgClassName="object-fill"
|
imgClassName="object-fill"
|
||||||
@@ -452,7 +452,7 @@ export function DesktopAnimal({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showStopOverlay ? (
|
{showStopOverlay && !hostingFlag ? (
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="absolute inset-0 z-50 flex items-center justify-center bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px]"
|
className="absolute inset-0 z-50 flex items-center justify-center bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px]"
|
||||||
@@ -469,22 +469,50 @@ export function DesktopAnimal({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{hostingFlag ? (
|
{hostingFlag ? (
|
||||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-[rgba(2,8,14,0.72)] px-design-24 backdrop-blur-[2px]">
|
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center gap-design-22 bg-[rgba(2,8,14,0.6)] px-design-24 backdrop-blur-[1px]">
|
||||||
|
{showStopOverlay ? (
|
||||||
|
<SmartImage
|
||||||
|
src={stopImageSrc}
|
||||||
|
alt="stop betting"
|
||||||
|
priority
|
||||||
|
showSkeleton={false}
|
||||||
|
className="h-design-170 w-design-520 max-w-[72%] overflow-visible"
|
||||||
|
imgClassName="object-contain drop-shadow-[0_0_calc(var(--design-unit)*22)_rgba(60,235,255,0.28)]"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<SmartBackground
|
<SmartBackground
|
||||||
src={hostingBg}
|
src={hostingBg}
|
||||||
|
size="100% 100%"
|
||||||
|
repeat="no-repeat"
|
||||||
|
position="center"
|
||||||
className="h-design-350 w-design-930 flex flex-col items-center justify-center"
|
className="h-design-350 w-design-930 flex flex-col items-center justify-center"
|
||||||
>
|
>
|
||||||
<div className={'flex flex-col gap-design-40 items-center'}>
|
<div className={'flex flex-col gap-design-44 items-center'}>
|
||||||
<div className={'flex items-center gap-design-20'}>
|
<div className={'flex items-center gap-design-20'}>
|
||||||
<SmartImage
|
<motion.span
|
||||||
src={refreshIcon}
|
aria-hidden="true"
|
||||||
alt="refreshIcon"
|
animate={prefersReducedMotion ? undefined : { rotate: 360 }}
|
||||||
priority
|
transition={{
|
||||||
showSkeleton={false}
|
duration: 1.4,
|
||||||
className="h-design-40 w-design-40"
|
ease: 'linear',
|
||||||
imgClassName="object-contain drop-shadow-[0_0_calc(var(--design-unit)*22)_rgba(60,235,255,0.28)]"
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
/>
|
}}
|
||||||
<div className={'text-design-20 text-[#ffffff] font-bold'}>
|
className="flex h-design-48 w-design-48 items-center justify-center"
|
||||||
|
>
|
||||||
|
<SmartImage
|
||||||
|
src={refreshIcon}
|
||||||
|
alt=""
|
||||||
|
priority
|
||||||
|
showSkeleton={false}
|
||||||
|
className="h-design-40 w-design-40"
|
||||||
|
imgClassName="object-contain drop-shadow-[0_0_calc(var(--design-unit)*22)_rgba(60,235,255,0.36)]"
|
||||||
|
/>
|
||||||
|
</motion.span>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'text-design-22 text-[#ffffff] font-bold [text-shadow:0_0_calc(var(--design-unit)*12)_rgba(76,236,255,0.45)]'
|
||||||
|
}
|
||||||
|
>
|
||||||
{t('game.autoSpin.runningRounds', {
|
{t('game.autoSpin.runningRounds', {
|
||||||
count: completedAutoHostingRounds,
|
count: completedAutoHostingRounds,
|
||||||
})}
|
})}
|
||||||
@@ -495,7 +523,10 @@ export function DesktopAnimal({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={stopHosting}
|
onClick={stopHosting}
|
||||||
src={hostingBtn}
|
src={hostingBtn}
|
||||||
className="h-design-80 w-design-170 flex cursor-pointer flex-col items-center justify-center transition-transform hover:-translate-y-[1px] active:translate-y-0"
|
size="100% 100%"
|
||||||
|
repeat="no-repeat"
|
||||||
|
position="center"
|
||||||
|
className="h-design-70 w-design-220 flex cursor-pointer items-center justify-center pb-design-4 text-design-24 font-bold text-[#EFFFFF] [text-shadow:0_1px_0_rgba(255,255,255,0.18),0_0_calc(var(--design-unit)*10)_rgba(46,220,255,0.5)] transition-transform hover:-translate-y-[1px] active:translate-y-0"
|
||||||
>
|
>
|
||||||
{t('game.actions.stopAuto')}
|
{t('game.actions.stopAuto')}
|
||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export function DesktopControl() {
|
|||||||
acceptingBets,
|
acceptingBets,
|
||||||
actionsEnabled,
|
actionsEnabled,
|
||||||
canClear,
|
canClear,
|
||||||
|
canDecreaseBetQuantity,
|
||||||
chips,
|
chips,
|
||||||
confirmLabel,
|
confirmLabel,
|
||||||
confirmState,
|
confirmState,
|
||||||
@@ -32,9 +33,11 @@ export function DesktopControl() {
|
|||||||
onChipSelect,
|
onChipSelect,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onClearSelections,
|
onClearSelections,
|
||||||
|
onDecreaseBetQuantity,
|
||||||
|
onIncreaseBetQuantity,
|
||||||
onOpenAutoSetting,
|
onOpenAutoSetting,
|
||||||
onRepeatSelections,
|
onRepeatSelections,
|
||||||
selectedChipAmountLabel,
|
selectedBetQuantityLabel,
|
||||||
selectedChipId,
|
selectedChipId,
|
||||||
selectedCountLabel,
|
selectedCountLabel,
|
||||||
totalBetAmountLabel,
|
totalBetAmountLabel,
|
||||||
@@ -297,21 +300,43 @@ export function DesktopControl() {
|
|||||||
'flex h-design-50 shrink-0 items-center rounded-md bg-[#091118] box-border px-design-2 py-design-3'
|
'flex h-design-50 shrink-0 items-center rounded-md bg-[#091118] box-border px-design-2 py-design-3'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SmartImage
|
<button
|
||||||
src={add}
|
type="button"
|
||||||
alt={`add`}
|
disabled={!acceptingBets || !canDecreaseBetQuantity}
|
||||||
className={'w-design-40 h-design-40'}
|
onClick={onDecreaseBetQuantity}
|
||||||
/>
|
className={cn(
|
||||||
|
'flex h-design-40 w-design-40 shrink-0 items-center justify-center',
|
||||||
|
acceptingBets && canDecreaseBetQuantity
|
||||||
|
? 'cursor-pointer'
|
||||||
|
: 'cursor-not-allowed opacity-50',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SmartImage
|
||||||
|
src={add}
|
||||||
|
alt={`add`}
|
||||||
|
className={'w-design-40 h-design-40'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
<div
|
<div
|
||||||
className={'w-design-80 h-full flex items-center justify-center'}
|
className={'w-design-80 h-full flex items-center justify-center'}
|
||||||
>
|
>
|
||||||
{selectedChipAmountLabel}
|
{selectedBetQuantityLabel}
|
||||||
</div>
|
</div>
|
||||||
<SmartImage
|
<button
|
||||||
src={reduce}
|
type="button"
|
||||||
alt={`reduce`}
|
disabled={!acceptingBets}
|
||||||
className={'w-design-40 h-design-40'}
|
onClick={onIncreaseBetQuantity}
|
||||||
/>
|
className={cn(
|
||||||
|
'flex h-design-40 w-design-40 shrink-0 items-center justify-center',
|
||||||
|
acceptingBets ? 'cursor-pointer' : 'cursor-not-allowed',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SmartImage
|
||||||
|
src={reduce}
|
||||||
|
alt={`reduce`}
|
||||||
|
className={'w-design-40 h-design-40'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
<SmartBackground
|
<SmartBackground
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
Maximize,
|
Maximize,
|
||||||
Minimize,
|
Minimize,
|
||||||
|
Plus,
|
||||||
UserKey,
|
UserKey,
|
||||||
UserRoundPlus,
|
UserRoundPlus,
|
||||||
Volume2,
|
Volume2,
|
||||||
VolumeX,
|
VolumeX,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import add from '@/assets/game/add.webp'
|
||||||
import avatar from '@/assets/system/avatar.webp'
|
import avatar from '@/assets/system/avatar.webp'
|
||||||
import diamond from '@/assets/system/diamond.webp'
|
import diamond from '@/assets/system/diamond.webp'
|
||||||
import logo from '@/assets/system/logo.webp'
|
import logo from '@/assets/system/logo.webp'
|
||||||
@@ -205,10 +207,19 @@ export function DesktopHeader() {
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'common-neon-inset text-design-16 !py-design-20 box-border flex h-design-36 w-design-180 items-center justify-end transition-[opacity,transform] duration-150 group-hover:opacity-90 group-active:scale-[0.98]'
|
'common-neon-inset text-design-16 !py-design-20 !pr-design-14 box-border flex h-design-36 w-design-180 items-center justify-end gap-design-8 transition-[opacity,transform] duration-150 group-hover:opacity-90 group-active:scale-[0.98]'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{currentUser?.coin || '--'}
|
<span className="truncate">{currentUser?.coin || '--'}</span>
|
||||||
|
<div className={'common-neon-inset !p-design-3'}>
|
||||||
|
<Plus
|
||||||
|
aria-hidden="true"
|
||||||
|
className="shrink-0"
|
||||||
|
color="#57B8BF"
|
||||||
|
size={18}
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import streakBg from '@/assets/game/pc-streak.webp'
|
||||||
import down5Animation from '@/assets/lottie/down5.json'
|
import down5Animation from '@/assets/lottie/down5.json'
|
||||||
import diamond from '@/assets/system/diamond.webp'
|
import diamond from '@/assets/system/diamond.webp'
|
||||||
import fire from '@/assets/system/fire.webp'
|
import fire from '@/assets/system/fire.webp'
|
||||||
@@ -12,6 +13,7 @@ import { SmartImage } from '@/components/smart-image.tsx'
|
|||||||
import { DesktopCountdown } from '@/features/game/components/desktop/desktop-countdown.tsx'
|
import { DesktopCountdown } from '@/features/game/components/desktop/desktop-countdown.tsx'
|
||||||
import { DesktopTitle } from '@/features/game/components/desktop/desktop-title.tsx'
|
import { DesktopTitle } from '@/features/game/components/desktop/desktop-title.tsx'
|
||||||
import { useGameStatusVm } from '@/features/game/hooks/use-game-status-vm.ts'
|
import { useGameStatusVm } from '@/features/game/hooks/use-game-status-vm.ts'
|
||||||
|
|
||||||
export function DesktopStatusLine() {
|
export function DesktopStatusLine() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
@@ -23,9 +25,11 @@ export function DesktopStatusLine() {
|
|||||||
phaseToneClassName,
|
phaseToneClassName,
|
||||||
roundId,
|
roundId,
|
||||||
streakLabel,
|
streakLabel,
|
||||||
|
streakValue,
|
||||||
} = useGameStatusVm()
|
} = useGameStatusVm()
|
||||||
const [remainingMs, setRemainingMs] = useState(countdownMs)
|
const [remainingMs, setRemainingMs] = useState(countdownMs)
|
||||||
const showWarningCountdown = remainingMs <= 5000 && remainingMs > 0
|
const showWarningCountdown = remainingMs <= 5000 && remainingMs > 0
|
||||||
|
const showStreakLimitOnly = typeof streakValue === 'number' && streakValue > 1
|
||||||
const countdownClassName = useMemo(
|
const countdownClassName = useMemo(
|
||||||
() =>
|
() =>
|
||||||
showWarningCountdown
|
showWarningCountdown
|
||||||
@@ -44,45 +48,59 @@ export function DesktopStatusLine() {
|
|||||||
{/* 状态栏左侧 */}
|
{/* 状态栏左侧 */}
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'relative h-full flex-1 flex items-center justify-center gap-design-50'
|
'relative h-full flex-1 flex items-center justify-center gap-design-50 overflow-visible'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/*<div className={'flex-1 absolute z-10 -right-20 -top-6 w-full !h-design-105'} style={{*/}
|
{showStreakLimitOnly && (
|
||||||
{/* backgroundImage: `url(${streakBg})`,*/}
|
<div
|
||||||
{/* backgroundSize: '100% 110%',*/}
|
className={
|
||||||
{/* backgroundRepeat: 'no-repeat',*/}
|
'pointer-events-none absolute z-0 bg-center bg-no-repeat'
|
||||||
{/*}} >*/}
|
}
|
||||||
{/*</div>*/}
|
style={{
|
||||||
|
top: 'calc(var(--design-unit)*-14)',
|
||||||
<div className={'text-[#CBD3D5] font-bold'}>
|
right: 'calc(var(--design-unit)*-190)',
|
||||||
{t('gameDesktop.status.odds')}:{' '}
|
bottom: 'calc(var(--design-unit)*-1)',
|
||||||
<span className={'text-[#E3D171]'}>{oddsLabel}</span>
|
left: 0,
|
||||||
</div>
|
backgroundImage: `url(${streakBg})`,
|
||||||
<div
|
backgroundPosition: 'center',
|
||||||
className={
|
backgroundSize: '113% 150%',
|
||||||
'flex items-center gap-design-5 text-[#CBD3D5] font-bold'
|
}}
|
||||||
}
|
|
||||||
>
|
|
||||||
<SmartImage
|
|
||||||
className={'w-design-37 h-design-47'}
|
|
||||||
alt={'fire'}
|
|
||||||
src={fire}
|
|
||||||
/>
|
/>
|
||||||
<div>
|
)}
|
||||||
{t('gameDesktop.status.streak')}:{' '}
|
|
||||||
<span
|
{!showStreakLimitOnly && (
|
||||||
|
<>
|
||||||
|
<div className={'text-[#CBD3D5] font-bold'}>
|
||||||
|
{t('gameDesktop.status.odds')}:{' '}
|
||||||
|
<span className={'text-[#E3D171]'}>{oddsLabel}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
className={
|
className={
|
||||||
'bg-gradient-to-b from-[#EBA661] to-[#FCC785] bg-clip-text text-transparent'
|
'flex items-center gap-design-5 text-[#CBD3D5] font-bold'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{streakLabel}
|
<SmartImage
|
||||||
</span>
|
className={'w-design-37 h-design-47'}
|
||||||
</div>
|
alt={'fire'}
|
||||||
</div>
|
src={fire}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{t('gameDesktop.status.streak')}:{' '}
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
'bg-gradient-to-b from-[#EBA661] to-[#FCC785] bg-clip-text text-transparent'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{streakLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'flex items-center gap-design-5 text-[#CBD3D5] font-bold'
|
'relative z-20 flex items-center gap-design-5 text-[#CBD3D5] font-bold'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SmartImage
|
<SmartImage
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export function useAnimalVm(
|
|||||||
)
|
)
|
||||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||||
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
||||||
|
const activeBetQuantity = useGameRoundStore(
|
||||||
|
(state) => state.activeBetQuantity,
|
||||||
|
)
|
||||||
const chips = useGameRoundStore((state) => state.chips)
|
const chips = useGameRoundStore((state) => state.chips)
|
||||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||||
const roundId = useGameRoundStore((state) => state.round.id)
|
const roundId = useGameRoundStore((state) => state.round.id)
|
||||||
@@ -188,7 +191,9 @@ export function useAnimalVm(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalBetAmount + (activeChip?.amount ?? 0) > balance) {
|
const nextBetAmount = (activeChip?.amount ?? 0) * activeBetQuantity
|
||||||
|
|
||||||
|
if (totalBetAmount + nextBetAmount > balance) {
|
||||||
setCellWarning({
|
setCellWarning({
|
||||||
cellId: animalId,
|
cellId: animalId,
|
||||||
type: 'balance',
|
type: 'balance',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { placeGameBet } from '@/features/game'
|
import { placeGameBet } from '@/features/game'
|
||||||
@@ -44,32 +44,40 @@ function toBetId(chipId: string) {
|
|||||||
return Number.isInteger(betId) && betId >= 1 && betId <= 6 ? betId : null
|
return Number.isInteger(betId) && betId >= 1 && betId <= 6 ? betId : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBetAmount(amount: number) {
|
||||||
|
if (Number.isInteger(amount)) {
|
||||||
|
return String(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount.toFixed(2).replace(/\.?0+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
function groupSelections(selections: BetSelection[]) {
|
function groupSelections(selections: BetSelection[]) {
|
||||||
return selections.reduce<Map<string, { betId: number; numbers: number[] }>>(
|
return selections.reduce<
|
||||||
(accumulator, selection) => {
|
Map<string, { amount: number; betId: number; numbers: number[] }>
|
||||||
const betId = toBetId(selection.chipId)
|
>((accumulator, selection) => {
|
||||||
|
const betId = toBetId(selection.chipId)
|
||||||
if (betId === null) {
|
|
||||||
return accumulator
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupKey = String(betId)
|
|
||||||
const current = accumulator.get(groupKey)
|
|
||||||
|
|
||||||
if (current) {
|
|
||||||
current.numbers.push(selection.cellId)
|
|
||||||
return accumulator
|
|
||||||
}
|
|
||||||
|
|
||||||
accumulator.set(groupKey, {
|
|
||||||
betId,
|
|
||||||
numbers: [selection.cellId],
|
|
||||||
})
|
|
||||||
|
|
||||||
|
if (betId === null) {
|
||||||
return accumulator
|
return accumulator
|
||||||
},
|
}
|
||||||
new Map(),
|
|
||||||
)
|
const groupKey = `${betId}:${selection.amount}`
|
||||||
|
const current = accumulator.get(groupKey)
|
||||||
|
|
||||||
|
if (current) {
|
||||||
|
current.numbers.push(selection.cellId)
|
||||||
|
return accumulator
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulator.set(groupKey, {
|
||||||
|
amount: selection.amount,
|
||||||
|
betId,
|
||||||
|
numbers: [selection.cellId],
|
||||||
|
})
|
||||||
|
|
||||||
|
return accumulator
|
||||||
|
}, new Map())
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAutoHostingRunner() {
|
export function useAutoHostingRunner() {
|
||||||
@@ -79,9 +87,10 @@ export function useAutoHostingRunner() {
|
|||||||
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
const setCurrentUser = useAuthStore((state) => state.setCurrentUser)
|
||||||
const round = useGameRoundStore((state) => state.round)
|
const round = useGameRoundStore((state) => state.round)
|
||||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||||
const balanceAfterBet = useGameAutoHostingStore(
|
const lastSingleWinAmount = useGameAutoHostingStore(
|
||||||
(state) => state.balanceAfterBet,
|
(state) => state.lastSingleWinAmount,
|
||||||
)
|
)
|
||||||
|
const lastIsJackpot = useGameAutoHostingStore((state) => state.lastIsJackpot)
|
||||||
const isHosting = useGameAutoHostingStore((state) => state.isHosting)
|
const isHosting = useGameAutoHostingStore((state) => state.isHosting)
|
||||||
const lastSubmittedRoundId = useGameAutoHostingStore(
|
const lastSubmittedRoundId = useGameAutoHostingStore(
|
||||||
(state) => state.lastSubmittedRoundId,
|
(state) => state.lastSubmittedRoundId,
|
||||||
@@ -92,14 +101,10 @@ export function useAutoHostingRunner() {
|
|||||||
(state) => state.markRoundSubmitted,
|
(state) => state.markRoundSubmitted,
|
||||||
)
|
)
|
||||||
const stopHosting = useGameAutoHostingStore((state) => state.stopHosting)
|
const stopHosting = useGameAutoHostingStore((state) => state.stopHosting)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const inFlightRoundIdRef = useRef<string | null>(null)
|
||||||
const previousJackpotRef = useRef(currentUser?.isJackpot === true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isJackpot = currentUser?.isJackpot === true
|
|
||||||
|
|
||||||
if (!isHosting) {
|
if (!isHosting) {
|
||||||
previousJackpotRef.current = isJackpot
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,30 +121,24 @@ export function useAutoHostingRunner() {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
rules.stopIfSingleWinAbove.enabled &&
|
rules.stopIfSingleWinAbove.enabled &&
|
||||||
balanceAfterBet !== null &&
|
lastSingleWinAmount !== null &&
|
||||||
balance - balanceAfterBet > rules.stopIfSingleWinAbove.amount
|
lastSingleWinAmount > rules.stopIfSingleWinAbove.amount
|
||||||
) {
|
) {
|
||||||
stopHosting()
|
stopHosting()
|
||||||
notify.success(t('commonUi.toast.autoHostingStoppedWin'))
|
notify.success(t('commonUi.toast.autoHostingStoppedWin'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (rules.stopOnJackpot && lastIsJackpot === true) {
|
||||||
rules.stopOnJackpot &&
|
|
||||||
isJackpot &&
|
|
||||||
previousJackpotRef.current === false
|
|
||||||
) {
|
|
||||||
stopHosting()
|
stopHosting()
|
||||||
notify.success(t('commonUi.toast.autoHostingStoppedJackpot'))
|
notify.success(t('commonUi.toast.autoHostingStoppedJackpot'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
previousJackpotRef.current = isJackpot
|
|
||||||
}, [
|
}, [
|
||||||
balanceAfterBet,
|
|
||||||
currentUser?.coin,
|
currentUser?.coin,
|
||||||
currentUser?.isJackpot,
|
|
||||||
isHosting,
|
isHosting,
|
||||||
|
lastIsJackpot,
|
||||||
|
lastSingleWinAmount,
|
||||||
rules,
|
rules,
|
||||||
stopHosting,
|
stopHosting,
|
||||||
t,
|
t,
|
||||||
@@ -148,7 +147,7 @@ export function useAutoHostingRunner() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!isHosting ||
|
!isHosting ||
|
||||||
isSubmitting ||
|
inFlightRoundIdRef.current !== null ||
|
||||||
authStatus !== 'authenticated' ||
|
authStatus !== 'authenticated' ||
|
||||||
!currentUser ||
|
!currentUser ||
|
||||||
round.phase !== 'betting' ||
|
round.phase !== 'betting' ||
|
||||||
@@ -178,11 +177,10 @@ export function useAutoHostingRunner() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = false
|
const submittingRoundId = round.id
|
||||||
|
inFlightRoundIdRef.current = submittingRoundId
|
||||||
|
|
||||||
const submitAutoBet = async () => {
|
const submitAutoBet = async () => {
|
||||||
setIsSubmitting(true)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let latestBalance = currentUser.coin ?? '0'
|
let latestBalance = currentUser.coin ?? '0'
|
||||||
|
|
||||||
@@ -190,11 +188,14 @@ export function useAutoHostingRunner() {
|
|||||||
const uniqueNumbers = [...new Set(group.numbers)].sort(
|
const uniqueNumbers = [...new Set(group.numbers)].sort(
|
||||||
(left, right) => left - right,
|
(left, right) => left - right,
|
||||||
)
|
)
|
||||||
|
const formattedSingleBetAmount = formatBetAmount(group.amount)
|
||||||
const result = await placeGameBet({
|
const result = await placeGameBet({
|
||||||
|
bet_amount: formattedSingleBetAmount,
|
||||||
bet_id: group.betId,
|
bet_id: group.betId,
|
||||||
idempotency_key: createIdempotencyKey(),
|
idempotency_key: createIdempotencyKey(),
|
||||||
numbers: uniqueNumbers.join(','),
|
numbers: uniqueNumbers.join(','),
|
||||||
period_no: round.id,
|
period_no: round.id,
|
||||||
|
single_bet_amount: formattedSingleBetAmount,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.status !== 'accepted') {
|
if (result.status !== 'accepted') {
|
||||||
@@ -204,42 +205,44 @@ export function useAutoHostingRunner() {
|
|||||||
latestBalance = result.balance_after
|
latestBalance = result.balance_after
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cancelled) {
|
const latestHostingState = useGameAutoHostingStore.getState()
|
||||||
|
const latestUser = useAuthStore.getState().currentUser
|
||||||
|
|
||||||
|
if (
|
||||||
|
!latestHostingState.isHosting ||
|
||||||
|
latestHostingState.lastSubmittedRoundId === submittingRoundId ||
|
||||||
|
!latestUser
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentUser({
|
setCurrentUser({
|
||||||
...currentUser,
|
...latestUser,
|
||||||
coin: latestBalance,
|
coin: latestBalance,
|
||||||
lastBetPeriodNo: round.id,
|
lastBetPeriodNo: submittingRoundId,
|
||||||
})
|
})
|
||||||
markRoundSubmitted(round.id, parseBalance(latestBalance))
|
markRoundSubmitted(submittingRoundId, parseBalance(latestBalance))
|
||||||
clearSelections()
|
clearSelections()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!cancelled) {
|
if (useGameAutoHostingStore.getState().isHosting) {
|
||||||
stopHosting()
|
stopHosting()
|
||||||
notify.error(t('commonUi.toast.autoHostingSubmitFailed'), {
|
notify.error(t('commonUi.toast.autoHostingSubmitFailed'), {
|
||||||
description: error instanceof Error ? error.message : undefined,
|
description: error instanceof Error ? error.message : undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) {
|
if (inFlightRoundIdRef.current === submittingRoundId) {
|
||||||
setIsSubmitting(false)
|
inFlightRoundIdRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void submitAutoBet()
|
void submitAutoBet()
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
authStatus,
|
authStatus,
|
||||||
clearSelections,
|
clearSelections,
|
||||||
currentUser,
|
currentUser,
|
||||||
isHosting,
|
isHosting,
|
||||||
isSubmitting,
|
|
||||||
lastSubmittedRoundId,
|
lastSubmittedRoundId,
|
||||||
markRoundSubmitted,
|
markRoundSubmitted,
|
||||||
round.id,
|
round.id,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
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'
|
||||||
|
|
||||||
@@ -38,25 +38,33 @@ export function useFinanceRecordsVm({ enabled }: { enabled: boolean }) {
|
|||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
const locale = i18n.resolvedLanguage ?? i18n.language ?? 'en-US'
|
const locale = i18n.resolvedLanguage ?? i18n.language ?? 'en-US'
|
||||||
const [recordType, setRecordType] = useState<FinanceRecordType>('deposit')
|
const [recordType, setRecordType] = useState<FinanceRecordType>('deposit')
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useInfiniteQuery({
|
||||||
queryKey: ['finance', 'user-info-order-list', recordType, page],
|
queryKey: ['finance', 'user-info-order-list', recordType],
|
||||||
queryFn: () =>
|
initialPageParam: 1,
|
||||||
|
queryFn: ({ pageParam }) =>
|
||||||
recordType === 'deposit'
|
recordType === 'deposit'
|
||||||
? getDepositOrderList({
|
? getDepositOrderList({
|
||||||
page,
|
page: pageParam,
|
||||||
pageSize: FINANCE_RECORD_PAGE_SIZE,
|
pageSize: FINANCE_RECORD_PAGE_SIZE,
|
||||||
})
|
})
|
||||||
: getWithdrawOrderList({
|
: getWithdrawOrderList({
|
||||||
page,
|
page: pageParam,
|
||||||
pageSize: FINANCE_RECORD_PAGE_SIZE,
|
pageSize: FINANCE_RECORD_PAGE_SIZE,
|
||||||
}),
|
}),
|
||||||
enabled,
|
enabled,
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
const nextPage = lastPage.pagination.page + 1
|
||||||
|
const loadedCount =
|
||||||
|
lastPage.pagination.page * lastPage.pagination.page_size
|
||||||
|
|
||||||
|
return loadedCount < lastPage.pagination.total ? nextPage : undefined
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const pagination = query.data?.pagination
|
const lastPage = query.data?.pages.at(-1)
|
||||||
const total = pagination?.total ?? 0
|
const total = lastPage?.pagination.total ?? 0
|
||||||
|
const loadedPage = lastPage?.pagination.page ?? 1
|
||||||
|
|
||||||
const recordTypes = useMemo(
|
const recordTypes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -69,58 +77,46 @@ export function useFinanceRecordsVm({ enabled }: { enabled: boolean }) {
|
|||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(query.data?.list ?? []).map((item) => ({
|
(query.data?.pages ?? []).flatMap((page) =>
|
||||||
amountLabel: formatFinanceAmount(item.amount, locale),
|
page.list.map((item, index) => ({
|
||||||
bonusAmountLabel: formatFinanceAmount(item.bonusAmount, locale),
|
amountLabel: formatFinanceAmount(item.amount, locale),
|
||||||
id: item.orderNo,
|
bonusAmountLabel: formatFinanceAmount(item.bonusAmount, locale),
|
||||||
orderNoLabel: item.orderNo || '--',
|
id: item.orderNo || `${page.pagination.page}-${index}`,
|
||||||
})),
|
orderNoLabel: item.orderNo || '--',
|
||||||
[locale, query.data?.list],
|
})),
|
||||||
|
),
|
||||||
|
[locale, query.data?.pages],
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectRecordType = useCallback((type: FinanceRecordType) => {
|
const selectRecordType = useCallback((type: FinanceRecordType) => {
|
||||||
setRecordType(type)
|
setRecordType(type)
|
||||||
setPage(1)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const goPreviousPage = useCallback(() => {
|
|
||||||
setPage((currentPage) => Math.max(1, currentPage - 1))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const goNextPage = useCallback(() => {
|
|
||||||
setPage((currentPage) => currentPage + 1)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
setRecordType('deposit')
|
setRecordType('deposit')
|
||||||
setPage(1)
|
|
||||||
}
|
}
|
||||||
}, [enabled])
|
}, [enabled])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canGoNextPage: page * FINANCE_RECORD_PAGE_SIZE < total,
|
|
||||||
canGoPreviousPage: page > 1,
|
|
||||||
emptyText: t('game.modals.userInfo.financeRecords.empty'),
|
emptyText: t('game.modals.userInfo.financeRecords.empty'),
|
||||||
goNextPage,
|
fetchNextPage: query.fetchNextPage,
|
||||||
goPreviousPage,
|
hasNextPage: query.hasNextPage,
|
||||||
headers: {
|
headers: {
|
||||||
amount: t('game.modals.userInfo.financeRecords.amount'),
|
amount: t('game.modals.userInfo.financeRecords.amount'),
|
||||||
bonusAmount: t('game.modals.userInfo.financeRecords.bonusAmount'),
|
bonusAmount: t('game.modals.userInfo.financeRecords.bonusAmount'),
|
||||||
orderNo: t('game.modals.userInfo.financeRecords.orderNo'),
|
orderNo: t('game.modals.userInfo.financeRecords.orderNo'),
|
||||||
},
|
},
|
||||||
isError: query.isError,
|
isError: query.isError,
|
||||||
isFetching: query.isFetching,
|
isFetchingNextPage: query.isFetchingNextPage,
|
||||||
isLoading: query.isLoading,
|
isLoading: query.isLoading,
|
||||||
items,
|
items,
|
||||||
loadFailedText: t('game.modals.userInfo.financeRecords.loadFailed'),
|
loadFailedText: t('game.modals.userInfo.financeRecords.loadFailed'),
|
||||||
loadingText: t('game.modals.userInfo.financeRecords.loading'),
|
loadingText: t('game.modals.userInfo.financeRecords.loading'),
|
||||||
pageLabel: t('game.modals.userInfo.financeRecords.page', {
|
pageLabel: t('game.modals.userInfo.financeRecords.page', {
|
||||||
page: pagination?.page ?? page,
|
page: loadedPage,
|
||||||
total,
|
total,
|
||||||
}),
|
}),
|
||||||
nextText: t('game.modals.userInfo.financeRecords.next'),
|
|
||||||
previousText: t('game.modals.userInfo.financeRecords.previous'),
|
|
||||||
recordType,
|
recordType,
|
||||||
recordTypes,
|
recordTypes,
|
||||||
selectRecordType,
|
selectRecordType,
|
||||||
|
|||||||
@@ -62,10 +62,16 @@ export function useGameControlVm() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const chips = useGameRoundStore((state) => state.chips)
|
const chips = useGameRoundStore((state) => state.chips)
|
||||||
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
const activeChipId = useGameRoundStore((state) => state.activeChipId)
|
||||||
|
const activeBetQuantity = useGameRoundStore(
|
||||||
|
(state) => state.activeBetQuantity,
|
||||||
|
)
|
||||||
const round = useGameRoundStore((state) => state.round)
|
const round = useGameRoundStore((state) => state.round)
|
||||||
const maxSelectionCount = useGameRoundStore(
|
const maxSelectionCount = useGameRoundStore(
|
||||||
(state) => state.maxSelectionCount,
|
(state) => state.maxSelectionCount,
|
||||||
)
|
)
|
||||||
|
const adjustBetQuantity = useGameRoundStore(
|
||||||
|
(state) => state.adjustBetQuantity,
|
||||||
|
)
|
||||||
const selections = useGameRoundStore((state) => state.selections)
|
const selections = useGameRoundStore((state) => state.selections)
|
||||||
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
const clearSelections = useGameRoundStore((state) => state.clearSelections)
|
||||||
const restoreRecentSuccessfulSelections = useGameRoundStore(
|
const restoreRecentSuccessfulSelections = useGameRoundStore(
|
||||||
@@ -151,32 +157,10 @@ export function useGameControlVm() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupedSelections = selections.reduce<
|
const betId = toBetId(selections[0]?.chipId ?? activeChipId)
|
||||||
Map<string, { betId: number; numbers: number[] }>
|
const singleBetAmount = selections[0]?.amount ?? selectedChip?.amount ?? 0
|
||||||
>((accumulator, selection) => {
|
|
||||||
const betId = toBetId(selection.chipId)
|
|
||||||
|
|
||||||
if (betId === null) {
|
if (betId === null || singleBetAmount <= 0) {
|
||||||
return accumulator
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupKey = String(betId)
|
|
||||||
const current = accumulator.get(groupKey)
|
|
||||||
|
|
||||||
if (current) {
|
|
||||||
current.numbers.push(selection.cellId)
|
|
||||||
return accumulator
|
|
||||||
}
|
|
||||||
|
|
||||||
accumulator.set(groupKey, {
|
|
||||||
betId,
|
|
||||||
numbers: [selection.cellId],
|
|
||||||
})
|
|
||||||
|
|
||||||
return accumulator
|
|
||||||
}, new Map())
|
|
||||||
|
|
||||||
if (groupedSelections.size === 0) {
|
|
||||||
notify.warning(t('commonUi.toast.betUnavailable'))
|
notify.warning(t('commonUi.toast.betUnavailable'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -186,24 +170,25 @@ export function useGameControlVm() {
|
|||||||
try {
|
try {
|
||||||
let latestBalance = currentUser?.coin ?? '0'
|
let latestBalance = currentUser?.coin ?? '0'
|
||||||
|
|
||||||
for (const group of groupedSelections.values()) {
|
const uniqueNumbers = [
|
||||||
const uniqueNumbers = [...new Set(group.numbers)].sort(
|
...new Set(selections.map((item) => item.cellId)),
|
||||||
(left, right) => left - right,
|
].sort((left, right) => left - right)
|
||||||
)
|
const formattedSingleBetAmount = formatChipDisplayValue(singleBetAmount)
|
||||||
const result = await placeGameBet({
|
const result = await placeGameBet({
|
||||||
bet_id: group.betId,
|
bet_amount: formattedSingleBetAmount,
|
||||||
idempotency_key: createIdempotencyKey(),
|
bet_id: betId,
|
||||||
numbers: uniqueNumbers.join(','),
|
idempotency_key: createIdempotencyKey(),
|
||||||
period_no: round.id,
|
numbers: uniqueNumbers.join(','),
|
||||||
})
|
period_no: round.id,
|
||||||
|
single_bet_amount: formattedSingleBetAmount,
|
||||||
|
})
|
||||||
|
|
||||||
if (result.status !== 'accepted') {
|
if (result.status !== 'accepted') {
|
||||||
throw new Error(t('commonUi.toast.betRejected'))
|
throw new Error(t('commonUi.toast.betRejected'))
|
||||||
}
|
|
||||||
|
|
||||||
latestBalance = result.balance_after
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
latestBalance = result.balance_after
|
||||||
|
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
setCurrentUser({
|
setCurrentUser({
|
||||||
...currentUser,
|
...currentUser,
|
||||||
@@ -223,6 +208,7 @@ export function useGameControlVm() {
|
|||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
activeChipId,
|
||||||
authStatus,
|
authStatus,
|
||||||
clearSelections,
|
clearSelections,
|
||||||
confirmState,
|
confirmState,
|
||||||
@@ -233,6 +219,7 @@ export function useGameControlVm() {
|
|||||||
round.id,
|
round.id,
|
||||||
round.phase,
|
round.phase,
|
||||||
selections,
|
selections,
|
||||||
|
selectedChip?.amount,
|
||||||
setRecentSuccessfulSelections,
|
setRecentSuccessfulSelections,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
setModalOpen,
|
setModalOpen,
|
||||||
@@ -282,6 +269,7 @@ export function useGameControlVm() {
|
|||||||
round.phase === 'betting' &&
|
round.phase === 'betting' &&
|
||||||
!hasSubmittedCurrentRound &&
|
!hasSubmittedCurrentRound &&
|
||||||
!isAutoHosting,
|
!isAutoHosting,
|
||||||
|
canDecreaseBetQuantity: activeBetQuantity > 1,
|
||||||
confirmLabel:
|
confirmLabel:
|
||||||
confirmState === 'idle'
|
confirmState === 'idle'
|
||||||
? t('gameDesktop.control.selectNumbers')
|
? t('gameDesktop.control.selectNumbers')
|
||||||
@@ -293,12 +281,14 @@ export function useGameControlVm() {
|
|||||||
confirmState,
|
confirmState,
|
||||||
isConfirmClickable: confirmState === 'ready' && !isAutoHosting,
|
isConfirmClickable: confirmState === 'ready' && !isAutoHosting,
|
||||||
onChipSelect: selectChip,
|
onChipSelect: selectChip,
|
||||||
|
onDecreaseBetQuantity: () => adjustBetQuantity(-1),
|
||||||
|
onIncreaseBetQuantity: () => adjustBetQuantity(1),
|
||||||
onConfirm: handleConfirm,
|
onConfirm: handleConfirm,
|
||||||
onClearSelections: clearSelections,
|
onClearSelections: clearSelections,
|
||||||
onOpenAutoSetting: handleOpenAutoSetting,
|
onOpenAutoSetting: handleOpenAutoSetting,
|
||||||
onRepeatSelections: handleRepeatSelections,
|
onRepeatSelections: handleRepeatSelections,
|
||||||
maxSelectionCountLabel: maxSelectionCount,
|
maxSelectionCountLabel: maxSelectionCount,
|
||||||
selectedChipAmountLabel: selectedChip?.valueLabel ?? '--',
|
selectedBetQuantityLabel: activeBetQuantity,
|
||||||
selectedChipId: activeChipId,
|
selectedChipId: activeChipId,
|
||||||
selectedCountLabel: selections.length,
|
selectedCountLabel: selections.length,
|
||||||
totalBetAmountLabel: formatChipDisplayValue(totalBetAmount),
|
totalBetAmountLabel: formatChipDisplayValue(totalBetAmount),
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import {
|
|||||||
type GameSocketMessage,
|
type GameSocketMessage,
|
||||||
} from '@/lib/ws/game-socket-client'
|
} from '@/lib/ws/game-socket-client'
|
||||||
import { getAuthDeviceId, useAuthStore } from '@/store/auth'
|
import { getAuthDeviceId, useAuthStore } from '@/store/auth'
|
||||||
import { useGameRoundStore, useGameSessionStore } from '@/store/game'
|
import {
|
||||||
|
useGameAutoHostingStore,
|
||||||
|
useGameRoundStore,
|
||||||
|
useGameSessionStore,
|
||||||
|
} from '@/store/game'
|
||||||
import { getGameLobbyInit, normalizePeriodTickRound } from '../api/game-api'
|
import { getGameLobbyInit, normalizePeriodTickRound } from '../api/game-api'
|
||||||
import type {
|
import type {
|
||||||
BetWinEventDataDto,
|
BetWinEventDataDto,
|
||||||
@@ -606,6 +610,10 @@ function applyBetWinMessage(message: GameSocketMessage) {
|
|||||||
totalWin: betWinData.total_win,
|
totalWin: betWinData.total_win,
|
||||||
winningCellId: betWinData.result_number,
|
winningCellId: betWinData.result_number,
|
||||||
})
|
})
|
||||||
|
useGameAutoHostingStore.getState().recordBetWin({
|
||||||
|
isJackpot: betWinData.is_jackpot,
|
||||||
|
singleWinAmount: toOptionalNumber(betWinData.total_win) ?? null,
|
||||||
|
})
|
||||||
|
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
return
|
return
|
||||||
|
|||||||
106
src/features/game/hooks/use-wallet-records-vm.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { getWalletRecordList } from '@/features/game/api'
|
||||||
|
|
||||||
|
const WALLET_RECORD_PAGE_SIZE = 20
|
||||||
|
const WALLET_RECORD_TYPE = 'payout'
|
||||||
|
|
||||||
|
function formatWalletAmount(value: string, locale: string) {
|
||||||
|
const numberValue = Number(value)
|
||||||
|
|
||||||
|
if (!Number.isFinite(numberValue)) {
|
||||||
|
return value || '--'
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.NumberFormat(locale, {
|
||||||
|
maximumFractionDigits: 4,
|
||||||
|
}).format(numberValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWalletRecordTime(value: number | string | null) {
|
||||||
|
if (value === null || value === '') {
|
||||||
|
return '--'
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericValue = Number(value)
|
||||||
|
const timestamp =
|
||||||
|
Number.isFinite(numericValue) && numericValue > 0
|
||||||
|
? numericValue < 10_000_000_000
|
||||||
|
? numericValue * 1000
|
||||||
|
: numericValue
|
||||||
|
: value
|
||||||
|
const formatted = dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
|
||||||
|
return formatted === 'Invalid Date' ? String(value) : formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWalletRecordsVm({ enabled }: { enabled: boolean }) {
|
||||||
|
const { i18n, t } = useTranslation()
|
||||||
|
const locale = i18n.resolvedLanguage ?? i18n.language ?? 'en-US'
|
||||||
|
|
||||||
|
const query = useInfiniteQuery({
|
||||||
|
queryKey: ['finance', 'wallet-record-list', WALLET_RECORD_TYPE],
|
||||||
|
initialPageParam: 1,
|
||||||
|
queryFn: ({ pageParam }) =>
|
||||||
|
getWalletRecordList({
|
||||||
|
page: pageParam,
|
||||||
|
pageSize: WALLET_RECORD_PAGE_SIZE,
|
||||||
|
type: WALLET_RECORD_TYPE,
|
||||||
|
}),
|
||||||
|
enabled,
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
const nextPage = lastPage.pagination.page + 1
|
||||||
|
const loadedCount =
|
||||||
|
lastPage.pagination.page * lastPage.pagination.page_size
|
||||||
|
|
||||||
|
return loadedCount < lastPage.pagination.total ? nextPage : undefined
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastPage = query.data?.pages.at(-1)
|
||||||
|
const total = lastPage?.pagination.total ?? 0
|
||||||
|
const loadedPage = lastPage?.pagination.page ?? 1
|
||||||
|
|
||||||
|
const items = useMemo(
|
||||||
|
() =>
|
||||||
|
(query.data?.pages ?? []).flatMap((page) =>
|
||||||
|
page.list.map((item, index) => ({
|
||||||
|
amountLabel: formatWalletAmount(item.amount, locale),
|
||||||
|
balanceAfterLabel: formatWalletAmount(item.balanceAfter, locale),
|
||||||
|
balanceBeforeLabel: formatWalletAmount(item.balanceBefore, locale),
|
||||||
|
id: item.id || `${page.pagination.page}-${index}`,
|
||||||
|
remarkLabel: item.remark || '--',
|
||||||
|
timeLabel: formatWalletRecordTime(item.createdAt),
|
||||||
|
typeLabel: item.type || WALLET_RECORD_TYPE,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
[locale, query.data?.pages],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
emptyText: t('game.modals.userInfo.walletRecords.empty'),
|
||||||
|
fetchNextPage: query.fetchNextPage,
|
||||||
|
hasNextPage: query.hasNextPage,
|
||||||
|
headers: {
|
||||||
|
amount: t('game.modals.userInfo.walletRecords.amount'),
|
||||||
|
balanceAfter: t('game.modals.userInfo.walletRecords.balanceAfter'),
|
||||||
|
balanceBefore: t('game.modals.userInfo.walletRecords.balanceBefore'),
|
||||||
|
remark: t('game.modals.userInfo.walletRecords.remark'),
|
||||||
|
time: t('game.modals.userInfo.walletRecords.time'),
|
||||||
|
type: t('game.modals.userInfo.walletRecords.type'),
|
||||||
|
},
|
||||||
|
isError: query.isError,
|
||||||
|
isFetchingNextPage: query.isFetchingNextPage,
|
||||||
|
isLoading: query.isLoading,
|
||||||
|
items,
|
||||||
|
loadFailedText: t('game.modals.userInfo.walletRecords.loadFailed'),
|
||||||
|
loadingText: t('game.modals.userInfo.walletRecords.loading'),
|
||||||
|
pageLabel: t('game.modals.userInfo.walletRecords.page', {
|
||||||
|
page: loadedPage,
|
||||||
|
total,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,41 @@
|
|||||||
import { AnimatePresence, motion } from 'motion/react'
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||||
|
import { motion } from 'motion/react'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import { useFinanceRecordsVm } from '@/features/game/hooks/use-finance-records-vm'
|
import { useFinanceRecordsVm } from '@/features/game/hooks/use-finance-records-vm'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
function DesktopFinanceRecordsTab({ enabled }: { enabled: boolean }) {
|
function DesktopFinanceRecordsTab({ enabled }: { enabled: boolean }) {
|
||||||
const vm = useFinanceRecordsVm({ enabled })
|
const vm = useFinanceRecordsVm({ enabled })
|
||||||
|
const parentRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const rowVirtualizer = useVirtualizer({
|
||||||
|
count: vm.items.length + (vm.hasNextPage ? 1 : 0),
|
||||||
|
estimateSize: () => 72,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
overscan: 6,
|
||||||
|
})
|
||||||
|
const virtualItems = rowVirtualizer.getVirtualItems()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const lastItem = virtualItems.at(-1)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!lastItem ||
|
||||||
|
lastItem.index < vm.items.length - 1 ||
|
||||||
|
!vm.hasNextPage ||
|
||||||
|
vm.isFetchingNextPage
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void vm.fetchNextPage()
|
||||||
|
}, [
|
||||||
|
virtualItems,
|
||||||
|
vm.fetchNextPage,
|
||||||
|
vm.hasNextPage,
|
||||||
|
vm.isFetchingNextPage,
|
||||||
|
vm.items.length,
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex h-full w-full flex-col p-design-10'}>
|
<div className={'flex h-full w-full flex-col p-design-10'}>
|
||||||
@@ -26,7 +57,10 @@ function DesktopFinanceRecordsTab({ enabled }: { enabled: boolean }) {
|
|||||||
key={recordType.key}
|
key={recordType.key}
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
onClick={() => vm.selectRecordType(recordType.key)}
|
onClick={() => {
|
||||||
|
vm.selectRecordType(recordType.key)
|
||||||
|
rowVirtualizer.scrollToOffset(0)
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative h-design-44 min-w-design-130 cursor-pointer rounded-md px-design-16 text-design-18 transition-colors duration-200',
|
'relative h-design-44 min-w-design-130 cursor-pointer rounded-md px-design-16 text-design-18 transition-colors duration-200',
|
||||||
isActive
|
isActive
|
||||||
@@ -56,7 +90,7 @@ function DesktopFinanceRecordsTab({ enabled }: { enabled: boolean }) {
|
|||||||
<div className={'text-design-16 text-[#7ECAD1]'}>{vm.pageLabel}</div>
|
<div className={'text-design-16 text-[#7ECAD1]'}>{vm.pageLabel}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'min-h-0 flex-1 overflow-auto rounded-md'}>
|
<div className={'min-h-0 flex-1 rounded-md'}>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'grid grid-cols-[minmax(0,1.55fr)_minmax(0,0.9fr)_minmax(0,0.9fr)] gap-design-10 rounded-md border border-[#2B8CA3]/35 bg-[#031B24]/75 px-design-16 py-design-12 text-design-16 text-[#7ECAD1]'
|
'grid grid-cols-[minmax(0,1.55fr)_minmax(0,0.9fr)_minmax(0,0.9fr)] gap-design-10 rounded-md border border-[#2B8CA3]/35 bg-[#031B24]/75 px-design-16 py-design-12 text-design-16 text-[#7ECAD1]'
|
||||||
@@ -68,91 +102,78 @@ function DesktopFinanceRecordsTab({ enabled }: { enabled: boolean }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
ref={parentRef}
|
||||||
className={
|
className={
|
||||||
'mt-design-10 flex min-h-[calc(var(--design-unit)*320)] flex-col gap-design-10'
|
'mt-design-10 max-h-[calc(var(--design-unit)*320)] min-h-0 overflow-auto pr-design-4'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AnimatePresence initial={false} mode="wait">
|
{vm.isLoading ? (
|
||||||
<motion.div
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
key={vm.recordType}
|
{vm.loadingText}
|
||||||
className={'flex flex-col gap-design-10'}
|
</div>
|
||||||
initial={{
|
) : vm.isError ? (
|
||||||
opacity: 0,
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
x: vm.recordType === 'deposit' ? -18 : 18,
|
{vm.loadFailedText}
|
||||||
}}
|
</div>
|
||||||
animate={{ opacity: 1, x: 0 }}
|
) : vm.items.length === 0 ? (
|
||||||
exit={{
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
opacity: 0,
|
{vm.emptyText}
|
||||||
x: vm.recordType === 'deposit' ? 18 : -18,
|
</div>
|
||||||
}}
|
) : (
|
||||||
transition={{ duration: 0.18, ease: 'easeOut' }}
|
<div
|
||||||
|
className={'relative w-full'}
|
||||||
|
style={{ height: `${rowVirtualizer.getTotalSize()}px` }}
|
||||||
>
|
>
|
||||||
{vm.isLoading ? (
|
{virtualItems.map((virtualRow) => {
|
||||||
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
const item = vm.items[virtualRow.index]
|
||||||
{vm.loadingText}
|
|
||||||
</div>
|
return (
|
||||||
) : vm.isError ? (
|
<div
|
||||||
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
key={virtualRow.key}
|
||||||
{vm.loadFailedText}
|
className={'absolute left-0 top-0 w-full pb-design-10'}
|
||||||
</div>
|
style={{
|
||||||
) : vm.items.length === 0 ? (
|
height: `${virtualRow.size}px`,
|
||||||
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
{vm.emptyText}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
vm.items.map((item, index) => (
|
|
||||||
<motion.div
|
|
||||||
key={item.id}
|
|
||||||
className={
|
|
||||||
'grid grid-cols-[minmax(0,1.55fr)_minmax(0,0.9fr)_minmax(0,0.9fr)] items-center gap-design-10 rounded-md bg-[#0A4252] px-design-16 py-design-14 text-design-18 text-[#C4F2F7] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(108,205,207,0.05)]'
|
|
||||||
}
|
|
||||||
initial={{ opacity: 0, y: 6 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{
|
|
||||||
delay: Math.min(index, 6) * 0.025,
|
|
||||||
duration: 0.16,
|
|
||||||
ease: 'easeOut',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={'truncate font-medium text-white'}>
|
{item ? (
|
||||||
{item.orderNoLabel}
|
<motion.div
|
||||||
</div>
|
className={
|
||||||
<div className={'text-[#FEEEB0]'}>{item.amountLabel}</div>
|
'grid h-[calc(var(--design-unit)*62)] grid-cols-[minmax(0,1.55fr)_minmax(0,0.9fr)_minmax(0,0.9fr)] items-center gap-design-10 rounded-md bg-[#0A4252] px-design-16 py-design-14 text-design-18 text-[#C4F2F7] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(108,205,207,0.05)]'
|
||||||
<div className={'text-[#7CFFCF]'}>
|
}
|
||||||
{item.bonusAmountLabel}
|
initial={{ opacity: 0, y: 6 }}
|
||||||
</div>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
</motion.div>
|
transition={{
|
||||||
))
|
duration: 0.16,
|
||||||
)}
|
ease: 'easeOut',
|
||||||
</motion.div>
|
}}
|
||||||
</AnimatePresence>
|
>
|
||||||
|
<div className={'truncate font-medium text-white'}>
|
||||||
|
{item.orderNoLabel}
|
||||||
|
</div>
|
||||||
|
<div className={'truncate text-[#FEEEB0]'}>
|
||||||
|
{item.amountLabel}
|
||||||
|
</div>
|
||||||
|
<div className={'truncate text-[#7CFFCF]'}>
|
||||||
|
{item.bonusAmountLabel}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'flex h-[calc(var(--design-unit)*62)] items-center justify-center rounded-md bg-[#0A4252]/60 text-design-16 text-[#6CCDCF]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{vm.loadingText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
className={'mt-design-12 flex items-center justify-end gap-design-10'}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!vm.canGoPreviousPage || vm.isFetching}
|
|
||||||
onClick={vm.goPreviousPage}
|
|
||||||
className={
|
|
||||||
'h-design-40 cursor-pointer rounded-md border border-[#3EAFC7]/35 bg-[#062E39] px-design-16 text-design-16 text-[#86DAE7] transition hover:bg-[#0A4252] disabled:cursor-not-allowed disabled:opacity-45'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{vm.previousText}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!vm.canGoNextPage || vm.isFetching}
|
|
||||||
onClick={vm.goNextPage}
|
|
||||||
className={
|
|
||||||
'h-design-40 cursor-pointer rounded-md border border-[#3EAFC7]/35 bg-[#062E39] px-design-16 text-design-16 text-[#86DAE7] transition hover:bg-[#0A4252] disabled:cursor-not-allowed disabled:opacity-45'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{vm.nextText}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,53 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { ArrowLeft } from 'lucide-react'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import lengthBlueBtn from '@/assets/system/length-blue-btn.webp'
|
import blueBtnBg from '@/assets/system/blue-btn.webp'
|
||||||
import lengthGreenBtn from '@/assets/system/length-green-btn.webp'
|
|
||||||
import noticeBg from '@/assets/system/notice-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 { getNoticeDetail, getNoticeList } from '@/features/game/api'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import { useModalStore } from '@/store'
|
import { useModalStore } from '@/store'
|
||||||
|
|
||||||
|
type NoticeViewState = 'detail' | 'list'
|
||||||
|
|
||||||
function DesktopNoticeModal() {
|
function DesktopNoticeModal() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const open = useModalStore((state) => state.modals.desktopNotice)
|
const open = useModalStore((state) => state.modals.desktopNotice)
|
||||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||||
|
const [noticeView, setNoticeView] = useState<NoticeViewState>('list')
|
||||||
|
const [selectedNoticeId, setSelectedNoticeId] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const noticeListQuery = useQuery({
|
||||||
|
queryKey: ['game', 'notice-list'],
|
||||||
|
queryFn: () => getNoticeList(),
|
||||||
|
enabled: open && noticeView === 'list',
|
||||||
|
})
|
||||||
|
|
||||||
|
const noticeDetailQuery = useQuery({
|
||||||
|
queryKey: ['game', 'notice-detail', selectedNoticeId],
|
||||||
|
queryFn: () => getNoticeDetail(selectedNoticeId ?? 0),
|
||||||
|
enabled: open && noticeView === 'detail' && selectedNoticeId !== null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const noticeItems = useMemo(
|
||||||
|
() => noticeListQuery.data?.list ?? [],
|
||||||
|
[noticeListQuery.data],
|
||||||
|
)
|
||||||
|
|
||||||
|
async function handleReturnToList() {
|
||||||
|
setNoticeView('list')
|
||||||
|
setSelectedNoticeId(null)
|
||||||
|
await noticeListQuery.refetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setNoticeView('list')
|
||||||
|
setSelectedNoticeId(null)
|
||||||
|
}
|
||||||
|
}, [open])
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
setModalOpen('desktopNotice', false)
|
setModalOpen('desktopNotice', false)
|
||||||
@@ -22,54 +59,177 @@ function DesktopNoticeModal() {
|
|||||||
onClose={handleSubmit}
|
onClose={handleSubmit}
|
||||||
title={
|
title={
|
||||||
<div className={'modal-title-glow text-design-26'}>
|
<div className={'modal-title-glow text-design-26'}>
|
||||||
{t('game.modals.notice.title')}
|
{t('game.modals.userInfo.message.title')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
isNormalBg={true}
|
isNormalBg={true}
|
||||||
titleAlign="left"
|
titleAlign="left"
|
||||||
className={'w-design-1000 h-design-690'}
|
className={'w-design-980 h-design-690'}
|
||||||
>
|
>
|
||||||
<div className={'flex flex-col h-full w-full p-design-10 gap-design-30'}>
|
<div className={'flex h-full w-full flex-col'}>
|
||||||
<div
|
{noticeView === 'detail' ? (
|
||||||
className={
|
<div
|
||||||
'w-full h-design-440 overflow-y-auto flex flex-col gap-design-20 bg-[#000000]/50 p-design-10 rounded-md'
|
className={
|
||||||
}
|
'mb-design-12 flex items-center mx-design-10 my-design-10 rounded-md border border-[#3EAFC7]/40 bg-[#062E39]/80 px-design-14 py-design-12'
|
||||||
>
|
}
|
||||||
<SmartImage
|
>
|
||||||
className={'w-full h-design-300 shrink-0 rounded-md'}
|
<button
|
||||||
imgClassName={'object-contain object-center'}
|
type="button"
|
||||||
alt={'notice'}
|
onClick={() => {
|
||||||
src={noticeBg}
|
void handleReturnToList()
|
||||||
/>
|
}}
|
||||||
|
className={
|
||||||
<div className={'text-[#74B3BA] text-design-18 leading-[1.6]'}>
|
'flex cursor-pointer items-center gap-design-10 text-[#86DAE7] transition hover:text-white'
|
||||||
{t('game.modals.notice.content')}
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
'flex h-design-40 w-design-40 items-center justify-center rounded-full border border-[#4AC6DE]/45 bg-[#0B4454]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ArrowLeft className={'h-design-22 w-design-22'} />
|
||||||
|
</span>
|
||||||
|
<span className={'text-design-20 font-medium tracking-wide'}>
|
||||||
|
{t('game.modals.userInfo.message.back')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
<div className={'w-full flex justify-around'}>
|
|
||||||
<SmartBackground
|
|
||||||
src={lengthGreenBtn}
|
|
||||||
size="106% 108%"
|
|
||||||
repeat="no-repeat"
|
|
||||||
position="center"
|
|
||||||
className={
|
|
||||||
'w-design-270 h-design-72 pb-design-5 flex items-center justify-center text-design-20 font-bold'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('game.modals.notice.check')}
|
|
||||||
</SmartBackground>
|
|
||||||
|
|
||||||
<SmartBackground
|
<div className={'h-full w-full overflow-auto rounded-md'}>
|
||||||
src={lengthBlueBtn}
|
{noticeView === 'list' ? (
|
||||||
size="100% 90%"
|
<div
|
||||||
repeat="no-repeat"
|
className={
|
||||||
position="center"
|
'flex h-full w-full flex-col gap-design-10 p-design-10'
|
||||||
className={
|
}
|
||||||
'w-design-270 h-design-72 pb-design-5 flex items-center justify-center text-design-20 font-bold'
|
>
|
||||||
}
|
{noticeListQuery.isLoading ? (
|
||||||
>
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
{t('game.modals.notice.check')}
|
{t('game.modals.userInfo.message.loading')}
|
||||||
</SmartBackground>
|
</div>
|
||||||
|
) : noticeListQuery.isError ? (
|
||||||
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
|
{t('game.modals.userInfo.message.loadFailed')}
|
||||||
|
</div>
|
||||||
|
) : noticeItems.length === 0 ? (
|
||||||
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
|
{t('game.modals.userInfo.message.empty')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
noticeItems.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.notice_id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedNoticeId(item.notice_id)
|
||||||
|
setNoticeView('detail')
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
'flex cursor-pointer items-center gap-design-20 rounded-md bg-[#0A4252] px-design-15 py-design-15 text-left transition hover:bg-[#0E576D]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex h-design-95 w-design-95 items-center justify-center rounded-md text-design-18 font-bold',
|
||||||
|
item.notice_type === 'popout'
|
||||||
|
? 'bg-[#203C49] text-[#FEEEB0]'
|
||||||
|
: 'bg-[#111111] text-[#6CCDCF]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'absolute -right-design-8 top-design-8 z-10 min-w-design-50 -rotate-[8deg] rounded-[calc(var(--design-unit)*4)] border px-design-6 py-design-4 text-center text-design-11 font-semibold leading-none shadow-[0_0_calc(var(--design-unit)*8)_rgba(0,0,0,0.2)]',
|
||||||
|
item.is_read
|
||||||
|
? 'border-[#2D7384] bg-[linear-gradient(180deg,#20596A,#153A47)] text-[#B4E9F0]'
|
||||||
|
: 'border-[#9B6427] bg-[linear-gradient(180deg,#8A5320,#5E3616)] text-[#FFF0A8]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.is_read
|
||||||
|
? t('game.modals.userInfo.message.read')
|
||||||
|
: t('game.modals.userInfo.message.unread')}
|
||||||
|
</span>
|
||||||
|
{item.notice_type.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<div className={'min-w-0 flex-1'}>
|
||||||
|
<div className={'text-design-18 text-[#BFEAEC]'}>
|
||||||
|
{dayjs(item.publish_time * 1000).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'mt-design-4 flex items-center gap-design-12'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={'truncate text-design-20 text-white'}>
|
||||||
|
{item.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SmartBackground
|
||||||
|
src={blueBtnBg}
|
||||||
|
size="100% 100%"
|
||||||
|
className={
|
||||||
|
'flex h-design-64 w-design-150 items-center justify-center text-design-20 font-bold'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('game.modals.userInfo.message.check')}
|
||||||
|
</SmartBackground>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'flex h-full w-full flex-col gap-design-16 p-design-10'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{noticeDetailQuery.isLoading ? (
|
||||||
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
|
{t('game.modals.userInfo.message.loading')}
|
||||||
|
</div>
|
||||||
|
) : noticeDetailQuery.isError ? (
|
||||||
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
|
{t('game.modals.userInfo.message.loadFailed')}
|
||||||
|
</div>
|
||||||
|
) : noticeDetailQuery.data ? (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'rounded-md border border-[#2B8CA3]/45 bg-[linear-gradient(180deg,rgba(9,63,78,0.96)_0%,rgba(6,42,53,0.98)_100%)] p-design-24 shadow-[0_0_24px_rgba(14,108,132,0.16)]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'mb-design-14 inline-flex rounded-full border border-[#51BCD1]/35 bg-[#0A4252]/80 px-design-14 py-design-6 text-design-16 text-[#9CE8F2]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{dayjs(noticeDetailQuery.data.publish_time * 1000).format(
|
||||||
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'text-design-28 font-semibold leading-tight text-white'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{noticeDetailQuery.data.title}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'mt-design-18 whitespace-pre-wrap text-design-18 leading-[1.8] text-[#C4F2F7]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{noticeDetailQuery.data.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
|
{t('game.modals.userInfo.message.empty')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CenterModal>
|
</CenterModal>
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { ArrowLeft, CircleUserRound, Mail, ReceiptText } from 'lucide-react'
|
import {
|
||||||
|
CircleUserRound,
|
||||||
|
ClipboardList,
|
||||||
|
ReceiptText,
|
||||||
|
WalletCards,
|
||||||
|
} from 'lucide-react'
|
||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import avatar from '@/assets/system/avatar.webp'
|
import avatar from '@/assets/system/avatar.webp'
|
||||||
import blueBtnBg from '@/assets/system/blue-btn.webp'
|
|
||||||
import userInfoBg from '@/assets/system/userInfo-bg.webp'
|
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 { getNoticeDetail, getNoticeList } from '@/features/game/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 { notify } from '@/lib/notify'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useAuthStore, useModalStore } from '@/store'
|
import { useAuthStore, useModalStore } from '@/store'
|
||||||
|
|
||||||
type UserInfoTabKey = 'financeRecords' | 'message' | 'profile'
|
type UserInfoTabKey = 'financeRecords' | 'profile' | 'walletRecords'
|
||||||
type MessageViewState = 'list' | 'detail'
|
|
||||||
|
|
||||||
const USER_INFO_TABS: Array<{
|
const USER_INFO_TABS: Array<{
|
||||||
key: UserInfoTabKey
|
key: UserInfoTabKey
|
||||||
@@ -34,67 +37,81 @@ const USER_INFO_TABS: Array<{
|
|||||||
icon: ReceiptText,
|
icon: ReceiptText,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'message',
|
key: 'walletRecords',
|
||||||
labelKey: 'game.modals.userInfo.tabs.message',
|
labelKey: 'game.modals.userInfo.tabs.walletRecords',
|
||||||
icon: Mail,
|
icon: WalletCards,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const REGISTER_INVITE_CODE_QUERY_PARAM = 'registerInviteCode'
|
||||||
|
|
||||||
|
function createRegisterInviteUrl(inviteCode: string) {
|
||||||
|
const url = new URL(window.location.href)
|
||||||
|
|
||||||
|
url.searchParams.set(REGISTER_INVITE_CODE_QUERY_PARAM, inviteCode)
|
||||||
|
|
||||||
|
return url.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyTextToClipboard(text: string) {
|
||||||
|
if (navigator.clipboard?.writeText && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
|
||||||
|
textarea.value = text
|
||||||
|
textarea.setAttribute('readonly', '')
|
||||||
|
textarea.style.position = 'fixed'
|
||||||
|
textarea.style.left = '-9999px'
|
||||||
|
textarea.style.top = '-9999px'
|
||||||
|
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const copied = document.execCommand('copy')
|
||||||
|
|
||||||
|
if (!copied) {
|
||||||
|
throw new Error('Copy command failed')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function DesktopUserInfoModal() {
|
function DesktopUserInfoModal() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const open = useModalStore((state) => state.modals.desktopUserInfo)
|
const open = useModalStore((state) => state.modals.desktopUserInfo)
|
||||||
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
const setModalOpen = useModalStore((state) => state.setModalOpen)
|
||||||
const [activeTab, setActiveTab] = useState<UserInfoTabKey>('profile')
|
const [activeTab, setActiveTab] = useState<UserInfoTabKey>('profile')
|
||||||
const [messageView, setMessageView] = useState<MessageViewState>('list')
|
|
||||||
const [selectedNoticeId, setSelectedNoticeId] = useState<number | null>(null)
|
|
||||||
const currentUser = useAuthStore((state) => state.currentUser)
|
const currentUser = useAuthStore((state) => state.currentUser)
|
||||||
|
const inviteCode = currentUser?.registerInviteCode?.trim() ?? ''
|
||||||
const noticeListQuery = useQuery({
|
|
||||||
queryKey: ['game', 'notice-list'],
|
|
||||||
queryFn: () => getNoticeList(),
|
|
||||||
enabled: open && activeTab === 'message' && messageView === 'list',
|
|
||||||
})
|
|
||||||
|
|
||||||
const noticeDetailQuery = useQuery({
|
|
||||||
queryKey: ['game', 'notice-detail', selectedNoticeId],
|
|
||||||
queryFn: () => getNoticeDetail(selectedNoticeId ?? 0),
|
|
||||||
enabled:
|
|
||||||
open &&
|
|
||||||
activeTab === 'message' &&
|
|
||||||
messageView === 'detail' &&
|
|
||||||
selectedNoticeId !== null,
|
|
||||||
})
|
|
||||||
|
|
||||||
const noticeItems = useMemo(
|
|
||||||
() => noticeListQuery.data?.list ?? [],
|
|
||||||
[noticeListQuery.data],
|
|
||||||
)
|
|
||||||
|
|
||||||
async function handleReturnToList() {
|
|
||||||
setMessageView('list')
|
|
||||||
setSelectedNoticeId(null)
|
|
||||||
await noticeListQuery.refetch()
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setActiveTab('profile')
|
setActiveTab('profile')
|
||||||
setMessageView('list')
|
|
||||||
setSelectedNoticeId(null)
|
|
||||||
}
|
}
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeTab !== 'message') {
|
|
||||||
setMessageView('list')
|
|
||||||
setSelectedNoticeId(null)
|
|
||||||
}
|
|
||||||
}, [activeTab])
|
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
setModalOpen('desktopUserInfo', false)
|
setModalOpen('desktopUserInfo', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCopyInviteLink() {
|
||||||
|
if (!inviteCode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await copyTextToClipboard(createRegisterInviteUrl(inviteCode))
|
||||||
|
notify.success(t('commonUi.toast.inviteLinkCopied'))
|
||||||
|
} catch {
|
||||||
|
notify.error(t('commonUi.toast.inviteLinkCopyFailed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenterModal
|
<CenterModal
|
||||||
open={open}
|
open={open}
|
||||||
@@ -217,7 +234,7 @@ function DesktopUserInfoModal() {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'w-design-600 flex-1 text-design-18 rounded-md bg-[#000000]/40 flex flex-col gap-design-20 p-design-20 '
|
'w-design-600 flex-1 text-design-18 rounded-md bg-[#000000]/40 flex flex-col gap-design-20 p-design-20'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={'text-[#6CCDCF]'}>
|
<div className={'text-[#6CCDCF]'}>
|
||||||
@@ -225,19 +242,35 @@ function DesktopUserInfoModal() {
|
|||||||
<span
|
<span
|
||||||
className={'text-design-18 text-[#599AA3] ml-design-10'}
|
className={'text-design-18 text-[#599AA3] ml-design-10'}
|
||||||
>
|
>
|
||||||
{dayjs(currentUser?.createTime).format(
|
{currentUser?.createTime
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
? dayjs
|
||||||
) || '--'}
|
.unix(currentUser.createTime)
|
||||||
|
.format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
: '--'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'text-[#6CCDCF]'}>
|
<div className={'flex items-center text-[#6CCDCF]'}>
|
||||||
{t('auth.register.fields.inviteCode.label')}
|
<span>{t('auth.register.fields.inviteCode.label')}</span>
|
||||||
<span
|
<span
|
||||||
className={'text-design-18 text-[#599AA3] ml-design-10'}
|
className={'text-design-18 text-[#599AA3] ml-design-10'}
|
||||||
>
|
>
|
||||||
{currentUser?.registerInviteCode || '--'}
|
{inviteCode || '--'}
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
void handleCopyInviteLink()
|
||||||
|
}}
|
||||||
|
disabled={!inviteCode}
|
||||||
|
aria-label={t(
|
||||||
|
'game.modals.userInfo.profile.copyInviteLink',
|
||||||
|
)}
|
||||||
|
title={t('game.modals.userInfo.profile.copyInviteLink')}
|
||||||
|
className="ml-design-10 flex h-design-30 w-design-30 cursor-pointer items-center justify-center rounded-md border border-[#356E76] bg-[#0B2F35]/70 text-[#6CCDCF] transition-colors duration-200 hover:border-[#6CCDCF] hover:text-[#D9FFFF] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#6CCDCF] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-45"
|
||||||
|
>
|
||||||
|
<ClipboardList className="h-design-18 w-design-18" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SmartBackground>
|
</SmartBackground>
|
||||||
@@ -246,188 +279,9 @@ function DesktopUserInfoModal() {
|
|||||||
enabled={open && activeTab === 'financeRecords'}
|
enabled={open && activeTab === 'financeRecords'}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className={'flex h-full w-full flex-col'}>
|
<DesktopWalletRecordsTab
|
||||||
{messageView === 'detail' ? (
|
enabled={open && activeTab === 'walletRecords'}
|
||||||
<div
|
/>
|
||||||
className={
|
|
||||||
'mb-design-12 flex items-center mx-design-10 my-design-10 rounded-md border border-[#3EAFC7]/40 bg-[#062E39]/80 px-design-14 py-design-12'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
void handleReturnToList()
|
|
||||||
}}
|
|
||||||
className={
|
|
||||||
'flex cursor-pointer items-center gap-design-10 text-[#86DAE7] transition hover:text-white'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
'flex h-design-40 w-design-40 items-center justify-center rounded-full border border-[#4AC6DE]/45 bg-[#0B4454]'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ArrowLeft className={'h-design-22 w-design-22'} />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={'text-design-20 font-medium tracking-wide'}
|
|
||||||
>
|
|
||||||
{t('game.modals.userInfo.message.back')}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className={'h-full w-full overflow-auto rounded-md'}>
|
|
||||||
{messageView === 'list' ? (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'flex h-full w-full flex-col gap-design-10 p-design-10'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{noticeListQuery.isLoading ? (
|
|
||||||
<div
|
|
||||||
className={'py-design-30 text-center text-[#6CCDCF]'}
|
|
||||||
>
|
|
||||||
{t('game.modals.userInfo.message.loading')}
|
|
||||||
</div>
|
|
||||||
) : noticeListQuery.isError ? (
|
|
||||||
<div
|
|
||||||
className={'py-design-30 text-center text-[#6CCDCF]'}
|
|
||||||
>
|
|
||||||
{t('game.modals.userInfo.message.loadFailed')}
|
|
||||||
</div>
|
|
||||||
) : noticeItems.length === 0 ? (
|
|
||||||
<div
|
|
||||||
className={'py-design-30 text-center text-[#6CCDCF]'}
|
|
||||||
>
|
|
||||||
{t('game.modals.userInfo.message.empty')}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
noticeItems.map((item) => (
|
|
||||||
<button
|
|
||||||
key={item.notice_id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedNoticeId(item.notice_id)
|
|
||||||
setMessageView('detail')
|
|
||||||
}}
|
|
||||||
className={
|
|
||||||
'flex cursor-pointer items-center gap-design-20 rounded-md bg-[#0A4252] px-design-15 py-design-15 text-left transition hover:bg-[#0E576D]'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative flex h-design-95 w-design-95 items-center justify-center rounded-md text-design-18 font-bold',
|
|
||||||
item.notice_type === 'popout'
|
|
||||||
? 'bg-[#203C49] text-[#FEEEB0]'
|
|
||||||
: 'bg-[#111111] text-[#6CCDCF]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'absolute -right-design-8 top-design-8 z-10 min-w-design-50 -rotate-[8deg] rounded-[calc(var(--design-unit)*4)] border px-design-6 py-design-4 text-center text-design-11 font-semibold leading-none shadow-[0_0_calc(var(--design-unit)*8)_rgba(0,0,0,0.2)]',
|
|
||||||
item.is_read
|
|
||||||
? 'border-[#2D7384] bg-[linear-gradient(180deg,#20596A,#153A47)] text-[#B4E9F0]'
|
|
||||||
: 'border-[#9B6427] bg-[linear-gradient(180deg,#8A5320,#5E3616)] text-[#FFF0A8]',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.is_read
|
|
||||||
? t('game.modals.userInfo.message.read')
|
|
||||||
: t('game.modals.userInfo.message.unread')}
|
|
||||||
</span>
|
|
||||||
{item.notice_type.toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<div className={'min-w-0 flex-1'}>
|
|
||||||
<div className={'text-design-18 text-[#BFEAEC]'}>
|
|
||||||
{dayjs(item.publish_time * 1000).format(
|
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'mt-design-4 flex items-center gap-design-12'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={'truncate text-design-20 text-white'}
|
|
||||||
>
|
|
||||||
{item.title}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SmartBackground
|
|
||||||
src={blueBtnBg}
|
|
||||||
size="100% 100%"
|
|
||||||
className={
|
|
||||||
'flex h-design-64 w-design-150 items-center justify-center text-design-20 font-bold'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('game.modals.userInfo.message.check')}
|
|
||||||
</SmartBackground>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'flex h-full w-full flex-col gap-design-16 p-design-10'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{noticeDetailQuery.isLoading ? (
|
|
||||||
<div
|
|
||||||
className={'py-design-30 text-center text-[#6CCDCF]'}
|
|
||||||
>
|
|
||||||
{t('game.modals.userInfo.message.loading')}
|
|
||||||
</div>
|
|
||||||
) : noticeDetailQuery.isError ? (
|
|
||||||
<div
|
|
||||||
className={'py-design-30 text-center text-[#6CCDCF]'}
|
|
||||||
>
|
|
||||||
{t('game.modals.userInfo.message.loadFailed')}
|
|
||||||
</div>
|
|
||||||
) : noticeDetailQuery.data ? (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'rounded-md border border-[#2B8CA3]/45 bg-[linear-gradient(180deg,rgba(9,63,78,0.96)_0%,rgba(6,42,53,0.98)_100%)] p-design-24 shadow-[0_0_24px_rgba(14,108,132,0.16)]'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'mb-design-14 inline-flex rounded-full border border-[#51BCD1]/35 bg-[#0A4252]/80 px-design-14 py-design-6 text-design-16 text-[#9CE8F2]'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{dayjs(
|
|
||||||
noticeDetailQuery.data.publish_time * 1000,
|
|
||||||
).format('YYYY-MM-DD HH:mm:ss')}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'text-design-28 font-semibold leading-tight text-white'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{noticeDetailQuery.data.title}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'mt-design-18 whitespace-pre-wrap text-design-18 leading-[1.8] text-[#C4F2F7]'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{noticeDetailQuery.data.content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={'py-design-30 text-center text-[#6CCDCF]'}
|
|
||||||
>
|
|
||||||
{t('game.modals.userInfo.message.empty')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
148
src/features/game/modal/desktop/desktop-wallet-records-tab.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||||
|
import { motion } from 'motion/react'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
import { useWalletRecordsVm } from '@/features/game/hooks/use-wallet-records-vm'
|
||||||
|
|
||||||
|
function DesktopWalletRecordsTab({ enabled }: { enabled: boolean }) {
|
||||||
|
const vm = useWalletRecordsVm({ enabled })
|
||||||
|
const parentRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const rowVirtualizer = useVirtualizer({
|
||||||
|
count: vm.items.length + (vm.hasNextPage ? 1 : 0),
|
||||||
|
estimateSize: () => 72,
|
||||||
|
getScrollElement: () => parentRef.current,
|
||||||
|
overscan: 6,
|
||||||
|
})
|
||||||
|
const virtualItems = rowVirtualizer.getVirtualItems()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const lastItem = virtualItems.at(-1)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!lastItem ||
|
||||||
|
lastItem.index < vm.items.length - 1 ||
|
||||||
|
!vm.hasNextPage ||
|
||||||
|
vm.isFetchingNextPage
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void vm.fetchNextPage()
|
||||||
|
}, [
|
||||||
|
virtualItems,
|
||||||
|
vm.fetchNextPage,
|
||||||
|
vm.hasNextPage,
|
||||||
|
vm.isFetchingNextPage,
|
||||||
|
vm.items.length,
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'flex h-full w-full flex-col p-design-10'}>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'mb-design-12 flex items-center justify-between gap-design-16 rounded-md border border-[#3EAFC7]/40 bg-[#062E39]/80 px-design-14 py-design-12'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={'text-design-20 font-medium text-[#BFEAEC]'}>
|
||||||
|
{vm.headers.type}
|
||||||
|
</div>
|
||||||
|
<div className={'text-design-16 text-[#7ECAD1]'}>{vm.pageLabel}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'min-h-0 flex-1 rounded-md'}>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'grid grid-cols-[minmax(0,1.1fr)_minmax(0,0.75fr)_minmax(0,0.8fr)_minmax(0,0.8fr)_minmax(0,1fr)] gap-design-10 rounded-md border border-[#2B8CA3]/35 bg-[#031B24]/75 px-design-16 py-design-12 text-design-16 text-[#7ECAD1]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div>{vm.headers.time}</div>
|
||||||
|
<div>{vm.headers.amount}</div>
|
||||||
|
<div>{vm.headers.balanceBefore}</div>
|
||||||
|
<div>{vm.headers.balanceAfter}</div>
|
||||||
|
<div>{vm.headers.remark}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={parentRef}
|
||||||
|
className={
|
||||||
|
'mt-design-10 max-h-[calc(var(--design-unit)*320)] min-h-0 overflow-auto pr-design-4'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{vm.isLoading ? (
|
||||||
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
|
{vm.loadingText}
|
||||||
|
</div>
|
||||||
|
) : vm.isError ? (
|
||||||
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
|
{vm.loadFailedText}
|
||||||
|
</div>
|
||||||
|
) : vm.items.length === 0 ? (
|
||||||
|
<div className={'py-design-30 text-center text-[#6CCDCF]'}>
|
||||||
|
{vm.emptyText}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={'relative w-full'}
|
||||||
|
style={{ height: `${rowVirtualizer.getTotalSize()}px` }}
|
||||||
|
>
|
||||||
|
{virtualItems.map((virtualRow) => {
|
||||||
|
const item = vm.items[virtualRow.index]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={virtualRow.key}
|
||||||
|
className={'absolute left-0 top-0 w-full pb-design-10'}
|
||||||
|
style={{
|
||||||
|
height: `${virtualRow.size}px`,
|
||||||
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item ? (
|
||||||
|
<motion.div
|
||||||
|
className={
|
||||||
|
'grid h-[calc(var(--design-unit)*62)] grid-cols-[minmax(0,1.1fr)_minmax(0,0.75fr)_minmax(0,0.8fr)_minmax(0,0.8fr)_minmax(0,1fr)] items-center gap-design-10 rounded-md bg-[#0A4252] px-design-16 py-design-14 text-design-17 text-[#C4F2F7] shadow-[inset_0_0_calc(var(--design-unit)*10)_rgba(108,205,207,0.05)]'
|
||||||
|
}
|
||||||
|
initial={{ opacity: 0, y: 6 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.16,
|
||||||
|
ease: 'easeOut',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={'truncate text-[#BFEAEC]'}>
|
||||||
|
{item.timeLabel}
|
||||||
|
</div>
|
||||||
|
<div className={'truncate font-medium text-[#FEEEB0]'}>
|
||||||
|
{item.amountLabel}
|
||||||
|
</div>
|
||||||
|
<div className={'truncate text-[#86DAE7]'}>
|
||||||
|
{item.balanceBeforeLabel}
|
||||||
|
</div>
|
||||||
|
<div className={'truncate text-[#7CFFCF]'}>
|
||||||
|
{item.balanceAfterLabel}
|
||||||
|
</div>
|
||||||
|
<div className={'truncate text-white'}>
|
||||||
|
{item.remarkLabel}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'flex h-[calc(var(--design-unit)*62)] items-center justify-center rounded-md bg-[#0A4252]/60 text-design-16 text-[#6CCDCF]'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{vm.loadingText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DesktopWalletRecordsTab
|
||||||
@@ -24,12 +24,23 @@ interface NotificationStoreState {
|
|||||||
closingDialogId: string | null
|
closingDialogId: string | null
|
||||||
dialogQueue: NotificationDialog[]
|
dialogQueue: NotificationDialog[]
|
||||||
dismissDialog: (id?: string) => void
|
dismissDialog: (id?: string) => void
|
||||||
pushDialog: (dialog: NotificationDialog) => void
|
pushDialog: (dialog: NotificationDialog) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
const dialogTimers = new Map<string, number>()
|
const dialogTimers = new Map<string, number>()
|
||||||
const dialogExitTimers = new Map<string, number>()
|
const dialogExitTimers = new Map<string, number>()
|
||||||
|
|
||||||
|
function isSameDialog(
|
||||||
|
firstDialog: NotificationDialog,
|
||||||
|
secondDialog: NotificationDialog,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
firstDialog.type === secondDialog.type &&
|
||||||
|
firstDialog.message === secondDialog.message &&
|
||||||
|
(firstDialog.description ?? '') === (secondDialog.description ?? '')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function clearDialogTimer(id: string) {
|
function clearDialogTimer(id: string) {
|
||||||
const timerId = dialogTimers.get(id)
|
const timerId = dialogTimers.get(id)
|
||||||
|
|
||||||
@@ -129,6 +140,8 @@ export const useNotificationStore = create<NotificationStoreState>()((set) => ({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
pushDialog: (dialog) => {
|
pushDialog: (dialog) => {
|
||||||
|
let dialogId = dialog.id
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (!state.activeDialog) {
|
if (!state.activeDialog) {
|
||||||
clearDialogExitTimer(dialog.id)
|
clearDialogExitTimer(dialog.id)
|
||||||
@@ -140,6 +153,42 @@ export const useNotificationStore = create<NotificationStoreState>()((set) => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSameDialog(state.activeDialog, dialog)) {
|
||||||
|
dialogId = state.activeDialog.id
|
||||||
|
clearDialogExitTimer(state.activeDialog.id)
|
||||||
|
scheduleDialogDismiss(state.activeDialog.id, dialog.duration)
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeDialog: {
|
||||||
|
...state.activeDialog,
|
||||||
|
duration: dialog.duration,
|
||||||
|
},
|
||||||
|
closingDialogId: null,
|
||||||
|
dialogQueue: state.dialogQueue.filter(
|
||||||
|
(item) => !isSameDialog(item, dialog),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queuedDialog = state.dialogQueue.find((item) =>
|
||||||
|
isSameDialog(item, dialog),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (queuedDialog) {
|
||||||
|
dialogId = queuedDialog.id
|
||||||
|
|
||||||
|
return {
|
||||||
|
dialogQueue: state.dialogQueue.map((item) =>
|
||||||
|
item.id === queuedDialog.id
|
||||||
|
? {
|
||||||
|
...item,
|
||||||
|
duration: dialog.duration,
|
||||||
|
}
|
||||||
|
: item,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dialogQueue: [
|
dialogQueue: [
|
||||||
...state.dialogQueue.filter((item) => item.id !== dialog.id),
|
...state.dialogQueue.filter((item) => item.id !== dialog.id),
|
||||||
@@ -147,6 +196,8 @@ export const useNotificationStore = create<NotificationStoreState>()((set) => ({
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return dialogId
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -162,15 +213,13 @@ function showToast(
|
|||||||
const id = createToastId()
|
const id = createToastId()
|
||||||
const duration = options?.duration ?? DEFAULT_ALERT_DURATION_MS
|
const duration = options?.duration ?? DEFAULT_ALERT_DURATION_MS
|
||||||
|
|
||||||
useNotificationStore.getState().pushDialog({
|
return useNotificationStore.getState().pushDialog({
|
||||||
description: options?.description,
|
description: options?.description,
|
||||||
duration,
|
duration,
|
||||||
id,
|
id,
|
||||||
message,
|
message,
|
||||||
type,
|
type,
|
||||||
})
|
})
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const notify = {
|
export const notify = {
|
||||||
|
|||||||
@@ -152,12 +152,14 @@ export default {
|
|||||||
tabs: {
|
tabs: {
|
||||||
profile: 'Profile',
|
profile: 'Profile',
|
||||||
financeRecords: 'Top Up / Withdraw Records',
|
financeRecords: 'Top Up / Withdraw Records',
|
||||||
|
walletRecords: 'Wallet Records',
|
||||||
message: 'Messages',
|
message: 'Messages',
|
||||||
},
|
},
|
||||||
profile: {
|
profile: {
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
tel: 'Phone',
|
tel: 'Phone',
|
||||||
registeredAt: 'Registered at',
|
registeredAt: 'Registered at',
|
||||||
|
copyInviteLink: 'Copy invite link',
|
||||||
signature:
|
signature:
|
||||||
'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.',
|
||||||
},
|
},
|
||||||
@@ -187,6 +189,20 @@ export default {
|
|||||||
previous: 'Previous',
|
previous: 'Previous',
|
||||||
next: 'Next',
|
next: 'Next',
|
||||||
},
|
},
|
||||||
|
walletRecords: {
|
||||||
|
amount: 'Amount',
|
||||||
|
balanceAfter: 'After',
|
||||||
|
balanceBefore: 'Before',
|
||||||
|
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',
|
||||||
|
remark: 'Remark',
|
||||||
|
time: 'Time',
|
||||||
|
type: 'Wallet Records',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
withdrawTopup: {
|
withdrawTopup: {
|
||||||
applyWithdraw: 'Apply for Withdrawal',
|
applyWithdraw: 'Apply for Withdrawal',
|
||||||
@@ -229,6 +245,9 @@ export default {
|
|||||||
loginRequired: 'Please log in before entering the game',
|
loginRequired: 'Please log in before entering the game',
|
||||||
loginSuccess: 'Login successful',
|
loginSuccess: 'Login successful',
|
||||||
registerSuccess: 'Registration successful',
|
registerSuccess: 'Registration successful',
|
||||||
|
inviteLinkCopied: 'Invite link copied',
|
||||||
|
inviteLinkCopyFailed:
|
||||||
|
'Failed to copy invite link. Please copy it manually.',
|
||||||
insufficientBalance: 'Insufficient balance. Please adjust your bet.',
|
insufficientBalance: 'Insufficient balance. Please adjust your bet.',
|
||||||
betUnavailable: 'Betting is not available for this round',
|
betUnavailable: 'Betting is not available for this round',
|
||||||
betPlaced: 'Bet placed successfully',
|
betPlaced: 'Bet placed successfully',
|
||||||
|
|||||||
@@ -151,12 +151,14 @@ export default {
|
|||||||
tabs: {
|
tabs: {
|
||||||
profile: 'Profil',
|
profile: 'Profil',
|
||||||
financeRecords: 'Riwayat Isi Ulang / Penarikan',
|
financeRecords: 'Riwayat Isi Ulang / Penarikan',
|
||||||
|
walletRecords: 'Riwayat Dompet',
|
||||||
message: 'Pesan',
|
message: 'Pesan',
|
||||||
},
|
},
|
||||||
profile: {
|
profile: {
|
||||||
name: 'Nama',
|
name: 'Nama',
|
||||||
tel: 'Telepon',
|
tel: 'Telepon',
|
||||||
registeredAt: 'Tanggal daftar',
|
registeredAt: 'Tanggal daftar',
|
||||||
|
copyInviteLink: 'Salin tautan undangan',
|
||||||
signature:
|
signature:
|
||||||
'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.',
|
||||||
},
|
},
|
||||||
@@ -186,6 +188,20 @@ export default {
|
|||||||
previous: 'Sebelumnya',
|
previous: 'Sebelumnya',
|
||||||
next: 'Berikutnya',
|
next: 'Berikutnya',
|
||||||
},
|
},
|
||||||
|
walletRecords: {
|
||||||
|
amount: 'Jumlah',
|
||||||
|
balanceAfter: 'Sesudah',
|
||||||
|
balanceBefore: 'Sebelum',
|
||||||
|
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',
|
||||||
|
remark: 'Catatan',
|
||||||
|
time: 'Waktu',
|
||||||
|
type: 'Riwayat Dompet',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
withdrawTopup: {
|
withdrawTopup: {
|
||||||
applyWithdraw: 'Ajukan Penarikan',
|
applyWithdraw: 'Ajukan Penarikan',
|
||||||
@@ -228,6 +244,9 @@ export default {
|
|||||||
loginRequired: 'Silakan masuk sebelum memasuki game',
|
loginRequired: 'Silakan masuk sebelum memasuki game',
|
||||||
loginSuccess: 'Berhasil masuk',
|
loginSuccess: 'Berhasil masuk',
|
||||||
registerSuccess: 'Pendaftaran berhasil',
|
registerSuccess: 'Pendaftaran berhasil',
|
||||||
|
inviteLinkCopied: 'Tautan undangan disalin',
|
||||||
|
inviteLinkCopyFailed:
|
||||||
|
'Gagal menyalin tautan undangan. Silakan salin secara manual.',
|
||||||
insufficientBalance: 'Saldo tidak cukup. Silakan sesuaikan taruhan.',
|
insufficientBalance: 'Saldo tidak cukup. Silakan sesuaikan taruhan.',
|
||||||
betUnavailable: 'Taruhan tidak tersedia untuk ronde ini',
|
betUnavailable: 'Taruhan tidak tersedia untuk ronde ini',
|
||||||
betPlaced: 'Taruhan berhasil dikirim',
|
betPlaced: 'Taruhan berhasil dikirim',
|
||||||
|
|||||||
@@ -154,12 +154,14 @@ export default {
|
|||||||
tabs: {
|
tabs: {
|
||||||
profile: 'Profil',
|
profile: 'Profil',
|
||||||
financeRecords: 'Rekod Tambah Nilai / Pengeluaran',
|
financeRecords: 'Rekod Tambah Nilai / Pengeluaran',
|
||||||
|
walletRecords: 'Rekod Dompet',
|
||||||
message: 'Mesej',
|
message: 'Mesej',
|
||||||
},
|
},
|
||||||
profile: {
|
profile: {
|
||||||
name: 'Nama',
|
name: 'Nama',
|
||||||
tel: 'Telefon',
|
tel: 'Telefon',
|
||||||
registeredAt: 'Tarikh daftar',
|
registeredAt: 'Tarikh daftar',
|
||||||
|
copyInviteLink: 'Salin pautan jemputan',
|
||||||
signature:
|
signature:
|
||||||
'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.',
|
||||||
},
|
},
|
||||||
@@ -189,6 +191,20 @@ export default {
|
|||||||
previous: 'Sebelumnya',
|
previous: 'Sebelumnya',
|
||||||
next: 'Seterusnya',
|
next: 'Seterusnya',
|
||||||
},
|
},
|
||||||
|
walletRecords: {
|
||||||
|
amount: 'Jumlah',
|
||||||
|
balanceAfter: 'Selepas',
|
||||||
|
balanceBefore: 'Sebelum',
|
||||||
|
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',
|
||||||
|
remark: 'Catatan',
|
||||||
|
time: 'Masa',
|
||||||
|
type: 'Rekod Dompet',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
withdrawTopup: {
|
withdrawTopup: {
|
||||||
applyWithdraw: 'Mohon Pengeluaran',
|
applyWithdraw: 'Mohon Pengeluaran',
|
||||||
@@ -231,6 +247,9 @@ export default {
|
|||||||
loginRequired: 'Sila log masuk sebelum memasuki permainan',
|
loginRequired: 'Sila log masuk sebelum memasuki permainan',
|
||||||
loginSuccess: 'Log masuk berjaya',
|
loginSuccess: 'Log masuk berjaya',
|
||||||
registerSuccess: 'Pendaftaran berjaya',
|
registerSuccess: 'Pendaftaran berjaya',
|
||||||
|
inviteLinkCopied: 'Pautan jemputan telah disalin',
|
||||||
|
inviteLinkCopyFailed:
|
||||||
|
'Gagal menyalin pautan jemputan. Sila salin secara manual.',
|
||||||
insufficientBalance: 'Baki tidak mencukupi. Sila laraskan taruhan.',
|
insufficientBalance: 'Baki tidak mencukupi. Sila laraskan taruhan.',
|
||||||
betUnavailable: 'Taruhan tidak tersedia untuk pusingan ini',
|
betUnavailable: 'Taruhan tidak tersedia untuk pusingan ini',
|
||||||
betPlaced: 'Taruhan berjaya dihantar',
|
betPlaced: 'Taruhan berjaya dihantar',
|
||||||
|
|||||||
@@ -149,12 +149,14 @@ export default {
|
|||||||
tabs: {
|
tabs: {
|
||||||
profile: '个人信息',
|
profile: '个人信息',
|
||||||
financeRecords: '充值/提现记录',
|
financeRecords: '充值/提现记录',
|
||||||
|
walletRecords: '钱包流水',
|
||||||
message: '站内消息',
|
message: '站内消息',
|
||||||
},
|
},
|
||||||
profile: {
|
profile: {
|
||||||
name: '用户名',
|
name: '用户名',
|
||||||
tel: '电话',
|
tel: '电话',
|
||||||
registeredAt: '注册时间',
|
registeredAt: '注册时间',
|
||||||
|
copyInviteLink: '复制邀请链接',
|
||||||
signature: '我的个性签名和灵魂一样独特,后续这里会接真实资料字段。',
|
signature: '我的个性签名和灵魂一样独特,后续这里会接真实资料字段。',
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
@@ -182,6 +184,20 @@ export default {
|
|||||||
previous: '上一页',
|
previous: '上一页',
|
||||||
next: '下一页',
|
next: '下一页',
|
||||||
},
|
},
|
||||||
|
walletRecords: {
|
||||||
|
amount: '变动金额',
|
||||||
|
balanceAfter: '变动后',
|
||||||
|
balanceBefore: '变动前',
|
||||||
|
empty: '暂无钱包流水',
|
||||||
|
loadFailed: '钱包流水加载失败,请稍后重试',
|
||||||
|
loading: '钱包流水加载中...',
|
||||||
|
next: '下一页',
|
||||||
|
page: '第 {{page}} 页 / 共 {{total}} 条',
|
||||||
|
previous: '上一页',
|
||||||
|
remark: '备注',
|
||||||
|
time: '时间',
|
||||||
|
type: '钱包流水',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
withdrawTopup: {
|
withdrawTopup: {
|
||||||
applyWithdraw: '申请提现',
|
applyWithdraw: '申请提现',
|
||||||
@@ -223,6 +239,8 @@ export default {
|
|||||||
loginRequired: '请先登录后进入游戏',
|
loginRequired: '请先登录后进入游戏',
|
||||||
loginSuccess: '登录成功',
|
loginSuccess: '登录成功',
|
||||||
registerSuccess: '注册成功',
|
registerSuccess: '注册成功',
|
||||||
|
inviteLinkCopied: '邀请链接已复制',
|
||||||
|
inviteLinkCopyFailed: '邀请链接复制失败,请手动复制',
|
||||||
insufficientBalance: '余额不足,请调整下注金额',
|
insufficientBalance: '余额不足,请调整下注金额',
|
||||||
betUnavailable: '当前期不可下注',
|
betUnavailable: '当前期不可下注',
|
||||||
betPlaced: '下注成功',
|
betPlaced: '下注成功',
|
||||||
|
|||||||
@@ -24,10 +24,16 @@ export interface GameAutoHostingStoreState {
|
|||||||
balanceAfterBet: number | null
|
balanceAfterBet: number | null
|
||||||
completedRounds: number
|
completedRounds: number
|
||||||
isHosting: boolean
|
isHosting: boolean
|
||||||
|
lastIsJackpot: boolean | null
|
||||||
|
lastSingleWinAmount: number | null
|
||||||
lastSubmittedRoundId: string | null
|
lastSubmittedRoundId: string | null
|
||||||
rules: AutoHostingStopRules
|
rules: AutoHostingStopRules
|
||||||
selections: BetSelection[]
|
selections: BetSelection[]
|
||||||
markRoundSubmitted: (roundId: string, balanceAfterBet: number | null) => void
|
markRoundSubmitted: (roundId: string, balanceAfterBet: number | null) => void
|
||||||
|
recordBetWin: (input: {
|
||||||
|
isJackpot: boolean
|
||||||
|
singleWinAmount: number | null
|
||||||
|
}) => void
|
||||||
startHosting: (input: StartAutoHostingInput) => void
|
startHosting: (input: StartAutoHostingInput) => void
|
||||||
stopHosting: () => void
|
stopHosting: () => void
|
||||||
}
|
}
|
||||||
@@ -49,6 +55,8 @@ export const useGameAutoHostingStore = create<GameAutoHostingStoreState>()(
|
|||||||
balanceAfterBet: null,
|
balanceAfterBet: null,
|
||||||
completedRounds: 0,
|
completedRounds: 0,
|
||||||
isHosting: false,
|
isHosting: false,
|
||||||
|
lastIsJackpot: null,
|
||||||
|
lastSingleWinAmount: null,
|
||||||
lastSubmittedRoundId: null,
|
lastSubmittedRoundId: null,
|
||||||
rules: DEFAULT_AUTO_HOSTING_RULES,
|
rules: DEFAULT_AUTO_HOSTING_RULES,
|
||||||
selections: [],
|
selections: [],
|
||||||
@@ -65,11 +73,25 @@ export const useGameAutoHostingStore = create<GameAutoHostingStoreState>()(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
recordBetWin: ({ isJackpot, singleWinAmount }) => {
|
||||||
|
set((state) => {
|
||||||
|
if (!state.isHosting) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lastIsJackpot: isJackpot,
|
||||||
|
lastSingleWinAmount: singleWinAmount,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
startHosting: ({ balanceAfterBet, rules, selections }) => {
|
startHosting: ({ balanceAfterBet, rules, selections }) => {
|
||||||
set({
|
set({
|
||||||
balanceAfterBet,
|
balanceAfterBet,
|
||||||
completedRounds: 0,
|
completedRounds: 0,
|
||||||
isHosting: true,
|
isHosting: true,
|
||||||
|
lastIsJackpot: null,
|
||||||
|
lastSingleWinAmount: null,
|
||||||
lastSubmittedRoundId: null,
|
lastSubmittedRoundId: null,
|
||||||
rules,
|
rules,
|
||||||
selections: selections.map((selection) => ({
|
selections: selections.map((selection) => ({
|
||||||
@@ -82,6 +104,8 @@ export const useGameAutoHostingStore = create<GameAutoHostingStoreState>()(
|
|||||||
balanceAfterBet: null,
|
balanceAfterBet: null,
|
||||||
completedRounds: 0,
|
completedRounds: 0,
|
||||||
isHosting: false,
|
isHosting: false,
|
||||||
|
lastIsJackpot: null,
|
||||||
|
lastSingleWinAmount: null,
|
||||||
lastSubmittedRoundId: null,
|
lastSubmittedRoundId: null,
|
||||||
selections: [],
|
selections: [],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ type GameRoundSlice = Pick<
|
|||||||
| 'trends'
|
| 'trends'
|
||||||
>
|
>
|
||||||
|
|
||||||
|
const MIN_BET_QUANTITY = 1
|
||||||
|
|
||||||
export type RevealAnimationPhase = 'idle' | 'spinning' | 'stopping' | 'result'
|
export type RevealAnimationPhase = 'idle' | 'spinning' | 'stopping' | 'result'
|
||||||
export type RewardAnimationType = 'none' | 'small' | 'big'
|
export type RewardAnimationType = 'none' | 'small' | 'big'
|
||||||
|
|
||||||
@@ -102,8 +104,49 @@ function resolveRecentActiveChipId(
|
|||||||
DEFAULT_ACTIVE_CHIP_ID)
|
DEFAULT_ACTIVE_CHIP_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeBetQuantity(quantity: number) {
|
||||||
|
if (!Number.isFinite(quantity)) {
|
||||||
|
return MIN_BET_QUANTITY
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(MIN_BET_QUANTITY, Math.floor(quantity))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectionBetAmount(chip: Chip, quantity: number) {
|
||||||
|
return chip.amount * normalizeBetQuantity(quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSelectionsBetAmount(
|
||||||
|
selections: BetSelection[],
|
||||||
|
chipId: string,
|
||||||
|
amount: number,
|
||||||
|
) {
|
||||||
|
return selections.map((selection) => ({
|
||||||
|
...selection,
|
||||||
|
amount,
|
||||||
|
chipId,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSelectionQuantity(
|
||||||
|
selections: BetSelection[],
|
||||||
|
chips: Chip[],
|
||||||
|
activeChipId: string,
|
||||||
|
) {
|
||||||
|
const chip = getChipById(chips, activeChipId)
|
||||||
|
const firstSelection = selections[0]
|
||||||
|
|
||||||
|
if (!chip || !firstSelection || chip.amount <= 0) {
|
||||||
|
return MIN_BET_QUANTITY
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeBetQuantity(firstSelection.amount / chip.amount)
|
||||||
|
}
|
||||||
|
|
||||||
export interface GameRoundStoreState extends GameRoundSlice {
|
export interface GameRoundStoreState extends GameRoundSlice {
|
||||||
activeChipId: string
|
activeChipId: string
|
||||||
|
activeBetQuantity: number
|
||||||
|
adjustBetQuantity: (delta: number) => void
|
||||||
clearSelections: () => void
|
clearSelections: () => void
|
||||||
clearRewardAnimation: () => void
|
clearRewardAnimation: () => void
|
||||||
finishRevealAnimation: () => void
|
finishRevealAnimation: () => void
|
||||||
@@ -135,6 +178,7 @@ export interface GameRoundStoreState extends GameRoundSlice {
|
|||||||
|
|
||||||
function createInitialRoundState(): GameRoundSlice & {
|
function createInitialRoundState(): GameRoundSlice & {
|
||||||
activeChipId: string
|
activeChipId: string
|
||||||
|
activeBetQuantity: number
|
||||||
recentSuccessfulSelections: BetSelection[]
|
recentSuccessfulSelections: BetSelection[]
|
||||||
revealAnimation: RevealAnimationState
|
revealAnimation: RevealAnimationState
|
||||||
} {
|
} {
|
||||||
@@ -142,6 +186,7 @@ function createInitialRoundState(): GameRoundSlice & {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
activeChipId: DEFAULT_ACTIVE_CHIP_ID,
|
activeChipId: DEFAULT_ACTIVE_CHIP_ID,
|
||||||
|
activeBetQuantity: MIN_BET_QUANTITY,
|
||||||
cells: snapshot.cells,
|
cells: snapshot.cells,
|
||||||
chips: snapshot.chips,
|
chips: snapshot.chips,
|
||||||
history: snapshot.history,
|
history: snapshot.history,
|
||||||
@@ -156,6 +201,30 @@ function createInitialRoundState(): GameRoundSlice & {
|
|||||||
|
|
||||||
export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
||||||
...createInitialRoundState(),
|
...createInitialRoundState(),
|
||||||
|
adjustBetQuantity: (delta) => {
|
||||||
|
set((state) => {
|
||||||
|
const activeChip =
|
||||||
|
getChipById(state.chips, state.activeChipId) ??
|
||||||
|
state.chips.find((chip) => chip.isDefault) ??
|
||||||
|
state.chips[0]
|
||||||
|
const nextQuantity = normalizeBetQuantity(state.activeBetQuantity + delta)
|
||||||
|
|
||||||
|
if (!activeChip) {
|
||||||
|
return {
|
||||||
|
activeBetQuantity: nextQuantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeBetQuantity: nextQuantity,
|
||||||
|
selections: syncSelectionsBetAmount(
|
||||||
|
state.selections,
|
||||||
|
activeChip.id,
|
||||||
|
getSelectionBetAmount(activeChip, nextQuantity),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
clearSelections: () => {
|
clearSelections: () => {
|
||||||
set({ selections: [] })
|
set({ selections: [] })
|
||||||
},
|
},
|
||||||
@@ -211,24 +280,29 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
hydrateRound: (snapshot) => {
|
hydrateRound: (snapshot) => {
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
activeChipId: getChipById(snapshot.chips, state.activeChipId)
|
const nextActiveChipId = getChipById(snapshot.chips, state.activeChipId)
|
||||||
? state.activeChipId
|
? state.activeChipId
|
||||||
: (snapshot.chips.find((chip) => chip.isDefault)?.id ??
|
: (snapshot.chips.find((chip) => chip.isDefault)?.id ??
|
||||||
snapshot.chips[0]?.id ??
|
snapshot.chips[0]?.id ??
|
||||||
DEFAULT_ACTIVE_CHIP_ID),
|
DEFAULT_ACTIVE_CHIP_ID)
|
||||||
cells: snapshot.cells,
|
|
||||||
chips: snapshot.chips,
|
return {
|
||||||
history: snapshot.history,
|
activeChipId: nextActiveChipId,
|
||||||
maxSelectionCount: snapshot.maxSelectionCount,
|
cells: snapshot.cells,
|
||||||
revealAnimation:
|
chips: snapshot.chips,
|
||||||
snapshot.round.phase === 'betting' || snapshot.round.phase === 'waiting'
|
history: snapshot.history,
|
||||||
? createIdleRevealAnimationPreservingReward(state.revealAnimation)
|
maxSelectionCount: snapshot.maxSelectionCount,
|
||||||
: state.revealAnimation,
|
revealAnimation:
|
||||||
round: snapshot.round,
|
snapshot.round.phase === 'betting' ||
|
||||||
selections: snapshot.selections,
|
snapshot.round.phase === 'waiting'
|
||||||
trends: snapshot.trends,
|
? createIdleRevealAnimationPreservingReward(state.revealAnimation)
|
||||||
}))
|
: state.revealAnimation,
|
||||||
|
round: snapshot.round,
|
||||||
|
selections: snapshot.selections,
|
||||||
|
trends: snapshot.trends,
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
placeBet: (cellId) => {
|
placeBet: (cellId) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -256,7 +330,7 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
|||||||
selections: [
|
selections: [
|
||||||
...state.selections,
|
...state.selections,
|
||||||
{
|
{
|
||||||
amount: activeChip.amount,
|
amount: getSelectionBetAmount(activeChip, state.activeBetQuantity),
|
||||||
cellId,
|
cellId,
|
||||||
chipId: activeChip.id,
|
chipId: activeChip.id,
|
||||||
id: `bet-${cellId}-${state.selections.length + 1}-${Date.now()}`,
|
id: `bet-${cellId}-${state.selections.length + 1}-${Date.now()}`,
|
||||||
@@ -337,13 +411,28 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextActiveChipId = resolveRecentActiveChipId(
|
||||||
|
state.chips,
|
||||||
|
nextSelections,
|
||||||
|
state.activeChipId,
|
||||||
|
)
|
||||||
|
const nextActiveChip = getChipById(state.chips, nextActiveChipId)
|
||||||
|
const nextBetQuantity = resolveSelectionQuantity(
|
||||||
|
nextSelections,
|
||||||
|
state.chips,
|
||||||
|
nextActiveChipId,
|
||||||
|
)
|
||||||
|
|
||||||
set({
|
set({
|
||||||
activeChipId: resolveRecentActiveChipId(
|
activeBetQuantity: nextBetQuantity,
|
||||||
state.chips,
|
activeChipId: nextActiveChipId,
|
||||||
nextSelections,
|
selections: nextActiveChip
|
||||||
state.activeChipId,
|
? syncSelectionsBetAmount(
|
||||||
),
|
nextSelections,
|
||||||
selections: nextSelections,
|
nextActiveChipId,
|
||||||
|
getSelectionBetAmount(nextActiveChip, nextBetQuantity),
|
||||||
|
)
|
||||||
|
: nextSelections,
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -357,11 +446,21 @@ export const useGameRoundStore = create<GameRoundStoreState>()((set) => ({
|
|||||||
},
|
},
|
||||||
selectChip: (chipId) => {
|
selectChip: (chipId) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (!getChipById(state.chips, chipId)) {
|
const nextChip = getChipById(state.chips, chipId)
|
||||||
|
|
||||||
|
if (!nextChip) {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
return { activeChipId: chipId }
|
return {
|
||||||
|
activeBetQuantity: MIN_BET_QUANTITY,
|
||||||
|
activeChipId: chipId,
|
||||||
|
selections: syncSelectionsBetAmount(
|
||||||
|
state.selections,
|
||||||
|
chipId,
|
||||||
|
getSelectionBetAmount(nextChip, MIN_BET_QUANTITY),
|
||||||
|
),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setPhase: (phase) => {
|
setPhase: (phase) => {
|
||||||
|
|||||||
@@ -394,7 +394,7 @@
|
|||||||
.gold-reveal-shell {
|
.gold-reveal-shell {
|
||||||
--gold-angle: 0deg;
|
--gold-angle: 0deg;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: calc(var(--design-unit) * 3.2);
|
inset: calc(var(--design-unit) * 2.6);
|
||||||
border-radius: calc(var(--design-unit) * 16);
|
border-radius: calc(var(--design-unit) * 16);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
clip-path: inset(0 round calc(var(--design-unit) * 16));
|
clip-path: inset(0 round calc(var(--design-unit) * 16));
|
||||||
@@ -406,7 +406,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
border-radius: calc(var(--design-unit) * 16);
|
border-radius: calc(var(--design-unit) * 16);
|
||||||
padding: calc(var(--design-unit) * 2.6);
|
padding: calc(var(--design-unit) * 5.6);
|
||||||
background: conic-gradient(
|
background: conic-gradient(
|
||||||
from var(--gold-angle),
|
from var(--gold-angle),
|
||||||
#534217 10%,
|
#534217 10%,
|
||||||
@@ -428,9 +428,9 @@
|
|||||||
.gold-reveal-shell::after {
|
.gold-reveal-shell::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: calc(var(--design-unit) * 0.8);
|
inset: calc(var(--design-unit) * 1);
|
||||||
border-radius: calc(var(--design-unit) * 15);
|
border-radius: calc(var(--design-unit) * 15);
|
||||||
padding: calc(var(--design-unit) * 1.8);
|
padding: calc(var(--design-unit) * 4);
|
||||||
background: conic-gradient(
|
background: conic-gradient(
|
||||||
from calc(var(--gold-angle) + 180deg),
|
from calc(var(--gold-angle) + 180deg),
|
||||||
rgba(255, 247, 210, 0) 0%,
|
rgba(255, 247, 210, 0) 0%,
|
||||||
@@ -452,12 +452,12 @@
|
|||||||
|
|
||||||
.gold-reveal-static-border {
|
.gold-reveal-static-border {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: calc(var(--design-unit) * 3.2);
|
inset: calc(var(--design-unit) * 2.8);
|
||||||
border-radius: calc(var(--design-unit) * 16);
|
border-radius: calc(var(--design-unit) * 16);
|
||||||
border: calc(var(--design-unit) * 3.8) solid rgba(181, 138, 40, 0.98);
|
border: calc(var(--design-unit) * 7.4) solid rgba(181, 138, 40, 0.98);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 0 calc(var(--design-unit) * 10) rgba(255, 241, 181, 0.28),
|
inset 0 0 calc(var(--design-unit) * 14) rgba(255, 241, 181, 0.38),
|
||||||
0 0 calc(var(--design-unit) * 12) rgba(255, 210, 102, 0.2);
|
0 0 calc(var(--design-unit) * 18) rgba(255, 210, 102, 0.32);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||