216 lines
8.5 KiB
Vue
216 lines
8.5 KiB
Vue
<script setup lang="ts">
|
||
import { ref, 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';
|
||
|
||
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('/bets');
|
||
}
|
||
}
|
||
|
||
type BetDetail = {
|
||
betNo: string;
|
||
stake: string;
|
||
potentialReturn?: string;
|
||
status: string;
|
||
placedAt: string;
|
||
legs: Array<{
|
||
matchTitle?: string;
|
||
marketLabel?: string;
|
||
selectionName?: string;
|
||
odds?: number;
|
||
resultStatus?: string;
|
||
score?: { ht: string | null; ft: string | null } | null;
|
||
[key: string]: any;
|
||
}>;
|
||
[key: string]: any;
|
||
};
|
||
|
||
const detail = ref<BetDetail | null>(null);
|
||
const loading = ref(true);
|
||
const error = ref(false);
|
||
|
||
async function loadDetail() {
|
||
const betNo = route.params.betNo as string;
|
||
if (!betNo) return;
|
||
loading.value = true;
|
||
error.value = false;
|
||
try {
|
||
const { data } = await api.get(`/player/bets/${betNo}`);
|
||
detail.value = data.data ?? null;
|
||
} catch {
|
||
error.value = true;
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(loadDetail);
|
||
|
||
function statusClass(status: string) {
|
||
const map: Record<string, string> = { WON: 'status-won', LOST: 'status-lost', PENDING: 'status-pending', PUSH: 'status-push' };
|
||
return map[status?.toUpperCase()] ?? 'status-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'),
|
||
};
|
||
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;
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="desktop-account-page">
|
||
<main class="account-main">
|
||
<div class="page-header">
|
||
<button type="button" class="back-btn" @click="goBack">
|
||
‹ {{ t('common.back') || '返回' }}
|
||
</button>
|
||
<h1 class="page-title">{{ t('bet.detail_title') || '注单详情' }}</h1>
|
||
</div>
|
||
|
||
<div v-if="loading" class="loading-state">
|
||
<GoldSpinner :size="36" />
|
||
</div>
|
||
|
||
<div v-else-if="error" class="error-state">
|
||
<p>{{ t('common.load_failed') }}</p>
|
||
<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.potentialReturn || '-' }}</span>
|
||
</div>
|
||
<div class="amount-item">
|
||
<span class="amount-label">{{ t('bet.placed_at') || '下注时间' }}</span>
|
||
<span class="amount-val date">{{ detail.placedAt ? new Date(detail.placedAt).toLocaleString() : '-' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Selections -->
|
||
<div class="detail-card">
|
||
<div class="card-title">{{ t('bet.selections') || '投注选项' }}</div>
|
||
<div v-for="(leg, idx) in detail.legs" :key="idx" class="sel-row">
|
||
<div class="sel-match">{{ leg.matchTitle || t('bet.match') }}</div>
|
||
<div class="sel-meta">
|
||
<span class="sel-market">{{ leg.marketLabel || '' }}</span>
|
||
<span class="sel-name">{{ translateSel(leg.selectionName || '') }}</span>
|
||
<span class="sel-odds">@{{ leg.odds }}</span>
|
||
<span v-if="leg.score?.ft" class="sel-score">({{ t('history.ft') }}: {{ leg.score.ft }})</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="empty-state">{{ t('common.not_found') || '未找到记录' }}</div>
|
||
</main>
|
||
</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; overflow-y: auto; }
|
||
.page-header { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; }
|
||
.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; }
|
||
.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; }
|
||
.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-card {
|
||
background: var(--desktop-sidebar-bg);
|
||
border: 1px solid var(--desktop-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;
|
||
font-weight: 700;
|
||
color: var(--text-muted);
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.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(255, 255, 255,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; }
|
||
|
||
.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-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-name { color: var(--text); font-weight: 700; }
|
||
.sel-odds { color: var(--primary-light); font-weight: 800; margin-left: auto; }
|
||
</style>
|