From edc54e25f37e2f6620582690e8c99e85fcdcd6a2 Mon Sep 17 00:00:00 2001 From: Mars <3361409208a@gmail.com> Date: Tue, 30 Jun 2026 17:29:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(player-desktop):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=B8=BB=E9=A1=B5=E8=B5=9B=E4=BA=8B=E5=8D=A1=E7=89=87=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E3=80=81=E7=BB=9F=E4=B8=80=E6=B3=A8=E5=8D=95=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E9=A2=9C=E8=89=B2=E3=80=81=E6=B7=BB=E5=8A=A0=E7=BD=AE?= =?UTF-8?q?=E9=A1=B6=E7=B2=BE=E7=AE=80=E8=B5=B0=E9=A9=AC=E7=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/AnnouncementMarquee.vue | 21 +- .../src/components/desktop/BetSlipPanel.vue | 2 +- .../src/components/desktop/DesktopShell.vue | 7 +- .../src/views/desktop/DesktopHomeView.vue | 354 ++++++++++++++---- 4 files changed, 302 insertions(+), 82 deletions(-) diff --git a/apps/player/src/components/AnnouncementMarquee.vue b/apps/player/src/components/AnnouncementMarquee.vue index 872adf7..c62f667 100644 --- a/apps/player/src/components/AnnouncementMarquee.vue +++ b/apps/player/src/components/AnnouncementMarquee.vue @@ -65,20 +65,27 @@ function goDetail() { .marquee-bar.embedded { margin: 0; - padding: 8px 12px; + padding: 3px 12px; border: none; border-radius: 0; border-bottom: 1px solid var(--border); - background: #EFF6FF; + background: #F8FAFC; } .marquee-bar.embedded .marquee-badge { - padding: 3px 8px; - font-size: 10px; + padding: 1px 4px; + font-size: 9px; + background: rgba(0, 0, 0, 0.06); + color: var(--text-muted); + font-weight: 600; + letter-spacing: normal; + border-radius: 3px; } .marquee-bar.embedded .marquee-text { - font-size: 12px; + font-size: 11px; + font-weight: 500; + color: var(--text-muted); } .marquee-badge { @@ -103,13 +110,13 @@ function goDetail() { .marquee-track { display: flex; width: max-content; - animation: marquee-scroll 18s linear infinite; + animation: marquee-scroll 35s linear infinite; pointer-events: none; } .marquee-text { flex-shrink: 0; - padding-right: 80px; + padding-right: 240px; font-size: 13px; font-weight: 600; color: var(--primary-light); diff --git a/apps/player/src/components/desktop/BetSlipPanel.vue b/apps/player/src/components/desktop/BetSlipPanel.vue index d3be74a..95e562c 100644 --- a/apps/player/src/components/desktop/BetSlipPanel.vue +++ b/apps/player/src/components/desktop/BetSlipPanel.vue @@ -1765,7 +1765,7 @@ onUnmounted(() => { padding: 10px; border-radius: 4px; background: linear-gradient(180deg, var(--primary-light) 0%, var(--primary) 100%); - color: var(--text); + color: #FFFFFF; font-size: 13px; font-weight: 800; text-transform: uppercase; diff --git a/apps/player/src/components/desktop/DesktopShell.vue b/apps/player/src/components/desktop/DesktopShell.vue index 2dae93c..9027081 100644 --- a/apps/player/src/components/desktop/DesktopShell.vue +++ b/apps/player/src/components/desktop/DesktopShell.vue @@ -9,6 +9,7 @@ import LeagueSidebar from './LeagueSidebar.vue'; import MatchSidebar from './MatchSidebar.vue'; import DesktopBetSlipRail from './DesktopBetSlipRail.vue'; import FloatingMailbox from '../FloatingMailbox.vue'; +import AnnouncementMarquee from '../AnnouncementMarquee.vue'; import { usePlayerHome } from '../../composables/usePlayerHome'; import { useAuthStore } from '../../stores/auth'; import { useBetSlipStore } from '../../stores/betSlip'; @@ -17,7 +18,7 @@ const route = useRoute(); const { locale } = useI18n(); const auth = useAuthStore(); const slip = useBetSlipStore(); -const { load: loadPlayerHome } = usePlayerHome(); +const { load: loadPlayerHome, announcements } = usePlayerHome(); const layoutMode = computed<'betting' | 'account' | 'hub' | 'marketing'>(() => { const p = route.path; @@ -80,6 +81,10 @@ watch( 'betslip-hidden': !showBetSlipRail, }" > +
+ +
+
diff --git a/apps/player/src/views/desktop/DesktopHomeView.vue b/apps/player/src/views/desktop/DesktopHomeView.vue index bad2ad2..cb3a604 100644 --- a/apps/player/src/views/desktop/DesktopHomeView.vue +++ b/apps/player/src/views/desktop/DesktopHomeView.vue @@ -9,12 +9,99 @@ import TeamEmblem from '../../components/TeamEmblem.vue'; import GoldSpinner from '../../components/GoldSpinner.vue'; import { formatLocalMatchDateTime } from '@thebet365/shared'; import type { PlayerHomeMatch } from '../../composables/usePlayerHome'; +import { useBetSlipStore } from '../../stores/betSlip'; +import { useAuthStore } from '../../stores/auth'; +import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover'; +import { resolveSelectionLabel } from '../../utils/selectionLabel'; +import MarketSelectionsPanel from '../../components/match-detail/MarketSelectionsPanel.vue'; const { t, locale } = useI18n(); const router = useRouter(); const { banners, hotMatches, upcomingMatches, loading, load, announcementItems } = usePlayerHome(); const activeFeaturedIndex = ref(0); +const slip = useBetSlipStore(); +const auth = useAuthStore(); +const { openAt } = useDesktopBetPopover(); + +const activeMarketTypes = ref>({}); + +function getActiveMarketType(match: PlayerHomeMatch) { + const matchId = match.id; + if (activeMarketTypes.value[matchId]) { + return activeMarketTypes.value[matchId]; + } + if (match.markets?.some(m => m.marketType === 'FT_1X2')) { + return 'FT_1X2'; + } + return match.markets?.[0]?.marketType || 'FT_1X2'; +} + +function setActiveMarketType(matchId: string, marketType: string) { + activeMarketTypes.value[matchId] = marketType; +} + +function getAvailableMarketsForMatch(match: PlayerHomeMatch) { + const supported = ['FT_1X2', 'FT_HANDICAP', 'FT_OVER_UNDER']; + return (match.markets ?? []).filter(m => supported.includes(m.marketType)); +} + +function getActiveMarket(match: PlayerHomeMatch) { + const activeType = getActiveMarketType(match); + return match.markets?.find(m => m.marketType === activeType); +} + +function isMarketLocked(match: PlayerHomeMatch, market?: { status: string; allowSingle?: boolean; allowParlay?: boolean }) { + if (!market) return true; + if (match.bettingOpen === false) return true; + const status = (market.status ?? 'OPEN').toUpperCase(); + if (status !== 'OPEN') return true; + return market.allowSingle !== true && market.allowParlay !== true; +} + +function isSelected(id: string) { + return slip.isInSlip(id); +} + +function getMarketTabLabel(type: string) { + const raw = (() => { + if (type === 'FT_1X2') return t('bet.market_ft_1x2'); + if (type === 'FT_HANDICAP') return t('bet.market_ft_handicap'); + if (type === 'FT_OVER_UNDER') return t('bet.market_ft_ou'); + return type; + })(); + return raw.replace(/全场\s*|FT\s*|Penuh\s*/ig, '').trim(); +} + +function handleOddsClick(match: PlayerHomeMatch, market: any, selId: string, event?: MouseEvent) { + if (isMarketLocked(match, market)) return; + if (!auth.token) { + auth.showLoginPrompt(router.currentRoute.value.fullPath); + return; + } + const sel = market.selections?.find((s: any) => s.id === selId); + if (!sel || !event) return; + + const item = { + selectionId: sel.id, + oddsVersion: String(sel.oddsVersion), + matchId: match.id, + matchName: `${match.homeTeamName} vs ${match.awayTeamName}`, + marketId: market.id, + marketName: market.marketDisplayName?.trim() || market.marketType || '', + selectionName: resolveSelectionLabel(t, sel.selectionCode || '', sel.selectionName, { + lineValue: market.lineValue, + }), + odds: parseFloat(sel.odds), + marketType: market.marketType, + lineValue: market.lineValue != null && market.lineValue !== '' ? parseFloat(String(market.lineValue)) : null, + allowSingle: market.allowSingle, + allowParlay: market.allowParlay, + }; + + openAt(event, item); +} + let rotateInterval: number | undefined; function startFeaturedRotation() { @@ -59,7 +146,10 @@ function goMatch(id: string) { } function formatKickoff(startTime: string) { - return formatLocalMatchDateTime(startTime, locale.value, { variant: 'full' }); + return formatLocalMatchDateTime(startTime, locale.value, { + variant: 'compact', + includeTimeZone: false + }); } function setFeaturedIndex(index: number) { @@ -203,39 +293,81 @@ function setFeaturedIndex(index: number) {
-
- {{ match.leagueName }} - LIVE -
- -
-
- - {{ match.homeTeamName }} + +
+
+ {{ match.leagueName }} +
+ LIVE + {{ formatKickoff(match.startTime) }} +
-
VS
-
- - {{ match.awayTeamName }} + +
+
+ + {{ match.homeTeamName }} +
+
VS
+
+ + {{ match.awayTeamName }} +
-
@@ -612,106 +744,181 @@ function setFeaturedIndex(index: number) { .matches-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 14px; + grid-template-columns: repeat(auto-fill, minmax(460px, 1fr)); + gap: 12px; } .match-card { - background: var(--bg-body); - border: 1px solid var(--border); - border-radius: 6px; - padding: 14px; display: flex; - flex-direction: column; - gap: 12px; - transition: all 0.2s; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; + transition: border-color 0.2s, box-shadow 0.2s; + min-height: 76px; } .match-card:hover { border-color: var(--border-active); - box-shadow: var(--shadow); + box-shadow: 0 4px 12px rgba(0, 61, 107, 0.08); +} + +.match-info-side { + flex: 1; + padding: 8px 12px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 4px; + border-right: 1px solid var(--border); + min-width: 0; + cursor: pointer; + background: #FCFDFE; + transition: background-color 0.2s; +} + +.match-info-side:hover { + background: var(--bg-hover); +} + +.match-market-side { + width: 290px; + flex-shrink: 0; + padding: 8px 10px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 4px; + background: var(--bg-card); } .card-top { display: flex; justify-content: space-between; align-items: center; + gap: 8px; +} + +.top-meta { + display: flex; + align-items: center; + gap: 6px; } .league-tag { - font-size: 11px; + font-size: 10px; font-weight: 700; color: var(--primary-light); background: rgba(0, 102, 204, 0.08); - padding: 2px 6px; - border-radius: 4px; - max-width: 100%; + padding: 1px 4px; + border-radius: 3px; + max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .live-indicator { - font-size: 10px; + font-size: 9px; font-weight: 800; background: var(--danger); color: #fff; - padding: 1px 4px; + padding: 1px 3px; border-radius: 2px; + line-height: 1; } -.teams-versus { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 0; - border-bottom: 1px solid rgba(0, 0, 0, 0.04); +.match-time { + font-size: 10px; + color: var(--text-muted); + font-weight: 600; + white-space: nowrap; } -.team-side { +.teams-versus-compact { display: flex; flex-direction: column; + gap: 4px; +} + +.vs-text { + font-size: 10px; + font-weight: 800; + color: var(--text-muted); + font-style: italic; + padding-left: 36px; + line-height: 1; + margin: -2px 0; +} + +.team-row { + display: flex; align-items: center; - gap: 6px; - flex: 1; + gap: 8px; } .team-name { font-size: 12px; font-weight: 700; color: var(--text); - text-align: center; - max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.vs-divider { - font-size: 11px; - font-weight: 800; +.market-tabs { + display: flex; + gap: 2px; + border-bottom: 1px solid var(--border); + padding-bottom: 4px; + margin-bottom: 4px; +} + +.market-tab-btn { + padding: 2px 6px; + font-size: 10px; + font-weight: 700; + border-radius: 4px; + background: transparent; color: var(--text-muted); - background: var(--bg-body); - width: 22px; - height: 22px; + border: none; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} + +.market-tab-btn:hover { + background: var(--bg-hover); + color: var(--primary); +} + +.market-tab-btn.active { + background: rgba(0, 61, 107, 0.08); + color: var(--primary); +} + +.market-odds-area { + flex: 1; display: flex; align-items: center; justify-content: center; - border-radius: 50%; + width: 100%; } -.card-footer { - display: flex; - justify-content: space-between; - align-items: center; -} - -.match-time { - font-size: 11px; +.no-market-odds { + font-size: 10px; color: var(--text-muted); + text-align: center; } -.bet-action-btn { +.no-markets-fallback { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.go-detail-btn { padding: 4px 12px; border-radius: 4px; border: 1px solid var(--border-active); @@ -720,9 +927,10 @@ function setFeaturedIndex(index: number) { font-size: 11px; font-weight: 700; transition: all 0.2s; + cursor: pointer; } -.match-card:hover .bet-action-btn { +.go-detail-btn:hover { background: var(--primary); color: #FFFFFF; border-color: var(--primary);