feat(player): 新增桌面端 UI 与响应式双端路由架构
- 将原移动端页面重命名为 Mobile*,新增 22 个 Desktop* 视图与 DesktopShell 布局体系 - 路由入口改为 Wrapper 视图,通过 useViewport(≥1024px)自动切换移动/桌面端 - 新增投注单面板、盘口弹层、联赛侧栏、数据表格等桌面组件及独立样式令牌 - 扩展 betSlip store、串关筛选、冠军盘、Toast/悬浮信箱等交互与三语 i18n - 公告/消息 Hub 拆分移动与桌面实现;API 投注/钱包/充值记录支持 pageSize 分页
This commit is contained in:
189
apps/player/src/views/desktop/DesktopBetDetailView.vue
Normal file
189
apps/player/src/views/desktop/DesktopBetDetailView.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<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 } = 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;
|
||||
potentialPayout?: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
selections: Array<{
|
||||
matchName?: string;
|
||||
selectionName?: string;
|
||||
odds?: number;
|
||||
market?: string;
|
||||
homeScore?: number;
|
||||
awayScore?: number;
|
||||
matchStatus?: string;
|
||||
[key: string]: any;
|
||||
}>;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
const detail = ref<BetDetail | null>(null);
|
||||
const 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;
|
||||
}
|
||||
</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.potentialPayout || '-' }}</span>
|
||||
</div>
|
||||
<div class="amount-item">
|
||||
<span class="amount-label">{{ t('bet.placed_at') || '下注时间' }}</span>
|
||||
<span class="amount-val date">{{ new Date(detail.createdAt).toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selections -->
|
||||
<div class="detail-card">
|
||||
<div class="card-title">{{ t('bet.selections') || '投注选项' }}</div>
|
||||
<div v-for="(sel, idx) in detail.selections" :key="idx" class="sel-row">
|
||||
<div class="sel-match">{{ sel.matchName || t('bet.match') }}</div>
|
||||
<div class="sel-meta">
|
||||
<span class="sel-market">{{ sel.market || sel.marketName || '' }}</span>
|
||||
<span class="sel-name">{{ sel.selectionName || '' }}</span>
|
||||
<span class="sel-odds">@{{ sel.odds }}</span>
|
||||
</div>
|
||||
</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(212,175,55,0.12); color: var(--primary-light); }
|
||||
.status-push { background: rgba(100,100,100,0.12); color: #aaa; }
|
||||
.status-default { background: rgba(100,100,100,0.1); color: #888; }
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user