Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0b8e051ed | ||
|
|
0a4e4c36c8 | ||
|
|
5a6a8195f8 | ||
|
|
3af2fe72d4 | ||
|
|
4cbc846d05 | ||
|
|
ea4764934a | ||
|
|
14268eccaa | ||
|
|
49594110a5 | ||
| 9be3f87b2b | |||
| 6828067a89 | |||
| 3908e5539d | |||
| 9daf919d88 | |||
| edc54e25f3 | |||
| e4ce5941a5 | |||
| 7f563326d8 | |||
| ef5b9416cc | |||
| 4d8389a716 | |||
| b327f65c63 | |||
| c37b1a8548 | |||
| 7f30408524 | |||
| 01698bf410 | |||
| 713739475b | |||
| f9d68d94ce | |||
| 6ba541c133 | |||
| 43fa8ebef0 | |||
| b01213f637 | |||
| a8b7d5763f | |||
| dcce14cf74 | |||
| 26e2adf786 | |||
| 021d216613 | |||
| 5e7d6bc23f | |||
| fa8d3fbf0c | |||
| 27dc9eb3b0 | |||
| fa7b8cff25 | |||
| dd7802c6f7 | |||
| 074531f705 | |||
| c61d3c68f2 | |||
| c5a5e66bbd | |||
| f2f6927a75 | |||
| e1d33dde9a | |||
| ea695012cc | |||
| 3212cd6040 | |||
| 33e7092b77 | |||
| f4c71b22f2 | |||
| 75bd5c3512 | |||
| 740799a820 | |||
| e56029225e | |||
| ec51b675e3 | |||
| 9f1d263741 | |||
| cb4e48b9f4 | |||
| 4a93fcfce0 |
@@ -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);
|
||||
|
||||
@@ -1592,17 +1592,42 @@ export class MatchesService {
|
||||
);
|
||||
}
|
||||
|
||||
/** 首页卡片快速下注所需的核心玩法类型 */
|
||||
private static readonly HOME_CARD_MARKET_TYPES = [
|
||||
'FT_1X2',
|
||||
'FT_HANDICAP',
|
||||
'FT_OVER_UNDER',
|
||||
] as const;
|
||||
|
||||
/** 未来 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);
|
||||
|
||||
const marketsRelation = includeMarkets
|
||||
? {
|
||||
where: {
|
||||
status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] },
|
||||
showOnPlayer: true,
|
||||
marketType: { in: [...MatchesService.HOME_CARD_MARKET_TYPES] },
|
||||
},
|
||||
include: {
|
||||
selections: {
|
||||
where: { status: { in: ['OPEN', 'SUSPENDED', 'CLOSED'] } },
|
||||
orderBy: { sortOrder: 'asc' as const },
|
||||
},
|
||||
},
|
||||
orderBy: { sortOrder: 'asc' as const },
|
||||
}
|
||||
: this.playerMarketStatusInclude;
|
||||
|
||||
const matches = await this.prisma.match.findMany({
|
||||
where: {
|
||||
status: { in: ['PUBLISHED', 'CLOSED', 'PENDING_SETTLEMENT'] },
|
||||
@@ -1625,14 +1650,14 @@ export class MatchesService {
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
score: true,
|
||||
markets: this.playerMarketStatusInclude,
|
||||
markets: marketsRelation,
|
||||
},
|
||||
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 })),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,24 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="theme-color" content="#003D6B" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="TheBet365" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/logo.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Outfit:wght@200;300;400&family=Russo+One&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>TheBet365</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: #000; }
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: #F5F7FA; }
|
||||
#app-loading {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -22,7 +28,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #000;
|
||||
background: #F5F7FA;
|
||||
z-index: 9999;
|
||||
}
|
||||
#app-loading svg {
|
||||
@@ -40,16 +46,16 @@
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="gs-track" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#D4AF37" stop-opacity="0.18" />
|
||||
<stop offset="50%" stop-color="#F0D875" stop-opacity="0.25" />
|
||||
<stop offset="100%" stop-color="#D4AF37" stop-opacity="0.18" />
|
||||
<stop offset="0%" stop-color="#003D6B" stop-opacity="0.18" />
|
||||
<stop offset="50%" stop-color="#005B9F" stop-opacity="0.25" />
|
||||
<stop offset="100%" stop-color="#003D6B" stop-opacity="0.18" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gs-arc" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFF4C8" />
|
||||
<stop offset="22%" stop-color="#F0D875" />
|
||||
<stop offset="50%" stop-color="#D4AF37" />
|
||||
<stop offset="78%" stop-color="#B8942B" />
|
||||
<stop offset="100%" stop-color="#8B6914" />
|
||||
<stop offset="0%" stop-color="#E8F4FD" />
|
||||
<stop offset="22%" stop-color="#7CB9E8" />
|
||||
<stop offset="50%" stop-color="#005B9F" />
|
||||
<stop offset="78%" stop-color="#003D6B" />
|
||||
<stop offset="100%" stop-color="#002847" />
|
||||
</linearGradient>
|
||||
<filter id="gs-glow">
|
||||
<feGaussianBlur stdDeviation="1.2" result="blur" />
|
||||
@@ -59,9 +65,9 @@
|
||||
</feMerge>
|
||||
</filter>
|
||||
<radialGradient id="gs-dot" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#FFF4C8" />
|
||||
<stop offset="60%" stop-color="#F0D875" />
|
||||
<stop offset="100%" stop-color="#D4AF37" />
|
||||
<stop offset="0%" stop-color="#E8F4FD" />
|
||||
<stop offset="60%" stop-color="#7CB9E8" />
|
||||
<stop offset="100%" stop-color="#005B9F" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<circle cx="24" cy="24" r="20" stroke="url(#gs-track)" stroke-width="3" fill="none" />
|
||||
@@ -74,4 +80,4 @@
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
BIN
apps/player/src/assets/images/bento-bet-bg.png
Normal file
BIN
apps/player/src/assets/images/bento-bet-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 422 KiB |
BIN
apps/player/src/assets/images/zt2.webp
Normal file
BIN
apps/player/src/assets/images/zt2.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
@@ -52,7 +52,7 @@ function goDetail() {
|
||||
gap: 10px;
|
||||
margin: -4px -2px 14px;
|
||||
padding: 10px 12px;
|
||||
background: #141414;
|
||||
background: #EFF6FF;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
@@ -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: #111;
|
||||
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 {
|
||||
@@ -87,9 +94,9 @@ function goDetail() {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
color: #1A1000;
|
||||
background: var(--gradient-gold);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
color: #fff;
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -24,10 +24,10 @@ const { visible, message } = useAppToast();
|
||||
max-width: min(90vw, 360px);
|
||||
padding: 10px 18px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(22, 22, 22, 0.96);
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.15);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
|
||||
@@ -44,10 +44,10 @@ const { visible, scrollToTop } = useBackToTop(toRef(props, 'scrollEl'));
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-gold-soft, rgba(200, 168, 78, 0.35));
|
||||
background: rgba(17, 17, 17, 0.92);
|
||||
color: var(--primary-light, #c8a84e);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
color: #003D6B;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ const { visible, scrollToTop } = useBackToTop(toRef(props, 'scrollEl'));
|
||||
}
|
||||
|
||||
.back-top-btn:active {
|
||||
background: rgba(200, 168, 78, 0.16);
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
|
||||
@@ -168,20 +168,20 @@ onUnmounted(stopAutoPlay);
|
||||
<style scoped>
|
||||
.carousel {
|
||||
position: relative;
|
||||
margin: -12px -16px 14px;
|
||||
margin: -12px -16px 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.media {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #05080d;
|
||||
background: #E8EDF2;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: clamp(148px, 41.86vw, 180px);
|
||||
height: clamp(120px, 34vw, 150px);
|
||||
}
|
||||
|
||||
.track {
|
||||
@@ -199,9 +199,7 @@ onUnmounted(stopAutoPlay);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(7, 18, 31, 0.94), rgba(6, 8, 12, 0.98)),
|
||||
#070d15;
|
||||
background: linear-gradient(135deg, #F5F7FA, #E8EDF2);
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -213,8 +211,8 @@ onUnmounted(stopAutoPlay);
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.03) 48%, rgba(0, 0, 0, 0.18));
|
||||
box-shadow: inset 0 -1px 0 rgba(212, 175, 55, 0.16);
|
||||
linear-gradient(90deg, rgba(245, 247, 250, 0.12), rgba(255, 255, 255, 0.03) 48%, rgba(245, 247, 250, 0.12));
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 61, 107, 0.08);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -225,7 +223,7 @@ onUnmounted(stopAutoPlay);
|
||||
height: calc(100% + 36px);
|
||||
object-fit: cover;
|
||||
filter: blur(16px);
|
||||
opacity: 0.42;
|
||||
opacity: 0.3;
|
||||
transform: scale(1.04);
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -248,29 +246,30 @@ onUnmounted(stopAutoPlay);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.25), var(--secondary));
|
||||
background: linear-gradient(135deg, rgba(0, 61, 107, 0.08), #E8EDF2);
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.media .nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid var(--border);
|
||||
padding-bottom: 5px;
|
||||
font-size: 22px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
color: #003D6B;
|
||||
border: 1px solid #E5E7EB;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.nav.prev { left: 8px; }
|
||||
@@ -278,29 +277,30 @@ onUnmounted(stopAutoPlay);
|
||||
|
||||
.media .dots {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
bottom: 8px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
border: 2px solid transparent;
|
||||
background: rgba(0, 61, 107, 0.2);
|
||||
border: 1.5px solid transparent;
|
||||
padding: 0;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
width: 22px;
|
||||
border-radius: 6px;
|
||||
background: var(--gradient-gold);
|
||||
border-color: var(--primary-light);
|
||||
width: 18px;
|
||||
border-radius: 5px;
|
||||
background: #003D6B;
|
||||
border-color: #003D6B;
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -65,9 +65,9 @@ function close() {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
color: #003D6B;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
@@ -79,7 +79,7 @@ function close() {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 205;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -93,11 +93,11 @@ function close() {
|
||||
max-width: 340px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(165deg, #1a1810 0%, #121212 45%, #0a0a0a 100%);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: var(--radius);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
padding: 16px 14px 14px;
|
||||
box-shadow: var(--shadow), 0 0 24px rgba(212, 175, 55, 0.08);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.close-x {
|
||||
@@ -107,15 +107,15 @@ function close() {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-muted);
|
||||
background: #F5F7FA;
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
padding-right: 24px;
|
||||
@@ -126,14 +126,14 @@ function close() {
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.guide-body :deep(.flow-name) {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
@@ -152,21 +152,24 @@ function close() {
|
||||
|
||||
.guide-body :deep(.flow-block) {
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--border);
|
||||
border-top: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.guide-body :deep(.rules-link) {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--border);
|
||||
border-top: 1px dashed #E5E7EB;
|
||||
}
|
||||
|
||||
.btn-ok {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
background: #003D6B;
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -149,15 +149,19 @@ function goDetail() {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
transition: background 0.15s, box-shadow 0.15s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.bet-card:active {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@@ -178,9 +182,9 @@ function goDetail() {
|
||||
.cashback-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #f0b90b;
|
||||
background: rgba(240, 185, 11, 0.1);
|
||||
border: 1px solid rgba(240, 185, 11, 0.25);
|
||||
color: #F8971F;
|
||||
background: rgba(248, 151, 31, 0.08);
|
||||
border: 1px solid rgba(248, 151, 31, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
@@ -188,9 +192,9 @@ function goDetail() {
|
||||
.bet-type-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light, #c8a84e);
|
||||
background: rgba(200, 168, 78, 0.12);
|
||||
border: 1px solid rgba(200, 168, 78, 0.25);
|
||||
color: #003D6B;
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
border: 1px solid rgba(0, 61, 107, 0.15);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
letter-spacing: 0.02em;
|
||||
@@ -198,7 +202,7 @@ function goDetail() {
|
||||
|
||||
.card-date {
|
||||
font-size: 10px;
|
||||
color: #555;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
@@ -207,7 +211,7 @@ function goDetail() {
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: #e8e8e8;
|
||||
color: #1A1A2E;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -221,7 +225,7 @@ function goDetail() {
|
||||
|
||||
.pick-label {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -233,8 +237,8 @@ function goDetail() {
|
||||
.odds-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: #c8a84e;
|
||||
background: rgba(200, 168, 78, 0.1);
|
||||
color: #F8971F;
|
||||
background: rgba(248, 151, 31, 0.08);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
flex-shrink: 0;
|
||||
@@ -250,24 +254,24 @@ function goDetail() {
|
||||
|
||||
.stake-amt {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.return-amt {
|
||||
font-size: 15px;
|
||||
font-weight: 900;
|
||||
color: #888;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.return-amt.highlight { color: #3db865; }
|
||||
.return-amt.pending { color: #e8c84a; }
|
||||
.return-amt.lost { color: #e05050; }
|
||||
.return-amt.highlight { color: #059669; }
|
||||
.return-amt.pending { color: #F8971F; }
|
||||
.return-amt.lost { color: #DC2626; }
|
||||
|
||||
.chevron {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
color: #E5E7EB;
|
||||
margin-left: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -685,13 +685,13 @@ watch(
|
||||
.drawer {
|
||||
width: 100%;
|
||||
max-height: 88vh;
|
||||
background: #101010;
|
||||
background: var(--bg-card);
|
||||
border-radius: 16px 16px 0 0;
|
||||
border-top: 1px solid var(--border-gold-soft);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 -20px 44px rgba(0, 0, 0, 0.48);
|
||||
box-shadow: 0 -12px 32px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.drawer-head {
|
||||
@@ -703,8 +703,8 @@ watch(
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px 14px 10px;
|
||||
background: linear-gradient(180deg, #1d1d1d 0%, #111 100%);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: linear-gradient(180deg, #FFFFFF 0%, var(--bg-body) 100%);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.drawer-kicker {
|
||||
@@ -728,7 +728,7 @@ watch(
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
padding: 0;
|
||||
@@ -742,8 +742,8 @@ watch(
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
border-bottom: 1px solid rgba(212, 175, 55, 0.18);
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
border-bottom: 1px solid rgba(0, 102, 204, 0.12);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -759,7 +759,7 @@ watch(
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-muted);
|
||||
padding: 0;
|
||||
}
|
||||
@@ -769,23 +769,23 @@ watch(
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
padding: 8px 12px 0;
|
||||
gap: 6px;
|
||||
background: #101010;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.slip-tab {
|
||||
min-height: 38px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
border: 1px solid var(--border);
|
||||
background: #171717;
|
||||
background: var(--bg-card);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.slip-tab.active {
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.13);
|
||||
color: var(--primary-light);
|
||||
border-color: var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
@@ -797,7 +797,7 @@ watch(
|
||||
margin-left: 6px;
|
||||
border-radius: 9px;
|
||||
background: var(--primary);
|
||||
color: #111;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -822,15 +822,15 @@ watch(
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 11px 12px;
|
||||
background: #151515;
|
||||
border: 1px solid #292929;
|
||||
background: var(--bg-hover);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.slip-item--single {
|
||||
border-color: var(--border-gold-soft);
|
||||
background: linear-gradient(180deg, rgba(212, 175, 55, 0.12), rgba(18, 18, 18, 0.94));
|
||||
border-color: var(--border-active);
|
||||
background: linear-gradient(180deg, rgba(0, 102, 204, 0.06), var(--bg-card));
|
||||
}
|
||||
|
||||
.item-main {
|
||||
@@ -891,14 +891,12 @@ watch(
|
||||
min-height: 48px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, #4b370d 0%, #241905 100%);
|
||||
border: 1px solid var(--primary-light);
|
||||
color: #ffe892;
|
||||
background: linear-gradient(180deg, var(--primary-light) 0%, var(--primary) 100%);
|
||||
border: 1px solid var(--border-active);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 232, 146, 0.18) inset,
|
||||
0 6px 18px rgba(212, 175, 55, 0.18);
|
||||
box-shadow: 0 4px 12px rgba(0, 61, 107, 0.15);
|
||||
}
|
||||
|
||||
.btn-add-parlay span {
|
||||
@@ -908,8 +906,8 @@ watch(
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-light);
|
||||
color: #191100;
|
||||
background: #fff;
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -963,8 +961,8 @@ watch(
|
||||
.stake-area {
|
||||
margin: 12px 0 12px;
|
||||
padding: 12px;
|
||||
background: #151515;
|
||||
border: 1px solid #292929;
|
||||
background: var(--bg-hover);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -990,17 +988,17 @@ watch(
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border: 1px solid var(--border-active);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #0c0c0c;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.currency {
|
||||
padding: 0 10px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
border-right: 1px solid #2d2d2d;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.stake-input-row input {
|
||||
@@ -1065,7 +1063,7 @@ watch(
|
||||
|
||||
.quick-stakes button {
|
||||
min-height: 50px;
|
||||
background: #7b787d;
|
||||
background: var(--bg-body);
|
||||
color: var(--text);
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
@@ -1087,9 +1085,9 @@ watch(
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
background: rgba(14, 14, 14, 0.98);
|
||||
border-top: 1px solid var(--border-gold-soft);
|
||||
box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.45);
|
||||
background: var(--bg-card);
|
||||
border-top: 1px solid var(--border);
|
||||
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.drawer-foot--split {
|
||||
@@ -1101,7 +1099,7 @@ watch(
|
||||
min-height: 48px;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(180deg, var(--primary-light), var(--primary));
|
||||
color: #111;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@@ -79,12 +79,12 @@ const stats = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
.stats-panel {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
padding: 14px 12px 10px;
|
||||
margin-bottom: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
@@ -110,27 +110,28 @@ const stats = computed(() => {
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.stat-val.small {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.stat-val.gold {
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.stat-item.won .stat-val { color: #3db865; }
|
||||
.stat-item.lost .stat-val { color: #e05050; }
|
||||
.stat-item.pending .stat-val { color: #e8c84a; }
|
||||
.stat-item.push .stat-val { color: #888; }
|
||||
.stat-item.won .stat-val { color: #059669; }
|
||||
.stat-item.lost .stat-val { color: #DC2626; }
|
||||
.stat-item.pending .stat-val { color: #F8971F; }
|
||||
.stat-item.push .stat-val { color: #6B7280; }
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
letter-spacing: 0.04em;
|
||||
margin-top: 2px;
|
||||
}
|
||||
@@ -144,7 +145,7 @@ const stats = computed(() => {
|
||||
}
|
||||
|
||||
.stats-bar.empty {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: #E8EDF2;
|
||||
}
|
||||
|
||||
.bar-seg {
|
||||
@@ -152,8 +153,8 @@ const stats = computed(() => {
|
||||
transition: flex 0.3s ease;
|
||||
}
|
||||
|
||||
.bar-seg.won { background: #3db865; }
|
||||
.bar-seg.lost { background: #e05050; }
|
||||
.bar-seg.pending { background: #e8c84a; }
|
||||
.bar-seg.push { background: #444; }
|
||||
.bar-seg.won { background: #059669; }
|
||||
.bar-seg.lost { background: #DC2626; }
|
||||
.bar-seg.pending { background: #F8971F; }
|
||||
.bar-seg.push { background: #6B7280; }
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,7 @@ interface Particle {
|
||||
color: string; shape: number;
|
||||
}
|
||||
|
||||
const COLORS = ['#D4AF37', '#FFD700', '#F0C040', '#C8A02E', '#E6C84C', '#B8960C'];
|
||||
const COLORS = ['#003D6B', '#005B9F', '#0066CC', '#3388DD', '#1A5FA0', '#4499E0'];
|
||||
let w = 0;
|
||||
let h = 0;
|
||||
|
||||
@@ -111,8 +111,8 @@ onUnmounted(stop);
|
||||
<canvas ref="canvasRef" class="confetti-canvas" />
|
||||
<div class="success-card">
|
||||
<svg class="check" viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle class="check-circle" cx="26" cy="26" r="24" fill="none" stroke="#D4AF37" stroke-width="3" />
|
||||
<path class="check-mark" d="M14 27l7 7 16-16" fill="none" stroke="#D4AF37" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle class="check-circle" cx="26" cy="26" r="24" fill="none" stroke="#003D6B" stroke-width="3" />
|
||||
<path class="check-mark" d="M14 27l7 7 16-16" fill="none" stroke="#003D6B" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<p class="success-text">{{ t('bet.place_success') }}</p>
|
||||
</div>
|
||||
@@ -125,7 +125,7 @@ onUnmounted(stop);
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 300;
|
||||
background: rgba(0, 0, 0, 0.82);
|
||||
background: rgba(245, 247, 250, 0.88);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -166,7 +166,7 @@ onUnmounted(stop);
|
||||
}
|
||||
|
||||
.success-text {
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
|
||||
@@ -144,10 +144,11 @@ onUnmounted(() => {
|
||||
padding: 0 8px 0 8px;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 6px;
|
||||
background: #141414;
|
||||
background: #FFFFFF;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.chip-body {
|
||||
@@ -159,7 +160,7 @@ onUnmounted(() => {
|
||||
|
||||
.chip-label {
|
||||
font-size: 8px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
@@ -172,14 +173,14 @@ onUnmounted(() => {
|
||||
.chip-amount {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
white-space: nowrap;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
font-size: 10px;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
transition: transform 0.2s;
|
||||
margin-left: 0;
|
||||
}
|
||||
@@ -195,10 +196,10 @@ onUnmounted(() => {
|
||||
width: min(260px, 78vw);
|
||||
padding: 12px 14px;
|
||||
z-index: 130;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #141414;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.cash-panel::before {
|
||||
@@ -212,21 +213,22 @@ onUnmounted(() => {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 6px 0;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.panel-row.muted {
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.panel-row.highlight {
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.panel-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--border-gold-soft), transparent);
|
||||
background: #E5E7EB;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
@@ -235,9 +237,9 @@ onUnmounted(() => {
|
||||
padding: 8px 0;
|
||||
margin-top: 4px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--primary, #c8a84e);
|
||||
background: rgba(200, 168, 78, 0.12);
|
||||
color: var(--primary-light, #c8a84e);
|
||||
border: 1px solid #003D6B;
|
||||
background: transparent;
|
||||
color: #003D6B;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -245,7 +247,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.panel-recharge-btn:active {
|
||||
background: rgba(200, 168, 78, 0.24);
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
|
||||
@@ -103,18 +103,18 @@ function onConfirm() {
|
||||
.confirm-modal {
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
background: linear-gradient(165deg, #1a1810 0%, #121212 45%, #0a0a0a 100%);
|
||||
border: 1px solid var(--border-gold-soft, rgba(200, 168, 78, 0.25));
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 22px 18px 16px;
|
||||
box-shadow: 0 0 24px rgba(212, 175, 55, 0.08);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.confirm-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: var(--gold, #c8a84e);
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
line-height: 1.35;
|
||||
}
|
||||
@@ -123,7 +123,7 @@ function onConfirm() {
|
||||
margin: 0 0 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #c8c8c8;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
white-space: pre-line;
|
||||
}
|
||||
@@ -149,15 +149,15 @@ function onConfirm() {
|
||||
}
|
||||
|
||||
.confirm-btn.cancel {
|
||||
border: 1px solid #333;
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: #888;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.confirm-btn.confirm {
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #d4a017, #e8c84a);
|
||||
color: #1a1a1a;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.confirm-btn.confirm.danger {
|
||||
@@ -184,4 +184,25 @@ function onConfirm() {
|
||||
.confirm-fade-leave-to .confirm-modal {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
/* === Desktop Theme Overrides === */
|
||||
:global(.is-desktop) .confirm-modal {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
:global(.is-desktop) .confirm-title {
|
||||
color: var(--text);
|
||||
}
|
||||
:global(.is-desktop) .confirm-message {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
:global(.is-desktop) .confirm-btn.cancel {
|
||||
border-color: var(--border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
:global(.is-desktop) .confirm-btn.confirm {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -32,12 +32,27 @@ const showMessageActions = computed(
|
||||
() => inboxEnabled.value && activeTab.value === 'messages' && auth.token && hasMessages.value && !selectedMessageId.value,
|
||||
);
|
||||
|
||||
const isBettingDesktop = computed(() => {
|
||||
if (!isDesktop.value) return false;
|
||||
const hideOnMessagesPage = computed(
|
||||
() => isDesktop.value && route.path.startsWith('/messages'),
|
||||
);
|
||||
|
||||
const offsetForBetSlip = computed(() => {
|
||||
if (!isDesktop.value || hideOnMessagesPage.value) return false;
|
||||
const p = route.path;
|
||||
return p === '/bet' || p.startsWith('/match/') || p.startsWith('/outright/');
|
||||
if (p === '/bets' || p.startsWith('/bets/')) return false;
|
||||
if (p.startsWith('/wallet') || p.startsWith('/profile')) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
(path) => {
|
||||
if (isDesktop.value && path.startsWith('/messages') && isOpen.value) {
|
||||
close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(isOpen, (opened) => {
|
||||
if (opened) {
|
||||
if (!inboxEnabled.value) activeTab.value = 'support';
|
||||
@@ -86,7 +101,11 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="floating-mailbox" :class="{ 'is-betting-desktop': isBettingDesktop }">
|
||||
<div
|
||||
v-if="!hideOnMessagesPage"
|
||||
class="floating-mailbox"
|
||||
:class="{ 'is-betting-desktop': offsetForBetSlip }"
|
||||
>
|
||||
<!-- Floating Button -->
|
||||
<button
|
||||
type="button"
|
||||
@@ -152,7 +171,12 @@ onMounted(() => {
|
||||
|
||||
<div class="popup-body">
|
||||
<template v-if="activeTab === 'messages' && inboxEnabled">
|
||||
<MobileMessageDetailView v-if="selectedMessageId" :id="selectedMessageId" embedded @back="selectedMessageId = null" />
|
||||
<MobileMessageDetailView
|
||||
v-if="selectedMessageId"
|
||||
:id="selectedMessageId"
|
||||
embedded
|
||||
@back="selectedMessageId = null"
|
||||
/>
|
||||
<MessageListPanel v-else class="panel-content" @click-message="handleMessageClick" />
|
||||
</template>
|
||||
<CustomerServicePanel v-else class="panel-content" />
|
||||
@@ -224,10 +248,10 @@ onMounted(() => {
|
||||
width: 380px;
|
||||
height: 600px;
|
||||
max-height: calc(100vh - 120px);
|
||||
background: var(--bg-card, #141414);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: var(--shadow-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -240,7 +264,7 @@ onMounted(() => {
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
background: var(--bg-body);
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
@@ -264,7 +288,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #fff;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
@@ -273,8 +297,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
@@ -282,7 +306,7 @@ onMounted(() => {
|
||||
|
||||
.tab-btn.active .tab-badge {
|
||||
background: var(--primary);
|
||||
color: #141414;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
@@ -305,8 +329,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #fff;
|
||||
background: var(--bg-hover);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
@@ -315,13 +339,13 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.action-btn.close:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
height: calc(100% - 48px);
|
||||
position: relative;
|
||||
background: #0f0f0f;
|
||||
background: var(--bg-body);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const wrapperStyle = computed(() => {
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:class="['gold-spinner', { 'is-active': active }]"
|
||||
:class="['spinner', { 'is-active': active }]"
|
||||
:width="size ?? 48"
|
||||
:height="size ?? 48"
|
||||
viewBox="0 0 48 48"
|
||||
@@ -28,44 +28,44 @@ const wrapperStyle = computed(() => {
|
||||
:style="!active ? wrapperStyle : undefined"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="gs-track" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#D4AF37" stop-opacity="0.18" />
|
||||
<stop offset="50%" stop-color="#F0D875" stop-opacity="0.25" />
|
||||
<stop offset="100%" stop-color="#D4AF37" stop-opacity="0.18" />
|
||||
<linearGradient id="sp-track" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#003D6B" stop-opacity="0.15" />
|
||||
<stop offset="50%" stop-color="#005B9F" stop-opacity="0.22" />
|
||||
<stop offset="100%" stop-color="#003D6B" stop-opacity="0.15" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gs-arc" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFF4C8" />
|
||||
<stop offset="22%" stop-color="#F0D875" />
|
||||
<stop offset="50%" stop-color="#D4AF37" />
|
||||
<stop offset="78%" stop-color="#B8942B" />
|
||||
<stop offset="100%" stop-color="#8B6914" />
|
||||
<linearGradient id="sp-arc" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#E8F4FD" />
|
||||
<stop offset="22%" stop-color="#7CB9E8" />
|
||||
<stop offset="50%" stop-color="#005B9F" />
|
||||
<stop offset="78%" stop-color="#003D6B" />
|
||||
<stop offset="100%" stop-color="#002847" />
|
||||
</linearGradient>
|
||||
<filter id="gs-glow">
|
||||
<filter id="sp-glow">
|
||||
<feGaussianBlur stdDeviation="1.2" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<radialGradient id="gs-dot" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#FFF4C8" />
|
||||
<stop offset="60%" stop-color="#F0D875" />
|
||||
<stop offset="100%" stop-color="#D4AF37" />
|
||||
<radialGradient id="sp-dot" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#E8F4FD" />
|
||||
<stop offset="60%" stop-color="#7CB9E8" />
|
||||
<stop offset="100%" stop-color="#005B9F" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<circle cx="24" cy="24" r="20" stroke="url(#gs-track)" stroke-width="3" fill="none" />
|
||||
<circle cx="24" cy="24" r="20" stroke="url(#sp-track)" stroke-width="3" fill="none" />
|
||||
<circle
|
||||
cx="24"
|
||||
cy="24"
|
||||
r="20"
|
||||
stroke="url(#gs-arc)"
|
||||
stroke="url(#sp-arc)"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="31.4 94.2"
|
||||
filter="url(#gs-glow)"
|
||||
filter="url(#sp-glow)"
|
||||
/>
|
||||
<circle cx="24" cy="4" r="2.5" fill="url(#gs-dot)" filter="url(#gs-glow)">
|
||||
<circle cx="24" cy="4" r="2.5" fill="url(#sp-dot)" filter="url(#sp-glow)">
|
||||
<animate
|
||||
v-if="active"
|
||||
attributeName="r"
|
||||
@@ -74,29 +74,27 @@ const wrapperStyle = computed(() => {
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
<circle v-if="active" cx="36" cy="13.5" r="1.8" fill="#F0D875" opacity="0.6">
|
||||
<circle v-if="active" cx="36" cy="13.5" r="1.8" fill="#7CB9E8" opacity="0.6">
|
||||
<animate attributeName="opacity" values="0.6;0.15;0.6" dur="0.9s" repeatCount="indefinite" begin="0.15s" />
|
||||
</circle>
|
||||
<circle v-if="active" cx="34" cy="34" r="1.5" fill="#D4AF37" opacity="0.4">
|
||||
<circle v-if="active" cx="34" cy="34" r="1.5" fill="#005B9F" opacity="0.4">
|
||||
<animate attributeName="opacity" values="0.4;0.1;0.4" dur="1.1s" repeatCount="indefinite" begin="0.35s" />
|
||||
</circle>
|
||||
<circle v-if="active" cx="14" cy="34" r="1.5" fill="#D4AF37" opacity="0.4">
|
||||
<circle v-if="active" cx="14" cy="34" r="1.5" fill="#005B9F" opacity="0.4">
|
||||
<animate attributeName="opacity" values="0.4;0.1;0.4" dur="1s" repeatCount="indefinite" begin="0.55s" />
|
||||
</circle>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gold-spinner {
|
||||
.spinner {
|
||||
display: block;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.gold-spinner.is-active {
|
||||
animation: gs-spin 0.8s linear infinite;
|
||||
.spinner.is-active {
|
||||
animation: sp-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes gs-spin {
|
||||
@keyframes sp-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -130,7 +130,7 @@ watch(totalPages, () => {
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(201, 162, 39, 0.05);
|
||||
background: rgba(0, 102, 204, 0.04);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
@@ -139,7 +139,7 @@ watch(totalPages, () => {
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.announce-icon {
|
||||
@@ -158,8 +158,8 @@ watch(totalPages, () => {
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: #1a1a1a;
|
||||
color: var(--gold);
|
||||
background: var(--bg-card);
|
||||
color: var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -189,7 +189,7 @@ watch(totalPages, () => {
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-bottom: 1px solid var(--border);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
@@ -198,22 +198,23 @@ watch(totalPages, () => {
|
||||
}
|
||||
|
||||
.announce-row:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.row-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -226,7 +227,7 @@ watch(totalPages, () => {
|
||||
}
|
||||
|
||||
.chevron {
|
||||
color: var(--gold);
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import MatchBetCard from './MatchBetCard.vue';
|
||||
import saishiImg from '../assets/images/saishi.webp';
|
||||
import cardBg from '../assets/images/card-bg.webp';
|
||||
|
||||
const matchCardBg = `url(${cardBg})`;
|
||||
|
||||
const props = defineProps<{
|
||||
leagueId: string;
|
||||
@@ -88,11 +85,12 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
.league-block {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-card);
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #CBD5E1;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.league-row {
|
||||
@@ -114,8 +112,7 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
|
||||
opacity: 0.25;
|
||||
background: linear-gradient(135deg, rgba(0, 61, 107, 0.02) 0%, transparent 60%);
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -129,17 +126,17 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: #141414;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: #003D6B;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toggle-mark {
|
||||
color: var(--primary-light);
|
||||
color: #FFFFFF;
|
||||
font-size: 17px;
|
||||
font-weight: 900;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-top: -1px;
|
||||
}
|
||||
@@ -154,8 +151,8 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
|
||||
.league-title {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
color: #1A1A2E;
|
||||
line-height: 1.35;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -165,16 +162,16 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: #ccc;
|
||||
color: #6B7280;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-live {
|
||||
color: #3db865;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.stat-open {
|
||||
color: #c8a84e;
|
||||
color: #D97706;
|
||||
}
|
||||
|
||||
.league-saishi {
|
||||
@@ -185,11 +182,13 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
object-fit: contain;
|
||||
margin-left: 4px;
|
||||
padding-left: 10px;
|
||||
border-left: 1px solid #2a2a2a;
|
||||
border-left: 1px solid #CBD5E1;
|
||||
}
|
||||
|
||||
.match-panel {
|
||||
padding: 10px 0 4px;
|
||||
padding: 8px 0 4px;
|
||||
background: #F0F2F6;
|
||||
border-top: 1px solid #D1D5DB;
|
||||
}
|
||||
|
||||
.match-grid {
|
||||
|
||||
@@ -27,6 +27,6 @@ const src = computed(() => `/flags/${countryCode.value}.svg`);
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -80,16 +80,17 @@ onUnmounted(() => {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 6px;
|
||||
background: #0d0d0d;
|
||||
color: var(--primary-light);
|
||||
background: #FFFFFF;
|
||||
color: #003D6B;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.locale-switch.compact .locale-trigger {
|
||||
@@ -106,10 +107,10 @@ onUnmounted(() => {
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
list-style: none;
|
||||
background: #141414;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.locale-option {
|
||||
@@ -118,7 +119,7 @@ onUnmounted(() => {
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
@@ -127,7 +128,7 @@ onUnmounted(() => {
|
||||
|
||||
.locale-option:hover,
|
||||
.locale-option.active {
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
color: var(--primary-light);
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
color: #003D6B;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -55,7 +55,7 @@ function continueBrowsing() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
@@ -64,10 +64,11 @@ function continueBrowsing() {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 360px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid var(--border-gold-soft, rgba(200, 168, 78, 0.25));
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 12px;
|
||||
padding: 28px 24px 24px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@@ -76,7 +77,7 @@ function continueBrowsing() {
|
||||
right: 14px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
color: #6B7280;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
@@ -84,20 +85,20 @@ function continueBrowsing() {
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #aaa;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light, #c8a84e);
|
||||
color: #003D6B;
|
||||
margin: 0 0 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
color: #6B7280;
|
||||
text-align: center;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
@@ -112,8 +113,8 @@ function continueBrowsing() {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #d4a017, #e8c84a);
|
||||
color: #1a1a1a;
|
||||
background: #003D6B;
|
||||
color: #FFFFFF;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
@@ -128,9 +129,9 @@ function continueBrowsing() {
|
||||
.login-secondary {
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #333;
|
||||
border: 1px solid #E5E7EB;
|
||||
background: transparent;
|
||||
color: #888;
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
@@ -139,8 +140,8 @@ function continueBrowsing() {
|
||||
}
|
||||
|
||||
.login-secondary:active {
|
||||
color: #aaa;
|
||||
border-color: #444;
|
||||
color: #1A1A2E;
|
||||
border-color: #003D6B;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { ref, computed, getCurrentInstance, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { teamFlagUrl } from '../utils/teamFlag';
|
||||
import { matchPhaseLabel, type MatchPhase } from '../utils/matchPhase';
|
||||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||||
import { useBetSlipStore } from '../stores/betSlip';
|
||||
import { useDesktopBetPopover } from '../composables/useDesktopBetPopover';
|
||||
import { resolveSelectionLabel } from '../utils/selectionLabel';
|
||||
import MarketSelectionsPanel from './match-detail/MarketSelectionsPanel.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
match: {
|
||||
@@ -31,12 +36,21 @@ const props = defineProps<{
|
||||
homeCards?: number | null;
|
||||
awayCards?: number | null;
|
||||
} | null;
|
||||
markets?: any[];
|
||||
};
|
||||
noQuickBet?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ bet: [id: string] }>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const slip = useBetSlipStore();
|
||||
const { openAt, visible: popoverVisible, pendingItem, cancelClose, scheduleClose, isActiveForCard } =
|
||||
useDesktopBetPopover();
|
||||
|
||||
const cardRootId = `bet-card-${props.match.id}-${getCurrentInstance()?.uid ?? 0}`;
|
||||
const isCardEngaged = computed(() => isActiveForCard(cardRootId));
|
||||
|
||||
function formatKickoff(startTime: string) {
|
||||
return formatLocalMatchDateTime(startTime, locale.value, {
|
||||
@@ -70,19 +84,149 @@ const liveScoreText = computed(() => {
|
||||
if (!s || s.ftHome == null || s.ftAway == null) return '';
|
||||
return `${s.ftHome} - ${s.ftAway}`;
|
||||
});
|
||||
|
||||
function getHoveredSide() {
|
||||
if (!popoverVisible.value || !pendingItem.value) return '';
|
||||
if (!isActiveForCard(cardRootId)) return '';
|
||||
if (String(pendingItem.value.matchId) !== String(props.match.id)) return '';
|
||||
|
||||
const selId = pendingItem.value.selectionId;
|
||||
for (const market of props.match.markets ?? []) {
|
||||
const sel = market.selections?.find((s: { id: string }) => s.id === selId);
|
||||
if (!sel) continue;
|
||||
const code = (sel.selectionCode || '').toUpperCase();
|
||||
if (code === 'HOME') return 'home';
|
||||
if (code === 'AWAY') return 'away';
|
||||
if (code === 'DRAW') return 'draw';
|
||||
if (code === 'OVER') return 'home';
|
||||
if (code === 'UNDER') return 'away';
|
||||
if (code === 'ODD' || code === 'EVEN') return 'draw';
|
||||
const selIndex = market.selections?.findIndex((s: { id: string }) => s.id === selId) ?? -1;
|
||||
if (selIndex === -1) continue;
|
||||
if (market.marketType === 'FT_1X2') {
|
||||
if (selIndex === 0) return 'home';
|
||||
if (selIndex === 1) return 'draw';
|
||||
if (selIndex === 2) return 'away';
|
||||
} else {
|
||||
if (selIndex === 0) return 'home';
|
||||
if (selIndex === 1) return 'away';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function pickQuickBetMarkets(markets: any[] | undefined) {
|
||||
const supported = ['FT_1X2', 'FT_HANDICAP', 'FT_OVER_UNDER'];
|
||||
const byType = new Map<string, any>();
|
||||
for (const m of markets ?? []) {
|
||||
if (!supported.includes(m.marketType)) continue;
|
||||
if (!Array.isArray(m.selections) || m.selections.length === 0) continue;
|
||||
if (!byType.has(m.marketType)) byType.set(m.marketType, m);
|
||||
}
|
||||
return supported.map((type) => byType.get(type)).filter(Boolean);
|
||||
}
|
||||
|
||||
const activeMarketType = ref('');
|
||||
|
||||
watch(
|
||||
() => props.match.id,
|
||||
() => {
|
||||
activeMarketType.value = '';
|
||||
},
|
||||
);
|
||||
|
||||
const currentMarketType = computed(() => {
|
||||
const available = pickQuickBetMarkets(props.match.markets);
|
||||
if (activeMarketType.value && available.some((m) => m.marketType === activeMarketType.value)) {
|
||||
return activeMarketType.value;
|
||||
}
|
||||
if (available.some((m) => m.marketType === 'FT_1X2')) return 'FT_1X2';
|
||||
return available[0]?.marketType ?? '';
|
||||
});
|
||||
|
||||
function isSelected(id: string) {
|
||||
return slip.isInSlip(id);
|
||||
}
|
||||
|
||||
function getAvailableMarketsForMatch(match: typeof props.match) {
|
||||
return pickQuickBetMarkets(match.markets);
|
||||
}
|
||||
|
||||
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*/gi, '').trim();
|
||||
}
|
||||
|
||||
function getSelLabel(sel: { selectionCode?: string; selectionName: string }, _marketType: string, lineValue?: string | number | null) {
|
||||
return resolveSelectionLabel(t, sel.selectionCode || '', sel.selectionName || '', { lineValue });
|
||||
}
|
||||
|
||||
function isMarketLocked(match: any, 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 getActiveMarket() {
|
||||
const type = currentMarketType.value;
|
||||
if (!type) return undefined;
|
||||
return pickQuickBetMarkets(props.match.markets).find((m) => m.marketType === type);
|
||||
}
|
||||
|
||||
function handleOddsClick(match: any, market: any, selId: string, event?: MouseEvent) {
|
||||
if (isMarketLocked(match, market)) return;
|
||||
const sel = market.selections?.find((s: { id: string }) => s.id === selId);
|
||||
if (!sel || !event) return;
|
||||
|
||||
openAt(event, {
|
||||
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: getSelLabel(sel, market.marketType, 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,
|
||||
});
|
||||
}
|
||||
|
||||
function goMatchDetail() {
|
||||
router.push(`/match/${props.match.id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="match-card"
|
||||
:class="{ 'match-card--phase': phase !== 'open' }"
|
||||
:data-bet-card-root="cardRootId"
|
||||
:class="{
|
||||
'match-card--phase': phase !== 'open',
|
||||
'no-quick-bet': noQuickBet,
|
||||
'match-card--engaged': isCardEngaged,
|
||||
}"
|
||||
@click="emit('bet', match.id)"
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="scheduleClose()"
|
||||
>
|
||||
<span v-if="phase === 'open'" class="status-tag status-tag--open">{{ t('bet.status_open') }}</span>
|
||||
<span v-else-if="phase === 'settled'" class="status-tag status-tag--settled">{{ phaseLabel }}</span>
|
||||
<span v-else class="status-tag status-tag--pending">{{ phaseLabel }}</span>
|
||||
|
||||
<div class="teams-row">
|
||||
<div class="teams-row" :class="getHoveredSide()">
|
||||
<div class="team">
|
||||
<span class="team-name">{{ match.homeTeamName }}</span>
|
||||
<img
|
||||
@@ -124,38 +268,73 @@ const liveScoreText = computed(() => {
|
||||
>
|
||||
{{ bettingOpen ? t('bet.place_bet_short') : t('bet.view_match') }}
|
||||
</button>
|
||||
|
||||
<div v-if="!noQuickBet" class="card-quick-bet" @click.stop>
|
||||
<div v-if="getAvailableMarketsForMatch(match).length" class="quick-bet-tabs">
|
||||
<button
|
||||
v-for="m in getAvailableMarketsForMatch(match)"
|
||||
:key="m.marketType"
|
||||
type="button"
|
||||
class="quick-bet-tab-btn"
|
||||
:class="{ active: currentMarketType === m.marketType }"
|
||||
@click="activeMarketType = m.marketType"
|
||||
>
|
||||
{{ getMarketTabLabel(m.marketType) }}
|
||||
</button>
|
||||
<span class="quick-bet-all-link" @click="goMatchDetail">{{ t('bet.all') }} →</span>
|
||||
</div>
|
||||
|
||||
<div class="quick-bet-odds">
|
||||
<template v-if="getActiveMarket()">
|
||||
<MarketSelectionsPanel
|
||||
desktop-card
|
||||
compact
|
||||
:locked="isMarketLocked(match, getActiveMarket())"
|
||||
:line-value="getActiveMarket()?.lineValue"
|
||||
:selections="(getActiveMarket()?.selections || []).map((s: any) => ({
|
||||
...s,
|
||||
odds: String(s.odds),
|
||||
}))"
|
||||
:is-selected="isSelected"
|
||||
@pick="(selId, ev) => handleOddsClick(match, getActiveMarket()!, selId, ev)"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="quick-bet-no-odds">
|
||||
{{ t('bet.no_market_odds') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.match-card {
|
||||
position: relative;
|
||||
background: linear-gradient(180deg, #1a1a1a 0%, #0d0d0d 100%);
|
||||
border: 1px solid rgba(140, 140, 140, 0.35);
|
||||
border-radius: 6px;
|
||||
padding: 10px 8px 10px;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 130px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s;
|
||||
}
|
||||
|
||||
.match-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(100, 100, 100, 0.08) 0%, transparent 50%, rgba(80, 80, 80, 0.05) 100%);
|
||||
background: linear-gradient(135deg, rgba(0, 61, 107, 0.03) 0%, transparent 50%, rgba(0, 91, 159, 0.03) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.match-card--phase {
|
||||
/* inherits base dark style */
|
||||
}
|
||||
|
||||
.teams-row {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@@ -163,6 +342,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 {
|
||||
@@ -173,6 +353,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 {
|
||||
@@ -180,32 +361,33 @@ const liveScoreText = computed(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 17px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
line-height: 1.2;
|
||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.95);
|
||||
word-break: break-word;
|
||||
text-align: center;
|
||||
padding: 0 6px;
|
||||
padding: 0 4px;
|
||||
align-self: stretch;
|
||||
transition: font-size 0.25s ease;
|
||||
}
|
||||
|
||||
.match-card--phase .team-name {
|
||||
color: #d8d8d8;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.team-flag {
|
||||
width: 72px;
|
||||
height: 48px;
|
||||
width: 56px;
|
||||
height: 38px;
|
||||
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;
|
||||
}
|
||||
|
||||
.team-flag.flag-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@@ -216,36 +398,27 @@ const liveScoreText = computed(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-width: 56px;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.kickoff {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.15;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
max-width: 96px;
|
||||
max-width: 80px;
|
||||
overflow-wrap: anywhere;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.live-score {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.04em;
|
||||
transition: opacity 0.2s ease, height 0.2s ease, margin 0.2s ease;
|
||||
}
|
||||
|
||||
.live-score,
|
||||
.vs {
|
||||
font-size: 22px;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--primary);
|
||||
line-height: 1;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
@@ -254,49 +427,222 @@ const liveScoreText = computed(() => {
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
padding: 3px 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 0 6px 0 8px;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.status-tag--open {
|
||||
background: linear-gradient(135deg, #e8c84a, #d4a017);
|
||||
color: #1a1a1a;
|
||||
box-shadow: 0 2px 6px rgba(212, 160, 23, 0.4);
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
border-bottom: 1px solid #fde68a;
|
||||
border-left: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
.status-tag--settled {
|
||||
background: linear-gradient(180deg, #2a2a3a 0%, #1a1a28 100%);
|
||||
color: #8a9ab8;
|
||||
border-bottom: 1px solid rgba(120, 140, 180, 0.3);
|
||||
border-left: 1px solid rgba(120, 140, 180, 0.3);
|
||||
background: #e8edf2;
|
||||
color: #4b5563;
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
border-left: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.status-tag--pending {
|
||||
background: linear-gradient(180deg, #3a3a2a 0%, #28251a 100%);
|
||||
color: #c8a84e;
|
||||
border-bottom: 1px solid rgba(200, 168, 78, 0.3);
|
||||
border-left: 1px solid rgba(200, 168, 78, 0.3);
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
border-bottom: 1px solid #fde68a;
|
||||
border-left: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
.bet-btn {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: auto;
|
||||
min-width: 80px;
|
||||
padding: 5px 24px;
|
||||
min-width: 72px;
|
||||
padding: 4px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.04em;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.bet-btn--view {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
color: #aaa;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.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-active);
|
||||
box-shadow: 0 4px 12px rgba(0, 61, 107, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .teams-row {
|
||||
transform: translateY(0);
|
||||
padding: 0 28px;
|
||||
}
|
||||
|
||||
.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-active);
|
||||
box-shadow: 0 4px 12px rgba(0, 61, 107, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.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,
|
||||
.quick-bet-tab-btn.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.quick-bet-all-link {
|
||||
margin-left: auto;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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.08) saturate(1.15);
|
||||
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.08) saturate(1.15);
|
||||
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.08) saturate(1.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -195,11 +195,11 @@ onActivated(tryLoad);
|
||||
}
|
||||
|
||||
.login-link {
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border: 1px solid var(--border-active);
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
color: var(--gold);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -230,7 +230,7 @@ onActivated(tryLoad);
|
||||
}
|
||||
|
||||
.list-row.unread .title {
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -243,8 +243,8 @@ onActivated(tryLoad);
|
||||
}
|
||||
|
||||
.list-row.unread .row-dot {
|
||||
background: var(--gold);
|
||||
box-shadow: 0 0 8px rgba(212, 175, 55, 0.45);
|
||||
background: var(--primary);
|
||||
box-shadow: 0 0 8px rgba(0, 102, 204, 0.35);
|
||||
}
|
||||
|
||||
.row-main {
|
||||
@@ -266,7 +266,7 @@ onActivated(tryLoad);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 15px;
|
||||
color: #d8d8d8;
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -279,13 +279,13 @@ onActivated(tryLoad);
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.status-badge.unread {
|
||||
color: var(--gold);
|
||||
background: rgba(212, 175, 55, 0.12);
|
||||
color: var(--primary);
|
||||
background: rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
.preview {
|
||||
@@ -341,8 +341,8 @@ onActivated(tryLoad);
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #141414;
|
||||
color: var(--gold);
|
||||
background: var(--bg-hover);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
73
apps/player/src/components/PageBackButton.vue
Normal file
73
apps/player/src/components/PageBackButton.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useSmartBack } from '../composables/useSmartBack';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
fallback?: string;
|
||||
variant?: 'desktop' | 'mobile';
|
||||
showLabel?: boolean;
|
||||
}>(),
|
||||
{
|
||||
variant: 'desktop',
|
||||
showLabel: true,
|
||||
},
|
||||
);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { goBack } = useSmartBack(() => props.fallback);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="page-back-btn"
|
||||
:class="[`page-back-btn--${variant}`]"
|
||||
:aria-label="t('common.back')"
|
||||
@click="goBack"
|
||||
>
|
||||
<span class="page-back-icon" aria-hidden="true">‹</span>
|
||||
<span v-if="showLabel && variant === 'desktop'" class="page-back-label">
|
||||
{{ t('common.back') }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.page-back-btn--desktop {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.page-back-btn--desktop:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.page-back-btn--mobile {
|
||||
color: #003d6b;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.page-back-icon {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.page-back-btn--desktop .page-back-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -129,19 +129,20 @@ onUnmounted(() => {
|
||||
gap: 3px;
|
||||
height: 36px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 6px;
|
||||
background: #0d0d0d;
|
||||
color: var(--text);
|
||||
background: #FFFFFF;
|
||||
color: #1A1A2E;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.country-trigger:active {
|
||||
background: var(--bg-hover);
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.country-iso {
|
||||
@@ -150,12 +151,12 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.country-sep {
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.country-dial {
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -166,31 +167,31 @@ onUnmounted(() => {
|
||||
z-index: 60;
|
||||
width: min(260px, calc(100vw - 48px));
|
||||
padding: 4px;
|
||||
background: #141414;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--shadow);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.country-search {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 4px;
|
||||
background: #0d0d0d;
|
||||
color: var(--text);
|
||||
background: #F5F7FA;
|
||||
color: #1A1A2E;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.country-search::placeholder {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.country-search:focus {
|
||||
outline: none;
|
||||
border-color: #555;
|
||||
border-color: #003D6B;
|
||||
}
|
||||
|
||||
.country-menu {
|
||||
@@ -213,20 +214,20 @@ onUnmounted(() => {
|
||||
|
||||
.country-option:hover,
|
||||
.country-option.active {
|
||||
background: var(--bg-hover);
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.country-option .country-iso {
|
||||
flex-shrink: 0;
|
||||
width: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.country-option .country-dial {
|
||||
flex-shrink: 0;
|
||||
width: 38px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.country-name {
|
||||
@@ -234,17 +235,17 @@ onUnmounted(() => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.country-option:hover .country-name,
|
||||
.country-option.active .country-name {
|
||||
color: var(--text);
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.country-empty {
|
||||
padding: 10px 8px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ function confirm() {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 210;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -69,11 +69,11 @@ function confirm() {
|
||||
max-width: 360px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(165deg, #1a1810 0%, #121212 45%, #0a0a0a 100%);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: var(--radius);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 12px;
|
||||
padding: 16px 14px 14px;
|
||||
box-shadow: var(--shadow), 0 0 24px rgba(212, 175, 55, 0.08);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.close-x {
|
||||
@@ -83,9 +83,9 @@ function confirm() {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #F5F7FA;
|
||||
color: #6B7280;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
@@ -95,7 +95,7 @@ function confirm() {
|
||||
margin: 0 28px 12px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -114,12 +114,14 @@ function confirm() {
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
border: 1px solid var(--border);
|
||||
background: #0a0a0a;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
border: none;
|
||||
background: #003D6B;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -78,7 +78,7 @@ function select(key: string) {
|
||||
.picker-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
@@ -87,15 +87,15 @@ function select(key: string) {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #0a0a0a;
|
||||
color: var(--text);
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #F5F7FA;
|
||||
color: #1A1A2E;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.picker-search:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: #003D6B;
|
||||
}
|
||||
|
||||
.picker-grid {
|
||||
@@ -114,16 +114,16 @@ function select(key: string) {
|
||||
gap: 4px;
|
||||
padding: 8px 4px 6px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.picker-item.active {
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: 0 0 0 1px rgba(212, 175, 55, 0.18);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
border-color: #003D6B;
|
||||
box-shadow: 0 0 0 1px rgba(0, 61, 107, 0.18);
|
||||
background: rgba(0, 61, 107, 0.04);
|
||||
}
|
||||
|
||||
.picker-photo {
|
||||
@@ -132,14 +132,14 @@ function select(key: string) {
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.picker-name {
|
||||
width: 100%;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
color: #1A1A2E;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
@@ -150,7 +150,7 @@ function select(key: string) {
|
||||
.picker-meta {
|
||||
width: 100%;
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
@@ -161,7 +161,7 @@ function select(key: string) {
|
||||
.picker-empty {
|
||||
margin: 8px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,6 +15,23 @@ const CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
const CANVAS_W = 96;
|
||||
const CANVAS_H = 36;
|
||||
|
||||
function cssVar(name: string, fallback: string): string {
|
||||
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
return value || fallback;
|
||||
}
|
||||
|
||||
function withAlpha(color: string, alpha: number): string {
|
||||
const c = color.trim();
|
||||
const rgb = c.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/);
|
||||
if (rgb) return `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${alpha})`;
|
||||
const hex = c.match(/^#([0-9a-f]{6})$/i);
|
||||
if (hex) {
|
||||
const n = parseInt(hex[1], 16);
|
||||
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
function generateCode() {
|
||||
let result = '';
|
||||
for (let i = 0; i < 4; i++) {
|
||||
@@ -31,18 +48,22 @@ function drawCaptcha() {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.fillStyle = '#0d0d0d';
|
||||
const bg = cssVar('--captcha-canvas-bg', '#0d0d0d');
|
||||
const charBase = cssVar('--captcha-char', '#f0d875');
|
||||
const noiseRgb = cssVar('--captcha-noise-rgb', '255, 255, 255');
|
||||
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${0.03 + Math.random() * 0.06})`;
|
||||
ctx.fillStyle = `rgba(${noiseRgb}, ${0.03 + Math.random() * 0.06})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(Math.random() * CANVAS_W, Math.random() * CANVAS_H, Math.random() * 1.8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${0.06 + Math.random() * 0.08})`;
|
||||
ctx.strokeStyle = `rgba(${noiseRgb}, ${0.06 + Math.random() * 0.08})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.random() * CANVAS_W, Math.random() * CANVAS_H);
|
||||
@@ -58,7 +79,7 @@ function drawCaptcha() {
|
||||
ctx.translate(x, y);
|
||||
ctx.rotate((Math.random() - 0.5) * 0.35);
|
||||
ctx.font = `bold ${15 + Math.random() * 4}px 'Courier New', monospace`;
|
||||
ctx.fillStyle = `rgba(240, 216, 117, ${0.85 + Math.random() * 0.15})`;
|
||||
ctx.fillStyle = withAlpha(charBase, 0.85 + Math.random() * 0.15);
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(code.value[i], 0, 0);
|
||||
@@ -139,14 +160,14 @@ defineExpose({ validate, refresh });
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 36px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #0d0d0d;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.captcha-row:focus-within {
|
||||
border-color: #555;
|
||||
border-color: #003D6B;
|
||||
}
|
||||
|
||||
.hp-field {
|
||||
@@ -164,7 +185,7 @@ defineExpose({ validate, refresh });
|
||||
padding: 0 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
color: #1A1A2E;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
@@ -174,7 +195,7 @@ defineExpose({ validate, refresh });
|
||||
}
|
||||
|
||||
.captcha-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.32);
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: none;
|
||||
@@ -187,11 +208,11 @@ defineExpose({ validate, refresh });
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
border: none;
|
||||
border-left: 1px solid var(--border);
|
||||
border-left: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.captcha-error {
|
||||
color: var(--danger);
|
||||
color: #DC2626;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin: 4px 0 0;
|
||||
|
||||
@@ -23,7 +23,7 @@ withDefaults(
|
||||
letter-spacing: 0.06em;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0.2;
|
||||
opacity: 0.15;
|
||||
max-width: 92%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -43,26 +43,26 @@ withDefaults(
|
||||
}
|
||||
|
||||
.status-watermark.won {
|
||||
color: #3db865;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.status-watermark.lost {
|
||||
color: #e05050;
|
||||
color: #DC2626;
|
||||
}
|
||||
|
||||
.status-watermark.push {
|
||||
color: #888;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.status-watermark.pending {
|
||||
color: #e8c84a;
|
||||
color: #F8971F;
|
||||
}
|
||||
|
||||
.status-watermark.closed {
|
||||
color: #c9a84a;
|
||||
color: #005B9F;
|
||||
}
|
||||
|
||||
.status-watermark.settled {
|
||||
color: #7a9ab8;
|
||||
color: #0066CC;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -62,7 +62,7 @@ watch(
|
||||
/* 队徽(非国旗):圆角 + 投影 */
|
||||
.team-emblem--logo {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.team-emblem--sm {
|
||||
|
||||
@@ -85,8 +85,8 @@ function logout() {
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: linear-gradient(145deg, #2a2210, #141008);
|
||||
border: 2px solid #003D6B;
|
||||
background: linear-gradient(145deg, #003D6B, #005B9F);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -106,7 +106,7 @@ function logout() {
|
||||
.avatar-letter {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.avatar-menu {
|
||||
@@ -115,10 +115,10 @@ function logout() {
|
||||
right: 0;
|
||||
min-width: 168px;
|
||||
padding: 8px 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #141414;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 140;
|
||||
}
|
||||
|
||||
@@ -126,8 +126,8 @@ function logout() {
|
||||
padding: 8px 14px 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: #003D6B;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -136,22 +136,22 @@ function logout() {
|
||||
text-align: left;
|
||||
padding: 10px 14px;
|
||||
background: none;
|
||||
color: var(--text);
|
||||
color: #1A1A2E;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.menu-item.recharge {
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.menu-item.danger {
|
||||
color: var(--danger);
|
||||
color: #DC2626;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
|
||||
@@ -10,14 +10,14 @@ const isWalletSection = computed(
|
||||
() => route.path.startsWith('/wallet') || route.path === '/profile/cashbacks',
|
||||
);
|
||||
|
||||
const hubTitleKey = computed(() => (isWalletSection.value ? 'nav.wallet' : 'nav.bet_history'));
|
||||
const hubTitleKey = computed(() => (isWalletSection.value ? 'nav.my_wallet' : 'nav.bet_history'));
|
||||
|
||||
const menuItems = computed(() => {
|
||||
if (isWalletSection.value) {
|
||||
return [
|
||||
{
|
||||
path: '/wallet',
|
||||
labelKey: 'nav.wallet',
|
||||
labelKey: 'nav.my_wallet',
|
||||
match: (p: string) =>
|
||||
p === '/wallet' ||
|
||||
p === '/wallet/detail' ||
|
||||
@@ -74,6 +74,7 @@ const menuItems = computed(() => {
|
||||
.hub-nav-title {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
@@ -89,17 +90,18 @@ const menuItems = computed(() => {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: #fff;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.nav-item.active .dot {
|
||||
@@ -111,7 +113,7 @@ const menuItems = computed(() => {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #333;
|
||||
background: var(--bg-hover);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter, RouterLink } from 'vue-router';
|
||||
import { PARLAY_MIN_LEGS, PARLAY_MAX_LEGS } from '@thebet365/shared';
|
||||
@@ -26,6 +26,7 @@ const { refreshProfile } = usePlayerProfile();
|
||||
const { requestLocate } = useDesktopBetLocate();
|
||||
|
||||
const activeTab = ref<PanelTab>('single');
|
||||
const transitionName = ref('list-slide');
|
||||
const historyScope = ref<'all' | 'match'>('all');
|
||||
const historyItems = ref<BetHistoryItem[]>([]);
|
||||
const historyTotal = ref(0);
|
||||
@@ -75,6 +76,8 @@ const currentMatchId = computed(() => {
|
||||
return outright ? outright[1] : '';
|
||||
});
|
||||
|
||||
const isHomePage = computed(() => route.path === '/');
|
||||
|
||||
const showMatchHistoryFilter = computed(() => Boolean(currentMatchId.value));
|
||||
|
||||
const singleSlipItems = computed<SlipItem[]>(() => {
|
||||
@@ -190,22 +193,32 @@ function genId() {
|
||||
}
|
||||
|
||||
function selectTab(tab: PanelTab) {
|
||||
transitionName.value = '';
|
||||
activeTab.value = tab;
|
||||
error.value = '';
|
||||
if (tab === 'history') {
|
||||
if (!auth.token) {
|
||||
auth.showLoginPrompt(route.fullPath);
|
||||
activeTab.value = slip.mode;
|
||||
nextTick(() => {
|
||||
transitionName.value = 'list-slide';
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (showMatchHistoryFilter.value) {
|
||||
historyScope.value = 'match';
|
||||
}
|
||||
void loadHistory(true);
|
||||
nextTick(() => {
|
||||
transitionName.value = 'list-slide';
|
||||
});
|
||||
return;
|
||||
}
|
||||
slip.setMode(tab);
|
||||
syncStakeInputFromSlip();
|
||||
nextTick(() => {
|
||||
transitionName.value = 'list-slide';
|
||||
});
|
||||
}
|
||||
|
||||
function selectHistoryScope(scope: 'all' | 'match') {
|
||||
@@ -805,7 +818,13 @@ watch(
|
||||
watch(
|
||||
() => slip.mode,
|
||||
(mode) => {
|
||||
if (activeTab.value !== 'history') activeTab.value = mode;
|
||||
if (activeTab.value !== 'history') {
|
||||
transitionName.value = '';
|
||||
activeTab.value = mode;
|
||||
nextTick(() => {
|
||||
transitionName.value = 'list-slide';
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -911,7 +930,7 @@ onUnmounted(() => {
|
||||
<p v-if="activeTab !== 'history' && oddsWarningText" class="odds-warning">{{ oddsWarningText }}</p>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="panel-body">
|
||||
<div class="panel-body" :class="{ 'no-scrollbar': isHomePage }">
|
||||
<template v-if="activeTab === 'history'">
|
||||
<div v-if="historyInitialLoading" class="history-loading">
|
||||
<GoldSpinner :size="28" />
|
||||
@@ -969,7 +988,7 @@ onUnmounted(() => {
|
||||
<p>{{ activeTab === 'parlay' ? t('bet.slip_parlay_empty_hint') : t('bet.slip_empty_hint') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="items-list">
|
||||
<TransitionGroup v-else :name="transitionName" tag="div" class="items-list">
|
||||
<p v-if="activeTab === 'parlay' && parlayWarning" class="panel-warning">{{ parlayWarning }}</p>
|
||||
<div
|
||||
v-for="item in activeItems"
|
||||
@@ -1073,7 +1092,7 @@ onUnmounted(() => {
|
||||
<span>{{ t('bet.slip_parlay_count', { n: slip.parlayItems.length }) }}</span>
|
||||
<span class="parlay-odds">{{ t('bet.slip_parlay_odds', { odds: totalOddsText }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
|
||||
<!-- Stake input (parlay only) -->
|
||||
<div v-if="activeTab === 'parlay' && activeItems.length" class="stake-control">
|
||||
@@ -1154,7 +1173,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: rgba(16, 16, 16, 0.95);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
@@ -1170,6 +1189,7 @@ onUnmounted(() => {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
@@ -1194,8 +1214,8 @@ onUnmounted(() => {
|
||||
|
||||
.clear-slip-btn:hover {
|
||||
color: var(--primary-light);
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.06);
|
||||
border-color: var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
}
|
||||
|
||||
.balance-info {
|
||||
@@ -1233,6 +1253,7 @@ onUnmounted(() => {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
padding: 10px 4px;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
@@ -1253,17 +1274,18 @@ onUnmounted(() => {
|
||||
|
||||
.badge {
|
||||
background: var(--primary);
|
||||
color: #111;
|
||||
color: var(--bg-card);
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
padding: 1px 4px;
|
||||
border-radius: 6px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.odds-warning {
|
||||
background: rgba(212, 175, 55, 0.12);
|
||||
border-bottom: 1px solid var(--border-gold-soft);
|
||||
background: rgba(0, 102, 204, 0.12);
|
||||
border-bottom: 1px solid var(--border-active);
|
||||
color: var(--primary-light);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
@@ -1305,7 +1327,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.slip-card {
|
||||
background: #1e1e1e;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 7px 8px;
|
||||
@@ -1314,7 +1336,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.slip-card:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
}
|
||||
|
||||
.card-top {
|
||||
@@ -1336,6 +1358,7 @@ onUnmounted(() => {
|
||||
.match-name {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -1398,7 +1421,8 @@ onUnmounted(() => {
|
||||
.selection-name {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -1429,7 +1453,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.odds-suspended {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: var(--text-muted);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
@@ -1437,7 +1461,7 @@ onUnmounted(() => {
|
||||
.card-stake-block {
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -1453,7 +1477,7 @@ onUnmounted(() => {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #0d0d0d;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
padding: 0 6px;
|
||||
@@ -1465,7 +1489,7 @@ onUnmounted(() => {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none !important;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
@@ -1518,39 +1542,39 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.card-single-btn {
|
||||
border: 1px solid rgba(212, 175, 55, 0.55);
|
||||
background: rgba(212, 175, 55, 0.14);
|
||||
border: 1px solid rgba(0, 102, 204, 0.55);
|
||||
background: rgba(0, 102, 204, 0.14);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.card-single-btn:hover:not(:disabled) {
|
||||
background: rgba(212, 175, 55, 0.22);
|
||||
border-color: var(--border-gold);
|
||||
color: #fff;
|
||||
background: rgba(0, 102, 204, 0.22);
|
||||
border-color: var(--border-active);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.card-parlay-btn {
|
||||
border: 1px dashed rgba(212, 175, 55, 0.45);
|
||||
background: rgba(212, 175, 55, 0.04);
|
||||
border: 1px dashed rgba(0, 102, 204, 0.45);
|
||||
background: rgba(0, 102, 204, 0.04);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.card-parlay-btn:hover:not(:disabled) {
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-color: var(--border-gold-soft);
|
||||
color: #fff;
|
||||
background: rgba(0, 102, 204, 0.1);
|
||||
border-color: var(--border-active);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.card-parlay-btn.active {
|
||||
border-style: solid;
|
||||
border-color: rgba(212, 175, 55, 0.35);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-color: rgba(0, 102, 204, 0.35);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-parlay-btn.active:hover {
|
||||
color: var(--primary-light);
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
}
|
||||
|
||||
.card-parlay-btn-icon {
|
||||
@@ -1560,7 +1584,7 @@ onUnmounted(() => {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: rgba(212, 175, 55, 0.16);
|
||||
background: rgba(0, 102, 204, 0.16);
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
@@ -1590,8 +1614,8 @@ onUnmounted(() => {
|
||||
.panel-warning {
|
||||
font-size: 11px;
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
border: 1px solid var(--border-active);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin: 6px 0;
|
||||
@@ -1604,7 +1628,7 @@ onUnmounted(() => {
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
padding: 4px 6px;
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -1613,7 +1637,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.stake-control {
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
@@ -1624,7 +1648,7 @@ onUnmounted(() => {
|
||||
.stake-input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #0d0d0d;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0 10px;
|
||||
@@ -1642,7 +1666,7 @@ onUnmounted(() => {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none !important;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
@@ -1668,7 +1692,7 @@ onUnmounted(() => {
|
||||
|
||||
.chip-btn {
|
||||
flex: 1;
|
||||
background: #1e1e1e;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
@@ -1679,8 +1703,8 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.chip-btn:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
color: #fff;
|
||||
border-color: var(--border-active);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.panel-error-msg {
|
||||
@@ -1700,7 +1724,7 @@ onUnmounted(() => {
|
||||
.panel-bottom-fixed {
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
background: rgba(16, 16, 16, 0.98);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.panel-summary {
|
||||
@@ -1744,16 +1768,17 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border: 1px solid var(--border-active);
|
||||
background: transparent;
|
||||
color: var(--primary-light);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.add-parlay-btn:hover {
|
||||
background: rgba(212, 175, 55, 0.05);
|
||||
background: rgba(0, 102, 204, 0.05);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -1762,10 +1787,11 @@ onUnmounted(() => {
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(180deg, #F0D875 0%, #D4AF37 100%);
|
||||
color: #3D2800;
|
||||
background: linear-gradient(180deg, var(--primary-light) 0%, var(--primary) 100%);
|
||||
color: #FFFFFF;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
text-transform: uppercase;
|
||||
transition: opacity 0.2s;
|
||||
border: none;
|
||||
@@ -1783,8 +1809,8 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
border: 1px solid var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
@@ -1794,7 +1820,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.history-view-all-btn:hover {
|
||||
background: rgba(212, 175, 55, 0.14);
|
||||
background: rgba(0, 102, 204, 0.14);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -1818,13 +1844,14 @@ onUnmounted(() => {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.scope-chip.active {
|
||||
color: var(--primary-light);
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
border-color: var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.history-loading {
|
||||
@@ -1844,15 +1871,15 @@ onUnmounted(() => {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: #1e1e1e;
|
||||
background: var(--bg-body);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.history-card:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.04);
|
||||
border-color: var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.04);
|
||||
}
|
||||
|
||||
.history-card-top {
|
||||
@@ -1866,7 +1893,8 @@ onUnmounted(() => {
|
||||
.history-pick {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -1875,6 +1903,7 @@ onUnmounted(() => {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
@@ -1896,11 +1925,12 @@ onUnmounted(() => {
|
||||
|
||||
.history-status.hist-pending {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
background: rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
.history-match {
|
||||
font-size: 10px;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
@@ -1964,4 +1994,23 @@ onUnmounted(() => {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.list-slide-enter-active,
|
||||
.list-slide-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.list-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.95);
|
||||
}
|
||||
|
||||
.list-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
|
||||
.list-slide-move {
|
||||
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -60,7 +60,7 @@ const { t } = useI18n();
|
||||
.data-table-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
@@ -73,7 +73,7 @@ const { t } = useI18n();
|
||||
}
|
||||
|
||||
th {
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
color: var(--text-muted);
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
|
||||
171
apps/player/src/components/desktop/DesktopAuthLayout.vue
Normal file
171
apps/player/src/components/desktop/DesktopAuthLayout.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* PC auth shell — admin-style card: large logo left, form slot right (theme-2).
|
||||
*/
|
||||
import LocaleSwitcher from '../LocaleSwitcher.vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
wide?: boolean;
|
||||
}>(),
|
||||
{ wide: false },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-auth-page">
|
||||
<div class="ambient-glow ambient-glow--1" aria-hidden="true"></div>
|
||||
<div class="ambient-glow ambient-glow--2" aria-hidden="true"></div>
|
||||
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-card" :class="{ 'auth-card--wide': wide }">
|
||||
<div class="form-lang">
|
||||
<LocaleSwitcher compact />
|
||||
</div>
|
||||
<div class="form-body">
|
||||
<aside class="form-brand">
|
||||
<img src="/logo.png" alt="TheBet365" class="logo-large" />
|
||||
</aside>
|
||||
<div class="form-fields">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-auth-page {
|
||||
position: relative;
|
||||
min-height: 100dvh;
|
||||
background: linear-gradient(160deg, #f5f7fa 0%, #e8edf2 55%, #f5f7fa 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktop-auth-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 70% 60% at 50% 20%, rgba(0, 102, 204, 0.08) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ambient-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(70px);
|
||||
pointer-events: none;
|
||||
opacity: 0.35;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.ambient-glow--1 {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
top: 8%;
|
||||
left: 10%;
|
||||
background: radial-gradient(circle, rgba(0, 91, 159, 0.22) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.ambient-glow--2 {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
bottom: 10%;
|
||||
right: 8%;
|
||||
background: radial-gradient(circle, rgba(0, 102, 204, 0.16) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.auth-wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100dvh;
|
||||
padding: 24px 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
background: var(--glass-bg-strong);
|
||||
backdrop-filter: var(--blur-lg);
|
||||
-webkit-backdrop-filter: var(--blur-lg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-lg), 0 0 40px rgba(0, 102, 204, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-card--wide {
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
.form-lang {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 14px 18px 0;
|
||||
}
|
||||
|
||||
.form-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(200px, 260px) 1fr;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.form-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-9) var(--space-8);
|
||||
background: linear-gradient(180deg, rgba(0, 102, 204, 0.05) 0%, rgba(232, 237, 242, 0.65) 100%);
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.logo-large {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 6px 20px rgba(0, 91, 159, 0.18));
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 22px 24px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.auth-wrap {
|
||||
align-items: flex-start;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.auth-card,
|
||||
.auth-card--wide {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.form-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-brand {
|
||||
padding: 20px 24px 12px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.logo-large {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
padding: 18px 20px 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,8 @@ const props = defineProps<{
|
||||
fallbackTo?: string;
|
||||
}>();
|
||||
|
||||
const AUTOPLAY_MS = 5500;
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const active = ref(0);
|
||||
@@ -18,6 +20,7 @@ const FALLBACK_IMG = '/uploads/banners/welcome.svg';
|
||||
|
||||
const slideCount = computed(() => props.banners.length);
|
||||
const canLoop = computed(() => slideCount.value > 1);
|
||||
const activeBanner = computed(() => props.banners[active.value]);
|
||||
|
||||
function imageUrl(banner: BannerItem) {
|
||||
return banner.translation?.imageUrl || defaultBannerImg || FALLBACK_IMG;
|
||||
@@ -34,61 +37,6 @@ function title(banner: BannerItem) {
|
||||
return banner.translation?.title || t('home.banner_fallback');
|
||||
}
|
||||
|
||||
function normalizeOffset(index: number) {
|
||||
const len = slideCount.value;
|
||||
if (!len) return 0;
|
||||
let offset = index - active.value;
|
||||
if (offset > len / 2) offset -= len;
|
||||
if (offset < -len / 2) offset += len;
|
||||
return offset;
|
||||
}
|
||||
|
||||
/** Coverflow:略重叠,中间卡片置顶 */
|
||||
function slideLayout(index: number) {
|
||||
const offset = normalizeOffset(index);
|
||||
const abs = Math.abs(offset);
|
||||
|
||||
if (abs > 1) {
|
||||
return { hidden: true, offset, active: false };
|
||||
}
|
||||
|
||||
const slotX = 50 + offset * 26;
|
||||
const scale = offset === 0 ? 1 : 0.9;
|
||||
const rotateY = offset === 0 ? 0 : offset < 0 ? 12 : -12;
|
||||
const zIndex = offset === 0 ? 10 : 5;
|
||||
|
||||
return {
|
||||
hidden: false,
|
||||
offset,
|
||||
active: offset === 0,
|
||||
slotX,
|
||||
scale,
|
||||
rotateY,
|
||||
zIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function slideStyle(index: number) {
|
||||
const layout = slideLayout(index);
|
||||
if (layout.hidden) {
|
||||
return {
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%) scale(0.7)',
|
||||
opacity: '0',
|
||||
zIndex: '0',
|
||||
pointerEvents: 'none' as const,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
left: `${layout.slotX}%`,
|
||||
transform: `translate(-50%, -50%) scale(${layout.scale}) rotateY(${layout.rotateY}deg)`,
|
||||
opacity: '1',
|
||||
zIndex: String(layout.zIndex),
|
||||
pointerEvents: 'auto' as const,
|
||||
};
|
||||
}
|
||||
|
||||
function goTo(index: number) {
|
||||
if (!slideCount.value) return;
|
||||
active.value = (index + slideCount.value) % slideCount.value;
|
||||
@@ -102,11 +50,7 @@ function prev() {
|
||||
goTo(active.value - 1);
|
||||
}
|
||||
|
||||
function onBannerClick(banner: BannerItem, index: number) {
|
||||
if (normalizeOffset(index) !== 0) {
|
||||
goTo(index);
|
||||
return;
|
||||
}
|
||||
function onBannerClick(banner: BannerItem) {
|
||||
if (banner.id) {
|
||||
void router.push(`/announcements/${banner.id}`);
|
||||
return;
|
||||
@@ -119,7 +63,7 @@ function onBannerClick(banner: BannerItem, index: number) {
|
||||
function startAutoPlay() {
|
||||
stopAutoPlay();
|
||||
if (!canLoop.value) return;
|
||||
timer.value = setInterval(next, 5000);
|
||||
timer.value = setInterval(next, AUTOPLAY_MS);
|
||||
}
|
||||
|
||||
function stopAutoPlay() {
|
||||
@@ -144,46 +88,54 @@ onUnmounted(stopAutoPlay);
|
||||
<template>
|
||||
<section
|
||||
v-if="banners.length"
|
||||
class="banner-coverflow"
|
||||
class="hero-carousel"
|
||||
@mouseenter="stopAutoPlay"
|
||||
@mouseleave="startAutoPlay"
|
||||
>
|
||||
<div class="coverflow-stage">
|
||||
<button
|
||||
v-for="(banner, i) in banners"
|
||||
:key="banner.id ?? i"
|
||||
type="button"
|
||||
class="coverflow-slide"
|
||||
:class="{ 'is-active': slideLayout(i).active, 'is-side': Math.abs(slideLayout(i).offset) === 1 }"
|
||||
:style="slideStyle(i)"
|
||||
:aria-label="title(banner)"
|
||||
@click="onBannerClick(banner, i)"
|
||||
>
|
||||
<div class="slide-card">
|
||||
<div class="hero-frame">
|
||||
<div class="hero-stage">
|
||||
<button
|
||||
v-for="(banner, i) in banners"
|
||||
:key="banner.id ?? i"
|
||||
type="button"
|
||||
class="hero-slide"
|
||||
:class="{ 'is-active': i === active }"
|
||||
:aria-label="title(banner)"
|
||||
@click="onBannerClick(banner)"
|
||||
>
|
||||
<img
|
||||
v-if="imageUrl(banner)"
|
||||
:src="imageUrl(banner)"
|
||||
:alt="''"
|
||||
class="slide-img"
|
||||
alt=""
|
||||
class="hero-backdrop"
|
||||
aria-hidden="true"
|
||||
:loading="i === 0 ? 'eager' : 'lazy'"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<div v-else class="slide-fallback" aria-hidden="true">
|
||||
<svg class="fallback-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="currentColor" d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
||||
</svg>
|
||||
<img
|
||||
v-if="imageUrl(banner)"
|
||||
:src="imageUrl(banner)"
|
||||
:alt="title(banner)"
|
||||
class="hero-img"
|
||||
:loading="i === 0 ? 'eager' : 'lazy'"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<div v-else class="hero-fallback">
|
||||
<span>{{ title(banner) }}</span>
|
||||
</div>
|
||||
<div v-if="slideLayout(i).active" class="click-hint" aria-hidden="true">
|
||||
<span class="click-ring" />
|
||||
<svg class="click-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9 11.24V7.5a2.5 2.5 0 0 1 5 0v3.74c1.21-.81 2-2.18 2-3.74C16 5.01 13.99 3 11.5 3S7 5.01 7 7.5c0 1.56.79 2.93 2 3.74zm9.84 4.63-4.54-2.26a1.5 1.5 0 0 0-.54-.11H13v-6c0-.83-.67-1.5-1.5-1.5S10 6.67 10 7.5v10.74l-3.43-.72a1 1 0 0 0-.24-.03c-.31 0-.59.13-.79.33l-.79.8 4.94 4.94c.27.27.65.44 1.06.44h6.79c.75 0 1.33-.55 1.44-1.28l.75-5.27a1.5 1.5 0 0 0-.91-1.38z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class="hero-shade" aria-hidden="true" />
|
||||
|
||||
<div v-if="i === active" class="hero-caption">
|
||||
<span class="hero-badge">{{ t('home.announcement_badge') || '公告' }}</span>
|
||||
<h3 class="hero-title">{{ title(banner) }}</h3>
|
||||
<span class="hero-link">
|
||||
{{ t('announcements.go_link') }}
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="canLoop"
|
||||
@@ -203,109 +155,89 @@ onUnmounted(stopAutoPlay);
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||||
</button>
|
||||
|
||||
<div v-if="canLoop" class="hero-footer">
|
||||
<div class="hero-dots">
|
||||
<button
|
||||
v-for="(_, i) in banners"
|
||||
:key="i"
|
||||
type="button"
|
||||
class="dot"
|
||||
:class="{ active: i === active }"
|
||||
:aria-label="t('home.banner_slide', { n: i + 1 })"
|
||||
@click.stop="goTo(i)"
|
||||
/>
|
||||
</div>
|
||||
<div class="hero-progress" aria-hidden="true">
|
||||
<div :key="active" class="hero-progress-fill" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canLoop" class="coverflow-dots">
|
||||
<button
|
||||
v-for="(_, i) in banners"
|
||||
:key="i"
|
||||
type="button"
|
||||
class="dot"
|
||||
:class="{ active: i === active }"
|
||||
:aria-label="t('home.banner_slide', { n: i + 1 })"
|
||||
@click.stop="goTo(i)"
|
||||
/>
|
||||
<div v-if="activeBanner && canLoop" class="hero-counter" aria-hidden="true">
|
||||
{{ String(active + 1).padStart(2, '0') }} / {{ String(slideCount).padStart(2, '0') }}
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.banner-coverflow {
|
||||
width: 100%;
|
||||
padding: 4px 0 0;
|
||||
}
|
||||
|
||||
.coverflow-stage {
|
||||
.hero-carousel {
|
||||
position: relative;
|
||||
height: clamp(260px, 28vw, 360px);
|
||||
perspective: 1000px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.coverflow-slide {
|
||||
.hero-frame {
|
||||
position: relative;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
background: linear-gradient(135deg, #EEF4FA 0%, #FFFFFF 55%, #F5F7FA 100%);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.hero-stage {
|
||||
position: relative;
|
||||
height: clamp(220px, 24vw, 300px);
|
||||
}
|
||||
|
||||
.hero-slide {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 42%;
|
||||
max-width: 560px;
|
||||
height: 90%;
|
||||
inset: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transform: scale(1.015);
|
||||
transition:
|
||||
left 0.55s cubic-bezier(0.25, 0.8, 0.25, 1),
|
||||
transform 0.55s cubic-bezier(0.25, 0.8, 0.25, 1),
|
||||
opacity 0.4s ease;
|
||||
transform-style: preserve-3d;
|
||||
will-change: left, transform;
|
||||
}
|
||||
|
||||
.slide-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 14px;
|
||||
opacity 0.65s ease,
|
||||
transform 0.65s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #141414 0%, #0a0a0a 100%);
|
||||
box-shadow: 0 10px 32px rgba(0, 0, 0, 0.45);
|
||||
transition: box-shadow 0.35s ease, filter 0.35s ease, transform 0.25s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.coverflow-slide.is-side .slide-card {
|
||||
filter: brightness(0.72) saturate(0.9);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
||||
.hero-slide.is-active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
pointer-events: auto;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.coverflow-slide.is-side:hover .slide-card {
|
||||
filter: brightness(0.88) saturate(1);
|
||||
.hero-backdrop {
|
||||
position: absolute;
|
||||
inset: -24px;
|
||||
width: calc(100% + 48px);
|
||||
height: calc(100% + 48px);
|
||||
object-fit: cover;
|
||||
filter: blur(22px) saturate(1.1);
|
||||
opacity: 0.42;
|
||||
transform: scale(1.06);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.coverflow-slide.is-active .slide-card {
|
||||
filter: none;
|
||||
box-shadow:
|
||||
0 16px 40px rgba(0, 0, 0, 0.5),
|
||||
0 0 28px rgba(212, 175, 55, 0.22),
|
||||
0 0 56px rgba(212, 175, 55, 0.1);
|
||||
animation: banner-breathe 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.coverflow-slide.is-active:hover .slide-card {
|
||||
animation: none;
|
||||
transform: scale(1.018);
|
||||
}
|
||||
|
||||
@keyframes banner-breathe {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow:
|
||||
0 16px 40px rgba(0, 0, 0, 0.5),
|
||||
0 0 22px rgba(212, 175, 55, 0.16),
|
||||
0 0 48px rgba(212, 175, 55, 0.08);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.008);
|
||||
box-shadow:
|
||||
0 18px 44px rgba(0, 0, 0, 0.52),
|
||||
0 0 32px rgba(212, 175, 55, 0.26),
|
||||
0 0 60px rgba(212, 175, 55, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-img {
|
||||
.hero-img {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
@@ -313,111 +245,123 @@ onUnmounted(stopAutoPlay);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.slide-fallback {
|
||||
.hero-fallback {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.12), #1a1a1a);
|
||||
background: linear-gradient(135deg, rgba(0, 61, 107, 0.08), rgba(0, 102, 204, 0.04));
|
||||
font-size: clamp(18px, 2.2vw, 26px);
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.fallback-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: rgba(212, 175, 55, 0.35);
|
||||
}
|
||||
|
||||
.click-hint {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.click-ring {
|
||||
.hero-shade {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(212, 175, 55, 0.45);
|
||||
animation: click-ring-pulse 2.8s ease-out infinite;
|
||||
z-index: 2;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0, 45, 82, 0.04) 0%, transparent 38%),
|
||||
linear-gradient(0deg, rgba(0, 36, 71, 0.72) 0%, rgba(0, 36, 71, 0.28) 42%, transparent 72%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.click-icon {
|
||||
position: relative;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: rgba(212, 175, 55, 0.88);
|
||||
animation: click-icon-bob 2.8s ease-in-out infinite;
|
||||
.hero-caption {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
padding: 20px 24px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
text-align: left;
|
||||
animation: caption-in 0.55s ease both;
|
||||
}
|
||||
|
||||
@keyframes click-ring-pulse {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: scale(1.35);
|
||||
@keyframes caption-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1.35);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes click-icon-bob {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(1px) scale(1);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(-1px) scale(1.06);
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.coverflow-slide.is-active:hover .click-hint {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.coverflow-slide.is-active .slide-card,
|
||||
.click-ring,
|
||||
.click-icon {
|
||||
animation: none;
|
||||
}
|
||||
.hero-title {
|
||||
margin: 0;
|
||||
max-width: min(72%, 560px);
|
||||
font-size: clamp(16px, 1.8vw, 22px);
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.25);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
transition: color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.hero-link svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.hero-slide:hover .hero-link {
|
||||
color: #fff;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(212, 175, 55, 0.4);
|
||||
background: rgba(10, 10, 10, 0.75);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid rgba(255, 255, 255, 0.65);
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
color: var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 20;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
z-index: 10;
|
||||
box-shadow: 0 4px 14px rgba(0, 61, 107, 0.12);
|
||||
transition: background 0.2s, border-color 0.2s, transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.nav-btn svg {
|
||||
@@ -426,30 +370,42 @@ onUnmounted(stopAutoPlay);
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: rgba(212, 175, 55, 0.2);
|
||||
border-color: var(--primary);
|
||||
background: #fff;
|
||||
border-color: #fff;
|
||||
transform: translateY(-50%) scale(1.05);
|
||||
box-shadow: 0 6px 18px rgba(0, 61, 107, 0.16);
|
||||
}
|
||||
|
||||
.nav-btn.prev {
|
||||
left: 4px;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.nav-btn.next {
|
||||
right: 4px;
|
||||
right: 14px;
|
||||
}
|
||||
|
||||
.coverflow-dots {
|
||||
.hero-footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-dots {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
gap: 6px;
|
||||
padding: 0 0 10px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
@@ -457,12 +413,64 @@ onUnmounted(stopAutoPlay);
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
width: 22px;
|
||||
border-radius: 4px;
|
||||
background: var(--gradient-gold);
|
||||
width: 20px;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.dot:hover:not(.active) {
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.hero-progress {
|
||||
height: 3px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.hero-progress-fill {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.55), #fff);
|
||||
animation: hero-progress 5.5s linear forwards;
|
||||
}
|
||||
|
||||
@keyframes hero-progress {
|
||||
from {
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
to {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-counter {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
z-index: 10;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid rgba(0, 61, 107, 0.1);
|
||||
color: var(--primary);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 2px 8px rgba(0, 61, 107, 0.08);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-slide,
|
||||
.hero-caption,
|
||||
.hero-progress-fill {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.hero-progress-fill {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
502
apps/player/src/components/desktop/DesktopBetDetailModal.vue
Normal file
502
apps/player/src/components/desktop/DesktopBetDetailModal.vue
Normal file
@@ -0,0 +1,502 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api from '../../api';
|
||||
import GoldSpinner from '../GoldSpinner.vue';
|
||||
import { formatMoney } from '../../utils/localeDisplay';
|
||||
import type { BetHistoryItem } from '../BetHistoryCard.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
betNo: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const detail = ref<BetHistoryItem | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(false);
|
||||
|
||||
async function loadDetail(betNo: string) {
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
detail.value = null;
|
||||
try {
|
||||
const { data } = await api.get(`/player/bets/${betNo}`);
|
||||
detail.value = data.data ?? null;
|
||||
} catch {
|
||||
error.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.visible, props.betNo] as const,
|
||||
([vis, no]) => {
|
||||
if (vis && no) void loadDetail(no);
|
||||
if (!vis) {
|
||||
detail.value = null;
|
||||
error.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false);
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && props.visible) close();
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', onKeydown));
|
||||
onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
|
||||
function statusClass(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
WON: 'st-won',
|
||||
LOST: 'st-lost',
|
||||
PENDING: 'st-pending',
|
||||
PUSH: 'st-push',
|
||||
CANCELLED: 'st-cancelled',
|
||||
};
|
||||
return map[status?.toUpperCase()] ?? 'st-default';
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
WON: t('history.filter_won'),
|
||||
LOST: t('history.filter_lost'),
|
||||
PENDING: t('history.filter_pending'),
|
||||
PUSH: t('history.filter_push'),
|
||||
CANCELLED: t('common.cancelled'),
|
||||
};
|
||||
return map[status?.toUpperCase()] ?? status;
|
||||
}
|
||||
|
||||
const SEL_TRANS: Record<string, Record<string, string>> = {
|
||||
'主胜': { 'en-US': 'Home Win', 'ms-MY': 'Rumah Menang' },
|
||||
'客胜': { 'en-US': 'Away Win', 'ms-MY': 'Tandang Menang' },
|
||||
'和局': { 'en-US': 'Draw', 'ms-MY': 'Seri' },
|
||||
'主': { 'en-US': 'Home', 'ms-MY': 'Rumah' },
|
||||
'客': { 'en-US': 'Away', 'ms-MY': 'Tandang' },
|
||||
'大': { 'en-US': 'Over', 'ms-MY': 'Atas' },
|
||||
'小': { 'en-US': 'Under', 'ms-MY': 'Bawah' },
|
||||
'单': { 'en-US': 'Odd', 'ms-MY': 'Ganjil' },
|
||||
'双': { 'en-US': 'Even', 'ms-MY': 'Genap' },
|
||||
'冠军': { 'en-US': 'Winner', 'ms-MY': 'Juara' },
|
||||
};
|
||||
|
||||
function translateSel(name: string): string {
|
||||
if (locale.value === 'zh-CN') return name;
|
||||
const exact = SEL_TRANS[name];
|
||||
if (exact) return exact[locale.value] ?? exact['en-US'] ?? name;
|
||||
const sp = name.indexOf(' ');
|
||||
if (sp > 0) {
|
||||
const head = name.slice(0, sp);
|
||||
const m = SEL_TRANS[head];
|
||||
if (m) return (m[locale.value] ?? m['en-US'] ?? head) + name.slice(sp);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function legResultIcon(status?: string | null) {
|
||||
switch (status?.toUpperCase()) {
|
||||
case 'WON': return '✓';
|
||||
case 'LOST': return '✗';
|
||||
case 'PUSH': return '=';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
function legResultClass(status?: string | null) {
|
||||
switch (status?.toUpperCase()) {
|
||||
case 'WON': return 'leg-won';
|
||||
case 'LOST': return 'leg-lost';
|
||||
case 'PUSH': return 'leg-push';
|
||||
default: return 'leg-pending';
|
||||
}
|
||||
}
|
||||
|
||||
const displayLegs = computed(() => {
|
||||
if (!detail.value) return [];
|
||||
if (detail.value.legs?.length) return detail.value.legs;
|
||||
if (detail.value.isParlay) return [];
|
||||
return [{
|
||||
matchTitle: detail.value.matchTitle,
|
||||
marketLabel: '',
|
||||
selectionName: detail.value.pickLabel,
|
||||
odds: detail.value.totalOdds,
|
||||
resultStatus: detail.value.status,
|
||||
score: detail.value.matchScore ?? null,
|
||||
}];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal-fade">
|
||||
<div v-if="visible" class="modal-overlay" @click.self="close">
|
||||
<div class="modal-container" role="dialog" aria-modal="true" :aria-label="t('bet.detail_title')">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">{{ t('bet.detail_title') }}</span>
|
||||
<button type="button" class="modal-close" :aria-label="t('support.close')" @click="close">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div v-if="loading" class="modal-loading">
|
||||
<GoldSpinner :size="32" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="modal-error">
|
||||
<p>{{ t('common.load_failed') }}</p>
|
||||
<button v-if="betNo" type="button" class="retry-btn" @click="loadDetail(betNo)">
|
||||
{{ t('common.retry') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-else-if="detail">
|
||||
<div class="summary-strip">
|
||||
<div class="summary-main">
|
||||
<span class="bet-no mono">{{ detail.betNo }}</span>
|
||||
<span class="st-badge" :class="statusClass(detail.status)">{{ statusLabel(detail.status) }}</span>
|
||||
</div>
|
||||
<div class="summary-metrics">
|
||||
<div class="metric">
|
||||
<span class="metric-label">{{ t('bet.stake') }}</span>
|
||||
<span class="metric-val">{{ formatMoney(detail.stake, locale) }}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">{{ t('bet.potential_payout') }}</span>
|
||||
<span class="metric-val accent">{{ formatMoney(detail.potentialReturn, locale) }}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">{{ t('bet.placed_at') }}</span>
|
||||
<span class="metric-val muted">
|
||||
{{ detail.placedAt ? new Date(detail.placedAt).toLocaleString() : '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sel-section">
|
||||
<div class="sel-section-title">{{ t('bet.selections') }}</div>
|
||||
<div v-for="(leg, idx) in displayLegs" :key="idx" class="sel-item">
|
||||
<div class="sel-item-left">
|
||||
<span v-if="displayLegs.length > 1" class="sel-index">{{ idx + 1 }}</span>
|
||||
<div class="sel-item-main">
|
||||
<span class="sel-item-match">{{ leg.matchTitle || t('bet.match') }}</span>
|
||||
<span class="sel-item-pick">
|
||||
<template v-if="leg.marketLabel">{{ leg.marketLabel }} · </template>
|
||||
{{ translateSel(leg.selectionName || '') }}
|
||||
</span>
|
||||
<span v-if="leg.score?.ft" class="sel-item-score">{{ t('history.ft') }}: {{ leg.score.ft }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sel-item-right">
|
||||
<span class="sel-item-odds">@{{ leg.odds ?? '-' }}</span>
|
||||
<span
|
||||
v-if="leg.resultStatus"
|
||||
class="leg-result"
|
||||
:class="legResultClass(leg.resultStatus)"
|
||||
>
|
||||
{{ legResultIcon(leg.resultStatus) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="modal-empty">{{ t('common.not_found') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
max-height: 82vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-loading,
|
||||
.modal-error,
|
||||
.modal-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 48px 20px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
padding: 7px 18px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-active);
|
||||
background: transparent;
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(0, 102, 204, 0.04);
|
||||
}
|
||||
|
||||
.summary-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.bet-no {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.summary-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.metric-val {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.metric-val.accent {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.metric-val.muted {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.st-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.st-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
||||
.st-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
||||
.st-pending { background: rgba(0, 102, 204, 0.12); color: var(--primary-light); }
|
||||
.st-push { background: rgba(100, 100, 100, 0.12); color: #aaa; }
|
||||
.st-cancelled { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||
.st-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||
|
||||
.sel-section {
|
||||
padding: 14px 18px 18px;
|
||||
}
|
||||
|
||||
.sel-section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sel-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.sel-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sel-item-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sel-index {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-hover);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sel-item-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sel-item-match {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.sel-item-pick {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sel-item-score {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sel-item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sel-item-odds {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.leg-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.leg-won { background: rgba(52, 199, 89, 0.15); color: #4cd964; }
|
||||
.leg-lost { background: rgba(255, 69, 58, 0.15); color: #ff453a; }
|
||||
.leg-push { background: rgba(100, 100, 100, 0.15); color: #aaa; }
|
||||
.leg-pending { background: rgba(0, 102, 204, 0.1); color: var(--primary-light); font-size: 10px; }
|
||||
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.summary-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
180
apps/player/src/components/desktop/DesktopBetSlipRail.vue
Normal file
180
apps/player/src/components/desktop/DesktopBetSlipRail.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import BetSlipPanel from './BetSlipPanel.vue';
|
||||
import { useBetSlipStore } from '../../stores/betSlip';
|
||||
|
||||
const { t } = useI18n();
|
||||
const slip = useBetSlipStore();
|
||||
|
||||
watch(
|
||||
() => slip.totalCount,
|
||||
(next, prev) => {
|
||||
if (next > prev) slip.expandPanel();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside
|
||||
class="desktop-betslip-rail"
|
||||
:class="{ 'is-collapsed': !slip.panelExpanded }"
|
||||
:aria-expanded="slip.panelExpanded"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="rail-toggle"
|
||||
:class="{ 'is-collapsed': !slip.panelExpanded }"
|
||||
:aria-label="slip.panelExpanded ? t('common.collapse') : t('common.expand')"
|
||||
@click="slip.togglePanelExpanded()"
|
||||
>
|
||||
<svg class="rail-toggle-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M10 8l4 4-4 4"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="!slip.panelExpanded"
|
||||
type="button"
|
||||
class="rail-collapsed-hint"
|
||||
:aria-label="t('bet.bet_slip')"
|
||||
@click="slip.expandPanel()"
|
||||
>
|
||||
<span v-if="slip.totalCount" class="rail-badge">{{ slip.totalCount > 99 ? '99+' : slip.totalCount }}</span>
|
||||
<span class="rail-label">{{ t('bet.bet_slip') }}</span>
|
||||
</button>
|
||||
|
||||
<div class="rail-content" :hidden="!slip.panelExpanded">
|
||||
<BetSlipPanel />
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-betslip-rail {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: var(--desktop-right-betslip-w);
|
||||
min-width: 0;
|
||||
border-left: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
transition: width 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.rail-toggle {
|
||||
position: absolute;
|
||||
left: -18px;
|
||||
top: 50%;
|
||||
z-index: 30;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
color: var(--primary-light);
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
transform: translateY(-50%);
|
||||
box-shadow:
|
||||
0 2px 10px rgba(0, 61, 107, 0.12),
|
||||
0 0 0 3px rgba(255, 255, 255, 0.92);
|
||||
transition:
|
||||
transform 0.22s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background 0.2s,
|
||||
color 0.2s,
|
||||
border-color 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.rail-toggle-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.rail-toggle.is-collapsed .rail-toggle-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.rail-toggle:hover {
|
||||
border-color: var(--border-active);
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
box-shadow:
|
||||
0 4px 14px rgba(0, 61, 107, 0.2),
|
||||
0 0 0 3px rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
.rail-toggle:active {
|
||||
transform: translateY(-50%) scale(0.94);
|
||||
}
|
||||
|
||||
.rail-toggle:focus-visible {
|
||||
outline: 2px solid var(--border-active);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.rail-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: var(--desktop-right-betslip-expanded-w, 288px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktop-betslip-rail.is-collapsed .rail-content {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.rail-collapsed-hint {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 16px 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.rail-collapsed-hint:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.rail-label {
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.rail-badge {
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 5px;
|
||||
border-radius: 10px;
|
||||
background: var(--odds-accent, #F8971F);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatMoney, formatMoneyCompact } from '../../utils/localeDisplay';
|
||||
import type { CashbackRecord } from '../../utils/cashback';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
record: CashbackRecord | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false);
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && props.visible) close();
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', onKeydown));
|
||||
onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
|
||||
function formatPeriod(start: string, end: string) {
|
||||
const opts: Intl.DateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit' };
|
||||
const s = new Date(start).toLocaleDateString(locale.value, opts);
|
||||
const e = new Date(end).toLocaleDateString(locale.value, opts);
|
||||
return `${s} – ${e}`;
|
||||
}
|
||||
|
||||
function formatRate(rate: string) {
|
||||
const n = parseFloat(rate);
|
||||
if (!Number.isFinite(n)) return rate;
|
||||
return `${(n * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
function formatTime(value: string | null) {
|
||||
if (!value) return '—';
|
||||
return new Date(value).toLocaleString(locale.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal-fade">
|
||||
<div v-if="visible" class="modal-overlay" @click.self="close">
|
||||
<div class="modal-container" role="dialog" aria-modal="true" :aria-label="t('cashback.title')">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">{{ t('cashback.title') }}</span>
|
||||
<button type="button" class="modal-close" :aria-label="t('support.close')" @click="close">✕</button>
|
||||
</div>
|
||||
|
||||
<div v-if="record" class="modal-body">
|
||||
<div class="amount-hero pos">{{ formatMoney(record.amount, locale) }}</div>
|
||||
<div class="type-label">{{ formatPeriod(record.periodStart, record.periodEnd) }}</div>
|
||||
|
||||
<div class="info-rows">
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('cashback.effective_stake') }}</span>
|
||||
<span class="row-val">{{ formatMoneyCompact(record.effectiveStake, locale) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('cashback.rate') }}</span>
|
||||
<span class="row-val">{{ formatRate(record.rate) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('nav.bet_history') }}</span>
|
||||
<span class="row-val">{{ t('cashback.bet_count', { n: record.betCount }) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('cashback.batch_no') }}</span>
|
||||
<span class="row-val mono muted">{{ record.batchNo }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('wallet.time') }}</span>
|
||||
<span class="row-val muted">{{ formatTime(record.confirmedAt ?? record.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="modal-empty">{{ t('common.not_found') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 82vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 20px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.amount-hero {
|
||||
padding: 20px 18px 8px;
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: var(--font-mono);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.amount-hero.pos {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.type-label {
|
||||
padding: 0 18px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.info-rows {
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 11px 18px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
width: 96px;
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row-val {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.row-val.muted {
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -12,7 +12,7 @@ import { buildBetPlaceConfirmMessage } from '../../utils/betPlaceConfirmMessage'
|
||||
import { useAppToast } from '../../composables/useAppToast';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { visible, anchorX, anchorY, pendingItem, close } = useDesktopBetPopover();
|
||||
const { visible, anchorX, anchorY, pendingItem, placement, close, cancelClose, scheduleClose } = useDesktopBetPopover();
|
||||
const slip = useBetSlipStore();
|
||||
const auth = useAuthStore();
|
||||
const { refreshProfile } = usePlayerProfile();
|
||||
@@ -25,6 +25,16 @@ const error = ref('');
|
||||
const showPlaceConfirm = ref(false);
|
||||
const placeConfirmMessage = ref('');
|
||||
const MIN_STAKE = 5;
|
||||
const confirmingItem = ref<typeof pendingItem.value>(null);
|
||||
|
||||
watch(showPlaceConfirm, (val) => {
|
||||
if (!val) confirmingItem.value = null;
|
||||
});
|
||||
|
||||
function onPopoverMouseLeave() {
|
||||
if (showPlaceConfirm.value) return;
|
||||
scheduleClose(200);
|
||||
}
|
||||
|
||||
let outsideClickTimer = 0;
|
||||
|
||||
@@ -95,6 +105,7 @@ function validatePlaceNow(): boolean {
|
||||
function onPlaceNowClick() {
|
||||
if (!validatePlaceNow()) return;
|
||||
const item = pendingItem.value!;
|
||||
confirmingItem.value = item;
|
||||
placeConfirmMessage.value = buildBetPlaceConfirmMessage(t, {
|
||||
mode: 'single',
|
||||
items: [item],
|
||||
@@ -107,7 +118,7 @@ function onPlaceNowClick() {
|
||||
showPlaceConfirm.value = true;
|
||||
}
|
||||
|
||||
async function executePlaceNow(item = pendingItem.value) {
|
||||
async function executePlaceNow(item = confirmingItem.value || pendingItem.value) {
|
||||
if (!item) return;
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
@@ -134,7 +145,7 @@ async function executePlaceNow(item = pendingItem.value) {
|
||||
}
|
||||
|
||||
async function confirmPlaceNow() {
|
||||
const item = pendingItem.value;
|
||||
const item = confirmingItem.value;
|
||||
if (!item) return;
|
||||
showPlaceConfirm.value = false;
|
||||
await executePlaceNow(item);
|
||||
@@ -184,11 +195,13 @@ function addToParlayList() {
|
||||
v-if="visible && pendingItem"
|
||||
ref="popRef"
|
||||
class="bet-popover"
|
||||
:class="`placement-${placement}`"
|
||||
tabindex="-1"
|
||||
:style="{ left: `${anchorX}px`, top: `${anchorY}px` }"
|
||||
@click.stop
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="onPopoverMouseLeave"
|
||||
>
|
||||
<button type="button" class="pop-close" aria-label="Close" @click="close">✕</button>
|
||||
|
||||
<div class="pop-match">{{ pendingItem.matchName }}</div>
|
||||
<div class="pop-market">{{ pendingItem.marketName }}</div>
|
||||
@@ -239,98 +252,138 @@ function addToParlayList() {
|
||||
.bet-popover {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
width: 280px;
|
||||
padding: 12px 28px 12px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(18, 18, 18, 0.98);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
|
||||
width: 172px;
|
||||
padding: 6px 7px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-active);
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 6px 18px rgba(0, 61, 107, 0.14);
|
||||
}
|
||||
|
||||
.pop-close {
|
||||
.bet-popover::after,
|
||||
.bet-popover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.pop-close:hover {
|
||||
color: #fff;
|
||||
.bet-popover.placement-bottom::after {
|
||||
top: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 0 6px 6px 6px;
|
||||
border-color: transparent transparent var(--bg-card) transparent;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.bet-popover.placement-bottom::before {
|
||||
top: -7px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 0 7px 7px 7px;
|
||||
border-color: transparent transparent var(--border-active) transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bet-popover.placement-top::after {
|
||||
bottom: -6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 6px 6px 0 6px;
|
||||
border-color: var(--bg-card) transparent transparent transparent;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.bet-popover.placement-top::before {
|
||||
bottom: -7px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 7px 7px 0 7px;
|
||||
border-color: var(--border-active) transparent transparent transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pop-match {
|
||||
font-size: 11px;
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.35;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pop-market {
|
||||
font-size: 10px;
|
||||
font-size: 8px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.pop-pick-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
background: rgba(212, 175, 55, 0.06);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
gap: 6px;
|
||||
padding: 4px 6px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 3px;
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
border: 1px solid var(--border-active);
|
||||
}
|
||||
|
||||
.pick {
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.odds {
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stake-label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-size: 8px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.stake-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--border);
|
||||
background: #0d0d0d;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.stake-input:focus {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.est-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 10px;
|
||||
font-size: 8px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.est-val {
|
||||
@@ -339,20 +392,20 @@ function addToParlayList() {
|
||||
}
|
||||
|
||||
.pop-error {
|
||||
font-size: 10px;
|
||||
font-size: 8px;
|
||||
color: var(--danger);
|
||||
margin: 0 0 8px;
|
||||
margin: 0 0 5px;
|
||||
}
|
||||
|
||||
.pop-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pop-actions-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pop-actions-row .btn-outline {
|
||||
@@ -363,12 +416,12 @@ function addToParlayList() {
|
||||
|
||||
.btn-primary-gold {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
padding: 5px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(180deg, #f0d875 0%, #d4af37 100%);
|
||||
color: #3d2800;
|
||||
font-size: 12px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(180deg, var(--primary-light) 0%, var(--primary) 100%);
|
||||
color: #FFFFFF;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -384,17 +437,17 @@ function addToParlayList() {
|
||||
|
||||
.btn-outline {
|
||||
width: 100%;
|
||||
padding: 7px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.05);
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.05);
|
||||
color: var(--primary-light);
|
||||
font-size: 11px;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(212, 175, 55, 0.12);
|
||||
background: rgba(0, 102, 204, 0.12);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { OutrightEvent } from '../outright/OutrightEventSection.vue';
|
||||
import type { OutrightEvent, OutrightSelection } from '../outright/OutrightEventSection.vue';
|
||||
import saishiImg from '../../assets/images/saishi.webp';
|
||||
|
||||
const props = defineProps<{
|
||||
event: OutrightEvent;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ open: [] }>();
|
||||
const emit = defineEmits<{
|
||||
open: [];
|
||||
pick: [selection: OutrightSelection];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -22,38 +25,129 @@ const teamCount = computed(() => props.event.selectionCount ?? props.event.selec
|
||||
const isSettled = computed(
|
||||
() => props.event.bettingOpen === false || props.event.status === 'SETTLED',
|
||||
);
|
||||
|
||||
const MIN_BTN_W = 52;
|
||||
const GAP = 4;
|
||||
|
||||
const cardRef = ref<HTMLElement | null>(null);
|
||||
const cardWidth = ref(400);
|
||||
let ro: ResizeObserver | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (!cardRef.value) return;
|
||||
ro = new ResizeObserver(([entry]) => {
|
||||
cardWidth.value = entry.contentRect.width;
|
||||
});
|
||||
ro.observe(cardRef.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => ro?.disconnect());
|
||||
|
||||
const maxVisible = computed(() => {
|
||||
const panelW = cardWidth.value - 24;
|
||||
return Math.max(1, Math.min(5, Math.floor((panelW + GAP) / (MIN_BTN_W + GAP))));
|
||||
});
|
||||
|
||||
const topSelections = computed(() => {
|
||||
if (!props.event.selections) return [];
|
||||
return [...props.event.selections]
|
||||
.sort((a, b) => {
|
||||
const oa = parseFloat(a.odds) || 9999;
|
||||
const ob = parseFloat(b.odds) || 9999;
|
||||
return oa - ob;
|
||||
})
|
||||
.slice(0, maxVisible.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button type="button" class="outright-event-card hover-bg" @click="emit('open')">
|
||||
<div class="card-main">
|
||||
<div class="title-row">
|
||||
<span class="title">{{ headTitle }}</span>
|
||||
<span v-if="isSettled" class="settled-tag">{{ t('bet.outright_settled') }}</span>
|
||||
<article ref="cardRef" class="outright-event-card" :class="{ settled: isSettled }">
|
||||
<div class="card-clickable-area" @click="emit('open')">
|
||||
<div class="card-main">
|
||||
<div class="title-row">
|
||||
<span class="title">{{ headTitle }}</span>
|
||||
<span v-if="isSettled" class="settled-tag">{{ t('bet.outright_settled') }}</span>
|
||||
</div>
|
||||
<p v-if="event.leagueName && event.leagueName !== headTitle" class="league">{{ event.leagueName }}</p>
|
||||
<p class="meta">{{ t('bet.outright_teams_count', { n: teamCount }) }}</p>
|
||||
</div>
|
||||
<p v-if="event.leagueName && event.leagueName !== headTitle" class="league">{{ event.leagueName }}</p>
|
||||
<p class="meta">{{ t('bet.outright_teams_count', { n: teamCount }) }}</p>
|
||||
<img :src="saishiImg" alt="" class="saishi" />
|
||||
</div>
|
||||
<img :src="saishiImg" alt="" class="saishi" />
|
||||
</button>
|
||||
|
||||
<div v-if="!isSettled && topSelections.length" class="quick-bet-panel">
|
||||
<div class="quick-bet-title">
|
||||
{{ t('bet.quick_bet') }}
|
||||
<span class="quick-bet-hint">({{ t('bet.popular_teams') }})</span>
|
||||
</div>
|
||||
<div class="odds-row">
|
||||
<button
|
||||
v-for="sel in topSelections"
|
||||
:key="sel.id"
|
||||
type="button"
|
||||
class="odds-btn"
|
||||
@click.stop="emit('pick', sel)"
|
||||
>
|
||||
<span class="selection-name" :title="sel.teamName">{{ sel.teamName }}</span>
|
||||
<span class="selection-odds">{{ sel.odds }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="view-all-btn" @click.stop="emit('open')">
|
||||
{{ t('bet.outright_view_all') }} →
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.outright-event-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 140px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-card);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.outright-event-card:hover {
|
||||
border-color: var(--border-active);
|
||||
box-shadow: 0 4px 12px rgba(0, 61, 107, 0.1);
|
||||
}
|
||||
|
||||
.outright-event-card:hover .card-clickable-area {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
|
||||
.outright-event-card:hover .league,
|
||||
.outright-event-card:hover .meta {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.outright-event-card:hover .saishi {
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.outright-event-card:hover .quick-bet-panel {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.card-clickable-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
text-align: left;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.outright-event-card:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
transition: transform 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.card-main {
|
||||
@@ -76,29 +170,35 @@ const isSettled = computed(
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
line-height: 1.35;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.outright-event-card:hover .title {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.settled-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: #c9a227;
|
||||
color: var(--odds-accent);
|
||||
border: 1px solid rgba(201, 162, 39, 0.45);
|
||||
border-radius: 999px;
|
||||
padding: 1px 7px;
|
||||
}
|
||||
|
||||
.league {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.league,
|
||||
.meta {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.league {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.saishi {
|
||||
@@ -108,5 +208,106 @@ const isSettled = computed(
|
||||
max-width: 40px;
|
||||
object-fit: contain;
|
||||
opacity: 0.85;
|
||||
transition: transform 0.25s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.quick-bet-panel {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(12px);
|
||||
transition: opacity 0.2s ease, transform 0.22s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.quick-bet-title {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.quick-bet-hint {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.odds-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.odds-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
gap: 1px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
min-height: 32px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.odds-btn:hover {
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
border-color: var(--border-active);
|
||||
}
|
||||
|
||||
.selection-name {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.odds-btn:hover .selection-name {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.selection-odds {
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
color: var(--primary);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.view-all-btn {
|
||||
align-self: flex-end;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 2px 0;
|
||||
opacity: 0.85;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.view-all-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { formatMoney } from '../../utils/localeDisplay';
|
||||
import type { DepositAuditLog, PlayerDepositOrder } from '../../utils/depositOrderLookup';
|
||||
import {
|
||||
auditActionTone,
|
||||
auditActorSecondary,
|
||||
formatDepositAuditRemark,
|
||||
shouldShowAuditRejectInTimeline,
|
||||
} from '../../utils/depositAuditDisplay';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
order: PlayerDepositOrder | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
type RechargeDetail = PlayerDepositOrder;
|
||||
|
||||
const detail = computed(() => props.order);
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false);
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && props.visible) close();
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', onKeydown));
|
||||
onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
|
||||
function statusClass(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
APPROVED: 'st-won',
|
||||
REJECTED: 'st-lost',
|
||||
PENDING: 'st-pending',
|
||||
};
|
||||
return map[status?.toUpperCase()] ?? 'st-default';
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
const s = status?.toUpperCase();
|
||||
if (s === 'APPROVED') return t('recharge.status_approved');
|
||||
if (s === 'REJECTED') return t('recharge.status_rejected');
|
||||
return t('recharge.status_pending');
|
||||
}
|
||||
|
||||
function methodLabel(order: RechargeDetail) {
|
||||
if (order.methodType === 'USDT') return 'USDT';
|
||||
return order.paymentMethodName || order.methodType || '-';
|
||||
}
|
||||
|
||||
function reapply(order: RechargeDetail) {
|
||||
close();
|
||||
const query: Record<string, string> = {
|
||||
orderId: order.id,
|
||||
methodType: order.methodType,
|
||||
amount: order.amount,
|
||||
};
|
||||
if (order.paymentMethodId) query.methodId = order.paymentMethodId;
|
||||
void router.push({ path: '/wallet/recharge', query });
|
||||
}
|
||||
|
||||
function normalizeText(value: string | null | undefined) {
|
||||
return value?.trim() ?? '';
|
||||
}
|
||||
|
||||
function orderNote(order: RechargeDetail): { label: string; text: string } | null {
|
||||
const rejectReason = normalizeText(order.rejectReason);
|
||||
const remark = normalizeText(order.remark);
|
||||
if (order.status === 'REJECTED') {
|
||||
const text = rejectReason || remark;
|
||||
if (!text) return null;
|
||||
return { label: t('recharge.reject_reason'), text };
|
||||
}
|
||||
if (remark) return { label: t('recharge.remark'), text: remark };
|
||||
return null;
|
||||
}
|
||||
|
||||
function auditActionLabel(action: string) {
|
||||
const key = `recharge.audit_${action.toLowerCase()}`;
|
||||
const translated = t(key);
|
||||
return translated !== key ? translated : action;
|
||||
}
|
||||
|
||||
function auditStepClass(action: string) {
|
||||
return `audit-step--${auditActionTone(action)}`;
|
||||
}
|
||||
|
||||
function formatAuditTime(iso: string) {
|
||||
return new Date(iso).toLocaleString(locale.value, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function auditRemarkForTimeline(log: DepositAuditLog, order: RechargeDetail) {
|
||||
if (log.action === 'REJECTED' && !shouldShowAuditRejectInTimeline(log, order.rejectReason)) {
|
||||
return null;
|
||||
}
|
||||
return formatDepositAuditRemark(log, t);
|
||||
}
|
||||
|
||||
function auditLogsForDisplay(order: RechargeDetail) {
|
||||
return [...(order.auditLogs ?? [])]
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
.map((log) => ({
|
||||
log,
|
||||
actor: auditActorSecondary(log, t),
|
||||
remark: auditRemarkForTimeline(log, order),
|
||||
}));
|
||||
}
|
||||
|
||||
function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
if (remark.text === t('wallet.remark_deposit_revoke_generic')) return remark.text;
|
||||
return `${t('recharge.audit_remark_label')}: ${remark.text}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal-fade">
|
||||
<div v-if="visible" class="modal-overlay" @click.self="close">
|
||||
<div class="modal-container" role="dialog" aria-modal="true" :aria-label="t('recharge.order_detail')">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">{{ t('recharge.order_detail') }}</span>
|
||||
<button type="button" class="modal-close" :aria-label="t('support.close')" @click="close">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<template v-if="detail">
|
||||
<div class="summary-strip">
|
||||
<div class="summary-main">
|
||||
<span class="order-no mono">{{ detail.orderNo }}</span>
|
||||
<span class="st-badge" :class="statusClass(detail.status)">{{ statusLabel(detail.status) }}</span>
|
||||
</div>
|
||||
<div class="amount-hero">{{ formatMoney(detail.amount, locale) }}</div>
|
||||
<div class="method-label">{{ methodLabel(detail) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="info-rows">
|
||||
<div
|
||||
v-if="detail.approvedAmount && detail.approvedAmount !== detail.amount"
|
||||
class="info-row"
|
||||
>
|
||||
<span class="row-label">{{ t('recharge.credited') }}</span>
|
||||
<span class="row-val accent">{{ formatMoney(detail.approvedAmount, locale) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('recharge.apply_time') }}</span>
|
||||
<span class="row-val muted">{{ new Date(detail.createdAt).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="detail.reviewedAt" class="info-row">
|
||||
<span class="row-label">{{ t('recharge.review_time') }}</span>
|
||||
<span class="row-val muted">{{ new Date(detail.reviewedAt).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="orderNote(detail)" class="info-row info-row--note">
|
||||
<span class="row-label">{{ orderNote(detail)!.label }}</span>
|
||||
<span class="row-val" :class="{ reject: detail.status === 'REJECTED' }">
|
||||
{{ orderNote(detail)!.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.auditLogs?.length" class="audit-section">
|
||||
<div class="section-title">{{ t('recharge.audit_title') }}</div>
|
||||
<div
|
||||
v-for="(entry, logIdx) in auditLogsForDisplay(detail)"
|
||||
:key="entry.log.id"
|
||||
class="audit-step"
|
||||
:class="auditStepClass(entry.log.action)"
|
||||
>
|
||||
<div class="audit-step-rail" aria-hidden="true">
|
||||
<span class="audit-dot" />
|
||||
<span
|
||||
v-if="logIdx < auditLogsForDisplay(detail).length - 1"
|
||||
class="audit-line"
|
||||
/>
|
||||
</div>
|
||||
<div class="audit-step-body">
|
||||
<div class="audit-step-head">
|
||||
<span class="audit-step-title">{{ auditActionLabel(entry.log.action) }}</span>
|
||||
<time class="audit-step-time">{{ formatAuditTime(entry.log.createdAt) }}</time>
|
||||
</div>
|
||||
<p v-if="entry.actor" class="audit-step-actor">{{ entry.actor }}</p>
|
||||
<p
|
||||
v-if="entry.log.approvedAmount && entry.log.action === 'APPROVED'"
|
||||
class="audit-step-credited"
|
||||
>
|
||||
{{ t('recharge.audit_credited') }} {{ formatMoney(entry.log.approvedAmount, locale) }}
|
||||
</p>
|
||||
<div v-if="entry.remark?.kind === 'reject'" class="audit-step-box audit-step-box--reject">
|
||||
<span class="audit-step-box-label">{{ t('recharge.reject_reason') }}</span>
|
||||
<span class="audit-step-box-text">{{ entry.remark.text }}</span>
|
||||
</div>
|
||||
<p v-else-if="entry.remark?.kind === 'note'" class="audit-step-note">
|
||||
{{ auditNoteDisplayText(entry.remark) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.status === 'REJECTED'" class="modal-actions">
|
||||
<button type="button" class="reapply-btn" @click="reapply(detail)">
|
||||
{{ t('recharge.reapply') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="modal-empty">{{ t('common.not_found') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
max-height: 82vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-loading,
|
||||
.modal-error,
|
||||
.modal-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 48px 20px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
padding: 7px 18px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-active);
|
||||
background: transparent;
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(0, 102, 204, 0.04);
|
||||
}
|
||||
|
||||
.summary-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.amount-hero {
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text);
|
||||
line-height: 1.1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.method-label {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.st-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.st-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
||||
.st-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
||||
.st-pending { background: rgba(0, 102, 204, 0.12); color: var(--primary-light); }
|
||||
.st-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||
|
||||
.info-rows {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 11px 18px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.info-row--note {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
width: 96px;
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-row--note .row-label {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.row-val {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.row-val.accent {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.row-val.muted {
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row-val.reject {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.audit-section {
|
||||
padding: 14px 18px 18px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.audit-step {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.audit-step-rail {
|
||||
flex: 0 0 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.audit-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #555;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.audit-line {
|
||||
flex: 1;
|
||||
width: 1px;
|
||||
min-height: 12px;
|
||||
margin: 4px 0;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.audit-step-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.audit-step:last-child .audit-step-body {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.audit-step-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.audit-step-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.audit-step-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.audit-step-actor {
|
||||
margin: 3px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.audit-step-credited {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #4cd964;
|
||||
}
|
||||
|
||||
.audit-step-note {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.audit-step-box {
|
||||
margin-top: 4px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.audit-step-box--reject {
|
||||
border: 1px solid rgba(255, 69, 58, 0.22);
|
||||
}
|
||||
|
||||
.audit-step-box-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #c07070;
|
||||
}
|
||||
|
||||
.audit-step-box-text {
|
||||
font-size: 12px;
|
||||
color: #b08888;
|
||||
}
|
||||
|
||||
.audit-step--submitted .audit-dot { background: var(--primary); }
|
||||
.audit-step--approved .audit-dot { background: #4cd964; }
|
||||
.audit-step--rejected .audit-dot { background: #ff453a; }
|
||||
.audit-step--revoked .audit-dot { background: #888; }
|
||||
.audit-step--reopened .audit-dot { background: #c9a227; }
|
||||
|
||||
.modal-actions {
|
||||
padding: 0 18px 18px;
|
||||
}
|
||||
|
||||
.reapply-btn {
|
||||
width: 100%;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reapply-btn:hover {
|
||||
background: rgba(0, 102, 204, 0.14);
|
||||
}
|
||||
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -7,15 +7,19 @@ import DesktopOddsBetPopover from './DesktopOddsBetPopover.vue';
|
||||
import SportsCategoryBar from './SportsCategoryBar.vue';
|
||||
import LeagueSidebar from './LeagueSidebar.vue';
|
||||
import MatchSidebar from './MatchSidebar.vue';
|
||||
import BetSlipPanel from './BetSlipPanel.vue';
|
||||
import OutrightSidebar from './OutrightSidebar.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';
|
||||
|
||||
const route = useRoute();
|
||||
const { locale } = useI18n();
|
||||
const auth = useAuthStore();
|
||||
const { load: loadPlayerHome } = usePlayerHome();
|
||||
const slip = useBetSlipStore();
|
||||
const { load: loadPlayerHome, announcements } = usePlayerHome();
|
||||
|
||||
const layoutMode = computed<'betting' | 'account' | 'hub' | 'marketing'>(() => {
|
||||
const p = route.path;
|
||||
@@ -26,7 +30,7 @@ const layoutMode = computed<'betting' | 'account' | 'hub' | 'marketing'>(() => {
|
||||
p === '/bets' ||
|
||||
p.startsWith('/bets/') ||
|
||||
p.startsWith('/wallet') ||
|
||||
p === '/profile/cashbacks'
|
||||
p.startsWith('/profile')
|
||||
) {
|
||||
return 'account';
|
||||
}
|
||||
@@ -40,10 +44,23 @@ const layoutMode = computed<'betting' | 'account' | 'hub' | 'marketing'>(() => {
|
||||
});
|
||||
|
||||
const isMatchDetail = computed(() => route.path.startsWith('/match/'));
|
||||
const isDetailPage = computed(
|
||||
() => route.path.startsWith('/match/') || route.path.startsWith('/outright/'),
|
||||
);
|
||||
const isBettingDetail = computed(
|
||||
() => route.path.startsWith('/match/') || route.path.startsWith('/outright/'),
|
||||
);
|
||||
|
||||
const showBetSlipRail = computed(() => {
|
||||
const p = route.path;
|
||||
if (p === '/bets' || p.startsWith('/bets/')) return false;
|
||||
if (p.startsWith('/wallet') || p.startsWith('/profile')) return false;
|
||||
if (route.params.id && (p.startsWith('/announcements/') || p.startsWith('/messages/'))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void loadPlayerHome(true);
|
||||
});
|
||||
@@ -61,11 +78,22 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-shell">
|
||||
<div
|
||||
class="desktop-shell"
|
||||
:class="{
|
||||
'betslip-collapsed': showBetSlipRail && !slip.panelExpanded,
|
||||
'betslip-hidden': !showBetSlipRail,
|
||||
}"
|
||||
>
|
||||
<div v-if="announcements && announcements.length" class="desktop-marquee-wrap">
|
||||
<AnnouncementMarquee :items="announcements" embedded />
|
||||
</div>
|
||||
|
||||
<header class="desktop-header-wrap">
|
||||
<DesktopTopNav />
|
||||
</header>
|
||||
|
||||
<div class="desktop-body">
|
||||
<div class="desktop-main-container">
|
||||
<!-- Betting Layout -->
|
||||
<div v-if="layoutMode === 'betting'" class="desktop-layout-betting-wrap">
|
||||
@@ -74,8 +102,9 @@ watch(
|
||||
</div>
|
||||
<div class="desktop-layout-betting">
|
||||
<aside class="desktop-betting-left">
|
||||
<LeagueSidebar v-if="!isMatchDetail" />
|
||||
<MatchSidebar v-else />
|
||||
<LeagueSidebar v-if="!isDetailPage" />
|
||||
<MatchSidebar v-else-if="isMatchDetail" />
|
||||
<OutrightSidebar v-else />
|
||||
</aside>
|
||||
<main class="desktop-betting-center" :class="{ 'is-betting-detail': isBettingDetail }">
|
||||
<RouterView v-slot="{ Component, route: viewRoute }">
|
||||
@@ -85,9 +114,6 @@ watch(
|
||||
<component v-else :is="Component" :key="viewRoute.fullPath" />
|
||||
</RouterView>
|
||||
</main>
|
||||
<aside class="desktop-betting-right">
|
||||
<BetSlipPanel />
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +140,7 @@ watch(
|
||||
</div>
|
||||
|
||||
<!-- Marketing/Single Column Layout -->
|
||||
<div v-else class="desktop-layout-marketing">
|
||||
<div v-else class="desktop-layout-marketing no-scrollbar">
|
||||
<main class="desktop-marketing-content">
|
||||
<RouterView v-slot="{ Component, route: viewRoute }">
|
||||
<KeepAlive v-if="viewRoute.meta.keepAlive" :max="10">
|
||||
@@ -125,6 +151,8 @@ watch(
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopBetSlipRail v-if="showBetSlipRail" />
|
||||
</div>
|
||||
<FloatingMailbox />
|
||||
<DesktopOddsBetPopover />
|
||||
</div>
|
||||
|
||||
@@ -16,8 +16,7 @@ const activeTab = computed(() => {
|
||||
if (p === '/') return 'home';
|
||||
if (p === '/bet' || p.startsWith('/match/') || p.startsWith('/outright/')) return 'sports';
|
||||
if (p === '/bets' || p.startsWith('/bets/')) return 'bet_history';
|
||||
if (p.startsWith('/wallet') || p === '/profile/cashbacks') return 'wallet';
|
||||
if (p.startsWith('/profile')) return 'profile';
|
||||
if (p.startsWith('/wallet') || p.startsWith('/profile')) return 'my_wallet';
|
||||
if (p.startsWith('/messages')) return 'messages';
|
||||
return '';
|
||||
});
|
||||
@@ -57,17 +56,9 @@ const activeTab = computed(() => {
|
||||
<RouterLink
|
||||
to="/wallet"
|
||||
class="nav-link"
|
||||
:class="{ active: activeTab === 'wallet' }"
|
||||
:class="{ active: activeTab === 'my_wallet' }"
|
||||
>
|
||||
{{ t('nav.wallet') }}
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink
|
||||
to="/profile"
|
||||
class="nav-link"
|
||||
:class="{ active: activeTab === 'profile' }"
|
||||
>
|
||||
{{ t('nav.profile') }}
|
||||
{{ t('nav.my_wallet') }}
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
@@ -114,18 +105,18 @@ const activeTab = computed(() => {
|
||||
height: 38px;
|
||||
width: auto;
|
||||
display: block;
|
||||
filter: drop-shadow(0 0 4px rgba(212, 175, 55, 0.2));
|
||||
filter: drop-shadow(0 0 4px rgba(0, 102, 204, 0.2));
|
||||
}
|
||||
|
||||
.site-name {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-family: var(--font-heading);
|
||||
letter-spacing: 0.5px;
|
||||
background: var(--gradient-gold);
|
||||
background: var(--primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 8px rgba(212, 175, 55, 0.15);
|
||||
text-shadow: 0 0 8px rgba(0, 102, 204, 0.15);
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
@@ -144,6 +135,7 @@ const activeTab = computed(() => {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text-muted);
|
||||
padding: 18px 4px;
|
||||
transition: color 0.2s;
|
||||
@@ -169,7 +161,7 @@ const activeTab = computed(() => {
|
||||
height: 3px;
|
||||
background: var(--primary);
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 -1px 8px rgba(212, 175, 55, 0.5);
|
||||
box-shadow: 0 -1px 8px rgba(0, 102, 204, 0.5);
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -203,8 +195,8 @@ const activeTab = computed(() => {
|
||||
.login-btn {
|
||||
padding: 6px 18px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.05);
|
||||
border: 1px solid var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.05);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
@@ -212,15 +204,15 @@ const activeTab = computed(() => {
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background: rgba(212, 175, 55, 0.12);
|
||||
background: rgba(0, 102, 204, 0.12);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
padding: 7px 18px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(180deg, #F0D875 0%, #D4AF37 100%);
|
||||
color: #3D2800;
|
||||
background: linear-gradient(180deg, var(--primary-light) 0%, var(--primary) 100%);
|
||||
color: #FFFFFF;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
378
apps/player/src/components/desktop/DesktopTxDetailModal.vue
Normal file
378
apps/player/src/components/desktop/DesktopTxDetailModal.vue
Normal file
@@ -0,0 +1,378 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import api from '../../api';
|
||||
import GoldSpinner from '../GoldSpinner.vue';
|
||||
import { formatMoney } from '../../utils/localeDisplay';
|
||||
import { txTypeKey, txDisplayType, txSummaryLabel, isCashbackType } from '../../utils/walletTx';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
transactionId: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
type TxDetail = {
|
||||
transactionId: string;
|
||||
transactionType: string;
|
||||
displayType?: string;
|
||||
summary?: string | null;
|
||||
amount: string;
|
||||
balanceBefore?: string;
|
||||
balanceAfter?: string;
|
||||
frozenBefore?: string;
|
||||
frozenAfter?: string;
|
||||
createdAt: string;
|
||||
referenceType?: string | null;
|
||||
referenceId?: string | null;
|
||||
betNo?: string | null;
|
||||
cashbackBatchNo?: string | null;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
const detail = ref<TxDetail | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(false);
|
||||
|
||||
async function loadDetail(id: string) {
|
||||
loading.value = true;
|
||||
error.value = false;
|
||||
detail.value = null;
|
||||
try {
|
||||
const { data } = await api.get(`/player/wallet/transactions/${id}`);
|
||||
detail.value = data.data ?? null;
|
||||
} catch {
|
||||
error.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.visible, props.transactionId] as const,
|
||||
([vis, id]) => {
|
||||
if (vis && id) void loadDetail(id);
|
||||
if (!vis) {
|
||||
detail.value = null;
|
||||
error.value = false;
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function close() {
|
||||
emit('update:visible', false);
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && props.visible) close();
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', onKeydown));
|
||||
onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
|
||||
function txLabel(tx: TxDetail): string {
|
||||
const key = txTypeKey(txDisplayType(tx));
|
||||
if (key) {
|
||||
const translated = t(key);
|
||||
if (translated !== key) return translated;
|
||||
}
|
||||
return tx.transactionType;
|
||||
}
|
||||
|
||||
const showFrozen = (tx: TxDetail) =>
|
||||
parseFloat(tx.frozenBefore || '0') > 0 || parseFloat(tx.frozenAfter || '0') > 0;
|
||||
|
||||
const betNo = computed(() => detail.value?.betNo?.trim() || null);
|
||||
|
||||
const cashbackBatchNo = computed(() => {
|
||||
const tx = detail.value;
|
||||
if (!tx || !isCashbackType(tx.transactionType)) return null;
|
||||
return tx.cashbackBatchNo ?? tx.referenceId ?? null;
|
||||
});
|
||||
|
||||
function goBetDetail() {
|
||||
if (!betNo.value) return;
|
||||
close();
|
||||
void router.push({ path: '/bets', query: { betNo: betNo.value } });
|
||||
}
|
||||
|
||||
function goCashbackDetail() {
|
||||
if (!cashbackBatchNo.value) return;
|
||||
close();
|
||||
void router.push({ path: '/wallet/cashbacks', query: { batchNo: cashbackBatchNo.value } });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal-fade">
|
||||
<div v-if="visible" class="modal-overlay" @click.self="close">
|
||||
<div class="modal-container" role="dialog" aria-modal="true" :aria-label="t('wallet.transaction_detail')">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">{{ t('wallet.transaction_detail') }}</span>
|
||||
<button type="button" class="modal-close" :aria-label="t('support.close')" @click="close">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div v-if="loading" class="modal-loading">
|
||||
<GoldSpinner :size="32" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="error" class="modal-error">
|
||||
<p>{{ t('common.load_failed') }}</p>
|
||||
<button v-if="transactionId" type="button" class="retry-btn" @click="loadDetail(transactionId)">
|
||||
{{ t('common.retry') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-else-if="detail">
|
||||
<div class="amount-hero" :class="parseFloat(detail.amount) >= 0 ? 'pos' : 'neg'">
|
||||
{{ formatMoney(detail.amount, locale) }}
|
||||
</div>
|
||||
<div class="type-label">{{ txLabel(detail) }}</div>
|
||||
|
||||
<div class="info-rows">
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('wallet.note') }}</span>
|
||||
<span class="row-val">{{ txSummaryLabel(detail, t) || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('wallet.balance_before') }}</span>
|
||||
<span class="row-val mono">{{ formatMoney(detail.balanceBefore, locale) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('wallet.balance_after') }}</span>
|
||||
<span class="row-val mono">{{ formatMoney(detail.balanceAfter, locale) }}</span>
|
||||
</div>
|
||||
<div v-if="showFrozen(detail)" class="info-row">
|
||||
<span class="row-label">{{ t('wallet.frozen_before') }}</span>
|
||||
<span class="row-val mono">{{ formatMoney(detail.frozenBefore, locale) }}</span>
|
||||
</div>
|
||||
<div v-if="showFrozen(detail)" class="info-row">
|
||||
<span class="row-label">{{ t('wallet.frozen_after') }}</span>
|
||||
<span class="row-val mono">{{ formatMoney(detail.frozenAfter, locale) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('wallet.time') }}</span>
|
||||
<span class="row-val muted">{{ new Date(detail.createdAt).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="row-label">{{ t('wallet.transaction_id') }}</span>
|
||||
<span class="row-val mono muted">{{ detail.transactionId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="betNo || cashbackBatchNo" class="link-actions">
|
||||
<button v-if="betNo" type="button" class="link-btn" @click="goBetDetail">
|
||||
{{ t('wallet.detail_bet_link') }} · {{ betNo }}
|
||||
</button>
|
||||
<button v-if="cashbackBatchNo" type="button" class="link-btn" @click="goCashbackDetail">
|
||||
{{ t('wallet.detail_cashback_link') }} · {{ cashbackBatchNo }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="modal-empty">{{ t('common.not_found') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 82vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
|
||||
.modal-loading,
|
||||
.modal-error,
|
||||
.modal-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 48px 20px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
padding: 7px 18px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-active);
|
||||
background: transparent;
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.amount-hero {
|
||||
padding: 20px 18px 8px;
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: var(--font-mono);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.amount-hero.pos {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.amount-hero.neg {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.type-label {
|
||||
padding: 0 18px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.info-rows {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 11px 18px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
width: 96px;
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row-val {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.row-val.muted {
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.link-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 18px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
width: 100%;
|
||||
padding: 9px 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.link-btn:hover {
|
||||
background: rgba(0, 102, 204, 0.12);
|
||||
}
|
||||
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import DesktopWalletSubNav from './DesktopWalletSubNav.vue';
|
||||
|
||||
defineProps<{
|
||||
showRecharge?: boolean;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="desktop-records-header wallet-page-header">
|
||||
<div class="wallet-page-header-main">
|
||||
<DesktopWalletSubNav />
|
||||
</div>
|
||||
<div class="desktop-records-actions">
|
||||
<slot name="actions">
|
||||
<RouterLink
|
||||
v-if="showRecharge !== false"
|
||||
to="/wallet/recharge"
|
||||
class="desktop-records-action-btn"
|
||||
>
|
||||
{{ t('wallet.recharge_btn') }}
|
||||
</RouterLink>
|
||||
</slot>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wallet-page-header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wallet-page-header-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -9,12 +9,15 @@ const route = useRoute();
|
||||
const tabs = [
|
||||
{
|
||||
path: '/wallet',
|
||||
labelKey: 'nav.wallet',
|
||||
labelKey: 'wallet.my_balance',
|
||||
match: (p: string) => p === '/wallet',
|
||||
},
|
||||
{
|
||||
path: '/wallet/detail',
|
||||
labelKey: 'wallet.view_all',
|
||||
match: (p: string) =>
|
||||
p === '/wallet' ||
|
||||
p === '/wallet/detail' ||
|
||||
p === '/wallet/recharge' ||
|
||||
p.startsWith('/wallet/transactions'),
|
||||
p.startsWith('/wallet/transactions/'),
|
||||
},
|
||||
{
|
||||
path: '/wallet/recharge/history',
|
||||
@@ -32,7 +35,7 @@ const activePath = computed(() => route.path);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="desktop-wallet-subnav" :aria-label="t('nav.wallet')">
|
||||
<nav class="desktop-wallet-subnav" :aria-label="t('nav.my_wallet')">
|
||||
<RouterLink
|
||||
v-for="tab in tabs"
|
||||
:key="tab.path"
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { computed, onMounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMatchFilters } from '../../composables/useMatchFilters';
|
||||
import { useDesktopParlayMatches } from '../../composables/useDesktopParlayMatches';
|
||||
import { useOutrightEvents } from '../../composables/useOutrightEvents';
|
||||
import {
|
||||
isAfterLocalTodayMatchWindow as isAfterTodayMatchWindow,
|
||||
isInLocalTodayMatchWindow as isInTodayMatchWindow,
|
||||
} from '@thebet365/shared';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const { parlayMatches, loadParlayMatches } = useDesktopParlayMatches();
|
||||
const { searchQuery, filterState, toggleLeague, clearFilters } = useMatchFilters();
|
||||
const { searchQuery, filterState, toggleLeague, clearFilters, isLeagueSelected } = useMatchFilters();
|
||||
const { events: outrightEvents, load: loadOutrightEvents } = useOutrightEvents();
|
||||
|
||||
onMounted(() => {
|
||||
void loadParlayMatches();
|
||||
const isOutrightMode = computed(() => {
|
||||
return route.query.tab === 'outright';
|
||||
});
|
||||
|
||||
watch(
|
||||
isOutrightMode,
|
||||
(outright) => {
|
||||
if (outright) {
|
||||
void loadOutrightEvents({ silent: outrightEvents.value.length > 0 });
|
||||
} else {
|
||||
void loadParlayMatches();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function normalizeLeagueName(name: string): string {
|
||||
return name
|
||||
.replace(/\.unit$/i, '')
|
||||
@@ -26,43 +42,69 @@ function normalizeLeagueName(name: string): string {
|
||||
|
||||
const availableLeagues = computed(() => {
|
||||
const map = new Map<string, { id: string; name: string; count: number }>();
|
||||
const now = new Date();
|
||||
const keyword = searchQuery.value.trim().toLowerCase();
|
||||
|
||||
for (const m of parlayMatches.value) {
|
||||
if (keyword) {
|
||||
const haystack = `${m.homeTeamName} ${m.awayTeamName} ${m.leagueName}`.toLowerCase();
|
||||
if (!haystack.includes(keyword)) continue;
|
||||
if (isOutrightMode.value) {
|
||||
for (const e of outrightEvents.value) {
|
||||
if (keyword) {
|
||||
const haystack = `${e.title} ${e.leagueName || ''}`.toLowerCase();
|
||||
if (!haystack.includes(keyword)) continue;
|
||||
}
|
||||
|
||||
const id = e.leagueId ?? e.leagueName ?? 'unknown';
|
||||
const existing = map.get(id);
|
||||
if (existing) {
|
||||
existing.count += 1;
|
||||
} else {
|
||||
map.set(id, {
|
||||
id,
|
||||
name: normalizeLeagueName(e.leagueName || e.title),
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const now = new Date();
|
||||
for (const m of parlayMatches.value) {
|
||||
if (keyword) {
|
||||
const haystack = `${m.homeTeamName} ${m.awayTeamName} ${m.leagueName}`.toLowerCase();
|
||||
if (!haystack.includes(keyword)) continue;
|
||||
}
|
||||
|
||||
let timeMatch = true;
|
||||
if (filterState.value.time === 'today') {
|
||||
timeMatch = isInTodayMatchWindow(m.startTime, now);
|
||||
} else if (filterState.value.time === 'early') {
|
||||
timeMatch = isAfterTodayMatchWindow(m.startTime, now);
|
||||
}
|
||||
if (!timeMatch) continue;
|
||||
let timeMatch = true;
|
||||
if (filterState.value.time === 'today') {
|
||||
timeMatch = isInTodayMatchWindow(m.startTime, now);
|
||||
} else if (filterState.value.time === 'early') {
|
||||
timeMatch = isAfterTodayMatchWindow(m.startTime, now);
|
||||
}
|
||||
if (!timeMatch) continue;
|
||||
|
||||
if (filterState.value.status === 'open' && m.matchPhase !== 'open' && m.matchPhase !== undefined) continue;
|
||||
if (filterState.value.status === 'settled' && m.matchPhase !== 'settled') continue;
|
||||
if (filterState.value.status === 'open' && m.matchPhase !== 'open' && m.matchPhase !== undefined) continue;
|
||||
if (filterState.value.status === 'settled' && m.matchPhase !== 'settled') continue;
|
||||
|
||||
const id = m.leagueId ?? m.leagueName;
|
||||
const existing = map.get(id);
|
||||
if (existing) {
|
||||
existing.count += 1;
|
||||
} else {
|
||||
map.set(id, {
|
||||
id,
|
||||
name: normalizeLeagueName(m.leagueName),
|
||||
count: 1,
|
||||
});
|
||||
const id = m.leagueId ?? m.leagueName;
|
||||
const existing = map.get(id);
|
||||
if (existing) {
|
||||
existing.count += 1;
|
||||
} else {
|
||||
map.set(id, {
|
||||
id,
|
||||
name: normalizeLeagueName(m.leagueName),
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(leagueId);
|
||||
const isSelected = (leagueId: string) =>
|
||||
isLeagueSelected(leagueId, filterState.value.leagueIds);
|
||||
|
||||
function onToggleLeague(leagueId: string) {
|
||||
toggleLeague(leagueId, availableLeagues.value.map((lg) => lg.id));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -76,7 +118,7 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<div v-if="!isOutrightMode" class="sidebar-section">
|
||||
<div class="section-hdr">{{ t('bet.filter_time') }}</div>
|
||||
<div class="time-filters">
|
||||
<button
|
||||
@@ -110,7 +152,7 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
<div class="section-hdr leagues-hdr">
|
||||
<span>{{ t('bet.filter_league') }}</span>
|
||||
<button
|
||||
v-if="filterState.leagueIds.length > 0"
|
||||
v-if="filterState.leagueIds.length > 0 || filterState.time !== 'all' || searchQuery.trim()"
|
||||
type="button"
|
||||
class="clear-btn"
|
||||
@click="clearFilters"
|
||||
@@ -124,14 +166,14 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
:key="lg.id"
|
||||
class="league-item"
|
||||
:class="{ active: isSelected(lg.id) }"
|
||||
@click="toggleLeague(lg.id)"
|
||||
@click="onToggleLeague(lg.id)"
|
||||
>
|
||||
<div class="checkbox" :class="{ checked: isSelected(lg.id) }"></div>
|
||||
<span class="league-name" :title="lg.name">{{ lg.name }}</span>
|
||||
<span class="league-count">{{ lg.count }}</span>
|
||||
</div>
|
||||
<div v-if="availableLeagues.length === 0" class="empty-leagues">
|
||||
{{ t('bet.no_matches') }}
|
||||
{{ isOutrightMode ? t('bet.no_outright') : t('bet.no_matches') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,7 +195,7 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
|
||||
.sidebar-search-input {
|
||||
width: 100%;
|
||||
background: #0d0d0d !important;
|
||||
background: var(--bg-card) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
color: var(--text);
|
||||
padding: 6px 10px;
|
||||
@@ -162,8 +204,8 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
}
|
||||
|
||||
.sidebar-search-input:focus {
|
||||
border-color: var(--border-gold-soft) !important;
|
||||
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.1) !important;
|
||||
border-color: var(--border-active) !important;
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.1) !important;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
@@ -187,7 +229,7 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
|
||||
.time-btn {
|
||||
flex: 1;
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
padding: 5px 2px;
|
||||
@@ -199,12 +241,12 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
}
|
||||
|
||||
.time-btn:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.time-btn.active {
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
@@ -256,7 +298,7 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
}
|
||||
|
||||
.league-item.active {
|
||||
background: rgba(212, 175, 55, 0.04);
|
||||
background: rgba(0, 102, 204, 0.04);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@@ -266,7 +308,7 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background: #0d0d0d;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.checkbox.checked {
|
||||
@@ -281,16 +323,17 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
left: 4px;
|
||||
width: 3px;
|
||||
height: 6px;
|
||||
border: solid #111;
|
||||
border: solid var(--bg-card);
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.league-name {
|
||||
flex: 1;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #ddd;
|
||||
flex: 1;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -305,7 +348,7 @@ const isSelected = (leagueId: string) => filterState.value.leagueIds.includes(le
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
background: #181818;
|
||||
background: var(--bg-body);
|
||||
padding: 1px 5px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ watch(currentMatchId, scrollToActive);
|
||||
|
||||
.sidebar-search-input {
|
||||
width: 100%;
|
||||
background: #0d0d0d !important;
|
||||
background: var(--bg-card) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
color: var(--text);
|
||||
padding: 6px 10px;
|
||||
@@ -169,8 +169,8 @@ watch(currentMatchId, scrollToActive);
|
||||
}
|
||||
|
||||
.sidebar-search-input:focus {
|
||||
border-color: var(--border-gold-soft) !important;
|
||||
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.1) !important;
|
||||
border-color: var(--border-active) !important;
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.1) !important;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
@@ -200,7 +200,7 @@ watch(currentMatchId, scrollToActive);
|
||||
|
||||
.time-btn {
|
||||
flex: 1;
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
padding: 5px 2px;
|
||||
@@ -212,12 +212,12 @@ watch(currentMatchId, scrollToActive);
|
||||
}
|
||||
|
||||
.time-btn:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.time-btn.active {
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
@@ -274,7 +274,8 @@ watch(currentMatchId, scrollToActive);
|
||||
.matches-list :deep(.team-name) {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
padding: 0 4px;
|
||||
max-width: 80px;
|
||||
@@ -323,20 +324,20 @@ watch(currentMatchId, scrollToActive);
|
||||
.matches-list :deep(.match-card) {
|
||||
padding: 10px 8px;
|
||||
min-height: unset;
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.matches-list :deep(.match-card:hover) {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
background: var(--bg-hover) !important;
|
||||
}
|
||||
|
||||
.matches-list :deep(.match-card.active) {
|
||||
border-color: var(--primary) !important;
|
||||
background: rgba(212, 175, 55, 0.08) !important;
|
||||
box-shadow: 0 0 8px rgba(212, 175, 55, 0.25);
|
||||
background: rgba(0, 102, 204, 0.08) !important;
|
||||
box-shadow: 0 0 8px rgba(0, 102, 204, 0.25);
|
||||
}
|
||||
</style>
|
||||
|
||||
266
apps/player/src/components/desktop/OutrightSidebar.vue
Normal file
266
apps/player/src/components/desktop/OutrightSidebar.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch, nextTick } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMatchFilters } from '../../composables/useMatchFilters';
|
||||
import { useOutrightEvents } from '../../composables/useOutrightEvents';
|
||||
import GoldSpinner from '../GoldSpinner.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { searchQuery } = useMatchFilters();
|
||||
const { events: outrightEvents, loading, load: loadOutrightEvents } = useOutrightEvents();
|
||||
|
||||
const listRef = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
void loadOutrightEvents({ silent: outrightEvents.value.length > 0 }).then(() => {
|
||||
scrollToActive();
|
||||
});
|
||||
});
|
||||
|
||||
const currentOutrightId = computed(() => {
|
||||
const id = route.params.id;
|
||||
return Array.isArray(id) ? id[0] : id;
|
||||
});
|
||||
|
||||
function normalizeLeagueName(name: string): string {
|
||||
return name
|
||||
.replace(/\.unit$/i, '')
|
||||
.replace(/[-_]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
const filteredEvents = computed(() => {
|
||||
const keyword = searchQuery.value.trim().toLowerCase();
|
||||
return outrightEvents.value.filter((e) => {
|
||||
// If it's the active event, always show it
|
||||
const isCurrent = String(e.id) === String(currentOutrightId.value);
|
||||
if (isCurrent) return true;
|
||||
|
||||
if (keyword) {
|
||||
const haystack = `${e.title} ${e.leagueName || ''}`.toLowerCase();
|
||||
if (!haystack.includes(keyword)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
function goOutright(id: string) {
|
||||
router.replace(`/outright/${id}`);
|
||||
}
|
||||
|
||||
function scrollToActive() {
|
||||
nextTick(() => {
|
||||
if (!listRef.value) return;
|
||||
const activeEl = listRef.value.querySelector('.active') as HTMLElement;
|
||||
if (activeEl) {
|
||||
activeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch(currentOutrightId, scrollToActive);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="outright-sidebar">
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="t('nav.search')"
|
||||
class="sidebar-search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section outrights-section">
|
||||
<div class="section-hdr">
|
||||
<span>{{ t('bet.tab_outright') || '优胜冠军' }}</span>
|
||||
</div>
|
||||
<div class="outrights-list" ref="listRef">
|
||||
<div v-if="loading && !filteredEvents.length" class="sidebar-loading">
|
||||
<GoldSpinner :size="24" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="e in filteredEvents"
|
||||
:key="e.id"
|
||||
class="outright-card"
|
||||
:class="{ active: String(e.id) === String(currentOutrightId) }"
|
||||
@click="goOutright(e.id)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<span class="league-tag" :title="e.leagueName">{{ normalizeLeagueName(e.leagueName || e.title) }}</span>
|
||||
</div>
|
||||
<div class="event-title" :title="e.title">{{ e.title }}</div>
|
||||
<div class="card-footer">
|
||||
<span class="selections-badge">
|
||||
{{ e.selections?.length || 0 }} {{ t('bet.selections') || '项' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredEvents.length === 0 && !loading" class="empty-outrights">
|
||||
{{ t('bet.no_outright') }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.outright-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
padding: 0 12px 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sidebar-search-input {
|
||||
width: 100%;
|
||||
background: var(--bg-card) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
color: var(--text);
|
||||
padding: 6px 10px;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sidebar-search-input:focus {
|
||||
border-color: var(--border-active) !important;
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.1) !important;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.section-hdr {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.outrights-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.outrights-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-right: -4px;
|
||||
padding-right: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sidebar-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.empty-outrights {
|
||||
padding: 16px 0;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.outright-card {
|
||||
padding: 12px 10px;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.outright-card:hover {
|
||||
border-color: var(--border-active);
|
||||
background: var(--bg-hover) !important;
|
||||
}
|
||||
|
||||
.outright-card.active {
|
||||
border-color: var(--primary) !important;
|
||||
background: rgba(0, 102, 204, 0.08) !important;
|
||||
box-shadow: 0 0 8px rgba(0, 102, 204, 0.25);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.league-tag {
|
||||
font-size: 9px;
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.outright-card.active .event-title {
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.selections-badge {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
background: var(--bg-card);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.outright-card.active .selections-badge {
|
||||
background: rgba(0, 102, 204, 0.15);
|
||||
color: var(--primary-light);
|
||||
border-color: rgba(0, 102, 204, 0.3);
|
||||
}
|
||||
</style>
|
||||
@@ -120,7 +120,7 @@ function onPageSizeChange(e: Event) {
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -129,15 +129,15 @@ function onPageSizeChange(e: Event) {
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
color: var(--text);
|
||||
background: rgba(212, 175, 55, 0.05);
|
||||
background: rgba(0, 102, 204, 0.05);
|
||||
}
|
||||
|
||||
.page-btn.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #111;
|
||||
color: var(--bg-card);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ function onPageSizeChange(e: Event) {
|
||||
|
||||
.page-size-select {
|
||||
height: 32px;
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
border-radius: 6px;
|
||||
@@ -166,7 +166,7 @@ function onPageSizeChange(e: Event) {
|
||||
}
|
||||
|
||||
.page-size-select:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
color: var(--text);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -64,23 +64,24 @@ function selectSport(id: SportCategory, enabled: boolean) {
|
||||
padding: 7px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
transition: background 0.18s, border-color 0.18s, color 0.18s;
|
||||
}
|
||||
|
||||
.sport-category:hover:not(:disabled) {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--primary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.sport-category.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: inset 0 0 0 1px rgba(212, 175, 55, 0.08);
|
||||
background: rgba(0, 102, 204, 0.1);
|
||||
border-color: var(--border-active);
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.sport-category.disabled {
|
||||
@@ -100,9 +101,9 @@ function selectSport(id: SportCategory, enabled: boolean) {
|
||||
.sport-category-soon {
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -96,18 +96,18 @@ function formatOdds(odds: string) {
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 210;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
background: linear-gradient(180deg, #222 0%, #141414 100%);
|
||||
background: #FFFFFF;
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
border-radius: 16px 16px 0 0;
|
||||
border-top: 1px solid var(--border);
|
||||
border-top: 1px solid #E5E7EB;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -123,9 +123,9 @@ function formatOdds(odds: string) {
|
||||
.drawer-foot {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
background: rgba(14, 14, 14, 0.98);
|
||||
border-top: 1px solid var(--border-gold-soft);
|
||||
box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.45);
|
||||
background: #F5F7FA;
|
||||
border-top: 1px solid #E5E7EB;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
@@ -134,48 +134,50 @@ function formatOdds(odds: string) {
|
||||
align-items: center;
|
||||
padding: 14px 14px 12px;
|
||||
flex-shrink: 0;
|
||||
background: rgba(14, 14, 14, 0.98);
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
background: #FFFFFF;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.drawer-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-size: 20px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.slip-item {
|
||||
padding: 10px 12px;
|
||||
background: #111;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #F5F7FA;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.item-sel {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.odds {
|
||||
color: var(--primary-light);
|
||||
color: #F8971F;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@@ -188,24 +190,24 @@ function formatOdds(odds: string) {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.summary-row strong {
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.return {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.return strong {
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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;
|
||||
@@ -24,6 +25,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { cancelClose } = useDesktopBetPopover();
|
||||
|
||||
const columns = computed(() =>
|
||||
groupCorrectScoreSelections(
|
||||
@@ -48,7 +50,11 @@ function formatOdds(odds: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cs-panel" :class="{ 'cs-panel--locked': locked, 'cs-panel--dense': dense }">
|
||||
<div
|
||||
class="cs-panel"
|
||||
:class="{ 'cs-panel--locked': locked, 'cs-panel--dense': dense }"
|
||||
@mouseenter="cancelClose"
|
||||
>
|
||||
<div class="cols-head">
|
||||
<span>{{ t('bet.col_home') }}</span>
|
||||
<span>{{ t('bet.col_draw') }}</span>
|
||||
@@ -65,6 +71,7 @@ function formatOdds(odds: string) {
|
||||
:class="{ selected: isSelected(sel.id), 'score-card--locked': locked }"
|
||||
:data-bet-selection-id="sel.id"
|
||||
:disabled="locked"
|
||||
@mouseenter="onPick(sel, $event)"
|
||||
@click="onPick(sel, $event)"
|
||||
>
|
||||
<span class="score-line">{{ sel.scoreDisplay }}</span>
|
||||
@@ -121,46 +128,54 @@ function formatOdds(odds: string) {
|
||||
gap: 3px;
|
||||
min-height: 42px;
|
||||
padding: 6px 3px;
|
||||
background: #161616;
|
||||
border: 1px solid #282828;
|
||||
border-radius: 3px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #CBD5E1;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.score-card:hover:not(.score-card--locked) {
|
||||
border-color: var(--primary-light);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.score-card.selected {
|
||||
border-color: var(--primary);
|
||||
background: rgba(212, 175, 55, 0.14);
|
||||
box-shadow: inset 0 0 0 1px rgba(212, 175, 55, 0.35), 0 0 0 1px rgba(212, 175, 55, 0.2);
|
||||
border-width: 2px;
|
||||
background: rgba(0, 61, 107, 0.08);
|
||||
}
|
||||
|
||||
.score-card.selected .score-line {
|
||||
color: #e8d48a;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.score-card.selected .odds {
|
||||
color: #f0d875;
|
||||
color: var(--odds-accent);
|
||||
}
|
||||
|
||||
.score-card--locked {
|
||||
background: #111;
|
||||
border-color: #333;
|
||||
background: #E8EDF2;
|
||||
border-color: #E5E7EB;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.score-card--locked .score-line,
|
||||
.score-card--locked .odds {
|
||||
color: #666;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.score-line {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.odds {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
color: var(--odds-accent);
|
||||
}
|
||||
|
||||
.cs-panel--dense {
|
||||
@@ -184,6 +199,7 @@ function formatOdds(odds: string) {
|
||||
min-height: 34px;
|
||||
padding: 4px 2px;
|
||||
gap: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cs-panel--dense .score-line {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { resolveSelectionLabel } from '../../utils/selectionLabel';
|
||||
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
selections: {
|
||||
@@ -13,9 +15,9 @@ const props = defineProps<{
|
||||
}[];
|
||||
isSelected: (id: string) => boolean;
|
||||
compact?: boolean;
|
||||
/** PC 行内横排:左标签右赔率,适合详情页全宽行 */
|
||||
/** PC 琛屽唴妯帓锛氬乏鏍囩鍙宠禂鐜囷紝閫傚悎璇︽儏椤靛叏瀹借 */
|
||||
horizontal?: boolean;
|
||||
/** PC 卡片内:上标签下赔率,列数随选项数量 */
|
||||
/** PC 鍗$墖鍐咃細涓婃爣绛句笅璧旂巼锛屽垪鏁伴殢閫夐」鏁伴噺 */
|
||||
desktopCard?: boolean;
|
||||
locked?: boolean;
|
||||
lineValue?: string | number | null;
|
||||
@@ -23,6 +25,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{ pick: [id: string, event?: MouseEvent] }>();
|
||||
const { t } = useI18n();
|
||||
const { cancelClose } = useDesktopBetPopover();
|
||||
|
||||
function label(sel: (typeof props.selections)[number]) {
|
||||
if (sel.selectionDisplayName?.trim()) return sel.selectionDisplayName;
|
||||
@@ -53,7 +56,7 @@ const panelStyle = computed(() =>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrap" :class="{ compact, horizontal, desktopCard, locked }">
|
||||
<div class="wrap" :class="{ compact, horizontal, desktopCard, locked }" @mouseenter="cancelClose">
|
||||
<div
|
||||
class="panel"
|
||||
:class="{ horizontal, 'desktop-card': desktopCard }"
|
||||
@@ -67,6 +70,7 @@ const panelStyle = computed(() =>
|
||||
:class="{ selected: isSelected(sel.id), 'odds-btn--locked': locked }"
|
||||
:data-bet-selection-id="sel.id"
|
||||
:disabled="locked"
|
||||
@mouseenter="onPick(sel.id, $event)"
|
||||
@click="onPick(sel.id, $event)"
|
||||
>
|
||||
<span class="label">{{ label(sel) }}</span>
|
||||
@@ -79,7 +83,7 @@ const panelStyle = computed(() =>
|
||||
<style scoped>
|
||||
.wrap {
|
||||
padding: 6px 8px 8px;
|
||||
background: #0c0c0c;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.wrap.compact {
|
||||
@@ -147,14 +151,14 @@ const panelStyle = computed(() =>
|
||||
}
|
||||
|
||||
.panel.desktop-card .odds-btn {
|
||||
min-height: 38px;
|
||||
padding: 4px 6px;
|
||||
gap: 2px;
|
||||
min-height: 34px;
|
||||
padding: 3px 4px;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.panel.desktop-card .odds-btn .label {
|
||||
font-size: 9px;
|
||||
line-height: 1.2;
|
||||
font-size: 8px;
|
||||
line-height: 1.15;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -162,7 +166,7 @@ const panelStyle = computed(() =>
|
||||
}
|
||||
|
||||
.panel.desktop-card .odds-btn .odds {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
@@ -184,46 +188,54 @@ const panelStyle = computed(() =>
|
||||
gap: 2px;
|
||||
min-height: 44px;
|
||||
padding: 6px 4px;
|
||||
border-radius: 4px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 8px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #CBD5E1;
|
||||
text-align: center;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.odds-btn:hover:not(.odds-btn--locked) {
|
||||
border-color: var(--primary-light);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.odds-btn.selected {
|
||||
border-color: var(--primary);
|
||||
background: rgba(212, 175, 55, 0.14);
|
||||
box-shadow: inset 0 0 0 1px rgba(212, 175, 55, 0.35), 0 0 0 1px rgba(212, 175, 55, 0.2);
|
||||
border-width: 2px;
|
||||
background: rgba(0, 61, 107, 0.08);
|
||||
}
|
||||
|
||||
.odds-btn.selected .label {
|
||||
color: #e8d48a;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.odds-btn.selected .odds {
|
||||
color: #f0d875;
|
||||
color: var(--odds-accent);
|
||||
}
|
||||
|
||||
.odds-btn--locked {
|
||||
background: #111;
|
||||
border-color: #333;
|
||||
background: #E8EDF2;
|
||||
border-color: #E5E7EB;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.odds-btn--locked .label,
|
||||
.odds-btn--locked .odds {
|
||||
color: #666;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
color: #4B5563;
|
||||
line-height: 1.15;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.odds {
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--odds-accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -45,11 +45,11 @@ const { t } = useI18n();
|
||||
}
|
||||
|
||||
.row:not(.disabled):active {
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
background: rgba(0, 61, 107, 0.07);
|
||||
}
|
||||
|
||||
.row.expanded {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: rgba(0, 61, 107, 0.07);
|
||||
}
|
||||
|
||||
.row.expanded::before {
|
||||
@@ -58,9 +58,9 @@ const { t } = useI18n();
|
||||
top: 0;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent 0%, var(--primary) 30%, var(--primary-light) 50%, var(--primary) 70%, transparent 100%);
|
||||
opacity: 0.6;
|
||||
height: 2.5px;
|
||||
background: #003D6B;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.row.disabled {
|
||||
@@ -72,7 +72,7 @@ const { t } = useI18n();
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
color: #1A1A2E;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
@@ -80,11 +80,22 @@ const { t } = useI18n();
|
||||
flex-shrink: 0;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: #ffb800;
|
||||
color: #F8971F;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 184, 0, 0.12);
|
||||
border: 1px solid rgba(255, 184, 0, 0.35);
|
||||
background: rgba(248, 151, 31, 0.1);
|
||||
border: 1px solid rgba(248, 151, 31, 0.3);
|
||||
}
|
||||
|
||||
.row-status {
|
||||
flex-shrink: 0;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: #f0b429;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: rgba(240, 180, 41, 0.12);
|
||||
border: 1px solid rgba(240, 180, 41, 0.35);
|
||||
}
|
||||
|
||||
.row-status {
|
||||
@@ -99,12 +110,12 @@ const { t } = useI18n();
|
||||
}
|
||||
|
||||
.row.expanded .row-label {
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.row-muted {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -437,12 +437,12 @@ function formatOdds(odds: string) {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
background: linear-gradient(165deg, #1a1810 0%, #121212 45%, #0a0a0a 100%);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px 16px 16px;
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow), 0 0 24px rgba(212, 175, 55, 0.08);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.close-x {
|
||||
@@ -452,7 +452,7 @@ function formatOdds(odds: string) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
@@ -545,7 +545,7 @@ function formatOdds(odds: string) {
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 12px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
background: var(--bg-hover);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
@@ -577,7 +577,7 @@ function formatOdds(odds: string) {
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #0a0a0a;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
@@ -586,12 +586,12 @@ function formatOdds(odds: string) {
|
||||
}
|
||||
|
||||
.stake-input:focus {
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: 0 0 0 1px rgba(212, 175, 55, 0.15);
|
||||
border-color: var(--border-active);
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
.stake-input::placeholder {
|
||||
color: #555;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -607,16 +607,16 @@ function formatOdds(odds: string) {
|
||||
padding: 6px 4px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #141414;
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chip:active {
|
||||
border-color: var(--border-gold-soft);
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
border-color: var(--border-active);
|
||||
color: var(--primary);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.est-return {
|
||||
@@ -646,7 +646,7 @@ function formatOdds(odds: string) {
|
||||
.btn-cancel {
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
background: #1a1a1a;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
@@ -687,7 +687,7 @@ function formatOdds(odds: string) {
|
||||
.summary {
|
||||
margin: 12px 0 14px;
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
background: var(--bg-hover);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import OutrightOptionCard from './OutrightOptionCard.vue';
|
||||
import saishiImg from '../../assets/images/saishi.webp';
|
||||
import cardBg from '../../assets/images/card-bg.webp';
|
||||
|
||||
const matchCardBg = `url(${cardBg})`;
|
||||
|
||||
export interface OutrightSelection {
|
||||
id: string;
|
||||
@@ -117,23 +114,13 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.event-head::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
|
||||
opacity: 0.25;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
flex-shrink: 0;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: #141414;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: var(--bg-hover);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -170,8 +157,8 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: #c9a227;
|
||||
border: 1px solid rgba(201, 162, 39, 0.45);
|
||||
color: var(--odds-accent);
|
||||
border: 1px solid rgba(248, 151, 31, 0.45);
|
||||
border-radius: 999px;
|
||||
padding: 1px 7px;
|
||||
line-height: 1.4;
|
||||
|
||||
@@ -86,42 +86,37 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 8px 4px 6px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(180deg, #1a1a1a 0%, #0d0d0d 100%);
|
||||
border: 1px solid rgba(140, 140, 140, 0.35);
|
||||
padding: 10px 4px 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(100, 100, 100, 0.08) 0%, transparent 50%, rgba(80, 80, 80, 0.05) 100%);
|
||||
pointer-events: none;
|
||||
.option-card:hover:not(.option-card--disabled) {
|
||||
border-color: var(--border-active);
|
||||
background: var(--bg-hover) !important;
|
||||
}
|
||||
|
||||
.option-card:active {
|
||||
border-color: var(--border-gold-soft);
|
||||
.option-card:active:not(.option-card--disabled) {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.option-card--disabled {
|
||||
cursor: default;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.option-card--disabled:active {
|
||||
border-color: rgba(140, 140, 140, 0.35);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.option-card--winner {
|
||||
border-color: rgba(201, 162, 39, 0.75);
|
||||
box-shadow: 0 0 0 1px rgba(201, 162, 39, 0.25);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 1px var(--primary);
|
||||
}
|
||||
|
||||
.flag {
|
||||
@@ -143,8 +138,8 @@ onUnmounted(() => {
|
||||
|
||||
.name {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -153,8 +148,8 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.odds {
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--odds-accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -149,7 +149,7 @@ function closeModal() {
|
||||
border: 1px solid rgba(201, 162, 39, 0.22);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #c9a227;
|
||||
color: var(--odds-accent);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,27 +5,80 @@ const visible = ref(false);
|
||||
const anchorX = ref(0);
|
||||
const anchorY = ref(0);
|
||||
const pendingItem = ref<SlipItem | null>(null);
|
||||
const activeCardRootId = ref<string | null>(null);
|
||||
const placement = ref<'top' | 'bottom'>('bottom');
|
||||
let closeTimer: number | undefined;
|
||||
|
||||
export function useDesktopBetPopover() {
|
||||
function openAt(event: MouseEvent, item: SlipItem) {
|
||||
cancelClose();
|
||||
|
||||
const el = (event.currentTarget || event.target) as HTMLElement;
|
||||
const btn = el?.closest?.('button') || el;
|
||||
const rect = btn?.getBoundingClientRect?.();
|
||||
if (!rect) return;
|
||||
|
||||
const popW = 172;
|
||||
const popH = 155;
|
||||
const pad = 12;
|
||||
const popW = 300;
|
||||
const popH = 320;
|
||||
let x = event.clientX + pad;
|
||||
let y = event.clientY + pad;
|
||||
|
||||
const btnCenterX = rect.left + rect.width / 2;
|
||||
let x = btnCenterX - popW / 2;
|
||||
|
||||
let y = rect.bottom + pad;
|
||||
placement.value = 'bottom';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
x = Math.min(x, window.innerWidth - popW - pad);
|
||||
y = Math.min(y, window.innerHeight - popH - pad);
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
if (spaceBelow < popH + pad) {
|
||||
y = rect.top - popH - pad;
|
||||
placement.value = 'top';
|
||||
}
|
||||
|
||||
x = Math.max(pad, Math.min(x, window.innerWidth - popW - pad));
|
||||
y = Math.max(pad, Math.min(y, window.innerHeight - popH - pad));
|
||||
}
|
||||
anchorX.value = Math.max(pad, x);
|
||||
anchorY.value = Math.max(pad, y);
|
||||
|
||||
anchorX.value = x;
|
||||
anchorY.value = y;
|
||||
pendingItem.value = item;
|
||||
const root = el?.closest?.('[data-bet-card-root]') as HTMLElement | null;
|
||||
activeCardRootId.value = root?.dataset.betCardRoot ?? null;
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible.value = false;
|
||||
pendingItem.value = null;
|
||||
activeCardRootId.value = null;
|
||||
if (closeTimer !== undefined) {
|
||||
window.clearTimeout(closeTimer);
|
||||
closeTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleClose(delay = 200) {
|
||||
if (closeTimer !== undefined) {
|
||||
window.clearTimeout(closeTimer);
|
||||
}
|
||||
closeTimer = window.setTimeout(() => {
|
||||
visible.value = false;
|
||||
pendingItem.value = null;
|
||||
activeCardRootId.value = null;
|
||||
closeTimer = undefined;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function cancelClose() {
|
||||
if (closeTimer !== undefined) {
|
||||
window.clearTimeout(closeTimer);
|
||||
closeTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isActiveForCard(cardRootId: string | null | undefined) {
|
||||
if (!cardRootId || !visible.value || !pendingItem.value) return false;
|
||||
return activeCardRootId.value === cardRootId;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -33,7 +86,12 @@ export function useDesktopBetPopover() {
|
||||
anchorX,
|
||||
anchorY,
|
||||
pendingItem,
|
||||
activeCardRootId,
|
||||
placement,
|
||||
openAt,
|
||||
close,
|
||||
scheduleClose,
|
||||
cancelClose,
|
||||
isActiveForCard,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const searchQuery = ref('');
|
||||
const filterState = ref({
|
||||
time: 'all' as 'all' | 'today' | 'early',
|
||||
status: 'open' as 'all' | 'open' | 'settled',
|
||||
leagueIds: [] as string[]
|
||||
leagueIds: [] as string[],
|
||||
});
|
||||
|
||||
/** 空数组表示未限定联赛,等价于全部选中 */
|
||||
export function isLeagueSelected(leagueId: string, leagueIds: string[]) {
|
||||
return leagueIds.length === 0 || leagueIds.includes(leagueId);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => filterState.value.time,
|
||||
() => {
|
||||
filterState.value.leagueIds = [];
|
||||
},
|
||||
);
|
||||
|
||||
export function useMatchFilters() {
|
||||
function clearFilters() {
|
||||
searchQuery.value = '';
|
||||
@@ -15,12 +27,23 @@ export function useMatchFilters() {
|
||||
filterState.value.leagueIds = [];
|
||||
}
|
||||
|
||||
function toggleLeague(leagueId: string) {
|
||||
const idx = filterState.value.leagueIds.indexOf(leagueId);
|
||||
function toggleLeague(leagueId: string, availableIds: string[]) {
|
||||
const current = filterState.value.leagueIds;
|
||||
|
||||
if (current.length === 0) {
|
||||
filterState.value.leagueIds = availableIds.filter((id) => id !== leagueId);
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = current.indexOf(leagueId);
|
||||
if (idx > -1) {
|
||||
filterState.value.leagueIds.splice(idx, 1);
|
||||
} else {
|
||||
filterState.value.leagueIds.push(leagueId);
|
||||
current.splice(idx, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
current.push(leagueId);
|
||||
if (availableIds.every((id) => current.includes(id))) {
|
||||
filterState.value.leagueIds = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,5 +52,6 @@ export function useMatchFilters() {
|
||||
filterState,
|
||||
clearFilters,
|
||||
toggleLeague,
|
||||
isLeagueSelected,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,27 @@ import { resolveBanners } from '../constants/defaultBanner';
|
||||
import { resolveAnnouncements } from '../constants/defaultAnnouncement';
|
||||
import { stripHtml } from '../utils/html';
|
||||
|
||||
export interface PlayerHomeMatchSelection {
|
||||
id: string;
|
||||
selectionCode?: string;
|
||||
selectionName: string;
|
||||
selectionDisplayName?: string;
|
||||
odds: string;
|
||||
status?: string;
|
||||
oddsVersion?: string;
|
||||
}
|
||||
|
||||
export interface PlayerHomeMatchMarket {
|
||||
id: string;
|
||||
marketType: string;
|
||||
marketDisplayName?: string;
|
||||
lineValue?: number | null;
|
||||
status: string;
|
||||
allowSingle?: boolean;
|
||||
allowParlay?: boolean;
|
||||
selections: PlayerHomeMatchSelection[];
|
||||
}
|
||||
|
||||
export interface PlayerHomeMatch {
|
||||
id: string;
|
||||
leagueName?: string;
|
||||
@@ -19,6 +40,8 @@ export interface PlayerHomeMatch {
|
||||
startTime: string;
|
||||
isHot?: boolean;
|
||||
displayOrder?: number;
|
||||
bettingOpen?: boolean;
|
||||
markets?: PlayerHomeMatchMarket[];
|
||||
}
|
||||
|
||||
export interface PlayerContentItem {
|
||||
|
||||
45
apps/player/src/composables/useSmartBack.ts
Normal file
45
apps/player/src/composables/useSmartBack.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { computed, toValue, type MaybeRefOrGetter } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const FALLBACK_RULES: Array<{ test: (path: string) => boolean; fallback: string }> = [
|
||||
{ test: (p) => /^\/wallet\/transactions\//.test(p), fallback: '/wallet/detail' },
|
||||
{ test: (p) => p.startsWith('/wallet/recharge/history'), fallback: '/wallet' },
|
||||
{ test: (p) => p === '/wallet/recharge', fallback: '/wallet' },
|
||||
{ test: (p) => p === '/wallet/detail', fallback: '/wallet' },
|
||||
{ test: (p) => p === '/wallet/cashbacks' || p === '/profile/cashbacks', fallback: '/wallet' },
|
||||
{ test: (p) => p === '/profile/edit', fallback: '/wallet' },
|
||||
{ test: (p) => p.startsWith('/bets/'), fallback: '/bets' },
|
||||
{ test: (p) => p.startsWith('/match/'), fallback: '/bet' },
|
||||
{ test: (p) => p.startsWith('/outright/'), fallback: '/bet' },
|
||||
{ test: (p) => p.startsWith('/announcements/'), fallback: '/announcements' },
|
||||
{ test: (p) => p.startsWith('/messages/'), fallback: '/messages' },
|
||||
{ test: (p) => p === '/wallet', fallback: '/' },
|
||||
{ test: (p) => p === '/bets', fallback: '/' },
|
||||
];
|
||||
|
||||
export function resolveSmartBackFallback(path: string): string {
|
||||
for (const rule of FALLBACK_RULES) {
|
||||
if (rule.test(path)) return rule.fallback;
|
||||
}
|
||||
return '/';
|
||||
}
|
||||
|
||||
export function useSmartBack(fallback?: MaybeRefOrGetter<string | undefined>) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const resolvedFallback = computed(
|
||||
() => toValue(fallback) ?? resolveSmartBackFallback(route.path),
|
||||
);
|
||||
|
||||
function goBack() {
|
||||
const state = window.history.state as { back?: string | null } | null;
|
||||
if (state?.back != null) {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
void router.push(resolvedFallback.value);
|
||||
}
|
||||
|
||||
return { goBack, fallback: resolvedFallback };
|
||||
}
|
||||
@@ -29,9 +29,12 @@ export default {
|
||||
total: '{total} total',
|
||||
per_page: '{size} / page',
|
||||
},
|
||||
nav: { home: 'Home', sports: 'Sports', football: 'Football', bet: 'Bet', bet_history: 'History', wallet: 'Transactions', account_hub: 'Bets & Wallet', profile: 'Profile', search: 'Search teams, leagues…', announcements: 'Announcements', sports_more_soon: 'More sports coming soon' },
|
||||
nav: { home: 'Home', sports: 'Sports', football: 'Football', bet: 'Bet', bet_history: 'History', wallet: 'Transactions', my_wallet: 'My Wallet', account_hub: 'Bets & Wallet', profile: 'Profile', search: 'Search teams, leagues…', announcements: 'Announcements', sports_more_soon: 'More sports coming soon' },
|
||||
home: {
|
||||
hot_matches: 'Hot matches',
|
||||
section_featured: 'Featured',
|
||||
section_upcoming: 'Upcoming',
|
||||
sidebar_recommend: 'Recommended',
|
||||
hot_tab: 'Hot',
|
||||
upcoming_tab: 'Upcoming',
|
||||
no_matches: 'No matches',
|
||||
@@ -43,6 +46,9 @@ export default {
|
||||
banner_next: 'Next slide',
|
||||
banner_slide: 'Slide {n}',
|
||||
banner_fallback: 'Banner',
|
||||
bento_desc: 'Explore hot events worldwide',
|
||||
view_all_matches: 'View all matches',
|
||||
view_bet_history: 'View bet history',
|
||||
},
|
||||
announcements: {
|
||||
title: 'Announcements',
|
||||
@@ -158,6 +164,7 @@ export default {
|
||||
},
|
||||
auth:
|
||||
{ login: 'Login',
|
||||
logging_in: 'Logging in…',
|
||||
register: 'Create Account',
|
||||
logout: 'Log out',
|
||||
username: 'Username',
|
||||
@@ -212,6 +219,7 @@ export default {
|
||||
forgot_password: 'Forgot password?',
|
||||
forgot_password_title: 'Reset password',
|
||||
reset_password_btn: 'Reset password',
|
||||
resetting_password: 'Resetting…',
|
||||
reset_success: 'Password reset. Please sign in with your new password. Contact your agent if you cannot log in.',
|
||||
reset_failed: 'Reset failed. Please try again.',
|
||||
phone_not_registered: 'This phone number is not registered',
|
||||
@@ -227,6 +235,7 @@ export default {
|
||||
},
|
||||
wallet: {
|
||||
balance: 'Balance',
|
||||
my_balance: 'My Balance',
|
||||
cash_balance: 'Cash Balance',
|
||||
card_holder: 'Cardholder',
|
||||
unsettled: 'Unsettled',
|
||||
@@ -263,6 +272,9 @@ export default {
|
||||
filter_bet: 'Bet',
|
||||
filter_cashback: 'Cashback',
|
||||
view_all: 'View all transactions',
|
||||
recent_title: 'Recent transactions',
|
||||
overview: 'Overview',
|
||||
all_records: 'All transactions',
|
||||
detail_summary: 'Details',
|
||||
detail_amount: 'Amount',
|
||||
detail_balance_before: 'Balance Before',
|
||||
@@ -293,6 +305,8 @@ export default {
|
||||
transaction_detail: 'Transaction details',
|
||||
balance_before: 'Balance before',
|
||||
balance_after: 'Balance after',
|
||||
frozen_before: 'Frozen before',
|
||||
frozen_after: 'Frozen after',
|
||||
transaction_id: 'Transaction ID',
|
||||
detail_title: 'Wallet details',
|
||||
recharge_btn: 'Deposit',
|
||||
@@ -394,16 +408,18 @@ export default {
|
||||
sport_coming_soon: 'Coming soon',
|
||||
tab_today: 'Today',
|
||||
tab_early: 'Early',
|
||||
filter_time: 'Time',
|
||||
filter_time: 'Filter',
|
||||
filter_league: 'Leagues',
|
||||
filter_market: 'Markets',
|
||||
time_all: 'All',
|
||||
all: 'All',
|
||||
time_today: 'Today',
|
||||
time_early: 'Early',
|
||||
market_all: 'All markets',
|
||||
market_handicap_ou: 'Handicap / O-U',
|
||||
correct_score: 'Correct score',
|
||||
no_markets: 'No markets available',
|
||||
no_market_odds: 'No odds available',
|
||||
no_markets_filtered: 'No markets match the current filter',
|
||||
popover_title: 'Confirm bet',
|
||||
place_now: 'Bet now',
|
||||
@@ -446,6 +462,9 @@ export default {
|
||||
outright_load_more: 'Load more',
|
||||
outright_settled: 'Settled',
|
||||
outright_settled_hint: 'This event is settled. Odds and results are view-only.',
|
||||
quick_bet: 'Quick bet',
|
||||
popular_teams: 'Popular teams',
|
||||
outright_view_all: 'View all teams',
|
||||
cancel: 'Cancel',
|
||||
parlay_max_legs: 'Parlay allows up to 5 legs',
|
||||
parlay_block_outright: 'Outright cannot be parlayed',
|
||||
|
||||
@@ -36,6 +36,7 @@ export default {
|
||||
bet: 'Pertaruhan',
|
||||
bet_history: 'Sejarah',
|
||||
wallet: 'Transaksi',
|
||||
my_wallet: 'Dompet Saya',
|
||||
account_hub: 'Pertaruhan & Bil',
|
||||
profile: 'Profil',
|
||||
search: 'Cari pasukan, liga…',
|
||||
@@ -44,6 +45,9 @@ export default {
|
||||
},
|
||||
home: {
|
||||
hot_matches: 'Perlawanan popular',
|
||||
section_featured: 'Perlawanan utama',
|
||||
section_upcoming: 'Akan bermula',
|
||||
sidebar_recommend: 'Disyorkan',
|
||||
hot_tab: 'Popular',
|
||||
upcoming_tab: 'Terdekat',
|
||||
no_matches: 'Tiada perlawanan',
|
||||
@@ -55,6 +59,9 @@ export default {
|
||||
banner_next: 'Slaid seterusnya',
|
||||
banner_slide: 'Slaid {n}',
|
||||
banner_fallback: 'Banner',
|
||||
bento_desc: 'Teroka acara sukan hangat',
|
||||
view_all_matches: 'Lihat semua perlawanan',
|
||||
view_bet_history: 'Lihat sejarah pertaruhan',
|
||||
},
|
||||
announcements: {
|
||||
title: 'Pusat Pengumuman',
|
||||
@@ -170,6 +177,7 @@ export default {
|
||||
},
|
||||
auth: {
|
||||
login: 'Log Masuk',
|
||||
logging_in: 'Sedang log masuk…',
|
||||
register: 'Daftar Akaun',
|
||||
logout: 'Log Keluar',
|
||||
username: 'Nama Pengguna',
|
||||
@@ -224,6 +232,7 @@ export default {
|
||||
forgot_password: 'Lupa kata laluan?',
|
||||
forgot_password_title: 'Tetapkan semula kata laluan',
|
||||
reset_password_btn: 'Tetapkan semula',
|
||||
resetting_password: 'Sedang tetapkan semula…',
|
||||
reset_success: 'Kata laluan ditetapkan semula. Sila log masuk dengan kata laluan baharu. Hubungi ejen anda jika tidak dapat log masuk.',
|
||||
reset_failed: 'Gagal menetapkan semula. Sila cuba lagi.',
|
||||
phone_not_registered: 'Nombor telefon ini belum didaftarkan',
|
||||
@@ -239,6 +248,7 @@ export default {
|
||||
},
|
||||
wallet: {
|
||||
balance: 'Baki',
|
||||
my_balance: 'Baki Saya',
|
||||
cash_balance: 'Baki Tunai',
|
||||
card_holder: 'Pemegang',
|
||||
unsettled: 'Belum Selesai',
|
||||
@@ -275,6 +285,9 @@ export default {
|
||||
filter_bet: 'Pertaruhan',
|
||||
filter_cashback: 'Rebat',
|
||||
view_all: 'Lihat semua transaksi',
|
||||
recent_title: 'Transaksi terkini',
|
||||
overview: 'Gambaran akaun',
|
||||
all_records: 'Semua transaksi',
|
||||
detail_summary: 'Butiran',
|
||||
detail_amount: 'Jumlah',
|
||||
detail_balance_before: 'Baki Sebelum',
|
||||
@@ -305,6 +318,8 @@ export default {
|
||||
transaction_detail: 'Butiran transaksi',
|
||||
balance_before: 'Baki sebelum',
|
||||
balance_after: 'Baki selepas',
|
||||
frozen_before: 'Beku sebelum',
|
||||
frozen_after: 'Beku selepas',
|
||||
transaction_id: 'ID transaksi',
|
||||
detail_title: 'Butiran bil',
|
||||
recharge_btn: 'Topup',
|
||||
@@ -406,16 +421,18 @@ export default {
|
||||
sport_coming_soon: 'Akan datang',
|
||||
tab_today: 'Hari Ini',
|
||||
tab_early: 'Awal',
|
||||
filter_time: 'Masa',
|
||||
filter_time: 'Tapis',
|
||||
filter_league: 'Liga',
|
||||
filter_market: 'Pasaran',
|
||||
time_all: 'Semua',
|
||||
all: 'Semua',
|
||||
time_today: 'Hari ini',
|
||||
time_early: 'Awal',
|
||||
market_all: 'Semua pasaran',
|
||||
market_handicap_ou: 'Handicap / O-U',
|
||||
correct_score: 'Skor tepat',
|
||||
no_markets: 'Tiada pasaran tersedia',
|
||||
no_market_odds: 'Tiada odds tersedia',
|
||||
no_markets_filtered: 'Tiada pasaran sepadan dengan penapis semasa',
|
||||
popover_title: 'Sahkan pertaruhan',
|
||||
place_now: 'Pertaruhan sekarang',
|
||||
@@ -458,6 +475,9 @@ export default {
|
||||
outright_load_more: 'Muat lagi',
|
||||
outright_settled: 'Selesai',
|
||||
outright_settled_hint: 'Acara ini telah diselesaikan. Hanya paparan odds dan keputusan.',
|
||||
quick_bet: 'Pertaruhan pantas',
|
||||
popular_teams: 'Pasukan popular',
|
||||
outright_view_all: 'Lihat semua pasukan',
|
||||
cancel: 'Batal',
|
||||
parlay_max_legs: 'Maksimum 5 pilihan parlay',
|
||||
parlay_block_outright: 'Outright tidak boleh parlay',
|
||||
|
||||
@@ -29,9 +29,12 @@ export default {
|
||||
total: '共 {total} 条',
|
||||
per_page: '{size} / 页',
|
||||
},
|
||||
nav: { home: '主页', sports: '体育赛事', football: '足球', bet: '投注', bet_history: '历史投注', wallet: '资金明细', account_hub: '投注账单', profile: '我的', search: '搜索球队、联赛…', announcements: '公告', sports_more_soon: '更多体育,敬请期待' },
|
||||
nav: { home: '主页', sports: '体育赛事', football: '足球', bet: '投注', bet_history: '历史投注', wallet: '资金明细', my_wallet: '我的钱包', account_hub: '投注账单', profile: '我的', search: '搜索球队、联赛…', announcements: '公告', sports_more_soon: '更多体育,敬请期待' },
|
||||
home: {
|
||||
hot_matches: '热门赛事',
|
||||
section_featured: '焦点赛事',
|
||||
section_upcoming: '即将开赛',
|
||||
sidebar_recommend: '推荐赛事',
|
||||
hot_tab: '热门',
|
||||
upcoming_tab: '近期',
|
||||
no_matches: '暂无赛事',
|
||||
@@ -43,6 +46,9 @@ export default {
|
||||
banner_next: '下一张',
|
||||
banner_slide: '第 {n} 张',
|
||||
banner_fallback: 'Banner',
|
||||
bento_desc: '探索全球热门体育赛事',
|
||||
view_all_matches: '查看全部赛事',
|
||||
view_bet_history: '查看历史投注',
|
||||
},
|
||||
announcements: {
|
||||
title: '公告中心',
|
||||
@@ -158,6 +164,7 @@ export default {
|
||||
},
|
||||
auth: {
|
||||
login: '登录',
|
||||
logging_in: '登录中…',
|
||||
register: '注册账号',
|
||||
logout: '退出登录',
|
||||
username: '账号',
|
||||
@@ -212,6 +219,7 @@ export default {
|
||||
forgot_password: '忘记密码?',
|
||||
forgot_password_title: '重置密码',
|
||||
reset_password_btn: '重置密码',
|
||||
resetting_password: '重置中…',
|
||||
reset_success: '密码已重置,请使用新密码登录。若无法登录请联系代理。',
|
||||
reset_failed: '重置失败,请重试',
|
||||
phone_not_registered: '该手机号未注册',
|
||||
@@ -227,6 +235,7 @@ export default {
|
||||
},
|
||||
wallet: {
|
||||
balance: '余额',
|
||||
my_balance: '我的余额',
|
||||
cash_balance: '现金余额',
|
||||
card_holder: '持卡人',
|
||||
unsettled: '未结算',
|
||||
@@ -263,6 +272,9 @@ export default {
|
||||
filter_bet: '投注',
|
||||
filter_cashback: '反水',
|
||||
view_all: '查看全部账单',
|
||||
recent_title: '最近账单',
|
||||
overview: '账户概览',
|
||||
all_records: '全部账单',
|
||||
detail_summary: '账务明细',
|
||||
detail_amount: '变动金额',
|
||||
detail_balance_before: '变动前余额',
|
||||
@@ -293,6 +305,8 @@ export default {
|
||||
transaction_detail: '交易详情',
|
||||
balance_before: '变动前余额',
|
||||
balance_after: '变动后余额',
|
||||
frozen_before: '变动前冻结',
|
||||
frozen_after: '变动后冻结',
|
||||
transaction_id: '流水号',
|
||||
detail_title: '账单明细',
|
||||
recharge_btn: '充值',
|
||||
@@ -394,16 +408,18 @@ export default {
|
||||
sport_coming_soon: '即将上线',
|
||||
tab_today: '今日',
|
||||
tab_early: '早盘',
|
||||
filter_time: '时间筛选',
|
||||
filter_time: '筛选',
|
||||
filter_league: '联赛',
|
||||
filter_market: '玩法筛选',
|
||||
time_all: '全部',
|
||||
all: '全部',
|
||||
time_today: '今天',
|
||||
time_early: '早盘',
|
||||
market_all: '所有盘口',
|
||||
market_handicap_ou: '让球 / 大小',
|
||||
correct_score: '波胆',
|
||||
no_markets: '暂无可用盘口',
|
||||
no_market_odds: '暂无盘口赔率',
|
||||
no_markets_filtered: '当前筛选下暂无盘口',
|
||||
popover_title: '确认投注',
|
||||
place_now: '立即下注',
|
||||
@@ -446,6 +462,9 @@ export default {
|
||||
outright_load_more: '加载更多',
|
||||
outright_settled: '已结算',
|
||||
outright_settled_hint: '本赛事已结算,仅可查看赔率与冠军结果',
|
||||
quick_bet: '快速下注',
|
||||
popular_teams: '热门队伍',
|
||||
outright_view_all: '查看全部队伍',
|
||||
cancel: '取消',
|
||||
parlay_max_legs: '串关最多 5 项',
|
||||
parlay_block_outright: '冠军盘不可串关',
|
||||
|
||||
@@ -12,6 +12,7 @@ import BackToTopButton from '../components/BackToTopButton.vue';
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useViewport } from '../composables/useViewport';
|
||||
import DesktopShell from '../components/desktop/DesktopShell.vue';
|
||||
import AnnouncementMarquee from '../components/AnnouncementMarquee.vue';
|
||||
|
||||
const BetSlipDrawer = defineAsyncComponent(() => import('../components/BetSlipDrawer.vue'));
|
||||
import { usePlayerHome } from '../composables/usePlayerHome';
|
||||
@@ -43,6 +44,9 @@ const isDetailPage = computed(() => {
|
||||
});
|
||||
|
||||
const showHeader = computed(() => !isDetailPage.value);
|
||||
const showAnnouncement = computed(
|
||||
() => !isDetailPage.value && !route.path.startsWith('/profile') && !route.path.startsWith('/announcements'),
|
||||
);
|
||||
|
||||
const showBottomNav = computed(() => {
|
||||
const p = route.path;
|
||||
@@ -54,18 +58,19 @@ const showBottomNav = computed(() => {
|
||||
p.startsWith('/match/') ||
|
||||
p === '/bets' ||
|
||||
p === '/wallet' ||
|
||||
p === '/profile'
|
||||
p.startsWith('/wallet/')
|
||||
) return true;
|
||||
// 邮箱关闭时客服页保留底部导航,避免用户无法离开
|
||||
if (!inboxEnabled.value && p === '/messages') return true;
|
||||
return false;
|
||||
});
|
||||
const { load: loadPlayerHome } = usePlayerHome();
|
||||
const { announcements, announcementItems, load: loadPlayerHome } = usePlayerHome();
|
||||
const { loadProfile, refreshProfile, bindProfileVisibilityRefresh } = usePlayerProfile();
|
||||
const { startPolling, stopPolling } = useDepositNotifications();
|
||||
const { unreadCount, refreshUnreadCount, resetMessagesState } = usePlayerMessages();
|
||||
const mainRef = ref<HTMLElement | null>(null);
|
||||
const tabScrollTops = new Map<string, number>();
|
||||
const primaryAnnouncementId = computed(() => announcementItems.value[0]?.id ?? '');
|
||||
const bottomNavHidden = ref(false);
|
||||
let lastMainScrollTop = 0;
|
||||
|
||||
@@ -103,7 +108,7 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
const balanceRefreshPaths = ['/profile', '/wallet', '/bets'];
|
||||
const balanceRefreshPaths = ['/wallet', '/bets'];
|
||||
let mainScrollEl: HTMLElement | null = null;
|
||||
|
||||
watch(locale, (next, prev) => {
|
||||
@@ -160,11 +165,15 @@ watch(
|
||||
<DesktopShell v-if="isDesktop" />
|
||||
<div v-else class="layout">
|
||||
<header v-if="showHeader" class="header">
|
||||
<img src="/logo.png" alt="TheBet365" class="logo" />
|
||||
<svg class="logo" viewBox="0 0 130 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="TheBet365">
|
||||
<text x="0" y="18" font-family="'Inter','SF Pro Display',system-ui,sans-serif" font-size="11" font-weight="800" fill="#6B7280" letter-spacing="0.14em">THE</text>
|
||||
<text x="32" y="18" font-family="'Inter','SF Pro Display',system-ui,sans-serif" font-size="18" font-weight="900" fill="#003D6B" letter-spacing="0.01em">BET</text>
|
||||
<text x="74" y="18" font-family="'Inter','SF Pro Display',system-ui,sans-serif" font-size="18" font-weight="900" fill="#F8971F" letter-spacing="0.01em">365</text>
|
||||
</svg>
|
||||
<div class="header-actions">
|
||||
<RouterLink
|
||||
:to="hubRoute"
|
||||
class="hub-btn"
|
||||
class="support-btn"
|
||||
:aria-label="inboxEnabled && unreadCount ? t('messages.unread_badge', { count: unreadCount }) : t(hubOpenLabelKey)"
|
||||
>
|
||||
<svg class="hub-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
@@ -181,6 +190,7 @@ watch(
|
||||
</svg>
|
||||
<span v-if="auth.user && inboxEnabled && unreadCount > 0" class="hub-badge">{{ unreadCount > 99 ? '99+' : unreadCount }}</span>
|
||||
</RouterLink>
|
||||
<div class="header-divider" />
|
||||
<LocaleSwitcher />
|
||||
<template v-if="auth.user">
|
||||
<CashBalanceChip />
|
||||
@@ -192,7 +202,15 @@ watch(
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main ref="mainRef" :class="['main', { 'has-nav': showBottomNav }]">
|
||||
<div v-if="showAnnouncement" class="announce-strip">
|
||||
<AnnouncementMarquee
|
||||
:items="announcements"
|
||||
:target-id="primaryAnnouncementId"
|
||||
embedded
|
||||
/>
|
||||
</div>
|
||||
|
||||
<main ref="mainRef" :class="['main', { 'has-nav': showBottomNav, 'main--detail': isDetailPage }]">
|
||||
<RouterView v-slot="{ Component, route: viewRoute }">
|
||||
<KeepAlive v-if="viewRoute.meta.keepAlive" :max="10">
|
||||
<component :is="Component" :key="viewRoute.path" />
|
||||
@@ -223,13 +241,13 @@ watch(
|
||||
<BottomNavIcon name="history" />
|
||||
<span class="nav-label">{{ t('nav.bet_history') }}</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/wallet" class="nav-item" :class="{ active: route.path === '/wallet' }">
|
||||
<RouterLink
|
||||
to="/wallet"
|
||||
class="nav-item"
|
||||
:class="{ active: route.path === '/wallet' || route.path.startsWith('/wallet/') }"
|
||||
>
|
||||
<BottomNavIcon name="wallet" />
|
||||
<span class="nav-label">{{ t('nav.wallet') }}</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/profile" class="nav-item" :class="{ active: route.path.startsWith('/profile') }">
|
||||
<BottomNavIcon name="profile" />
|
||||
<span class="nav-label">{{ t('nav.profile') }}</span>
|
||||
<span class="nav-label">{{ t('nav.my_wallet') }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
@@ -249,7 +267,7 @@ watch(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
background: var(--bg-body);
|
||||
box-sizing: border-box;
|
||||
padding-top: var(--safe-top);
|
||||
}
|
||||
@@ -260,17 +278,19 @@ watch(
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
min-height: var(--player-header-h);
|
||||
background: rgba(17, 17, 17, 0.94);
|
||||
min-height: var(--player-header-h, 48px);
|
||||
background: #FFFFFF;
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 120;
|
||||
}
|
||||
.logo {
|
||||
height: 36px; width: auto; display: block;
|
||||
filter: drop-shadow(0 0 4px rgba(212, 175, 55, 0.2));
|
||||
height: 22px;
|
||||
width: auto;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header-actions {
|
||||
--header-chip-h: 36px;
|
||||
--header-chip-h: 32px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
@@ -286,28 +306,43 @@ watch(
|
||||
width: var(--header-chip-h);
|
||||
}
|
||||
|
||||
.hub-btn {
|
||||
position: relative;
|
||||
.header-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: #E5E7EB;
|
||||
flex-shrink: 0;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.support-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--header-chip-h, 36px);
|
||||
height: var(--header-chip-h, 36px);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-gold-soft, rgba(200, 168, 78, 0.25));
|
||||
background: rgba(200, 168, 78, 0.08);
|
||||
color: var(--primary-light, #c8a84e);
|
||||
gap: 0;
|
||||
width: var(--header-chip-h, 32px);
|
||||
height: var(--header-chip-h, 32px);
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
background: #F5F7FA;
|
||||
color: var(--primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hub-btn:active {
|
||||
background: rgba(200, 168, 78, 0.16);
|
||||
.support-btn:active {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--border-active);
|
||||
}
|
||||
|
||||
.hub-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
.support-icon, .hub-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hub-badge {
|
||||
@@ -315,37 +350,41 @@ watch(
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
border-radius: 999px;
|
||||
background: #e74c3c;
|
||||
border-radius: 8px;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 0 2px rgba(17, 17, 17, 0.94);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.support-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--primary, #c8a84e);
|
||||
background: transparent;
|
||||
color: var(--primary-light, #c8a84e);
|
||||
padding: 6px 18px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--primary);
|
||||
background: var(--primary);
|
||||
color: #FFFFFF;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.login-btn:active {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
background: rgba(200, 168, 78, 0.15);
|
||||
}
|
||||
.announce-strip {
|
||||
flex-shrink: 0;
|
||||
z-index: 105;
|
||||
background: #FFFFFF;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -355,12 +394,14 @@ watch(
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 12px 16px 0;
|
||||
padding: 10px 14px 0;
|
||||
}
|
||||
|
||||
.main.has-nav {
|
||||
padding-bottom: calc(var(--player-bottom-nav-h) + var(--safe-bottom));
|
||||
}
|
||||
.main.main--detail {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
@@ -368,12 +409,11 @@ watch(
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
background: #FFFFFF;
|
||||
box-sizing: border-box;
|
||||
min-height: calc(var(--player-bottom-nav-h) + var(--safe-bottom));
|
||||
padding-bottom: var(--safe-bottom);
|
||||
background: rgba(17, 17, 17, 0.96);
|
||||
border-top: 1px solid var(--border);
|
||||
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.35);
|
||||
z-index: 100;
|
||||
transition: transform 0.28s ease, opacity 0.28s ease;
|
||||
will-change: transform;
|
||||
@@ -391,15 +431,16 @@ watch(
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
padding: 6px 2px 4px;
|
||||
font-size: 9px;
|
||||
padding: 7px 2px 8px;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
position: relative;
|
||||
transition: color 0.2s;
|
||||
line-height: 1.2;
|
||||
min-width: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
@@ -411,22 +452,23 @@ watch(
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--primary-light);
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.nav-item.active :deep(.nav-icon) {
|
||||
filter: drop-shadow(0 0 4px rgba(212, 175, 55, 0.35));
|
||||
color: var(--primary);
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 61, 107, 0.2));
|
||||
}
|
||||
|
||||
.nav-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 22%;
|
||||
right: 22%;
|
||||
height: 2px;
|
||||
left: 18%;
|
||||
right: 18%;
|
||||
height: 2.5px;
|
||||
background: var(--primary);
|
||||
border-radius: 0 0 4px 4px;
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -15,25 +15,25 @@ const router = createRouter({
|
||||
{ path: '', component: () => import('../views/HomeView.vue'), meta: { keepAlive: true } },
|
||||
{ path: 'bet', component: () => import('../views/FootballView.vue'), meta: { keepAlive: true } },
|
||||
{ path: 'football', redirect: '/bet' },
|
||||
{
|
||||
path: 'announcements',
|
||||
component: () => import('../views/AnnouncementsHubView.vue'),
|
||||
children: [
|
||||
{ path: '', component: () => import('../views/AnnouncementListView.vue'), meta: { keepAlive: true } },
|
||||
{ path: ':id', component: () => import('../views/AnnouncementDetailView.vue') },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'messages',
|
||||
component: () => import('../views/MessagesHubView.vue'),
|
||||
meta: { keepAlive: true, requiresAuth: false },
|
||||
children: [
|
||||
{ path: '', component: () => import('../views/InboxHubView.vue') },
|
||||
{ path: ':id', component: () => import('../views/MessageDetailView.vue'), meta: { requiresAuth: true } },
|
||||
],
|
||||
},
|
||||
{ path: 'match/:id', component: () => import('../views/MatchDetailView.vue') },
|
||||
{ path: 'outright/:id', component: () => import('../views/OutrightDetailView.vue') },
|
||||
{
|
||||
path: 'announcements',
|
||||
component: () => import('../views/AnnouncementsHubView.vue'),
|
||||
children: [
|
||||
{ path: '', component: () => import('../views/AnnouncementListView.vue'), meta: { keepAlive: true } },
|
||||
{ path: ':id', component: () => import('../views/AnnouncementDetailView.vue') },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'messages',
|
||||
component: () => import('../views/MessagesHubView.vue'),
|
||||
meta: { keepAlive: true, requiresAuth: false },
|
||||
children: [
|
||||
{ path: '', component: () => import('../views/InboxHubView.vue') },
|
||||
{ path: ':id', component: () => import('../views/MessageDetailView.vue'), meta: { requiresAuth: true } },
|
||||
],
|
||||
},
|
||||
{ path: 'match/:id', component: () => import('../views/MatchDetailView.vue') },
|
||||
{ path: 'outright/:id', component: () => import('../views/OutrightDetailView.vue') },
|
||||
// 需要登录的页面
|
||||
{ path: 'bets', component: () => import('../views/MyBetsView.vue'), meta: { keepAlive: true, requiresAuth: true } },
|
||||
{ path: 'bets/:betNo', component: () => import('../views/BetDetailView.vue'), meta: { requiresAuth: true } },
|
||||
@@ -42,9 +42,10 @@ const router = createRouter({
|
||||
{ path: 'wallet/cashbacks', component: () => import('../views/CashbackRecordsView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: 'wallet/recharge', component: () => import('../views/RechargeView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: 'wallet/recharge/history', component: () => import('../views/RechargeHistoryView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: 'wallet/recharge/history/:id', component: () => import('../views/RechargeDetailView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: 'wallet/transactions/:transactionId', component: () => import('../views/WalletTransactionDetailView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: 'profile', component: () => import('../views/ProfileView.vue'), meta: { keepAlive: true, requiresAuth: true } },
|
||||
{ path: 'profile/cashbacks', component: () => import('../views/CashbackRecordsView.vue'), meta: { requiresAuth: true } },
|
||||
{ path: 'profile', redirect: '/wallet' },
|
||||
{ path: 'profile/cashbacks', redirect: '/wallet/cashbacks' },
|
||||
{ path: 'profile/edit', component: () => import('../views/ProfileEditView.vue'), meta: { requiresAuth: true } },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -27,6 +27,23 @@ export type SlipMode = 'single' | 'parlay';
|
||||
export type ParlaySlipError = ParlayRejectReason | 'MAX_LEGS' | 'SAME_MATCH';
|
||||
|
||||
const BET_SLIP_STORAGE_PREFIX = 'player:betSlip:v1';
|
||||
const DESKTOP_PANEL_EXPANDED_KEY = 'player:desktopBetSlipExpanded:v1';
|
||||
|
||||
function readDesktopPanelExpanded() {
|
||||
try {
|
||||
return localStorage.getItem(DESKTOP_PANEL_EXPANDED_KEY) !== '0';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function writeDesktopPanelExpanded(expanded: boolean) {
|
||||
try {
|
||||
localStorage.setItem(DESKTOP_PANEL_EXPANDED_KEY, expanded ? '1' : '0');
|
||||
} catch {
|
||||
// ignore quota / private mode errors
|
||||
}
|
||||
}
|
||||
|
||||
interface BetSlipPersisted {
|
||||
singleItem: SlipItem | null;
|
||||
@@ -238,7 +255,7 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
function addToSingleCart(item: SlipItem) {
|
||||
mode.value = 'single';
|
||||
if (singleCartItems.value.some((i) => i.selectionId === item.selectionId)) return;
|
||||
singleCartItems.value.push(item);
|
||||
singleCartItems.value.unshift(item);
|
||||
initItemStake(item.selectionId);
|
||||
lastParlayError.value = null;
|
||||
}
|
||||
@@ -281,7 +298,7 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
return 'MAX_LEGS';
|
||||
}
|
||||
|
||||
parlayItems.value.push(item);
|
||||
parlayItems.value.unshift(item);
|
||||
lastParlayError.value = null;
|
||||
return null;
|
||||
}
|
||||
@@ -370,6 +387,11 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
);
|
||||
|
||||
const drawerOpen = ref(false);
|
||||
const panelExpanded = ref(readDesktopPanelExpanded());
|
||||
|
||||
watch(panelExpanded, (expanded) => {
|
||||
writeDesktopPanelExpanded(expanded);
|
||||
});
|
||||
|
||||
function openDrawer() {
|
||||
drawerOpen.value = true;
|
||||
@@ -379,6 +401,18 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
drawerOpen.value = false;
|
||||
}
|
||||
|
||||
function expandPanel() {
|
||||
panelExpanded.value = true;
|
||||
}
|
||||
|
||||
function collapsePanel() {
|
||||
panelExpanded.value = false;
|
||||
}
|
||||
|
||||
function togglePanelExpanded() {
|
||||
panelExpanded.value = !panelExpanded.value;
|
||||
}
|
||||
|
||||
function updateSelectionOdds(selectionId: string, odds: number, oddsVersion: string) {
|
||||
if (singleItem.value?.selectionId === selectionId) {
|
||||
singleItem.value = { ...singleItem.value, odds, oddsVersion };
|
||||
@@ -414,6 +448,7 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
canSubmit,
|
||||
lastParlayError,
|
||||
drawerOpen,
|
||||
panelExpanded,
|
||||
setMode,
|
||||
setSingleItem,
|
||||
addItem,
|
||||
@@ -428,6 +463,9 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
clearAll,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
expandPanel,
|
||||
collapsePanel,
|
||||
togglePanelExpanded,
|
||||
updateSelectionOdds,
|
||||
getItemStake,
|
||||
setItemStake,
|
||||
|
||||
@@ -1,230 +1,71 @@
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--primary: #D4AF37;
|
||||
--primary-dark: #A8841F;
|
||||
--primary-light: #F0D875;
|
||||
--secondary: #1A1A1A;
|
||||
--tertiary: #000000;
|
||||
--neutral: #8E8E93;
|
||||
--bg-body: #000000;
|
||||
--bg-card: rgba(20, 20, 20, 0.65);
|
||||
--bg-hover: rgba(34, 34, 34, 0.65);
|
||||
--bg-elevated: rgba(42, 42, 42, 0.65);
|
||||
--text: #FFFFFF;
|
||||
--text-muted: #8E8E93;
|
||||
--border: #333333;
|
||||
--border-gold: #D4AF37;
|
||||
--border-gold-soft: rgba(212, 175, 55, 0.28);
|
||||
/* Pinnacle-inspired: Navy blue + clean white theme */
|
||||
--primary: #003D6B;
|
||||
--primary-dark: #002847;
|
||||
--primary-light: #005B9F;
|
||||
--secondary: #E8EDF2;
|
||||
--tertiary: #F5F7FA;
|
||||
--neutral: #6B7280;
|
||||
--bg-body: #F5F7FA;
|
||||
--bg-card: #FFFFFF;
|
||||
--bg-hover: #F0F4F8;
|
||||
--bg-elevated: #FFFFFF;
|
||||
--text: #1A1A2E;
|
||||
--text-muted: #6B7280;
|
||||
--border: #E5E7EB;
|
||||
--border-active: #0066CC;
|
||||
--border-w: 1px;
|
||||
--border-w-thick: 1px;
|
||||
--danger: #FF453A;
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
|
||||
--shadow-gold: 0 2px 12px rgba(212, 175, 55, 0.12);
|
||||
--glow-gold: 0 0 8px rgba(212, 175, 55, 0.2);
|
||||
--gradient-gold: linear-gradient(
|
||||
180deg,
|
||||
#FFF4C8 0%,
|
||||
#F0D875 18%,
|
||||
#D4AF37 50%,
|
||||
#B8942B 78%,
|
||||
#8B6914 100%
|
||||
);
|
||||
--gradient-gold-border: linear-gradient(
|
||||
180deg,
|
||||
#FFF8DC 0%,
|
||||
#E8C96A 35%,
|
||||
#A8841F 70%,
|
||||
#5C4A12 100%
|
||||
);
|
||||
--gradient-card: linear-gradient(160deg, #1E1A12 0%, #141414 40%, #0A0A0A 100%);
|
||||
--player-header-h: 52px;
|
||||
--danger: #DC2626;
|
||||
--success: #059669;
|
||||
--warning: #F8971F;
|
||||
--radius: 8px;
|
||||
--radius-sm: 6px;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
--shadow-gold: 0 2px 12px rgba(0, 61, 107, 0.12);
|
||||
--glow-gold: 0 0 8px rgba(0, 61, 107, 0.12);
|
||||
--gradient-gold: linear-gradient(180deg, #F0F4F8 0%, #FFFFFF 100%);
|
||||
--gradient-gold-border: linear-gradient(180deg, #E5E7EB 0%, #FFFFFF 100%);
|
||||
--gradient-card: linear-gradient(160deg, #FFFFFF 0%, #F5F7FA 100%);
|
||||
--space-card: 16px;
|
||||
--space-section: 12px;
|
||||
--surface-accent: rgba(0, 102, 204, 0.06);
|
||||
--border-gold-soft: rgba(0, 102, 204, 0.2);
|
||||
--player-header-h: 48px;
|
||||
--player-bottom-nav-h: 54px;
|
||||
--safe-top: env(safe-area-inset-top, 0px);
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
/** 金色描边按钮(避免大面积实心填充) */
|
||||
.btn-gold-outline {
|
||||
background: #141414;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
color: var(--primary-light);
|
||||
font-weight: 800;
|
||||
}
|
||||
.btn-gold-outline:active:not(:disabled) {
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-color: var(--border-gold);
|
||||
/* Gold kept only for odds/betting accent as orange-amber */
|
||||
:root {
|
||||
--odds-accent: #F8971F;
|
||||
--odds-accent-light: #FBBF24;
|
||||
--odds-accent-soft: rgba(248, 151, 31, 0.1);
|
||||
--captcha-canvas-bg: #E8EDF2;
|
||||
--captcha-char: #003D6B;
|
||||
--captcha-noise-rgb: 0, 61, 107;
|
||||
}
|
||||
|
||||
/** 选中态:浅底 + 金边,非整块金色 */
|
||||
.tab-gold-active {
|
||||
background: rgba(212, 175, 55, 0.08) !important;
|
||||
border-color: var(--border-gold) !important;
|
||||
color: var(--primary-light) !important;
|
||||
}
|
||||
|
||||
/* 登录等关键区域:轻量金边(非厚重 PS 描边) */
|
||||
.ps-gold-frame {
|
||||
position: relative;
|
||||
border: 1px solid var(--border-gold-soft) !important;
|
||||
border-radius: var(--radius);
|
||||
background: rgba(14, 14, 14, 0.92) !important;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.ps-gold-frame::before,
|
||||
.ps-gold-frame::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ps-gold-input {
|
||||
background: #0d0d0d !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
box-shadow: none;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.ps-gold-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.ps-gold-input:focus {
|
||||
border-color: var(--border-gold-soft) !important;
|
||||
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.12) !important;
|
||||
}
|
||||
|
||||
/* 主按钮:斜面浮雕 + 渐变描边 + 外发光 */
|
||||
.btn-primary {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
padding: 17px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: #3D2800;
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
text-shadow:
|
||||
0 1px 0 rgba(255, 252, 235, 0.95),
|
||||
0 -1px 0 rgba(120, 85, 15, 0.35);
|
||||
transition: transform 0.12s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.btn-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
border-radius: calc(var(--radius-sm) + 3px);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
#FFFCE8 0%,
|
||||
#F7E08A 12%,
|
||||
#D4AF37 35%,
|
||||
#8B6914 55%,
|
||||
#5C4A12 72%,
|
||||
#E8C040 90%,
|
||||
#FFF8DC 100%
|
||||
);
|
||||
z-index: -2;
|
||||
box-shadow:
|
||||
0 0 14px rgba(255, 210, 70, 0.55),
|
||||
0 0 28px rgba(212, 175, 55, 0.28),
|
||||
0 6px 14px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.btn-primary::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 2px;
|
||||
border-radius: calc(var(--radius-sm) - 1px);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
#FFFBE8 0%,
|
||||
#FFE566 6%,
|
||||
#F0D050 22%,
|
||||
#D4AF37 48%,
|
||||
#B8860B 72%,
|
||||
#8B6914 100%
|
||||
);
|
||||
z-index: -1;
|
||||
box-shadow:
|
||||
inset 0 2px 0 rgba(255, 255, 255, 0.95),
|
||||
inset 0 4px 10px rgba(255, 240, 180, 0.45),
|
||||
inset 0 -2px 0 rgba(90, 65, 12, 0.85),
|
||||
inset 0 -6px 14px rgba(45, 32, 6, 0.5);
|
||||
}
|
||||
|
||||
.btn-primary:active:not(:disabled) {
|
||||
transform: translateY(2px) scale(0.985);
|
||||
}
|
||||
|
||||
.btn-primary:active:not(:disabled)::after {
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
#E8C040 0%,
|
||||
#C9962A 40%,
|
||||
#8B6914 100%
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 4px 10px rgba(35, 24, 4, 0.65),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.btn-primary:active:not(:disabled)::before {
|
||||
box-shadow:
|
||||
0 0 10px rgba(255, 200, 50, 0.35),
|
||||
0 2px 6px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
filter: saturate(0.4);
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
/* ── Global resets ── */
|
||||
html, body, #app { height: 100%; }
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
min-height: -webkit-fill-available;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
/* 保留 bg 位图;用 scroll 替代 fixed,减轻移动端滚动重绘(后续可换 WebP/AVIF) */
|
||||
background-color: var(--tertiary);
|
||||
background-image:
|
||||
radial-gradient(ellipse 120% 60% at 50% -10%, rgba(212, 175, 55, 0.14), transparent 55%),
|
||||
linear-gradient(rgba(0, 0, 0, 0.88), rgba(0, 0, 0, 0.88)),
|
||||
url('./assets/images/bg.webp');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
background-attachment: scroll;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: var(--bg-body);
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
background-image: url('./assets/images/pcbg.webp');
|
||||
background-attachment: fixed;
|
||||
background-color: var(--bg-body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,29 +75,25 @@ body {
|
||||
}
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
button { cursor: pointer; border: none; font-family: inherit; }
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* ── Input ── */
|
||||
input {
|
||||
font-family: inherit;
|
||||
background: #0D0D0D;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 14px 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
@@ -265,13 +102,84 @@ input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-text-fill-color: var(--text);
|
||||
caret-color: var(--text);
|
||||
box-shadow: 0 0 0 1000px #0a0a0a inset;
|
||||
box-shadow: 0 0 0 1000px #F0F4F8 inset;
|
||||
border: 1px solid var(--border);
|
||||
transition: background-color 99999s ease-out 0s;
|
||||
}
|
||||
|
||||
/* ── Primary button (blue CTA) ── */
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 48px;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--primary);
|
||||
color: #FFFFFF;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.02em;
|
||||
width: 100%;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
box-shadow: 0 2px 8px rgba(0, 61, 107, 0.2);
|
||||
}
|
||||
.btn-primary:hover { background: var(--primary-light); }
|
||||
.btn-primary:active:not(:disabled) {
|
||||
background: var(--primary-dark);
|
||||
transform: scale(0.985);
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Gold-outline (now blue outline) ── */
|
||||
.btn-gold-outline {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid var(--border-active);
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn-gold-outline:active:not(:disabled) {
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* ── Active tab: light blue ── */
|
||||
.tab-gold-active {
|
||||
background: rgba(0, 102, 204, 0.06) !important;
|
||||
border-color: var(--border-active) !important;
|
||||
color: var(--primary) !important;
|
||||
}
|
||||
|
||||
/* ── Gold frame (now light blue frame) ── */
|
||||
.ps-gold-frame {
|
||||
position: relative;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius);
|
||||
background: #FFFFFF !important;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.ps-gold-frame::before,
|
||||
.ps-gold-frame::after { display: none; }
|
||||
|
||||
.ps-gold-input {
|
||||
background: #F9FAFB !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
box-shadow: none;
|
||||
color: var(--text);
|
||||
}
|
||||
.ps-gold-input::placeholder { color: rgba(0, 0, 0, 0.3); }
|
||||
.ps-gold-input:focus {
|
||||
border-color: var(--border-active) !important;
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1) !important;
|
||||
}
|
||||
|
||||
/* ── Odds button ── */
|
||||
.odds-btn {
|
||||
background: #161616;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 12px 14px;
|
||||
@@ -280,25 +188,14 @@ input:-webkit-autofill:active {
|
||||
min-width: 78px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s;
|
||||
}
|
||||
|
||||
.odds-btn:active { transform: scale(0.96); }
|
||||
|
||||
.odds-btn.selected {
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.12);
|
||||
border: 1px solid var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.odds-btn.selected::before,
|
||||
.odds-btn.selected::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.odds-btn.selected .label,
|
||||
.odds-btn.selected .value {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.odds-btn.selected::after { display: none; }
|
||||
|
||||
.odds-btn .label {
|
||||
font-size: 11px;
|
||||
@@ -307,15 +204,14 @@ input:-webkit-autofill:active {
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.odds-btn .value {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
text-shadow: 0 0 8px rgba(212, 175, 55, 0.5);
|
||||
color: var(--primary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ── Card ── */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
@@ -323,20 +219,20 @@ input:-webkit-autofill:active {
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
box-shadow: var(--shadow);
|
||||
background: rgba(26, 26, 26, 0.92);
|
||||
}
|
||||
|
||||
/* ── Section title ── */
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
padding: 4px 0 4px 12px;
|
||||
border-left: 3px solid var(--border-gold);
|
||||
color: var(--primary-light);
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
border-left: 3px solid var(--primary);
|
||||
color: var(--text);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* ── Empty / loading state ── */
|
||||
.state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -348,4 +244,4 @@ input:-webkit-autofill:active {
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,99 @@
|
||||
/* Hover Cursors and active resets */
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
Desktop Interaction System — Theme-2 Blue-White
|
||||
════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Global Font System ── */
|
||||
/* Heading font: Russo One — titles, labels, headings, status badges, nav, section names */
|
||||
.desktop-shell h1,
|
||||
.desktop-shell h2,
|
||||
.desktop-shell h3,
|
||||
.desktop-shell h4,
|
||||
.desktop-shell h5,
|
||||
.desktop-shell h6,
|
||||
.desktop-shell .page-title,
|
||||
.desktop-shell [class*="-title"]:not(input),
|
||||
.desktop-shell [class*="-header"]:not(input),
|
||||
.desktop-shell [class*="-label"]:not(input),
|
||||
.desktop-shell [class*="-lbl"]:not(input),
|
||||
.desktop-shell [class*="-badge"]:not(input),
|
||||
.desktop-shell [class*="-meta"]:not(input),
|
||||
.desktop-shell [class*="-name"]:not(input),
|
||||
.desktop-shell [class*="-status"]:not(input),
|
||||
.desktop-shell [class*="-pick"]:not(input),
|
||||
.desktop-shell [class*="-section"]:not(input),
|
||||
.desktop-shell [class*="-heading"]:not(input),
|
||||
.desktop-shell [class*="-nav"]:not(input),
|
||||
.desktop-shell [class*="-tab"]:not(input),
|
||||
.desktop-shell [class*="-tag"]:not(input),
|
||||
.desktop-shell [class*="-brand"]:not(input),
|
||||
.desktop-shell [class*="-league"]:not(input),
|
||||
.desktop-shell [class*="-action"]:not(input),
|
||||
.desktop-shell [class*="-chip"]:not(input),
|
||||
.desktop-shell [class*="-scope"]:not(input),
|
||||
.desktop-shell .site-name,
|
||||
.desktop-shell .market-title,
|
||||
.desktop-shell .market-hdr,
|
||||
.desktop-shell .market-filter-label,
|
||||
.desktop-shell .market-filter-chip,
|
||||
.desktop-shell .score-section-title,
|
||||
.desktop-shell .panel-head,
|
||||
.desktop-shell .versus-row,
|
||||
.desktop-shell .kickoff-time,
|
||||
.desktop-shell .locked-lbl,
|
||||
.desktop-shell .lbl,
|
||||
.desktop-shell .label,
|
||||
.desktop-shell .section-hdr,
|
||||
.desktop-shell .leagues-hdr,
|
||||
.desktop-shell .time-btn,
|
||||
.desktop-shell .hero-sub,
|
||||
.desktop-shell .lang-opt,
|
||||
.desktop-shell .hero-caption,
|
||||
.desktop-shell .title,
|
||||
.desktop-shell .saishi,
|
||||
.desktop-shell .nav-link,
|
||||
.desktop-shell .nav-item,
|
||||
.desktop-shell .main-tab,
|
||||
.desktop-shell .name,
|
||||
.desktop-shell .match-name,
|
||||
.desktop-shell .selection-name,
|
||||
.desktop-shell .league-name,
|
||||
.desktop-shell .league-title,
|
||||
.desktop-shell .hub-nav-title,
|
||||
.desktop-shell .desktop-records-status,
|
||||
.desktop-shell .desktop-records-filter-group-label,
|
||||
.desktop-shell .desktop-records-table th,
|
||||
.desktop-shell .data-table th,
|
||||
.desktop-shell .desktop-wallet-subnav-link,
|
||||
.desktop-shell .desktop-records-action-btn {
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
|
||||
/* Data font: Chakra Petch / SF Mono — odds, amounts, balances, stakes, returns, numbers */
|
||||
.desktop-shell [class*="odds"]:not(input),
|
||||
.desktop-shell [class*="amount"]:not(input),
|
||||
.desktop-shell [class*="balance"]:not(input),
|
||||
.desktop-shell [class*="stat-"]:not(input),
|
||||
.desktop-shell [class*="-val"]:not(input),
|
||||
.desktop-shell [class*="-value"]:not(input),
|
||||
.desktop-shell [class*="stake"]:not(input),
|
||||
.desktop-shell [class*="return"]:not(input),
|
||||
.desktop-shell [class*="-num"]:not(input),
|
||||
.desktop-shell [class*="-price"]:not(input),
|
||||
.desktop-shell [class*="-payout"]:not(input),
|
||||
.desktop-shell [class*="-win"]:not(input),
|
||||
.desktop-shell .num-cell,
|
||||
.desktop-shell .id-cell,
|
||||
.desktop-shell .mono,
|
||||
.desktop-shell .currency,
|
||||
.desktop-shell .score,
|
||||
.desktop-shell .val,
|
||||
.desktop-shell .order-no,
|
||||
.desktop-shell .bank-card-number,
|
||||
.desktop-shell .hero-counter {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Hover Cursors and active resets ── */
|
||||
.desktop-shell a,
|
||||
.desktop-shell button,
|
||||
.desktop-shell select,
|
||||
@@ -8,23 +103,54 @@
|
||||
touch-action: auto;
|
||||
}
|
||||
|
||||
/* Scrollbar customizations */
|
||||
/* ── Global focus-visible (accessibility) ── */
|
||||
.desktop-shell :focus-visible {
|
||||
outline: 2px solid var(--border-active);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-xs);
|
||||
transition: outline-offset var(--transition-fast);
|
||||
}
|
||||
|
||||
.desktop-shell input:focus-visible,
|
||||
.desktop-shell textarea:focus-visible,
|
||||
.desktop-shell select:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--border-active) !important;
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.15) !important;
|
||||
}
|
||||
|
||||
/* ── Scrollbar customizations ── */
|
||||
.hide-scrollbar,
|
||||
.no-scrollbar {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar,
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.desktop-shell ::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.desktop-shell ::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.desktop-shell ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.desktop-shell ::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 61, 107, 0.18);
|
||||
border-radius: 3px;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
.desktop-shell ::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-light);
|
||||
background: rgba(0, 61, 107, 0.35);
|
||||
}
|
||||
|
||||
/* Hover highlights */
|
||||
/* ── Hover highlights ── */
|
||||
.desktop-shell .hover-bg:hover {
|
||||
background-color: var(--bg-hover) !important;
|
||||
}
|
||||
@@ -35,62 +161,71 @@
|
||||
}
|
||||
|
||||
.desktop-shell .hover-gold-text:hover {
|
||||
color: var(--primary-light) !important;
|
||||
color: var(--primary-lighter) !important;
|
||||
}
|
||||
|
||||
/* Form elements styling */
|
||||
/* ── Form elements styling ── */
|
||||
.desktop-shell input {
|
||||
padding: 10px 12px;
|
||||
padding: var(--space-4) var(--space-5);
|
||||
font-size: 13px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-xs);
|
||||
transition: border-color var(--transition), box-shadow var(--transition);
|
||||
}
|
||||
|
||||
.desktop-shell input:focus {
|
||||
border-color: var(--border-gold) !important;
|
||||
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.2) !important;
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.15) !important;
|
||||
}
|
||||
|
||||
/* Button hover overrides */
|
||||
/* ── Button hover overrides ── */
|
||||
.desktop-shell .btn-gold-outline {
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
|
||||
}
|
||||
.desktop-shell .btn-gold-outline:hover:not(:disabled) {
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
border-color: var(--border-gold);
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
border-color: var(--border-active);
|
||||
box-shadow: var(--glow-gold);
|
||||
}
|
||||
.desktop-shell .btn-gold-outline:active {
|
||||
transform: none; /* No mobile shrink on PC */
|
||||
transform: scale(0.98); /* subtle press feedback */
|
||||
}
|
||||
|
||||
.desktop-shell .btn-primary {
|
||||
transition: filter 0.2s, transform 0.1s;
|
||||
transition: filter var(--transition), transform var(--transition-fast), box-shadow var(--transition);
|
||||
}
|
||||
.desktop-shell .btn-primary:hover:not(:disabled) {
|
||||
filter: brightness(1.1);
|
||||
filter: brightness(1.08);
|
||||
box-shadow: var(--glow-gold-lg);
|
||||
}
|
||||
.desktop-shell .btn-primary:active {
|
||||
transform: translateY(1px);
|
||||
transform: translateY(1px) scale(0.99);
|
||||
}
|
||||
|
||||
/* Skeleton rows animation for high density PC loaders */
|
||||
/* ── Skeleton loading animation ── */
|
||||
@keyframes desktop-skeleton-glow {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
.desktop-skeleton {
|
||||
background: linear-gradient(90deg, #161616 25%, #222222 37%, #161616 63%);
|
||||
background: linear-gradient(90deg,
|
||||
#E5E7EB 25%,
|
||||
#EFF3F8 37%,
|
||||
#E5E7EB 63%
|
||||
);
|
||||
background-size: 400% 100%;
|
||||
animation: desktop-skeleton-glow 1.4s ease infinite;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* ── Bet locate pulse ── */
|
||||
@keyframes bet-locate-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(212, 175, 55, 0);
|
||||
box-shadow: 0 0 0 0 rgba(0, 102, 204, 0);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.45);
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,5 +233,17 @@
|
||||
animation: bet-locate-pulse 0.6s ease 3;
|
||||
border-color: var(--border-gold) !important;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: var(--z-base);
|
||||
}
|
||||
|
||||
/* ── Reduced motion support ── */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.desktop-shell *,
|
||||
.desktop-shell *::before,
|
||||
.desktop-shell *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,71 @@
|
||||
/* Desktop Global layout structure */
|
||||
/* Desktop Global layout structure — Theme-2 Blue-White */
|
||||
.desktop-shell {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #000;
|
||||
background-image: url('../../assets/images/pcbg.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
background-color: var(--desktop-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Ambient gradient overlay for depth (light blue tint) */
|
||||
.desktop-shell::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 800px 500px at 0% 0%, rgba(0, 91, 159, 0.04), transparent 60%),
|
||||
radial-gradient(ellipse 600px 400px at 100% 100%, rgba(0, 102, 204, 0.03), transparent 60%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.desktop-header-wrap {
|
||||
flex-shrink: 0;
|
||||
height: var(--desktop-header-h);
|
||||
background: #111111;
|
||||
background: var(--glass-bg-strong);
|
||||
backdrop-filter: var(--blur-md);
|
||||
-webkit-backdrop-filter: var(--blur-md);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 120;
|
||||
z-index: var(--z-header);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
padding: 0 var(--space-7);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
.desktop-marquee-wrap {
|
||||
flex-shrink: 0;
|
||||
z-index: 110;
|
||||
z-index: var(--z-sticky);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #111;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: var(--blur-sm);
|
||||
-webkit-backdrop-filter: var(--blur-sm);
|
||||
}
|
||||
|
||||
.desktop-shell.betslip-hidden {
|
||||
--desktop-right-betslip-w: 0px;
|
||||
}
|
||||
|
||||
.desktop-shell.betslip-collapsed {
|
||||
--desktop-right-betslip-w: var(--desktop-right-betslip-collapsed-w);
|
||||
}
|
||||
|
||||
.desktop-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: var(--z-base);
|
||||
}
|
||||
|
||||
.desktop-main-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
@@ -51,14 +82,16 @@
|
||||
.desktop-betting-sports-row {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
padding: 0 var(--space-6);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(14, 14, 14, 0.85);
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: var(--blur-sm);
|
||||
-webkit-backdrop-filter: var(--blur-sm);
|
||||
}
|
||||
|
||||
.desktop-layout-betting {
|
||||
display: grid;
|
||||
grid-template-columns: var(--desktop-left-sidebar-w) 1fr var(--desktop-right-betslip-w);
|
||||
grid-template-columns: var(--desktop-left-sidebar-w) 1fr;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
@@ -66,13 +99,13 @@
|
||||
|
||||
.desktop-betting-left {
|
||||
border-right: 1px solid var(--border);
|
||||
background: rgba(14, 14, 14, 0.6);
|
||||
background: var(--desktop-sidebar-bg);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.desktop-betting-center {
|
||||
overflow-y: auto;
|
||||
padding: 14px 16px;
|
||||
padding: var(--space-6) var(--space-6);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -80,12 +113,6 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.desktop-betting-right {
|
||||
border-left: 1px solid var(--border);
|
||||
background: rgba(14, 14, 14, 0.85);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Account Layout: full-width records center */
|
||||
.desktop-layout-account {
|
||||
display: flex;
|
||||
@@ -101,8 +128,10 @@
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 28px 40px 40px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: var(--space-9) var(--space-11) var(--space-12);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.desktop-account-center > .desktop-records-page {
|
||||
@@ -132,7 +161,7 @@
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
padding: var(--space-7);
|
||||
}
|
||||
|
||||
.desktop-marketing-content {
|
||||
@@ -143,23 +172,31 @@
|
||||
/* Hub Layout: Left Sidebar (List) + Right Outlet (Detail) */
|
||||
.desktop-layout-hub {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
grid-template-columns: 360px 1fr;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.desktop-hub-left {
|
||||
border-right: 1px solid var(--border);
|
||||
background: rgba(14, 14, 14, 0.6);
|
||||
background: var(--desktop-sidebar-bg);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.desktop-hub-left > * {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.desktop-hub-right {
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background: rgba(10, 10, 10, 0.3);
|
||||
padding: var(--space-8);
|
||||
background: var(--desktop-bg);
|
||||
}
|
||||
|
||||
/* Auth Layout: Centered container */
|
||||
@@ -170,15 +207,17 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 40px;
|
||||
padding: var(--space-11);
|
||||
}
|
||||
|
||||
.desktop-auth-card {
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
padding: 30px;
|
||||
background: rgba(20, 20, 20, 0.95);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
padding: var(--space-9);
|
||||
background: var(--glass-bg-strong);
|
||||
backdrop-filter: var(--blur-lg);
|
||||
-webkit-backdrop-filter: var(--blur-lg);
|
||||
border: 1px solid var(--border-active);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
/* PC account records: bets, wallet, recharge, cashbacks */
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
PC Account Records — Theme-2 Blue-White
|
||||
Bets, wallet, recharge, cashbacks
|
||||
════════════════════════════════════════════════════════════ */
|
||||
.desktop-records-page {
|
||||
width: 100%;
|
||||
max-width: 1480px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: var(--space-6);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
@@ -16,10 +19,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px 32px;
|
||||
gap: var(--space-7) var(--space-11);
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
padding-bottom: 14px;
|
||||
padding-bottom: var(--space-5);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -34,7 +37,7 @@
|
||||
.desktop-records-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: var(--space-4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -42,27 +45,28 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 18px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
padding: var(--space-4) var(--space-7);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
transition: background var(--transition), border-color var(--transition), box-shadow var(--transition);
|
||||
}
|
||||
|
||||
.desktop-records-action-btn:hover {
|
||||
background: rgba(212, 175, 55, 0.14);
|
||||
border-color: var(--border-gold);
|
||||
background: rgba(0, 102, 204, 0.14);
|
||||
border-color: var(--border-active);
|
||||
box-shadow: var(--glow-gold);
|
||||
}
|
||||
|
||||
.desktop-records-filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -70,14 +74,14 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px 14px;
|
||||
gap: var(--space-4) var(--space-6);
|
||||
}
|
||||
|
||||
.desktop-records-filter-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.desktop-records-filter-group-label {
|
||||
@@ -92,73 +96,76 @@
|
||||
.desktop-records-filter-divider {
|
||||
width: 1px;
|
||||
height: 22px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
background: var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.desktop-records-filter-chip {
|
||||
padding: 7px 16px;
|
||||
border-radius: 6px;
|
||||
padding: 7px var(--space-6);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: var(--bg-hover);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s, border-color 0.2s, background 0.2s;
|
||||
transition: color var(--transition), border-color var(--transition), background var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.desktop-records-filter-chip:hover {
|
||||
color: #fff;
|
||||
border-color: rgba(212, 175, 55, 0.25);
|
||||
color: var(--text);
|
||||
border-color: rgba(0, 102, 204, 0.25);
|
||||
}
|
||||
|
||||
.desktop-records-filter-chip.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-color: rgba(212, 175, 55, 0.35);
|
||||
background: rgba(0, 102, 204, 0.1);
|
||||
border-color: rgba(0, 102, 204, 0.35);
|
||||
box-shadow: 0 0 0 1px rgba(0, 102, 204, 0.1);
|
||||
}
|
||||
|
||||
.desktop-wallet-subnav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.desktop-wallet-subnav-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
padding: var(--space-4) var(--space-6);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||
transition: color var(--transition), background var(--transition), border-color var(--transition);
|
||||
}
|
||||
|
||||
.desktop-wallet-subnav-link:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.desktop-wallet-subnav-link.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-color: rgba(212, 175, 55, 0.28);
|
||||
background: rgba(0, 102, 204, 0.1);
|
||||
border-color: rgba(0, 102, 204, 0.28);
|
||||
}
|
||||
|
||||
/* ── Records Panel ── */
|
||||
.desktop-records-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: rgba(18, 18, 18, 0.94);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.desktop-records-table-wrap {
|
||||
@@ -173,19 +180,23 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Table Header: glassmorphism + heading font ── */
|
||||
.desktop-records-table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
padding: 12px 16px;
|
||||
z-index: var(--z-sticky);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
background: #121212;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--primary);
|
||||
background: var(--glass-bg-strong);
|
||||
backdrop-filter: var(--blur-md);
|
||||
-webkit-backdrop-filter: var(--blur-md);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: 2px solid var(--border-active);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -194,28 +205,44 @@
|
||||
}
|
||||
|
||||
.desktop-records-table td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.desktop-records-table tbody tr:nth-child(even) {
|
||||
background: rgba(255, 255, 255, 0.015);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* ── Row hover: blue left-border indicator ── */
|
||||
.desktop-records-table tbody tr.hover-row {
|
||||
transition: background 0.15s;
|
||||
transition: background var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.desktop-records-table tbody tr.hover-row:hover {
|
||||
background: rgba(212, 175, 55, 0.06);
|
||||
.desktop-records-table tbody tr.clickable-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.desktop-records-table tbody tr.hover-row:hover,
|
||||
.desktop-records-table tbody tr.clickable-row:hover {
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
box-shadow: inset 3px 0 0 var(--primary-lighter);
|
||||
}
|
||||
|
||||
.desktop-records-table .id-cell {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.desktop-records-table .num-cell {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-data);
|
||||
}
|
||||
|
||||
.desktop-records-table .date-cell {
|
||||
@@ -228,29 +255,33 @@
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.desktop-records-table .link-cell:hover {
|
||||
color: var(--primary-lighter);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ── Pagination ── */
|
||||
.desktop-records-pagination {
|
||||
flex-shrink: 0;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(10, 10, 10, 0.35);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--desktop-bg);
|
||||
}
|
||||
|
||||
/* ── Empty / Loading States ── */
|
||||
.desktop-records-loading,
|
||||
.desktop-records-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
gap: var(--space-5);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 48px 24px;
|
||||
padding: var(--space-13) var(--space-9);
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -264,18 +295,18 @@
|
||||
|
||||
.desktop-records-table-empty {
|
||||
text-align: center;
|
||||
padding: 56px 16px !important;
|
||||
padding: var(--space-13) var(--space-6) !important;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.desktop-records-table-empty .empty-hint {
|
||||
margin: 8px 0 0;
|
||||
margin: var(--space-4) 0 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
color: var(--text-muted);
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.desktop-records-loading-cell {
|
||||
@@ -283,27 +314,76 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 280px;
|
||||
padding: 48px 24px;
|
||||
padding: var(--space-13) var(--space-9);
|
||||
}
|
||||
|
||||
/* ── Status Badges: dot indicator + pulse ── */
|
||||
.desktop-records-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
gap: 6px;
|
||||
padding: var(--space-2) var(--space-5);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.desktop-records-status.status-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
||||
.desktop-records-status.status-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
||||
.desktop-records-status.status-pending { background: rgba(212, 175, 55, 0.12); color: var(--primary-light); }
|
||||
.desktop-records-status.status-push { background: rgba(100, 100, 100, 0.12); color: #aaa; }
|
||||
.desktop-records-status.status-cancelled { background: rgba(100, 100, 100, 0.1); color: #777; }
|
||||
.desktop-records-status.status-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||
/* Dot indicator before status text */
|
||||
.desktop-records-status::before {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.desktop-records-amount.pos { color: var(--primary-light); }
|
||||
.desktop-records-amount.neg { color: var(--danger); }
|
||||
.desktop-records-status.status-won {
|
||||
background: rgba(52, 199, 89, 0.12);
|
||||
color: #2BB058;
|
||||
}
|
||||
.desktop-records-status.status-won::before { background: #34C759; }
|
||||
|
||||
.desktop-records-status.status-lost {
|
||||
background: rgba(255, 69, 58, 0.12);
|
||||
color: #E53935;
|
||||
}
|
||||
.desktop-records-status.status-lost::before { background: #FF453A; }
|
||||
|
||||
.desktop-records-status.status-pending {
|
||||
background: rgba(0, 102, 204, 0.12);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
.desktop-records-status.status-pending::before {
|
||||
background: var(--primary-lighter);
|
||||
animation: pulse-dot 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.desktop-records-status.status-push {
|
||||
background: rgba(100, 100, 100, 0.12);
|
||||
color: #888;
|
||||
}
|
||||
.desktop-records-status.status-push::before { background: #aaa; }
|
||||
|
||||
.desktop-records-status.status-cancelled {
|
||||
background: rgba(100, 100, 100, 0.1);
|
||||
color: #777;
|
||||
}
|
||||
.desktop-records-status.status-cancelled::before { background: #999; }
|
||||
|
||||
.desktop-records-status.status-default {
|
||||
background: rgba(100, 100, 100, 0.1);
|
||||
color: #888;
|
||||
}
|
||||
.desktop-records-status.status-default::before { background: #aaa; }
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.3); }
|
||||
}
|
||||
|
||||
/* ── Amount styling ── */
|
||||
.desktop-records-amount.pos { color: var(--primary-light); font-weight: 800; }
|
||||
.desktop-records-amount.neg { color: var(--danger); font-weight: 800; }
|
||||
.desktop-records-amount.zero { color: var(--text-muted); }
|
||||
|
||||
@@ -1,38 +1,123 @@
|
||||
.desktop-shell {
|
||||
/* Color Palette overrides */
|
||||
--primary: #D4AF37;
|
||||
--primary-dark: #A8841F;
|
||||
--primary-light: #F0D875;
|
||||
--secondary: #141414;
|
||||
--tertiary: #0A0A0A;
|
||||
--neutral: #8E8E93;
|
||||
--bg-body: #050505;
|
||||
--bg-card: rgba(20, 20, 20, 0.95);
|
||||
--bg-hover: rgba(38, 38, 38, 0.95);
|
||||
--bg-elevated: rgba(45, 45, 45, 0.95);
|
||||
--text: #FFFFFF;
|
||||
--text-muted: #8E8E93;
|
||||
--border: #262626;
|
||||
--border-gold: #D4AF37;
|
||||
--border-gold-soft: rgba(212, 175, 55, 0.25);
|
||||
--border-w: 1px;
|
||||
--border-w-thick: 1px;
|
||||
--danger: #FF453A;
|
||||
--radius: 4px;
|
||||
--radius-sm: 2px;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.65);
|
||||
--shadow-gold: 0 2px 12px rgba(212, 175, 55, 0.15);
|
||||
--glow-gold: 0 0 6px rgba(212, 175, 55, 0.15);
|
||||
/* ════════════════════════════════════════════════════════════
|
||||
THEME-2 · Pinnacle Blue-White Design System
|
||||
════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* PC Dimensioning */
|
||||
/* ── Font System ── */
|
||||
--font-heading: 'Russo One', 'Chakra Petch', 'Segoe UI', sans-serif;
|
||||
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-mono: 'SF Mono', 'Consolas', 'Monaco', monospace;
|
||||
--font-data: 'Chakra Petch', 'SF Mono', 'Consolas', monospace;
|
||||
|
||||
/* ── Color Palette ── */
|
||||
--primary: #003D6B;
|
||||
--primary-dark: #002847;
|
||||
--primary-light: #005B9F;
|
||||
--primary-lighter: #0078D4;
|
||||
--secondary: #E8EDF2;
|
||||
--tertiary: #F5F7FA;
|
||||
--neutral: #6B7280;
|
||||
--bg-body: #F5F7FA;
|
||||
--bg-card: #FFFFFF;
|
||||
--bg-hover: #F0F4F8;
|
||||
--bg-elevated: #FFFFFF;
|
||||
--text: #1A1A2E;
|
||||
--text-muted: #6B7280;
|
||||
--text-light: #9CA3AF;
|
||||
--border: #E5E7EB;
|
||||
--border-active: #0066CC;
|
||||
--border-soft: rgba(0, 102, 204, 0.12);
|
||||
|
||||
/* ── Blue Accent Tokens (mapped from gold) ── */
|
||||
--border-gold: #0066CC;
|
||||
--border-gold-soft: rgba(0, 102, 204, 0.2);
|
||||
--shadow-gold: 0 2px 12px rgba(0, 61, 107, 0.1);
|
||||
--shadow-gold-lg: 0 8px 32px rgba(0, 61, 107, 0.16), 0 2px 8px rgba(0, 102, 204, 0.06);
|
||||
--glow-gold: 0 0 6px rgba(0, 102, 204, 0.12);
|
||||
--glow-gold-lg: 0 0 16px rgba(0, 102, 204, 0.2), 0 0 4px rgba(0, 102, 204, 0.1);
|
||||
--odds-accent: #F8971F;
|
||||
--odds-glow: 0 0 10px rgba(248, 151, 31, 0.35);
|
||||
|
||||
/* ── Spacing Scale (8dp rhythm) ── */
|
||||
--space-0: 0px;
|
||||
--space-1: 2px;
|
||||
--space-2: 4px;
|
||||
--space-3: 6px;
|
||||
--space-4: 8px;
|
||||
--space-5: 12px;
|
||||
--space-6: 16px;
|
||||
--space-7: 20px;
|
||||
--space-8: 24px;
|
||||
--space-9: 28px;
|
||||
--space-10: 32px;
|
||||
--space-11: 40px;
|
||||
--space-12: 48px;
|
||||
--space-13: 56px;
|
||||
--space-14: 64px;
|
||||
|
||||
/* ── Radius Scale ── */
|
||||
--radius: 8px;
|
||||
--radius-sm: 6px;
|
||||
--radius-xs: 4px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-2xl: 20px;
|
||||
--radius-full: 999px;
|
||||
|
||||
/* ── Shadow Scale (light-mode tuned) ── */
|
||||
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
--shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.1), 0 4px 12px rgba(0, 0, 0, 0.04);
|
||||
--shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.14), 0 6px 18px rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* ── Blur / Glassmorphism (light-mode tuned) ── */
|
||||
--blur-sm: blur(6px);
|
||||
--blur-md: blur(10px);
|
||||
--blur-lg: blur(16px);
|
||||
--glass-bg: rgba(255, 255, 255, 0.88);
|
||||
--glass-bg-strong: rgba(255, 255, 255, 0.95);
|
||||
--glass-border: rgba(255, 255, 255, 0.6);
|
||||
|
||||
/* ── Transition Tokens ── */
|
||||
--transition-fast: 0.12s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-slow: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-bounce: 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
--transition-spring: 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
|
||||
/* ── Z-Index Scale ── */
|
||||
--z-base: 1;
|
||||
--z-sticky: 10;
|
||||
--z-dropdown: 100;
|
||||
--z-header: 120;
|
||||
--z-modal: 1000;
|
||||
--z-toast: 2000;
|
||||
|
||||
/* ── Desktop Layout Dimensions ── */
|
||||
--desktop-header-h: 56px;
|
||||
--desktop-left-sidebar-w: 220px;
|
||||
--desktop-right-betslip-w: 288px;
|
||||
|
||||
/* Reset typography inside desktop */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--desktop-right-betslip-expanded-w: 288px;
|
||||
--desktop-right-betslip-collapsed-w: 40px;
|
||||
|
||||
/* ── Desktop Background ── */
|
||||
--desktop-bg: #F5F7FA;
|
||||
--desktop-border: #E5E7EB;
|
||||
--desktop-sidebar-bg: #FFFFFF;
|
||||
|
||||
/* ── Borders ── */
|
||||
--border-w: 1px;
|
||||
--border-w-thick: 1px;
|
||||
--danger: #DC2626;
|
||||
--success: #34C759;
|
||||
|
||||
/* ── Typography ── */
|
||||
font-family: var(--font-body);
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
line-height: 1.5;
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
63
apps/player/src/utils/depositOrderLookup.ts
Normal file
63
apps/player/src/utils/depositOrderLookup.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import api from '../api';
|
||||
|
||||
export type DepositAuditLog = {
|
||||
id: string;
|
||||
action: string;
|
||||
actorType: string;
|
||||
statusBefore: string | null;
|
||||
statusAfter: string;
|
||||
amount: string | null;
|
||||
approvedAmount: string | null;
|
||||
remark: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type PlayerDepositOrder = {
|
||||
id: string;
|
||||
orderNo: string;
|
||||
paymentMethodId?: string;
|
||||
methodType: string;
|
||||
amount: string;
|
||||
status: string;
|
||||
approvedAmount: string | null;
|
||||
rejectReason: string | null;
|
||||
remark: string | null;
|
||||
createdAt: string;
|
||||
reviewedAt: string | null;
|
||||
paymentMethodName: string | null;
|
||||
bankName?: string | null;
|
||||
auditLogs?: DepositAuditLog[];
|
||||
};
|
||||
|
||||
export async function fetchDepositOrderAuditLogs(orderId: string): Promise<DepositAuditLog[]> {
|
||||
try {
|
||||
const { data } = await api.get(`/player/deposit-orders/${orderId}/audit-logs`);
|
||||
const items = data.data?.items ?? data.data ?? [];
|
||||
return Array.isArray(items) ? (items as DepositAuditLog[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function enrichDepositOrder(order: PlayerDepositOrder): Promise<PlayerDepositOrder> {
|
||||
if (order.auditLogs?.length) return order;
|
||||
const auditLogs = await fetchDepositOrderAuditLogs(order.id);
|
||||
return auditLogs.length ? { ...order, auditLogs } : order;
|
||||
}
|
||||
|
||||
export async function findDepositOrderById(orderId: string): Promise<PlayerDepositOrder | null> {
|
||||
let page = 1;
|
||||
const pageSize = 20;
|
||||
|
||||
while (true) {
|
||||
const { data } = await api.get('/player/deposit-orders', { params: { page, pageSize } });
|
||||
const result = data.data ?? { items: [], total: 0, pageSize };
|
||||
const items = (result.items ?? []) as PlayerDepositOrder[];
|
||||
const found = items.find((order) => order.id === orderId);
|
||||
if (found) return enrichDepositOrder(found);
|
||||
|
||||
const total = result.total ?? 0;
|
||||
if (!items.length || page * pageSize >= total) return null;
|
||||
page += 1;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useViewport } from '../composables/useViewport';
|
||||
import MobileBetDetailView from './MobileBetDetailView.vue';
|
||||
import DesktopBetDetailView from './desktop/DesktopBetDetailView.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { isDesktop } = useViewport();
|
||||
|
||||
watch(
|
||||
() => [isDesktop.value, route.params.betNo] as const,
|
||||
([desktop, betNo]) => {
|
||||
if (desktop && betNo) {
|
||||
router.replace({ path: '/bets', query: { betNo: String(betNo) } });
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopBetDetailView v-if="isDesktop" />
|
||||
<MobileBetDetailView v-else />
|
||||
<MobileBetDetailView v-if="!isDesktop" />
|
||||
</template>
|
||||
|
||||
@@ -7,7 +7,10 @@ import { useSmsCode } from '../composables/useSmsCode';
|
||||
import api from '../api';
|
||||
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
||||
import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
|
||||
import loginBg from '../assets/images/h5bg.webp';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import loginBg from '../assets/images/zt2.webp';
|
||||
|
||||
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
@@ -98,11 +101,16 @@ function goLogin() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page" :style="{ backgroundImage: `url(${loginBg})` }">
|
||||
<div class="login-lang">
|
||||
<div class="login-page" :class="{ 'login-page--desktop': props.desktop }" :style="props.desktop ? undefined : { backgroundImage: `url(${loginBg})` }">
|
||||
<div v-if="!props.desktop" class="login-lang">
|
||||
<LocaleSwitcher compact />
|
||||
</div>
|
||||
<form @submit.prevent="submit" class="login-form ps-gold-frame">
|
||||
<div class="login-stack">
|
||||
<form @submit.prevent="submit" class="login-form" :class="{ 'ps-gold-frame': !props.desktop, 'is-busy': loading }">
|
||||
<div v-if="loading" class="form-busy-overlay" aria-hidden="true">
|
||||
<GoldSpinner :size="40" :active="true" />
|
||||
<p class="form-busy-text">{{ t('auth.resetting_password') }}</p>
|
||||
</div>
|
||||
<h2 class="form-title">{{ t('auth.forgot_password_title') }}</h2>
|
||||
|
||||
<div class="field">
|
||||
@@ -111,10 +119,11 @@ function goLogin() {
|
||||
<PhoneCountrySelect v-model="countryIso" />
|
||||
<input
|
||||
v-model="phone"
|
||||
class="field-input"
|
||||
class="field-input ps-gold-input"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel-national"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.phone_local_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
@@ -125,16 +134,17 @@ function goLogin() {
|
||||
<div class="inline-row">
|
||||
<input
|
||||
v-model="smsCode"
|
||||
class="field-input"
|
||||
class="field-input ps-gold-input"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.sms_code_placeholder')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
:disabled="sending || countdown > 0 || !phone.trim()"
|
||||
:disabled="sending || countdown > 0 || !phone.trim() || loading"
|
||||
@click="sendCode"
|
||||
>
|
||||
{{ countdown > 0 ? `${countdown}s` : t('auth.send_sms') }}
|
||||
@@ -146,11 +156,12 @@ function goLogin() {
|
||||
<label>{{ t('auth.password') }}</label>
|
||||
<input
|
||||
v-model="password"
|
||||
class="field-input"
|
||||
class="field-input ps-gold-input"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.password_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
@@ -159,23 +170,35 @@ function goLogin() {
|
||||
<label>{{ t('auth.confirm_password') }}</label>
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
class="field-input"
|
||||
class="field-input ps-gold-input"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="fieldError()" class="error">{{ fieldError() }}</p>
|
||||
|
||||
<button type="submit" class="btn-login btn-gold-outline" :disabled="loading">
|
||||
{{ t('auth.reset_password_btn') }}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-login btn-gold-outline"
|
||||
:class="{ 'is-loading': loading }"
|
||||
:disabled="loading"
|
||||
:aria-busy="loading"
|
||||
>
|
||||
<span v-if="loading" class="btn-inline-loading">
|
||||
<GoldSpinner :size="18" :active="true" />
|
||||
{{ t('auth.resetting_password') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.reset_password_btn') }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn-skip" @click="goLogin">
|
||||
<button type="button" class="btn-skip" :disabled="loading" @click="goLogin">
|
||||
{{ t('auth.back_to_login') }}
|
||||
</button>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -184,7 +207,7 @@ function goLogin() {
|
||||
position: absolute;
|
||||
top: max(12px, env(safe-area-inset-top));
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
@@ -197,29 +220,144 @@ function goLogin() {
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 30vh 16px calc(10vh + max(20px, env(safe-area-inset-bottom)));
|
||||
background-color: var(--tertiary);
|
||||
background-color: var(--bg-body);
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.login-page--desktop {
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-stack {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-stack {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.login-form.is-busy {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-busy-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border-radius: inherit;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(4px);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.form-busy-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.btn-inline-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-form {
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-page--desktop .form-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-page--desktop .field-input,
|
||||
.login-page--desktop .btn-secondary {
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login {
|
||||
margin-top: 10px;
|
||||
min-height: 50px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip:hover:not(:disabled) {
|
||||
border-color: var(--border-active);
|
||||
background: var(--surface-accent);
|
||||
}
|
||||
|
||||
.login-page--desktop .error {
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border-left: 2px solid var(--danger);
|
||||
}
|
||||
|
||||
.btn-login.is-loading:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -239,19 +377,19 @@ label {
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #0d0d0d;
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.field-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.32);
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
outline: none;
|
||||
border-color: #555;
|
||||
border-color: var(--border-gold-soft);
|
||||
}
|
||||
|
||||
.inline-row {
|
||||
@@ -271,7 +409,7 @@ label {
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #141414;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
@@ -279,6 +417,12 @@ label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:not(:disabled):active {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface-accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
@@ -302,14 +446,14 @@ label {
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-skip:active {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.error {
|
||||
|
||||
@@ -8,7 +8,10 @@ import { useAppLocale } from '../composables/useAppLocale';
|
||||
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
||||
import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
|
||||
import RobotVerify from '../components/RobotVerify.vue';
|
||||
import loginBg from '../assets/images/h5bg.webp';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import loginBg from '../assets/images/zt2.webp';
|
||||
|
||||
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
|
||||
|
||||
type LoginMode = 'account' | 'phone';
|
||||
|
||||
@@ -85,19 +88,51 @@ function goRegister() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page" :style="{ backgroundImage: `url(${loginBg})` }">
|
||||
<div class="login-lang">
|
||||
<div class="login-page" :class="{ 'login-page--desktop': props.desktop }" :style="props.desktop ? undefined : { backgroundImage: `url(${loginBg})` }">
|
||||
<div v-if="!props.desktop" class="login-lang">
|
||||
<LocaleSwitcher compact />
|
||||
</div>
|
||||
<form @submit.prevent="submit" class="login-form ps-gold-frame">
|
||||
<div class="login-stack">
|
||||
<form
|
||||
@submit.prevent="submit"
|
||||
class="login-form"
|
||||
:class="{ 'ps-gold-frame': !props.desktop, 'is-busy': loading }"
|
||||
>
|
||||
<div v-if="loading" class="form-busy-overlay" aria-hidden="true">
|
||||
<GoldSpinner :size="40" :active="true" />
|
||||
<p class="form-busy-text">{{ t('auth.logging_in') }}</p>
|
||||
</div>
|
||||
<h2 v-if="props.desktop" class="form-title">{{ t('auth.login') }}</h2>
|
||||
<div class="auth-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="auth-tab"
|
||||
:class="{ active: loginMode === 'account' }"
|
||||
:disabled="loading"
|
||||
@click="switchLoginMode('account')"
|
||||
>
|
||||
{{ t('auth.login_by_account') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="auth-tab"
|
||||
:class="{ active: loginMode === 'phone' }"
|
||||
:disabled="loading"
|
||||
@click="switchLoginMode('phone')"
|
||||
>
|
||||
{{ t('auth.login_by_phone') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loginMode === 'account'" class="field">
|
||||
<label>{{ t('auth.username') }}</label>
|
||||
<input
|
||||
v-model="account"
|
||||
class="field-input"
|
||||
class="field-input ps-gold-input"
|
||||
required
|
||||
autocomplete="username"
|
||||
maxlength="32"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.username_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
@@ -108,10 +143,11 @@ function goRegister() {
|
||||
<PhoneCountrySelect v-model="countryIso" />
|
||||
<input
|
||||
v-model="phone"
|
||||
class="field-input"
|
||||
class="field-input ps-gold-input"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel-national"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.phone_local_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
@@ -119,30 +155,42 @@ function goRegister() {
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.password') }}</label>
|
||||
<input v-model="password" class="field-input" type="password" required autocomplete="current-password" />
|
||||
<input v-model="password" class="field-input ps-gold-input" type="password" required autocomplete="current-password" :disabled="loading" />
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-forgot" @click="goForgotPassword">
|
||||
<button type="button" class="btn-forgot" :disabled="loading" @click="goForgotPassword">
|
||||
{{ t('auth.forgot_password') }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn-mode-switch" @click="switchLoginMode(loginMode === 'account' ? 'phone' : 'account')">
|
||||
{{ loginMode === 'account' ? t('auth.login_by_phone') : t('auth.login_by_account') }}
|
||||
</button>
|
||||
|
||||
<RobotVerify ref="captchaRef" />
|
||||
<p v-if="resetSuccess" class="success">{{ t('auth.reset_success') }}</p>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<button type="submit" class="btn-login btn-gold-outline" :disabled="loading">
|
||||
{{ t('auth.login') }}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-login btn-gold-outline"
|
||||
:class="{ 'is-loading': loading }"
|
||||
:disabled="loading"
|
||||
:aria-busy="loading"
|
||||
>
|
||||
<span v-if="loading" class="btn-inline-loading">
|
||||
<GoldSpinner :size="18" :active="true" />
|
||||
{{ t('auth.logging_in') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.login') }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn-skip" @click="goRegister">
|
||||
<button v-if="props.desktop" type="button" class="btn-skip" :disabled="loading" @click="goRegister">
|
||||
{{ t('auth.go_register') }}
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn-skip-light" @click="continueBrowsing">
|
||||
{{ t('auth.continue_browsing') }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-if="!props.desktop" class="login-actions">
|
||||
<button type="button" class="btn-skip" :disabled="loading" @click="goRegister">
|
||||
{{ t('auth.go_register') }}
|
||||
</button>
|
||||
<button type="button" class="btn-skip-light" :disabled="loading" @click="continueBrowsing">
|
||||
{{ t('auth.continue_browsing') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -151,7 +199,7 @@ function goRegister() {
|
||||
position: absolute;
|
||||
top: max(12px, env(safe-area-inset-top));
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
@@ -163,20 +211,196 @@ function goRegister() {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 34vh 20px calc(15vh + max(28px, env(safe-area-inset-bottom)));
|
||||
background-color: var(--tertiary);
|
||||
padding: 34vh 20px max(20px, env(safe-area-inset-bottom));
|
||||
background-color: var(--bg-body);
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.login-page--desktop {
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-stack {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-stack {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.login-form.is-busy {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-busy-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border-radius: inherit;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(4px);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.form-busy-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.btn-inline-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-form {
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
text-align: left;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-page--desktop .field-input {
|
||||
height: 46px;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.login-page--desktop .auth-tabs {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login {
|
||||
margin-top: 10px;
|
||||
min-height: 50px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip:hover:not(:disabled) {
|
||||
border-color: var(--border-active);
|
||||
background: var(--surface-accent);
|
||||
}
|
||||
|
||||
.login-page--desktop .error {
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border-left: 2px solid var(--danger);
|
||||
}
|
||||
|
||||
.login-page--desktop .success {
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(5, 150, 105, 0.08);
|
||||
border-left: 2px solid var(--success);
|
||||
}
|
||||
|
||||
.btn-login.is-loading:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.auth-tab:disabled,
|
||||
.btn-forgot:disabled,
|
||||
.btn-skip:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0 14px 4px;
|
||||
}
|
||||
|
||||
.auth-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.auth-tab {
|
||||
flex: 1;
|
||||
min-height: 34px;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.auth-tab.active {
|
||||
border-color: var(--primary);
|
||||
color: var(--text);
|
||||
background: var(--surface-accent);
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -191,7 +415,7 @@ function goRegister() {
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #0d0d0d;
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
@@ -199,7 +423,7 @@ function goRegister() {
|
||||
|
||||
.field-input:focus {
|
||||
outline: none;
|
||||
border-color: #555;
|
||||
border-color: var(--border-gold-soft);
|
||||
}
|
||||
|
||||
.inline-row {
|
||||
@@ -220,36 +444,20 @@ label {
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.btn-mode-switch {
|
||||
align-self: flex-start;
|
||||
margin: -2px 0 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(240, 216, 117, 0.75);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-forgot {
|
||||
align-self: flex-end;
|
||||
margin: -4px 0 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-forgot:active {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.btn-mode-switch:active {
|
||||
color: rgba(240, 216, 117, 0.95);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
@@ -267,37 +475,34 @@ label {
|
||||
}
|
||||
|
||||
.btn-skip {
|
||||
margin-top: 2px;
|
||||
padding: 8px 14px;
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-skip:active {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-skip-light {
|
||||
position: absolute;
|
||||
bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 8px 16px;
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-skip-light:active {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.error {
|
||||
@@ -308,7 +513,7 @@ label {
|
||||
|
||||
.success {
|
||||
margin: 0;
|
||||
color: #6ee7a0;
|
||||
color: var(--success);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, watch } from 'vue';
|
||||
|
||||
const { embedded = false, desktop = false } = defineProps<{ embedded?: boolean; desktop?: boolean }>();
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||||
@@ -15,24 +13,25 @@ const FALLBACK_IMG = '/uploads/banners/welcome.svg';
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const { hubContentItems, loading, load } = usePlayerHome();
|
||||
|
||||
const { embedded = false } = defineProps<{ embedded?: boolean }>();
|
||||
const { announcementItems, bannerItems, loading, load } = usePlayerHome();
|
||||
|
||||
const announcementId = computed(() => String(route.params.id ?? ''));
|
||||
|
||||
const item = computed<PlayerContentItem | null>(() => {
|
||||
const id = announcementId.value;
|
||||
return hubContentItems.value.find((entry) => entry.id === id) ?? null;
|
||||
return (
|
||||
bannerItems.value.find((entry) => entry.id === id) ??
|
||||
announcementItems.value.find((entry) => entry.id === id) ??
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
const isBanner = computed(() => item.value?.contentType === 'BANNER');
|
||||
|
||||
function goBack() {
|
||||
if (embedded) return;
|
||||
if (window.history.length > 1) {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
router.push('/announcements');
|
||||
router.back();
|
||||
}
|
||||
|
||||
function goList() {
|
||||
@@ -108,21 +107,13 @@ watch(announcementId, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="announce-detail" :class="{ 'announce-detail--embedded': embedded, 'announce-detail--desktop': desktop }">
|
||||
<header v-if="embedded" class="page-header page-header--embedded-detail">
|
||||
<div class="announce-detail" :class="{ 'is-embedded': embedded }">
|
||||
<header v-if="!embedded" class="page-header">
|
||||
<button type="button" class="back-btn" :aria-label="t('announcements.back')" @click="goBack">‹</button>
|
||||
<h1>{{ t('announcements.detail_title') }}</h1>
|
||||
</header>
|
||||
|
||||
<header v-else-if="desktop" class="page-header page-header--desktop">
|
||||
<button type="button" class="back-btn" @click="goBack">
|
||||
<span class="back-icon" aria-hidden="true">‹</span>
|
||||
{{ t('announcements.back') }}
|
||||
</button>
|
||||
<h1>{{ t('announcements.detail_title') }}</h1>
|
||||
</header>
|
||||
|
||||
<header v-else class="page-header">
|
||||
<button type="button" class="back-btn back-btn--icon" :aria-label="t('announcements.back')" @click="goBack">‹</button>
|
||||
<header v-else class="page-header page-header--hub">
|
||||
<h1>{{ t('announcements.detail_title') }}</h1>
|
||||
</header>
|
||||
|
||||
@@ -132,7 +123,7 @@ watch(announcementId, () => {
|
||||
|
||||
<div v-else-if="!item" class="empty">
|
||||
<p>{{ t('announcements.not_found') }}</p>
|
||||
<button type="button" class="link-btn" @click="goList">{{ t('announcements.view_all') }}</button>
|
||||
<button v-if="!embedded" type="button" class="link-btn" @click="goList">{{ t('announcements.view_all') }}</button>
|
||||
</div>
|
||||
|
||||
<article v-else class="detail-article">
|
||||
@@ -176,16 +167,6 @@ watch(announcementId, () => {
|
||||
padding: 0 0 24px;
|
||||
}
|
||||
|
||||
.announce-detail--embedded {
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.announce-detail--desktop {
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -195,68 +176,23 @@ watch(announcementId, () => {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.page-header--desktop {
|
||||
padding: 0 0 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.page-header--embedded-detail {
|
||||
padding: 0 0 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.page-header--embedded-detail h1 {
|
||||
font-size: 16px;
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #141414;
|
||||
color: var(--gold);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
}
|
||||
|
||||
.back-btn--icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-card);
|
||||
color: var(--primary);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.page-header--desktop h1 {
|
||||
font-size: 16px;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary-dark, #002847);
|
||||
}
|
||||
|
||||
.state,
|
||||
@@ -270,13 +206,14 @@ watch(announcementId, () => {
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border: 1px solid var(--border-active);
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
color: var(--gold);
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.detail-article {
|
||||
@@ -284,32 +221,12 @@ watch(announcementId, () => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.announce-detail--embedded .detail-article,
|
||||
.announce-detail--desktop .detail-article {
|
||||
background: rgba(18, 18, 18, 0.92);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px 24px;
|
||||
}
|
||||
|
||||
.detail-hero {
|
||||
margin: 16px -16px 0;
|
||||
padding: 0;
|
||||
background: #080808;
|
||||
background: var(--bg-body);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.announce-detail--embedded .detail-hero,
|
||||
.announce-detail--desktop .detail-hero {
|
||||
margin: 0 0 18px;
|
||||
max-height: min(280px, 36vh);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: #0a0a0a;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.detail-hero img {
|
||||
@@ -320,22 +237,10 @@ watch(announcementId, () => {
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.announce-detail--embedded .detail-hero img,
|
||||
.announce-detail--desktop .detail-hero img {
|
||||
max-height: min(280px, 36vh);
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.detail-body-wrap {
|
||||
padding: 18px 0 0;
|
||||
}
|
||||
|
||||
.announce-detail--embedded .detail-body-wrap,
|
||||
.announce-detail--desktop .detail-body-wrap {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.detail-date {
|
||||
margin: 0 0 10px;
|
||||
font-size: 12px;
|
||||
@@ -347,20 +252,14 @@ watch(announcementId, () => {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.announce-detail--embedded .detail-title,
|
||||
.announce-detail--desktop .detail-title {
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--primary-dark, #002847);
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
color: #d0d0d0;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.detail-body :deep(p) {
|
||||
@@ -379,16 +278,6 @@ watch(announcementId, () => {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.announce-detail--embedded .detail-body :deep(img),
|
||||
.announce-detail--desktop .detail-body :deep(img) {
|
||||
max-height: min(320px, 40vh);
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.detail-body :deep(ul),
|
||||
.detail-body :deep(ol) {
|
||||
margin: 0 0 12px;
|
||||
@@ -425,8 +314,8 @@ watch(announcementId, () => {
|
||||
padding: 11px 14px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--gradient-gold);
|
||||
color: #1a1000;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -434,7 +323,31 @@ watch(announcementId, () => {
|
||||
|
||||
.link-action--outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
color: var(--gold);
|
||||
border: 1px solid var(--border-active);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.announce-detail.is-embedded {
|
||||
padding: 0 4px 24px;
|
||||
}
|
||||
|
||||
.is-embedded .page-header--hub {
|
||||
padding: 0 0 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.is-embedded .page-header--hub h1 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-dark, #002847);
|
||||
}
|
||||
|
||||
.is-embedded .detail-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.is-embedded .detail-body-wrap {
|
||||
padding-top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,41 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated } from 'vue';
|
||||
|
||||
const { embedded = false } = defineProps<{ embedded?: boolean }>();
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePlayerHome, type PlayerContentItem } from '../composables/usePlayerHome';
|
||||
import { usePlayerHome } from '../composables/usePlayerHome';
|
||||
import { stripHtml } from '../utils/html';
|
||||
|
||||
const { embedded = false } = defineProps<{ embedded?: boolean }>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const { hubContentItems, loading, load } = usePlayerHome();
|
||||
const { announcementItems, loading, load } = usePlayerHome();
|
||||
|
||||
const items = computed(() => hubContentItems.value);
|
||||
const activeId = computed(() => String(route.params.id ?? ''));
|
||||
|
||||
function leaveAnnouncementHub() {
|
||||
const prev = (window.history.state as { back?: string } | null)?.back;
|
||||
if (prev && !prev.startsWith('/announcements')) {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
router.push('/');
|
||||
}
|
||||
const items = computed(() => announcementItems.value);
|
||||
|
||||
function goBack() {
|
||||
if (embedded) {
|
||||
leaveAnnouncementHub();
|
||||
return;
|
||||
}
|
||||
if (window.history.length > 1) {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
router.push('/');
|
||||
router.back();
|
||||
}
|
||||
|
||||
function openDetail(id: string) {
|
||||
@@ -47,28 +29,29 @@ function formatDate(createdAt?: string) {
|
||||
return formatLocalMatchDateTime(createdAt, locale.value, { variant: 'full' });
|
||||
}
|
||||
|
||||
function itemTitle(item: PlayerContentItem) {
|
||||
function itemTitle(item: (typeof items.value)[number]) {
|
||||
const title = item.translation?.title?.trim();
|
||||
if (title) return title;
|
||||
return stripHtml(item.translation?.body ?? '') || t('home.announcement_badge');
|
||||
}
|
||||
|
||||
function itemPreview(item: PlayerContentItem) {
|
||||
function itemPreview(item: (typeof items.value)[number]) {
|
||||
const title = item.translation?.title?.trim();
|
||||
const bodyText = stripHtml(item.translation?.body ?? '');
|
||||
if (bodyText && bodyText !== title) return bodyText;
|
||||
return '';
|
||||
}
|
||||
|
||||
function typeLabel(item: PlayerContentItem) {
|
||||
switch (item.contentType) {
|
||||
case 'BANNER':
|
||||
return t('announcements.type_banner');
|
||||
case 'TICKER':
|
||||
return t('announcements.type_ticker');
|
||||
default:
|
||||
return t('announcements.type_notice');
|
||||
function isActive(id: string) {
|
||||
return embedded && String(route.params.id ?? '') === id;
|
||||
}
|
||||
|
||||
function onBack() {
|
||||
if (embedded) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
goBack();
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
@@ -77,17 +60,17 @@ onActivated(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="announce-page" :class="{ 'announce-page--embedded': embedded }">
|
||||
<header v-if="!embedded" class="page-header">
|
||||
<button type="button" class="back-btn" :aria-label="t('announcements.back')" @click="goBack">‹</button>
|
||||
<h1>{{ t('announcements.title') }}</h1>
|
||||
</header>
|
||||
<header v-else class="embedded-header">
|
||||
<button type="button" class="back-btn" @click="goBack">
|
||||
<span class="back-icon" aria-hidden="true">‹</span>
|
||||
<span class="back-label">{{ t('announcements.back') }}</span>
|
||||
<div class="announce-page" :class="{ 'is-embedded': embedded }">
|
||||
<header class="page-header">
|
||||
<button
|
||||
type="button"
|
||||
class="back-btn"
|
||||
:aria-label="t('announcements.back')"
|
||||
@click="onBack"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<span class="embedded-title">{{ t('announcements.title') }}</span>
|
||||
<h1>{{ t('announcements.title') }}</h1>
|
||||
</header>
|
||||
|
||||
<div v-if="loading && !items.length" class="state">
|
||||
@@ -104,14 +87,11 @@ onActivated(() => {
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="list-row"
|
||||
:class="{ 'list-row--active': embedded && activeId === item.id }"
|
||||
:class="{ 'is-active': isActive(item.id) }"
|
||||
@click="openDetail(item.id)"
|
||||
>
|
||||
<span class="row-main">
|
||||
<span class="row-top">
|
||||
<span class="type-badge">{{ typeLabel(item) }}</span>
|
||||
<span class="title">{{ itemTitle(item) }}</span>
|
||||
</span>
|
||||
<span class="title">{{ itemTitle(item) }}</span>
|
||||
<span v-if="itemPreview(item)" class="preview">{{ itemPreview(item) }}</span>
|
||||
<span v-if="item.createdAt" class="date">{{ formatDate(item.createdAt) }}</span>
|
||||
</span>
|
||||
@@ -127,86 +107,6 @@ onActivated(() => {
|
||||
padding: 0 0 24px;
|
||||
}
|
||||
|
||||
.announce-page--embedded {
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.announce-page--embedded .list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.embedded-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.embedded-header .back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #141414;
|
||||
color: var(--gold);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.embedded-header .back-btn:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.back-label {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.embedded-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.announce-page--embedded .list-row {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.announce-page--embedded .list-row:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.list-row--active {
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
box-shadow: inset 3px 0 0 var(--gold);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -216,16 +116,15 @@ onActivated(() => {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.page-header .back-btn {
|
||||
.back-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #141414;
|
||||
color: var(--gold);
|
||||
background: var(--bg-card);
|
||||
color: var(--primary);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -233,7 +132,7 @@ onActivated(() => {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--primary-dark, #002847);
|
||||
}
|
||||
|
||||
.state,
|
||||
@@ -268,36 +167,12 @@ onActivated(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.row-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.type-badge {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(212, 175, 55, 0.12);
|
||||
border: 1px solid rgba(212, 175, 55, 0.22);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
color: var(--primary-dark, #002847);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview {
|
||||
@@ -324,4 +199,101 @@ onActivated(() => {
|
||||
color: var(--text-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Desktop hub sidebar */
|
||||
.announce-page.is-embedded {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 16px 16px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.is-embedded .page-header {
|
||||
flex-shrink: 0;
|
||||
padding: 0 4px 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.is-embedded .page-header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.is-embedded .back-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.is-embedded .list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
margin: 0 -4px;
|
||||
padding: 0 4px 16px;
|
||||
}
|
||||
|
||||
.is-embedded .list-row {
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px 12px;
|
||||
margin-bottom: 4px;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.is-embedded .list-row:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.is-embedded .list-row.is-active {
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
border-color: var(--border-active);
|
||||
}
|
||||
|
||||
.is-embedded .list-row.is-active .title {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.is-embedded .list-row.is-active .chevron {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.is-embedded .title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.is-embedded .preview {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.is-embedded .date {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.is-embedded .chevron {
|
||||
margin-top: 2px;
|
||||
font-size: 16px;
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api from '../api';
|
||||
import { formatMoney, parseAmount } from '../utils/localeDisplay';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import StatusWatermark from '../components/StatusWatermark.vue';
|
||||
@@ -52,8 +52,6 @@ const statusKey = computed(() => {
|
||||
|
||||
const statusLabel = computed(() => t(`history.status_${statusKey.value}`));
|
||||
|
||||
const isSettled = computed(() => statusKey.value !== 'pending');
|
||||
|
||||
const placedDateTime = computed(() => {
|
||||
if (!bet.value) return '';
|
||||
const d = new Date(bet.value.placedAt);
|
||||
@@ -66,53 +64,10 @@ const returnAmount = computed(() => {
|
||||
if (!bet.value) return '';
|
||||
if (statusKey.value === 'won') return formatMoney(bet.value.actualReturn, locale.value);
|
||||
if (statusKey.value === 'pending') return formatMoney(bet.value.potentialReturn, locale.value);
|
||||
if (statusKey.value === 'lost') return formatMoney(-parseAmount(bet.value.stake), locale.value);
|
||||
if (statusKey.value === 'lost') return formatMoney(-parseFloat(String(bet.value.stake ?? 0)), locale.value);
|
||||
return formatMoney(bet.value.actualReturn ?? bet.value.potentialReturn, locale.value);
|
||||
});
|
||||
|
||||
const profitAmount = computed(() => {
|
||||
if (!bet.value || statusKey.value !== 'won') return '';
|
||||
const profit = parseAmount(bet.value.actualReturn) - parseAmount(bet.value.stake);
|
||||
return `+${formatMoney(profit, locale.value)}`;
|
||||
});
|
||||
|
||||
const heroAmount = computed(() => {
|
||||
if (statusKey.value === 'won') return profitAmount.value;
|
||||
return returnAmount.value;
|
||||
});
|
||||
|
||||
const heroTitle = computed(() => {
|
||||
if (statusKey.value === 'won') return t('history.profit');
|
||||
if (statusKey.value === 'pending') return t('history.est_return');
|
||||
return statusLabel.value;
|
||||
});
|
||||
|
||||
const formulaText = computed(() => {
|
||||
if (!bet.value || !oddsText.value || statusKey.value === 'pending') return '';
|
||||
return t('history.stake_to_return', {
|
||||
stake: stakeAmount.value,
|
||||
odds: oddsText.value,
|
||||
amount: returnAmount.value,
|
||||
});
|
||||
});
|
||||
|
||||
const stakeAmount = computed(() =>
|
||||
bet.value ? formatMoney(bet.value.stake, locale.value) : '',
|
||||
);
|
||||
|
||||
const oddsText = computed(() => {
|
||||
const o = bet.value?.totalOdds;
|
||||
if (o == null || o === '' || o === 0) return '';
|
||||
const n = parseFloat(String(o));
|
||||
return Number.isFinite(n) ? n.toFixed(2) : String(o);
|
||||
});
|
||||
|
||||
const betTypeLabel = computed(() => {
|
||||
if (!bet.value) return '';
|
||||
if (bet.value.isParlay) return t('bet.parlay');
|
||||
return bet.value.betType || '';
|
||||
});
|
||||
|
||||
function formatOdds(v: unknown): string {
|
||||
const n = parseFloat(String(v));
|
||||
return isNaN(n) || n <= 0 ? '-' : n.toFixed(2);
|
||||
@@ -161,20 +116,13 @@ const matchTitle = computed(() => {
|
||||
return bet.value.matchTitle;
|
||||
});
|
||||
|
||||
const pickMarket = computed(() => {
|
||||
const myPick = computed(() => {
|
||||
if (!bet.value || bet.value.isParlay) return '';
|
||||
const raw = bet.value.pickLabel ?? '';
|
||||
if (locale.value === 'zh-CN' || !raw) return raw;
|
||||
const ci = raw.indexOf(': ');
|
||||
return ci >= 0 ? raw.slice(0, ci) : raw;
|
||||
});
|
||||
|
||||
const pickSelection = computed(() => {
|
||||
if (!bet.value || bet.value.isParlay) return '';
|
||||
const raw = bet.value.pickLabel ?? '';
|
||||
const ci = raw.indexOf(': ');
|
||||
if (ci < 0) return '';
|
||||
const sel = raw.slice(ci + 2);
|
||||
return locale.value === 'zh-CN' ? sel : translateSel(sel);
|
||||
if (ci < 0) return raw;
|
||||
return raw.slice(0, ci + 2) + translateSel(raw.slice(ci + 2));
|
||||
});
|
||||
|
||||
const matchPhase = computed(
|
||||
@@ -182,20 +130,20 @@ const matchPhase = computed(
|
||||
);
|
||||
|
||||
const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
|
||||
const showPhaseWatermark = computed(
|
||||
() => !isSettled.value && matchPhase.value && matchPhase.value !== 'open',
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="detail-page">
|
||||
<div class="pull-indicator" :style="pullIndicatorStyle()">
|
||||
<div
|
||||
class="pull-indicator"
|
||||
:style="pullIndicatorStyle()"
|
||||
>
|
||||
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
|
||||
</div>
|
||||
|
||||
<!-- back bar -->
|
||||
<div class="top-bar">
|
||||
<button type="button" class="back-btn" @click="router.back()">‹ {{ t('history.back') }}</button>
|
||||
<button class="back-btn" @click="router.back()">‹ {{ t('history.back') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="state">
|
||||
@@ -203,131 +151,113 @@ const showPhaseWatermark = computed(
|
||||
</div>
|
||||
<div v-else-if="notFound || !bet" class="state">{{ t('history.not_found') }}</div>
|
||||
|
||||
<article v-else class="receipt">
|
||||
<!-- result strip -->
|
||||
<div class="receipt-result" :class="statusKey">
|
||||
<div class="result-icon" :class="statusKey">
|
||||
<svg v-if="statusKey === 'won'" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M7 12.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<svg v-else-if="statusKey === 'lost'" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M8 8l8 8M16 8l-8 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
<svg v-else-if="statusKey === 'pending'" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M12 7v5l3 2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1.5" />
|
||||
<path d="M8 12h8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="result-text">
|
||||
<span class="result-label">{{ heroTitle }}</span>
|
||||
<span class="result-amount">{{ heroAmount }}</span>
|
||||
<span v-if="statusKey === 'won'" class="result-sub">
|
||||
{{ t('history.return_incl_stake', { amount: returnAmount }) }}
|
||||
</span>
|
||||
<span v-else-if="formulaText && statusKey !== 'pending'" class="result-sub">{{ formulaText }}</span>
|
||||
<span v-else-if="statusKey === 'pending' && oddsText" class="result-sub">
|
||||
{{ stakeAmount }} × {{ oddsText }}
|
||||
</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<!-- status hero -->
|
||||
<div class="hero" :class="statusKey">
|
||||
<span class="hero-status">{{ statusLabel }}</span>
|
||||
<span class="hero-return">{{ returnAmount }}</span>
|
||||
<span class="hero-return-label">
|
||||
{{ statusKey === 'pending' ? t('history.est_return') : t('history.return') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="receipt-perforation" aria-hidden="true" />
|
||||
|
||||
<!-- body -->
|
||||
<div class="receipt-body">
|
||||
<div class="receipt-meta">
|
||||
<div class="meta-tags">
|
||||
<span v-if="betTypeLabel" class="meta-tag">{{ betTypeLabel }}</span>
|
||||
<span v-if="oddsText" class="meta-tag dim">@{{ oddsText }}</span>
|
||||
<span v-if="bet.isCashbacked" class="meta-tag cashback">{{ t('history.cashbacked') }}</span>
|
||||
</div>
|
||||
<span class="meta-time">{{ placedDateTime }}</span>
|
||||
<!-- match / parlay title -->
|
||||
<section class="section">
|
||||
<div class="section-head">
|
||||
<span class="league-tag">
|
||||
{{ bet.isParlay ? t('history.parlay_league') : (bet.leagueName || t('history.league_default')) }}
|
||||
</span>
|
||||
<span class="placed-time">{{ placedDateTime }}</span>
|
||||
</div>
|
||||
<div class="match-name">{{ matchTitle }}</div>
|
||||
|
||||
<h2 class="match-title">{{ matchTitle }}</h2>
|
||||
|
||||
<!-- single pick -->
|
||||
<div v-if="!bet.isParlay" class="pick-block">
|
||||
<!-- single bet: score comparison -->
|
||||
<div v-if="!bet.isParlay" class="score-block" :class="{ 'score-block--phase': matchPhase && matchPhase !== 'open' }">
|
||||
<StatusWatermark
|
||||
v-if="showPhaseWatermark"
|
||||
v-if="matchPhase && matchPhase !== 'open'"
|
||||
:label="matchPhaseText"
|
||||
:variant="matchPhaseVariant(matchPhase!)"
|
||||
:variant="matchPhaseVariant(matchPhase)"
|
||||
size="md"
|
||||
/>
|
||||
<span class="pick-market">{{ pickMarket }}</span>
|
||||
<div class="pick-line">
|
||||
<span class="pick-selection">{{ pickSelection }}</span>
|
||||
<span v-if="oddsText" class="pick-odds">@{{ oddsText }}</span>
|
||||
<!-- my pick row -->
|
||||
<div class="row-label-val">
|
||||
<span class="row-label">{{ t('history.my_pick') }}</span>
|
||||
<span class="row-val pick">{{ myPick }}</span>
|
||||
</div>
|
||||
<div v-if="bet.matchScore?.ft || bet.matchScore?.ht" class="score-row">
|
||||
<span v-if="bet.matchScore.ft" class="score-pill">
|
||||
<em>{{ t('history.ft') }}</em>{{ bet.matchScore.ft }}
|
||||
</span>
|
||||
<span v-if="bet.matchScore.ht" class="score-pill dim">
|
||||
<em>{{ t('history.ht') }}</em>{{ bet.matchScore.ht }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-else-if="statusKey === 'pending'" class="awaiting">{{ t('history.awaiting_result') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- parlay legs -->
|
||||
<div v-if="bet.isParlay && bet.legs?.length" class="legs-list">
|
||||
<div
|
||||
v-for="(leg, i) in bet.legs"
|
||||
:key="i"
|
||||
class="leg-item"
|
||||
:class="legStatusKey(leg.resultStatus)"
|
||||
>
|
||||
<div class="leg-status-dot" :class="legStatusKey(leg.resultStatus)">
|
||||
<svg v-if="legStatusKey(leg.resultStatus) === 'won'" viewBox="0 0 12 12"><path d="M2 6l3 3 5-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||||
<svg v-else-if="legStatusKey(leg.resultStatus) === 'lost'" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
|
||||
<span v-else>{{ i + 1 }}</span>
|
||||
<!-- scores -->
|
||||
<div v-if="bet.matchScore?.ft || bet.matchScore?.ht" class="score-chips">
|
||||
<div v-if="bet.matchScore.ft" class="score-item">
|
||||
<span class="score-period">{{ t('history.ft') }}</span>
|
||||
<span class="score-value">{{ bet.matchScore.ft }}</span>
|
||||
</div>
|
||||
<div class="leg-content">
|
||||
<div class="leg-head">
|
||||
<div v-if="bet.matchScore.ht" class="score-item">
|
||||
<span class="score-period">{{ t('history.ht') }}</span>
|
||||
<span class="score-value muted">{{ bet.matchScore.ht }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="statusKey === 'pending'" class="no-score">{{ t('history.awaiting_result') }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- parlay legs -->
|
||||
<section v-if="bet.isParlay && bet.legs?.length" class="section">
|
||||
<div class="section-title">{{ t('history.legs') }}</div>
|
||||
<div class="legs-list">
|
||||
<div v-for="(leg, i) in bet.legs" :key="i" class="leg-row" :class="legStatusKey(leg.resultStatus)">
|
||||
<div class="leg-left">
|
||||
<span class="leg-num" :class="legStatusKey(leg.resultStatus)">{{ i + 1 }}</span>
|
||||
<div class="leg-body">
|
||||
<span class="leg-match">{{ leg.matchTitle }}</span>
|
||||
<span class="leg-odds">@{{ formatOdds(leg.odds) }}</span>
|
||||
</div>
|
||||
<span class="leg-pick">{{ leg.marketLabel }} · {{ translateSel(leg.selectionName) }}</span>
|
||||
<div v-if="leg.score?.ft || leg.score?.ht" class="score-row compact">
|
||||
<span v-if="leg.score.ft" class="score-pill sm"><em>{{ t('history.ft') }}</em>{{ leg.score.ft }}</span>
|
||||
<span v-if="leg.score.ht" class="score-pill sm dim"><em>{{ t('history.ht') }}</em>{{ leg.score.ht }}</span>
|
||||
<span class="leg-pick-line">
|
||||
{{ leg.marketLabel }}: {{ translateSel(leg.selectionName) }}
|
||||
</span>
|
||||
<div v-if="leg.score?.ft || leg.score?.ht" class="leg-scores">
|
||||
<span v-if="leg.score.ft">{{ t('history.ft') }} {{ leg.score.ft }}</span>
|
||||
<span v-if="leg.score.ht" class="muted-score">{{ t('history.ht') }} {{ leg.score.ht }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="leg-right">
|
||||
<span class="leg-odds-val">{{ formatOdds(leg.odds) }}</span>
|
||||
<span v-if="leg.resultStatus" class="leg-result-badge" :class="legStatusKey(leg.resultStatus)">
|
||||
{{ t(`history.status_${legStatusKey(leg.resultStatus)}`) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="receipt-perforation" aria-hidden="true" />
|
||||
|
||||
<!-- footer -->
|
||||
<div class="receipt-foot">
|
||||
<div class="foot-row">
|
||||
<span>{{ t('history.stake') }}</span>
|
||||
<span>{{ stakeAmount }}</span>
|
||||
<!-- bet summary -->
|
||||
<section class="section">
|
||||
<div class="section-title">{{ t('history.summary') }}</div>
|
||||
<div class="summary-rows">
|
||||
<div class="sum-row">
|
||||
<span>{{ t('history.stake') }}</span>
|
||||
<span>{{ formatMoney(bet.stake, locale) }}</span>
|
||||
</div>
|
||||
<div v-if="bet.totalOdds" class="sum-row">
|
||||
<span>{{ t('history.odds') }}</span>
|
||||
<span class="odds-val">{{ formatOdds(bet.totalOdds) }}</span>
|
||||
</div>
|
||||
<div class="sum-row">
|
||||
<span>{{ statusKey === 'pending' ? t('history.est_return') : t('history.return') }}</span>
|
||||
<span :class="{ 'amt-won': statusKey === 'won', 'amt-pending': statusKey === 'pending', 'amt-lost': statusKey === 'lost' }">
|
||||
{{ returnAmount }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="sum-row muted">
|
||||
<span>{{ t('history.bet_no') }}</span>
|
||||
<span class="bet-no-val">{{ bet.betNo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bet.totalOdds" class="foot-row">
|
||||
<span>{{ t('history.odds') }}</span>
|
||||
<span class="gold">{{ formatOdds(bet.totalOdds) }}</span>
|
||||
</div>
|
||||
<div class="foot-row id">
|
||||
<span>{{ t('history.bet_no') }}</span>
|
||||
<span class="mono">{{ bet.betNo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-page {
|
||||
padding-bottom: 36px;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.pull-indicator {
|
||||
@@ -339,396 +269,361 @@ const showPhaseWatermark = computed(
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
padding: 4px 0 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── receipt ticket ── */
|
||||
.receipt {
|
||||
position: relative;
|
||||
border-radius: var(--radius);
|
||||
background: rgba(18, 18, 18, 0.96);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* result strip */
|
||||
.receipt-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px 18px 18px;
|
||||
}
|
||||
|
||||
.receipt-result.won {
|
||||
background: linear-gradient(135deg, rgba(52, 199, 89, 0.1) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.receipt-result.lost {
|
||||
background: linear-gradient(135deg, rgba(255, 69, 58, 0.08) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.receipt-result.pending {
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.08) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.result-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.result-icon.won { color: #34c759; }
|
||||
.result-icon.lost { color: var(--danger); }
|
||||
.result-icon.pending { color: var(--primary-light); }
|
||||
.result-icon.push { color: var(--text-muted); }
|
||||
|
||||
.result-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
/* ── hero ── */
|
||||
.hero {
|
||||
border-radius: 12px;
|
||||
padding: 20px 20px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.04em;
|
||||
.hero.won { background: rgba(5, 150, 105, 0.08); border: 1px solid rgba(5, 150, 105, 0.2); }
|
||||
.hero.lost { background: rgba(220, 38, 38, 0.06); border: 1px solid rgba(220, 38, 38, 0.15); }
|
||||
.hero.pending { background: rgba(248, 151, 31, 0.06); border: 1px solid rgba(248, 151, 31, 0.18); }
|
||||
.hero.push { background: #FFFFFF; border: 1px solid #E5E7EB; }
|
||||
|
||||
.hero-status {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.hero.won .hero-status { color: #059669; }
|
||||
.hero.lost .hero-status { color: #DC2626; }
|
||||
.hero.pending .hero-status { color: #F8971F; }
|
||||
.hero.push .hero-status { color: #6B7280; }
|
||||
|
||||
.result-amount {
|
||||
font-size: 32px;
|
||||
.hero-return {
|
||||
font-size: 36px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.02em;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.hero.won .hero-return { color: #059669; }
|
||||
.hero.lost .hero-return { color: #DC2626; }
|
||||
.hero.pending .hero-return { color: #F8971F; }
|
||||
.hero.push .hero-return { color: #6B7280; }
|
||||
|
||||
.receipt-result.won .result-amount { color: #34c759; }
|
||||
.receipt-result.lost .result-amount { color: var(--danger); }
|
||||
.receipt-result.pending .result-amount { color: var(--primary-light); }
|
||||
.receipt-result.push .result-amount { color: var(--text-muted); }
|
||||
|
||||
.result-sub {
|
||||
font-size: 12px;
|
||||
.hero-return-label {
|
||||
font-size: 10.5px;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* perforated divider */
|
||||
.receipt-perforation {
|
||||
height: 1px;
|
||||
margin: 0 14px;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
transparent 0,
|
||||
transparent 4px,
|
||||
rgba(255, 255, 255, 0.08) 4px,
|
||||
rgba(255, 255, 255, 0.08) 8px
|
||||
);
|
||||
position: relative;
|
||||
/* ── sections ── */
|
||||
.section {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.receipt-perforation::before,
|
||||
.receipt-perforation::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-body, #000);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.receipt-perforation::before { left: -20px; }
|
||||
.receipt-perforation::after { right: -20px; }
|
||||
|
||||
/* body */
|
||||
.receipt-body {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.receipt-meta {
|
||||
.section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.meta-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.meta-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
}
|
||||
|
||||
.meta-tag.dim {
|
||||
color: var(--text-muted);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.meta-tag.cashback {
|
||||
color: #f0b90b;
|
||||
background: rgba(240, 185, 11, 0.1);
|
||||
border-color: rgba(240, 185, 11, 0.25);
|
||||
}
|
||||
|
||||
.meta-time {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: #555;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.match-title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* pick block */
|
||||
.pick-block {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 14px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.pick-market {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.04em;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.pick-line {
|
||||
.league-tag {
|
||||
font-size: 11px;
|
||||
color: #6B7280;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.placed-time {
|
||||
font-size: 11px;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.match-name {
|
||||
font-size: 17px;
|
||||
font-weight: 900;
|
||||
color: #1A1A2E;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* score block */
|
||||
.score-block {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.score-block--phase {
|
||||
padding: 8px 0 4px;
|
||||
}
|
||||
|
||||
.row-label-val {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pick-selection {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
.row-label {
|
||||
font-size: 11px;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pick-odds {
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
color: var(--text-muted);
|
||||
.row-val {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.score-row {
|
||||
.row-val.pick {
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.score-chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.score-row.compact {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.score-pill {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
.score-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
background: #F5F7FA;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 7px;
|
||||
padding: 5px 10px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.score-pill em {
|
||||
font-style: normal;
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.06em;
|
||||
.score-period {
|
||||
font-size: 10px;
|
||||
color: #6B7280;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.score-pill.dim { color: var(--text-muted); }
|
||||
.score-pill.sm { font-size: 11px; padding: 3px 8px; }
|
||||
.score-value {
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.awaiting {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
.score-value.muted {
|
||||
color: #6B7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.no-score {
|
||||
font-size: 11.5px;
|
||||
color: #6B7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* section title */
|
||||
.section-title {
|
||||
font-size: 10.5px;
|
||||
color: #6B7280;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* parlay legs */
|
||||
.legs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.leg-item {
|
||||
.leg-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
background: #F5F7FA;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.leg-item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.leg-row.won { border-color: rgba(5, 150, 105, 0.25); }
|
||||
.leg-row.lost { border-color: rgba(220, 38, 38, 0.2); }
|
||||
|
||||
.leg-status-dot {
|
||||
flex-shrink: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
.leg-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1.5px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.leg-status-dot svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.leg-status-dot.won {
|
||||
background: rgba(52, 199, 89, 0.12);
|
||||
border-color: rgba(52, 199, 89, 0.45);
|
||||
color: #34c759;
|
||||
}
|
||||
|
||||
.leg-status-dot.lost {
|
||||
background: rgba(255, 69, 58, 0.1);
|
||||
border-color: rgba(255, 69, 58, 0.35);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.leg-content {
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.leg-head {
|
||||
.leg-num {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #E8EDF2;
|
||||
color: #6B7280;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.leg-num.won { background: rgba(5, 150, 105, 0.12); color: #059669; }
|
||||
.leg-num.lost { background: rgba(220, 38, 38, 0.1); color: #DC2626; }
|
||||
|
||||
.leg-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.leg-match {
|
||||
font-size: 13px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.leg-odds {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.leg-pick {
|
||||
font-size: 12px;
|
||||
.leg-pick-line {
|
||||
font-size: 11.5px;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* footer */
|
||||
.receipt-foot {
|
||||
padding: 14px 18px 16px;
|
||||
.leg-scores {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.foot-row {
|
||||
.leg-scores span {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.leg-scores .muted-score {
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.leg-right {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.leg-odds-val {
|
||||
font-size: 15px;
|
||||
font-weight: 900;
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.leg-result-badge {
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.leg-result-badge.won { color: #059669; background: rgba(5, 150, 105, 0.1); }
|
||||
.leg-result-badge.lost { color: #DC2626; background: rgba(220, 38, 38, 0.08); }
|
||||
.leg-result-badge.push { color: #6B7280; background: #E8EDF2; }
|
||||
.leg-result-badge.pending { color: #6B7280; background: #F5F7FA; }
|
||||
|
||||
/* summary */
|
||||
.summary-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.sum-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 7px 0;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
color: #1A1A2E;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.foot-row span:last-child {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
.sum-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.foot-row .gold {
|
||||
color: var(--primary-light);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.foot-row.id {
|
||||
margin-top: 4px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
.sum-row.muted {
|
||||
color: #6B7280;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.foot-row.id span:last-child {
|
||||
color: #555;
|
||||
font-weight: 600;
|
||||
.odds-val {
|
||||
color: #F8971F;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: ui-monospace, monospace;
|
||||
letter-spacing: 0.02em;
|
||||
.amt-won {
|
||||
color: #059669;
|
||||
font-weight: 900;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.amt-pending {
|
||||
color: #F8971F;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.amt-lost {
|
||||
color: #DC2626;
|
||||
font-weight: 900;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.bet-no-val {
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -202,7 +202,7 @@ const pullIndicatorStyle = () => ({
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary-light, #d4af37);
|
||||
color: #003D6B;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
padding: 4px 0 8px;
|
||||
@@ -226,7 +226,7 @@ const pullIndicatorStyle = () => ({
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
padding: 56px 20px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
@@ -243,29 +243,29 @@ const pullIndicatorStyle = () => ({
|
||||
gap: 4px;
|
||||
padding: 18px 16px 16px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: var(--radius, 12px);
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.14), rgba(26, 26, 26, 0.92));
|
||||
border: 1px solid rgba(212, 175, 55, 0.28);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(0, 61, 107, 0.06), #FFFFFF);
|
||||
border: 1px solid #E5E7EB;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.hero-amount {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: #f0b90b;
|
||||
color: #003D6B;
|
||||
line-height: 1.2;
|
||||
text-shadow: 0 0 20px rgba(240, 185, 11, 0.2);
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 10.5px;
|
||||
color: #555;
|
||||
color: #6B7280;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
@@ -273,16 +273,16 @@ const pullIndicatorStyle = () => ({
|
||||
}
|
||||
|
||||
.list-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.record-row {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.record-row:last-child {
|
||||
@@ -290,15 +290,15 @@ const pullIndicatorStyle = () => ({
|
||||
}
|
||||
|
||||
.record-row--highlight {
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
box-shadow: inset 3px 0 0 #d4af37;
|
||||
background: rgba(0, 61, 107, 0.04);
|
||||
box-shadow: inset 3px 0 0 #003D6B;
|
||||
}
|
||||
|
||||
.ledger-hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #666;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.row-main {
|
||||
@@ -318,12 +318,12 @@ const pullIndicatorStyle = () => ({
|
||||
.row-amount {
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.row-period {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -331,9 +331,9 @@ const pullIndicatorStyle = () => ({
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #f0b90b;
|
||||
background: rgba(240, 185, 11, 0.1);
|
||||
border: 1px solid rgba(240, 185, 11, 0.22);
|
||||
color: #F8971F;
|
||||
background: rgba(248, 151, 31, 0.08);
|
||||
border: 1px solid rgba(248, 151, 31, 0.22);
|
||||
border-radius: 999px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
@@ -344,7 +344,7 @@ const pullIndicatorStyle = () => ({
|
||||
gap: 8px 16px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.row-foot {
|
||||
@@ -354,9 +354,9 @@ const pullIndicatorStyle = () => ({
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed rgba(255, 255, 255, 0.06);
|
||||
border-top: 1px dashed #E5E7EB;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.row-batch {
|
||||
@@ -380,14 +380,14 @@ const pullIndicatorStyle = () => ({
|
||||
margin: 0 0 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #666;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.sentinel {
|
||||
@@ -403,7 +403,7 @@ const pullIndicatorStyle = () => ({
|
||||
.end-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
padding: 16px 0 4px;
|
||||
letter-spacing: 0.03em;
|
||||
|
||||
@@ -146,9 +146,6 @@ const availableLeagues = computed(() => {
|
||||
timeMatch = isAfterTodayMatchWindow(m.startTime, filterNow.value);
|
||||
}
|
||||
if (!timeMatch) continue;
|
||||
|
||||
if (filterState.value.status === 'open' && m.matchPhase !== 'open' && m.matchPhase !== undefined) continue;
|
||||
if (filterState.value.status === 'settled' && m.matchPhase !== 'settled') continue;
|
||||
|
||||
const id = m.leagueId ?? m.leagueName;
|
||||
if (!map.has(id)) {
|
||||
@@ -256,13 +253,6 @@ onActivated(() => {
|
||||
filterNow.value = new Date();
|
||||
filterState.value.time = 'all';
|
||||
syncSearchFromRoute();
|
||||
|
||||
if (route.query.tab === 'outright') {
|
||||
selectMainTab('outright');
|
||||
} else {
|
||||
selectMainTab('matches');
|
||||
}
|
||||
|
||||
void loadSummary(true, true);
|
||||
});
|
||||
|
||||
@@ -292,7 +282,6 @@ function goMatch(id: string) {
|
||||
:class="{ active: mainTab === 'matches', 'tab-gold-active': mainTab === 'matches' }"
|
||||
@click="selectMainTab('matches')"
|
||||
>
|
||||
<span class="tab-icon">⚽</span>
|
||||
{{ t('bet.tab_matches') }}
|
||||
</button>
|
||||
<button
|
||||
@@ -301,7 +290,6 @@ function goMatch(id: string) {
|
||||
:class="{ active: mainTab === 'outright', 'tab-gold-active': mainTab === 'outright' }"
|
||||
@click="selectMainTab('outright')"
|
||||
>
|
||||
<span class="tab-icon">🏆</span>
|
||||
{{ t('bet.tab_outright') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -504,71 +492,24 @@ function goMatch(id: string) {
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 12px 8px;
|
||||
padding: 0 10px;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: var(--gold);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.search-bar input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.filters-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 0 12px 12px;
|
||||
gap: 12px;
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
|
||||
.phase-filter {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.right-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-tabs {
|
||||
@@ -577,16 +518,15 @@ function goMatch(id: string) {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
height: 24px;
|
||||
padding: 2px 3px;
|
||||
height: 26px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-tab {
|
||||
padding: 0 8px;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
@@ -597,8 +537,6 @@ function goMatch(id: string) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-tab.active {
|
||||
@@ -614,8 +552,8 @@ function goMatch(id: string) {
|
||||
}
|
||||
|
||||
.status-toggle-btn {
|
||||
padding: 0 8px;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
height: 26px;
|
||||
box-sizing: border-box;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
@@ -628,8 +566,6 @@ function goMatch(id: string) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-toggle-btn.active {
|
||||
@@ -643,15 +579,14 @@ function goMatch(id: string) {
|
||||
.league-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 10px;
|
||||
font-size: 11px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
background: var(--bg-card);
|
||||
@@ -660,8 +595,7 @@ function goMatch(id: string) {
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.dropdown-text {
|
||||
@@ -699,37 +633,32 @@ function goMatch(id: string) {
|
||||
max-width: 240px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #0f1217;
|
||||
border: 1px solid #2a3240;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.65);
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.35);
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
color: #e5e7eb;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-bottom: 1px solid #1f2937;
|
||||
}
|
||||
|
||||
.dropdown-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item:active {
|
||||
background: #1b2330;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
color: #f4d06f;
|
||||
background: #242f42;
|
||||
color: var(--primary-light);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -737,6 +666,68 @@ function goMatch(id: string) {
|
||||
padding: 4px 12px 0;
|
||||
}
|
||||
|
||||
.state,
|
||||
.placeholder,
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 48px 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.outright-tab {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 12px 8px;
|
||||
padding: 0 10px;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: var(--primary-light);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.search-bar input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-results {
|
||||
padding: 0 12px;
|
||||
}
|
||||
@@ -768,27 +759,4 @@ function goMatch(id: string) {
|
||||
color: var(--primary-light);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.state,
|
||||
.placeholder,
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 48px 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.outright-tab {
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { onActivated, computed, ref } from 'vue';
|
||||
import { onActivated, computed, ref, watch, onDeactivated } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import emptyMatchesImg from '../assets/images/empty-matches.svg';
|
||||
import vsImg from '../assets/images/vs.png';
|
||||
import cardBg from '../assets/images/card-bg.webp';
|
||||
import bentoBetBg from '../assets/images/bento-bet-bg.png';
|
||||
import BannerCarousel from '../components/BannerCarousel.vue';
|
||||
import HomeAnnouncementCard from '../components/HomeAnnouncementCard.vue';
|
||||
import { usePlayerHome } from '../composables/usePlayerHome';
|
||||
import TeamEmblem from '../components/TeamEmblem.vue';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
@@ -17,29 +17,82 @@ import type { PlayerHomeMatch } from '../composables/usePlayerHome';
|
||||
type HotTab = 'hot' | 'upcoming';
|
||||
|
||||
const matchCardBg = `url(${cardBg})`;
|
||||
const bentoBetCardBg = `url(${bentoBetBg})`;
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const { banners, hotMatches, upcomingMatches, loading, load, announcementItems } = usePlayerHome();
|
||||
const activeTab = ref<HotTab>('hot');
|
||||
const activeFeaturedIndex = ref(0);
|
||||
const touchStartX = ref(0);
|
||||
const touchDeltaX = ref(0);
|
||||
|
||||
let rotateInterval: number | undefined;
|
||||
|
||||
function startFeaturedRotation() {
|
||||
stopFeaturedRotation();
|
||||
if (hotMatches.value.length > 1) {
|
||||
rotateInterval = window.setInterval(() => {
|
||||
activeFeaturedIndex.value = (activeFeaturedIndex.value + 1) % hotMatches.value.length;
|
||||
}, 4500);
|
||||
}
|
||||
}
|
||||
|
||||
function stopFeaturedRotation() {
|
||||
if (rotateInterval) {
|
||||
window.clearInterval(rotateInterval);
|
||||
rotateInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => hotMatches.value.length, () => {
|
||||
activeFeaturedIndex.value = 0;
|
||||
startFeaturedRotation();
|
||||
});
|
||||
|
||||
function onFeaturedTouchStart(e: TouchEvent) {
|
||||
touchStartX.value = e.touches[0].clientX;
|
||||
touchDeltaX.value = 0;
|
||||
stopFeaturedRotation();
|
||||
}
|
||||
|
||||
function onFeaturedTouchMove(e: TouchEvent) {
|
||||
touchDeltaX.value = e.touches[0].clientX - touchStartX.value;
|
||||
}
|
||||
|
||||
function onFeaturedTouchEnd() {
|
||||
const threshold = 40;
|
||||
if (hotMatches.value.length > 1) {
|
||||
if (touchDeltaX.value > threshold) {
|
||||
activeFeaturedIndex.value = (activeFeaturedIndex.value - 1 + hotMatches.value.length) % hotMatches.value.length;
|
||||
} else if (touchDeltaX.value < -threshold) {
|
||||
activeFeaturedIndex.value = (activeFeaturedIndex.value + 1) % hotMatches.value.length;
|
||||
}
|
||||
}
|
||||
touchDeltaX.value = 0;
|
||||
startFeaturedRotation();
|
||||
}
|
||||
|
||||
const bannerFallbackTo = computed(() => {
|
||||
const id = announcementItems.value[0]?.id;
|
||||
return id ? `/announcements/${id}` : '/announcements';
|
||||
});
|
||||
|
||||
const displayedMatches = computed<PlayerHomeMatch[]>(() =>
|
||||
activeTab.value === 'hot' ? hotMatches.value : upcomingMatches.value,
|
||||
);
|
||||
const displayedMatches = computed<PlayerHomeMatch[]>(() => upcomingMatches.value);
|
||||
const limitedMatches = computed(() => displayedMatches.value.slice(0, 6));
|
||||
|
||||
const emptyMessage = computed(() =>
|
||||
activeTab.value === 'hot' ? t('home.no_matches') : t('home.upcoming_empty'),
|
||||
);
|
||||
const emptyMessage = computed(() => t('home.upcoming_empty'));
|
||||
|
||||
const { pullDistance, refreshing, spinning, progress } = usePullToRefresh({
|
||||
onRefresh: async () => { await load(true); },
|
||||
});
|
||||
|
||||
onActivated(() => { void load(true); });
|
||||
onActivated(() => {
|
||||
void load(true);
|
||||
startFeaturedRotation();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
stopFeaturedRotation();
|
||||
});
|
||||
|
||||
const pullIndicatorStyle = () => ({
|
||||
height: `${pullDistance.value}px`,
|
||||
@@ -67,37 +120,129 @@ function formatKickoff(startTime: string) {
|
||||
|
||||
<BannerCarousel :banners="banners" :fallback-to="bannerFallbackTo" />
|
||||
|
||||
<div class="home-card-wrapper">
|
||||
<HomeAnnouncementCard />
|
||||
<div class="quick-bento">
|
||||
<div
|
||||
class="bento-item bento-item--large bento-item--featured-carousel"
|
||||
@touchstart.passive="onFeaturedTouchStart"
|
||||
@touchmove.passive="onFeaturedTouchMove"
|
||||
@touchend="onFeaturedTouchEnd"
|
||||
>
|
||||
<!-- Fixed header, does not swipe/scroll -->
|
||||
<div v-if="hotMatches.length" class="bento-header-row fixed-header">
|
||||
<span class="featured-tag">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" width="12" height="12" class="tag-icon"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||
{{ t('home.hot_matches') }}
|
||||
</span>
|
||||
<span class="bento-badge">HOT</span>
|
||||
</div>
|
||||
|
||||
<div class="carousel-viewport">
|
||||
<div
|
||||
class="carousel-track"
|
||||
:style="{ transform: `translateX(-${activeFeaturedIndex * 100}%)` }"
|
||||
>
|
||||
<template v-if="hotMatches.length">
|
||||
<div
|
||||
v-for="match in hotMatches"
|
||||
:key="match.id"
|
||||
class="carousel-slide"
|
||||
@click="goMatch(match.id)"
|
||||
>
|
||||
<div class="bento-content--vertical">
|
||||
<div class="bento-header-row placeholder-header" aria-hidden="true">
|
||||
<span class="featured-tag">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" width="12" height="12" class="tag-icon"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||||
{{ t('home.hot_matches') }}
|
||||
</span>
|
||||
<span class="bento-badge">HOT</span>
|
||||
</div>
|
||||
|
||||
<div class="featured-vs-area">
|
||||
<div class="featured-team">
|
||||
<TeamEmblem
|
||||
size="sm"
|
||||
:team-code="match.homeTeamCode"
|
||||
:team-name="match.homeTeamName"
|
||||
:logo-url="match.homeTeamLogoUrl"
|
||||
/>
|
||||
<span class="featured-team-name">{{ match.homeTeamName }}</span>
|
||||
</div>
|
||||
|
||||
<span class="featured-vs">VS</span>
|
||||
|
||||
<div class="featured-team">
|
||||
<TeamEmblem
|
||||
size="sm"
|
||||
:team-code="match.awayTeamCode"
|
||||
:team-name="match.awayTeamName"
|
||||
:logo-url="match.awayTeamLogoUrl"
|
||||
/>
|
||||
<span class="featured-team-name">{{ match.awayTeamName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="featured-footer">
|
||||
<span class="featured-time">{{ formatKickoff(match.startTime) }}</span>
|
||||
<span class="featured-action-btn">{{ t('announcements.go_link') }} ›</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="carousel-slide" @click="router.push('/bet')">
|
||||
<div class="bento-content--vertical">
|
||||
<div class="bento-header-row">
|
||||
<span class="bento-icon bento-icon--bet">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" width="22" height="22"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 000 20 14.5 14.5 0 000-20M2 12h20"/></svg>
|
||||
</span>
|
||||
<span class="bento-badge">HOT</span>
|
||||
</div>
|
||||
<div class="bento-text">
|
||||
<span class="bento-title bento-title--lg">{{ t('nav.bet') }}</span>
|
||||
<span class="bento-desc">{{ t('home.bento_desc') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hotMatches.length > 1" class="carousel-dots">
|
||||
<span
|
||||
v-for="(_, i) in hotMatches"
|
||||
:key="i"
|
||||
class="carousel-dot"
|
||||
:class="{ active: i === activeFeaturedIndex }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="bento-item bento-item--small" @click="router.push('/wallet/recharge')">
|
||||
<span class="bento-icon bento-icon--recharge">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M2 10h20M12 15h4"/></svg>
|
||||
</span>
|
||||
<span class="bento-title bento-title--sm">{{ t('recharge.title') }}</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="bento-item bento-item--small" @click="router.push('/bets')">
|
||||
<span class="bento-icon bento-icon--history">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
||||
<path d="M6 4h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Z" stroke-linejoin="round"/>
|
||||
<path d="M8 9h8M8 12.5h8M8 16h5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="bento-title bento-title--sm">{{ t('home.view_bet_history') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="hot-tabs" role="tablist" :aria-label="t('home.hot_matches')">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="hot-tab"
|
||||
:class="{ active: activeTab === 'hot' }"
|
||||
:aria-selected="activeTab === 'hot'"
|
||||
@click="activeTab = 'hot'"
|
||||
>
|
||||
{{ t('home.hot_tab') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="hot-tab"
|
||||
:class="{ active: activeTab === 'upcoming' }"
|
||||
:aria-selected="activeTab === 'upcoming'"
|
||||
@click="activeTab = 'upcoming'"
|
||||
>
|
||||
{{ t('home.upcoming_tab') }}
|
||||
</button>
|
||||
<div class="section-header">
|
||||
<div class="section-title">{{ t('home.upcoming_tab') }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(match, index) in displayedMatches"
|
||||
v-for="(match, index) in limitedMatches"
|
||||
:key="match.id"
|
||||
class="match-card"
|
||||
:class="{ 'match-card--live-anim': activeTab === 'hot' && index < 3 }"
|
||||
:class="{ 'match-card--live-anim': index < 3 }"
|
||||
@click="goMatch(match.id)"
|
||||
>
|
||||
<div class="match-info">
|
||||
@@ -145,6 +290,12 @@ function formatKickoff(startTime: string) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="displayedMatches.length > 6" class="view-all-container">
|
||||
<button type="button" class="view-all-link" @click="router.push('/bet')">
|
||||
{{ t('home.view_all_matches') }} ›
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && !displayedMatches.length" class="empty">
|
||||
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
|
||||
<p>{{ emptyMessage }}</p>
|
||||
@@ -153,10 +304,6 @@ function formatKickoff(startTime: string) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-card-wrapper {
|
||||
margin: 12px 16px;
|
||||
}
|
||||
|
||||
.pull-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -165,34 +312,334 @@ function formatKickoff(startTime: string) {
|
||||
transition: height 0.15s ease;
|
||||
}
|
||||
|
||||
.hot-tabs {
|
||||
display: flex;
|
||||
.section-header {
|
||||
margin-top: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.quick-bento {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
grid-template-rows: 54px 54px;
|
||||
gap: 8px;
|
||||
margin: 14px 0;
|
||||
margin-bottom: 16px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.hot-tab {
|
||||
flex: 1;
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
.bento-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
|
||||
padding: 10px 12px;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hot-tab.active {
|
||||
.bento-item:active {
|
||||
transform: scale(0.97);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.bento-item--large {
|
||||
grid-row: 1 / 3;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #F3F8FC 0%, #FFFFFF 100%);
|
||||
border-color: rgba(0, 61, 107, 0.15);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bento-item--featured-carousel {
|
||||
/* Carousel container styles */
|
||||
}
|
||||
|
||||
.carousel-viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.carousel-track {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
.carousel-slide {
|
||||
flex: 0 0 100%;
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px 10px 8px 12px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.carousel-dots {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.carousel-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 61, 107, 0.15);
|
||||
transition: background 0.25s, width 0.25s;
|
||||
}
|
||||
|
||||
.carousel-dot.active {
|
||||
width: 12px;
|
||||
border-radius: 2px;
|
||||
background: #003D6B;
|
||||
}
|
||||
|
||||
.featured-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
padding: 3px 6px;
|
||||
border-radius: 6px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hot-tab:active {
|
||||
opacity: 0.92;
|
||||
.tag-icon {
|
||||
color: #F97316;
|
||||
}
|
||||
|
||||
.featured-vs-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
margin: 6px 0;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.featured-team {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.featured-team-name {
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
color: #1A1A2E;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featured-vs {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: #F97316;
|
||||
font-style: italic;
|
||||
opacity: 0.95;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.featured-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border-top: 1px dashed rgba(0, 61, 107, 0.1);
|
||||
padding-top: 5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.featured-time {
|
||||
font-size: 8px;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.featured-action-btn {
|
||||
font-size: 8.5px;
|
||||
font-weight: 800;
|
||||
color: #003D6B;
|
||||
background: rgba(0, 61, 107, 0.05);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.bento-item--featured:active .featured-action-btn {
|
||||
background: rgba(0, 61, 107, 0.12);
|
||||
}
|
||||
|
||||
.bento-item--large .bento-title {
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.bento-item--large .bento-desc {
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.bento-item--large .bento-badge {
|
||||
background: #F97316;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.bento-content--vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bento-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fixed-header {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 12px;
|
||||
right: 10px;
|
||||
width: auto;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.placeholder-header {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bento-badge {
|
||||
font-size: 8px;
|
||||
font-weight: 800;
|
||||
color: #FFFFFF;
|
||||
background: #003D6B;
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
letter-spacing: 0.05em;
|
||||
line-height: 1;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.bento-item--large:active {
|
||||
border-color: rgba(0, 61, 107, 0.3);
|
||||
}
|
||||
|
||||
.bento-item--small {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: flex-start;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bento-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bento-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bento-item--large .bento-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.bento-icon--bet {
|
||||
background: rgba(0, 61, 107, 0.08);
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.bento-icon--recharge {
|
||||
background: rgba(5, 150, 105, 0.08);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.bento-icon--history {
|
||||
background: rgba(248, 151, 31, 0.08);
|
||||
color: #E8870A;
|
||||
}
|
||||
|
||||
.bento-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.bento-title {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: #1A1A2E;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bento-title--sm {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bento-title--lg {
|
||||
font-size: 14.5px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.bento-desc {
|
||||
font-size: 9.5px;
|
||||
color: #6B7280;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1A1A2E;
|
||||
margin: 0 0 8px;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid #003D6B;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.match-card {
|
||||
@@ -201,16 +648,16 @@ function formatKickoff(startTime: string) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 14px 16px;
|
||||
min-height: 72px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: none;
|
||||
background: var(--bg-card);
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 10px 12px;
|
||||
min-height: 60px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
background: #FFFFFF;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -218,15 +665,14 @@ function formatKickoff(startTime: string) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
|
||||
opacity: 0.25;
|
||||
background: linear-gradient(135deg, rgba(0, 61, 107, 0.02) 0%, transparent 60%);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.match-card:active {
|
||||
opacity: 0.92;
|
||||
transform: scale(0.995);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.match-info,
|
||||
@@ -241,15 +687,16 @@ function formatKickoff(startTime: string) {
|
||||
}
|
||||
|
||||
.match-teams {
|
||||
font-weight: 800;
|
||||
margin-bottom: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.match-time {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
color: #6B7280;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -257,15 +704,15 @@ function formatKickoff(startTime: string) {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 3px;
|
||||
max-width: 46%;
|
||||
}
|
||||
|
||||
.vs-arena {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 64px;
|
||||
height: 52px;
|
||||
width: 52px;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -286,7 +733,7 @@ function formatKickoff(startTime: string) {
|
||||
stroke-width: 2.2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 0 4px rgba(120, 210, 255, 0.95)) drop-shadow(0 0 8px rgba(255, 180, 80, 0.55));
|
||||
filter: drop-shadow(0 0 4px rgba(0, 102, 204, 0.6)) drop-shadow(0 0 8px rgba(0, 91, 159, 0.35));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -311,11 +758,11 @@ function formatKickoff(startTime: string) {
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(94, 184, 255, 0) 0%,
|
||||
rgba(184, 236, 255, 0.95) 28%,
|
||||
#fff 50%,
|
||||
rgba(255, 208, 128, 0.95) 72%,
|
||||
rgba(255, 144, 64, 0) 100%
|
||||
rgba(0, 91, 159, 0) 0%,
|
||||
rgba(0, 102, 204, 0.7) 28%,
|
||||
#0066CC 50%,
|
||||
rgba(0, 91, 159, 0.7) 72%,
|
||||
rgba(0, 61, 107, 0) 100%
|
||||
);
|
||||
opacity: 0;
|
||||
filter: blur(0.4px);
|
||||
@@ -329,10 +776,10 @@ function formatKickoff(startTime: string) {
|
||||
.vs-img {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 48px;
|
||||
width: 40px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 0 3px rgba(212, 175, 55, 0.35));
|
||||
filter: drop-shadow(0 0 3px rgba(0, 61, 107, 0.25));
|
||||
}
|
||||
|
||||
.match-card--live-anim .vs-img {
|
||||
@@ -420,21 +867,21 @@ function formatKickoff(startTime: string) {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.82;
|
||||
filter: drop-shadow(0 0 2px rgba(212, 175, 55, 0.3));
|
||||
filter: drop-shadow(0 0 2px rgba(0, 61, 107, 0.2));
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
filter:
|
||||
drop-shadow(0 0 3px rgba(255, 230, 140, 0.7))
|
||||
drop-shadow(0 0 6px rgba(212, 175, 55, 0.35));
|
||||
drop-shadow(0 0 3px rgba(0, 102, 204, 0.5))
|
||||
drop-shadow(0 0 6px rgba(0, 61, 107, 0.25));
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vs-img {
|
||||
animation: none;
|
||||
filter: drop-shadow(0 0 3px rgba(212, 175, 55, 0.35));
|
||||
filter: drop-shadow(0 0 3px rgba(0, 61, 107, 0.25));
|
||||
}
|
||||
|
||||
.hz-path,
|
||||
@@ -446,14 +893,48 @@ function formatKickoff(startTime: string) {
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 40px 20px;
|
||||
color: #6B7280;
|
||||
padding: 28px 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-bottom: 14px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.view-all-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.view-all-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #003D6B;
|
||||
cursor: pointer;
|
||||
padding: 6px 12px;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.view-all-link:hover {
|
||||
opacity: 0.85;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.view-all-link:active {
|
||||
transform: scale(0.97);
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -159,7 +159,7 @@ onActivated(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<MessageListPanel v-if="inboxEnabled" v-show="activeTab === 'messages'" @click-message="id => $router.push('/messages/' + id)" />
|
||||
<MessageListPanel v-if="inboxEnabled" v-show="activeTab === 'messages'" />
|
||||
<CustomerServicePanel v-if="activeTab === 'support'" />
|
||||
|
||||
<ConfirmDialog
|
||||
@@ -201,8 +201,8 @@ onActivated(() => {
|
||||
height: 36px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #141414;
|
||||
color: var(--gold);
|
||||
background: var(--bg-card);
|
||||
color: var(--primary);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
@@ -212,7 +212,7 @@ onActivated(() => {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--primary-dark, #002847);
|
||||
}
|
||||
|
||||
.hub-top-bar {
|
||||
@@ -248,8 +248,8 @@ onActivated(() => {
|
||||
}
|
||||
|
||||
.hub-tab.active {
|
||||
color: var(--gold);
|
||||
border-bottom-color: var(--gold);
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
.hub-tab-label {
|
||||
@@ -260,8 +260,8 @@ onActivated(() => {
|
||||
min-width: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: var(--gold);
|
||||
color: #111;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
@@ -277,10 +277,10 @@ onActivated(() => {
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border: 1px solid var(--border-active);
|
||||
border-radius: 8px;
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
color: var(--gold);
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
color: var(--primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
@@ -292,8 +292,8 @@ onActivated(() => {
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
border-color: rgba(255, 80, 80, 0.35);
|
||||
background: rgba(255, 80, 80, 0.08);
|
||||
color: #f88;
|
||||
border-color: rgba(220, 38, 38, 0.35);
|
||||
background: rgba(220, 38, 38, 0.06);
|
||||
color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -453,7 +453,9 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
<style scoped>
|
||||
.detail-page {
|
||||
margin: 0 -16px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 16px;
|
||||
background: #E8EDF2;
|
||||
}
|
||||
|
||||
.pull-indicator {
|
||||
@@ -471,19 +473,19 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
padding: 4px 12px 8px;
|
||||
gap: 8px;
|
||||
position: sticky;
|
||||
top: -12px;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
margin-top: -12px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
margin-bottom: 8px;
|
||||
background: #FFFFFF;
|
||||
border-bottom: 1px solid #CBD5E1;
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
color: #003D6B;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -501,9 +503,9 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #F5F7FA;
|
||||
color: #003D6B;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
@@ -514,75 +516,77 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* ── match hero ── */
|
||||
/* -- match hero -- */
|
||||
.match-hero {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
margin: 0 0 10px;
|
||||
margin: 0 12px 10px;
|
||||
padding: 14px 12px 16px;
|
||||
overflow: hidden;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #CBD5E1;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.match-hero--phase {
|
||||
opacity: 0.98;
|
||||
}
|
||||
|
||||
/* ── hero status tag (top-right corner) ── */
|
||||
/* -- hero status tag (top-right corner) -- */
|
||||
.status-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
padding: 4px 14px;
|
||||
border-radius: 0 6px 0 10px;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.status-tag--settled {
|
||||
background: linear-gradient(180deg, #2a2a3a 0%, #1a1a28 100%);
|
||||
color: #8a9ab8;
|
||||
border-bottom: 1px solid rgba(120, 140, 180, 0.3);
|
||||
border-left: 1px solid rgba(120, 140, 180, 0.3);
|
||||
background: #E8EDF2;
|
||||
color: #4B5563;
|
||||
border-bottom: 1px solid #D1D5DB;
|
||||
border-left: 1px solid #D1D5DB;
|
||||
}
|
||||
|
||||
.status-tag--pending {
|
||||
background: linear-gradient(180deg, #3a3a2a 0%, #28251a 100%);
|
||||
color: #c8a84e;
|
||||
border-bottom: 1px solid rgba(200, 168, 78, 0.3);
|
||||
border-left: 1px solid rgba(200, 168, 78, 0.3);
|
||||
background: #FEF3C7;
|
||||
color: #92400E;
|
||||
border-bottom: 1px solid #FDE68A;
|
||||
border-left: 1px solid #FDE68A;
|
||||
}
|
||||
|
||||
/* ── market panel smaller status tag ── */
|
||||
/* -- market panel smaller status tag -- */
|
||||
.market-status-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 0 4px 0 8px;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.market-status-tag--settled {
|
||||
background: linear-gradient(180deg, #2a2a3a 0%, #1a1a28 100%);
|
||||
color: #8a9ab8;
|
||||
border-bottom: 1px solid rgba(120, 140, 180, 0.3);
|
||||
border-left: 1px solid rgba(120, 140, 180, 0.3);
|
||||
background: #E8EDF2;
|
||||
color: #4B5563;
|
||||
border-bottom: 1px solid #D1D5DB;
|
||||
border-left: 1px solid #D1D5DB;
|
||||
}
|
||||
|
||||
.market-status-tag--pending {
|
||||
background: linear-gradient(180deg, #3a3a2a 0%, #28251a 100%);
|
||||
color: #c8a84e;
|
||||
border-bottom: 1px solid rgba(200, 168, 78, 0.3);
|
||||
border-left: 1px solid rgba(200, 168, 78, 0.3);
|
||||
background: #FEF3C7;
|
||||
color: #92400E;
|
||||
border-bottom: 1px solid #FDE68A;
|
||||
border-left: 1px solid #FDE68A;
|
||||
}
|
||||
|
||||
.market-status-tag--suspended {
|
||||
@@ -612,7 +616,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
text-align: left;
|
||||
margin-top: 10px;
|
||||
padding-left: 2px;
|
||||
@@ -624,18 +628,17 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
margin: 8px 0 0;
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
color: #003D6B;
|
||||
text-align: center;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.result-stats-section {
|
||||
margin: 0 10px 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(212, 175, 55, 0.22);
|
||||
border: 1px solid #CBD5E1;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, #1b1b1b 0%, #121212 100%);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.28);
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.result-stats-head {
|
||||
@@ -650,8 +653,8 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
font-weight: 900;
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.result-stats-head span {
|
||||
@@ -661,7 +664,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.result-stats-grid {
|
||||
@@ -678,8 +681,8 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: #F5F7FA;
|
||||
border: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.result-stat-label {
|
||||
@@ -688,15 +691,15 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.result-stat-value {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.hero-teams {
|
||||
@@ -719,8 +722,8 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
|
||||
.hero-name {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
color: #1A1A2E;
|
||||
text-align: center;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
@@ -729,7 +732,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* VS arena — same as HomeView */
|
||||
/* VS arena -- blue themed */
|
||||
.vs-arena {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
@@ -755,7 +758,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
stroke-width: 2.2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 0 4px rgba(120, 210, 255, 0.95)) drop-shadow(0 0 8px rgba(255, 180, 80, 0.55));
|
||||
filter: drop-shadow(0 0 4px rgba(0, 102, 204, 0.6)) drop-shadow(0 0 8px rgba(0, 91, 159, 0.35));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -780,11 +783,11 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(94, 184, 255, 0) 0%,
|
||||
rgba(184, 236, 255, 0.95) 28%,
|
||||
#fff 50%,
|
||||
rgba(255, 208, 128, 0.95) 72%,
|
||||
rgba(255, 144, 64, 0) 100%
|
||||
rgba(0, 91, 159, 0) 0%,
|
||||
rgba(0, 102, 204, 0.7) 28%,
|
||||
#0066CC 50%,
|
||||
rgba(0, 91, 159, 0.7) 72%,
|
||||
rgba(0, 61, 107, 0) 100%
|
||||
);
|
||||
opacity: 0;
|
||||
filter: blur(0.4px);
|
||||
@@ -826,11 +829,11 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
@keyframes vs-glow {
|
||||
0%, 100% {
|
||||
opacity: 0.82;
|
||||
filter: drop-shadow(0 0 2px rgba(212, 175, 55, 0.3));
|
||||
filter: drop-shadow(0 0 2px rgba(0, 61, 107, 0.2));
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
filter: drop-shadow(0 0 3px rgba(255, 230, 140, 0.7)) drop-shadow(0 0 6px rgba(212, 175, 55, 0.35));
|
||||
filter: drop-shadow(0 0 3px rgba(0, 102, 204, 0.5)) drop-shadow(0 0 6px rgba(0, 61, 107, 0.25));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,22 +848,22 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
.market-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.market-group {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #252525;
|
||||
background: linear-gradient(180deg, #1a1a1a 0%, #151515 100%);
|
||||
border: 1px solid #CBD5E1;
|
||||
background: #FFFFFF;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.35);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.market-group.open {
|
||||
border-color: rgba(212, 175, 55, 0.25);
|
||||
box-shadow: 0 4px 16px rgba(212, 175, 55, 0.08);
|
||||
border-color: #9CA3AF;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.market-group :deep(.row) {
|
||||
@@ -876,28 +879,28 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
width: calc(100% - 16px);
|
||||
margin: 0 8px 10px;
|
||||
padding: 9px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
color: var(--primary-light);
|
||||
border-radius: 6px;
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #F5F7FA;
|
||||
color: #003D6B;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.state {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.cs-toast {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
padding: 2px 0 6px;
|
||||
}
|
||||
|
||||
/* ── 我的投注 ── */
|
||||
/* -- my bets -- */
|
||||
.my-bets-section {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 12px;
|
||||
@@ -906,7 +909,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
.my-bets-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
color: #1A1A2E;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
@@ -917,16 +920,17 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
}
|
||||
|
||||
.my-bet-card {
|
||||
background: #1c1c1c;
|
||||
border: 1px solid #2e2e2e;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #CBD5E1;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.my-bet-card:active {
|
||||
background: #252525;
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.bet-header {
|
||||
@@ -939,7 +943,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
.bet-type {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: #1A1A2E;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -956,18 +960,18 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
}
|
||||
|
||||
.bet-status-won {
|
||||
background: rgba(61, 184, 101, 0.15);
|
||||
color: #3db865;
|
||||
background: #E6F4EA;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.bet-status-lost {
|
||||
background: rgba(224, 80, 80, 0.15);
|
||||
color: #e05050;
|
||||
background: #FDE8E8;
|
||||
color: #DC2626;
|
||||
}
|
||||
|
||||
.bet-status-pending {
|
||||
background: rgba(232, 200, 74, 0.15);
|
||||
color: #e8c84a;
|
||||
background: #FEF3C7;
|
||||
color: #D97706;
|
||||
}
|
||||
|
||||
.bet-footer {
|
||||
@@ -978,12 +982,12 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
|
||||
.bet-stake {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.bet-return {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{ embedded?: boolean; id?: string }>();
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import { sanitizeAnnouncementHtml } from '../utils/html';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
import {
|
||||
usePlayerMessages,
|
||||
type DepositMessagePayload,
|
||||
@@ -17,14 +15,26 @@ import {
|
||||
} from '../composables/usePlayerMessages';
|
||||
import { useInboxFeature } from '../composables/useInboxFeature';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
embedded?: boolean;
|
||||
/** 悬浮信箱内嵌时使用,不走路由 */
|
||||
id?: string;
|
||||
}>(),
|
||||
{ embedded: false },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{ back: [] }>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const { loadMessageDetail, markMessageRead, deleteMessage } = usePlayerMessages();
|
||||
const { hubRoute } = useInboxFeature();
|
||||
const emit = defineEmits<{ (e: 'back'): void }>();
|
||||
|
||||
const messageId = computed(() => props.id || String(route.params.id ?? ''));
|
||||
const messageId = computed(() => props.id ?? String(route.params.id ?? ''));
|
||||
const isPopupEmbedded = computed(() => props.embedded && Boolean(props.id));
|
||||
const isHubEmbedded = computed(() => props.embedded && !props.id);
|
||||
const message = ref<PlayerMessage | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(false);
|
||||
@@ -52,14 +62,22 @@ const contentPromoId = computed(() => {
|
||||
});
|
||||
|
||||
function goBack() {
|
||||
if (props.embedded) {
|
||||
if (isPopupEmbedded.value) {
|
||||
emit('back');
|
||||
} else {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
if (isHubEmbedded.value) {
|
||||
router.push(hubRoute.value);
|
||||
return;
|
||||
}
|
||||
router.back();
|
||||
}
|
||||
|
||||
function goList() {
|
||||
if (isPopupEmbedded.value) {
|
||||
emit('back');
|
||||
return;
|
||||
}
|
||||
router.push(hubRoute.value);
|
||||
}
|
||||
|
||||
@@ -131,7 +149,7 @@ async function confirmDelete() {
|
||||
try {
|
||||
await deleteMessage(messageId.value);
|
||||
deleteConfirmVisible.value = false;
|
||||
if (props.embedded) {
|
||||
if (isPopupEmbedded.value) {
|
||||
emit('back');
|
||||
} else {
|
||||
router.replace(hubRoute.value);
|
||||
@@ -155,8 +173,8 @@ watch(messageId, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="message-detail" :class="{ 'message-detail--embedded': embedded }">
|
||||
<header v-if="!embedded" class="page-header">
|
||||
<div class="message-detail" :class="{ 'is-embedded': embedded, 'is-hub-embedded': isHubEmbedded, 'is-popup-embedded': isPopupEmbedded }">
|
||||
<header v-if="!isHubEmbedded" class="page-header">
|
||||
<button type="button" class="back-btn" :aria-label="t('messages.back')" @click="goBack">‹</button>
|
||||
<h1>{{ t('messages.detail_title') }}</h1>
|
||||
<button
|
||||
@@ -170,10 +188,14 @@ watch(messageId, () => {
|
||||
{{ t('messages.delete') }}
|
||||
</button>
|
||||
</header>
|
||||
<div v-else-if="message" class="embedded-actions">
|
||||
<button type="button" class="back-btn embedded-back-btn" :aria-label="t('messages.back')" @click="goBack">‹</button>
|
||||
<h1 class="embedded-title">{{ t('messages.detail_title') }}</h1>
|
||||
|
||||
<header v-else class="page-header page-header--hub">
|
||||
<button type="button" class="back-btn" :aria-label="t('messages.back')" @click="goBack">
|
||||
‹ {{ t('messages.back') }}
|
||||
</button>
|
||||
<h1>{{ t('messages.detail_title') }}</h1>
|
||||
<button
|
||||
v-if="message"
|
||||
type="button"
|
||||
class="delete-header-btn"
|
||||
:aria-label="t('messages.delete')"
|
||||
@@ -182,7 +204,7 @@ watch(messageId, () => {
|
||||
>
|
||||
{{ t('messages.delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="loading && !message" class="state">
|
||||
<GoldSpinner :size="36" />
|
||||
@@ -190,7 +212,9 @@ watch(messageId, () => {
|
||||
|
||||
<div v-else-if="error || !message" class="empty">
|
||||
<p>{{ t('messages.not_found') }}</p>
|
||||
<button type="button" class="link-btn" @click="goList">{{ t('messages.view_all') }}</button>
|
||||
<button v-if="!isHubEmbedded" type="button" class="link-btn" @click="goList">
|
||||
{{ isPopupEmbedded ? t('common.back') : t('messages.view_all') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<article v-else class="detail-article">
|
||||
@@ -251,27 +275,6 @@ watch(messageId, () => {
|
||||
padding: 0 0 24px;
|
||||
}
|
||||
|
||||
.message-detail--embedded {
|
||||
padding: 0 24px 24px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.embedded-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 0 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.embedded-title {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -281,31 +284,15 @@ watch(messageId, () => {
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #141414;
|
||||
color: var(--text-muted);
|
||||
font-size: 20px;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-card);
|
||||
color: var(--primary);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.embedded-back-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: -4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
@@ -313,17 +300,18 @@ watch(messageId, () => {
|
||||
flex: 1;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--primary-dark, #002847);
|
||||
}
|
||||
|
||||
.delete-header-btn {
|
||||
border: 1px solid rgba(255, 80, 80, 0.35);
|
||||
border: 1px solid rgba(220, 38, 38, 0.35);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
background: rgba(255, 80, 80, 0.08);
|
||||
color: #f88;
|
||||
background: rgba(220, 38, 38, 0.06);
|
||||
color: #dc2626;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.delete-header-btn:disabled {
|
||||
@@ -337,13 +325,13 @@ watch(messageId, () => {
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-muted);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.status-badge.unread {
|
||||
color: var(--gold);
|
||||
background: rgba(212, 175, 55, 0.12);
|
||||
color: var(--primary);
|
||||
background: rgba(0, 61, 107, 0.08);
|
||||
}
|
||||
|
||||
.state,
|
||||
@@ -357,13 +345,14 @@ watch(messageId, () => {
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border: 1px solid var(--border-active);
|
||||
border-radius: 8px;
|
||||
padding: 8px 14px;
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
color: var(--gold);
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.detail-article {
|
||||
@@ -381,14 +370,14 @@ watch(messageId, () => {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
color: #fff;
|
||||
color: var(--primary-dark, #002847);
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
color: #d0d0d0;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.rich-body :deep(p) {
|
||||
@@ -414,21 +403,21 @@ watch(messageId, () => {
|
||||
}
|
||||
|
||||
.rich-body :deep(a) {
|
||||
color: var(--gold);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.reason-box {
|
||||
margin-top: 18px;
|
||||
padding: 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 107, 107, 0.25);
|
||||
background: rgba(255, 107, 107, 0.08);
|
||||
border: 1px solid rgba(220, 38, 38, 0.2);
|
||||
background: rgba(220, 38, 38, 0.05);
|
||||
}
|
||||
|
||||
.reason-label {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: #ffb4b4;
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -436,19 +425,47 @@ watch(messageId, () => {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
color: #ffe0e0;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
padding: 11px 14px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border: 1px solid var(--border-active);
|
||||
border-radius: 8px;
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
color: var(--gold);
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.is-hub-embedded {
|
||||
padding: 0 4px 24px;
|
||||
}
|
||||
|
||||
.is-hub-embedded .page-header--hub {
|
||||
padding: 0 0 16px;
|
||||
}
|
||||
|
||||
.is-hub-embedded .page-header--hub h1 {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.is-popup-embedded {
|
||||
padding: 12px 14px 16px;
|
||||
}
|
||||
|
||||
.is-popup-embedded .page-header {
|
||||
padding: 0 0 12px;
|
||||
}
|
||||
|
||||
.is-popup-embedded .page-header h1 {
|
||||
font-size: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,14 +21,13 @@ const statusFilter = ref('');
|
||||
const sentinel = ref<HTMLElement | null>(null);
|
||||
let observer: IntersectionObserver | null = null;
|
||||
|
||||
const STATUS_FILTERS = [
|
||||
const FILTERS = [
|
||||
{ key: '', label: 'history.filter_all' },
|
||||
{ key: 'WON', label: 'history.filter_won' },
|
||||
{ key: 'LOST', label: 'history.filter_lost' },
|
||||
{ key: 'PENDING', label: 'history.filter_pending' },
|
||||
] as const;
|
||||
|
||||
const PUSH_FILTER = { key: 'PUSH', label: 'history.filter_push' } as const;
|
||||
{ key: 'PUSH', label: 'history.filter_push' },
|
||||
];
|
||||
|
||||
async function loadPage(p: number) {
|
||||
if (loading.value) return;
|
||||
@@ -117,7 +116,7 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
<div class="filter-tabs">
|
||||
<button
|
||||
v-for="f in STATUS_FILTERS"
|
||||
v-for="f in FILTERS"
|
||||
:key="f.key"
|
||||
type="button"
|
||||
class="filter-tab"
|
||||
@@ -126,16 +125,6 @@ const pullIndicatorStyle = () => ({
|
||||
>
|
||||
{{ t(f.label) }}
|
||||
</button>
|
||||
<span class="filter-divider" aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
class="filter-tab filter-tab--push"
|
||||
:class="{ active: statusFilter === PUSH_FILTER.key }"
|
||||
:title="t('history.filter_push_hint')"
|
||||
@click="changeFilter(PUSH_FILTER.key)"
|
||||
>
|
||||
{{ t(PUSH_FILTER.label) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="items.length">
|
||||
@@ -175,23 +164,10 @@ const pullIndicatorStyle = () => ({
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
overflow-x: auto;
|
||||
align-items: center;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.filter-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
flex-shrink: 0;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.filter-tab--push {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.filter-tabs::-webkit-scrollbar { display: none; }
|
||||
|
||||
.filter-tab {
|
||||
@@ -200,16 +176,16 @@ const pullIndicatorStyle = () => ({
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
background: #141414;
|
||||
border: 1px solid #2a2a2a;
|
||||
color: #6B7280;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-color: var(--border-gold-soft);
|
||||
color: #003D6B;
|
||||
background: rgba(0, 61, 107, 0.08);
|
||||
border-color: #003D6B;
|
||||
}
|
||||
|
||||
.state {
|
||||
@@ -218,7 +194,7 @@ const pullIndicatorStyle = () => ({
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
padding: 56px 20px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
@@ -241,7 +217,7 @@ const pullIndicatorStyle = () => ({
|
||||
.end-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
padding: 16px 0 4px;
|
||||
letter-spacing: 0.03em;
|
||||
|
||||
@@ -149,7 +149,7 @@ onUnmounted(() => {
|
||||
border: 1px solid rgba(201, 162, 39, 0.22);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #c9a227;
|
||||
color: var(--odds-accent);
|
||||
}
|
||||
|
||||
.teams-grid {
|
||||
|
||||
@@ -328,7 +328,7 @@ function back() {
|
||||
|
||||
.back-btn {
|
||||
background: none;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
@@ -338,15 +338,15 @@ function back() {
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.avatar-card,
|
||||
.form-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.avatar-card {
|
||||
@@ -361,12 +361,12 @@ function back() {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--border-gold-soft);
|
||||
border: 2px solid #E5E7EB;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(145deg, #2a2210, #141008);
|
||||
background: #E8EDF2;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
@@ -379,9 +379,9 @@ function back() {
|
||||
.avatar-change-btn {
|
||||
padding: 7px 18px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid #003D6B;
|
||||
background: rgba(0, 61, 107, 0.04);
|
||||
color: #003D6B;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -396,12 +396,12 @@ function back() {
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
background: #E5E7EB;
|
||||
margin: 6px 0 12px;
|
||||
}
|
||||
|
||||
@@ -412,13 +412,13 @@ function back() {
|
||||
justify-content: space-between;
|
||||
padding: 4px 0 12px;
|
||||
background: none;
|
||||
color: var(--text);
|
||||
color: #1A1A2E;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
transition: transform 0.15s ease;
|
||||
@@ -442,7 +442,7 @@ function back() {
|
||||
|
||||
.field-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
margin: 6px 0 0;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@@ -469,10 +469,10 @@ function back() {
|
||||
min-width: 48px;
|
||||
padding: 0 10px;
|
||||
border: none;
|
||||
border-left: 1px solid var(--border);
|
||||
border-left: 1px solid #E5E7EB;
|
||||
border-radius: 0 6px 6px 0;
|
||||
background: rgba(212, 175, 55, 0.06);
|
||||
color: var(--primary-light);
|
||||
background: #F5F7FA;
|
||||
color: #003D6B;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
@@ -491,7 +491,7 @@ function back() {
|
||||
label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
@@ -504,28 +504,28 @@ label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #0a0a0a;
|
||||
color: var(--text);
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
color: #1A1A2E;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: 0 0 0 1px rgba(212, 175, 55, 0.12);
|
||||
border-color: #0066CC;
|
||||
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.12);
|
||||
}
|
||||
|
||||
.field-input:-webkit-autofill,
|
||||
.field-input:-webkit-autofill:hover,
|
||||
.field-input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: var(--text);
|
||||
box-shadow: 0 0 0 1000px #0a0a0a inset;
|
||||
border: 1px solid var(--border);
|
||||
-webkit-text-fill-color: #1A1A2E;
|
||||
box-shadow: 0 0 0 1000px #FFFFFF inset;
|
||||
border: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.field-input::placeholder {
|
||||
color: #555;
|
||||
color: rgba(107, 114, 128, 0.6);
|
||||
}
|
||||
|
||||
.readonly {
|
||||
@@ -555,10 +555,10 @@ label {
|
||||
}
|
||||
|
||||
.msg.ok {
|
||||
color: var(--primary-light);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.msg.err {
|
||||
color: var(--danger);
|
||||
color: #DC2626;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,18 +8,15 @@ import LocaleFlag from '../components/LocaleFlag.vue';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { useAppLocale } from '../composables/useAppLocale';
|
||||
import { usePlayerProfile } from '../composables/usePlayerProfile';
|
||||
import { useInboxFeature } from '../composables/useInboxFeature';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import { parseCashbackApiData, sumCashbackAmount } from '../utils/cashback';
|
||||
import walletBg from '../assets/images/wallet-bg.webp';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const { locales, setLocale, initFromUser } = useAppLocale();
|
||||
const { profileRaw, refreshProfile } = usePlayerProfile();
|
||||
const { hubRoute } = useInboxFeature();
|
||||
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
@@ -125,9 +122,6 @@ const balanceAmountClass = computed(() => {
|
||||
|
||||
<template v-else>
|
||||
<div class="wallet-banner">
|
||||
<img class="wallet-banner-img" :src="walletBg" alt="" />
|
||||
<div class="wallet-banner-scrim" aria-hidden="true" />
|
||||
|
||||
<div class="card-overlay">
|
||||
<div class="bank-card-top">
|
||||
<div class="bank-card-top-left">
|
||||
@@ -144,72 +138,55 @@ const balanceAmountClass = computed(() => {
|
||||
</div>
|
||||
|
||||
<div class="bank-card-balance">
|
||||
<span class="bank-card-label">当前余额</span>
|
||||
<span class="bank-card-label">{{ t('wallet.balance') }}</span>
|
||||
<p class="bank-card-number balance-amount" :class="balanceAmountClass">{{ balanceDisplay }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bank-card-footer">
|
||||
<div class="bank-card-field">
|
||||
<span class="bank-card-label">持卡人</span>
|
||||
<span class="bank-card-label">{{ t('wallet.card_holder') }}</span>
|
||||
<span class="bank-card-holder">{{ profileRaw?.username }}</span>
|
||||
</div>
|
||||
<div class="bank-card-field bank-card-field--center">
|
||||
<span class="bank-card-label">累计返水</span>
|
||||
<span class="bank-card-label">{{ t('cashback.total_received') }}</span>
|
||||
<span class="bank-card-stat">{{ formatMoney(cashbackTotal, locale) }}</span>
|
||||
</div>
|
||||
<div class="bank-card-field bank-card-field--right">
|
||||
<span class="bank-card-label">未结算</span>
|
||||
<span class="bank-card-label">{{ t('wallet.unsettled') }}</span>
|
||||
<span class="bank-card-stat">{{ formatMoney(profileRaw?.wallet?.frozenBalance, locale) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<RouterLink to="/wallet/detail" class="qa-item">
|
||||
<span class="qa-icon qa-icon--wallet">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="24" height="24"><path d="M21 12V7H5a2 2 0 010-4h14v4"/><path d="M3 5v14a2 2 0 002 2h16v-5"/><path d="M18 12a2 2 0 100 4 2 2 0 000-4z"/></svg>
|
||||
</span>
|
||||
<span class="qa-label">{{ t('wallet.view_all') }}</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/wallet/recharge" class="qa-item">
|
||||
<span class="qa-icon qa-icon--recharge">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="24" height="24"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>
|
||||
</span>
|
||||
<span class="qa-label">{{ t('recharge.title') }}</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/profile/cashbacks" class="qa-item">
|
||||
<span class="qa-icon qa-icon--cashback">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="24" height="24"><circle cx="12" cy="12" r="8"/><path d="M12 8v4l2.5 2.5"/></svg>
|
||||
</span>
|
||||
<span class="qa-label">{{ t('wallet.view_cashbacks') }}</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/wallet/recharge/history" class="qa-item">
|
||||
<span class="qa-icon qa-icon--history">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="24" height="24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6M9 13h6M9 17h4"/></svg>
|
||||
</span>
|
||||
<span class="qa-label">{{ t('recharge.history_title') }}</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<section class="settings-group">
|
||||
<RouterLink to="/wallet/detail" class="settings-cell settings-cell--gold-entry">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path d="M9 5H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V9" />
|
||||
<path d="M9 5a2 2 0 012-2h2a2 2 0 012 2v0M9 12h6M9 16h4" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('wallet.view_all') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/profile/cashbacks" class="settings-cell settings-cell--gold-entry">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="8" />
|
||||
<path d="M12 8v4l2.5 2.5" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('wallet.view_cashbacks') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink :to="hubRoute" class="settings-cell settings-cell--gold-entry">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path d="M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v11A2.5 2.5 0 0 1 17.5 20H6.5A2.5 2.5 0 0 1 4 17.5v-11Z" />
|
||||
<path d="m4 7 8 5.5L20 7" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('messages.title') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/wallet/recharge/history" class="settings-cell settings-cell--gold-entry">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path d="M12 8v8M8 12h8" />
|
||||
<circle cx="12" cy="12" r="8" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('recharge.history_title') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/profile/edit" class="settings-cell">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
@@ -287,7 +264,7 @@ const balanceAmountClass = computed(() => {
|
||||
}
|
||||
|
||||
.profile-page {
|
||||
padding: 8px 0 12px;
|
||||
padding: 6px 0 10px;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
@@ -307,7 +284,7 @@ const balanceAmountClass = computed(() => {
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -315,75 +292,36 @@ const balanceAmountClass = computed(() => {
|
||||
.retry-btn {
|
||||
padding: 8px 24px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--primary);
|
||||
border: 1px solid #003D6B;
|
||||
background: transparent;
|
||||
color: var(--primary-light);
|
||||
color: #005B9F;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.retry-btn:active {
|
||||
background: rgba(200, 168, 78, 0.15);
|
||||
background: rgba(0, 61, 107, 0.08);
|
||||
}
|
||||
|
||||
.wallet-banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
aspect-ratio: 2 / 1;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 10px;
|
||||
aspect-ratio: 1.75 / 1;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 3px 10px rgba(0, 0, 0, 0.28),
|
||||
0 1px 3px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.wallet-banner::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.22);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wallet-banner-img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
object-position: center center;
|
||||
transform: scale(1.12);
|
||||
transform-origin: center center;
|
||||
filter: brightness(0.86);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wallet-banner-scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0.4) 0%,
|
||||
rgba(0, 0, 0, 0.18) 42%,
|
||||
rgba(0, 0, 0, 0.46) 100%
|
||||
);
|
||||
box-shadow: 0 4px 16px rgba(0, 61, 107, 0.18);
|
||||
background: linear-gradient(135deg, #003D6B 0%, #005B9F 52%, #002847 100%);
|
||||
}
|
||||
|
||||
.card-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 14px 16px 12px;
|
||||
height: 100%;
|
||||
padding: 10px 14px 10px;
|
||||
line-height: 1.3;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
@@ -404,8 +342,8 @@ const balanceAmountClass = computed(() => {
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
@@ -420,17 +358,16 @@ const balanceAmountClass = computed(() => {
|
||||
.brand-name {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0.14em;
|
||||
line-height: 1;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.bank-card-type {
|
||||
font-size: 8px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
color: rgba(212, 175, 55, 0.65);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-style: italic;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -441,9 +378,8 @@ const balanceAmountClass = computed(() => {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
line-height: 1.3;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.recharge-btn {
|
||||
@@ -453,20 +389,19 @@ const balanceAmountClass = computed(() => {
|
||||
gap: 3px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 14px;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border: 1px solid rgba(212, 175, 55, 0.35);
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #FFFFFF;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
backdrop-filter: blur(6px);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.recharge-btn:active {
|
||||
background: rgba(212, 175, 55, 0.15);
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.recharge-icon {
|
||||
@@ -481,8 +416,8 @@ const balanceAmountClass = computed(() => {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
padding: 2px 0;
|
||||
gap: 5px;
|
||||
padding: 1px 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.bank-card-number {
|
||||
@@ -491,39 +426,38 @@ const balanceAmountClass = computed(() => {
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--primary-light);
|
||||
color: #FFFFFF;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.balance-amount--xl { font-size: clamp(24px, 6.2vw, 30px); }
|
||||
.balance-amount--lg { font-size: clamp(21px, 5.6vw, 26px); }
|
||||
.balance-amount--md { font-size: clamp(18px, 4.9vw, 22px); }
|
||||
.balance-amount--sm { font-size: clamp(16px, 4.2vw, 19px); }
|
||||
.balance-amount--xs { font-size: clamp(14px, 3.6vw, 16px); }
|
||||
.balance-amount--xl { font-size: clamp(22px, 5.6vw, 27px); }
|
||||
.balance-amount--lg { font-size: clamp(19px, 5vw, 24px); }
|
||||
.balance-amount--md { font-size: clamp(16px, 4.4vw, 20px); }
|
||||
.balance-amount--sm { font-size: clamp(14px, 3.8vw, 17px); }
|
||||
.balance-amount--xs { font-size: clamp(12px, 3.2vw, 14px); }
|
||||
|
||||
.bank-card-footer {
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
gap: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.bank-card-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -533,11 +467,10 @@ const balanceAmountClass = computed(() => {
|
||||
.bank-card-holder {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: #FFFFFF;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
line-height: 1.2;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@@ -545,16 +478,93 @@ const balanceAmountClass = computed(() => {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #fff;
|
||||
color: #FFFFFF;
|
||||
line-height: 1.2;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.qa-item {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 4px 10px;
|
||||
border-radius: 10px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #CBD5E1;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.qa-item:active {
|
||||
transform: scale(0.96);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.qa-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qa-icon--wallet {
|
||||
background: rgba(0, 61, 107, 0.1);
|
||||
color: #003D6B;
|
||||
}
|
||||
|
||||
.qa-icon--recharge {
|
||||
background: rgba(5, 150, 105, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.qa-icon--cashback {
|
||||
background: rgba(248, 151, 31, 0.1);
|
||||
color: #E8870A;
|
||||
}
|
||||
|
||||
.qa-icon--history {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: #6366F1;
|
||||
}
|
||||
|
||||
.qa-icon--inbox {
|
||||
background: rgba(14, 165, 233, 0.1);
|
||||
color: #0EA5E9;
|
||||
}
|
||||
|
||||
.qa-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #1A1A2E;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
min-height: 2.4em;
|
||||
max-width: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -562,16 +572,16 @@ const balanceAmountClass = computed(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
background: none;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
color: #1A1A2E;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.settings-cell:last-child {
|
||||
@@ -579,41 +589,15 @@ const balanceAmountClass = computed(() => {
|
||||
}
|
||||
|
||||
.settings-cell:active {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.settings-cell--gold-entry {
|
||||
position: relative;
|
||||
min-height: 50px;
|
||||
border-color: rgba(212, 175, 55, 0.18);
|
||||
border-bottom-color: rgba(212, 175, 55, 0.18);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(212, 175, 55, 0.08), rgba(20, 20, 20, 0.65) 46%, rgba(212, 175, 55, 0.04));
|
||||
box-shadow:
|
||||
inset 1px 0 0 rgba(212, 175, 55, 0.22),
|
||||
inset 0 1px 0 rgba(255, 244, 200, 0.05);
|
||||
}
|
||||
|
||||
.settings-cell--gold-entry:active {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(212, 175, 55, 0.12), rgba(20, 20, 20, 0.74) 46%, rgba(212, 175, 55, 0.06));
|
||||
}
|
||||
|
||||
.settings-cell--gold-entry .cell-label {
|
||||
color: #f4dc8b;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.settings-cell--gold-entry .cell-chevron {
|
||||
color: rgba(240, 216, 117, 0.78);
|
||||
background: #F5F7FA;
|
||||
}
|
||||
|
||||
.settings-cell--stack {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px 14px;
|
||||
gap: 8px;
|
||||
padding: 10px 14px 12px;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
@@ -628,11 +612,7 @@ const balanceAmountClass = computed(() => {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.settings-cell--gold-entry .cell-icon {
|
||||
color: var(--primary-light);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.cell-head {
|
||||
@@ -642,11 +622,11 @@ const balanceAmountClass = computed(() => {
|
||||
}
|
||||
|
||||
.cell-label {
|
||||
color: var(--text);
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.cell-chevron {
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
font-weight: 300;
|
||||
@@ -664,8 +644,8 @@ const balanceAmountClass = computed(() => {
|
||||
}
|
||||
|
||||
.rules-cell .rules-body {
|
||||
padding: 0 16px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 14px 12px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.lang-segment {
|
||||
@@ -679,43 +659,43 @@ const balanceAmountClass = computed(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-height: 34px;
|
||||
padding: 0 6px;
|
||||
min-height: 30px;
|
||||
padding: 0 5px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #0a0a0a;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
color: #6B7280;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lang-opt.active {
|
||||
border-color: var(--border-gold-soft);
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-color: #003D6B;
|
||||
color: #003D6B;
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
min-height: 44px;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
color: var(--danger);
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
min-height: 40px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #DC2626;
|
||||
background: #FFFFFF;
|
||||
color: #DC2626;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logout-btn:active {
|
||||
background: rgba(255, 69, 58, 0.08);
|
||||
background: rgba(220, 38, 38, 0.06);
|
||||
}
|
||||
|
||||
.rules-body {
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.rules-body p {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api from '../api';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
import { findDepositOrderById, enrichDepositOrder } from '../utils/depositOrderLookup';
|
||||
import {
|
||||
auditActionTone,
|
||||
auditActorSecondary,
|
||||
formatDepositAuditRemark,
|
||||
shouldShowAuditRejectInTimeline,
|
||||
} from '../utils/depositAuditDisplay';
|
||||
import { useDepositNotifications } from '../composables/useDepositNotifications';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { t, locale } = useI18n();
|
||||
const { trackPendingOrder, pollOnce } = useDepositNotifications();
|
||||
|
||||
interface DepositAuditLog {
|
||||
id: string;
|
||||
@@ -68,10 +68,6 @@ async function fetchOrders(p = 1) {
|
||||
|
||||
if (p === 1) {
|
||||
items.value = newItems;
|
||||
for (const order of newItems) {
|
||||
if (order.status === 'PENDING') trackPendingOrder(order.id);
|
||||
}
|
||||
void pollOnce();
|
||||
} else {
|
||||
items.value = [...items.value, ...newItems];
|
||||
}
|
||||
@@ -91,7 +87,7 @@ const { pullDistance, spinning, progress } = usePullToRefresh({
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrders(1);
|
||||
void fetchOrders(1).then(() => openDetailFromQuery());
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasMore.value && !loading.value) {
|
||||
@@ -103,6 +99,10 @@ onMounted(() => {
|
||||
if (sentinel.value) observer.observe(sentinel.value);
|
||||
});
|
||||
|
||||
watch(() => route.query.orderId, () => {
|
||||
void openDetailFromQuery();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
observer?.disconnect();
|
||||
});
|
||||
@@ -127,14 +127,24 @@ function goRecharge() {
|
||||
router.push('/wallet/recharge');
|
||||
}
|
||||
|
||||
function openDetail(order: DepositOrder) {
|
||||
selectedOrder.value = order;
|
||||
async function openDetail(order: DepositOrder) {
|
||||
selectedOrder.value = await enrichDepositOrder(order);
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
selectedOrder.value = null;
|
||||
}
|
||||
|
||||
async function openDetailFromQuery() {
|
||||
const orderId = route.query.orderId;
|
||||
if (typeof orderId !== 'string' || !orderId) return;
|
||||
const existing = items.value.find((order) => order.id === orderId);
|
||||
const order = existing ?? await findDepositOrderById(orderId);
|
||||
if (order) openDetail(order);
|
||||
const { orderId: _removed, ...rest } = route.query;
|
||||
void router.replace({ path: route.path, query: rest });
|
||||
}
|
||||
|
||||
function reapply(order: DepositOrder) {
|
||||
const query: Record<string, string> = {
|
||||
orderId: order.id,
|
||||
@@ -322,10 +332,10 @@ function auditStepCount(order: DepositOrder) {
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-if="selectedOrder" class="detail-overlay" @click.self="closeDetail">
|
||||
<div class="detail-modal" role="dialog" aria-modal="true" :aria-label="t('recharge.audit_title')">
|
||||
<div class="detail-modal" role="dialog" aria-modal="true" :aria-label="t('recharge.order_detail')">
|
||||
<button type="button" class="detail-close" :aria-label="t('common.close')" @click="closeDetail">✕</button>
|
||||
|
||||
<h3 class="detail-title">{{ t('recharge.audit_title') }}</h3>
|
||||
<h3 class="detail-title">{{ t('recharge.order_detail') }}</h3>
|
||||
|
||||
<div class="detail-summary">
|
||||
<div class="detail-summary-head">
|
||||
@@ -415,25 +425,26 @@ function auditStepCount(order: DepositOrder) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.recharge-history-page { padding: 0 0 24px; }
|
||||
.recharge-history-page { padding: 0 16px 24px; }
|
||||
.page-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; }
|
||||
.page-header h2 { margin: 0; font-size: 17px; font-weight: 700; }
|
||||
.back-btn { background: none; border: none; color: var(--primary-light); font-size: 24px; cursor: pointer; padding: 0 8px; }
|
||||
.recharge-btn { background: none; border: none; color: var(--primary-light); font-size: 13px; cursor: pointer; font-weight: 600; }
|
||||
.page-header h2 { margin: 0; font-size: 17px; font-weight: 700; color: #1A1A2E; }
|
||||
.back-btn { background: none; border: none; color: #003D6B; font-size: 24px; cursor: pointer; padding: 0 8px; }
|
||||
.recharge-btn { background: none; border: none; color: #003D6B; font-size: 13px; cursor: pointer; font-weight: 600; }
|
||||
.state { display: flex; justify-content: center; padding: 48px; }
|
||||
.empty { text-align: center; color: #666; padding: 48px 16px; font-weight: 600; }
|
||||
.empty { text-align: center; color: #6B7280; padding: 48px 16px; font-weight: 600; }
|
||||
.pull-indicator { display: flex; align-items: center; justify-content: center; overflow: hidden; transition: height 0.15s ease; }
|
||||
|
||||
.order-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.order-card {
|
||||
background: linear-gradient(135deg, #1a1810 0%, #1f1b0e 40%, #16140c 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.2);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.order-card:active {
|
||||
opacity: 0.92;
|
||||
@@ -443,69 +454,66 @@ function auditStepCount(order: DepositOrder) {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.6), transparent);
|
||||
background: linear-gradient(90deg, transparent, #003D6B, transparent);
|
||||
}
|
||||
.order-card.rejected {
|
||||
background: #141414;
|
||||
border-color: rgba(245, 108, 108, 0.22);
|
||||
background: #FFFFFF;
|
||||
border-color: rgba(220, 38, 38, 0.25);
|
||||
}
|
||||
.order-card.rejected::before {
|
||||
background: linear-gradient(90deg, transparent, rgba(245, 108, 108, 0.35), transparent);
|
||||
background: linear-gradient(90deg, transparent, #DC2626, transparent);
|
||||
}
|
||||
.order-card.rejected .order-amount {
|
||||
background: none;
|
||||
-webkit-background-clip: unset;
|
||||
background-clip: unset;
|
||||
color: #bbb;
|
||||
color: #6B7280;
|
||||
}
|
||||
.order-card.rejected .info-label {
|
||||
color: #888;
|
||||
color: #6B7280;
|
||||
}
|
||||
.order-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||||
.method-badge { padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 700; }
|
||||
.method-badge.bank { background: rgba(30, 58, 95, 0.6); color: #66b1ff; }
|
||||
.method-badge.usdt { background: rgba(26, 58, 42, 0.6); color: #67c23a; }
|
||||
.method-badge.bank { background: rgba(0, 61, 107, 0.08); color: #005B9F; }
|
||||
.method-badge.usdt { background: rgba(5, 150, 105, 0.08); color: #059669; }
|
||||
.status-badge { font-size: 12px; font-weight: 700; }
|
||||
.status-pending { color: #e6a23c; }
|
||||
.status-approved { color: #67c23a; }
|
||||
.status-rejected { color: #f56c6c; }
|
||||
.status-pending { color: #F8971F; }
|
||||
.status-approved { color: #059669; }
|
||||
.status-rejected { color: #DC2626; }
|
||||
.order-body { }
|
||||
.order-amount {
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
margin-bottom: 4px;
|
||||
background: linear-gradient(135deg, #f0d060, #d4a830);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
color: #003D6B;
|
||||
}
|
||||
.approved-amount { font-size: 12px; color: #67c23a; margin-bottom: 6px; font-weight: 600; }
|
||||
.approved-amount { font-size: 12px; color: #059669; margin-bottom: 6px; font-weight: 600; }
|
||||
.order-info-row { margin-bottom: 8px; }
|
||||
.info-label { font-size: 13px; color: rgba(212, 175, 55, 0.7); font-weight: 600; }
|
||||
.info-label { font-size: 13px; color: #003D6B; font-weight: 600; }
|
||||
.order-times { display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; }
|
||||
.time-row { display: flex; justify-content: space-between; align-items: center; }
|
||||
.time-label { font-size: 11px; color: #888; }
|
||||
.time-value { font-size: 11px; color: #aaa; font-variant-numeric: tabular-nums; }
|
||||
.time-label { font-size: 11px; color: #6B7280; }
|
||||
.time-value { font-size: 11px; color: #1A1A2E; font-variant-numeric: tabular-nums; }
|
||||
.order-remark {
|
||||
font-size: 12px;
|
||||
color: rgba(212, 175, 55, 0.8);
|
||||
background: rgba(212, 175, 55, 0.06);
|
||||
color: #003D6B;
|
||||
background: rgba(0, 61, 107, 0.04);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
margin-top: 6px;
|
||||
border-left: 2px solid rgba(212, 175, 55, 0.3);
|
||||
border-left: 2px solid #003D6B;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
.reject-reason {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #c07070;
|
||||
color: #DC2626;
|
||||
background: transparent;
|
||||
padding: 6px 0 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-top: 1px solid rgba(245, 108, 108, 0.18);
|
||||
border-top: 1px solid rgba(220, 38, 38, 0.18);
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -516,16 +524,16 @@ function auditStepCount(order: DepositOrder) {
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(212, 175, 55, 0.12);
|
||||
border-top: 1px solid #E5E7EB;
|
||||
}
|
||||
.card-detail-summary {
|
||||
font-size: 11px;
|
||||
color: rgba(212, 175, 55, 0.65);
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
}
|
||||
.card-detail-link {
|
||||
font-size: 11px;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -538,7 +546,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
background: none;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
color: var(--primary-light);
|
||||
color: #003D6B;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -556,7 +564,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
.end-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
color: #6B7280;
|
||||
font-weight: 600;
|
||||
padding: 16px 0 4px;
|
||||
letter-spacing: 0.03em;
|
||||
@@ -566,7 +574,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
background: rgba(0, 0, 0, 0.48);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
@@ -579,12 +587,12 @@ function auditStepCount(order: DepositOrder) {
|
||||
max-width: 480px;
|
||||
max-height: min(88vh, 720px);
|
||||
overflow-y: auto;
|
||||
background: #141414;
|
||||
border: 1px solid #2a2a2a;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-bottom: none;
|
||||
border-radius: 14px 14px 0 0;
|
||||
padding: 16px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.28);
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.detail-close {
|
||||
@@ -596,7 +604,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
color: #777;
|
||||
color: #6B7280;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
@@ -606,15 +614,15 @@ function auditStepCount(order: DepositOrder) {
|
||||
margin: 0 28px 12px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #e8e8e8;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
|
||||
.detail-summary {
|
||||
margin-bottom: 14px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid #222;
|
||||
background: #F5F7FA;
|
||||
border: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.detail-summary-head {
|
||||
@@ -628,13 +636,13 @@ function auditStepCount(order: DepositOrder) {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
color: var(--primary-light, #d4af37);
|
||||
color: #003D6B;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.detail-method-name {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
color: #6B7280;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -649,13 +657,13 @@ function auditStepCount(order: DepositOrder) {
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
color: #6B7280;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
color: #1A1A2E;
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -663,12 +671,12 @@ function auditStepCount(order: DepositOrder) {
|
||||
.detail-summary .order-remark {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
color: #6B7280;
|
||||
background: transparent;
|
||||
padding: 6px 0 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-top: 1px solid #222;
|
||||
border-top: 1px solid #E5E7EB;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -676,12 +684,12 @@ function auditStepCount(order: DepositOrder) {
|
||||
.detail-summary .reject-reason {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: #b08888;
|
||||
color: #DC2626;
|
||||
background: transparent;
|
||||
padding: 6px 0 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-top: 1px solid rgba(245, 108, 108, 0.15);
|
||||
border-top: 1px solid rgba(220, 38, 38, 0.15);
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -694,14 +702,14 @@ function auditStepCount(order: DepositOrder) {
|
||||
.detail-audit {
|
||||
margin-top: 2px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #222;
|
||||
border-top: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.detail-audit-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
color: #6B7280;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
@@ -729,7 +737,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: #555;
|
||||
background: #E5E7EB;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -738,7 +746,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
width: 1px;
|
||||
min-height: 10px;
|
||||
margin: 3px 0;
|
||||
background: #2a2a2a;
|
||||
background: #E5E7EB;
|
||||
}
|
||||
|
||||
.audit-step-body {
|
||||
@@ -761,13 +769,13 @@ function auditStepCount(order: DepositOrder) {
|
||||
.audit-step-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #ccc;
|
||||
color: #1A1A2E;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.audit-step-time {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
color: #6B7280;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
@@ -776,7 +784,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
.audit-step-actor {
|
||||
margin: 2px 0 0;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
color: #6B7280;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -784,14 +792,14 @@ function auditStepCount(order: DepositOrder) {
|
||||
margin: 3px 0 0;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #7eb87a;
|
||||
color: #059669;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.audit-step-note {
|
||||
margin: 4px 0 0;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
color: #6B7280;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -809,35 +817,35 @@ function auditStepCount(order: DepositOrder) {
|
||||
|
||||
.audit-step-box--reject {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(245, 108, 108, 0.22);
|
||||
border: 1px solid rgba(220, 38, 38, 0.22);
|
||||
}
|
||||
|
||||
.audit-step-box-label {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #c07070;
|
||||
color: #DC2626;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.audit-step-box-text {
|
||||
font-size: 11px;
|
||||
color: #b08888;
|
||||
color: #DC2626;
|
||||
}
|
||||
|
||||
.audit-step--submitted .audit-dot {
|
||||
background: #c9a227;
|
||||
background: #003D6B;
|
||||
}
|
||||
.audit-step--approved .audit-dot {
|
||||
background: #5fad5a;
|
||||
background: #059669;
|
||||
}
|
||||
.audit-step--rejected .audit-dot {
|
||||
background: #d06060;
|
||||
background: #DC2626;
|
||||
}
|
||||
.audit-step--revoked .audit-dot {
|
||||
background: #777;
|
||||
background: #6B7280;
|
||||
}
|
||||
.audit-step--reopened .audit-dot {
|
||||
background: #c9a227;
|
||||
background: #003D6B;
|
||||
}
|
||||
|
||||
.modal-reapply-btn {
|
||||
|
||||
@@ -127,8 +127,7 @@ async function compressScreenshot(file: File): Promise<File> {
|
||||
throw new Error('COMPRESS_FAILED');
|
||||
}
|
||||
|
||||
// 重命名文件后缀为 .webp
|
||||
const name = file.name.replace(/\.[^/.]+$/, "") + '.webp';
|
||||
const name = file.name.replace(/\.[^/.]+$/, '') + '.webp';
|
||||
return new File([compressed], name, {
|
||||
type: 'image/webp',
|
||||
lastModified: Date.now(),
|
||||
@@ -380,15 +379,15 @@ onMounted(fetchMethods);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.recharge-page { padding: 0 0 24px; }
|
||||
.recharge-page { padding: 0 12px 24px; }
|
||||
|
||||
.page-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 8px 0 12px;
|
||||
}
|
||||
.page-header h2 { margin: 0; font-size: 16px; font-weight: 700; }
|
||||
.back-btn { background: none; border: none; color: var(--primary-light); font-size: 22px; cursor: pointer; padding: 0 6px; }
|
||||
.history-btn { background: none; border: none; color: var(--primary-light); font-size: 12px; cursor: pointer; font-weight: 600; }
|
||||
.page-header h2 { margin: 0; font-size: 16px; font-weight: 700; color: #1A1A2E; }
|
||||
.back-btn { background: none; border: none; color: #003D6B; font-size: 22px; cursor: pointer; padding: 0 6px; }
|
||||
.history-btn { background: none; border: none; color: #003D6B; font-size: 12px; cursor: pointer; font-weight: 600; }
|
||||
|
||||
.state { display: flex; justify-content: center; padding: 48px; }
|
||||
|
||||
@@ -398,24 +397,24 @@ onMounted(fetchMethods);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: #ffd0d0;
|
||||
background: rgba(245, 108, 108, 0.12);
|
||||
border: 1px solid rgba(245, 108, 108, 0.25);
|
||||
color: #DC2626;
|
||||
background: rgba(220, 38, 38, 0.06);
|
||||
border: 1px solid rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
.type-tabs {
|
||||
display: flex; margin-bottom: 12px;
|
||||
border-radius: 6px; overflow: hidden;
|
||||
border: 1px solid rgba(212, 175, 55, 0.2);
|
||||
border: 1px solid #E5E7EB;
|
||||
}
|
||||
.tab {
|
||||
flex: 1; padding: 8px; border: none;
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
color: var(--text-muted); font-weight: 700; font-size: 13px;
|
||||
background: #FFFFFF;
|
||||
color: #6B7280; font-weight: 700; font-size: 13px;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--primary-light); color: #000;
|
||||
background: #003D6B; color: #FFFFFF;
|
||||
}
|
||||
|
||||
.methods-list {
|
||||
@@ -423,19 +422,19 @@ onMounted(fetchMethods);
|
||||
}
|
||||
.method-pill {
|
||||
display: flex; flex-direction: column; gap: 1px;
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
border: 1px solid var(--border);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 6px; padding: 6px 12px;
|
||||
text-align: left; cursor: pointer;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.method-pill.selected {
|
||||
border-color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.06);
|
||||
border-color: #0066CC;
|
||||
background: rgba(0, 102, 204, 0.04);
|
||||
}
|
||||
.pill-name { font-weight: 700; font-size: 12px; color: var(--text); }
|
||||
.pill-sub { font-size: 10px; color: var(--text-muted); }
|
||||
.empty-methods { text-align: center; color: var(--text-muted); padding: 20px; font-size: 12px; }
|
||||
.pill-name { font-weight: 700; font-size: 12px; color: #1A1A2E; }
|
||||
.pill-sub { font-size: 10px; color: #6B7280; }
|
||||
.empty-methods { text-align: center; color: #6B7280; padding: 20px; font-size: 12px; }
|
||||
|
||||
.bank-card-wrap {
|
||||
margin-bottom: 16px;
|
||||
@@ -451,15 +450,9 @@ onMounted(fetchMethods);
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 100% 0%, rgba(212, 175, 55, 0.22), transparent 55%),
|
||||
radial-gradient(ellipse 50% 50% at 0% 100%, rgba(212, 175, 55, 0.1), transparent 50%),
|
||||
linear-gradient(135deg, #2a2218 0%, #1a1610 35%, #12100c 70%, #0a0908 100%);
|
||||
border: 1px solid rgba(212, 175, 55, 0.35);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(0, 0, 0, 0.55),
|
||||
0 2px 8px rgba(212, 175, 55, 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
background: linear-gradient(135deg, #003D6B 0%, #005B9F 50%, #003D6B 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.bank-card-shine {
|
||||
@@ -468,9 +461,9 @@ onMounted(fetchMethods);
|
||||
background: linear-gradient(
|
||||
115deg,
|
||||
transparent 30%,
|
||||
rgba(255, 255, 255, 0.04) 45%,
|
||||
rgba(255, 255, 255, 0.08) 50%,
|
||||
rgba(255, 255, 255, 0.04) 55%,
|
||||
rgba(255, 255, 255, 0.03) 45%,
|
||||
rgba(255, 255, 255, 0.06) 50%,
|
||||
rgba(255, 255, 255, 0.03) 55%,
|
||||
transparent 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
@@ -479,7 +472,7 @@ onMounted(fetchMethods);
|
||||
.bank-card-deco {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(212, 175, 55, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -488,7 +481,7 @@ onMounted(fetchMethods);
|
||||
height: 180px;
|
||||
right: -60px;
|
||||
bottom: -80px;
|
||||
background: radial-gradient(circle, rgba(212, 175, 55, 0.08), transparent 70%);
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.06), transparent 70%);
|
||||
}
|
||||
|
||||
.bank-card-deco--2 {
|
||||
@@ -496,7 +489,7 @@ onMounted(fetchMethods);
|
||||
height: 100px;
|
||||
right: 40px;
|
||||
bottom: 20px;
|
||||
border-color: rgba(212, 175, 55, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.bank-card-top,
|
||||
@@ -516,10 +509,9 @@ onMounted(fetchMethods);
|
||||
.bank-card-bank {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
color: #FFFFFF;
|
||||
text-align: right;
|
||||
letter-spacing: 0.5px;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||
line-height: 1.3;
|
||||
max-width: 58%;
|
||||
word-break: break-all;
|
||||
@@ -538,8 +530,7 @@ onMounted(fetchMethods);
|
||||
font-size: clamp(15px, 4.2vw, 19px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
color: var(--primary-light);
|
||||
text-shadow: 0 0 12px rgba(240, 216, 117, 0.25);
|
||||
color: #FFFFFF;
|
||||
word-break: break-all;
|
||||
line-height: 1.3;
|
||||
}
|
||||
@@ -549,9 +540,9 @@ onMounted(fetchMethods);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(212, 175, 55, 0.35);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #FFFFFF;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -566,7 +557,7 @@ onMounted(fetchMethods);
|
||||
|
||||
.bank-card-copy:active {
|
||||
opacity: 0.65;
|
||||
background: rgba(212, 175, 55, 0.12);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.bank-card-bottom {
|
||||
@@ -588,15 +579,14 @@ onMounted(fetchMethods);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.bank-card-holder-name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0.04em;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@@ -605,87 +595,89 @@ onMounted(fetchMethods);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.18em;
|
||||
color: rgba(212, 175, 55, 0.55);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.method-info {
|
||||
background: rgba(17, 17, 17, 0.9);
|
||||
background: #FFFFFF;
|
||||
border-radius: 8px; padding: 10px 12px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid #E5E7EB;
|
||||
}
|
||||
.info-row {
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-label { font-size: 11px; color: var(--text-muted); flex-shrink: 0; }
|
||||
.info-label { font-size: 11px; color: #6B7280; flex-shrink: 0; }
|
||||
.info-value {
|
||||
font-size: 13px; font-weight: 600;
|
||||
word-break: break-all; text-align: right;
|
||||
max-width: 60%;
|
||||
color: #1A1A2E;
|
||||
}
|
||||
.copyable {
|
||||
cursor: pointer; color: var(--primary-light);
|
||||
cursor: pointer; color: #0066CC;
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
}
|
||||
.copyable:active { opacity: 0.6; }
|
||||
.copy-icon { width: 14px; height: 14px; flex-shrink: 0; }
|
||||
.qr-container { display: flex; justify-content: center; padding: 8px 0 4px; }
|
||||
.qr-image { width: 140px; height: 140px; object-fit: contain; border-radius: 6px; background: #fff; padding: 6px; }
|
||||
.qr-image { width: 140px; height: 140px; object-fit: contain; border-radius: 6px; background: #fff; padding: 6px; border: 1px solid #E5E7EB; }
|
||||
|
||||
.form-section { margin-bottom: 12px; }
|
||||
.form-section label {
|
||||
display: block; font-size: 11px; font-weight: 700;
|
||||
color: var(--text-muted); margin-bottom: 4px;
|
||||
color: #6B7280; margin-bottom: 4px;
|
||||
}
|
||||
.amount-input {
|
||||
width: 100%; padding: 10px; background: #111;
|
||||
border: 1px solid var(--border); border-radius: 6px;
|
||||
color: #fff; font-size: 16px; font-weight: 700;
|
||||
width: 100%; padding: 10px; background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB; border-radius: 6px;
|
||||
color: #1A1A2E; font-size: 16px; font-weight: 700;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.amount-input:focus { border-color: var(--primary-light); outline: none; }
|
||||
.amount-input:focus { border-color: #0066CC; outline: none; box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.15); }
|
||||
|
||||
.upload-area {
|
||||
border: 1px dashed rgba(212, 175, 55, 0.3);
|
||||
border: 1px dashed #0066CC;
|
||||
border-radius: 6px; padding: 16px;
|
||||
text-align: center; position: relative;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer;
|
||||
background: rgba(0, 102, 204, 0.02);
|
||||
}
|
||||
.upload-area input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
|
||||
.upload-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.compress-hint { font-size: 12px; color: var(--primary-light); }
|
||||
.upload-hint { font-size: 12px; color: #6B7280; }
|
||||
.compress-hint { font-size: 12px; color: #003D6B; }
|
||||
.screenshot-preview { position: relative; display: inline-block; }
|
||||
.screenshot-preview img { max-width: 100%; max-height: 160px; border-radius: 6px; }
|
||||
.remove-btn {
|
||||
position: absolute; top: 4px; right: 4px;
|
||||
background: rgba(0,0,0,0.7); border: none; color: #fff;
|
||||
background: rgba(0, 0, 0, 0.6); border: none; color: #fff;
|
||||
width: 20px; height: 20px; border-radius: 50%;
|
||||
cursor: pointer; font-size: 11px;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%; padding: 12px;
|
||||
background: linear-gradient(135deg, #f0d060, #d4a830);
|
||||
color: #000; border: none; border-radius: 6px;
|
||||
background: #003D6B;
|
||||
color: #FFFFFF; border: none; border-radius: 6px;
|
||||
font-size: 14px; font-weight: 800;
|
||||
cursor: pointer; margin-top: 8px;
|
||||
box-shadow: 0 2px 8px rgba(212, 175, 55, 0.2);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.btn-submit:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
.success-state { text-align: center; padding: 40px 16px; }
|
||||
.success-icon { font-size: 40px; color: #67c23a; margin-bottom: 10px; }
|
||||
.success-state h3 { margin: 0 0 6px; font-size: 16px; }
|
||||
.order-no { font-family: monospace; color: var(--primary-light); font-size: 13px; margin: 4px 0; }
|
||||
.success-hint { font-size: 12px; color: var(--text-muted); margin-bottom: 20px; }
|
||||
.success-icon { font-size: 40px; color: #059669; margin-bottom: 10px; }
|
||||
.success-state h3 { margin: 0 0 6px; font-size: 16px; color: #1A1A2E; }
|
||||
.order-no { font-family: monospace; color: #003D6B; font-size: 13px; margin: 4px 0; }
|
||||
.success-hint { font-size: 12px; color: #6B7280; margin-bottom: 20px; }
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #f0d060, #d4a830);
|
||||
color: #000; border: none; border-radius: 6px;
|
||||
background: #003D6B;
|
||||
color: #FFFFFF; border: none; border-radius: 6px;
|
||||
padding: 10px 20px; font-weight: 700; font-size: 13px; cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -155,9 +155,9 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
<template>
|
||||
<div class="wallet-detail-page">
|
||||
<div class="top-bar">
|
||||
<header class="top-bar sub-toolbar">
|
||||
<button class="back-btn" type="button" @click="router.back()">‹ {{ t('history.back') }}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="pull-indicator"
|
||||
@@ -172,7 +172,7 @@ const pullIndicatorStyle = () => ({
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<WalletStatsPanel :items="items" :cashback-total="cashbackTotal" />
|
||||
<WalletStatsPanel class="stats-panel" :items="items" :cashback-total="cashbackTotal" />
|
||||
|
||||
<div class="filter-tabs">
|
||||
<button
|
||||
@@ -195,17 +195,15 @@ const pullIndicatorStyle = () => ({
|
||||
class="tx-row"
|
||||
@click="goDetail(tx)"
|
||||
>
|
||||
<div class="tx-main">
|
||||
<div class="tx-text">
|
||||
<span class="tx-type">{{ txLabel(tx) }}</span>
|
||||
<span v-if="txSubtitle(tx)" class="tx-summary">{{ txSubtitle(tx) }}</span>
|
||||
</div>
|
||||
<div class="tx-col-left">
|
||||
<span class="tx-type">{{ txLabel(tx) }}</span>
|
||||
<span v-if="txSubtitle(tx)" class="tx-summary">{{ txSubtitle(tx) }}</span>
|
||||
<span class="tx-time">{{ new Date(tx.createdAt).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="tx-col-right">
|
||||
<span :class="txAmountClass(tx.amount)">
|
||||
{{ formatMoney(tx.amount, locale) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tx-meta">
|
||||
<span class="tx-time">{{ new Date(tx.createdAt).toLocaleString() }}</span>
|
||||
<span class="tx-arrow">›</span>
|
||||
</div>
|
||||
</button>
|
||||
@@ -233,6 +231,10 @@ const pullIndicatorStyle = () => ({
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
margin-bottom: var(--space-section);
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -240,9 +242,9 @@ const pullIndicatorStyle = () => ({
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary-light, #d4af37);
|
||||
color: var(--text-muted);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
font-weight: 500;
|
||||
padding: 4px 0 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -260,8 +262,8 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--space-section);
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
@@ -271,20 +273,21 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
.filter-tab {
|
||||
flex-shrink: 0;
|
||||
padding: 7px 14px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
background: #141414;
|
||||
border: 1px solid #2a2a2a;
|
||||
transition: all 0.2s;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-color: var(--border-gold-soft);
|
||||
color: var(--text);
|
||||
background: var(--bg-elevated);
|
||||
border-color: var(--border-strong);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.state {
|
||||
@@ -305,14 +308,21 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
.tx-list {
|
||||
margin-bottom: 0;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tx-row {
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
padding: 12px 16px;
|
||||
padding: 12px 14px;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: transparent;
|
||||
@@ -324,21 +334,22 @@ const pullIndicatorStyle = () => ({
|
||||
}
|
||||
|
||||
.tx-row:active {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.tx-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tx-text {
|
||||
.tx-col-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tx-col-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tx-summary {
|
||||
@@ -350,19 +361,12 @@ const pullIndicatorStyle = () => ({
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tx-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tx-type { font-weight: 700; color: var(--text); }
|
||||
.pos { color: var(--primary-light); font-weight: 800; font-size: 15px; }
|
||||
.neg { color: var(--danger); font-weight: 700; }
|
||||
.zero { color: var(--text-muted); font-weight: 700; font-size: 15px; }
|
||||
.tx-time { font-size: 11px; color: var(--text-muted); }
|
||||
.tx-arrow { font-size: 16px; color: #555; font-weight: 700; line-height: 1; }
|
||||
.tx-type { font-weight: 600; color: var(--text); letter-spacing: -0.01em; }
|
||||
.pos { color: var(--success); font-weight: 600; font-size: 14px; font-variant-numeric: tabular-nums; }
|
||||
.neg { color: var(--text); font-weight: 600; font-size: 14px; font-variant-numeric: tabular-nums; }
|
||||
.zero { color: var(--text-muted); font-weight: 600; font-size: 14px; font-variant-numeric: tabular-nums; }
|
||||
.tx-time { font-size: 11px; color: var(--text-dim); }
|
||||
.tx-arrow { font-size: 15px; color: var(--text-dim); font-weight: 400; line-height: 1; }
|
||||
|
||||
.sentinel {
|
||||
height: 1px;
|
||||
@@ -377,7 +381,7 @@ const pullIndicatorStyle = () => ({
|
||||
.end-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
padding: 16px 0 4px;
|
||||
letter-spacing: 0.03em;
|
||||
|
||||
@@ -138,9 +138,9 @@ function goCashbackDetail() {
|
||||
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
|
||||
</div>
|
||||
|
||||
<div class="top-bar">
|
||||
<header class="top-bar sub-toolbar">
|
||||
<button class="back-btn" type="button" @click="router.back()">‹ {{ t('history.back') }}</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="loading" class="state">
|
||||
<GoldSpinner :size="36" />
|
||||
@@ -235,7 +235,7 @@ function goCashbackDetail() {
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary-light, #d4af37);
|
||||
color: var(--primary-light);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
padding: 4px 0 8px;
|
||||
@@ -248,13 +248,13 @@ function goCashbackDetail() {
|
||||
.state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero {
|
||||
border-radius: 14px;
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -262,23 +262,23 @@ function goCashbackDetail() {
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
background: #141414;
|
||||
border: 1px solid #222;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.hero.pos {
|
||||
border-color: rgba(212, 175, 55, 0.25);
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.1), rgba(212, 175, 55, 0.03));
|
||||
border-color: rgba(46, 204, 113, 0.35);
|
||||
background: linear-gradient(135deg, rgba(46, 204, 113, 0.16), rgba(46, 204, 113, 0.05));
|
||||
}
|
||||
|
||||
.hero.neg {
|
||||
border-color: rgba(224, 80, 80, 0.2);
|
||||
background: linear-gradient(135deg, rgba(224, 80, 80, 0.1), rgba(224, 80, 80, 0.03));
|
||||
border-color: rgba(0, 102, 204, 0.25);
|
||||
background: linear-gradient(135deg, rgba(0, 102, 204, 0.08), rgba(0, 102, 204, 0.03));
|
||||
}
|
||||
|
||||
.hero.zero {
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
background: #141414;
|
||||
border-color: var(--border);
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.hero-type {
|
||||
@@ -286,13 +286,13 @@ function goCashbackDetail() {
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: #888;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.hero-summary {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #bbb;
|
||||
color: var(--text);
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -303,28 +303,28 @@ function goCashbackDetail() {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.hero.pos .hero-amount { color: var(--primary-light); }
|
||||
.hero.neg .hero-amount { color: var(--danger); }
|
||||
.hero.zero .hero-amount { color: var(--text-muted, #888); }
|
||||
.hero.pos .hero-amount { color: var(--success); }
|
||||
.hero.neg .hero-amount { color: var(--primary); }
|
||||
.hero.zero .hero-amount { color: var(--text-muted); }
|
||||
|
||||
.hero-time {
|
||||
font-size: 11px;
|
||||
color: #555;
|
||||
color: var(--neutral);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #141414;
|
||||
border: 1px solid #222;
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-card) 16px;
|
||||
margin-bottom: var(--space-section);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: #888;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -341,19 +341,19 @@ function goCashbackDetail() {
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sum-row span:last-child {
|
||||
color: #f0f0f0;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pos { color: var(--primary-light) !important; }
|
||||
.neg { color: var(--danger) !important; }
|
||||
.zero { color: var(--text-muted, #888) !important; }
|
||||
.pos { color: var(--success) !important; }
|
||||
.neg { color: var(--primary) !important; }
|
||||
.zero { color: var(--text-muted) !important; }
|
||||
|
||||
.mono {
|
||||
font-family: ui-monospace, monospace;
|
||||
@@ -370,9 +370,9 @@ function goCashbackDetail() {
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-gold-soft, rgba(212, 175, 55, 0.35));
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid var(--border-active);
|
||||
background: var(--surface-accent);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -381,7 +381,7 @@ function goCashbackDetail() {
|
||||
.tx-id {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
color: var(--text-muted);
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -1,151 +1,225 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onActivated, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, onMounted, onActivated, computed } from 'vue';
|
||||
import { useRouter, RouterLink } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api from '../api';
|
||||
import { formatMoney, formatMoneyCompact } from '../utils/localeDisplay';
|
||||
import { txTypeKey, txDisplayType, txAmountClass, txSummaryLabel } from '../utils/walletTx';
|
||||
import { parseCashbackApiData, sumCashbackAmount } from '../utils/cashback';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
import LocaleFlag from '../components/LocaleFlag.vue';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { useAppLocale } from '../composables/useAppLocale';
|
||||
import { usePlayerProfile } from '../composables/usePlayerProfile';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import { parseCashbackApiData, sumCashbackAmount } from '../utils/cashback';
|
||||
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const { locales, setLocale, initFromUser } = useAppLocale();
|
||||
const { profileRaw, refreshProfile } = usePlayerProfile();
|
||||
|
||||
type Transaction = {
|
||||
transactionType: string;
|
||||
displayType?: string;
|
||||
summary?: string | null;
|
||||
summaryKind?: 'opening_bonus' | null;
|
||||
referenceType?: string | null;
|
||||
amount: string;
|
||||
frozenBefore?: string;
|
||||
frozenAfter?: string;
|
||||
createdAt: string;
|
||||
transactionId: string;
|
||||
};
|
||||
const profileLoading = ref(true);
|
||||
const profileError = ref(false);
|
||||
const rulesExpanded = ref(false);
|
||||
const cashbackTotal = ref('0');
|
||||
|
||||
const items = ref<Transaction[]>([]);
|
||||
const cashbackTotal = ref<string>('0');
|
||||
const loading = ref(true);
|
||||
const PREVIEW_COUNT = 15;
|
||||
|
||||
function txLabel(tx: Transaction): string {
|
||||
const key = txTypeKey(txDisplayType(tx));
|
||||
if (key) {
|
||||
const translated = t(key);
|
||||
if (translated !== key) return translated;
|
||||
}
|
||||
return tx.transactionType;
|
||||
}
|
||||
|
||||
function txSubtitle(tx: Transaction): string {
|
||||
return txSummaryLabel(tx, t);
|
||||
}
|
||||
|
||||
function goDetail(tx: Transaction) {
|
||||
if (!tx.transactionId) return;
|
||||
router.push(`/wallet/transactions/${tx.transactionId}`);
|
||||
}
|
||||
|
||||
function goCashbacks() {
|
||||
router.push('/wallet/cashbacks');
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true;
|
||||
async function fetchCashbackTotal() {
|
||||
try {
|
||||
const [txRes, cbRes] = await Promise.all([
|
||||
api.get('/player/wallet/transactions', { params: { page: 1 } }),
|
||||
api.get('/player/cashbacks').catch(() => ({ data: { data: [] } })),
|
||||
]);
|
||||
const result = txRes.data.data ?? { items: [] };
|
||||
items.value = (result.items ?? []).slice(0, PREVIEW_COUNT);
|
||||
const cbRows = parseCashbackApiData(cbRes.data?.data);
|
||||
cashbackTotal.value = sumCashbackAmount(cbRows).toString();
|
||||
const { data } = await api.get('/player/cashbacks');
|
||||
cashbackTotal.value = sumCashbackAmount(parseCashbackApiData(data.data)).toString();
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
loading.value = false;
|
||||
try {
|
||||
const { data } = await api.get('/player/wallet/transactions/stats');
|
||||
const byType = data.data?.byType ?? [];
|
||||
let sum = 0;
|
||||
for (const g of byType) {
|
||||
if (['CASHBACK', 'CASHBACK_DEPOSIT'].includes(g.transactionType?.toUpperCase())) {
|
||||
sum += Math.abs(parseFloat(g.totalAmount ?? '0'));
|
||||
}
|
||||
}
|
||||
cashbackTotal.value = sum.toString();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProfile() {
|
||||
profileLoading.value = true;
|
||||
profileError.value = false;
|
||||
try {
|
||||
await refreshProfile();
|
||||
initFromUser(profileRaw.value?.locale);
|
||||
void fetchCashbackTotal();
|
||||
} catch {
|
||||
profileError.value = true;
|
||||
} finally {
|
||||
profileLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
await fetchProfile();
|
||||
}
|
||||
|
||||
const { pullDistance, spinning, progress } = usePullToRefresh({
|
||||
onRefresh: fetchData,
|
||||
onRefresh: refreshAll,
|
||||
});
|
||||
|
||||
onMounted(fetchData);
|
||||
onActivated(fetchData);
|
||||
onMounted(refreshAll);
|
||||
onActivated(refreshAll);
|
||||
|
||||
const pullIndicatorStyle = () => ({
|
||||
height: `${pullDistance.value}px`,
|
||||
opacity: Math.min(pullDistance.value / 48, 1),
|
||||
});
|
||||
|
||||
async function changeLocale(code: string) {
|
||||
await setLocale(code);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
auth.logout();
|
||||
router.push('/');
|
||||
}
|
||||
|
||||
const balanceDisplay = computed(() =>
|
||||
formatMoney(profileRaw.value?.wallet?.availableBalance, locale.value),
|
||||
);
|
||||
|
||||
const balanceAmountClass = computed(() => {
|
||||
const len = balanceDisplay.value.length;
|
||||
if (len <= 10) return 'balance-amount--xl';
|
||||
if (len <= 13) return 'balance-amount--lg';
|
||||
if (len <= 16) return 'balance-amount--md';
|
||||
if (len <= 19) return 'balance-amount--sm';
|
||||
return 'balance-amount--xs';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wallet-page">
|
||||
<div
|
||||
class="pull-indicator"
|
||||
:style="pullIndicatorStyle()"
|
||||
>
|
||||
<div class="pull-indicator" :style="pullIndicatorStyle()">
|
||||
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="state">
|
||||
<GoldSpinner :size="36" />
|
||||
<span class="loading-text">{{ t('bet.loading') }}</span>
|
||||
<div v-if="profileLoading" class="loading-state">
|
||||
<GoldSpinner :size="36" :active="true" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="profileError" class="error-state">
|
||||
<p class="error-text">{{ t('common.load_failed') }}</p>
|
||||
<button type="button" class="retry-btn" @click="fetchProfile">{{ t('common.retry') }}</button>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="top-bar">
|
||||
<button type="button" class="more-link secondary" @click="goCashbacks">
|
||||
{{ t('wallet.view_cashbacks_detail') }} ›
|
||||
</button>
|
||||
<button
|
||||
v-if="items.length"
|
||||
type="button"
|
||||
class="more-link"
|
||||
@click="router.push('/wallet/detail')"
|
||||
>{{ t('wallet.view_all') }} ›</button>
|
||||
</div>
|
||||
|
||||
<div v-if="items.length" class="tx-list">
|
||||
<button
|
||||
v-for="tx in items"
|
||||
:key="tx.transactionId"
|
||||
type="button"
|
||||
class="tx-row"
|
||||
@click="goDetail(tx)"
|
||||
>
|
||||
<div class="tx-main">
|
||||
<div class="tx-text">
|
||||
<span class="tx-type">{{ txLabel(tx) }}</span>
|
||||
<span v-if="txSubtitle(tx)" class="tx-summary">{{ txSubtitle(tx) }}</span>
|
||||
<div class="wallet-banner">
|
||||
<div class="card-overlay">
|
||||
<div class="bank-card-top">
|
||||
<div class="bank-card-top-left">
|
||||
<img src="/logo.png" alt="TheBet365" class="brand-logo" />
|
||||
<div class="bank-card-brand-text">
|
||||
<span class="brand-name">THEBET365</span>
|
||||
<span class="bank-card-type">MEMBER</span>
|
||||
</div>
|
||||
</div>
|
||||
<span :class="txAmountClass(tx.amount)">
|
||||
{{ formatMoney(tx.amount, locale) }}
|
||||
</span>
|
||||
<RouterLink to="/wallet/recharge" class="recharge-btn">
|
||||
<span class="recharge-icon">+</span>
|
||||
<span>{{ t('recharge.title') }}</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="tx-meta">
|
||||
<span class="tx-time">{{ new Date(tx.createdAt).toLocaleString() }}</span>
|
||||
<span class="tx-arrow">›</span>
|
||||
|
||||
<div class="bank-card-balance">
|
||||
<span class="bank-card-label">{{ t('wallet.balance') }}</span>
|
||||
<p class="bank-card-number balance-amount" :class="balanceAmountClass">{{ balanceDisplay }}</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="bank-card-footer">
|
||||
<div class="bank-card-field">
|
||||
<span class="bank-card-label">{{ t('wallet.card_holder') }}</span>
|
||||
<span class="bank-card-holder">{{ profileRaw?.username }}</span>
|
||||
</div>
|
||||
<div class="bank-card-field bank-card-field--center">
|
||||
<span class="bank-card-label">{{ t('cashback.total_received') }}</span>
|
||||
<span class="bank-card-stat">{{ formatMoney(cashbackTotal, locale) }}</span>
|
||||
</div>
|
||||
<div class="bank-card-field bank-card-field--right">
|
||||
<span class="bank-card-label">{{ t('wallet.unsettled') }}</span>
|
||||
<span class="bank-card-stat">{{ formatMoney(profileRaw?.wallet?.frozenBalance, locale) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!items.length" class="empty">
|
||||
{{ t('wallet.no_records') }}
|
||||
</div>
|
||||
<section class="settings-group">
|
||||
<RouterLink to="/profile/edit" class="settings-cell">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<circle cx="12" cy="8" r="3.5" />
|
||||
<path d="M5 20c0-3.5 3.1-6 7-6s7 2.5 7 6" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('profile.edit') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</RouterLink>
|
||||
|
||||
<div class="rules-cell">
|
||||
<button
|
||||
type="button"
|
||||
class="settings-cell rules-toggle"
|
||||
:aria-expanded="rulesExpanded"
|
||||
@click="rulesExpanded = !rulesExpanded"
|
||||
>
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path d="M7 4h10v16H7z" />
|
||||
<path d="M10 8h6M10 12h6M10 16h4" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('profile.rules_title') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" :class="{ open: rulesExpanded }" aria-hidden="true">›</span>
|
||||
</button>
|
||||
<div v-show="rulesExpanded" class="rules-body">
|
||||
<p>{{ t('profile.rules_p1') }}</p>
|
||||
<p>{{ t('profile.rules_p2') }}</p>
|
||||
<p>{{ t('profile.rules_p3') }}</p>
|
||||
<p>{{ t('profile.rules_p4') }}</p>
|
||||
<p>{{ t('profile.rules_p5') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-cell settings-cell--stack">
|
||||
<div class="cell-head">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M3 12h18M12 3c2.5 2.8 4 6 4 9s-1.5 6.2-4 9M12 3c-2.5 2.8-4 6-4 9s1.5 6.2 4 9" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('profile.language') }}</span>
|
||||
</div>
|
||||
<div class="lang-segment" role="group" :aria-label="t('profile.language')">
|
||||
<button
|
||||
v-for="item in locales"
|
||||
:key="item.code"
|
||||
type="button"
|
||||
class="lang-opt"
|
||||
:class="{ active: locale === item.code }"
|
||||
@click="changeLocale(item.code)"
|
||||
>
|
||||
<LocaleFlag :locale="item.code" :size="14" />
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button type="button" class="logout-btn" @click="logout">
|
||||
{{ t('auth.logout') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wallet-page {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.pull-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -154,110 +228,298 @@ const pullIndicatorStyle = () => ({
|
||||
transition: height 0.15s ease;
|
||||
}
|
||||
|
||||
.state {
|
||||
.wallet-page {
|
||||
padding: 6px 0 24px;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 56px 20px;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tx-list {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tx-row {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
.retry-btn {
|
||||
padding: 8px 24px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #003D6B;
|
||||
background: transparent;
|
||||
color: #005B9F;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tx-row:last-child {
|
||||
border-bottom: none;
|
||||
.wallet-banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
aspect-ratio: 1.75 / 1;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0, 61, 107, 0.18);
|
||||
background: linear-gradient(135deg, #003D6B 0%, #005B9F 52%, #002847 100%);
|
||||
}
|
||||
|
||||
.tx-row:active {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.tx-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tx-text {
|
||||
.card-overlay {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
height: 100%;
|
||||
padding: 10px 14px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.bank-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bank-card-top-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tx-summary {
|
||||
.brand-logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.bank-card-brand-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
|
||||
.bank-card-type {
|
||||
font-size: 8px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.bank-card-label {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.recharge-btn {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.recharge-icon {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bank-card-balance {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
padding: 1px 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.bank-card-number {
|
||||
margin: 0;
|
||||
font-family: 'SF Mono', 'Consolas', 'Courier New', monospace;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tx-meta {
|
||||
.balance-amount--xl { font-size: clamp(22px, 5.6vw, 27px); }
|
||||
.balance-amount--lg { font-size: clamp(19px, 5vw, 24px); }
|
||||
.balance-amount--md { font-size: clamp(16px, 4.4vw, 20px); }
|
||||
.balance-amount--sm { font-size: clamp(14px, 3.8vw, 17px); }
|
||||
.balance-amount--xs { font-size: clamp(12px, 3.2vw, 14px); }
|
||||
|
||||
.bank-card-footer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.bank-card-field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tx-type { font-weight: 700; color: var(--text); }
|
||||
.pos { color: var(--primary-light); font-weight: 800; font-size: 15px; }
|
||||
.neg { color: var(--danger); font-weight: 700; }
|
||||
.zero { color: var(--text-muted); font-weight: 700; font-size: 15px; }
|
||||
.tx-time { font-size: 11px; color: var(--text-muted); }
|
||||
.tx-arrow { font-size: 16px; color: #555; font-weight: 700; line-height: 1; }
|
||||
.bank-card-field--center { text-align: center; }
|
||||
.bank-card-field--right { text-align: right; }
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 4px 2px;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.top-bar .more-link.secondary {
|
||||
color: #f0b90b;
|
||||
}
|
||||
|
||||
.more-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary-light);
|
||||
.bank-card-holder,
|
||||
.bank-card-stat {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bank-card-holder {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
background: #fff;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.settings-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
background: none;
|
||||
color: #1A1A2E;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.settings-cell:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-cell--stack {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
padding: 10px 14px 12px;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.cell-main,
|
||||
.cell-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cell-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.cell-chevron {
|
||||
color: #6B7280;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.cell-chevron.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.rules-toggle {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.more-link:active {
|
||||
opacity: 1;
|
||||
.rules-cell .rules-body {
|
||||
padding: 0 14px 12px;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.empty { text-align: center; color: var(--text-muted); padding: 40px 16px; font-weight: 600; }
|
||||
.rules-body p {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.lang-segment {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.lang-opt {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-height: 30px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #fff;
|
||||
color: #6B7280;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lang-opt.active {
|
||||
border-color: #003D6B;
|
||||
color: #003D6B;
|
||||
background: rgba(0, 61, 107, 0.06);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #DC2626;
|
||||
background: #fff;
|
||||
color: #DC2626;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useViewport } from '../composables/useViewport';
|
||||
import MobileProfileView from './MobileProfileView.vue';
|
||||
import DesktopProfileView from './desktop/DesktopProfileView.vue';
|
||||
const { isDesktop } = useViewport();
|
||||
import { onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
onMounted(() => {
|
||||
void router.replace('/wallet');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopProfileView v-if="isDesktop" />
|
||||
<MobileProfileView v-else />
|
||||
<div />
|
||||
</template>
|
||||
|
||||
21
apps/player/src/views/RechargeDetailView.vue
Normal file
21
apps/player/src/views/RechargeDetailView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
(id) => {
|
||||
if (!id) return;
|
||||
router.replace({
|
||||
path: '/wallet/recharge/history',
|
||||
query: { orderId: String(id) },
|
||||
});
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template />
|
||||
@@ -9,7 +9,9 @@ import { useSmsCode } from '../composables/useSmsCode';
|
||||
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
||||
import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import loginBg from '../assets/images/h5bg.webp';
|
||||
import loginBg from '../assets/images/zt2.webp';
|
||||
|
||||
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { initFromUser } = useAppLocale();
|
||||
@@ -101,11 +103,16 @@ const fieldError = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page" :style="{ backgroundImage: `url(${loginBg})` }">
|
||||
<div class="login-lang">
|
||||
<div class="login-page" :class="{ 'login-page--desktop': props.desktop }" :style="props.desktop ? undefined : { backgroundImage: `url(${loginBg})` }">
|
||||
<div v-if="!props.desktop" class="login-lang">
|
||||
<LocaleSwitcher compact />
|
||||
</div>
|
||||
<form @submit.prevent="submit" class="login-form ps-gold-frame" :class="{ 'is-busy': loading }">
|
||||
<div class="login-stack">
|
||||
<form
|
||||
@submit.prevent="submit"
|
||||
class="login-form"
|
||||
:class="{ 'ps-gold-frame': !props.desktop, 'is-busy': loading }"
|
||||
>
|
||||
<div v-if="loading" class="form-busy-overlay" aria-hidden="true">
|
||||
<GoldSpinner :size="40" :active="true" />
|
||||
<p class="form-busy-text">{{ t('auth.registering') }}</p>
|
||||
@@ -116,7 +123,7 @@ const fieldError = () => {
|
||||
<label>{{ t('auth.username') }}</label>
|
||||
<input
|
||||
v-model="account"
|
||||
class="field-input"
|
||||
class="field-input ps-gold-input"
|
||||
required
|
||||
autocomplete="username"
|
||||
maxlength="32"
|
||||
@@ -132,7 +139,7 @@ const fieldError = () => {
|
||||
<PhoneCountrySelect v-model="countryIso" />
|
||||
<input
|
||||
v-model="phone"
|
||||
class="field-input"
|
||||
class="field-input ps-gold-input"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel-national"
|
||||
@@ -147,7 +154,7 @@ const fieldError = () => {
|
||||
<div class="inline-row">
|
||||
<input
|
||||
v-model="smsCode"
|
||||
class="field-input"
|
||||
class="field-input ps-gold-input"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
@@ -171,17 +178,17 @@ const fieldError = () => {
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.password') }}</label>
|
||||
<input v-model="password" class="field-input" type="password" required autocomplete="new-password" minlength="8" :disabled="loading" />
|
||||
<input v-model="password" class="field-input ps-gold-input" type="password" required autocomplete="new-password" minlength="8" :disabled="loading" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.confirm_password') }}</label>
|
||||
<input v-model="confirmPassword" class="field-input" type="password" required autocomplete="new-password" minlength="8" :disabled="loading" />
|
||||
<input v-model="confirmPassword" class="field-input ps-gold-input" type="password" required autocomplete="new-password" minlength="8" :disabled="loading" />
|
||||
</div>
|
||||
|
||||
<div class="field field-optional">
|
||||
<label>{{ t('auth.invite_code') }} <span class="optional-tag">{{ t('auth.optional') }}</span></label>
|
||||
<input v-model="inviteCode" class="field-input" autocomplete="off" :disabled="loading" />
|
||||
<input v-model="inviteCode" class="field-input ps-gold-input" autocomplete="off" :disabled="loading" />
|
||||
</div>
|
||||
|
||||
<p v-if="fieldError()" class="error">{{ fieldError() }}</p>
|
||||
@@ -199,13 +206,19 @@ const fieldError = () => {
|
||||
</span>
|
||||
<span v-else>{{ t('auth.register_btn') }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn-skip" :disabled="loading" @click="goLogin">
|
||||
<button v-if="props.desktop" type="button" class="btn-skip" :disabled="loading" @click="goLogin">
|
||||
{{ t('auth.have_account') }}
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" class="btn-skip-light" @click="continueBrowsing">
|
||||
{{ t('auth.continue_browsing') }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-if="!props.desktop" class="login-actions">
|
||||
<button type="button" class="btn-skip" :disabled="loading" @click="goLogin">
|
||||
{{ t('auth.have_account') }}
|
||||
</button>
|
||||
<button type="button" class="btn-skip-light" :disabled="loading" @click="continueBrowsing">
|
||||
{{ t('auth.continue_browsing') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -214,7 +227,7 @@ const fieldError = () => {
|
||||
position: absolute;
|
||||
top: max(12px, env(safe-area-inset-top));
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
@@ -226,21 +239,54 @@ const fieldError = () => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 30vh 16px calc(10vh + max(20px, env(safe-area-inset-bottom)));
|
||||
background-color: var(--tertiary);
|
||||
padding: 30vh 16px max(20px, env(safe-area-inset-bottom));
|
||||
background-color: var(--bg-body);
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.login-page--desktop {
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-stack {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-stack {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0 14px 4px;
|
||||
}
|
||||
|
||||
.login-form.is-busy {
|
||||
@@ -257,8 +303,8 @@ const fieldError = () => {
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border-radius: inherit;
|
||||
background: rgba(8, 8, 8, 0.72);
|
||||
backdrop-filter: blur(2px);
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(4px);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@@ -266,7 +312,7 @@ const fieldError = () => {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(240, 216, 117, 0.95);
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@@ -277,11 +323,65 @@ const fieldError = () => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-form {
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.login-page--desktop .form-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-page--desktop .field-input,
|
||||
.login-page--desktop .btn-secondary {
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login {
|
||||
margin-top: 10px;
|
||||
min-height: 50px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip:hover:not(:disabled) {
|
||||
border-color: var(--border-active);
|
||||
background: var(--surface-accent);
|
||||
}
|
||||
|
||||
.login-page--desktop .error {
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border-left: 2px solid var(--danger);
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -305,7 +405,7 @@ label {
|
||||
.optional-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.field-input {
|
||||
@@ -314,19 +414,19 @@ label {
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #0d0d0d;
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.field-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.32);
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
outline: none;
|
||||
border-color: #555;
|
||||
border-color: var(--border-gold-soft);
|
||||
}
|
||||
|
||||
.inline-row {
|
||||
@@ -346,7 +446,7 @@ label {
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: #141414;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
@@ -354,6 +454,13 @@ label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:not(:disabled).active,
|
||||
.btn-secondary:active:not(:disabled) {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface-accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
@@ -377,37 +484,34 @@ label {
|
||||
}
|
||||
|
||||
.btn-skip {
|
||||
margin-top: 2px;
|
||||
padding: 8px 14px;
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-skip:active {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-skip-light {
|
||||
position: absolute;
|
||||
bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 8px 16px;
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-skip-light:active {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.error {
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useViewport } from '../composables/useViewport';
|
||||
import MobileWalletTransactionDetailView from './MobileWalletTransactionDetailView.vue';
|
||||
import DesktopWalletTransactionDetailView from './desktop/DesktopWalletTransactionDetailView.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { isDesktop } = useViewport();
|
||||
|
||||
watch(
|
||||
() => [isDesktop.value, route.params.transactionId] as const,
|
||||
([desktop, transactionId]) => {
|
||||
if (desktop && transactionId) {
|
||||
router.replace({ path: '/wallet/detail', query: { transactionId: String(transactionId) } });
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopWalletTransactionDetailView v-if="isDesktop" />
|
||||
<MobileWalletTransactionDetailView v-else />
|
||||
<MobileWalletTransactionDetailView v-if="!isDesktop" />
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { useViewport } from '../composables/useViewport';
|
||||
import MobileWalletView from './MobileWalletView.vue';
|
||||
import DesktopWalletView from './desktop/DesktopWalletView.vue';
|
||||
|
||||
const { isDesktop } = useViewport();
|
||||
|
||||
const DesktopWalletView = defineAsyncComponent(
|
||||
() => import('./desktop/DesktopWalletView.vue'),
|
||||
);
|
||||
const MobileWalletView = defineAsyncComponent(
|
||||
() => import('./MobileWalletView.vue'),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopWalletView v-if="isDesktop" />
|
||||
<MobileWalletView v-else />
|
||||
|
||||
@@ -135,7 +135,7 @@ onActivated(() => {
|
||||
padding: 18px 20px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.08) 0%, rgba(18, 18, 18, 0.95) 42%);
|
||||
background: linear-gradient(135deg, rgba(0, 102, 204, 0.08) 0%, var(--bg-card) 42%);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
@@ -147,8 +147,8 @@ onActivated(() => {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #141414;
|
||||
color: var(--gold);
|
||||
background: var(--bg-body);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
@@ -157,8 +157,8 @@ onActivated(() => {
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
border-color: var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
@@ -201,8 +201,8 @@ onActivated(() => {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
border: 1px solid var(--border-active);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
@@ -238,20 +238,20 @@ onActivated(() => {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: rgba(18, 18, 18, 0.96);
|
||||
background: var(--bg-card);
|
||||
text-align: left;
|
||||
transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.announce-card:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.35);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.card-cover {
|
||||
aspect-ratio: 16 / 7;
|
||||
background: #0a0a0a;
|
||||
background: var(--bg-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -281,11 +281,11 @@ onActivated(() => {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
background: rgba(212, 175, 55, 0.12);
|
||||
border: 1px solid rgba(212, 175, 55, 0.22);
|
||||
background: rgba(0, 102, 204, 0.12);
|
||||
border: 1px solid rgba(0, 102, 204, 0.22);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--gold);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.card-date {
|
||||
@@ -299,7 +299,7 @@ onActivated(() => {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
line-height: 1.45;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
@@ -30,6 +30,7 @@ const isDetailRoute = computed(
|
||||
.desktop-hub-page {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hub-placeholder {
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter, RouterLink } from 'vue-router';
|
||||
import api from '../../api';
|
||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||
import { formatMoney } from '../../utils/localeDisplay';
|
||||
import type { BetHistoryItem } from '../../components/BetHistoryCard.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function goBack() {
|
||||
if (window.history.state && window.history.state.back) {
|
||||
if (window.history.state?.back) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push('/bets');
|
||||
}
|
||||
}
|
||||
|
||||
type BetDetail = {
|
||||
betNo: string;
|
||||
stake: string;
|
||||
potentialPayout?: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
selections: Array<{
|
||||
matchName?: string;
|
||||
selectionName?: string;
|
||||
odds?: number;
|
||||
market?: string;
|
||||
homeScore?: number;
|
||||
awayScore?: number;
|
||||
matchStatus?: string;
|
||||
[key: string]: any;
|
||||
}>;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
const detail = ref<BetDetail | null>(null);
|
||||
const detail = ref<BetHistoryItem | null>(null);
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
|
||||
@@ -58,7 +41,13 @@ async function loadDetail() {
|
||||
onMounted(loadDetail);
|
||||
|
||||
function statusClass(status: string) {
|
||||
const map: Record<string, string> = { WON: 'status-won', LOST: 'status-lost', PENDING: 'status-pending', PUSH: 'status-push' };
|
||||
const map: Record<string, string> = {
|
||||
WON: 'status-won',
|
||||
LOST: 'status-lost',
|
||||
PENDING: 'status-pending',
|
||||
PUSH: 'status-push',
|
||||
CANCELLED: 'status-cancelled',
|
||||
};
|
||||
return map[status?.toUpperCase()] ?? 'status-default';
|
||||
}
|
||||
|
||||
@@ -68,9 +57,67 @@ function statusLabel(status: string) {
|
||||
LOST: t('history.filter_lost'),
|
||||
PENDING: t('history.filter_pending'),
|
||||
PUSH: t('history.filter_push'),
|
||||
CANCELLED: t('common.cancelled'),
|
||||
};
|
||||
return map[status?.toUpperCase()] ?? status;
|
||||
}
|
||||
|
||||
const SEL_TRANS: Record<string, Record<string, string>> = {
|
||||
'主胜': { 'en-US': 'Home Win', 'ms-MY': 'Rumah Menang' },
|
||||
'客胜': { 'en-US': 'Away Win', 'ms-MY': 'Tandang Menang' },
|
||||
'和局': { 'en-US': 'Draw', 'ms-MY': 'Seri' },
|
||||
'主': { 'en-US': 'Home', 'ms-MY': 'Rumah' },
|
||||
'客': { 'en-US': 'Away', 'ms-MY': 'Tandang' },
|
||||
'大': { 'en-US': 'Over', 'ms-MY': 'Atas' },
|
||||
'小': { 'en-US': 'Under', 'ms-MY': 'Bawah' },
|
||||
'单': { 'en-US': 'Odd', 'ms-MY': 'Ganjil' },
|
||||
'双': { 'en-US': 'Even', 'ms-MY': 'Genap' },
|
||||
'冠军': { 'en-US': 'Winner', 'ms-MY': 'Juara' },
|
||||
};
|
||||
|
||||
function translateSel(name: string): string {
|
||||
if (locale.value === 'zh-CN') return name;
|
||||
const exact = SEL_TRANS[name];
|
||||
if (exact) return exact[locale.value] ?? exact['en-US'] ?? name;
|
||||
const sp = name.indexOf(' ');
|
||||
if (sp > 0) {
|
||||
const head = name.slice(0, sp);
|
||||
const m = SEL_TRANS[head];
|
||||
if (m) return (m[locale.value] ?? m['en-US'] ?? head) + name.slice(sp);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function legResultIcon(status?: string | null) {
|
||||
switch (status?.toUpperCase()) {
|
||||
case 'WON': return '✓';
|
||||
case 'LOST': return '✗';
|
||||
case 'PUSH': return '=';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
function legResultClass(status?: string | null) {
|
||||
switch (status?.toUpperCase()) {
|
||||
case 'WON': return 'leg-won';
|
||||
case 'LOST': return 'leg-lost';
|
||||
case 'PUSH': return 'leg-push';
|
||||
default: return 'leg-pending';
|
||||
}
|
||||
}
|
||||
|
||||
const displayLegs = computed(() => {
|
||||
if (!detail.value) return [];
|
||||
if (detail.value.legs?.length) return detail.value.legs;
|
||||
return [{
|
||||
matchTitle: detail.value.matchTitle,
|
||||
marketLabel: '',
|
||||
selectionName: detail.value.pickLabel,
|
||||
odds: detail.value.totalOdds,
|
||||
resultStatus: detail.value.status,
|
||||
score: detail.value.matchScore ?? null,
|
||||
}];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -78,9 +125,9 @@ function statusLabel(status: string) {
|
||||
<main class="account-main">
|
||||
<div class="page-header">
|
||||
<button type="button" class="back-btn" @click="goBack">
|
||||
‹ {{ t('common.back') || '返回' }}
|
||||
‹ {{ t('common.back') }}
|
||||
</button>
|
||||
<h1 class="page-title">{{ t('bet.detail_title') || '注单详情' }}</h1>
|
||||
<h1 class="page-title">{{ t('bet.detail_title') }}</h1>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
@@ -92,42 +139,69 @@ function statusLabel(status: string) {
|
||||
<button type="button" class="retry-btn" @click="loadDetail">{{ t('common.retry') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="detail" class="detail-layout">
|
||||
<!-- Header info -->
|
||||
<div class="detail-card header-card">
|
||||
<div class="bet-no">{{ detail.betNo }}</div>
|
||||
<span class="status-badge" :class="statusClass(detail.status)">{{ statusLabel(detail.status) }}</span>
|
||||
<div class="amount-row">
|
||||
<div class="amount-item">
|
||||
<span class="amount-label">{{ t('bet.stake') || '投注额' }}</span>
|
||||
<span class="amount-val">{{ detail.stake }}</span>
|
||||
</div>
|
||||
<div class="amount-item">
|
||||
<span class="amount-label">{{ t('bet.potential_payout') || '预计派彩' }}</span>
|
||||
<span class="amount-val gold">{{ detail.potentialPayout || '-' }}</span>
|
||||
</div>
|
||||
<div class="amount-item">
|
||||
<span class="amount-label">{{ t('bet.placed_at') || '下注时间' }}</span>
|
||||
<span class="amount-val date">{{ new Date(detail.createdAt).toLocaleString() }}</span>
|
||||
<div v-else-if="detail" class="detail-grid">
|
||||
<div class="detail-left">
|
||||
<div class="detail-card">
|
||||
<div class="card-title">{{ t('bet.selections') }}</div>
|
||||
<div v-for="(leg, idx) in displayLegs" :key="idx" class="sel-row">
|
||||
<div class="sel-row-left">
|
||||
<span v-if="displayLegs.length > 1" class="sel-index">{{ idx + 1 }}</span>
|
||||
<div class="sel-info">
|
||||
<div class="sel-match">{{ leg.matchTitle || t('bet.match') }}</div>
|
||||
<div class="sel-meta">
|
||||
<span v-if="leg.marketLabel" class="sel-market">{{ leg.marketLabel }}</span>
|
||||
<span class="sel-name">{{ translateSel(leg.selectionName || '') }}</span>
|
||||
<span v-if="leg.score?.ft" class="sel-score">{{ t('history.ft') }}: {{ leg.score.ft }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sel-row-right">
|
||||
<span class="sel-odds">@{{ leg.odds ?? '-' }}</span>
|
||||
<span
|
||||
v-if="leg.resultStatus"
|
||||
class="leg-result"
|
||||
:class="legResultClass(leg.resultStatus)"
|
||||
>
|
||||
{{ legResultIcon(leg.resultStatus) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selections -->
|
||||
<div class="detail-card">
|
||||
<div class="card-title">{{ t('bet.selections') || '投注选项' }}</div>
|
||||
<div v-for="(sel, idx) in detail.selections" :key="idx" class="sel-row">
|
||||
<div class="sel-match">{{ sel.matchName || t('bet.match') }}</div>
|
||||
<div class="sel-meta">
|
||||
<span class="sel-market">{{ sel.market || sel.marketName || '' }}</span>
|
||||
<span class="sel-name">{{ sel.selectionName || '' }}</span>
|
||||
<span class="sel-odds">@{{ sel.odds }}</span>
|
||||
<div class="detail-right">
|
||||
<div class="detail-card summary-card">
|
||||
<div class="card-title">{{ t('history.summary') }}</div>
|
||||
<div class="summary-bet-no">
|
||||
<span class="summary-label">{{ t('bet.bet_no') }}</span>
|
||||
<span class="summary-value mono">{{ detail.betNo }}</span>
|
||||
</div>
|
||||
<div class="summary-status">
|
||||
<span class="status-badge" :class="statusClass(detail.status)">{{ statusLabel(detail.status) }}</span>
|
||||
</div>
|
||||
<div class="summary-amounts">
|
||||
<div class="summary-amount-item">
|
||||
<span class="summary-amount-label">{{ t('bet.stake') }}</span>
|
||||
<span class="summary-amount-val">{{ formatMoney(detail.stake, locale) }}</span>
|
||||
</div>
|
||||
<div class="summary-amount-item">
|
||||
<span class="summary-amount-label">{{ t('bet.potential_payout') }}</span>
|
||||
<span class="summary-amount-val accent">{{ formatMoney(detail.potentialReturn, locale) }}</span>
|
||||
</div>
|
||||
<div class="summary-amount-item">
|
||||
<span class="summary-amount-label">{{ t('bet.placed_at') }}</span>
|
||||
<span class="summary-amount-val date">
|
||||
{{ detail.placedAt ? new Date(detail.placedAt).toLocaleString() : '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RouterLink to="/bets" class="back-to-list-btn">{{ t('common.back') }}</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">{{ t('common.not_found') || '未找到记录' }}</div>
|
||||
<div v-else class="empty-state">{{ t('common.not_found') }}</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -142,48 +216,136 @@ function statusLabel(status: string) {
|
||||
.page-title { font-size: 18px; font-weight: 800; color: var(--primary-light); margin: 0; }
|
||||
.loading-state { display: flex; justify-content: center; align-items: center; padding: 80px 0; }
|
||||
.error-state { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 80px 20px; color: var(--text-muted); }
|
||||
.retry-btn { padding: 8px 24px; border-radius: 6px; border: 1px solid var(--primary); background: transparent; color: var(--primary-light); font-size: 13px; font-weight: 700; cursor: pointer; }
|
||||
.retry-btn { padding: 8px 24px; border-radius: 6px; border: 1px solid var(--border-active); background: transparent; color: var(--primary-light); font-size: 13px; font-weight: 700; cursor: pointer; }
|
||||
.empty-state { display: flex; align-items: center; justify-content: center; padding: 80px 20px; color: var(--text-muted); font-size: 14px; font-weight: 600; }
|
||||
|
||||
.detail-layout { display: flex; flex-direction: column; gap: 16px; max-width: 680px; }
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 340px;
|
||||
gap: 20px;
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.detail-left { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
||||
.detail-right { display: flex; flex-direction: column; gap: 16px; }
|
||||
|
||||
.detail-card {
|
||||
background: var(--desktop-sidebar-bg);
|
||||
border: 1px solid var(--desktop-border);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-card { display: flex; flex-direction: column; gap: 12px; }
|
||||
|
||||
.bet-no {
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
.card-title {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.status-badge { display: inline-block; padding: 4px 14px; border-radius: 999px; font-size: 12px; font-weight: 700; align-self: flex-start; }
|
||||
.status-won { background: rgba(52,199,89,0.12); color: #4cd964; }
|
||||
.status-lost { background: rgba(255,69,58,0.12); color: #ff453a; }
|
||||
.status-pending { background: rgba(212,175,55,0.12); color: var(--primary-light); }
|
||||
.status-push { background: rgba(100,100,100,0.12); color: #aaa; }
|
||||
.status-default { background: rgba(100,100,100,0.1); color: #888; }
|
||||
.sel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.amount-row { display: flex; gap: 32px; padding-top: 4px; flex-wrap: wrap; }
|
||||
.amount-item { display: flex; flex-direction: column; gap: 4px; }
|
||||
.amount-label { font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.amount-val { font-size: 18px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
|
||||
.amount-val.gold { color: var(--primary-light); }
|
||||
.amount-val.date { font-size: 13px; color: var(--text-muted); font-weight: 600; }
|
||||
|
||||
.card-title { font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid var(--desktop-border); }
|
||||
|
||||
.sel-row { padding: 12px 0; border-bottom: 1px solid rgba(255,255,255,0.04); }
|
||||
.sel-row:last-child { border-bottom: none; }
|
||||
|
||||
.sel-row-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||
|
||||
.sel-index {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-hover);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sel-info { min-width: 0; }
|
||||
.sel-match { font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
|
||||
.sel-meta { display: flex; gap: 10px; align-items: center; font-size: 12px; color: var(--text-muted); }
|
||||
.sel-meta { display: flex; gap: 10px; align-items: center; font-size: 12px; color: var(--text-muted); flex-wrap: wrap; }
|
||||
.sel-name { color: var(--text); font-weight: 700; }
|
||||
.sel-odds { color: var(--primary-light); font-weight: 800; margin-left: auto; }
|
||||
.sel-score { color: var(--text-muted); }
|
||||
|
||||
.sel-row-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
||||
.sel-odds { color: var(--primary); font-weight: 800; font-size: 14px; }
|
||||
|
||||
.leg-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.leg-won { background: rgba(52, 199, 89, 0.15); color: #4cd964; }
|
||||
.leg-lost { background: rgba(255, 69, 58, 0.15); color: #ff453a; }
|
||||
.leg-push { background: rgba(100, 100, 100, 0.15); color: #aaa; }
|
||||
.leg-pending { background: rgba(0, 102, 204, 0.1); color: var(--primary-light); font-size: 10px; }
|
||||
|
||||
.summary-card { display: flex; flex-direction: column; gap: 16px; }
|
||||
|
||||
.summary-bet-no { display: flex; flex-direction: column; gap: 4px; }
|
||||
.summary-label { font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.summary-value { font-size: 14px; font-weight: 700; color: var(--text); }
|
||||
.mono { font-family: var(--font-mono); letter-spacing: 0.03em; }
|
||||
|
||||
.summary-status { display: flex; }
|
||||
|
||||
.status-badge { display: inline-block; padding: 4px 14px; border-radius: 999px; font-size: 12px; font-weight: 700; }
|
||||
.status-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
||||
.status-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
||||
.status-pending { background: rgba(0, 102, 204, 0.12); color: var(--primary-light); }
|
||||
.status-push { background: rgba(100, 100, 100, 0.12); color: #aaa; }
|
||||
.status-cancelled { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||
.status-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||
|
||||
.summary-amounts { display: flex; flex-direction: column; gap: 14px; padding-top: 16px; border-top: 1px solid var(--border); }
|
||||
.summary-amount-item { display: flex; flex-direction: column; gap: 4px; }
|
||||
.summary-amount-label { font-size: 11px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.summary-amount-val { font-size: 18px; font-weight: 800; color: var(--text); font-variant-numeric: tabular-nums; }
|
||||
.summary-amount-val.accent { color: var(--primary); }
|
||||
.summary-amount-val.date { font-size: 13px; color: var(--text-muted); font-weight: 600; }
|
||||
|
||||
.back-to-list-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.back-to-list-btn:hover {
|
||||
border-color: var(--border-active);
|
||||
color: var(--primary-light);
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.detail-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,22 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onActivated, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import api from '../../api';
|
||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||
import Pagination from '../../components/desktop/Pagination.vue';
|
||||
import DesktopWalletSubNav from '../../components/desktop/DesktopWalletSubNav.vue';
|
||||
import DesktopWalletPageHeader from '../../components/desktop/DesktopWalletPageHeader.vue';
|
||||
import DesktopCashbackDetailModal from '../../components/desktop/DesktopCashbackDetailModal.vue';
|
||||
import { formatMoney, formatMoneyCompact } from '../../utils/localeDisplay';
|
||||
import { parseCashbackApiData, type CashbackRecord } from '../../utils/cashback';
|
||||
|
||||
const COL_COUNT = 6;
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const detailModalVisible = ref(false);
|
||||
const detailRecord = ref<CashbackRecord | null>(null);
|
||||
|
||||
const allItems = ref<CashbackRecord[]>([]);
|
||||
const loading = ref(true);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
|
||||
function openDetail(record: CashbackRecord) {
|
||||
detailRecord.value = record;
|
||||
detailModalVisible.value = true;
|
||||
}
|
||||
|
||||
function openDetailFromQuery() {
|
||||
const batchNo = route.query.batchNo;
|
||||
if (typeof batchNo !== 'string' || !batchNo) return;
|
||||
const match = allItems.value.find((row) => row.batchNo === batchNo);
|
||||
if (match) {
|
||||
openDetail(match);
|
||||
const { batchNo: _removed, ...rest } = route.query;
|
||||
void router.replace({ path: route.path, query: rest });
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => route.query.batchNo, openDetailFromQuery);
|
||||
watch(allItems, openDetailFromQuery);
|
||||
|
||||
const total = computed(() => allItems.value.length);
|
||||
|
||||
const items = computed(() => {
|
||||
@@ -75,21 +101,26 @@ watch(pageSize, () => {
|
||||
page.value = 1;
|
||||
});
|
||||
|
||||
onMounted(loadData);
|
||||
onActivated(loadData);
|
||||
onMounted(async () => {
|
||||
await loadData();
|
||||
openDetailFromQuery();
|
||||
});
|
||||
onActivated(async () => {
|
||||
await loadData();
|
||||
openDetailFromQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-records-page">
|
||||
<header class="desktop-records-header">
|
||||
<div class="desktop-records-header-main">
|
||||
<DesktopWalletSubNav />
|
||||
</div>
|
||||
<div v-if="!loading && allItems.length" class="desktop-records-actions summary-chip">
|
||||
<span class="summary-label">{{ t('cashback.total_received') }}</span>
|
||||
<span class="summary-value">{{ formatMoney(totalAmount, locale) }}</span>
|
||||
</div>
|
||||
</header>
|
||||
<DesktopWalletPageHeader :show-recharge="false">
|
||||
<template v-if="!loading && allItems.length" #actions>
|
||||
<div class="summary-chip">
|
||||
<span class="summary-label">{{ t('cashback.total_received') }}</span>
|
||||
<span class="summary-value">{{ formatMoney(totalAmount, locale) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</DesktopWalletPageHeader>
|
||||
|
||||
<section class="desktop-records-panel">
|
||||
<div class="desktop-records-table-wrap">
|
||||
@@ -113,7 +144,12 @@ onActivated(loadData);
|
||||
</td>
|
||||
</tr>
|
||||
<template v-else-if="items.length">
|
||||
<tr v-for="row in items" :key="row.id" class="hover-row">
|
||||
<tr
|
||||
v-for="row in items"
|
||||
:key="row.id"
|
||||
class="hover-row clickable-row"
|
||||
@click="openDetail(row)"
|
||||
>
|
||||
<td>{{ formatPeriod(row.periodStart, row.periodEnd) }}</td>
|
||||
<td>{{ formatMoneyCompact(row.effectiveStake, locale) }}</td>
|
||||
<td>{{ formatRate(row.rate) }}</td>
|
||||
@@ -140,6 +176,8 @@ onActivated(loadData);
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<DesktopCashbackDetailModal v-model:visible="detailModalVisible" :record="detailRecord" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -149,9 +187,9 @@ onActivated(loadData);
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid rgba(212, 175, 55, 0.22);
|
||||
border: 1px solid rgba(0, 102, 204, 0.22);
|
||||
border-radius: 8px;
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
@@ -168,7 +206,7 @@ onActivated(loadData);
|
||||
}
|
||||
|
||||
.batch-cell {
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
isAfterLocalTodayMatchWindow as isAfterTodayMatchWindow,
|
||||
isInLocalTodayMatchWindow as isInTodayMatchWindow,
|
||||
} from '@thebet365/shared';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import type { OutrightEvent, OutrightSelection } from '../../components/outright/OutrightEventSection.vue';
|
||||
import OutrightBetModal, { type OutrightPick } from '../../components/outright/OutrightBetModal.vue';
|
||||
|
||||
type MainTab = 'matches' | 'outright';
|
||||
|
||||
@@ -102,6 +105,22 @@ const leagueGroups = computed(() => {
|
||||
return Object.values(groups).sort((a, b) => a.leagueName.localeCompare(b.leagueName));
|
||||
});
|
||||
|
||||
const filteredOutrightEvents = computed(() => {
|
||||
if (mainTab.value !== 'outright') return [];
|
||||
const keyword = searchQuery.value.trim().toLowerCase();
|
||||
return outrightEvents.value.filter((e) => {
|
||||
if (keyword) {
|
||||
const haystack = `${e.title} ${e.leagueName || ''}`.toLowerCase();
|
||||
if (!haystack.includes(keyword)) return false;
|
||||
}
|
||||
if (filterState.value.leagueIds.length > 0) {
|
||||
const id = e.leagueId ?? e.leagueName;
|
||||
if (!filterState.value.leagueIds.includes(id)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
function goMatch(id: string) {
|
||||
router.push(`/match/${id}`);
|
||||
}
|
||||
@@ -110,6 +129,32 @@ function goOutright(id: string) {
|
||||
router.push(`/outright/${id}`);
|
||||
}
|
||||
|
||||
const auth = useAuthStore();
|
||||
const modalOpen = ref(false);
|
||||
const activePick = ref<OutrightPick | null>(null);
|
||||
|
||||
function openBet(event: OutrightEvent, sel: OutrightSelection) {
|
||||
if (event.bettingOpen === false || event.status === 'SETTLED') return;
|
||||
if (!auth.token) {
|
||||
auth.showLoginPrompt(route.fullPath);
|
||||
return;
|
||||
}
|
||||
activePick.value = {
|
||||
selectionId: sel.id,
|
||||
oddsVersion: sel.oddsVersion,
|
||||
teamCode: sel.teamCode,
|
||||
teamName: sel.teamName,
|
||||
odds: sel.odds,
|
||||
eventTitle: event.title,
|
||||
};
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modalOpen.value = false;
|
||||
activePick.value = null;
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
if (mainTab.value === 'outright') {
|
||||
await loadOutrights({ silent: false });
|
||||
@@ -131,7 +176,7 @@ void loadParlayMatches(true);
|
||||
:class="{ active: mainTab === 'matches' }"
|
||||
@click="setMainTab('matches')"
|
||||
>
|
||||
⚽ {{ t('bet.tab_matches') }}
|
||||
{{ t('bet.tab_matches') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -139,7 +184,7 @@ void loadParlayMatches(true);
|
||||
:class="{ active: mainTab === 'outright' }"
|
||||
@click="setMainTab('outright')"
|
||||
>
|
||||
🏆 {{ t('bet.tab_outright') }}
|
||||
{{ t('bet.tab_outright') }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@@ -179,25 +224,26 @@ void loadParlayMatches(true);
|
||||
</div>
|
||||
|
||||
<div v-else class="outright-panel-wrap">
|
||||
<div v-if="outrightLoading && !outrightEvents.length" class="skeleton-container">
|
||||
<div v-if="outrightLoading && !filteredOutrightEvents.length" class="skeleton-container">
|
||||
<div v-for="i in 4" :key="i" class="skeleton-card desktop-skeleton"></div>
|
||||
</div>
|
||||
|
||||
<template v-else-if="outrightEvents.length">
|
||||
<p v-if="outrightEvents.length > 1" class="outright-summary">
|
||||
<template v-else-if="filteredOutrightEvents.length">
|
||||
<p v-if="filteredOutrightEvents.length > 1" class="outright-summary">
|
||||
{{
|
||||
t('bet.outright_events_summary', {
|
||||
events: outrightEvents.length,
|
||||
teams: outrightEvents.reduce((n, e) => n + e.selections.length, 0),
|
||||
events: filteredOutrightEvents.length,
|
||||
teams: filteredOutrightEvents.reduce((n, e) => n + e.selections.length, 0),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<div class="outright-event-grid">
|
||||
<DesktopOutrightEventCard
|
||||
v-for="event in outrightEvents"
|
||||
v-for="event in filteredOutrightEvents"
|
||||
:key="event.id"
|
||||
:event="event"
|
||||
@open="goOutright(event.id)"
|
||||
@pick="(sel) => openBet(event, sel)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -207,6 +253,8 @@ void loadParlayMatches(true);
|
||||
<p v-if="!outrightError" class="empty-hint">{{ t('bet.no_outright_hint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OutrightBetModal :open="modalOpen" :pick="activePick" @close="closeModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -238,20 +286,21 @@ void loadParlayMatches(true);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.main-tab:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--primary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.main-tab.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
border: 1px solid var(--border-active);
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
@@ -293,8 +342,9 @@ void loadParlayMatches(true);
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.06);
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
border-left: 3px solid var(--primary);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
@@ -1,99 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Desktop Forgot Password View
|
||||
*/
|
||||
import DesktopAuthLayout from '../../components/desktop/DesktopAuthLayout.vue';
|
||||
import ForgotPasswordView from '../ForgotPasswordView.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-auth-shell">
|
||||
<div class="auth-brand">
|
||||
<div class="brand-inner">
|
||||
<img src="/logo.png" alt="TheBet365" class="brand-logo" />
|
||||
<h1 class="brand-name">THEBET365</h1>
|
||||
<p class="brand-tagline">重置您的登录密码</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-card-wrap">
|
||||
<div class="auth-card">
|
||||
<ForgotPasswordView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopAuthLayout wide>
|
||||
<ForgotPasswordView desktop />
|
||||
</DesktopAuthLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-auth-shell {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: var(--desktop-bg);
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0a0a0a 0%, #111 50%, #0d0d0d 100%);
|
||||
border-right: 1px solid var(--desktop-border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-brand::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 60% 60% at 50% 40%, rgba(212,175,55,0.06) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 16px rgba(212,175,55,0.3));
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--primary-light);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-card-wrap {
|
||||
width: 480px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
background: var(--desktop-bg);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { onMounted, computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -9,26 +9,27 @@ import TeamEmblem from '../../components/TeamEmblem.vue';
|
||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||||
import type { PlayerHomeMatch } from '../../composables/usePlayerHome';
|
||||
|
||||
type HotTab = 'hot' | 'upcoming';
|
||||
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 activeTab = ref<HotTab>('hot');
|
||||
|
||||
const { openAt, visible: popoverVisible, pendingItem, cancelClose, scheduleClose, isActiveForCard } = useDesktopBetPopover();
|
||||
|
||||
const activeMarketTypes = ref<Record<string, string>>({});
|
||||
|
||||
const bannerFallbackTo = computed(() => {
|
||||
const id = announcementItems.value[0]?.id;
|
||||
return id ? `/announcements/${id}` : '/announcements';
|
||||
});
|
||||
|
||||
const displayedMatches = computed<PlayerHomeMatch[]>(() =>
|
||||
activeTab.value === 'hot' ? hotMatches.value : upcomingMatches.value,
|
||||
);
|
||||
|
||||
const emptyMessage = computed(() =>
|
||||
activeTab.value === 'hot' ? t('home.no_matches') : t('home.upcoming_empty'),
|
||||
);
|
||||
const featuredMatch = computed<PlayerHomeMatch | null>(() => hotMatches.value[0] ?? null);
|
||||
const restHotMatches = computed<PlayerHomeMatch[]>(() => hotMatches.value.slice(1));
|
||||
const upcomingList = computed<PlayerHomeMatch[]>(() => upcomingMatches.value);
|
||||
const sideRecommendMatches = computed<PlayerHomeMatch[]>(() => hotMatches.value.slice(1, 5));
|
||||
|
||||
onMounted(() => {
|
||||
void load(true);
|
||||
@@ -38,103 +39,505 @@ function goMatch(id: string) {
|
||||
router.push(`/match/${id}`);
|
||||
}
|
||||
|
||||
function goPath(path: string) {
|
||||
router.push(path);
|
||||
}
|
||||
|
||||
function formatKickoff(startTime: string) {
|
||||
return formatLocalMatchDateTime(startTime, locale.value, { variant: 'full' });
|
||||
return formatLocalMatchDateTime(startTime, locale.value, {
|
||||
variant: 'compact',
|
||||
includeTimeZone: false,
|
||||
});
|
||||
}
|
||||
|
||||
function pickQuickBetMarkets(markets: PlayerHomeMatch['markets']) {
|
||||
const supported = ['FT_1X2', 'FT_HANDICAP', 'FT_OVER_UNDER'];
|
||||
const byType = new Map<string, NonNullable<PlayerHomeMatch['markets']>[number]>();
|
||||
for (const m of markets ?? []) {
|
||||
if (!supported.includes(m.marketType)) continue;
|
||||
if (!Array.isArray(m.selections) || m.selections.length === 0) continue;
|
||||
if (!byType.has(m.marketType)) byType.set(m.marketType, m);
|
||||
}
|
||||
return supported.map((type) => byType.get(type)).filter(Boolean) as NonNullable<PlayerHomeMatch['markets']>;
|
||||
}
|
||||
|
||||
function getActiveMarketType(match: PlayerHomeMatch) {
|
||||
const matchId = match.id;
|
||||
const available = pickQuickBetMarkets(match.markets);
|
||||
const saved = activeMarketTypes.value[matchId];
|
||||
if (saved && available.some((m) => m.marketType === saved)) return saved;
|
||||
if (available.some((m) => m.marketType === 'FT_1X2')) return 'FT_1X2';
|
||||
return available[0]?.marketType ?? 'FT_1X2';
|
||||
}
|
||||
|
||||
function setActiveMarketType(matchId: string, marketType: string) {
|
||||
activeMarketTypes.value[matchId] = marketType;
|
||||
}
|
||||
|
||||
function getAvailableMarketsForMatch(match: PlayerHomeMatch) {
|
||||
return pickQuickBetMarkets(match.markets);
|
||||
}
|
||||
|
||||
function getActiveMarket(match: PlayerHomeMatch) {
|
||||
const activeType = getActiveMarketType(match);
|
||||
return pickQuickBetMarkets(match.markets).find((m) => m.marketType === activeType);
|
||||
}
|
||||
|
||||
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 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 false;
|
||||
}
|
||||
|
||||
function handleOddsClick(match: PlayerHomeMatch, market: any, selId: string, event?: MouseEvent) {
|
||||
if (isMarketLocked(match, market)) 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);
|
||||
}
|
||||
function getSelLabel(sel: any, marketType: string, lineValue?: string | number | null) {
|
||||
return resolveSelectionLabel(t, sel.selectionCode || '', sel.selectionName || '', { lineValue });
|
||||
}
|
||||
|
||||
function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: string) {
|
||||
if (!match || !popoverVisible.value || !pendingItem.value) return '';
|
||||
if (!isActiveForCard(cardRootId)) return '';
|
||||
if (String(pendingItem.value.matchId) !== String(match.id)) return '';
|
||||
|
||||
const selId = pendingItem.value.selectionId;
|
||||
// Scan all markets across all tabs to find the selection
|
||||
for (const market of match.markets ?? []) {
|
||||
const sel = market.selections?.find((s: any) => s.id === selId);
|
||||
if (!sel) continue;
|
||||
const code = (sel.selectionCode || '').toUpperCase();
|
||||
if (code === 'HOME') return 'home';
|
||||
if (code === 'AWAY') return 'away';
|
||||
if (code === 'DRAW') return 'draw';
|
||||
if (code === 'OVER') return 'home';
|
||||
if (code === 'UNDER') return 'away';
|
||||
if (code === 'ODD' || code === 'EVEN') return 'draw';
|
||||
// Fallback: use selection index
|
||||
const selIndex = market.selections?.findIndex((s: any) => s.id === selId) ?? -1;
|
||||
if (selIndex === -1) continue;
|
||||
if (market.marketType === 'FT_1X2') {
|
||||
if (selIndex === 0) return 'home';
|
||||
if (selIndex === 1) return 'draw';
|
||||
if (selIndex === 2) return 'away';
|
||||
} else {
|
||||
if (selIndex === 0) return 'home';
|
||||
if (selIndex === 1) return 'away';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-home-view">
|
||||
<section class="hero-banner-full">
|
||||
<DesktopBanner3DCarousel :banners="banners" :fallback-to="bannerFallbackTo" />
|
||||
<!-- [B] Hero Row: 3D Banner -->
|
||||
<section class="hero-row anim-fade-up" style="--anim-delay:0ms">
|
||||
<div class="hero-banner-full">
|
||||
<DesktopBanner3DCarousel :banners="banners" :fallback-to="bannerFallbackTo" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="content-row">
|
||||
<div class="matches-hub">
|
||||
<div class="hub-header">
|
||||
<div class="tabs-list" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="hub-tab"
|
||||
:class="{ active: activeTab === 'hot' }"
|
||||
:aria-selected="activeTab === 'hot'"
|
||||
@click="activeTab = 'hot'"
|
||||
>
|
||||
{{ t('home.hot_tab') || '热门赛事' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="hub-tab"
|
||||
:class="{ active: activeTab === 'upcoming' }"
|
||||
:aria-selected="activeTab === 'upcoming'"
|
||||
@click="activeTab = 'upcoming'"
|
||||
>
|
||||
{{ t('home.upcoming_tab') || '即将开赛' }}
|
||||
</button>
|
||||
<!-- [D] 主区: 左主内容 + 右侧栏 -->
|
||||
<div class="main-row">
|
||||
<div class="main-col">
|
||||
<!-- [D1] 鐒︾偣璧涗簨 -->
|
||||
<section class="content-section anim-fade-up" style="--anim-delay:160ms">
|
||||
<div class="section-title">
|
||||
<span class="st-line st-line-animated" aria-hidden="true"></span>
|
||||
<span class="st-text">{{ t('home.section_featured') }}</span>
|
||||
</div>
|
||||
<button type="button" class="refresh-btn" :disabled="loading" @click="load(true)">
|
||||
<span v-if="loading" class="spin">↻</span>
|
||||
<span v-else>↻ {{ t('bet.refresh') || '刷新' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading && !displayedMatches.length" class="loading-state">
|
||||
<GoldSpinner :active="true" />
|
||||
</div>
|
||||
<div v-if="loading && !hotMatches.length" class="loading-state">
|
||||
<GoldSpinner :active="true" />
|
||||
</div>
|
||||
|
||||
<!-- Matches Grid -->
|
||||
<div v-else-if="displayedMatches.length" class="matches-grid">
|
||||
<div
|
||||
v-for="match in displayedMatches"
|
||||
:key="match.id"
|
||||
class="match-card clickable"
|
||||
@click="goMatch(match.id)"
|
||||
>
|
||||
<div class="card-top">
|
||||
<span class="league-tag">{{ match.leagueName }}</span>
|
||||
<span class="live-indicator" v-if="(match as any).status === 'LIVE'">LIVE</span>
|
||||
</div>
|
||||
|
||||
<div class="teams-versus">
|
||||
<div class="team-side home">
|
||||
<TeamEmblem
|
||||
size="md"
|
||||
:team-code="match.homeTeamCode"
|
||||
:team-name="match.homeTeamName"
|
||||
:logo-url="match.homeTeamLogoUrl"
|
||||
/>
|
||||
<span class="team-name">{{ match.homeTeamName }}</span>
|
||||
<template v-else-if="featuredMatch">
|
||||
<!-- 鐒︾偣澶у崱 -->
|
||||
<div
|
||||
class="featured-match anim-fade-up"
|
||||
:class="{ 'featured-match--engaged': isActiveForCard(`featured-${featuredMatch.id}`) }"
|
||||
:data-bet-card-root="`featured-${featuredMatch.id}`"
|
||||
style="--anim-delay:240ms"
|
||||
@click.self="goMatch(featuredMatch.id)"
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="scheduleClose()"
|
||||
>
|
||||
<div class="featured-top" @click="goMatch(featuredMatch.id)">
|
||||
<div class="featured-meta">
|
||||
<span class="league-tag lg">{{ featuredMatch.leagueName }}</span>
|
||||
<span
|
||||
v-if="(featuredMatch as any).status === 'LIVE'"
|
||||
class="live-indicator lg live-pulse"
|
||||
>LIVE</span>
|
||||
</div>
|
||||
<span class="featured-time">{{ formatKickoff(featuredMatch.startTime) }}</span>
|
||||
</div>
|
||||
<div class="vs-divider">VS</div>
|
||||
<div class="team-side away">
|
||||
<TeamEmblem
|
||||
size="md"
|
||||
:team-code="match.awayTeamCode"
|
||||
:team-name="match.awayTeamName"
|
||||
:logo-url="match.awayTeamLogoUrl"
|
||||
/>
|
||||
<span class="team-name">{{ match.awayTeamName }}</span>
|
||||
|
||||
<div class="featured-teams" :class="getHoveredSideForMatch(featuredMatch, `featured-${featuredMatch.id}`)" @click="goMatch(featuredMatch.id)">
|
||||
<div class="featured-team">
|
||||
<TeamEmblem
|
||||
size="lg"
|
||||
:team-code="featuredMatch.homeTeamCode"
|
||||
:team-name="featuredMatch.homeTeamName"
|
||||
:logo-url="featuredMatch.homeTeamLogoUrl"
|
||||
/>
|
||||
<span class="featured-team-name">{{ featuredMatch.homeTeamName }}</span>
|
||||
</div>
|
||||
<div class="featured-vs vs-heartbeat">VS</div>
|
||||
<div class="featured-team">
|
||||
<TeamEmblem
|
||||
size="lg"
|
||||
:team-code="featuredMatch.awayTeamCode"
|
||||
:team-name="featuredMatch.awayTeamName"
|
||||
:logo-url="featuredMatch.awayTeamLogoUrl"
|
||||
/>
|
||||
<span class="featured-team-name">{{ featuredMatch.awayTeamName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="featured-markets">
|
||||
<div
|
||||
v-for="m in getAvailableMarketsForMatch(featuredMatch)"
|
||||
:key="m.marketType"
|
||||
class="featured-market-block"
|
||||
>
|
||||
<div class="featured-market-label">{{ getMarketTabLabel(m.marketType) }}</div>
|
||||
<MarketSelectionsPanel
|
||||
desktop-card
|
||||
compact
|
||||
:locked="isMarketLocked(featuredMatch, m)"
|
||||
:line-value="m.lineValue"
|
||||
:selections="m.selections || []"
|
||||
:is-selected="isSelected"
|
||||
@pick="(selId, ev) => handleOddsClick(featuredMatch!, m, selId, ev)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!getAvailableMarketsForMatch(featuredMatch).length" class="no-market-odds">
|
||||
{{ t('bet.no_market_odds') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<span class="match-time">{{ formatKickoff(match.startTime) }}</span>
|
||||
<button type="button" class="bet-action-btn">{{ t('nav.bet') || '投注' }}</button>
|
||||
<!-- 鍏朵綑鐑棬 2 鍒?-->
|
||||
<div v-if="restHotMatches.length" class="matches-grid">
|
||||
<div
|
||||
v-for="(match, idx) in restHotMatches"
|
||||
:key="match.id"
|
||||
class="match-card anim-fade-up"
|
||||
:class="{ 'match-card--engaged': isActiveForCard(`hot-${match.id}`) }"
|
||||
:data-bet-card-root="`hot-${match.id}`"
|
||||
:style="{ '--anim-delay': (320 + idx * 60) + 'ms' }"
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="scheduleClose()"
|
||||
>
|
||||
<div class="match-info-side" @click="goMatch(match.id)">
|
||||
<!-- Default info layout (visible at rest) -->
|
||||
<div class="default-info-layout">
|
||||
<div class="card-top">
|
||||
<span class="league-tag">{{ match.leagueName }}</span>
|
||||
<div class="top-meta">
|
||||
<span class="live-indicator live-pulse" v-if="(match as any).status === 'LIVE'">LIVE</span>
|
||||
<span class="match-time">{{ formatKickoff(match.startTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="teams-versus-compact">
|
||||
<div class="team-row">
|
||||
<TeamEmblem
|
||||
size="sm"
|
||||
:team-code="match.homeTeamCode"
|
||||
:team-name="match.homeTeamName"
|
||||
:logo-url="match.homeTeamLogoUrl"
|
||||
/>
|
||||
<span class="team-name">{{ match.homeTeamName }}</span>
|
||||
</div>
|
||||
<div class="vs-text">VS</div>
|
||||
<div class="team-row">
|
||||
<TeamEmblem
|
||||
size="sm"
|
||||
:team-code="match.awayTeamCode"
|
||||
:team-name="match.awayTeamName"
|
||||
:logo-url="match.awayTeamLogoUrl"
|
||||
/>
|
||||
<span class="team-name">{{ match.awayTeamName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hover versus layout (visible on hover) -->
|
||||
<div class="hover-versus-layout" :class="getHoveredSideForMatch(match, `hot-${match.id}`)">
|
||||
<div class="hover-team home-slide">
|
||||
<TeamEmblem
|
||||
size="md"
|
||||
:team-code="match.homeTeamCode"
|
||||
:team-name="match.homeTeamName"
|
||||
:logo-url="match.homeTeamLogoUrl"
|
||||
/>
|
||||
<span class="hover-team-name">{{ match.homeTeamName }}</span>
|
||||
</div>
|
||||
<div class="hover-vs-glow">VS</div>
|
||||
<div class="hover-team away-slide">
|
||||
<TeamEmblem
|
||||
size="md"
|
||||
:team-code="match.awayTeamCode"
|
||||
:team-name="match.awayTeamName"
|
||||
:logo-url="match.awayTeamLogoUrl"
|
||||
/>
|
||||
<span class="hover-team-name">{{ match.awayTeamName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="match-market-side">
|
||||
<div class="market-tabs">
|
||||
<button
|
||||
v-for="m in getAvailableMarketsForMatch(match)"
|
||||
:key="m.marketType"
|
||||
type="button"
|
||||
class="market-tab-btn"
|
||||
:class="{ active: getActiveMarketType(match) === m.marketType }"
|
||||
@click="setActiveMarketType(match.id, m.marketType)"
|
||||
>
|
||||
{{ getMarketTabLabel(m.marketType) }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="market-odds-area">
|
||||
<template v-if="getActiveMarket(match)">
|
||||
<MarketSelectionsPanel
|
||||
desktop-card
|
||||
compact
|
||||
:locked="isMarketLocked(match, getActiveMarket(match))"
|
||||
:line-value="getActiveMarket(match)?.lineValue"
|
||||
:selections="getActiveMarket(match)?.selections || []"
|
||||
:is-selected="isSelected"
|
||||
@pick="(selId, ev) => handleOddsClick(match, getActiveMarket(match)!, selId, ev)"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="no-market-odds">
|
||||
{{ t('bet.no_market_odds') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<p>{{ t('home.no_matches') }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- [D2] 鍗冲皢寮€璧?-->
|
||||
<section class="content-section anim-fade-up" style="--anim-delay:200ms">
|
||||
<div class="section-title">
|
||||
<span class="st-line" aria-hidden="true"></span>
|
||||
<span class="st-text">{{ t('home.section_upcoming') }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="upcomingList.length" class="matches-grid">
|
||||
<div
|
||||
v-for="(match, idx) in upcomingList"
|
||||
:key="match.id"
|
||||
class="match-card anim-fade-up"
|
||||
:class="{ 'match-card--engaged': isActiveForCard(`upcoming-${match.id}`) }"
|
||||
:data-bet-card-root="`upcoming-${match.id}`"
|
||||
:style="{ '--anim-delay': (260 + idx * 50) + 'ms' }"
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="scheduleClose()"
|
||||
>
|
||||
<div class="match-info-side" @click="goMatch(match.id)">
|
||||
<!-- Default info layout (visible at rest) -->
|
||||
<div class="default-info-layout">
|
||||
<div class="card-top">
|
||||
<span class="league-tag">{{ match.leagueName }}</span>
|
||||
<div class="top-meta">
|
||||
<span class="match-time">{{ formatKickoff(match.startTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="teams-versus-compact">
|
||||
<div class="team-row">
|
||||
<TeamEmblem
|
||||
size="sm"
|
||||
:team-code="match.homeTeamCode"
|
||||
:team-name="match.homeTeamName"
|
||||
:logo-url="match.homeTeamLogoUrl"
|
||||
/>
|
||||
<span class="team-name">{{ match.homeTeamName }}</span>
|
||||
</div>
|
||||
<div class="vs-text">VS</div>
|
||||
<div class="team-row">
|
||||
<TeamEmblem
|
||||
size="sm"
|
||||
:team-code="match.awayTeamCode"
|
||||
:team-name="match.awayTeamName"
|
||||
:logo-url="match.awayTeamLogoUrl"
|
||||
/>
|
||||
<span class="team-name">{{ match.awayTeamName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hover versus layout (visible on hover) -->
|
||||
<div class="hover-versus-layout" :class="getHoveredSideForMatch(match, `upcoming-${match.id}`)">
|
||||
<div class="hover-team home-slide">
|
||||
<TeamEmblem
|
||||
size="md"
|
||||
:team-code="match.homeTeamCode"
|
||||
:team-name="match.homeTeamName"
|
||||
:logo-url="match.homeTeamLogoUrl"
|
||||
/>
|
||||
<span class="hover-team-name">{{ match.homeTeamName }}</span>
|
||||
</div>
|
||||
<div class="hover-vs-glow">VS</div>
|
||||
<div class="hover-team away-slide">
|
||||
<TeamEmblem
|
||||
size="md"
|
||||
:team-code="match.awayTeamCode"
|
||||
:team-name="match.awayTeamName"
|
||||
:logo-url="match.awayTeamLogoUrl"
|
||||
/>
|
||||
<span class="hover-team-name">{{ match.awayTeamName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="match-market-side">
|
||||
<div class="market-tabs">
|
||||
<button
|
||||
v-for="m in getAvailableMarketsForMatch(match)"
|
||||
:key="m.marketType"
|
||||
type="button"
|
||||
class="market-tab-btn"
|
||||
:class="{ active: getActiveMarketType(match) === m.marketType }"
|
||||
@click="setActiveMarketType(match.id, m.marketType)"
|
||||
>
|
||||
{{ getMarketTabLabel(m.marketType) }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="market-odds-area">
|
||||
<template v-if="getActiveMarket(match)">
|
||||
<MarketSelectionsPanel
|
||||
desktop-card
|
||||
compact
|
||||
:locked="isMarketLocked(match, getActiveMarket(match))"
|
||||
:line-value="getActiveMarket(match)?.lineValue"
|
||||
:selections="getActiveMarket(match)?.selections || []"
|
||||
:is-selected="isSelected"
|
||||
@pick="(selId, ev) => handleOddsClick(match, getActiveMarket(match)!, selId, ev)"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="no-market-odds">
|
||||
{{ t('bet.no_market_odds') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="empty-state">
|
||||
<p>{{ emptyMessage }}</p>
|
||||
</div>
|
||||
<div v-else-if="!loading" class="empty-state">
|
||||
<p>{{ t('home.upcoming_empty') }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="home-sidebar">
|
||||
<!-- 鍙充晶澶氬崱杈规爮 -->
|
||||
<aside class="home-sidebar anim-fade-up" style="--anim-delay:120ms">
|
||||
<HomeAnnouncementCard />
|
||||
|
||||
<div v-if="sideRecommendMatches.length" class="side-card">
|
||||
<div class="side-card-header">
|
||||
<span class="side-card-title">{{ t('home.sidebar_recommend') }}</span>
|
||||
</div>
|
||||
<ul class="side-recommend-list">
|
||||
<li
|
||||
v-for="(m, idx) in sideRecommendMatches"
|
||||
:key="m.id"
|
||||
class="side-recommend-item anim-fade-up"
|
||||
:class="{ 'side-recommend-item--engaged': isActiveForCard(`sidebar-${m.id}`) }"
|
||||
:data-bet-card-root="`sidebar-${m.id}`"
|
||||
:style="{ '--anim-delay': (200 + idx * 70) + 'ms' }"
|
||||
@click="goMatch(m.id)"
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="scheduleClose()"
|
||||
>
|
||||
<div class="sri-top-row">
|
||||
<span class="sri-league">{{ m.leagueName }}</span>
|
||||
<span class="sri-time">{{ formatKickoff(m.startTime) }}</span>
|
||||
</div>
|
||||
<div class="sri-teams">
|
||||
<span>{{ m.homeTeamName }}</span>
|
||||
<span class="sri-vs">vs</span>
|
||||
<span>{{ m.awayTeamName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 鎮诞棰勮鍗?-->
|
||||
<div class="sri-preview-card" @click.stop @mouseenter="cancelClose" @mouseleave="scheduleClose()">
|
||||
<div class="sri-preview-top">
|
||||
<span class="sri-preview-league">{{ m.leagueName }}</span>
|
||||
<span class="sri-preview-time">{{ formatKickoff(m.startTime) }}</span>
|
||||
</div>
|
||||
<div class="sri-preview-vs" :class="getHoveredSideForMatch(m, `sidebar-${m.id}`)">
|
||||
<div class="sri-preview-team home-team">
|
||||
<TeamEmblem size="md" :team-code="m.homeTeamCode" :team-name="m.homeTeamName" :logo-url="m.homeTeamLogoUrl" />
|
||||
<span>{{ m.homeTeamName }}</span>
|
||||
</div>
|
||||
<div class="sri-preview-vs-text">VS</div>
|
||||
<div class="sri-preview-team away-team">
|
||||
<TeamEmblem size="md" :team-code="m.awayTeamCode" :team-name="m.awayTeamName" :logo-url="m.awayTeamLogoUrl" />
|
||||
<span>{{ m.awayTeamName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="getActiveMarket(m)" class="sri-preview-odds">
|
||||
<div
|
||||
v-for="sel in (getActiveMarket(m)?.selections || []).slice(0, 3)"
|
||||
:key="sel.id"
|
||||
class="sri-preview-odd-btn"
|
||||
:class="{ selected: isSelected(sel.id) }"
|
||||
@click.stop="handleOddsClick(m, getActiveMarket(m)!, sel.id, $event)"
|
||||
>
|
||||
<span class="sri-odd-label">{{ getSelLabel(sel, getActiveMarket(m)?.marketType || '', getActiveMarket(m)?.lineValue) }}</span>
|
||||
<span class="sri-odd-val">{{ sel.odds }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="sri-preview-no-odds">{{ t('bet.no_markets') }}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,70 +547,82 @@ function formatKickoff(startTime: string) {
|
||||
.desktop-home-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero-row {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero-banner-full {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content-row {
|
||||
.qe-icon {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.qe-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* [D] Main Row */
|
||||
.main-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.home-sidebar {
|
||||
.main-col {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.home-sidebar :deep(.home-announce-card) {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.matches-hub {
|
||||
background: rgba(20, 20, 20, 0.85);
|
||||
.content-section {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6) var(--space-7);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: box-shadow var(--transition);
|
||||
}
|
||||
|
||||
.hub-header {
|
||||
/* section-title 閲戠嚎 */
|
||||
.section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.tabs-list {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
.st-line {
|
||||
width: 4px;
|
||||
height: 18px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(180deg, var(--primary), var(--primary-lighter));
|
||||
box-shadow: 0 0 8px rgba(0, 102, 204, 0.4), var(--glow-gold);
|
||||
}
|
||||
|
||||
.hub-tab {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
padding: 8px 4px;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
.st-text {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text);
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hub-tab:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hub-tab.active {
|
||||
color: var(--primary-light);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
.st-refresh {
|
||||
margin-left: auto;
|
||||
background: transparent;
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
@@ -215,6 +630,12 @@ function formatKickoff(startTime: string) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.st-refresh:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.spin {
|
||||
@@ -231,125 +652,923 @@ function formatKickoff(startTime: string) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
min-height: 160px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.matches-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.match-card {
|
||||
background: #141414;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 14px;
|
||||
/* Featured 鐒︾偣澶у崱 */
|
||||
.featured-match {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
transition: all 0.2s;
|
||||
gap: var(--space-5);
|
||||
padding: var(--space-7) var(--space-8);
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(180deg, #f8fafc 0%, var(--bg-card) 100%);
|
||||
border: 1px solid var(--border-active);
|
||||
box-shadow: var(--shadow-gold-lg);
|
||||
margin-bottom: var(--space-5);
|
||||
cursor: pointer;
|
||||
transition: box-shadow var(--transition-slow), transform var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.match-card:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: var(--shadow-gold);
|
||||
.featured-match::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; right: 0;
|
||||
width: 140px; height: 140px;
|
||||
background: radial-gradient(circle at top right, rgba(0, 102, 204, 0.12), transparent 65%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
.featured-match:is(:hover, .featured-match--engaged) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-xl), inset 0 0 24px rgba(0, 102, 204, 0.08), var(--glow-gold);
|
||||
}
|
||||
|
||||
.featured-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.league-tag {
|
||||
.featured-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.league-tag.lg {
|
||||
font-size: 12px;
|
||||
padding: 3px 8px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.live-indicator.lg {
|
||||
font-size: 11px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
.featured-time {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.featured-teams {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.featured-team {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.featured-team-name {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
.featured-vs {
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--odds-accent, #F8971F);
|
||||
text-shadow: 0 0 12px rgba(248, 151, 31, 0.35);
|
||||
}
|
||||
|
||||
.featured-markets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
margin: 10px -20px -18px;
|
||||
padding: 10px 16px 14px;
|
||||
background: #F0F4F8;
|
||||
border-top: 1px solid rgba(0, 102, 204, 0.12);
|
||||
}
|
||||
|
||||
.featured-market-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.featured-market-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--primary);
|
||||
text-align: center;
|
||||
letter-spacing: 0.4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* 鏅€氳禌浜嬪崱鐗?grid锛? 鍒楋級 */
|
||||
.matches-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.match-card {
|
||||
display: flex;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
transition: border-color var(--transition), box-shadow var(--transition), transform var(--transition);
|
||||
min-height: 68px;
|
||||
}
|
||||
|
||||
.match-card:is(:hover, .match-card--engaged) {
|
||||
border-color: var(--border-active);
|
||||
box-shadow: var(--shadow-md), var(--glow-gold);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.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;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.match-info-side > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.match-card:is(:hover, .match-card--engaged) .match-info-side {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.match-market-side {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.top-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.league-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
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(255, 255, 255, 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;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.team-name {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #eee;
|
||||
text-align: center;
|
||||
max-width: 100px;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text);
|
||||
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: #222;
|
||||
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%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
.no-market-odds {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
padding: 2px 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 鍙充晶鏍?*/
|
||||
.home-sidebar {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.home-sidebar :deep(.home-announce-card) {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.side-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-5) var(--space-6);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
.side-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.side-card-title {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--primary-light);
|
||||
letter-spacing: 0.3px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.side-recommend-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.side-recommend-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.side-recommend-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.sri-top-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.sri-league {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
background: var(--border-gold-soft);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sri-teams {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sri-vs {
|
||||
color: var(--text-muted);
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sri-time {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* @media 鍗曞垪鍥為€€ */
|
||||
@media (max-width: 1100px) {
|
||||
.main-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.matches-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.featured-markets {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.featured-team-name {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
鉁?ANIMATION SYSTEM
|
||||
============================================================ */
|
||||
|
||||
/* --- 1. 閫氱敤鍏ュ満锛歠ade + slide-up --- */
|
||||
@keyframes fadeSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.anim-fade-up {
|
||||
opacity: 0;
|
||||
animation: fadeSlideUp 0.55s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
animation-delay: var(--anim-delay, 0ms);
|
||||
}
|
||||
|
||||
/* --- 2. 榛勯噾鍏夋檿鍛煎惛鑴夊啿锛堢劍鐐瑰ぇ鍗?::before锛?--- */
|
||||
@keyframes goldGlowPulse {
|
||||
0%, 100% {
|
||||
opacity: 0.18;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.32;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
}
|
||||
|
||||
.featured-match::before {
|
||||
animation: goldGlowPulse 3.5s ease-in-out infinite;
|
||||
transform-origin: top right;
|
||||
}
|
||||
|
||||
/* --- 3. VS 蹇冭烦 --- */
|
||||
@keyframes vsHeartbeat {
|
||||
0%, 100% { transform: scale(1); }
|
||||
14% { transform: scale(1.12); }
|
||||
28% { transform: scale(1); }
|
||||
42% { transform: scale(1.06); }
|
||||
56% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.vs-heartbeat {
|
||||
animation: vsHeartbeat 2.8s ease-in-out infinite;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* --- 4. LIVE 鑴夊啿闂儊 --- */
|
||||
@keyframes livePulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7); }
|
||||
50% { box-shadow: 0 0 0 5px rgba(220, 38, 38, 0); }
|
||||
}
|
||||
|
||||
.live-pulse {
|
||||
animation: livePulse 1.6s ease-out infinite;
|
||||
}
|
||||
|
||||
/* --- 5. section 鏍囬閲戠嚎婊戝叆 --- */
|
||||
@keyframes lineSlideIn {
|
||||
from { transform: scaleY(0); opacity: 0; }
|
||||
to { transform: scaleY(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.st-line-animated {
|
||||
transform-origin: top center;
|
||||
animation: lineSlideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s both;
|
||||
}
|
||||
|
||||
/* --- 6. 璧涗簨鍗$墖锛氶紶鏍囩Щ鍔ㄥ埌鍗$墖涓婃椂涓ゅ彧闃熶紞灞呬腑涓斿嚭鐜?VS 缁忓吀鍔ㄧ敾 --- */
|
||||
.default-info-layout {
|
||||
width: 100%;
|
||||
transition: opacity 0.35s cubic-bezier(0.25, 0.8, 0.25, 1), transform 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.match-card:is(:hover, .match-card--engaged) .default-info-layout {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.95);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hover-versus-layout {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding: 8px 16px;
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.95);
|
||||
transition: opacity 0.35s cubic-bezier(0.25, 0.8, 0.25, 1), transform 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
pointer-events: none;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.match-card:is(:hover, .match-card--engaged) .hover-versus-layout {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.hover-team.home-slide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transform: translateX(-35px);
|
||||
opacity: 0;
|
||||
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 0.05s, opacity 0.4s ease 0.05s;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.match-card:is(:hover, .match-card--engaged) .hover-team.home-slide {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hover-team.away-slide {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transform: translateX(35px);
|
||||
opacity: 0;
|
||||
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 0.05s, opacity 0.4s ease 0.05s;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.match-card:is(:hover, .match-card--engaged) .hover-team.away-slide {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hover-team-name {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hover-vs-glow {
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
color: var(--odds-accent, #F8971F);
|
||||
transform: scale(2.8);
|
||||
opacity: 0;
|
||||
text-shadow:
|
||||
0 0 10px rgba(248, 151, 31, 0.5),
|
||||
0 0 20px rgba(248, 151, 31, 0.25);
|
||||
transition: transform 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s, opacity 0.3s ease 0.1s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.match-card:is(:hover, .match-card--engaged) .hover-vs-glow {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
animation: vs-clash-impact 0.3s ease-out 0.35s;
|
||||
}
|
||||
|
||||
@keyframes vs-clash-impact {
|
||||
0% {
|
||||
text-shadow: 0 0 25px rgba(248, 151, 31, 0.7);
|
||||
transform: scale(1.25);
|
||||
}
|
||||
100% {
|
||||
text-shadow: 0 0 8px rgba(248, 151, 31, 0.4);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- 7. 渚ф爮鍗$墖鏁翠綋 hover 涓婃诞 --- */
|
||||
.side-card {
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.side-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md), var(--glow-gold);
|
||||
}
|
||||
|
||||
/* --- 8. 鎺ㄨ崘鍒楄〃 item 宸︿晶閲戠偣鎸囩ず hover --- */
|
||||
.side-recommend-item {
|
||||
position: relative;
|
||||
transition: background 0.18s, transform 0.18s;
|
||||
}
|
||||
|
||||
.side-recommend-item:hover {
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
/* 榧犳爣绉诲姩閫氶亾妗ユ帴妗ワ紙闃叉鍦ㄩ棿闅欏涓㈠けhover鐘舵€侊紝楂樺害闄愬埗涓烘帹鑽愰」楂樺害锛岄槻姝㈤噸鍙犻敊璇級 */
|
||||
.side-recommend-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -20px;
|
||||
width: 20px;
|
||||
background: transparent;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* --- 9. 鎺ㄨ崘璧涗簨鎮诞棰勮鍗?--- */
|
||||
.sri-preview-card {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: calc(100% + 6px);
|
||||
transform: translateY(-50%) translateX(8px);
|
||||
width: 210px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-active);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
box-shadow: 0 6px 24px rgba(0, 61, 107, 0.12), 0 0 12px rgba(0, 102, 204, 0.06);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.side-recommend-item:is(:hover, .side-recommend-item--engaged) .sri-preview-card {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) translateX(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.sri-preview-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.match-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
.sri-preview-league {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
background: var(--border-gold-soft);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bet-action-btn {
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: transparent;
|
||||
color: var(--primary-light);
|
||||
.sri-preview-time {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sri-preview-vs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sri-preview-team {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.35s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.sri-preview-team.home-team {
|
||||
transform: translateX(-24px);
|
||||
}
|
||||
|
||||
.sri-preview-team.away-team {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.side-recommend-item:hover .sri-preview-team {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sri-preview-team span {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
transition: all 0.2s;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.match-card:hover .bet-action-btn {
|
||||
background: var(--primary);
|
||||
color: #3D2800;
|
||||
.sri-preview-vs-text {
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
color: var(--odds-accent, #F8971F);
|
||||
text-shadow: 0 0 8px rgba(248, 151, 31, 0.35);
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transform: scale(2.2);
|
||||
transition: opacity 0.3s ease 0.1s, transform 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s;
|
||||
}
|
||||
|
||||
.side-recommend-item:hover .sri-preview-vs-text {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.sri-preview-odds {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.sri-preview-odd-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 5px 4px;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-hover);
|
||||
border: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.sri-preview-odd-btn:hover {
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
border-color: var(--border-active);
|
||||
}
|
||||
|
||||
.sri-preview-odd-btn.selected {
|
||||
background: rgba(0, 61, 107, 0.08);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.sri-odd-label {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sri-odd-val {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--odds-accent, #F8971F);
|
||||
}
|
||||
|
||||
.sri-preview-no-odds {
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* --- Sri preview: team highlight on odds selection --- */
|
||||
.sri-preview-team {
|
||||
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease;
|
||||
}
|
||||
|
||||
.sri-preview-vs.home .sri-preview-team.home-team {
|
||||
transform: translateX(0) scale(1.1) !important;
|
||||
filter: brightness(1.3) saturate(1.2);
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.sri-preview-vs.home .sri-preview-team.away-team {
|
||||
opacity: 0.35 !important;
|
||||
transform: translateX(0) scale(0.92) !important;
|
||||
filter: grayscale(0.6) brightness(0.7);
|
||||
}
|
||||
|
||||
.sri-preview-vs.away .sri-preview-team.away-team {
|
||||
transform: translateX(0) scale(1.1) !important;
|
||||
filter: brightness(1.3) saturate(1.2);
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.sri-preview-vs.away .sri-preview-team.home-team {
|
||||
opacity: 0.35 !important;
|
||||
transform: translateX(0) scale(0.92) !important;
|
||||
filter: grayscale(0.6) brightness(0.7);
|
||||
}
|
||||
|
||||
.sri-preview-vs.draw .sri-preview-team {
|
||||
transform: translateX(0) scale(1.05) !important;
|
||||
filter: brightness(1.15) saturate(1.1);
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* --- VS Selection Highlight: scale selected team, no VS animation --- */
|
||||
.featured-team,
|
||||
.hover-team {
|
||||
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease;
|
||||
}
|
||||
|
||||
/* featured match (home) */
|
||||
.featured-teams.home .featured-team:first-child {
|
||||
transform: scale(1.08);
|
||||
filter: brightness(1.3) saturate(1.2);
|
||||
}
|
||||
.featured-teams.home .featured-team:last-child {
|
||||
opacity: 0.35;
|
||||
transform: scale(0.94);
|
||||
filter: grayscale(0.6) brightness(0.7);
|
||||
}
|
||||
/* featured match (away) */
|
||||
.featured-teams.away .featured-team:last-child {
|
||||
transform: scale(1.08);
|
||||
filter: brightness(1.3) saturate(1.2);
|
||||
}
|
||||
.featured-teams.away .featured-team:first-child {
|
||||
opacity: 0.35;
|
||||
transform: scale(0.94);
|
||||
filter: grayscale(0.6) brightness(0.7);
|
||||
}
|
||||
/* featured match (draw) */
|
||||
.featured-teams.draw .featured-team {
|
||||
transform: scale(1.05);
|
||||
filter: brightness(1.15) saturate(1.1);
|
||||
}
|
||||
|
||||
/* hover-versus-layout (sidebar/upcoming) */
|
||||
.hover-versus-layout.home .hover-team.home-slide {
|
||||
transform: scale(1.08);
|
||||
filter: brightness(1.3) saturate(1.2);
|
||||
z-index: 2;
|
||||
}
|
||||
.hover-versus-layout.home .hover-team.away-slide {
|
||||
opacity: 0.35 !important;
|
||||
transform: scale(0.94);
|
||||
filter: grayscale(0.6) brightness(0.7);
|
||||
}
|
||||
.hover-versus-layout.away .hover-team.away-slide {
|
||||
transform: scale(1.08);
|
||||
filter: brightness(1.3) saturate(1.2);
|
||||
z-index: 2;
|
||||
}
|
||||
.hover-versus-layout.away .hover-team.home-slide {
|
||||
opacity: 0.35 !important;
|
||||
transform: scale(0.94);
|
||||
filter: grayscale(0.6) brightness(0.7);
|
||||
}
|
||||
.hover-versus-layout.draw .hover-team.home-slide,
|
||||
.hover-versus-layout.draw .hover-team.away-slide {
|
||||
transform: scale(1.05);
|
||||
filter: brightness(1.15) saturate(1.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -181,11 +181,11 @@ onActivated(refreshUnreadCount);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hub-tab:hover { color: #fff; }
|
||||
.hub-tab:hover { color: var(--primary); }
|
||||
|
||||
.hub-tab.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.hub-tab.active .dot { background: var(--primary); box-shadow: 0 0 8px var(--primary); }
|
||||
@@ -194,7 +194,7 @@ onActivated(refreshUnreadCount);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #333;
|
||||
background: var(--bg-hover);
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
@@ -1,134 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Desktop Login View
|
||||
* Reuses the existing LoginView form in a centered two-column auth layout.
|
||||
*/
|
||||
import DesktopAuthLayout from '../../components/desktop/DesktopAuthLayout.vue';
|
||||
import LoginView from '../LoginView.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-auth-shell">
|
||||
<div class="auth-brand">
|
||||
<div class="brand-inner">
|
||||
<img src="/logo.png" alt="TheBet365" class="brand-logo" />
|
||||
<h1 class="brand-name">THEBET365</h1>
|
||||
<p class="brand-tagline">专业体育赛事投注平台</p>
|
||||
<div class="brand-features">
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon">⚡</span>
|
||||
<span>实时赔率更新</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon">🔒</span>
|
||||
<span>安全稳定平台</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon">💰</span>
|
||||
<span>快速充值提款</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-card-wrap">
|
||||
<div class="auth-card">
|
||||
<LoginView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopAuthLayout>
|
||||
<LoginView desktop />
|
||||
</DesktopAuthLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-auth-shell {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: var(--desktop-bg);
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0a0a0a 0%, #111 50%, #0d0d0d 100%);
|
||||
border-right: 1px solid var(--desktop-border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-brand::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 60% 60% at 50% 40%, rgba(212,175,55,0.06) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 16px rgba(212,175,55,0.3));
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--primary-light);
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 12px rgba(212,175,55,0.25);
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.brand-features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.auth-card-wrap {
|
||||
width: 480px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
background: var(--desktop-bg);
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api from '../../api';
|
||||
import { useBetSlipStore } from '../../stores/betSlip';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import TeamEmblem from '../../components/TeamEmblem.vue';
|
||||
import MatchBetGuide from '../../components/match-detail/MatchBetGuide.vue';
|
||||
import MarketSelectionsPanel from '../../components/match-detail/MarketSelectionsPanel.vue';
|
||||
@@ -22,8 +21,7 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const slip = useBetSlipStore();
|
||||
const auth = useAuthStore();
|
||||
const { openAt, visible: popoverVisible, pendingItem: popoverItem } = useDesktopBetPopover();
|
||||
const { openAt, visible: popoverVisible, pendingItem: popoverItem, cancelClose, scheduleClose } = useDesktopBetPopover();
|
||||
const { pendingLocate, clearLocate } = useDesktopBetLocate();
|
||||
|
||||
function goBack() {
|
||||
@@ -291,14 +289,39 @@ function buildSlipItem(sel: Selection, market: Market) {
|
||||
|
||||
function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
if (!match.value || isMarketLocked(market)) return;
|
||||
if (!auth.token) {
|
||||
auth.showLoginPrompt(route.fullPath);
|
||||
return;
|
||||
}
|
||||
const item = buildSlipItem(sel, market);
|
||||
if (!item || !event) return;
|
||||
openAt(event, item);
|
||||
}
|
||||
|
||||
function getHoveredSide() {
|
||||
if (!match.value || !popoverVisible.value || !popoverItem.value) return '';
|
||||
if (String(popoverItem.value.matchId) !== String(match.value.id)) return '';
|
||||
|
||||
const selId = popoverItem.value.selectionId;
|
||||
for (const market of allMarkets.value) {
|
||||
const sel = market.selections?.find((s) => s.id === selId);
|
||||
if (!sel) continue;
|
||||
const code = (sel.selectionCode || '').toUpperCase();
|
||||
if (code === 'HOME') return 'home';
|
||||
if (code === 'AWAY') return 'away';
|
||||
if (code === 'DRAW') return 'draw';
|
||||
if (code === 'OVER') return 'home';
|
||||
if (code === 'UNDER') return 'away';
|
||||
if (code === 'ODD' || code === 'EVEN') return 'draw';
|
||||
const selIndex = market.selections?.findIndex((s) => s.id === selId) ?? -1;
|
||||
if (selIndex === -1) continue;
|
||||
if (market.marketType === 'FT_1X2') {
|
||||
if (selIndex === 0) return 'home';
|
||||
if (selIndex === 1) return 'draw';
|
||||
if (selIndex === 2) return 'away';
|
||||
} else {
|
||||
if (selIndex === 0) return 'home';
|
||||
if (selIndex === 1) return 'away';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -318,7 +341,12 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
|
||||
<template v-else-if="match">
|
||||
<!-- High Density Score Band -->
|
||||
<div class="match-score-band" :class="{ 'phase-settled': matchPhase === 'settled' }">
|
||||
<div
|
||||
class="match-score-band"
|
||||
:class="[{ 'phase-settled': matchPhase === 'settled' }, getHoveredSide()]"
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="scheduleClose()"
|
||||
>
|
||||
<div class="team-side home">
|
||||
<TeamEmblem size="xl" :team-code="match.homeTeamCode" :team-name="match.homeTeamName" :logo-url="match.homeTeamLogoUrl" />
|
||||
<span class="name">{{ match.homeTeamName }}</span>
|
||||
@@ -382,6 +410,8 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
:key="market.id"
|
||||
class="market-card"
|
||||
:class="{ locked: isMarketLocked(market) }"
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="scheduleClose()"
|
||||
>
|
||||
<div class="market-hdr">
|
||||
<span class="market-title">{{ marketLabel(market) }}</span>
|
||||
@@ -410,6 +440,8 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
:key="market.id"
|
||||
class="score-block"
|
||||
:class="{ locked: isMarketLocked(market) }"
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="scheduleClose()"
|
||||
>
|
||||
<div class="score-block-hdr">
|
||||
<span>{{ marketLabel(market) }}</span>
|
||||
@@ -456,7 +488,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
background: #0c0c0c;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -503,7 +535,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
}
|
||||
|
||||
.match-score-band {
|
||||
background: linear-gradient(180deg, #181818 0%, #111 100%);
|
||||
background: linear-gradient(180deg, var(--bg-body) 0%, var(--bg-card) 100%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 10px 16px;
|
||||
@@ -527,7 +559,8 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
.team-side .name {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.score-arena {
|
||||
@@ -548,7 +581,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
color: var(--primary-light);
|
||||
text-shadow: 0 0 10px rgba(212, 175, 55, 0.3);
|
||||
text-shadow: 0 0 10px rgba(0, 102, 204, 0.3);
|
||||
}
|
||||
|
||||
.versus-row .divider {
|
||||
@@ -568,7 +601,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
}
|
||||
|
||||
.result-stats-card {
|
||||
background: rgba(20, 20, 20, 0.8);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
@@ -589,7 +622,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
}
|
||||
|
||||
.stat-pill {
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
@@ -599,7 +632,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
}
|
||||
|
||||
.stat-pill .lbl { color: var(--text-muted); }
|
||||
.stat-pill .val { color: #fff; font-weight: 700; }
|
||||
.stat-pill .val { color: var(--text); font-weight: 700; }
|
||||
|
||||
.market-filter-bar {
|
||||
display: flex;
|
||||
@@ -608,7 +641,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: rgba(18, 18, 18, 0.85);
|
||||
background: var(--bg-card);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -642,7 +675,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
padding: 3px 7px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
@@ -652,15 +685,15 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
}
|
||||
|
||||
.market-filter-chip:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--primary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.market-filter-chip.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.1);
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: inset 0 0 0 1px rgba(212, 175, 55, 0.08);
|
||||
background: rgba(0, 102, 204, 0.1);
|
||||
border-color: var(--border-active);
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.markets-score-section {
|
||||
@@ -700,7 +733,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: rgba(18, 18, 18, 0.98);
|
||||
background: var(--bg-card);
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
@@ -714,14 +747,14 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.market-title {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: #eee;
|
||||
color: var(--text);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
@@ -741,7 +774,7 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: rgba(18, 18, 18, 0.98);
|
||||
background: var(--bg-card);
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
@@ -755,11 +788,11 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #141414;
|
||||
background: var(--bg-body);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: #eee;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.locked-lbl {
|
||||
@@ -768,4 +801,35 @@ function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.team-side {
|
||||
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease;
|
||||
}
|
||||
|
||||
.match-score-band.home .team-side.home {
|
||||
transform: scale(1.06);
|
||||
filter: brightness(1.08) saturate(1.15);
|
||||
}
|
||||
|
||||
.match-score-band.home .team-side.away {
|
||||
opacity: 0.35;
|
||||
transform: scale(0.94);
|
||||
filter: grayscale(0.6) brightness(0.7);
|
||||
}
|
||||
|
||||
.match-score-band.away .team-side.away {
|
||||
transform: scale(1.06);
|
||||
filter: brightness(1.08) saturate(1.15);
|
||||
}
|
||||
|
||||
.match-score-band.away .team-side.home {
|
||||
opacity: 0.35;
|
||||
transform: scale(0.94);
|
||||
filter: grayscale(0.6) brightness(0.7);
|
||||
}
|
||||
|
||||
.match-score-band.draw .team-side {
|
||||
transform: scale(1.04);
|
||||
filter: brightness(1.08) saturate(1.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -80,6 +80,7 @@ onActivated(refreshUnreadCount);
|
||||
.desktop-hub-page {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hub-aside {
|
||||
@@ -123,12 +124,12 @@ onActivated(refreshUnreadCount);
|
||||
}
|
||||
|
||||
.hub-tab:hover {
|
||||
color: #fff;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.hub-tab.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.hub-tab.active .dot {
|
||||
@@ -140,7 +141,7 @@ onActivated(refreshUnreadCount);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #333;
|
||||
background: var(--bg-hover);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onActivated } from 'vue';
|
||||
import { ref, onMounted, onActivated, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import api from '../../api';
|
||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||
import Pagination from '../../components/desktop/Pagination.vue';
|
||||
import DesktopBetDetailModal from '../../components/desktop/DesktopBetDetailModal.vue';
|
||||
import { type BetHistoryItem } from '../../components/BetHistoryCard.vue';
|
||||
import { useOnLocaleChange } from '../../composables/useOnLocaleChange';
|
||||
|
||||
const COL_COUNT = 8;
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const detailModalVisible = ref(false);
|
||||
const detailBetNo = ref<string | null>(null);
|
||||
|
||||
function openDetail(betNo: string) {
|
||||
detailBetNo.value = betNo;
|
||||
detailModalVisible.value = true;
|
||||
}
|
||||
|
||||
function openDetailFromQuery() {
|
||||
const betNo = route.query.betNo;
|
||||
if (typeof betNo === 'string' && betNo) {
|
||||
openDetail(betNo);
|
||||
const { betNo: _removed, ...rest } = route.query;
|
||||
void router.replace({ path: route.path, query: rest });
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => route.query.betNo, openDetailFromQuery);
|
||||
|
||||
const items = ref<BetHistoryItem[]>([]);
|
||||
const total = ref(0);
|
||||
@@ -60,8 +82,14 @@ useOnLocaleChange(() => {
|
||||
void loadPage(1);
|
||||
});
|
||||
|
||||
onActivated(() => { void loadPage(page.value); });
|
||||
onMounted(() => { void loadPage(1); });
|
||||
onActivated(() => {
|
||||
void loadPage(page.value);
|
||||
openDetailFromQuery();
|
||||
});
|
||||
onMounted(() => {
|
||||
void loadPage(1);
|
||||
openDetailFromQuery();
|
||||
});
|
||||
|
||||
function statusClass(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
@@ -149,10 +177,13 @@ function statusLabel(status: string) {
|
||||
</td>
|
||||
</tr>
|
||||
<template v-else-if="items.length">
|
||||
<tr v-for="bet in items" :key="bet.betNo" class="hover-row">
|
||||
<td>
|
||||
<RouterLink :to="`/bets/${bet.betNo}`" class="link-cell">{{ bet.betNo }}</RouterLink>
|
||||
</td>
|
||||
<tr
|
||||
v-for="bet in items"
|
||||
:key="bet.betNo"
|
||||
class="hover-row clickable-row"
|
||||
@click="openDetail(bet.betNo)"
|
||||
>
|
||||
<td class="id-cell">{{ bet.betNo }}</td>
|
||||
<td>{{ bet.matchTitle || '-' }}</td>
|
||||
<td class="muted-cell">{{ bet.pickLabel || '-' }}</td>
|
||||
<td class="num-cell">{{ bet.totalOdds ?? '-' }}</td>
|
||||
@@ -183,6 +214,8 @@ function statusLabel(status: string) {
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<DesktopBetDetailModal v-model:visible="detailModalVisible" :bet-no="detailBetNo" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -190,5 +223,7 @@ function statusLabel(status: string) {
|
||||
.muted-cell {
|
||||
color: var(--text-muted);
|
||||
max-width: 280px;
|
||||
font-size: 12px;
|
||||
font-family: var(--font-data);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,10 +9,8 @@ import OutrightBetModal, { type OutrightPick } from '../../components/outright/O
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||
import saishiImg from '../../assets/images/saishi.webp';
|
||||
import cardBg from '../../assets/images/card-bg.webp';
|
||||
import { useDesktopBetLocate, scrollToBetSelection } from '../../composables/useDesktopBetLocate';
|
||||
|
||||
const matchCardBg = `url(${cardBg})`;
|
||||
const ODDS_POLL_MS = 15000;
|
||||
|
||||
const route = useRoute();
|
||||
@@ -182,7 +180,7 @@ onUnmounted(stopPolling);
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
background: #0c0c0c;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -237,19 +235,10 @@ onUnmounted(stopPolling);
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: rgba(18, 18, 18, 0.98);
|
||||
background: var(--bg-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.event-hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
|
||||
opacity: 0.22;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -274,8 +263,8 @@ onUnmounted(stopPolling);
|
||||
.settled-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: #c9a227;
|
||||
border: 1px solid rgba(201, 162, 39, 0.45);
|
||||
color: var(--odds-accent);
|
||||
border: 1px solid rgba(248, 151, 31, 0.45);
|
||||
border-radius: 999px;
|
||||
padding: 1px 7px;
|
||||
}
|
||||
@@ -291,14 +280,14 @@ onUnmounted(stopPolling);
|
||||
margin: 4px 0 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.hero-hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #c9a227;
|
||||
color: var(--odds-accent);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import PlayerAvatarModal from '../../components/PlayerAvatarModal.vue';
|
||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||
import { usePlayerProfile } from '../../composables/usePlayerProfile';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import PageBackButton from '../../components/PageBackButton.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
@@ -110,6 +111,7 @@ async function onAvatarSelect(key: string | null) {
|
||||
<main class="account-main">
|
||||
<div class="edit-layout">
|
||||
<div class="page-header">
|
||||
<PageBackButton fallback="/wallet" />
|
||||
<h1 class="page-title">{{ t('profile.edit_title') }}</h1>
|
||||
</div>
|
||||
|
||||
@@ -216,7 +218,7 @@ async function onAvatarSelect(key: string | null) {
|
||||
.desktop-account-page { display: flex; flex-direction: column; min-height: 0; flex: 1; width: 100%; }
|
||||
|
||||
.account-main { flex: 1; min-width: 0; padding: 24px 28px; overflow-y: auto; }
|
||||
.page-header { margin-bottom: 24px; }
|
||||
.page-header { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; }
|
||||
.page-title { font-size: 18px; font-weight: 800; color: var(--primary-light); margin: 0; }
|
||||
|
||||
.edit-layout {
|
||||
@@ -230,7 +232,7 @@ async function onAvatarSelect(key: string | null) {
|
||||
.edit-card {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -260,14 +262,14 @@ async function onAvatarSelect(key: string | null) {
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid var(--border-gold-soft);
|
||||
border: 2px solid var(--border-active);
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: #1a1a1a;
|
||||
background: var(--bg-body);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -287,7 +289,7 @@ async function onAvatarSelect(key: string | null) {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.change-avatar-btn:hover { border-color: var(--border-gold-soft); color: var(--primary-light); }
|
||||
.change-avatar-btn:hover { border-color: var(--border-active); color: var(--primary-light); }
|
||||
|
||||
.field-group { display: flex; flex-direction: column; gap: 14px; margin-bottom: 16px; }
|
||||
|
||||
@@ -305,7 +307,7 @@ async function onAvatarSelect(key: string | null) {
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--desktop-border);
|
||||
background: #111;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -313,7 +315,7 @@ async function onAvatarSelect(key: string | null) {
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.field-input:focus { border-color: var(--border-gold-soft); }
|
||||
.field-input:focus { border-color: var(--border-active); }
|
||||
.field-input:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
|
||||
.field-hint { font-size: 11px; color: var(--text-muted); margin: 0; }
|
||||
@@ -329,7 +331,7 @@ async function onAvatarSelect(key: string | null) {
|
||||
padding: 9px 24px;
|
||||
border-radius: 6px;
|
||||
background: var(--primary);
|
||||
color: #111;
|
||||
color: var(--bg-card);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
border: none;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { usePlayerProfile } from '../../composables/usePlayerProfile';
|
||||
import { useInboxFeature } from '../../composables/useInboxFeature';
|
||||
import { formatMoney } from '../../utils/localeDisplay';
|
||||
import { parseCashbackApiData, sumCashbackAmount } from '../../utils/cashback';
|
||||
import walletBg from '../../assets/images/wallet-bg.webp';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
@@ -91,9 +90,6 @@ const balanceDisplay = computed(() =>
|
||||
<template v-else>
|
||||
<!-- Wallet Banner -->
|
||||
<div class="wallet-banner">
|
||||
<img class="wallet-banner-img" :src="walletBg" alt="" />
|
||||
<div class="wallet-banner-scrim" aria-hidden="true" />
|
||||
|
||||
<div class="card-overlay">
|
||||
<div class="bank-card-top">
|
||||
<div class="bank-card-top-left">
|
||||
@@ -199,46 +195,16 @@ const balanceDisplay = computed(() =>
|
||||
aspect-ratio: 21 / 9;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.wallet-banner::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.22);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wallet-banner-img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
transform: scale(1.05);
|
||||
filter: brightness(0.86);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wallet-banner-scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.18) 42%, rgba(0,0,0,0.46) 100%);
|
||||
box-shadow: 0 4px 16px rgba(0, 61, 107, 0.18);
|
||||
background: linear-gradient(135deg, #003D6B 0%, #005B9F 52%, #002847 100%);
|
||||
}
|
||||
|
||||
.card-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@@ -276,7 +242,7 @@ const balanceDisplay = computed(() =>
|
||||
.bank-card-type {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: rgba(212, 175, 55, 0.8);
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -286,17 +252,16 @@ const balanceDisplay = computed(() =>
|
||||
gap: 4px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 14px;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border: 1px solid rgba(212, 175, 55, 0.35);
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
backdrop-filter: blur(6px);
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.recharge-btn:hover { background: rgba(212, 175, 55, 0.15); }
|
||||
.recharge-btn:hover { background: rgba(255, 255, 255, 0.2); }
|
||||
|
||||
.bank-card-balance {
|
||||
flex: 1;
|
||||
@@ -310,16 +275,15 @@ const balanceDisplay = computed(() =>
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.bank-card-number {
|
||||
margin: 0;
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
color: #fff;
|
||||
font-size: 38px;
|
||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.bank-card-footer {
|
||||
@@ -355,7 +319,7 @@ const balanceDisplay = computed(() =>
|
||||
.info-card, .lang-card {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -376,7 +340,7 @@ const balanceDisplay = computed(() =>
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.info-label { width: 80px; flex-shrink: 0; font-size: 12px; color: var(--text-muted); font-weight: 600; }
|
||||
@@ -388,7 +352,7 @@ const balanceDisplay = computed(() =>
|
||||
align-items: center;
|
||||
padding: 7px 18px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border: 1px solid var(--border-active);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
@@ -396,7 +360,7 @@ const balanceDisplay = computed(() =>
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.edit-btn:hover { background: rgba(212,175,55,0.08); }
|
||||
.edit-btn:hover { background: rgba(0, 102, 204,0.08); }
|
||||
|
||||
.lang-group { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
@@ -416,9 +380,9 @@ const balanceDisplay = computed(() =>
|
||||
}
|
||||
|
||||
.lang-opt.active {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
color: var(--primary-light);
|
||||
background: rgba(212,175,55,0.08);
|
||||
background: rgba(0, 102, 204,0.08);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
|
||||
@@ -1,30 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onActivated } from 'vue';
|
||||
import { ref, onMounted, onActivated, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import api from '../../api';
|
||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||
import Pagination from '../../components/desktop/Pagination.vue';
|
||||
import DesktopWalletSubNav from '../../components/desktop/DesktopWalletSubNav.vue';
|
||||
import DesktopWalletPageHeader from '../../components/desktop/DesktopWalletPageHeader.vue';
|
||||
import DesktopRechargeDetailModal from '../../components/desktop/DesktopRechargeDetailModal.vue';
|
||||
import { formatMoney } from '../../utils/localeDisplay';
|
||||
import { enrichDepositOrder, findDepositOrderById, type PlayerDepositOrder } from '../../utils/depositOrderLookup';
|
||||
|
||||
const COL_COUNT = 5;
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
type RechargeOrder = {
|
||||
id: string;
|
||||
orderNo: string;
|
||||
amount: string;
|
||||
status: string;
|
||||
methodType?: string;
|
||||
bankName?: string | null;
|
||||
paymentMethodName?: string | null;
|
||||
approvedAmount?: string | null;
|
||||
createdAt: string;
|
||||
};
|
||||
const detailModalVisible = ref(false);
|
||||
const detailOrder = ref<PlayerDepositOrder | null>(null);
|
||||
|
||||
const items = ref<RechargeOrder[]>([]);
|
||||
async function openDetail(order: PlayerDepositOrder) {
|
||||
detailOrder.value = await enrichDepositOrder(order);
|
||||
detailModalVisible.value = true;
|
||||
}
|
||||
|
||||
async function openDetailFromQuery() {
|
||||
const orderId = route.query.orderId;
|
||||
if (typeof orderId !== 'string' || !orderId) return;
|
||||
|
||||
const existing = items.value.find((order) => order.id === orderId);
|
||||
const order = existing ?? await findDepositOrderById(orderId);
|
||||
if (order) openDetail(order);
|
||||
|
||||
const { orderId: _removed, ...rest } = route.query;
|
||||
void router.replace({ path: route.path, query: rest });
|
||||
}
|
||||
|
||||
watch(() => route.query.orderId, () => {
|
||||
void openDetailFromQuery();
|
||||
});
|
||||
|
||||
watch(detailModalVisible, (visible) => {
|
||||
if (!visible) detailOrder.value = null;
|
||||
});
|
||||
|
||||
const items = ref<PlayerDepositOrder[]>([]);
|
||||
const loading = ref(true);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
@@ -46,10 +66,16 @@ async function loadPage(p: number) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadPage(1));
|
||||
onActivated(() => loadPage(page.value));
|
||||
onMounted(async () => {
|
||||
await loadPage(1);
|
||||
await openDetailFromQuery();
|
||||
});
|
||||
onActivated(async () => {
|
||||
await loadPage(page.value);
|
||||
await openDetailFromQuery();
|
||||
});
|
||||
|
||||
function methodLabel(order: RechargeOrder) {
|
||||
function methodLabel(order: PlayerDepositOrder) {
|
||||
if (order.methodType === 'USDT') return 'USDT';
|
||||
return order.paymentMethodName || order.bankName || order.methodType || '-';
|
||||
}
|
||||
@@ -73,16 +99,7 @@ function statusLabel(status: string) {
|
||||
|
||||
<template>
|
||||
<div class="desktop-records-page">
|
||||
<header class="desktop-records-header">
|
||||
<div class="desktop-records-header-main">
|
||||
<DesktopWalletSubNav />
|
||||
</div>
|
||||
<div class="desktop-records-actions">
|
||||
<RouterLink to="/wallet/recharge" class="desktop-records-action-btn">
|
||||
{{ t('wallet.recharge_btn') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</header>
|
||||
<DesktopWalletPageHeader />
|
||||
|
||||
<section class="desktop-records-panel">
|
||||
<div class="desktop-records-table-wrap">
|
||||
@@ -105,8 +122,13 @@ function statusLabel(status: string) {
|
||||
</td>
|
||||
</tr>
|
||||
<template v-else-if="items.length">
|
||||
<tr v-for="order in items" :key="order.id" class="hover-row">
|
||||
<td class="order-no">{{ order.orderNo || order.id }}</td>
|
||||
<tr
|
||||
v-for="order in items"
|
||||
:key="order.id"
|
||||
class="hover-row clickable-row"
|
||||
@click="openDetail(order)"
|
||||
>
|
||||
<td class="id-cell">{{ order.orderNo || order.id }}</td>
|
||||
<td>{{ methodLabel(order) }}</td>
|
||||
<td class="num-cell">{{ formatMoney(order.amount, locale) }}</td>
|
||||
<td>
|
||||
@@ -134,12 +156,7 @@ function statusLabel(status: string) {
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<DesktopRechargeDetailModal v-model:visible="detailModalVisible" :order="detailOrder" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.order-no {
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,7 @@ import imageCompression from 'browser-image-compression';
|
||||
import api from '../../api';
|
||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||
import { useDepositNotifications } from '../../composables/useDepositNotifications';
|
||||
import PageBackButton from '../../components/PageBackButton.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
@@ -128,6 +129,7 @@ onActivated(fetchMethods);
|
||||
<main class="account-main">
|
||||
<div class="recharge-layout">
|
||||
<div class="page-header">
|
||||
<PageBackButton fallback="/wallet" />
|
||||
<h1 class="page-title">{{ t('recharge.title') }}</h1>
|
||||
</div>
|
||||
|
||||
@@ -268,6 +270,9 @@ onActivated(fetchMethods);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@@ -319,7 +324,7 @@ onActivated(fetchMethods);
|
||||
padding: 9px 24px;
|
||||
border-radius: 6px;
|
||||
background: var(--primary);
|
||||
color: #111;
|
||||
color: var(--bg-card);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
border: none;
|
||||
@@ -350,9 +355,9 @@ onActivated(fetchMethods);
|
||||
}
|
||||
|
||||
.type-tab.active {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--border-active);
|
||||
color: var(--primary-light);
|
||||
background: rgba(212,175,55,0.08);
|
||||
background: rgba(0, 102, 204,0.08);
|
||||
}
|
||||
|
||||
.recharge-cols {
|
||||
@@ -398,11 +403,11 @@ onActivated(fetchMethods);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.method-card:hover { border-color: rgba(255, 255, 255, 0.15); }
|
||||
.method-card:hover { border-color: rgba(0, 0, 0, 0.08); }
|
||||
|
||||
.method-card.selected {
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(212,175,55,0.06);
|
||||
border-color: var(--border-active);
|
||||
background: rgba(0, 102, 204,0.06);
|
||||
}
|
||||
|
||||
.method-name {
|
||||
@@ -425,7 +430,7 @@ onActivated(fetchMethods);
|
||||
.bank-detail-card {
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-radius: 8px;
|
||||
padding: 16px 18px;
|
||||
display: flex;
|
||||
@@ -457,7 +462,7 @@ onActivated(fetchMethods);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.mono { font-family: 'SF Mono', 'Consolas', monospace; letter-spacing: 0.04em; }
|
||||
.mono { font-family: var(--font-mono); letter-spacing: 0.04em; }
|
||||
.break-all { word-break: break-all; }
|
||||
|
||||
.qr-wrap {
|
||||
@@ -492,7 +497,7 @@ onActivated(fetchMethods);
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--desktop-border);
|
||||
background: #111;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
@@ -501,7 +506,7 @@ onActivated(fetchMethods);
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.field-input:focus { border-color: var(--border-gold-soft); }
|
||||
.field-input:focus { border-color: var(--border-active); }
|
||||
|
||||
.preview-wrap {
|
||||
position: relative;
|
||||
@@ -524,9 +529,9 @@ onActivated(fetchMethods);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #333;
|
||||
background: var(--bg-hover);
|
||||
border: none;
|
||||
color: #fff;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -548,7 +553,7 @@ onActivated(fetchMethods);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.upload-zone:hover { border-color: var(--border-gold-soft); }
|
||||
.upload-zone:hover { border-color: var(--border-active); }
|
||||
|
||||
.hidden-input { display: none; }
|
||||
|
||||
@@ -566,7 +571,7 @@ onActivated(fetchMethods);
|
||||
padding: 11px 32px;
|
||||
border-radius: 6px;
|
||||
background: var(--primary);
|
||||
color: #111;
|
||||
color: var(--bg-card);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
border: none;
|
||||
|
||||
@@ -1,100 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Desktop Register View
|
||||
* Reuses the existing RegisterView form in a centered desktop auth layout.
|
||||
*/
|
||||
import DesktopAuthLayout from '../../components/desktop/DesktopAuthLayout.vue';
|
||||
import RegisterView from '../RegisterView.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-auth-shell">
|
||||
<div class="auth-brand">
|
||||
<div class="brand-inner">
|
||||
<img src="/logo.png" alt="TheBet365" class="brand-logo" />
|
||||
<h1 class="brand-name">THEBET365</h1>
|
||||
<p class="brand-tagline">加入我们,开始精彩体育投注</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-card-wrap">
|
||||
<div class="auth-card">
|
||||
<RegisterView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopAuthLayout wide>
|
||||
<RegisterView desktop />
|
||||
</DesktopAuthLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-auth-shell {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: var(--desktop-bg);
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0a0a0a 0%, #111 50%, #0d0d0d 100%);
|
||||
border-right: 1px solid var(--desktop-border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-brand::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 60% 60% at 50% 40%, rgba(212,175,55,0.06) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 16px rgba(212,175,55,0.3));
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--primary-light);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-card-wrap {
|
||||
width: 520px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
background: var(--desktop-bg);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onActivated } from 'vue';
|
||||
import { ref, onMounted, onActivated, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import api from '../../api';
|
||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||
import Pagination from '../../components/desktop/Pagination.vue';
|
||||
import DesktopWalletPageHeader from '../../components/desktop/DesktopWalletPageHeader.vue';
|
||||
import DesktopTxDetailModal from '../../components/desktop/DesktopTxDetailModal.vue';
|
||||
import { formatMoney } from '../../utils/localeDisplay';
|
||||
import { txTypeKey, txDisplayType, txAmountClass, txSummaryLabel } from '../../utils/walletTx';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function goBack() {
|
||||
if (window.history.state && window.history.state.back) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push('/wallet');
|
||||
const detailModalVisible = ref(false);
|
||||
const detailTxId = ref<string | null>(null);
|
||||
|
||||
function openDetail(txId: string) {
|
||||
detailTxId.value = txId;
|
||||
detailModalVisible.value = true;
|
||||
}
|
||||
|
||||
function openDetailFromQuery() {
|
||||
const transactionId = route.query.transactionId;
|
||||
if (typeof transactionId === 'string' && transactionId) {
|
||||
openDetail(transactionId);
|
||||
const { transactionId: _removed, ...rest } = route.query;
|
||||
void router.replace({ path: route.path, query: rest });
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => route.query.transactionId, openDetailFromQuery);
|
||||
|
||||
type Transaction = {
|
||||
transactionType: string;
|
||||
displayType?: string;
|
||||
summary?: string | null;
|
||||
referenceType?: string | null;
|
||||
amount: string;
|
||||
createdAt: string;
|
||||
transactionId: string;
|
||||
@@ -31,9 +45,10 @@ type Transaction = {
|
||||
const items = ref<Transaction[]>([]);
|
||||
const loading = ref(true);
|
||||
const page = ref(1);
|
||||
const hasMore = ref(true);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
|
||||
function txLabel(tx: Transaction): string {
|
||||
function txLabel(tx: Transaction) {
|
||||
const key = txTypeKey(txDisplayType(tx));
|
||||
if (key) {
|
||||
const translated = t(key);
|
||||
@@ -42,152 +57,107 @@ function txLabel(tx: Transaction): string {
|
||||
return tx.transactionType;
|
||||
}
|
||||
|
||||
async function loadPage(p: number) {
|
||||
function txSubtitle(tx: Transaction): string {
|
||||
return txSummaryLabel(tx, t);
|
||||
}
|
||||
|
||||
function amountClass(amount: string) {
|
||||
return `desktop-records-amount ${txAmountClass(amount)}`;
|
||||
}
|
||||
|
||||
async function fetchData(p = 1) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await api.get('/player/wallet/transactions', { params: { page: p } });
|
||||
const result = data.data ?? { items: [], total: 0 };
|
||||
if (p === 1) {
|
||||
items.value = result.items ?? [];
|
||||
} else {
|
||||
items.value = [...items.value, ...(result.items ?? [])];
|
||||
}
|
||||
hasMore.value = items.value.length < (result.total ?? 0);
|
||||
const { data } = await api.get('/player/wallet/transactions', {
|
||||
params: { page: p, pageSize: pageSize.value },
|
||||
});
|
||||
const result = data.data ?? { items: [], total: 0, pageSize: pageSize.value };
|
||||
total.value = result.total ?? 0;
|
||||
items.value = result.items ?? [];
|
||||
page.value = p;
|
||||
} catch {
|
||||
if (p === 1) items.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadPage(1));
|
||||
onActivated(() => loadPage(1));
|
||||
onMounted(() => {
|
||||
void fetchData(1);
|
||||
openDetailFromQuery();
|
||||
});
|
||||
onActivated(() => {
|
||||
void fetchData(page.value);
|
||||
openDetailFromQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-account-page">
|
||||
<main class="account-main">
|
||||
<div class="records-layout">
|
||||
<div class="page-header">
|
||||
<button type="button" class="back-btn" @click="router.back()">‹ {{ t('common.back') }}</button>
|
||||
<h1 class="page-title">{{ t('wallet.all_records') }}</h1>
|
||||
</div>
|
||||
<div class="desktop-records-page">
|
||||
<DesktopWalletPageHeader />
|
||||
|
||||
<div v-if="loading && items.length === 0" class="loading-state">
|
||||
<GoldSpinner :size="36" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="items.length" class="table-container">
|
||||
<div class="table-wrap">
|
||||
<table class="dt">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('wallet.type') || '类型' }}</th>
|
||||
<th>{{ t('wallet.note') || '备注' }}</th>
|
||||
<th class="num-th">{{ t('wallet.amount') || '金额' }}</th>
|
||||
<th>{{ t('wallet.time') || '时间' }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="tx in items"
|
||||
:key="tx.transactionId"
|
||||
class="hover-row"
|
||||
style="cursor:pointer"
|
||||
@click="router.push(`/wallet/transactions/${tx.transactionId}`)"
|
||||
>
|
||||
<td class="type-cell">{{ txLabel(tx) }}</td>
|
||||
<td class="note-cell">{{ txSummaryLabel(tx, t) || '-' }}</td>
|
||||
<td :class="['num-cell', txAmountClass(tx.amount)]">{{ formatMoney(tx.amount, locale) }}</td>
|
||||
<td class="date-cell">{{ new Date(tx.createdAt).toLocaleString() }}</td>
|
||||
<td class="arrow-cell">›</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="hasMore" class="load-more">
|
||||
<button type="button" class="load-more-btn" :disabled="loading" @click="loadPage(page + 1)">
|
||||
<GoldSpinner v-if="loading" :size="18" />
|
||||
<span v-else>{{ t('common.load_more') || '加载更多' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else class="end-hint">{{ t('common.no_more') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">{{ t('wallet.no_records') }}</div>
|
||||
</template>
|
||||
<section v-if="loading && !items.length" class="desktop-records-panel">
|
||||
<div class="desktop-records-loading">
|
||||
<GoldSpinner :size="36" />
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<section v-else-if="items.length" class="desktop-records-panel">
|
||||
<div class="desktop-records-table-wrap">
|
||||
<table class="desktop-records-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('wallet.type') }}</th>
|
||||
<th>{{ t('wallet.note') }}</th>
|
||||
<th class="num-th">{{ t('wallet.amount') }}</th>
|
||||
<th>{{ t('wallet.time') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="tx in items"
|
||||
:key="tx.transactionId"
|
||||
class="hover-row clickable-row"
|
||||
@click="openDetail(tx.transactionId)"
|
||||
>
|
||||
<td class="type-cell">{{ txLabel(tx) }}</td>
|
||||
<td class="muted-cell">{{ txSubtitle(tx) || '-' }}</td>
|
||||
<td :class="['num-cell', amountClass(tx.amount)]">{{ formatMoney(tx.amount, locale) }}</td>
|
||||
<td class="date-cell">{{ new Date(tx.createdAt).toLocaleString() }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="desktop-records-pagination">
|
||||
<Pagination
|
||||
v-if="total > 0"
|
||||
v-model="page"
|
||||
v-model:pageSize="pageSize"
|
||||
:total="total"
|
||||
@change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else class="desktop-records-panel">
|
||||
<div class="desktop-records-empty">{{ t('wallet.no_records') }}</div>
|
||||
</section>
|
||||
|
||||
<DesktopTxDetailModal v-model:visible="detailModalVisible" :transaction-id="detailTxId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-account-page { display: flex; flex-direction: column; min-height: 0; flex: 1; width: 100%; }
|
||||
|
||||
.account-main { flex: 1; min-width: 0; padding: 24px 28px; display: flex; flex-direction: column; overflow: hidden; }
|
||||
|
||||
.records-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.page-header { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
|
||||
.back-btn { background: none; border: none; color: var(--text-muted); font-size: 14px; font-weight: 700; cursor: pointer; padding: 0; }
|
||||
.back-btn:hover { color: var(--text); }
|
||||
.page-title { font-size: 18px; font-weight: 800; color: var(--primary-light); margin: 0; }
|
||||
.loading-state { display: flex; justify-content: center; align-items: center; padding: 80px 0; }
|
||||
.table-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.table-wrap { flex: 1; overflow-x: auto; overflow-y: auto; }
|
||||
.dt { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.dt th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
padding: 9px 12px;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
.type-cell {
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
background: #141414;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
border-bottom: 1px solid var(--desktop-border);
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.muted-cell {
|
||||
color: var(--text-muted);
|
||||
max-width: 420px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.dt th:first-child { border-top-left-radius: 8px; }
|
||||
.dt th:last-child { border-top-right-radius: 8px; }
|
||||
.num-th { text-align: right; }
|
||||
.dt td { padding: 11px 12px; border-bottom: 1px solid rgba(255,255,255,0.04); color: var(--text); vertical-align: middle; }
|
||||
.hover-row { transition: background 0.15s; }
|
||||
.hover-row:hover { background: rgba(255,255,255,0.03); }
|
||||
.type-cell { font-weight: 700; }
|
||||
.note-cell { color: var(--text-muted); font-size: 12px; max-width: 220px; }
|
||||
.num-cell { text-align: right; font-variant-numeric: tabular-nums; font-weight: 700; }
|
||||
.date-cell { white-space: nowrap; color: var(--text-muted); font-size: 12px; }
|
||||
.arrow-cell { color: #555; font-size: 18px; font-weight: 300; text-align: right; width: 24px; }
|
||||
.pos { color: var(--primary-light); }
|
||||
.neg { color: var(--danger); }
|
||||
.zero { color: var(--text-muted); }
|
||||
.load-more { display: flex; justify-content: center; padding: 20px 0 8px; }
|
||||
.load-more-btn { display: inline-flex; align-items: center; gap: 8px; padding: 8px 28px; border-radius: 6px; border: 1px solid var(--desktop-border); background: transparent; color: var(--text-muted); font-size: 13px; font-weight: 700; cursor: pointer; transition: all 0.2s; }
|
||||
.load-more-btn:hover:not(:disabled) { border-color: var(--border-gold-soft); color: var(--primary-light); }
|
||||
.load-more-btn:disabled { opacity: 0.5; cursor: default; }
|
||||
.end-hint { text-align: center; font-size: 12px; color: #444; padding: 16px 0 4px; }
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 80px 20px; color: var(--text-muted); font-size: 14px; font-weight: 600; }
|
||||
</style>
|
||||
|
||||
@@ -165,7 +165,7 @@ function txLabel(tx: TxDetail): string {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
font-family: var(--font-mono);
|
||||
line-height: 1.1;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
@@ -188,7 +188,7 @@ function txLabel(tx: TxDetail): string {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.detail-row:last-child { border-bottom: none; }
|
||||
@@ -197,5 +197,5 @@ function txLabel(tx: TxDetail): string {
|
||||
.row-val { flex: 1; font-size: 13px; font-weight: 700; color: var(--text); word-break: break-all; }
|
||||
.row-val.pos { color: var(--primary-light); }
|
||||
.row-val.neg { color: var(--danger); }
|
||||
.mono { font-family: 'SF Mono', 'Consolas', monospace; font-size: 12px; letter-spacing: 0.03em; }
|
||||
.mono { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.03em; }
|
||||
</style>
|
||||
|
||||
@@ -1,155 +1,428 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onActivated } from 'vue';
|
||||
import { ref, onMounted, onActivated, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter, RouterLink } from 'vue-router';
|
||||
import api from '../../api';
|
||||
import GoldSpinner from '../../components/GoldSpinner.vue';
|
||||
import Pagination from '../../components/desktop/Pagination.vue';
|
||||
import DesktopWalletSubNav from '../../components/desktop/DesktopWalletSubNav.vue';
|
||||
import DesktopWalletPageHeader from '../../components/desktop/DesktopWalletPageHeader.vue';
|
||||
import LocaleFlag from '../../components/LocaleFlag.vue';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
import { useAppLocale } from '../../composables/useAppLocale';
|
||||
import { usePlayerProfile } from '../../composables/usePlayerProfile';
|
||||
import { formatMoney } from '../../utils/localeDisplay';
|
||||
import { txTypeKey, txDisplayType, txAmountClass, txSummaryLabel } from '../../utils/walletTx';
|
||||
import { parseCashbackApiData, sumCashbackAmount } from '../../utils/cashback';
|
||||
import api from '../../api';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const { locales, setLocale, initFromUser } = useAppLocale();
|
||||
const { profileRaw, refreshProfile } = usePlayerProfile();
|
||||
|
||||
type Transaction = {
|
||||
transactionType: string;
|
||||
displayType?: string;
|
||||
summary?: string | null;
|
||||
summaryKind?: 'opening_bonus' | null;
|
||||
referenceType?: string | null;
|
||||
amount: string;
|
||||
frozenBefore?: string;
|
||||
frozenAfter?: string;
|
||||
createdAt: string;
|
||||
transactionId: string;
|
||||
};
|
||||
|
||||
const items = ref<Transaction[]>([]);
|
||||
const cashbackTotal = ref('0');
|
||||
const loading = ref(true);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const total = ref(0);
|
||||
|
||||
function txLabel(tx: Transaction): string {
|
||||
const key = txTypeKey(txDisplayType(tx));
|
||||
if (key) {
|
||||
const translated = t(key);
|
||||
if (translated !== key) return translated;
|
||||
async function fetchCashbackTotal() {
|
||||
try {
|
||||
const { data } = await api.get('/player/cashbacks');
|
||||
cashbackTotal.value = sumCashbackAmount(parseCashbackApiData(data.data)).toString();
|
||||
} catch {
|
||||
try {
|
||||
const { data } = await api.get('/player/wallet/transactions/stats');
|
||||
const byType = data.data?.byType ?? [];
|
||||
let sum = 0;
|
||||
for (const g of byType) {
|
||||
if (['CASHBACK', 'CASHBACK_DEPOSIT'].includes(g.transactionType?.toUpperCase())) {
|
||||
sum += Math.abs(parseFloat(g.totalAmount ?? '0'));
|
||||
}
|
||||
}
|
||||
cashbackTotal.value = sum.toString();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return tx.transactionType;
|
||||
}
|
||||
|
||||
function txSubtitle(tx: Transaction): string {
|
||||
return txSummaryLabel(tx, t);
|
||||
}
|
||||
|
||||
function amountClass(amount: string) {
|
||||
return `desktop-records-amount ${txAmountClass(amount)}`;
|
||||
}
|
||||
|
||||
async function fetchData(p = 1) {
|
||||
async function loadPage() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [txRes, cbRes] = await Promise.all([
|
||||
api.get('/player/wallet/transactions', { params: { page: p, pageSize: pageSize.value } }),
|
||||
p === 1 ? api.get('/player/cashbacks').catch(() => ({ data: { data: [] } })) : Promise.resolve(null),
|
||||
]);
|
||||
const result = txRes.data.data ?? { items: [], total: 0, pageSize: pageSize.value };
|
||||
total.value = result.total ?? 0;
|
||||
if (p === 1) {
|
||||
items.value = result.items ?? [];
|
||||
const cbRows = parseCashbackApiData(cbRes?.data?.data);
|
||||
cashbackTotal.value = sumCashbackAmount(cbRows).toString();
|
||||
} else {
|
||||
items.value = result.items ?? [];
|
||||
}
|
||||
page.value = p;
|
||||
} catch {
|
||||
/* ignore */
|
||||
await refreshProfile();
|
||||
initFromUser(profileRaw.value?.locale);
|
||||
await fetchCashbackTotal();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData(1));
|
||||
onActivated(() => fetchData(1));
|
||||
function logout() {
|
||||
auth.logout();
|
||||
router.push('/');
|
||||
}
|
||||
|
||||
const balanceDisplay = computed(() =>
|
||||
formatMoney(profileRaw.value?.wallet?.availableBalance, locale.value),
|
||||
);
|
||||
|
||||
onMounted(loadPage);
|
||||
onActivated(loadPage);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-records-page">
|
||||
<header class="desktop-records-header">
|
||||
<div class="desktop-records-header-main">
|
||||
<DesktopWalletSubNav />
|
||||
</div>
|
||||
<div class="desktop-records-actions">
|
||||
<RouterLink to="/wallet/recharge" class="desktop-records-action-btn">
|
||||
{{ t('wallet.recharge_btn') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</header>
|
||||
<div class="desktop-records-page wallet-overview-page">
|
||||
<DesktopWalletPageHeader />
|
||||
|
||||
<section v-if="loading && !items.length" class="desktop-records-panel">
|
||||
<div class="desktop-records-loading">
|
||||
<GoldSpinner :size="36" />
|
||||
</div>
|
||||
</section>
|
||||
<div v-if="loading" class="wallet-loading">
|
||||
<GoldSpinner :size="36" />
|
||||
</div>
|
||||
|
||||
<section v-else-if="items.length" class="desktop-records-panel">
|
||||
<div class="desktop-records-table-wrap">
|
||||
<table class="desktop-records-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ t('wallet.type') || '类型' }}</th>
|
||||
<th>{{ t('wallet.note') || '备注' }}</th>
|
||||
<th class="num-th">{{ t('wallet.amount') || '金额' }}</th>
|
||||
<th>{{ t('wallet.time') || '时间' }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="tx in items"
|
||||
:key="tx.transactionId"
|
||||
class="hover-row"
|
||||
style="cursor: pointer"
|
||||
@click="router.push(`/wallet/transactions/${tx.transactionId}`)"
|
||||
>
|
||||
<td class="type-cell">{{ txLabel(tx) }}</td>
|
||||
<td class="muted-cell">{{ txSubtitle(tx) || '-' }}</td>
|
||||
<td :class="['num-cell', amountClass(tx.amount)]">{{ formatMoney(tx.amount, locale) }}</td>
|
||||
<td class="date-cell">{{ new Date(tx.createdAt).toLocaleString() }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="desktop-records-pagination">
|
||||
<Pagination
|
||||
v-if="total > 0"
|
||||
v-model="page"
|
||||
v-model:pageSize="pageSize"
|
||||
:total="total"
|
||||
@change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<template v-else>
|
||||
<div class="wallet-overview-content">
|
||||
<section class="wallet-hero">
|
||||
<div class="wallet-hero-top">
|
||||
<div class="wallet-hero-brand">
|
||||
<img src="/logo.png" alt="" class="wallet-hero-logo" />
|
||||
<span class="wallet-hero-brand-text">THEBET365</span>
|
||||
</div>
|
||||
<RouterLink to="/wallet/recharge" class="wallet-hero-recharge">
|
||||
+ {{ t('recharge.title') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="wallet-hero-body">
|
||||
<span class="wallet-hero-label">{{ t('wallet.my_balance') }}</span>
|
||||
<p class="wallet-hero-balance">{{ balanceDisplay }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else class="desktop-records-panel">
|
||||
<div class="desktop-records-empty">
|
||||
<span>{{ t('wallet.no_records') }}</span>
|
||||
<div class="wallet-stat-grid">
|
||||
<div class="wallet-stat-card">
|
||||
<span class="wallet-stat-label">{{ t('profile.username') }}</span>
|
||||
<span class="wallet-stat-value">{{ profileRaw?.username || '-' }}</span>
|
||||
</div>
|
||||
<div class="wallet-stat-card">
|
||||
<span class="wallet-stat-label">{{ t('wallet.cashback_total') }}</span>
|
||||
<span class="wallet-stat-value">{{ formatMoney(cashbackTotal, locale) }}</span>
|
||||
</div>
|
||||
<div class="wallet-stat-card">
|
||||
<span class="wallet-stat-label">{{ t('wallet.frozen') }}</span>
|
||||
<span class="wallet-stat-value">{{ formatMoney(profileRaw?.wallet?.frozenBalance, locale) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="wallet-panel">
|
||||
<div class="wallet-panel-row">
|
||||
<div class="wallet-panel-label">{{ t('profile.account') }}</div>
|
||||
<div class="wallet-panel-content">
|
||||
<div class="wallet-info-line">
|
||||
<span class="wallet-info-key">{{ t('profile.username') }}</span>
|
||||
<span class="wallet-info-val">{{ profileRaw?.username }}</span>
|
||||
</div>
|
||||
<div class="wallet-info-line">
|
||||
<span class="wallet-info-key">{{ t('profile.phone') }}</span>
|
||||
<span class="wallet-info-val">{{ profileRaw?.preferences?.phone || '-' }}</span>
|
||||
</div>
|
||||
<RouterLink to="/profile/edit" class="wallet-panel-link">
|
||||
{{ t('profile.edit') }}
|
||||
<span aria-hidden="true">›</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wallet-panel-row">
|
||||
<div class="wallet-panel-label">{{ t('profile.language') }}</div>
|
||||
<div class="wallet-panel-content">
|
||||
<div class="wallet-lang-group" role="group" :aria-label="t('profile.language')">
|
||||
<button
|
||||
v-for="item in locales"
|
||||
:key="item.code"
|
||||
type="button"
|
||||
class="wallet-lang-btn"
|
||||
:class="{ active: locale === item.code }"
|
||||
@click="setLocale(item.code)"
|
||||
>
|
||||
<LocaleFlag :locale="item.code" :size="14" />
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wallet-panel-row wallet-panel-row--actions">
|
||||
<button type="button" class="wallet-logout-btn" @click="logout">
|
||||
{{ t('auth.logout') }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.type-cell {
|
||||
font-weight: 700;
|
||||
.wallet-overview-page {
|
||||
gap: 14px;
|
||||
flex: 0 1 auto !important;
|
||||
height: auto !important;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.muted-cell {
|
||||
.wallet-overview-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wallet-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 48px 0;
|
||||
}
|
||||
|
||||
.wallet-hero {
|
||||
padding: var(--space-8) var(--space-9);
|
||||
border-radius: var(--radius-xl);
|
||||
background: linear-gradient(135deg, #003D6B 0%, #005B9F 52%, #002847 100%);
|
||||
box-shadow: 0 4px 20px rgba(0, 61, 107, 0.2), inset 0 0 24px rgba(0, 102, 204, 0.08);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.wallet-hero-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.wallet-hero-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wallet-hero-logo {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.wallet-hero-brand-text {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
letter-spacing: 0.14em;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.wallet-hero-recharge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.32);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wallet-hero-recharge:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.wallet-hero-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.wallet-hero-balance {
|
||||
margin: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: clamp(30px, 3.2vw, 42px);
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.wallet-stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wallet-stat-card {
|
||||
padding: var(--space-5) var(--space-6);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
box-shadow: var(--shadow-xs);
|
||||
transition: box-shadow var(--transition), transform var(--transition);
|
||||
}
|
||||
|
||||
.wallet-stat-card:hover {
|
||||
box-shadow: var(--shadow-sm), var(--glow-gold);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.wallet-stat-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text-muted);
|
||||
max-width: 420px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.wallet-stat-value {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-data);
|
||||
color: var(--primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.wallet-panel {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-card);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
|
||||
.wallet-panel-row {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
padding: 16px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.wallet-panel-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.wallet-panel-row--actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
grid-template-columns: none;
|
||||
}
|
||||
|
||||
.wallet-panel-label {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-family: var(--font-heading);
|
||||
color: var(--text-muted);
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.wallet-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wallet-info-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.wallet-info-key {
|
||||
width: 64px;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wallet-info-val {
|
||||
flex: 1;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.wallet-panel-link {
|
||||
align-self: flex-start;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-active);
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.wallet-panel-link:hover {
|
||||
background: rgba(0, 102, 204, 0.06);
|
||||
}
|
||||
|
||||
.wallet-lang-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wallet-lang-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wallet-lang-btn.active {
|
||||
border-color: var(--border-active);
|
||||
color: var(--primary-light);
|
||||
background: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.wallet-logout-btn {
|
||||
padding: 8px 18px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(220, 38, 38, 0.35);
|
||||
background: transparent;
|
||||
color: var(--danger);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wallet-logout-btn:hover {
|
||||
background: rgba(220, 38, 38, 0.06);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.wallet-stat-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wallet-panel-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
156
docs/玩家端缺失功能分析.md
Normal file
156
docs/玩家端缺失功能分析.md
Normal file
@@ -0,0 +1,156 @@
|
||||
## 玩家端缺失功能分析
|
||||
|
||||
> 基于 PRD v1.2 要求 + 行业通用标准,对比当前代码实际实现情况
|
||||
> 分析日期:2026-06-17
|
||||
|
||||
---
|
||||
|
||||
### 一、PRD 明确要求但尚未完整实现的功能
|
||||
|
||||
#### 1. 首页「今日赛事」板块
|
||||
|
||||
PRD 4.4 节明确要求首页同时展示「热门赛事」和「今日赛事」两个板块。当前 HomeView.vue 只渲染了 `hotMatches`,缺少按当天开赛时间筛选的「今日赛事」列表。后端 `/player/home` 接口实际已返回 `todayMatches` 数据,前端未消费。
|
||||
|
||||
#### 2. 公告详情页
|
||||
|
||||
PRD 17.2 节将公告定义为"可点击查看详情的完整内容",与走马灯(短文案滚动)是两种不同的内容形态。当前只有 AnnouncementMarquee 走马灯组件,点击后没有跳转到公告详情页查看完整正文。
|
||||
|
||||
#### 3. 投注规则独立页面
|
||||
|
||||
PRD 3.3 节要求"投注规则说明必须三语,减少争议"。当前规则内容硬编码在 ProfileView.vue 的 5 段 i18n 翻译文本中(rules_p1 ~ rules_p5),不可由后台动态维护,也没有独立的规则页面。建议做成后台可编辑、前台可独立访问的规则中心页面。
|
||||
|
||||
---
|
||||
|
||||
### 二、业务闭环缺失的关键功能
|
||||
|
||||
#### 4. 提款 / 出金
|
||||
|
||||
当前已实现充值(Recharge)全流程——银行转账、USDT、截图上传、审核、入账。但完全没有提款/出金功能。玩家赢了钱却无法主动申请提款,这在业务闭环上是最大的缺口。至少需要:提款申请页面、提款方式选择、金额输入、提款记录与状态追踪。
|
||||
|
||||
#### 5. 注单结算/派彩通知
|
||||
|
||||
玩家下注后,赛事结算完成时没有任何主动通知机制。玩家只能反复刷"我的投注"才知道结果。需要:注单结算后推送通知(站内消息或 WebSocket 实时推送),至少在下注成功页和注单列表中有明确的"等待开奖"与"已开奖"状态区分。
|
||||
|
||||
#### 6. 充值/提款状态通知
|
||||
|
||||
充值订单审核通过或被拒绝后,玩家没有收到即时通知。目前只能在充值历史页面手动查看状态变化。
|
||||
|
||||
---
|
||||
|
||||
### 三、体验层面的缺失功能
|
||||
|
||||
#### 7. 赛事 / 球队搜索
|
||||
|
||||
当前赛事浏览路径只有:联赛筛选 → 时间筛选 → 列表滚动。当赛事数量多时,玩家无法通过关键词搜索球队名或联赛名快速定位比赛。需要在赛事列表页或首页增加搜索框。
|
||||
|
||||
#### 8. 投注单清空确认
|
||||
|
||||
BetSlipDrawer 中清空投注单没有二次确认弹窗,容易误操作导致已选择的投注项丢失。
|
||||
|
||||
#### 9. 投注单赔率实时更新提示
|
||||
|
||||
PRD 7.3 节要求"赔率已变化,拒绝下注,提示重新确认"。当前提交时校验赔率版本会报错,但在投注单停留期间没有实时检测赔率变化并主动标记(如高亮变化项、显示新旧赔率对比),玩家只有在点提交时才知道赔率变了。
|
||||
|
||||
#### 10. 盘口封盘倒计时
|
||||
|
||||
赛事列表和详情页没有显示距离封盘的倒计时。玩家不知道赛事何时关闭投注,影响下注决策。
|
||||
|
||||
#### 11. 快捷金额按钮自定义
|
||||
|
||||
PRD 4.3 节桌面端布局提到"快捷投注金额"。当前 BetSlipDrawer 中有固定的快捷金额按钮(Min/5、+50、+100、Max),但不能由后台配置或让玩家自定义。
|
||||
|
||||
#### 12. 我的投注 - 按赛事/时间筛选
|
||||
|
||||
MyBetsView 目前支持按状态筛选(全部/赢/输/待结算/走水),缺少按赛事、按时间范围筛选的能力,注单多了之后查找困难。
|
||||
|
||||
#### 13. 注单详情页 - 缺少分享/截图功能
|
||||
|
||||
玩家无法分享或保存注单截图。在社交平台传播场景中,注单截图是常见的拉新素材。
|
||||
|
||||
---
|
||||
|
||||
### 四、安全与合规相关缺失
|
||||
|
||||
#### 14. 负责任博彩 / 自我限额
|
||||
|
||||
PRD 18.1 节提到玩家每日投注上限和派彩上限由后台配置,但前台没有向玩家展示当前限额使用情况(如"今日已投注 X / 上限 Y"),也没有自我设置存款限额、投注限额、冷静期等负责任博彩工具。对于面向合法市场的平台,这通常是合规要求。
|
||||
|
||||
#### 15. 登录安全 - 缺少二次验证
|
||||
|
||||
玩家端登录只有密码 + 人机验证(RobotVerify),没有可选的短信验证码二次验证或 TOTP,安全性偏弱。
|
||||
|
||||
#### 16. 会话管理
|
||||
|
||||
玩家无法查看当前活跃会话(哪些设备在登录),也无法远程踢出其他设备的会话。
|
||||
|
||||
---
|
||||
|
||||
### 五、运营与增长相关缺失
|
||||
|
||||
#### 17. 邀请好友入口
|
||||
|
||||
后端已有完整的邀请码系统(生成邀请码、设置返水比例、邀请列表、撤销),但玩家前台没有"邀请好友"入口页面,无法查看自己的邀请码、邀请链接、邀请记录和奖励。
|
||||
|
||||
#### 18. 活动 / 优惠专区
|
||||
|
||||
没有活动页面或优惠专区。运营无法通过前台展示限时活动、新人奖励、充值优惠等信息。目前只能依赖 Banner 和公告,缺乏独立的活动落地页。
|
||||
|
||||
#### 19. VIP / 等级体系
|
||||
|
||||
没有玩家等级体系。成熟平台通常根据投注量、充值额等划分 VIP 等级,不同等级享受不同返水比例、专属客服、提款加速等权益。
|
||||
|
||||
---
|
||||
|
||||
### 六、数据辅助功能缺失
|
||||
|
||||
#### 20. 赛事数据 / 历史战绩
|
||||
|
||||
赛事详情页只展示盘口和赔率,没有历史交锋记录、球队近期战绩、联赛积分榜等辅助数据。这些数据可以帮助玩家做投注决策,也是提升用户粘性的手段。
|
||||
|
||||
#### 21. 投注统计 / 个人报表
|
||||
|
||||
虽然有 BetStatsPanel 显示注单数/赢/输/待结算的基础统计,但缺少更丰富的个人投注报表:按时间段统计、按玩法统计、盈亏趋势图、返水收入统计等。
|
||||
|
||||
---
|
||||
|
||||
### 七、其他体验优化建议
|
||||
|
||||
| 编号 | 功能 | 说明 |
|
||||
|---|---|---|
|
||||
| 22 | 赛事收藏 / 关注 | 玩家可以收藏感兴趣的赛事,快速筛选查看 |
|
||||
| 23 | 多赔率格式切换 | PRD 二期提到支持 Decimal/Malay/HK/Indo/American 格式,当前只有十进制 |
|
||||
| 24 | PWA / 添加到主屏幕 | 移动端 H5 支持 PWA 安装提示,提升留存 |
|
||||
| 25 | 网络状态提示 | 弱网或断网时显示明确提示,避免下注失败无感知 |
|
||||
| 26 | 暗色/亮色主题切换 | 当前只有深色主题,没有亮色模式 |
|
||||
| 27 | 系统公告弹窗 | 维护、重大赛事等场景的全屏弹窗通知,目前只有走马灯 |
|
||||
| 28 | 盘口排序偏好 | 允许玩家调整盘口分组默认排序(如把让球放在独赢前面) |
|
||||
|
||||
---
|
||||
|
||||
### 优先级建议
|
||||
|
||||
**P0 - 必须补齐(影响业务闭环):**
|
||||
- 提款功能(#4)
|
||||
- 邀请好友入口(#17)— 后端已就绪,只差前端页面
|
||||
- 注单结算通知(#5)
|
||||
|
||||
**P1 - 高优先级(影响核心体验):**
|
||||
- 今日赛事板块(#1)— 后端数据已有,前端未消费
|
||||
- 赛事搜索(#7)
|
||||
- 投注规则独立页面(#3)
|
||||
- 公告详情页(#2)
|
||||
- 负责任博彩 / 限额展示(#14)
|
||||
|
||||
**P2 - 中优先级(体验优化):**
|
||||
- 盘口封盘倒计时(#10)
|
||||
- 赔率变化实时标记(#9)
|
||||
- 注单筛选增强(#12)
|
||||
- 投注单清空确认(#8)
|
||||
- 充值/提款状态通知(#6)
|
||||
|
||||
**P3 - 低优先级(长期迭代):**
|
||||
- VIP 等级体系(#19)
|
||||
- 活动专区(#18)
|
||||
- 赛事数据/历史战绩(#20)
|
||||
- 多赔率格式(#23)
|
||||
- PWA(#24)
|
||||
28
scripts/dev-api-target.mjs
Normal file
28
scripts/dev-api-target.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
/** 本地 dev 默认端口;3000 在部分 Windows(Hyper-V 保留 2960–3059)上会 EACCES */
|
||||
const DEFAULT_DEV_API_PORT = '3100';
|
||||
|
||||
/** 与 apps/api/.env 的 PORT 对齐 */
|
||||
export function resolveApiDevPort() {
|
||||
if (process.env.PORT) return String(process.env.PORT);
|
||||
|
||||
let port = DEFAULT_DEV_API_PORT;
|
||||
const envPath = resolve(repoRoot, 'apps/api/.env');
|
||||
if (existsSync(envPath)) {
|
||||
const match = readFileSync(envPath, 'utf8').match(/^PORT=(\d+)\s*$/m);
|
||||
if (match) port = match[1];
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
/** 供 player/admin Vite 代理使用 */
|
||||
export function resolveApiDevTarget() {
|
||||
if (process.env.VITE_DEV_API_TARGET) return process.env.VITE_DEV_API_TARGET;
|
||||
if (process.env.API_DEV_TARGET) return process.env.API_DEV_TARGET;
|
||||
return `http://localhost:${resolveApiDevPort()}`;
|
||||
}
|
||||
Reference in New Issue
Block a user