diff --git a/apps/api/src/applications/player/player.controller.ts b/apps/api/src/applications/player/player.controller.ts index f2f52c4..0a20eb0 100644 --- a/apps/api/src/applications/player/player.controller.ts +++ b/apps/api/src/applications/player/player.controller.ts @@ -189,8 +189,8 @@ export class PlayerController { const [banners, announcements, allMatches, upcomingMatches, inboxEnabled] = await Promise.all([ this.content.listActive('BANNER', locale), this.content.listActiveAnnouncements(locale), - this.matches.listPublished(locale, undefined, { includeMarkets: false }), - this.matches.listUpcomingPublished(locale), + this.matches.listPublished(locale, undefined, { includeMarkets: true }), + this.matches.listUpcomingPublished(locale, { includeMarkets: true }), this.systemConfig.getInboxFeatureEnabled(), ]); const timeZone = safeTimeZone(headerTimeZone); diff --git a/apps/api/src/domains/catalog/matches.service.ts b/apps/api/src/domains/catalog/matches.service.ts index e0139e1..e048cfb 100644 --- a/apps/api/src/domains/catalog/matches.service.ts +++ b/apps/api/src/domains/catalog/matches.service.ts @@ -1595,10 +1595,11 @@ export class MatchesService { /** 未来 N 天内开赛的已发布赛事(按开赛时间升序,不限 isHot) */ async listUpcomingPublished( locale = 'en-US', - options?: { limit?: number; days?: number }, + options?: { limit?: number; days?: number; includeMarkets?: boolean }, ) { const limit = options?.limit ?? 50; const days = options?.days ?? 3; + const includeMarkets = options?.includeMarkets ?? false; const now = new Date(); const end = new Date(now); end.setDate(end.getDate() + days); @@ -1625,14 +1626,14 @@ export class MatchesService { homeTeam: true, awayTeam: true, score: true, - markets: this.playerMarketStatusInclude, + markets: includeMarkets ? this.playerMarketInclude : this.playerMarketStatusInclude, }, orderBy: [{ startTime: 'asc' }, { displayOrder: 'asc' }], take: limit, }); return Promise.all( - matches.map((m) => this.enrichMatch(m, locale, { omitMarkets: true })), + matches.map((m) => this.enrichMatch(m, locale, { omitMarkets: !includeMarkets })), ); } 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/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/CashBalanceChip.vue b/apps/player/src/components/CashBalanceChip.vue index 4951456..e37a983 100644 --- a/apps/player/src/components/CashBalanceChip.vue +++ b/apps/player/src/components/CashBalanceChip.vue @@ -7,6 +7,8 @@ import { useAuthStore } from '../stores/auth'; import { formatMoney } from '../utils/localeDisplay'; import { usePlayerProfile } from '../composables/usePlayerProfile'; +const props = withDefaults(defineProps<{ hideAvatar?: boolean }>(), { hideAvatar: false }); + const { locale, t } = useI18n(); const { profileRaw, refreshProfile, avatarUrl } = usePlayerProfile(); const auth = useAuthStore(); @@ -84,8 +86,8 @@ onUnmounted(() => { {{ available }} - - + + diff --git a/apps/player/src/components/CustomerServicePanel.vue b/apps/player/src/components/CustomerServicePanel.vue index 93404df..b3c2701 100644 --- a/apps/player/src/components/CustomerServicePanel.vue +++ b/apps/player/src/components/CustomerServicePanel.vue @@ -46,7 +46,6 @@ const iframeSrc = computed(() => { min-height: 0; display: flex; flex-direction: column; - margin: 0 -16px; background: var(--bg-body); } @@ -55,6 +54,6 @@ const iframeSrc = computed(() => { width: 100%; min-height: calc(100dvh - 140px); border: 0; - background: #fff; + background: var(--bg-body); } diff --git a/apps/player/src/components/FloatingMailbox.vue b/apps/player/src/components/FloatingMailbox.vue new file mode 100644 index 0000000..f5d0b6c --- /dev/null +++ b/apps/player/src/components/FloatingMailbox.vue @@ -0,0 +1,471 @@ + + + + + diff --git a/apps/player/src/components/HomeAnnouncementCard.vue b/apps/player/src/components/HomeAnnouncementCard.vue new file mode 100644 index 0000000..650816a --- /dev/null +++ b/apps/player/src/components/HomeAnnouncementCard.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/apps/player/src/components/MatchBetCard.vue b/apps/player/src/components/MatchBetCard.vue index c66cc72..828e5a3 100644 --- a/apps/player/src/components/MatchBetCard.vue +++ b/apps/player/src/components/MatchBetCard.vue @@ -1,10 +1,14 @@ @@ -132,7 +290,7 @@ const liveScoreText = computed(() => { .match-card { position: relative; background: var(--gradient-card); - border: 1px solid rgba(255, 255, 255, 0.11); + border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px 10px 10px; display: flex; @@ -140,6 +298,7 @@ const liveScoreText = computed(() => { align-items: center; gap: 10px; min-width: 0; + min-height: 130px; overflow: hidden; cursor: pointer; box-shadow: var(--shadow-card-sm); @@ -173,6 +332,7 @@ const liveScoreText = computed(() => { align-items: center; width: 100%; gap: 4px; + transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), padding 0.3s ease; } .team { @@ -183,6 +343,7 @@ const liveScoreText = computed(() => { gap: 6px; min-width: 0; transform: translateY(8px); + transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); } .team-name { @@ -199,6 +360,7 @@ const liveScoreText = computed(() => { padding: 0 6px; align-self: stretch; letter-spacing: -0.02em; + transition: font-size 0.25s ease; } .match-card--phase .team-name { @@ -211,12 +373,14 @@ const liveScoreText = computed(() => { object-fit: cover; border-radius: 3px; display: block; + transition: transform 0.25s ease, opacity 0.2s ease, height 0.25s ease, width 0.25s ease, margin 0.25s ease; } .team-flag.flag-logo { width: 48px; height: 48px; object-fit: contain; + transition: transform 0.25s ease, opacity 0.2s ease, height 0.25s ease, width 0.25s ease, margin 0.25s ease; } .center-col { @@ -226,7 +390,7 @@ const liveScoreText = computed(() => { align-items: center; justify-content: center; gap: 5px; - min-width: 72px; + min-width: 56px; } .kickoff { @@ -238,6 +402,7 @@ const liveScoreText = computed(() => { text-align: center; max-width: 96px; overflow-wrap: anywhere; + transition: opacity 0.2s ease, height 0.2s ease, margin 0.2s ease; } .live-score { @@ -246,35 +411,43 @@ const liveScoreText = computed(() => { color: var(--text); line-height: 1; letter-spacing: 0.02em; + transition: font-size 0.25s ease; +} + +.vs { + font-size: 22px; + font-weight: 900; + color: var(--text); + letter-spacing: 0.08em; + line-height: 1; + transition: font-size 0.25s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); } .status-tag { position: absolute; top: 0; - left: 0; - right: auto; + right: 0; z-index: 3; font-size: 10px; - font-weight: 600; - padding: 4px 10px 4px 8px; - border-radius: 0 0 10px 0; + font-weight: 700; + padding: 3px 10px; + border-radius: 0 6px 0 8px; line-height: 1.3; white-space: nowrap; - letter-spacing: 0; + letter-spacing: 0.02em; +} + +.status-tag--open { + background: rgba(255, 255, 255, 0.12); + color: var(--primary-light); + border: 1px solid var(--border-gold-soft); } .status-tag--settled { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 20, 40, 0.7) 100%); + background: rgba(255, 255, 255, 0.06); color: var(--text-muted); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - border-right: 1px solid rgba(255, 255, 255, 0.1); -} - -.status-tag--pending { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(10, 53, 88, 0.55) 100%); - color: var(--text-muted); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - border-right: 1px solid rgba(255, 255, 255, 0.1); + border-bottom: 1px solid var(--border); + border-left: 1px solid var(--border); } .bet-btn { @@ -287,6 +460,8 @@ const liveScoreText = computed(() => { font-size: 13px; letter-spacing: 0.04em; line-height: 1.2; + overflow: hidden; + transition: opacity 0.2s ease, transform 0.2s ease, height 0.25s ease, padding 0.25s ease, min-width 0.25s ease, min-height 0.25s ease, margin 0.25s ease; } .bet-btn--view { @@ -294,4 +469,184 @@ const liveScoreText = computed(() => { border: 1px solid var(--border); color: var(--text-muted); } + +/* --- Morphing Quick Bet panel --- */ +.card-quick-bet { + position: absolute; + left: 8px; + right: 8px; + bottom: 8px; + display: flex; + flex-direction: column; + gap: 4px; + opacity: 0; + pointer-events: none; + transform: translateY(12px); + transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); + z-index: 5; +} + +@media (hover: hover) { + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) { + border-color: var(--border-gold); + box-shadow: var(--shadow-sm); + } + + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .teams-row { + transform: translateY(0); + padding: 0 40px; + } + + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .team { + transform: translateY(0); + gap: 3px; + } + + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .team-flag { + width: 40px; + height: 28px; + } + + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .team-flag.flag-logo { + width: 28px; + height: 28px; + } + + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .team-name { + font-size: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + } + + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .kickoff { + opacity: 0; + height: 0; + margin: 0; + overflow: hidden; + pointer-events: none; + } + + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .vs, + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .live-score { + font-size: 16px; + } + + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .bet-btn { + opacity: 0; + pointer-events: none; + } + + .match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .card-quick-bet { + opacity: 1; + pointer-events: auto; + transform: translateY(0); + } + + .match-card.no-quick-bet:hover { + border-color: var(--border-gold); + box-shadow: var(--shadow-sm); + } +} + +.quick-bet-tabs { + display: flex; + align-items: center; + border-bottom: 1px solid var(--border); + padding-bottom: 4px; + margin-bottom: 2px; +} + +.quick-bet-tab-btn { + padding: 2px 6px; + font-size: 9px; + font-weight: 700; + color: var(--text-muted); + background: transparent; + border: none; + cursor: pointer; + border-radius: 3px; + transition: all 0.15s; +} + +.quick-bet-tab-btn:hover { + color: var(--primary-light); +} + +.quick-bet-tab-btn.active { + color: var(--primary-light); + background: var(--border-gold-soft); +} + +.quick-bet-all-link { + margin-left: auto; + font-size: 9px; + font-weight: 700; + color: var(--primary-light); + cursor: pointer; + padding: 2px 4px; + transition: opacity 0.15s; +} + +.quick-bet-all-link:hover { + opacity: 0.7; +} + +.quick-bet-odds { + display: flex; + align-items: center; + justify-content: center; + width: 100%; +} + +.quick-bet-odds :deep(.odds-btn) { + min-height: 26px !important; + padding: 1px 4px !important; + gap: 1px !important; +} + +.quick-bet-odds :deep(.odds) { + font-size: 11px !important; +} + +.quick-bet-odds :deep(.label) { + font-size: 8px !important; +} + +.quick-bet-no-odds { + font-size: 10px; + color: var(--text-muted); +} + +/* --- VS Selection Highlight: scale selected team --- */ +.teams-row .team { + transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease; +} +.teams-row.home .team:first-child { + transform: scale(1.08); + filter: brightness(1.3) saturate(1.2); + z-index: 2; +} +.teams-row.home .team:last-child { + opacity: 0.35 !important; + transform: scale(0.94); + filter: grayscale(0.6) brightness(0.7); +} + +.teams-row.away .team:last-child { + transform: scale(1.08); + filter: brightness(1.3) saturate(1.2); + z-index: 2; +} +.teams-row.away .team:first-child { + opacity: 0.35 !important; + transform: scale(0.94); + filter: grayscale(0.6) brightness(0.7); +} + +.teams-row.draw .team { + transform: scale(1.05); + filter: brightness(1.15) saturate(1.1); +} diff --git a/apps/player/src/components/MessageListPanel.vue b/apps/player/src/components/MessageListPanel.vue index 0fca05b..c84cf4d 100644 --- a/apps/player/src/components/MessageListPanel.vue +++ b/apps/player/src/components/MessageListPanel.vue @@ -251,7 +251,7 @@ onActivated(tryLoad); flex: 1; min-width: 0; font-size: 15px; - color: #d8d8d8; + color: var(--text-muted); line-height: 1.35; overflow: hidden; text-overflow: ellipsis; @@ -264,7 +264,7 @@ onActivated(tryLoad); border-radius: 4px; font-size: 10px; font-weight: 600; - color: #888; + color: var(--text-muted); background: rgba(255, 255, 255, 0.06); } @@ -283,7 +283,7 @@ onActivated(tryLoad); .date { font-size: 12px; - color: #777; + color: var(--text-muted); } .chevron { @@ -301,7 +301,7 @@ onActivated(tryLoad); border: none; border-radius: 8px; background: transparent; - color: #666; + color: var(--text-muted); cursor: pointer; } diff --git a/apps/player/src/components/MouseRippleGrid.vue b/apps/player/src/components/MouseRippleGrid.vue new file mode 100644 index 0000000..9a6990f --- /dev/null +++ b/apps/player/src/components/MouseRippleGrid.vue @@ -0,0 +1,284 @@ + + + + + diff --git a/apps/player/src/components/TeamEmblem.vue b/apps/player/src/components/TeamEmblem.vue index 06d5b70..d7b8690 100644 --- a/apps/player/src/components/TeamEmblem.vue +++ b/apps/player/src/components/TeamEmblem.vue @@ -7,7 +7,7 @@ const props = withDefaults( teamCode?: string; teamName?: string; logoUrl?: string | null; - size?: 'sm' | 'md' | 'lg'; + size?: 'sm' | 'md' | 'lg' | 'xl'; }>(), { size: 'md' }, ); @@ -80,6 +80,11 @@ watch( height: 52px; } +.team-emblem--xl { + width: 72px; + height: 72px; +} + /* 国旗:横向比例 + 铺满 */ .team-emblem:not(.team-emblem--logo) { object-fit: cover; @@ -102,6 +107,11 @@ watch( height: 36px; } +.team-emblem--xl:not(.team-emblem--logo) { + width: 80px; + height: 54px; +} + /* 队徽:正方形容器 + 完整显示 */ .team-emblem--logo { object-fit: contain; @@ -129,4 +139,8 @@ watch( .team-emblem--lg.team-emblem--placeholder { font-size: 22px; } + +.team-emblem--xl.team-emblem--placeholder { + font-size: 30px; +} diff --git a/apps/player/src/components/desktop/AccountSideNav.vue b/apps/player/src/components/desktop/AccountSideNav.vue new file mode 100644 index 0000000..fb68313 --- /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..5fba4cd --- /dev/null +++ b/apps/player/src/components/desktop/BetSlipPanel.vue @@ -0,0 +1,2033 @@ + + + + + diff --git a/apps/player/src/components/desktop/DataTable.vue b/apps/player/src/components/desktop/DataTable.vue new file mode 100644 index 0000000..e39e1e2 --- /dev/null +++ b/apps/player/src/components/desktop/DataTable.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopAuthLayout.vue b/apps/player/src/components/desktop/DesktopAuthLayout.vue new file mode 100644 index 0000000..29db87a --- /dev/null +++ b/apps/player/src/components/desktop/DesktopAuthLayout.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopBanner3DCarousel.vue b/apps/player/src/components/desktop/DesktopBanner3DCarousel.vue new file mode 100644 index 0000000..75c1c04 --- /dev/null +++ b/apps/player/src/components/desktop/DesktopBanner3DCarousel.vue @@ -0,0 +1,471 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopBetSlipRail.vue b/apps/player/src/components/desktop/DesktopBetSlipRail.vue new file mode 100644 index 0000000..b8ecb88 --- /dev/null +++ b/apps/player/src/components/desktop/DesktopBetSlipRail.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopOddsBetPopover.vue b/apps/player/src/components/desktop/DesktopOddsBetPopover.vue new file mode 100644 index 0000000..0d7bba9 --- /dev/null +++ b/apps/player/src/components/desktop/DesktopOddsBetPopover.vue @@ -0,0 +1,462 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopOutrightEventCard.vue b/apps/player/src/components/desktop/DesktopOutrightEventCard.vue new file mode 100644 index 0000000..53c8d32 --- /dev/null +++ b/apps/player/src/components/desktop/DesktopOutrightEventCard.vue @@ -0,0 +1,330 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopShell.vue b/apps/player/src/components/desktop/DesktopShell.vue new file mode 100644 index 0000000..d9c592f --- /dev/null +++ b/apps/player/src/components/desktop/DesktopShell.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/apps/player/src/components/desktop/DesktopTopNav.vue b/apps/player/src/components/desktop/DesktopTopNav.vue new file mode 100644 index 0000000..c38e24c --- /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..d81dc01 --- /dev/null +++ b/apps/player/src/components/desktop/LeagueSidebar.vue @@ -0,0 +1,363 @@ + + + + + diff --git a/apps/player/src/components/desktop/MatchSidebar.vue b/apps/player/src/components/desktop/MatchSidebar.vue new file mode 100644 index 0000000..263a723 --- /dev/null +++ b/apps/player/src/components/desktop/MatchSidebar.vue @@ -0,0 +1,343 @@ + + + + + diff --git a/apps/player/src/components/desktop/OutrightSidebar.vue b/apps/player/src/components/desktop/OutrightSidebar.vue new file mode 100644 index 0000000..2a40a56 --- /dev/null +++ b/apps/player/src/components/desktop/OutrightSidebar.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/apps/player/src/components/desktop/Pagination.vue b/apps/player/src/components/desktop/Pagination.vue new file mode 100644 index 0000000..53163cc --- /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..1d9d24f --- /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 eca0bff..aff7c4b 100644 --- a/apps/player/src/components/match-detail/CorrectScorePanel.vue +++ b/apps/player/src/components/match-detail/CorrectScorePanel.vue @@ -2,6 +2,8 @@ import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { groupCorrectScoreSelections, type CsSelection } from '../../utils/correctScoreLayout'; +import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover'; + const props = defineProps<{ marketType: string; @@ -15,13 +17,16 @@ 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(); +const { cancelClose } = useDesktopBetPopover(); const columns = computed(() => groupCorrectScoreSelections( @@ -34,9 +39,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 +51,11 @@ function formatOdds(odds: string) {