@@ -335,7 +366,11 @@ function toggleCollapse(id: string) {
:key="sel.id"
type="button"
class="odd-btn"
- :class="{ picked: isPicked(sel.id) }"
+ :class="{
+ picked: isPicked(sel.id),
+ 'odd-btn--locked': match.bettingOpen === false,
+ }"
+ :disabled="match.bettingOpen === false"
@click="pickSelection(match, getMarket(match, col.key)!, sel)"
>
{{ selLabel(sel) }}
@@ -467,6 +502,8 @@ function toggleCollapse(id: string) {
}
.match-head {
+ position: relative;
+ overflow: hidden;
width: 100%;
display: flex;
flex-direction: column;
@@ -647,6 +684,27 @@ function toggleCollapse(id: string) {
background: rgba(212, 175, 55, 0.15);
}
+.odd-btn--locked {
+ opacity: 0.65;
+ cursor: not-allowed;
+ border-color: #333;
+}
+
+.odd-btn--locked .odd-label,
+.odd-btn--locked .odd-val {
+ color: #666;
+}
+
+.match-card--phase {
+ opacity: 0.94;
+}
+
+.m-score {
+ font-size: 12px;
+ font-weight: 800;
+ color: #fff;
+}
+
.odd-label {
font-size: 8.5px;
font-weight: 700;
diff --git a/apps/player/src/main.ts b/apps/player/src/main.ts
index b9bfcc2..b3ee820 100644
--- a/apps/player/src/main.ts
+++ b/apps/player/src/main.ts
@@ -196,6 +196,9 @@ const i18n = createI18n({
download: '下载',
reward_active: '奖励生效中!',
market_closed: '暂未开盘',
+ match_phase_closed_pending: '封盘待结算',
+ match_phase_settled: '已结算',
+ view_match: '查看赛况',
expand_market: '展开玩法',
collapse_market: '收起玩法',
market_cs: '波胆',
@@ -502,6 +505,9 @@ const i18n = createI18n({
download: 'Download',
reward_active: 'Reward active!',
market_closed: 'Not open',
+ match_phase_closed_pending: 'Closed pending',
+ match_phase_settled: 'Settled',
+ view_match: 'View match',
expand_market: 'Expand',
collapse_market: 'Collapse',
market_cs: 'Correct Score',
@@ -814,6 +820,9 @@ const i18n = createI18n({
download: 'Muat turun',
reward_active: 'Ganjaran aktif!',
market_closed: 'Belum dibuka',
+ match_phase_closed_pending: 'Ditutup menunggu',
+ match_phase_settled: 'Selesai',
+ view_match: 'Lihat perlawanan',
expand_market: 'Kembang',
collapse_market: 'Tutup',
market_cs: 'Skor Tepat',
diff --git a/apps/player/src/utils/matchPhase.ts b/apps/player/src/utils/matchPhase.ts
new file mode 100644
index 0000000..7d83d17
--- /dev/null
+++ b/apps/player/src/utils/matchPhase.ts
@@ -0,0 +1,13 @@
+export type MatchPhase = 'open' | 'closed_pending' | 'settled';
+
+export function matchPhaseLabel(t: (key: string) => string, phase?: MatchPhase | null) {
+ if (phase === 'closed_pending') return t('bet.match_phase_closed_pending');
+ if (phase === 'settled') return t('bet.match_phase_settled');
+ return '';
+}
+
+export function matchPhaseVariant(phase?: MatchPhase | null): 'closed' | 'settled' | 'pending' {
+ if (phase === 'settled') return 'settled';
+ if (phase === 'closed_pending') return 'closed';
+ return 'pending';
+}
diff --git a/apps/player/src/views/BetDetailView.vue b/apps/player/src/views/BetDetailView.vue
index 710a3a0..1f65d1a 100644
--- a/apps/player/src/views/BetDetailView.vue
+++ b/apps/player/src/views/BetDetailView.vue
@@ -6,6 +6,8 @@ import api from '../api';
import { formatMoney } from '../utils/localeDisplay';
import GoldSpinner from '../components/GoldSpinner.vue';
import { usePullToRefresh } from '../composables/usePullToRefresh';
+import StatusWatermark from '../components/StatusWatermark.vue';
+import { matchPhaseLabel, matchPhaseVariant, type MatchPhase } from '../utils/matchPhase';
import type { BetHistoryItem } from '../components/BetHistoryCard.vue';
const route = useRoute();
@@ -122,6 +124,12 @@ const myPick = computed(() => {
if (ci < 0) return raw;
return raw.slice(0, ci + 2) + translateSel(raw.slice(ci + 2));
});
+
+const matchPhase = computed(
+ (): MatchPhase | null => bet.value?.matchPhase ?? null,
+);
+
+const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
@@ -164,7 +172,13 @@ const myPick = computed(() => {
{{ matchTitle }}
-
+
+
{{ t('history.my_pick') }}
@@ -364,11 +378,17 @@ const myPick = computed(() => {
/* 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;
diff --git a/apps/player/src/views/FootballView.vue b/apps/player/src/views/FootballView.vue
index 7221049..070b2c9 100644
--- a/apps/player/src/views/FootballView.vue
+++ b/apps/player/src/views/FootballView.vue
@@ -11,6 +11,7 @@ import emptyMatchesImg from '../assets/images/empty-matches.svg';
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
import GoldSpinner from '../components/GoldSpinner.vue';
import { usePullToRefresh } from '../composables/usePullToRefresh';
+import type { MatchPhase } from '../utils/matchPhase';
type MainTab = 'matches' | 'outright' | 'parlay';
type TimeTab = 'today' | 'early';
@@ -29,6 +30,15 @@ interface Match {
leagueLogoUrl?: string | null;
displayOrder?: number;
isHot?: boolean;
+ status?: string;
+ bettingOpen?: boolean;
+ matchPhase?: MatchPhase;
+ score?: {
+ htHome: number;
+ htAway: number;
+ ftHome: number;
+ ftAway: number;
+ } | null;
}
interface LeagueGroup {
diff --git a/apps/player/src/views/LoginView.vue b/apps/player/src/views/LoginView.vue
index d63a5a2..7ef7229 100644
--- a/apps/player/src/views/LoginView.vue
+++ b/apps/player/src/views/LoginView.vue
@@ -13,8 +13,8 @@ const { initFromUser } = useAppLocale();
const auth = useAuthStore();
const router = useRouter();
const captchaRef = ref | null>(null);
-const username = ref('player1');
-const password = ref('Player@123');
+const username = ref('');
+const password = ref('');
const error = ref('');
const loading = ref(false);
diff --git a/apps/player/src/views/MatchDetailView.vue b/apps/player/src/views/MatchDetailView.vue
index 59f9cc6..9392c3b 100644
--- a/apps/player/src/views/MatchDetailView.vue
+++ b/apps/player/src/views/MatchDetailView.vue
@@ -19,6 +19,8 @@ import { usePullToRefresh } from '../composables/usePullToRefresh';
import vsImg from '../assets/images/vs.png';
import GoldSpinner from '../components/GoldSpinner.vue';
import BetSuccessOverlay from '../components/BetSuccessOverlay.vue';
+import StatusWatermark from '../components/StatusWatermark.vue';
+import { matchPhaseLabel, matchPhaseVariant, type MatchPhase } from '../utils/matchPhase';
import cardBg from '../assets/images/card-bg.png';
const heroCardBg = `url(${cardBg})`;
@@ -59,6 +61,15 @@ interface MatchDetail {
startTime: string;
stage?: string | null;
groupName?: string | null;
+ status?: string;
+ bettingOpen?: boolean;
+ matchPhase?: MatchPhase;
+ score?: {
+ htHome: number;
+ htAway: number;
+ ftHome: number;
+ ftAway: number;
+ } | null;
markets: Market[];
}
@@ -96,6 +107,21 @@ const kickoff = computed(() => {
});
});
+const bettingOpen = computed(() => match.value?.bettingOpen !== false);
+
+const matchPhase = computed(
+ (): MatchPhase =>
+ match.value?.matchPhase ?? (bettingOpen.value ? 'open' : 'closed_pending'),
+);
+
+const phaseLabel = computed(() => matchPhaseLabel(t, matchPhase.value));
+
+const liveScoreText = computed(() => {
+ const s = match.value?.score;
+ if (!s) return '';
+ return `${s.ftHome} - ${s.ftAway}`;
+});
+
function marketLabel(marketType: string) {
const key = MARKET_I18N_KEY[marketType];
return key ? t(key) : marketType;
@@ -170,6 +196,7 @@ async function confirmCorrectScoreBets() {
}
async function placeCorrectScoreBets(marketType: string) {
+ if (!bettingOpen.value) return;
const market = marketsByType.value.get(marketType);
if (!market || !match.value) return;
const entries = market.selections.filter((s) => (correctScoreStakes.value[s.id] ?? 0) > 0);
@@ -228,7 +255,7 @@ function isSelected(id: string) {
}
function toggleSelection(sel: Selection, market: Market) {
- if (!match.value) return;
+ if (!match.value || !bettingOpen.value) return;
slip.addItem({
selectionId: sel.id,
oddsVersion: String(sel.oddsVersion),
@@ -295,7 +322,13 @@ function hasSlipPickForMarket(marketType: string) {
-
+
+
@@ -348,6 +381,7 @@ function hasSlipPickForMarket(marketType: string) {
{{ t('bet.kickoff_time') }}{{ kickoff }}
+
{{ liveScoreText }}
@@ -379,14 +413,22 @@ function hasSlipPickForMarket(marketType: string) {
@toggle="toggleMarket(marketType)"
/>
+
+
+
@@ -501,6 +545,19 @@ function hasSlipPickForMarket(marketType: string) {
pointer-events: none;
}
+.match-hero--phase {
+ opacity: 0.98;
+}
+
+.market-panel-wrap {
+ position: relative;
+ overflow: hidden;
+}
+
+.market-panel-wrap.locked {
+ opacity: 0.82;
+}
+
.kickoff {
position: relative;
z-index: 1;
@@ -511,6 +568,17 @@ function hasSlipPickForMarket(marketType: string) {
padding-left: 2px;
}
+.live-score {
+ position: relative;
+ z-index: 2;
+ margin: 8px 0 0;
+ font-size: 28px;
+ font-weight: 900;
+ color: #fff;
+ text-align: center;
+ letter-spacing: 0.06em;
+}
+
.hero-teams {
position: relative;
z-index: 1;