feat(theme-4): fix dark backgrounds, quick bet, inbox spacing, customer service theme

This commit is contained in:
mars
2026-07-09 18:03:57 +08:00
parent dcf9afacd6
commit ecfea51545
124 changed files with 26710 additions and 8578 deletions

View File

@@ -0,0 +1,215 @@
<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>