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);