diff --git a/apps/api/src/applications/player/player.controller.ts b/apps/api/src/applications/player/player.controller.ts index f2f52c4..ec16982 100644 --- a/apps/api/src/applications/player/player.controller.ts +++ b/apps/api/src/applications/player/player.controller.ts @@ -306,13 +306,14 @@ export class PlayerController { @CurrentUser('locale') locale: string, @Query('status') status?: string, @Query('page') page?: string, + @Query('pageSize') pageSize?: string, @Query('matchId') matchId?: string, ) { const result = await this.bets.getUserBets( userId, status, page ? parseInt(page, 10) : 1, - 20, + pageSize ? parseInt(pageSize, 10) : 20, matchId ? BigInt(matchId) : undefined, ); const items = await this.matches.enrichBetsForHistory(result.items, locale); @@ -347,9 +348,10 @@ export class PlayerController { async transactions( @CurrentUser('id') userId: bigint, @Query('page') page?: string, + @Query('pageSize') pageSize?: string, @Query('type') type?: string, ) { - const result = await this.wallet.getTransactions(userId, page ? parseInt(page) : 1, 20, type); + const result = await this.wallet.getTransactions(userId, page ? parseInt(page) : 1, pageSize ? parseInt(pageSize) : 20, type); return jsonResponse(result); } @@ -424,10 +426,12 @@ export class PlayerController { async myDepositOrders( @CurrentUser('id') userId: bigint, @Query('page') page?: string, + @Query('pageSize') pageSize?: string, ) { const result = await this.deposit.getPlayerDepositOrders( userId, page ? parseInt(page, 10) : 1, + pageSize ? parseInt(pageSize, 10) : 20, ); return jsonResponse(result); } diff --git a/apps/player/scripts/check-desktop-i18n.mjs b/apps/player/scripts/check-desktop-i18n.mjs new file mode 100644 index 0000000..25c1455 --- /dev/null +++ b/apps/player/scripts/check-desktop-i18n.mjs @@ -0,0 +1,44 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.join(__dirname, '..'); + +function flatten(obj, prefix = '') { + const out = {}; + for (const [k, v] of Object.entries(obj)) { + const key = prefix ? `${prefix}.${k}` : k; + if (v && typeof v === 'object' && !Array.isArray(v)) Object.assign(out, flatten(v, key)); + else out[key] = v; + } + return out; +} + +function extractKeysFromDir(dir) { + const keys = new Set(); + const re = /\bt\s*\(\s*['"`]([^'"`]+)['"`]/g; + function walk(d) { + for (const ent of fs.readdirSync(d, { withFileTypes: true })) { + const p = path.join(d, ent.name); + if (ent.isDirectory()) walk(p); + else if (ent.name.endsWith('.vue')) { + const src = fs.readFileSync(p, 'utf8'); + let m; + while ((m = re.exec(src))) keys.add(m[1]); + } + } + } + walk(dir); + return keys; +} + +const zhMod = await import('../src/i18n/zh-CN.ts'); +const zh = flatten(zhMod.default); +const dirs = ['src/views/desktop', 'src/components/desktop'].map((d) => path.join(root, d)); +const used = new Set(); +for (const d of dirs) extractKeysFromDir(d).forEach((k) => used.add(k)); + +const missing = [...used].filter((k) => !(k in zh)).sort(); +console.log(`Missing keys (${missing.length}):`); +missing.forEach((k) => console.log(` ${k}`)); diff --git a/apps/player/src/App.vue b/apps/player/src/App.vue index f4ac569..4acb453 100644 --- a/apps/player/src/App.vue +++ b/apps/player/src/App.vue @@ -1,8 +1,10 @@ diff --git a/apps/player/src/assets/images/pcbg.webp b/apps/player/src/assets/images/pcbg.webp new file mode 100644 index 0000000..693abbc Binary files /dev/null and b/apps/player/src/assets/images/pcbg.webp differ diff --git a/apps/player/src/components/AppToast.vue b/apps/player/src/components/AppToast.vue new file mode 100644 index 0000000..6b68d8c --- /dev/null +++ b/apps/player/src/components/AppToast.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/apps/player/src/components/BetSlipDrawer.vue b/apps/player/src/components/BetSlipDrawer.vue index fe166bc..11d295d 100644 --- a/apps/player/src/components/BetSlipDrawer.vue +++ b/apps/player/src/components/BetSlipDrawer.vue @@ -11,8 +11,10 @@ import { import { useAuthStore } from '../stores/auth'; import { formatMoney, parseAmount } from '../utils/localeDisplay'; import BetSuccessOverlay from './BetSuccessOverlay.vue'; +import ConfirmDialog from './ConfirmDialog.vue'; import api from '../api'; import { usePlayerProfile } from '../composables/usePlayerProfile'; +import { buildBetPlaceConfirmMessage } from '../utils/betPlaceConfirmMessage'; const props = defineProps<{ modelValue: boolean }>(); const emit = defineEmits<{ 'update:modelValue': [boolean] }>(); @@ -33,6 +35,8 @@ const balance = ref(null); const error = ref(''); const success = ref(''); const showSuccess = ref(false); +const showPlaceConfirm = ref(false); +const placeConfirmMessage = ref(''); const MIN_STAKE = 5; const MAX_STAKE_INTEGER_LENGTH = 9; const stakeInput = ref(''); @@ -345,34 +349,54 @@ function oddsTrendClass(delta: OddsDelta) { return delta.newOdds >= delta.oldOdds ? 'odds-up' : 'odds-down'; } -async function placeBet() { - if (!activeItems.value.length) return; +function validatePlaceBet(): boolean { + if (!activeItems.value.length) return false; if (!auth.token) { auth.showLoginPrompt(); - return; + return false; } if (slip.stake < MIN_STAKE) { error.value = t('bet.slip_min_error', { amount: MIN_STAKE }); - return; + return false; } if (balance.value != null && slip.stake > balance.value) { error.value = t('bet.outright_insufficient'); - return; + return false; } if (activeTab.value === 'parlay' && !canSubmitActive.value) { error.value = slip.parlayItems.length > PARLAY_MAX_LEGS ? t('bet.parlay_max_legs') : t('bet.parlay_need_more'); - return; + return false; } if (hasSuspendedSelections.value) { error.value = t('bet.odds_suspended'); - return; + return false; } + return true; +} + +function onPlaceBetClick() { + error.value = ''; + if (!validatePlaceBet()) return; if (hasPendingOddsChanges.value) { acceptPendingOdds(); } + const items = activeTab.value === 'parlay' ? [...slip.parlayItems] : activeItems.value; + placeConfirmMessage.value = buildBetPlaceConfirmMessage(t, { + mode: activeTab.value, + items, + totalStake: Number(slip.stake) || 0, + totalReturn: activeEstimatedReturn.value, + totalOdds: activeTab.value === 'parlay' ? activeTotalOdds.value : undefined, + formatMoney: (amount) => formatMoney(amount, locale.value), + getStake: () => Number(slip.stake) || 0, + getOdds: (item) => effectiveOdds(item), + }); + showPlaceConfirm.value = true; +} +async function executePlaceBet() { loading.value = true; error.value = ''; success.value = ''; @@ -414,6 +438,11 @@ async function placeBet() { } } +async function confirmPlaceBet() { + showPlaceConfirm.value = false; + await executePlaceBet(); +} + watch( () => props.modelValue, (open) => { @@ -622,7 +651,7 @@ watch( type="button" class="btn-primary" :disabled="loading || !canSubmitWithOdds" - @click="placeBet" + @click="onPlaceBetClick" > {{ submitButtonLabel }} @@ -630,6 +659,16 @@ watch( + + diff --git a/apps/player/src/components/CashBalanceChip.vue b/apps/player/src/components/CashBalanceChip.vue index d104b25..6a42e6f 100644 --- a/apps/player/src/components/CashBalanceChip.vue +++ b/apps/player/src/components/CashBalanceChip.vue @@ -76,6 +76,9 @@ onUnmounted(() => { +
@@ -105,6 +108,32 @@ onUnmounted(() => { .cash-chip-wrap { position: relative; z-index: 120; + display: flex; + align-items: center; + gap: 6px; +} + +.direct-recharge-btn { + height: 36px; + padding: 0 12px; + border-radius: 6px; + background: var(--primary); + color: var(--tertiary); + font-size: 13px; + font-weight: 700; + border: none; + cursor: pointer; + white-space: nowrap; +} + +.direct-recharge-btn:active { + opacity: 0.8; +} + +@media (max-width: 1023px) { + .direct-recharge-btn { + display: none; + } } .cash-chip { diff --git a/apps/player/src/components/ConfirmDialog.vue b/apps/player/src/components/ConfirmDialog.vue index 9dc1d3e..e2422ba 100644 --- a/apps/player/src/components/ConfirmDialog.vue +++ b/apps/player/src/components/ConfirmDialog.vue @@ -89,7 +89,7 @@ function onConfirm() { .confirm-overlay { position: fixed; inset: 0; - z-index: 1000; + z-index: 1100; display: flex; align-items: center; justify-content: center; @@ -125,6 +125,7 @@ function onConfirm() { line-height: 1.6; color: #c8c8c8; text-align: center; + white-space: pre-line; } .confirm-actions { diff --git a/apps/player/src/components/FloatingMailbox.vue b/apps/player/src/components/FloatingMailbox.vue new file mode 100644 index 0000000..331f457 --- /dev/null +++ b/apps/player/src/components/FloatingMailbox.vue @@ -0,0 +1,350 @@ + + + + + diff --git a/apps/player/src/components/HomeAnnouncementCard.vue b/apps/player/src/components/HomeAnnouncementCard.vue new file mode 100644 index 0000000..f7ef060 --- /dev/null +++ b/apps/player/src/components/HomeAnnouncementCard.vue @@ -0,0 +1,248 @@ + + + + + diff --git a/apps/player/src/components/MessageListPanel.vue b/apps/player/src/components/MessageListPanel.vue index a93d947..5c65d25 100644 --- a/apps/player/src/components/MessageListPanel.vue +++ b/apps/player/src/components/MessageListPanel.vue @@ -12,6 +12,9 @@ import { stripHtml } from '../utils/html'; const router = useRouter(); const { t, locale } = useI18n(); const auth = useAuthStore(); +const emit = defineEmits<{ + (e: 'click-message', id: string): void; +}>(); const { messages, loading, @@ -50,7 +53,7 @@ function messagePreview(item: PlayerMessage) { } function openDetail(id: string) { - router.push(`/messages/${id}`); + emit('click-message', id); } async function fetchPage(nextPage: number, append = false) { @@ -178,8 +181,6 @@ onActivated(tryLoad); diff --git a/apps/player/src/components/desktop/AccountSideNav.vue b/apps/player/src/components/desktop/AccountSideNav.vue new file mode 100644 index 0000000..748611d --- /dev/null +++ b/apps/player/src/components/desktop/AccountSideNav.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/apps/player/src/components/desktop/BetSlipPanel.vue b/apps/player/src/components/desktop/BetSlipPanel.vue new file mode 100644 index 0000000..6f364ec --- /dev/null +++ b/apps/player/src/components/desktop/BetSlipPanel.vue @@ -0,0 +1,1967 @@ + + + + + diff --git a/apps/player/src/components/desktop/DataTable.vue b/apps/player/src/components/desktop/DataTable.vue new file mode 100644 index 0000000..4df4261 --- /dev/null +++ b/apps/player/src/components/desktop/DataTable.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopBanner3DCarousel.vue b/apps/player/src/components/desktop/DesktopBanner3DCarousel.vue new file mode 100644 index 0000000..c03d518 --- /dev/null +++ b/apps/player/src/components/desktop/DesktopBanner3DCarousel.vue @@ -0,0 +1,468 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopOddsBetPopover.vue b/apps/player/src/components/desktop/DesktopOddsBetPopover.vue new file mode 100644 index 0000000..c933bae --- /dev/null +++ b/apps/player/src/components/desktop/DesktopOddsBetPopover.vue @@ -0,0 +1,400 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopOutrightEventCard.vue b/apps/player/src/components/desktop/DesktopOutrightEventCard.vue new file mode 100644 index 0000000..9bc9501 --- /dev/null +++ b/apps/player/src/components/desktop/DesktopOutrightEventCard.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopShell.vue b/apps/player/src/components/desktop/DesktopShell.vue new file mode 100644 index 0000000..1ed6b87 --- /dev/null +++ b/apps/player/src/components/desktop/DesktopShell.vue @@ -0,0 +1,131 @@ + + + diff --git a/apps/player/src/components/desktop/DesktopTopNav.vue b/apps/player/src/components/desktop/DesktopTopNav.vue new file mode 100644 index 0000000..fb4d940 --- /dev/null +++ b/apps/player/src/components/desktop/DesktopTopNav.vue @@ -0,0 +1,243 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopWalletSubNav.vue b/apps/player/src/components/desktop/DesktopWalletSubNav.vue new file mode 100644 index 0000000..db241ef --- /dev/null +++ b/apps/player/src/components/desktop/DesktopWalletSubNav.vue @@ -0,0 +1,46 @@ + + + diff --git a/apps/player/src/components/desktop/LeagueSidebar.vue b/apps/player/src/components/desktop/LeagueSidebar.vue new file mode 100644 index 0000000..fec7edf --- /dev/null +++ b/apps/player/src/components/desktop/LeagueSidebar.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/apps/player/src/components/desktop/MatchSidebar.vue b/apps/player/src/components/desktop/MatchSidebar.vue new file mode 100644 index 0000000..1a77efd --- /dev/null +++ b/apps/player/src/components/desktop/MatchSidebar.vue @@ -0,0 +1,342 @@ + + + + + diff --git a/apps/player/src/components/desktop/Pagination.vue b/apps/player/src/components/desktop/Pagination.vue new file mode 100644 index 0000000..253f4a9 --- /dev/null +++ b/apps/player/src/components/desktop/Pagination.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/apps/player/src/components/desktop/SportsCategoryBar.vue b/apps/player/src/components/desktop/SportsCategoryBar.vue new file mode 100644 index 0000000..a48786e --- /dev/null +++ b/apps/player/src/components/desktop/SportsCategoryBar.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/apps/player/src/components/match-detail/CorrectScorePanel.vue b/apps/player/src/components/match-detail/CorrectScorePanel.vue index 5e38d01..ce07e18 100644 --- a/apps/player/src/components/match-detail/CorrectScorePanel.vue +++ b/apps/player/src/components/match-detail/CorrectScorePanel.vue @@ -15,10 +15,12 @@ const props = defineProps<{ }>; isSelected: (id: string) => boolean; locked?: boolean; + /** PC 全宽紧凑:更小单元格、三列均分 */ + dense?: boolean; }>(); const emit = defineEmits<{ - pick: [id: string]; + pick: [id: string, event?: MouseEvent]; }>(); const { t } = useI18n(); @@ -34,9 +36,9 @@ const columns = computed(() => ), ); -function onPick(sel: CsSelection) { +function onPick(sel: CsSelection, event?: MouseEvent) { if (props.locked) return; - emit('pick', sel.id); + emit('pick', sel.id, event); } function formatOdds(odds: string) { @@ -46,7 +48,7 @@ function formatOdds(odds: string) {