feat(player): refresh dark gold betting theme

- add generated dark-gold WebP assets for PC/mobile backgrounds, cards, dialogs, tabs, mailbox, VS and bet buttons
- apply the new assets across player home, match detail, outright cards, modals, wallet/profile/detail screens and bet slip controls
- simplify confirm dialogs and reuse bet button resources for normal/pressed states
- hide mobile announcement center, tighten mobile bet list layout, and remove extra dark page backgrounds/scrollbar styling
- cache and deduplicate outright market loading to speed up repeated tab/detail navigation
- document player image-generation constraints in AGENTS.md
This commit is contained in:
mars
2026-07-23 17:34:40 +08:00
parent 375d352df2
commit b02e79785e
63 changed files with 796 additions and 328 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -101,18 +101,33 @@ function onConfirm() {
}
.confirm-modal {
position: relative;
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));
border-radius: 12px;
padding: 22px 18px 16px;
box-shadow: 0 0 24px rgba(212, 175, 55, 0.08);
max-width: 374px;
overflow: hidden;
background:
linear-gradient(180deg, rgba(212, 175, 55, 0.08), transparent 38%),
rgba(14, 14, 12, 0.96);
border: 1px solid rgba(212, 175, 55, 0.34);
border-radius: 8px;
padding: 31px 28px 25px;
box-shadow: none;
}
.confirm-modal::before {
content: '';
position: absolute;
top: 0;
left: 24px;
right: 24px;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(232, 200, 74, 0.65), transparent);
pointer-events: none;
}
.confirm-title {
margin: 0 0 10px;
font-size: 17px;
margin: 0 0 12px;
font-size: 18px;
font-weight: 800;
color: var(--gold, #c8a84e);
text-align: center;
@@ -120,7 +135,7 @@ function onConfirm() {
}
.confirm-message {
margin: 0 0 20px;
margin: 0 0 26px;
font-size: 14px;
line-height: 1.6;
color: #c8c8c8;
@@ -130,17 +145,17 @@ function onConfirm() {
.confirm-actions {
display: flex;
gap: 10px;
gap: 12px;
}
.confirm-btn {
flex: 1;
min-height: 44px;
border-radius: 8px;
min-height: 49px;
border-radius: 0;
font-size: 14px;
font-weight: 700;
cursor: pointer;
transition: opacity 0.15s;
transition: opacity 0.15s, filter 0.15s;
}
.confirm-btn:disabled {
@@ -149,20 +164,30 @@ function onConfirm() {
}
.confirm-btn.cancel {
border: 1px solid #333;
background: transparent;
color: #888;
border: 0;
background: url('../assets/images/generated/pc-betslip-btn-secondary-v1.webp') center / 100% 100% no-repeat;
color: #9f9273;
}
.confirm-btn.confirm {
border: none;
background: linear-gradient(135deg, #d4a017, #e8c84a);
color: #1a1a1a;
background: url('../assets/images/generated/pc-betslip-btn-primary-v1.webp') center / 100% 100% no-repeat;
color: #1b1200;
}
.confirm-btn.confirm.danger {
background: linear-gradient(135deg, #c0392b, #e74c3c);
color: #fff;
.confirm-btn.cancel:not(:disabled):hover,
.confirm-btn.confirm:not(:disabled):hover {
filter: brightness(1.06);
}
.confirm-btn.cancel:not(:disabled):active {
background-image: url('../assets/images/generated/pc-betslip-btn-secondary-pressed-v1.webp');
filter: brightness(0.96);
}
.confirm-btn.confirm:not(:disabled):active {
background-image: url('../assets/images/generated/pc-betslip-btn-primary-pressed-v1.webp');
filter: brightness(0.98);
}
.confirm-fade-enter-active,

View File

@@ -317,47 +317,61 @@ onMounted(() => {
}
.fab-btn {
width: 56px;
height: 56px;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--primary);
color: #fff;
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
background: url('../assets/images/generated/floating-mailbox-fab-v1.webp') center / cover no-repeat;
color: var(--primary-light);
border: 0;
box-shadow:
0 8px 22px rgba(0, 0, 0, 0.42),
0 0 16px rgba(212, 175, 55, 0.12);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.2s, background 0.2s;
transition: transform 0.2s, box-shadow 0.2s, color 0.2s, filter 0.2s;
position: relative;
}
.fab-btn:hover {
transform: scale(1.05);
background: var(--primary-light);
transform: translateY(-1px);
color: #fff1a8;
filter: brightness(1.08);
box-shadow:
0 10px 26px rgba(0, 0, 0, 0.5),
0 0 20px rgba(212, 175, 55, 0.18);
}
.fab-active {
transform: scale(0.95);
transform: scale(0.96);
color: #fff1a8;
filter: brightness(0.94);
}
.fab-icon {
width: 28px;
height: 28px;
width: 24px;
height: 24px;
}
.badge {
position: absolute;
top: 0;
right: 0;
top: -3px;
right: -3px;
background: var(--danger, #ff4d4f);
color: #fff;
font-size: 11px;
font-size: 10px;
font-weight: 700;
padding: 2px 6px;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 10px;
border: 2px solid var(--bg-card);
border: 1px solid rgba(255, 255, 255, 0.9);
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(255, 69, 58, 0.42);
}
.popup-panel {

View File

@@ -2,8 +2,8 @@
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';
import saishiImg from '../assets/images/generated/saishi-gold-trophy-v1.webp';
import cardBg from '../assets/images/generated/pc-match-list-card-bg-v1.webp';
const matchCardBg = `url(${cardBg})`;

View File

@@ -28,7 +28,7 @@ function continueBrowsing() {
<div v-if="auth.loginPromptVisible" class="login-overlay" @click.self="continueBrowsing">
<div class="login-modal">
<button type="button" class="close-btn" :aria-label="t('auth.continue_browsing')" @click="continueBrowsing">
�
</button>
<h2 class="login-title">{{ t('auth.login_required') }}</h2>
<p class="login-hint">{{ t('auth.login_hint') }}</p>
@@ -64,10 +64,13 @@ function continueBrowsing() {
position: relative;
width: 90%;
max-width: 360px;
background: #1a1a1a;
border: 1px solid var(--border-gold-soft, rgba(200, 168, 78, 0.25));
border-radius: 12px;
padding: 28px 24px 24px;
background:
linear-gradient(rgba(16, 14, 10, 0.12), rgba(16, 14, 10, 0.16)),
url('../assets/images/generated/modal-gold-frame-v4.webp') center / 102% 102% no-repeat;
border: 0;
border-radius: 0;
padding: 36px 30px 30px;
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.55), 0 0 24px rgba(212, 175, 55, 0.1);
}
.close-btn {

View File

@@ -9,6 +9,10 @@ import { useBetSlipStore } from '../stores/betSlip';
import { useDesktopBetPopover } from '../composables/useDesktopBetPopover';
import { resolveSelectionLabel } from '../utils/selectionLabel';
import MarketSelectionsPanel from './match-detail/MarketSelectionsPanel.vue';
import vsImg from '../assets/images/generated/vs-gold-alpha-v1.webp';
import matchListCardBg from '../assets/images/generated/pc-match-list-card-bg-v1.webp';
const matchCardBg = `url(${matchListCardBg})`;
const props = defineProps<{
match: {
@@ -229,7 +233,9 @@ function goMatchDetail() {
<div class="center-col">
<span class="kickoff">{{ kickoffText }}</span>
<span v-if="phase !== 'open' && liveScoreText" class="live-score">{{ liveScoreText }}</span>
<span v-else class="vs">VS</span>
<span v-else class="vs">
<img :src="vsImg" alt="VS" class="vs-img" />
</span>
</div>
<div class="team">
@@ -294,9 +300,9 @@ function goMatchDetail() {
<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;
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
border: 0;
border-radius: 0;
padding: 10px 8px 10px;
display: flex;
flex-direction: column;
@@ -312,7 +318,7 @@ function goMatchDetail() {
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(180deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.2));
pointer-events: none;
z-index: 0;
}
@@ -412,13 +418,21 @@ function goMatchDetail() {
}
.vs {
font-size: 22px;
font-weight: 900;
color: #fff;
letter-spacing: 0.08em;
width: 48px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9);
transition: font-size 0.25s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), text-shadow 0.3s ease;
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), filter 0.3s ease;
}
.vs-img {
width: 46px;
max-width: 100%;
height: auto;
object-fit: contain;
filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.9));
}
.status-tag {

View File

@@ -36,7 +36,7 @@ function confirm() {
<Teleport to="body">
<div v-if="open" class="overlay" @click.self="close">
<div class="modal" role="dialog" aria-modal="true" :aria-label="t('profile.avatar')">
<button type="button" class="close-x" :aria-label="t('bet.cancel')" @click="close"></button>
<button type="button" class="close-x" :aria-label="t('bet.cancel')" @click="close">&#10005;</button>
<h3 class="title">{{ t('profile.avatar') }}</h3>
<PlayerAvatarPicker v-model="draft" />
<div class="actions">
@@ -69,11 +69,13 @@ 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);
padding: 16px 14px 14px;
box-shadow: var(--shadow), 0 0 24px rgba(212, 175, 55, 0.08);
background:
linear-gradient(rgba(16, 14, 10, 0.12), rgba(16, 14, 10, 0.16)),
url('../assets/images/generated/modal-gold-frame-v4.webp') center / 102% 102% no-repeat;
border: 0;
border-radius: 0;
padding: 28px 24px 22px;
box-shadow: var(--shadow), 0 0 24px rgba(212, 175, 55, 0.12);
}
.close-x {

View File

@@ -1181,7 +1181,9 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
height: 100%;
background: var(--desktop-sidebar-panel-bg);
background:
linear-gradient(rgba(14, 12, 9, 0.58), rgba(14, 12, 9, 0.68)),
url('../../assets/images/generated/pc-betslip-sidebar-bg-v1.webp') no-repeat center / cover;
}
.panel-head {
@@ -1233,14 +1235,22 @@ onUnmounted(() => {
flex: 0 0 20%;
min-width: 0;
padding: 10px 4px;
border: 0;
border-radius: 0;
background: url('../../assets/images/generated/pc-betslip-btn-secondary-v1.webp') center / 100% 100% no-repeat;
color: var(--primary-light);
font-size: 12px;
cursor: pointer;
}
.clear-slip-btn:hover {
color: var(--primary-light);
border-color: var(--border-gold-soft);
background: rgba(212, 175, 55, 0.06);
color: #fff0a8;
filter: brightness(1.08);
}
.clear-slip-btn.foot-clear:active {
background-image: url('../../assets/images/generated/pc-betslip-btn-secondary-pressed-v1.webp');
filter: brightness(0.96);
}
.balance-info {
@@ -1806,17 +1816,26 @@ onUnmounted(() => {
flex: 1;
min-width: 0;
padding: 10px;
border-radius: 4px;
background: linear-gradient(180deg, #F0D875 0%, #D4AF37 100%);
color: #3D2800;
border-radius: 0;
background: url('../../assets/images/generated/pc-betslip-btn-primary-v1.webp') center / 100% 100% no-repeat;
color: #1b1200;
font-size: 13px;
font-weight: 800;
text-transform: uppercase;
transition: opacity 0.2s;
transition: opacity 0.2s, filter 0.15s;
border: none;
cursor: pointer;
}
.submit-btn:not(:disabled):hover {
filter: brightness(1.06);
}
.submit-btn:not(:disabled):active {
background-image: url('../../assets/images/generated/pc-betslip-btn-primary-pressed-v1.webp');
filter: brightness(0.98);
}
.submit-btn:disabled {
opacity: 0.4;
cursor: not-allowed;

View File

@@ -83,16 +83,16 @@ function statusLabel(status: string) {
}
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' },
'\u4e3b\u80dc': { 'en-US': 'Home Win', 'ms-MY': 'Rumah Menang' },
'\u5ba2\u80dc': { 'en-US': 'Away Win', 'ms-MY': 'Tandang Menang' },
'\u548c\u5c40': { 'en-US': 'Draw', 'ms-MY': 'Seri' },
'\u4e3b': { 'en-US': 'Home', 'ms-MY': 'Rumah' },
'\u5ba2': { 'en-US': 'Away', 'ms-MY': 'Tandang' },
'\u5927': { 'en-US': 'Over', 'ms-MY': 'Atas' },
'\u5c0f': { 'en-US': 'Under', 'ms-MY': 'Bawah' },
'\u5355': { 'en-US': 'Odd', 'ms-MY': 'Ganjil' },
'\u53cc': { 'en-US': 'Even', 'ms-MY': 'Genap' },
'\u51a0\u519b': { 'en-US': 'Winner', 'ms-MY': 'Juara' },
};
function translateSel(name: string): string {
@@ -115,8 +115,8 @@ function translateSel(name: string): string {
<div v-if="visible" class="modal-overlay" @click.self="close">
<div class="modal-container" role="dialog" aria-modal="true">
<div class="modal-header">
<span class="modal-title">{{ t('bet.detail_title') || '注单详情' }}</span>
<button type="button" class="modal-close" @click="close"></button>
<span class="modal-title">{{ t('bet.detail_title') }}</span>
<button type="button" class="modal-close" @click="close">&#10005;</button>
</div>
<div class="modal-body">
@@ -132,36 +132,36 @@ function translateSel(name: string): string {
<template v-else-if="detail">
<div class="info-rows">
<div class="info-row">
<span class="row-label">{{ t('bet.bet_no') || '注单号' }}</span>
<span class="row-label">{{ t('bet.bet_no') }}</span>
<span class="row-val mono">{{ detail.betNo }}</span>
</div>
<div class="info-row">
<span class="row-label">{{ t('common.status') || '状态' }}</span>
<span class="row-label">{{ t('common.status') }}</span>
<span class="row-val">
<span class="st-badge" :class="statusClass(detail.status)">{{ statusLabel(detail.status) }}</span>
</span>
</div>
<div class="info-row">
<span class="row-label">{{ t('bet.stake') || '投注额' }}</span>
<span class="row-label">{{ t('bet.stake') }}</span>
<span class="row-val">{{ detail.stake }}</span>
</div>
<div class="info-row">
<span class="row-label">{{ t('bet.potential_payout') || '预计派彩' }}</span>
<span class="row-label">{{ t('bet.potential_payout') }}</span>
<span class="row-val gold">{{ detail.potentialReturn || '-' }}</span>
</div>
<div class="info-row">
<span class="row-label">{{ t('bet.placed_at') || '下注时间' }}</span>
<span class="row-label">{{ t('bet.placed_at') }}</span>
<span class="row-val muted">{{ detail.placedAt ? new Date(detail.placedAt).toLocaleString() : '-' }}</span>
</div>
</div>
<div class="sel-section">
<div class="sel-section-title">{{ t('bet.selections') || '投注选项' }}</div>
<div class="sel-section-title">{{ t('bet.selections') }}</div>
<div v-for="(leg, idx) in detail.legs" :key="idx" class="sel-item">
<div class="sel-item-main">
<span class="sel-item-match">{{ leg.matchTitle || t('bet.match') }}</span>
<span class="sel-item-pick">
{{ leg.marketLabel || '' }} · {{ translateSel(leg.selectionName || '') }}
{{ leg.marketLabel || '' }} - {{ translateSel(leg.selectionName || '') }}
</span>
<span v-if="leg.score?.ft" class="sel-item-score">{{ t('history.ft') }}: {{ leg.score.ft }}</span>
</div>
@@ -170,7 +170,7 @@ function translateSel(name: string): string {
</div>
</template>
<div v-else class="modal-empty">{{ t('common.not_found') || '未找到记录' }}</div>
<div v-else class="modal-empty">{{ t('common.not_found') }}</div>
</div>
</div>
</div>
@@ -191,15 +191,19 @@ function translateSel(name: string): string {
}
.modal-container {
position: relative;
width: 100%;
max-width: 520px;
max-height: 80vh;
display: flex;
flex-direction: column;
background: var(--desktop-sidebar-bg, rgba(20, 20, 20, 0.95));
border: 1px solid var(--desktop-border, #262626);
border-radius: 4px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
padding: 20px 22px 22px;
background:
linear-gradient(rgba(15, 14, 11, 0.18), rgba(15, 14, 11, 0.2)),
url('../../assets/images/generated/modal-gold-frame-v4.webp') center / 102% 102% no-repeat;
border: 0;
border-radius: 0;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.58), 0 0 28px rgba(212, 175, 55, 0.12);
overflow: hidden;
}
@@ -207,8 +211,8 @@ function translateSel(name: string): string {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--desktop-border, #262626);
padding: 2px 0 12px;
border-bottom: 1px solid rgba(212, 175, 55, 0.16);
flex-shrink: 0;
}
@@ -275,9 +279,7 @@ function translateSel(name: string): string {
}
/* Info rows */
.info-rows {
border-bottom: 1px solid var(--desktop-border, #262626);
}
.info-rows { border-bottom: 1px solid rgba(212, 175, 55, 0.12); }
.info-row {
display: flex;
@@ -324,7 +326,7 @@ function translateSel(name: string): string {
.st-default { background: rgba(100,100,100,0.1); color: #888; }
/* Selections */
.sel-section { padding: 12px 16px 16px; }
.sel-section { padding: 12px 0 2px; }
.sel-section-title {
font-size: 12px;

View File

@@ -47,7 +47,9 @@ watch(
width: var(--desktop-right-betslip-w);
min-width: 0;
border-left: 1px solid var(--border);
background: var(--desktop-sidebar-panel-bg);
background:
linear-gradient(rgba(14, 12, 9, 0.58), rgba(14, 12, 9, 0.68)),
url('../../assets/images/generated/pc-betslip-sidebar-bg-v1.webp') no-repeat center / cover;
display: flex;
flex-direction: column;
min-height: 0;

View File

@@ -2,7 +2,10 @@
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import type { OutrightEvent, OutrightSelection } from '../outright/OutrightEventSection.vue';
import saishiImg from '../../assets/images/saishi.webp';
import saishiImg from '../../assets/images/generated/saishi-gold-trophy-v1.webp';
import outrightListCardBg from '../../assets/images/generated/pc-match-list-card-bg-v1.webp';
const outrightCardBg = `url(${outrightListCardBg})`;
const props = defineProps<{
event: OutrightEvent;
@@ -107,9 +110,9 @@ const topSelections = computed(() => {
position: relative;
width: 100%;
min-height: 140px;
border: 1px solid rgba(140, 140, 140, 0.35);
border-radius: 6px;
background: linear-gradient(180deg, #1a1a1a 0%, #0d0d0d 100%);
border: 0;
border-radius: 0;
background: v-bind(outrightCardBg) center / 100% 100% no-repeat;
overflow: hidden;
display: flex;
flex-direction: column;

View File

@@ -82,8 +82,8 @@ function txLabel(tx: TxDetail): string {
<div v-if="visible" class="modal-overlay" @click.self="close">
<div class="modal-container" role="dialog" aria-modal="true">
<div class="modal-header">
<span class="modal-title">{{ t('wallet.transaction_detail') || '交易详情' }}</span>
<button type="button" class="modal-close" @click="close"></button>
<span class="modal-title">{{ t('wallet.transaction_detail') }}</span>
<button type="button" class="modal-close" @click="close">&#10005;</button>
</div>
<div class="modal-body">
@@ -99,45 +99,45 @@ function txLabel(tx: TxDetail): string {
<template v-else-if="detail">
<div class="info-rows">
<div class="info-row">
<span class="row-label">{{ t('wallet.type') || '类型' }}</span>
<span class="row-label">{{ t('wallet.type') }}</span>
<span class="row-val">{{ txLabel(detail) }}</span>
</div>
<div class="info-row">
<span class="row-label">{{ t('wallet.amount') || '金额' }}</span>
<span class="row-label">{{ t('wallet.amount') }}</span>
<span class="row-val mono" :class="parseFloat(detail.amount) >= 0 ? 'pos' : 'neg'">{{ formatMoney(detail.amount, locale) }}</span>
</div>
<div class="info-row">
<span class="row-label">{{ t('wallet.note') || '备注' }}</span>
<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-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-label">{{ t('wallet.balance_after') }}</span>
<span class="row-val mono">{{ formatMoney(detail.balanceAfter, locale) }}</span>
</div>
<div v-if="parseFloat(detail.frozenBefore || '0') > 0 || parseFloat(detail.frozenAfter || '0') > 0" class="info-row">
<span class="row-label">{{ t('wallet.frozen_before') || '冻结前' }}</span>
<span class="row-label">{{ t('wallet.frozen_before') }}</span>
<span class="row-val mono">{{ formatMoney(detail.frozenBefore, locale) }}</span>
</div>
<div v-if="parseFloat(detail.frozenBefore || '0') > 0 || parseFloat(detail.frozenAfter || '0') > 0" class="info-row">
<span class="row-label">{{ t('wallet.frozen_after') || '冻结后' }}</span>
<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-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-label">{{ t('wallet.transaction_id') }}</span>
<span class="row-val mono muted">{{ detail.transactionId }}</span>
</div>
</div>
</template>
<div v-else class="modal-empty">{{ t('common.not_found') || '未找到记录' }}</div>
<div v-else class="modal-empty">{{ t('common.not_found') }}</div>
</div>
</div>
</div>
@@ -158,15 +158,19 @@ function txLabel(tx: TxDetail): string {
}
.modal-container {
position: relative;
width: 100%;
max-width: 460px;
max-height: 80vh;
display: flex;
flex-direction: column;
background: var(--desktop-sidebar-bg, rgba(20, 20, 20, 0.95));
border: 1px solid var(--desktop-border, #262626);
border-radius: 4px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
padding: 20px 22px 22px;
background:
linear-gradient(rgba(15, 14, 11, 0.18), rgba(15, 14, 11, 0.2)),
url('../../assets/images/generated/modal-gold-frame-v4.webp') center / 102% 102% no-repeat;
border: 0;
border-radius: 0;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.58), 0 0 28px rgba(212, 175, 55, 0.12);
overflow: hidden;
}
@@ -174,8 +178,8 @@ function txLabel(tx: TxDetail): string {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--desktop-border, #262626);
padding: 2px 0 12px;
border-bottom: 1px solid rgba(212, 175, 55, 0.16);
flex-shrink: 0;
}

View File

@@ -242,7 +242,7 @@ function onSubmitClick() {
matchName: props.pick.eventTitle,
marketId: '',
marketName: props.pick.eventTitle,
selectionName: `${props.pick.teamName} · ${props.pick.eventTitle}`,
selectionName: `${props.pick.teamName} - ${props.pick.eventTitle}`,
odds: oddsNum.value,
marketType: 'OUTRIGHT',
lineValue: null,
@@ -304,7 +304,7 @@ function formatOdds(odds: string) {
<template>
<div v-if="open && pick" class="overlay" @click.self="close">
<div class="modal" role="dialog" aria-modal="true">
<button type="button" class="close-x" :aria-label="t('bet.cancel')" @click="close"></button>
<button type="button" class="close-x" :aria-label="t('bet.cancel')" @click="close">&#10005;</button>
<template v-if="step === 'form'">
<p class="hint">{{ t('bet.outright_enter_stake') }}</p>
@@ -321,7 +321,7 @@ function formatOdds(odds: string) {
}"
>
<template v-if="oddsDelta && !oddsDelta.suspended">
@ {{ oddsDelta.oldOdds.toFixed(2) }} {{ oddsDelta.newOdds.toFixed(2) }}
@ {{ oddsDelta.oldOdds.toFixed(2) }} -> {{ oddsDelta.newOdds.toFixed(2) }}
</template>
<template v-else>
@ {{ formatOdds(currentOdds || pick.odds) }}
@@ -357,7 +357,7 @@ function formatOdds(odds: string) {
</div>
<p class="est-return">
{{ t('history.est_return') }}<span class="est-value">{{ estReturnText }}</span>
{{ t('history.est_return') }}: <span class="est-value">{{ estReturnText }}</span>
</p>
<p v-if="error" class="error">{{ error }}</p>
@@ -368,7 +368,7 @@ function formatOdds(odds: string) {
</button>
<button
type="button"
class="btn-confirm btn-gold-outline"
class="btn-confirm"
:disabled="loading || stake <= 0 || hasSuspendedSelection"
@click="onSubmitClick"
>
@@ -378,7 +378,7 @@ function formatOdds(odds: string) {
</template>
<template v-else>
<div class="success-icon"></div>
<div class="success-icon">&#10003;</div>
<h3 class="success-title">{{ t('bet.outright_success') }}</h3>
<div class="hero hero--compact">
@@ -400,7 +400,7 @@ function formatOdds(odds: string) {
</div>
</div>
<button type="button" class="btn-done btn-gold-outline" @click="close">
<button type="button" class="btn-done" @click="close">
{{ t('bet.outright_done') }}
</button>
</template>
@@ -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);
border-radius: var(--radius);
padding: 18px 16px 16px;
background: url('../../assets/images/generated/outright-bet-modal-frame-v2.webp') center / 100% 100% no-repeat;
border: 0;
border-radius: 0;
padding: 30px 24px 24px;
text-align: center;
box-shadow: var(--shadow), 0 0 24px rgba(212, 175, 55, 0.08);
box-shadow: none;
}
.close-x {
@@ -644,22 +644,36 @@ function formatOdds(odds: string) {
}
.btn-cancel {
padding: 10px;
border-radius: 6px;
background: #1a1a1a;
border: 1px solid var(--border);
height: 46px;
padding: 10px 12px;
border: 0;
border-radius: 0;
background: url('../../assets/images/generated/pc-betslip-btn-secondary-v1.webp') center / 100% 100% no-repeat;
color: var(--text-muted);
font-size: 14px;
font-weight: 700;
}
.btn-cancel:active {
background-image: url('../../assets/images/generated/pc-betslip-btn-secondary-pressed-v1.webp');
}
.btn-confirm {
padding: 10px;
border-radius: 6px;
height: 46px;
padding: 10px 12px;
border: 0;
border-radius: 0;
background: url('../../assets/images/generated/pc-betslip-btn-primary-v1.webp') center / 100% 100% no-repeat;
color: #100d04;
font-size: 14px;
font-weight: 800;
}
.btn-confirm:active {
background-image: url('../../assets/images/generated/pc-betslip-btn-primary-pressed-v1.webp');
}
.btn-cancel:disabled,
.btn-confirm:disabled {
opacity: 0.45;
}
@@ -708,9 +722,17 @@ function formatOdds(odds: string) {
.btn-done {
width: 100%;
padding: 11px;
border-radius: 6px;
height: 46px;
padding: 11px 12px;
border: 0;
border-radius: 0;
background: url('../../assets/images/generated/pc-betslip-btn-primary-v1.webp') center / 100% 100% no-repeat;
color: #100d04;
font-size: 14px;
font-weight: 800;
}
.btn-done:active {
background-image: url('../../assets/images/generated/pc-betslip-btn-primary-pressed-v1.webp');
}
</style>

View File

@@ -2,8 +2,8 @@
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';
import saishiImg from '../../assets/images/generated/saishi-gold-trophy-v1.webp';
import cardBg from '../../assets/images/generated/pc-match-list-card-bg-v1.webp';
const matchCardBg = `url(${cardBg})`;

View File

@@ -7,7 +7,7 @@ import OutrightEventSection, {
} from './OutrightEventSection.vue';
import OutrightBetModal, { type OutrightPick } from './OutrightBetModal.vue';
import { useAuthStore } from '../../stores/auth';
import emptyMatchesImg from '../../assets/images/empty-matches.svg';
import emptyMatchesImg from '../../assets/images/generated/empty-matches-stadium-v1.webp';
import GoldSpinner from '../../components/GoldSpinner.vue';
import { useOutrightEvents } from '../../composables/useOutrightEvents';
@@ -159,16 +159,25 @@ function closeModal() {
.state,
.empty {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
color: var(--text-muted);
padding: 48px 20px;
padding: 34px 20px 44px;
font-weight: 600;
}
.empty-icon {
width: 96px;
height: 96px;
margin-bottom: 14px;
width: min(270px, 82vw);
aspect-ratio: 16 / 9;
height: auto;
margin-bottom: 12px;
border-radius: 8px;
object-fit: cover;
opacity: 0.86;
mask-image: linear-gradient(90deg, transparent 0, #000 10%, #000 90%, transparent 100%);
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 10%, #000 90%, transparent 100%);
}
.empty-hint {

View File

@@ -1,15 +1,23 @@
import { ref } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import api from '../api';
import type { OutrightEvent } from '../components/outright/OutrightEventSection.vue';
import type { OutrightSelection } from '../components/outright/OutrightEventSection.vue';
import { useOnLocaleChange } from './useOnLocaleChange';
const CACHE_TTL_MS = 8000;
const loading = ref(false);
const loadError = ref('');
const events = ref<OutrightEvent[]>([]);
let lastLoadedAt = 0;
let inFlight: Promise<OutrightEvent[]> | null = null;
type LoadOptions = {
silent?: boolean;
force?: boolean;
};
export function useOutrightEvents() {
const { t } = useI18n();
const loading = ref(true);
const loadError = ref('');
const events = ref<OutrightEvent[]>([]);
const { t, locale } = useI18n();
function mergeOddsOnly(fresh: OutrightEvent[]) {
const freshMap = new Map<string, OutrightEvent>();
@@ -30,15 +38,34 @@ export function useOutrightEvents() {
}
}
async function load(options?: { silent?: boolean }) {
async function load(options?: LoadOptions) {
const hadData = events.value.length > 0;
if (!options?.force && hadData && Date.now() - lastLoadedAt < CACHE_TTL_MS) {
loading.value = false;
loadError.value = '';
return;
}
if (inFlight) {
if (!hadData && !options?.silent) loading.value = true;
try {
await inFlight;
} finally {
if (!hadData && !options?.silent) loading.value = false;
}
return;
}
if (!hadData && !options?.silent) loading.value = true;
loadError.value = '';
try {
const { data } = await api.get('/player/outrights');
const list = (data?.data ?? []) as OutrightEvent[];
const fresh = list.filter((e) => e.selections?.length > 0);
if (!hadData) {
inFlight = api.get('/player/outrights').then(({ data }) => {
const list = (data?.data ?? []) as OutrightEvent[];
return list.filter((e) => e.selections?.length > 0);
});
const fresh = await inFlight;
lastLoadedAt = Date.now();
if (!hadData || options?.force) {
events.value = fresh;
} else {
mergeOddsOnly(fresh);
@@ -53,10 +80,19 @@ export function useOutrightEvents() {
}
} finally {
if (!hadData && !options?.silent) loading.value = false;
inFlight = null;
}
}
useOnLocaleChange(load);
onMounted(() => {
void load({ silent: events.value.length > 0 });
});
watch(locale, (next, prev) => {
if (prev && next !== prev) {
void load({ silent: events.value.length > 0, force: true });
}
});
return {
loading,

View File

@@ -261,7 +261,7 @@ watch(
align-items: center;
padding: 8px 12px;
min-height: var(--player-header-h);
background: rgba(17, 17, 17, 0.94);
background: rgba(29, 27, 22, 0.88);
border-bottom: 1px solid var(--border);
z-index: 120;
}
@@ -371,7 +371,7 @@ watch(
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);
background: rgba(29, 27, 22, 0.9);
border-top: 1px solid var(--border);
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.35);
z-index: 100;

View File

@@ -7,13 +7,13 @@
--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);
--bg-body: #080806;
--bg-card: rgba(33, 31, 25, 0.76);
--bg-hover: rgba(47, 44, 35, 0.82);
--bg-elevated: rgba(58, 54, 43, 0.86);
--text: #FFFFFF;
--text-muted: #8E8E93;
--border: #333333;
--text-muted: #B7B0A0;
--border: rgba(255, 226, 156, 0.18);
--border-gold: #D4AF37;
--border-gold-soft: rgba(212, 175, 55, 0.28);
--border-w: 1px;
@@ -209,9 +209,9 @@ body {
/* 保留 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');
radial-gradient(ellipse 120% 60% at 50% -10%, rgba(246, 210, 108, 0.2), transparent 56%),
linear-gradient(rgba(0, 0, 0, 0.62), rgba(0, 0, 0, 0.7)),
url('./assets/images/generated/saishi-gold-trophy-v1.webp');
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
@@ -223,7 +223,10 @@ body {
@media (min-width: 1024px) {
body {
background-image: url('./assets/images/pcbg.png');
background-image:
radial-gradient(ellipse 90% 70% at 50% 0%, rgba(246, 210, 108, 0.14), transparent 58%),
linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.58)),
url('./assets/images/generated/pc-global-bg-v1.webp');
background-attachment: fixed;
}
}

View File

@@ -8,8 +8,11 @@
inset: 0;
display: flex;
flex-direction: column;
background-color: #000;
background-image: url('../../assets/images/pcbg.png');
background-color: #0d0b08;
background-image:
radial-gradient(ellipse 90% 68% at 50% 0%, rgba(246, 210, 108, 0.14), transparent 58%),
linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.58)),
url('../../assets/images/generated/pc-global-bg-v1.webp');
background-size: cover;
background-position: center;
background-attachment: fixed;
@@ -21,7 +24,7 @@
content: '';
position: fixed;
inset: 0;
background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(212, 175, 55, 0.03) 0%, transparent 60%);
background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(255, 221, 128, 0.08) 0%, transparent 60%);
pointer-events: none;
z-index: 0;
}
@@ -29,10 +32,8 @@
.desktop-header-wrap {
flex-shrink: 0;
height: var(--desktop-header-h);
background: rgba(14, 14, 14, 0.88);
backdrop-filter: var(--blur-md);
-webkit-backdrop-filter: var(--blur-md);
border-bottom: 1px solid var(--border);
background: #12100c url('../../assets/images/generated/pc-top-nav-bg-v1.webp') center / 100% 100% no-repeat;
border-bottom: 0;
z-index: var(--z-header);
display: flex;
align-items: center;
@@ -45,7 +46,7 @@
flex-shrink: 0;
z-index: var(--z-sticky);
border-bottom: 1px solid var(--border);
background: rgba(14, 14, 14, 0.85);
background: rgba(26, 24, 19, 0.78);
backdrop-filter: var(--blur-sm);
-webkit-backdrop-filter: var(--blur-sm);
}
@@ -90,7 +91,7 @@
width: 100%;
padding: 0 16px;
border-bottom: 1px solid var(--border);
background: rgba(14, 14, 14, 0.85);
background: rgba(26, 24, 19, 0.76);
backdrop-filter: var(--blur-sm);
-webkit-backdrop-filter: var(--blur-sm);
}
@@ -105,7 +106,7 @@
.desktop-betting-left {
border-right: 1px solid var(--border);
background: rgba(14, 14, 14, 0.6);
background: rgba(26, 24, 19, 0.56);
backdrop-filter: var(--blur-sm);
-webkit-backdrop-filter: var(--blur-sm);
overflow-y: auto;
@@ -209,7 +210,7 @@
.desktop-hub-right {
overflow-y: auto;
padding: var(--space-5);
background: rgba(10, 10, 10, 0.3);
background: rgba(18, 16, 12, 0.18);
}
/* ── Auth Layout: Centered container ── */

View File

@@ -12,20 +12,20 @@
--primary: #D4AF37;
--primary-dark: #A8841F;
--primary-light: #F0D875;
--secondary: #141414;
--tertiary: #0A0A0A;
--secondary: #1c1a15;
--tertiary: #0f0d09;
--neutral: #8E8E93;
--bg-body: #050505;
--bg-card: rgba(20, 20, 20, 0.92);
--bg-card-hover: rgba(28, 28, 28, 0.95);
--bg-hover: rgba(38, 38, 38, 0.95);
--bg-elevated: rgba(45, 45, 45, 0.95);
--bg-surface: rgba(16, 16, 16, 0.88);
--bg-body: #0d0b08;
--bg-card: rgba(32, 30, 24, 0.82);
--bg-card-hover: rgba(44, 40, 31, 0.9);
--bg-hover: rgba(52, 48, 38, 0.9);
--bg-elevated: rgba(62, 57, 45, 0.92);
--bg-surface: rgba(28, 26, 21, 0.8);
--text: #FFFFFF;
--text-secondary: #B8B8BD;
--text-muted: #8E8E93;
--border: #262626;
--border-subtle: rgba(255, 255, 255, 0.06);
--text-secondary: #D3CBB8;
--text-muted: #B5AD9B;
--border: rgba(255, 226, 156, 0.16);
--border-subtle: rgba(255, 235, 185, 0.1);
--border-gold: #D4AF37;
--border-gold-soft: rgba(212, 175, 55, 0.25);
--border-w: 1px;
@@ -62,10 +62,10 @@
--desktop-right-betslip-collapsed-w: 40px;
/* ── Desktop specific vars ── */
--desktop-bg: #050505;
--desktop-border: #262626;
--desktop-sidebar-bg: rgba(20, 20, 20, 0.95);
--desktop-sidebar-panel-bg: linear-gradient(rgba(16, 16, 16, 0.88), rgba(16, 16, 16, 0.88)), url('../../assets/images/cebian.webp') no-repeat center / cover;
--desktop-bg: #0d0b08;
--desktop-border: rgba(255, 226, 156, 0.16);
--desktop-sidebar-bg: rgba(30, 28, 23, 0.82);
--desktop-sidebar-panel-bg: linear-gradient(rgba(18, 16, 12, 0.52), rgba(18, 16, 12, 0.64)), url('../../assets/images/generated/pc-sidebar-bg-v1.webp') no-repeat center / cover;
/* ── Spacing Scale (8dp rhythm) ── */
--space-1: 4px;

View File

@@ -364,12 +364,19 @@ const showPhaseWatermark = computed(
.receipt {
position: relative;
border-radius: var(--radius);
background: rgba(18, 18, 18, 0.96);
background:
linear-gradient(rgba(18, 16, 12, 0.78), rgba(16, 14, 10, 0.86)),
url('../assets/images/generated/detail-ledger-ticket-bg-v1.webp') center / cover no-repeat;
border: 1px solid var(--border);
box-shadow: var(--shadow);
overflow: hidden;
}
.receipt > * {
position: relative;
z-index: 1;
}
/* result strip */
.receipt-result {
display: flex;
@@ -533,7 +540,7 @@ const showPhaseWatermark = computed(
position: relative;
overflow: hidden;
padding: 14px;
background: rgba(255, 255, 255, 0.02);
background: rgba(255, 242, 198, 0.045);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: var(--radius-sm);
}

View File

@@ -6,7 +6,7 @@ import { usePlayerMatches } from '../composables/usePlayerMatches';
import LeagueAccordionItem from '../components/LeagueAccordionItem.vue';
import MatchBetCard from '../components/MatchBetCard.vue';
import OutrightPanel from '../components/outright/OutrightPanel.vue';
import emptyMatchesImg from '../assets/images/empty-matches.svg';
import emptyMatchesImg from '../assets/images/generated/empty-matches-stadium-v1.webp';
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
import GoldSpinner from '../components/GoldSpinner.vue';
import { usePullToRefresh } from '../composables/usePullToRefresh';
@@ -452,7 +452,6 @@ function goMatch(id: string) {
.bet-page {
margin: 0 -16px;
padding-bottom: 8px;
background: var(--bg-body);
}
/* Tab panel toggle: avoid display:none to prevent browser releasing image resources */
@@ -772,16 +771,25 @@ function goMatch(id: string) {
.state,
.placeholder,
.empty {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
color: var(--text-muted);
padding: 48px 20px;
padding: 34px 20px 44px;
font-weight: 600;
}
.empty-icon {
width: 96px;
height: 96px;
margin-bottom: 14px;
width: min(270px, 82vw);
aspect-ratio: 16 / 9;
height: auto;
margin-bottom: 12px;
border-radius: 8px;
object-fit: cover;
opacity: 0.86;
mask-image: linear-gradient(90deg, transparent 0, #000 10%, #000 90%, transparent 100%);
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 10%, #000 90%, transparent 100%);
}
.placeholder {

View File

@@ -2,11 +2,11 @@
import { onActivated, computed, ref } 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 emptyMatchesImg from '../assets/images/generated/empty-matches-stadium-v1.webp';
import vsImg from '../assets/images/generated/vs-gold-alpha-v1.webp';
import hotCardBg from '../assets/images/generated/match-card-bg-gold-stadium-v1.webp';
import upcomingCardBg from '../assets/images/generated/match-card-bg-gold-tunnel-v1.webp';
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';
@@ -16,7 +16,8 @@ import type { PlayerHomeMatch } from '../composables/usePlayerHome';
type HotTab = 'hot' | 'upcoming';
const matchCardBg = `url(${cardBg})`;
const hotMatchCardBg = `url(${hotCardBg})`;
const upcomingMatchCardBg = `url(${upcomingCardBg})`;
const { t, locale } = useI18n();
const router = useRouter();
const { banners, hotMatches, upcomingMatches, loading, load, announcementItems } = usePlayerHome();
@@ -54,10 +55,16 @@ function formatKickoff(startTime: string) {
return formatLocalMatchDateTime(startTime, locale.value, { variant: 'full' });
}
function matchCardStyle(tab: HotTab) {
return {
'--match-card-bg': tab === 'hot' ? hotMatchCardBg : upcomingMatchCardBg,
};
}
</script>
<template>
<div>
<div class="mobile-home">
<div
class="pull-indicator"
:style="pullIndicatorStyle()"
@@ -67,10 +74,6 @@ function formatKickoff(startTime: string) {
<BannerCarousel :banners="banners" :fallback-to="bannerFallbackTo" />
<div class="home-card-wrapper">
<HomeAnnouncementCard />
</div>
<div class="hot-tabs" role="tablist" :aria-label="t('home.hot_matches')">
<button
type="button"
@@ -98,6 +101,7 @@ function formatKickoff(startTime: string) {
:key="match.id"
class="match-card"
:class="{ 'match-card--live-anim': activeTab === 'hot' && index < 3 }"
:style="matchCardStyle(activeTab)"
@click="goMatch(match.id)"
>
<div class="match-info">
@@ -153,8 +157,9 @@ function formatKickoff(startTime: string) {
</template>
<style scoped>
.home-card-wrapper {
margin: 12px 16px;
.mobile-home {
width: 100%;
margin: 0 auto;
}
.pull-indicator {
@@ -166,33 +171,79 @@ function formatKickoff(startTime: string) {
}
.hot-tabs {
display: flex;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin: 14px 0;
}
.hot-tab {
flex: 1;
min-height: 36px;
position: relative;
isolation: isolate;
width: 100%;
min-height: 44px;
aspect-ratio: 1024 / 275;
padding: 0 12px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-muted);
border: 0;
background: transparent;
color: rgba(255, 255, 255, 0.62);
font-size: 13px;
font-weight: 600;
transition: background 0.2s, border-color 0.2s, color 0.2s;
font-weight: 700;
letter-spacing: 0;
transition: color 0.2s ease, transform 0.2s ease;
-webkit-tap-highlight-color: transparent;
user-select: none;
appearance: none;
}
.hot-tab::before,
.hot-tab::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
transition: opacity 0.2s ease, background 0.2s ease;
}
.hot-tab::before {
z-index: -2;
background: url('../assets/images/generated/home-tab-frame-gold-v1.webp') center / 100% 100% no-repeat;
opacity: 0.58;
filter: saturate(0.86) brightness(0.76);
}
.hot-tab::after {
z-index: -1;
inset: 8px 16px;
background: rgba(0, 0, 0, 0.16);
opacity: 1;
}
.hot-tab.active {
font-weight: 700;
background: rgba(244, 162, 97, 0.1);
border-color: var(--primary);
color: var(--primary-light);
text-shadow: 0 0 10px rgba(247, 211, 101, 0.32);
}
.hot-tab.active::before,
.hot-tab:active::before {
opacity: 1;
filter: saturate(1.08) brightness(1.02);
}
.hot-tab.active::after {
background: rgba(0, 0, 0, 0.04);
}
.hot-tab:focus {
outline: none;
}
.hot-tab:focus-visible::before {
filter: drop-shadow(0 0 6px rgba(247, 211, 101, 0.45));
}
.hot-tab:active {
opacity: 0.92;
transform: scale(0.985);
}
.match-card {
@@ -218,8 +269,8 @@ function formatKickoff(startTime: string) {
content: '';
position: absolute;
inset: 0;
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
opacity: 0.25;
background: var(--match-card-bg) center / 100% 100% no-repeat;
opacity: 0.28;
z-index: 0;
pointer-events: none;
}
@@ -232,7 +283,7 @@ function formatKickoff(startTime: string) {
.match-info,
.match-flags {
position: relative;
z-index: 1;
z-index: 2;
}
.match-info {
@@ -445,15 +496,24 @@ function formatKickoff(startTime: string) {
}
.empty {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
color: var(--text-muted);
padding: 40px 20px;
padding: 30px 20px 36px;
font-weight: 600;
}
.empty-icon {
width: 96px;
height: 96px;
margin-bottom: 14px;
width: min(260px, 82vw);
aspect-ratio: 16 / 9;
height: auto;
margin-bottom: 12px;
border-radius: 8px;
object-fit: cover;
opacity: 0.86;
mask-image: linear-gradient(90deg, transparent 0, #000 10%, #000 90%, transparent 100%);
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 10%, #000 90%, transparent 100%);
}
</style>

View File

@@ -14,7 +14,7 @@ import CorrectScorePanel from '../components/match-detail/CorrectScorePanel.vue'
import { isCorrectScoreMarket, parseScoreCode } from '../utils/correctScoreLayout';
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
import { usePullToRefresh } from '../composables/usePullToRefresh';
import vsImg from '../assets/images/vs.png';
import vsImg from '../assets/images/generated/vs-gold-alpha-v1.webp';
import GoldSpinner from '../components/GoldSpinner.vue';
import { matchPhaseLabel, type MatchPhase } from '../utils/matchPhase';
import { formatLocalMatchDateTime } from '@thebet365/shared';
@@ -911,18 +911,36 @@ function onPickSelection(selId: string, marketId: string) {
}
.my-bets-list {
display: flex;
flex-direction: column;
gap: 8px;
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(2, minmax(72px, auto));
grid-auto-columns: minmax(158px, calc((100% - 6px) / 2));
gap: 6px;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 2px;
scroll-snap-type: x proximity;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.my-bets-list::-webkit-scrollbar {
display: none;
}
.my-bet-card {
background: #1c1c1c;
border: 1px solid #2e2e2e;
border-radius: 8px;
padding: 10px 12px;
padding: 8px 9px;
cursor: pointer;
transition: background 0.15s;
min-width: 0;
min-height: 72px;
display: flex;
flex-direction: column;
justify-content: space-between;
scroll-snap-align: start;
}
.my-bet-card:active {
@@ -931,27 +949,29 @@ function onPickSelection(selId: string, marketId: string) {
.bet-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.bet-type {
font-size: 12px;
font-size: 11px;
font-weight: 700;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
margin-right: 8px;
min-width: 0;
}
.bet-status {
font-size: 11px;
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
padding: 1px 6px;
border-radius: 4px;
line-height: 1.5;
flex-shrink: 0;
}
@@ -972,18 +992,30 @@ function onPickSelection(selId: string, marketId: string) {
.bet-footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
align-items: center;
gap: 6px;
}
.bet-stake {
font-size: 11px;
font-size: 10px;
color: #888;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bet-return {
font-size: 13px;
font-size: 12px;
font-weight: 800;
flex-shrink: 0;
}
@media (max-width: 340px) {
.my-bets-list {
grid-auto-columns: minmax(148px, calc((100% - 6px) / 2));
}
}
</style>

View File

@@ -281,13 +281,23 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
.summary-card,
.audit-card {
background: linear-gradient(135deg, #1a1810 0%, #1f1b0e 40%, #16140c 100%);
position: relative;
overflow: hidden;
background:
linear-gradient(rgba(22, 20, 15, 0.74), rgba(18, 16, 12, 0.84)),
url('../assets/images/generated/detail-ledger-ticket-bg-v1.webp') center / cover no-repeat;
border: 1px solid rgba(212, 175, 55, 0.2);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
}
.summary-card > *,
.audit-card > * {
position: relative;
z-index: 1;
}
.summary-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.method-badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
.method-badge.bank { background: rgba(212,175,55,0.15); color: var(--primary-light); }

View File

@@ -262,23 +262,31 @@ function goCashbackDetail() {
gap: 6px;
margin-bottom: 12px;
text-align: center;
background: #141414;
background:
linear-gradient(rgba(22, 20, 15, 0.74), rgba(18, 16, 12, 0.84)),
url('../assets/images/generated/detail-ledger-ticket-bg-v1.webp') center / cover no-repeat;
border: 1px solid #222;
}
.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));
background:
linear-gradient(135deg, rgba(212, 175, 55, 0.14), rgba(20, 16, 8, 0.76)),
url('../assets/images/generated/detail-ledger-ticket-bg-v1.webp') center / cover no-repeat;
}
.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));
background:
linear-gradient(135deg, rgba(224, 80, 80, 0.1), rgba(20, 12, 10, 0.78)),
url('../assets/images/generated/detail-ledger-ticket-bg-v1.webp') center / cover no-repeat;
}
.hero.zero {
border-color: rgba(255, 255, 255, 0.08);
background: #141414;
background:
linear-gradient(rgba(22, 20, 15, 0.76), rgba(18, 16, 12, 0.84)),
url('../assets/images/generated/detail-ledger-ticket-bg-v1.webp') center / cover no-repeat;
}
.hero-type {
@@ -314,13 +322,22 @@ function goCashbackDetail() {
}
.section {
background: #141414;
position: relative;
overflow: hidden;
background:
linear-gradient(rgba(22, 20, 15, 0.78), rgba(18, 16, 12, 0.86)),
url('../assets/images/generated/detail-ledger-ticket-bg-v1.webp') center / cover no-repeat;
border: 1px solid #222;
border-radius: 12px;
padding: 14px 16px;
margin-bottom: 10px;
}
.section > * {
position: relative;
z-index: 1;
}
.section-title {
font-size: 12px;
font-weight: 800;

View File

@@ -228,12 +228,21 @@ function legResultClass(status?: string) {
.detail-right { display: flex; flex-direction: column; gap: 16px; }
.detail-card {
background: var(--desktop-sidebar-bg);
position: relative;
overflow: hidden;
background:
linear-gradient(rgba(22, 20, 15, 0.76), rgba(18, 16, 12, 0.82)),
url('../../assets/images/generated/detail-ledger-ticket-bg-v1.webp') center / cover no-repeat;
border: 1px solid var(--desktop-border);
border-radius: 12px;
padding: 20px;
}
.detail-card > * {
position: relative;
z-index: 1;
}
.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); }
/* Selection rows */

View File

@@ -7,6 +7,7 @@ import { useDesktopParlayMatches, type DesktopParlayMatch } from '../../composab
import { useOutrightEvents } from '../../composables/useOutrightEvents';
import MatchBetCard from '../../components/MatchBetCard.vue';
import DesktopOutrightEventCard from '../../components/desktop/DesktopOutrightEventCard.vue';
import emptyMatchesImg from '../../assets/images/generated/empty-matches-stadium-v1.webp';
import {
isAfterLocalTodayMatchWindow as isAfterTodayMatchWindow,
isInLocalTodayMatchWindow as isInTodayMatchWindow,
@@ -159,7 +160,7 @@ function closeModal() {
async function refresh() {
if (mainTab.value === 'outright') {
await loadOutrights({ silent: false });
await loadOutrights({ silent: false, force: true });
return;
}
await loadParlayMatches(true);
@@ -224,6 +225,7 @@ void loadParlayMatches(true);
</template>
<div v-else class="empty-state">
<img :src="emptyMatchesImg" alt="" class="empty-state-art" />
<p>{{ t('bet.no_matches') }}</p>
</div>
</div>
@@ -254,6 +256,7 @@ void loadParlayMatches(true);
</template>
<div v-else class="empty-state">
<img :src="emptyMatchesImg" alt="" class="empty-state-art" />
<p>{{ outrightError || t('bet.no_outright') }}</p>
<p v-if="!outrightError" class="empty-hint">{{ t('bet.no_outright_hint') }}</p>
</div>
@@ -438,12 +441,31 @@ void loadParlayMatches(true);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
text-align: center;
padding: 40px 16px;
color: var(--text-muted);
font-size: 13px;
}
.empty-state-art {
width: min(460px, 70%);
aspect-ratio: 16 / 9;
height: auto;
border-radius: 8px;
object-fit: cover;
opacity: 0.72;
mask-image: linear-gradient(90deg, transparent 0, #000 10%, #000 90%, transparent 100%);
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 10%, #000 90%, transparent 100%);
}
.empty-state p {
margin: 0;
}
.empty-hint {
margin-top: 8px;
font-size: 12px;
@@ -459,7 +481,8 @@ void loadParlayMatches(true);
.outright-event-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(340px, 420px));
justify-content: start;
gap: 12px;
}
</style>

View File

@@ -8,6 +8,8 @@ import AnnouncementMarquee from '../../components/AnnouncementMarquee.vue';
import { usePlayerHome } from '../../composables/usePlayerHome';
import TeamEmblem from '../../components/TeamEmblem.vue';
import GoldSpinner from '../../components/GoldSpinner.vue';
import emptyMatchesImg from '../../assets/images/generated/empty-matches-stadium-v1.webp';
import vsImg from '../../assets/images/generated/vs-gold-alpha-v1.webp';
import { formatLocalMatchDateTime } from '@thebet365/shared';
import type { PlayerHomeMatch } from '../../composables/usePlayerHome';
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
@@ -226,7 +228,9 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
/>
<span class="featured-team-name">{{ featuredMatch.homeTeamName }}</span>
</div>
<div class="featured-vs vs-heartbeat">VS</div>
<div class="featured-vs vs-heartbeat">
<img :src="vsImg" alt="VS" class="featured-vs-img" />
</div>
<div class="featured-team">
<TeamEmblem
size="lg"
@@ -293,7 +297,9 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
/>
<span class="team-name">{{ match.homeTeamName }}</span>
</div>
<div class="vs-text">VS</div>
<div class="vs-text">
<img :src="vsImg" alt="VS" class="vs-mini-img" />
</div>
<div class="team-row">
<TeamEmblem
size="sm"
@@ -317,7 +323,9 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
/>
<span class="hover-team-name">{{ match.homeTeamName }}</span>
</div>
<div class="hover-vs-glow">VS</div>
<div class="hover-vs-glow">
<img :src="vsImg" alt="VS" class="hover-vs-img" />
</div>
<div class="hover-team away-slide">
<TeamEmblem
size="md"
@@ -364,6 +372,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
</template>
<div v-else class="empty-state">
<img :src="emptyMatchesImg" alt="" class="empty-state-art" />
<p>{{ t('home.no_matches') }}</p>
</div>
</section>
@@ -405,7 +414,9 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
/>
<span class="team-name">{{ match.homeTeamName }}</span>
</div>
<div class="vs-text">VS</div>
<div class="vs-text">
<img :src="vsImg" alt="VS" class="vs-mini-img" />
</div>
<div class="team-row">
<TeamEmblem
size="sm"
@@ -429,7 +440,9 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
/>
<span class="hover-team-name">{{ match.homeTeamName }}</span>
</div>
<div class="hover-vs-glow">VS</div>
<div class="hover-vs-glow">
<img :src="vsImg" alt="VS" class="hover-vs-img" />
</div>
<div class="hover-team away-slide">
<TeamEmblem
size="md"
@@ -474,6 +487,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
</div>
</div>
<div v-else-if="!loading" class="empty-state">
<img :src="emptyMatchesImg" alt="" class="empty-state-art" />
<p>{{ t('home.upcoming_empty') }}</p>
</div>
</section>
@@ -505,7 +519,9 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
</div>
<div class="sri-teams">
<span>{{ m.homeTeamName }}</span>
<span class="sri-vs">vs</span>
<span class="sri-vs">
<img :src="vsImg" alt="VS" class="sri-vs-img" />
</span>
<span>{{ m.awayTeamName }}</span>
</div>
@@ -520,7 +536,9 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
<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-vs-text">
<img :src="vsImg" alt="VS" class="sri-preview-vs-img" />
</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>
@@ -623,7 +641,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
}
.content-section {
background: rgba(20, 20, 20, 0.88);
background: rgba(32, 30, 24, 0.78);
backdrop-filter: var(--blur-md);
-webkit-backdrop-filter: var(--blur-md);
border: 1px solid var(--border);
@@ -685,22 +703,39 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
.loading-state,
.empty-state {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
min-height: 160px;
color: var(--text-muted);
}
.empty-state-art {
width: min(420px, 72%);
aspect-ratio: 16 / 9;
height: auto;
border-radius: 8px;
object-fit: cover;
opacity: 0.72;
mask-image: linear-gradient(90deg, transparent 0, #000 10%, #000 90%, transparent 100%);
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 10%, #000 90%, transparent 100%);
}
.empty-state p {
margin: 0;
}
/* Featured 焦点大卡 */
.featured-match {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: 18px 20px;
padding: 18px 20px 20px;
border-radius: var(--radius-lg);
background:
linear-gradient(135deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04) 50%, rgba(15, 10, 0, 0.1)),
url('../../assets/images/card-bg.webp') center/cover no-repeat;
linear-gradient(90deg, rgba(0, 0, 0, 0.34), rgba(0, 0, 0, 0.12) 50%, rgba(10, 6, 0, 0.36)),
url('../../assets/images/generated/pc-featured-match-bg-v1.webp') center / 100% 100% no-repeat;
border: 1px solid var(--border-gold);
box-shadow: var(--shadow-gold-lg), inset 0 0 20px rgba(212, 175, 55, 0.06);
margin-bottom: var(--space-4);
@@ -711,6 +746,11 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
overflow: hidden;
}
.featured-match > * {
position: relative;
z-index: 1;
}
.featured-match::before {
content: '';
position: absolute;
@@ -718,10 +758,30 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
width: 180px; height: 180px;
background: radial-gradient(circle at top right, rgba(212, 175, 55, 0.2), transparent 60%);
pointer-events: none;
z-index: 0;
animation: goldGlowPulse 3.5s ease-in-out infinite;
transform-origin: top right;
}
.featured-match::after {
content: '';
position: absolute;
inset: 0;
z-index: 2;
pointer-events: none;
background: url('../../assets/images/generated/pc-featured-match-bg-v1.webp') center / 100% 100% no-repeat;
mask:
linear-gradient(#000 0 0) top / 100% 22px no-repeat,
linear-gradient(#000 0 0) bottom / 100% 22px no-repeat,
linear-gradient(#000 0 0) left / 22px 100% no-repeat,
linear-gradient(#000 0 0) right / 22px 100% no-repeat;
-webkit-mask:
linear-gradient(#000 0 0) top / 100% 22px no-repeat,
linear-gradient(#000 0 0) bottom / 100% 22px no-repeat,
linear-gradient(#000 0 0) left / 22px 100% no-repeat,
linear-gradient(#000 0 0) right / 22px 100% no-repeat;
}
.featured-match:hover {
transform: translateY(-2px);
box-shadow: var(--glow-gold-lg), inset 0 0 24px rgba(212, 175, 55, 0.08);
@@ -786,22 +846,35 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
}
.featured-vs {
font-family: var(--font-display);
font-size: var(--text-2xl);
font-weight: 400;
font-style: italic;
color: var(--primary-light);
text-shadow: 0 0 12px rgba(212, 175, 55, 0.5);
width: 72px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
}
.featured-vs-img {
width: 68px;
max-width: 100%;
height: auto;
object-fit: contain;
filter:
drop-shadow(0 0 4px rgba(255, 231, 150, 0.42))
drop-shadow(0 0 8px rgba(212, 175, 55, 0.26));
}
.featured-markets {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin: 14px -20px -18px;
padding: 14px 20px 18px;
background: #000000;
border-top: 1px solid rgba(212, 175, 55, 0.2);
margin: 14px -12px 0;
padding: 14px 12px 8px;
background: rgba(10, 8, 4, 0.46);
border-top: 1px solid rgba(246, 210, 108, 0.22);
border-radius: 0 0 8px 8px;
box-shadow: inset 0 1px 0 rgba(255, 236, 174, 0.08);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.featured-market-block {
@@ -947,15 +1020,20 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
}
.vs-text {
font-size: 10px;
font-weight: 800;
color: var(--text-muted);
font-style: italic;
display: flex;
align-items: center;
padding-left: 36px;
line-height: 1;
margin: -2px 0;
}
.vs-mini-img {
width: 30px;
height: auto;
object-fit: contain;
opacity: 0.78;
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.75));
}
.team-row {
display: flex;
align-items: center;
@@ -1124,10 +1202,17 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
}
.sri-vs {
color: var(--text-muted);
font-size: 9px;
font-weight: 600;
font-style: italic;
display: inline-flex;
align-items: center;
flex-shrink: 0;
}
.sri-vs-img {
width: 22px;
height: auto;
object-fit: contain;
opacity: 0.7;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.7));
}
.sri-time {
@@ -1309,19 +1394,26 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
}
.hover-vs-glow {
font-size: 18px;
font-weight: 900;
font-style: italic;
color: var(--primary-light);
width: 58px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
transform: scale(2.8);
opacity: 0;
text-shadow:
0 0 10px rgba(212, 175, 55, 0.8),
0 0 20px rgba(212, 175, 55, 0.4);
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;
}
.hover-vs-img {
width: 56px;
height: auto;
object-fit: contain;
filter:
drop-shadow(0 0 5px rgba(255, 231, 150, 0.52))
drop-shadow(0 0 9px rgba(212, 175, 55, 0.34));
}
.match-card:is(:hover, .match-card--engaged) .hover-vs-glow {
transform: scale(1);
opacity: 1;
@@ -1330,11 +1422,9 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
@keyframes vs-clash-impact {
0% {
text-shadow: 0 0 25px rgba(212, 175, 55, 1);
transform: scale(1.25);
}
100% {
text-shadow: 0 0 8px rgba(212, 175, 55, 0.7);
transform: scale(1);
}
}
@@ -1471,17 +1561,26 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
}
.sri-preview-vs-text {
font-size: 16px;
font-weight: 900;
font-style: italic;
color: var(--primary-light);
text-shadow: 0 0 8px rgba(212, 175, 55, 0.6);
width: 48px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
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;
}
.sri-preview-vs-img {
width: 46px;
height: auto;
object-fit: contain;
filter:
drop-shadow(0 0 4px rgba(255, 231, 150, 0.42))
drop-shadow(0 0 8px rgba(212, 175, 55, 0.26));
}
.side-recommend-item:hover .sri-preview-vs-text {
opacity: 1;
transform: scale(1);

View File

@@ -16,6 +16,7 @@ import { formatLocalMatchDateTime, defaultMarketName, DETAIL_MARKET_TYPES } from
import { resolveSelectionLabel } from '../../utils/selectionLabel';
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
import { useDesktopBetLocate, scrollToBetSelection } from '../../composables/useDesktopBetLocate';
import vsImg from '../../assets/images/generated/vs-gold-alpha-v1.webp';
const route = useRoute();
const router = useRouter();
@@ -360,7 +361,9 @@ function getHoveredSide() {
<span class="divider">-</span>
<span class="score">{{ match.score?.ftAway }}</span>
</div>
<div class="versus-row vs" v-else>VS</div>
<div class="versus-row vs" v-else>
<img :src="vsImg" alt="VS" class="score-vs-img" />
</div>
<span class="kickoff-time">{{ kickoff }}</span>
</div>
@@ -537,13 +540,15 @@ function getHoveredSide() {
}
.match-score-band {
background:
linear-gradient(135deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04) 50%, rgba(15, 10, 0, 0.1)),
url('../../assets/images/card-bg.webp') center/cover no-repeat;
border: 1px solid var(--border-gold);
box-shadow: var(--shadow-gold), inset 0 0 20px rgba(212, 175, 55, 0.06);
border-radius: 12px;
padding: 18px 24px;
aspect-ratio: 1600 / 320;
background-image: url('../../assets/images/generated/pc-featured-match-bg-v1.webp');
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
border: 0;
box-shadow: var(--shadow-gold);
border-radius: 4px;
padding: 12px 24px;
overflow: hidden;
display: flex;
align-items: center;
@@ -595,9 +600,17 @@ function getHoveredSide() {
}
.versus-row.vs {
font-size: 22px;
font-weight: 800;
color: var(--text-muted);
min-height: 42px;
justify-content: center;
}
.score-vs-img {
width: 72px;
height: auto;
object-fit: contain;
filter:
drop-shadow(0 0 4px rgba(255, 231, 150, 0.42))
drop-shadow(0 0 8px rgba(212, 175, 55, 0.26));
}
.kickoff-time {

View File

@@ -8,11 +8,11 @@ import OutrightOptionCard from '../../components/outright/OutrightOptionCard.vue
import OutrightBetModal, { type OutrightPick } from '../../components/outright/OutrightBetModal.vue';
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 saishiImg from '../../assets/images/generated/saishi-gold-trophy-v1.webp';
import outrightDetailBg from '../../assets/images/generated/pc-outright-detail-bg-v1.webp';
import { useDesktopBetLocate, scrollToBetSelection } from '../../composables/useDesktopBetLocate';
const matchCardBg = `url(${cardBg})`;
const matchCardBg = `url(${outrightDetailBg})`;
const ODDS_POLL_MS = 15000;
const route = useRoute();
@@ -246,7 +246,7 @@ onUnmounted(stopPolling);
position: absolute;
inset: 0;
background: v-bind(matchCardBg) center / cover no-repeat;
opacity: 0.22;
opacity: 0.36;
pointer-events: none;
}

View File

@@ -10,7 +10,7 @@ import { useAppLocale } from '../../composables/useAppLocale';
import { usePlayerProfile } from '../../composables/usePlayerProfile';
import { formatMoney } from '../../utils/localeDisplay';
import { parseCashbackApiData, sumCashbackAmount } from '../../utils/cashback';
import walletBg from '../../assets/images/wallet-bg.webp';
import walletBg from '../../assets/images/generated/pc-wallet-banner-v1.webp';
const { t, locale } = useI18n();
const router = useRouter();
@@ -223,6 +223,12 @@ const quickLinks = computed(() => [
overflow-y: auto;
height: auto;
max-height: 100%;
-ms-overflow-style: none;
scrollbar-width: none;
}
.profile-page::-webkit-scrollbar {
display: none;
}
.page-title {
@@ -306,8 +312,8 @@ const quickLinks = computed(() => [
display: block;
object-fit: cover;
object-position: center center;
transform: scale(1.05);
filter: brightness(0.86);
transform: scale(1.01);
filter: brightness(0.94);
pointer-events: none;
user-select: none;
}

View File

@@ -292,12 +292,21 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
.detail-layout { display: flex; flex-direction: column; gap: 16px; max-width: 680px; }
.detail-card {
background: var(--desktop-sidebar-bg);
position: relative;
overflow: hidden;
background:
linear-gradient(rgba(22, 20, 15, 0.76), rgba(18, 16, 12, 0.82)),
url('../../assets/images/generated/detail-ledger-ticket-bg-v1.webp') center / cover no-repeat;
border: 1px solid var(--desktop-border);
border-radius: 12px;
padding: 20px;
}
.detail-card > * {
position: relative;
z-index: 1;
}
.header-card { display: flex; flex-direction: column; gap: 12px; }
.order-no {

View File

@@ -166,12 +166,21 @@ function txLabel(tx: TxDetail): string {
}
.detail-card {
background: var(--desktop-sidebar-bg);
position: relative;
overflow: hidden;
background:
linear-gradient(rgba(22, 20, 15, 0.76), rgba(18, 16, 12, 0.82)),
url('../../assets/images/generated/detail-ledger-ticket-bg-v1.webp') center / cover no-repeat;
border: 1px solid var(--desktop-border);
border-radius: 12px;
padding: 28px;
}
.detail-card > * {
position: relative;
z-index: 1;
}
/* Hero card (left) */
.hero-card {
display: flex;