初始化足球投注平台 MVP Monorepo
包含 NestJS 后端、三端前端、Prisma 数据模型、结算引擎测试与 PRD 文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
102
apps/player/src/views/MatchDetailView.vue
Normal file
102
apps/player/src/views/MatchDetailView.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import api from '../api';
|
||||
import { useBetSlipStore } from '../stores/betSlip';
|
||||
|
||||
const route = useRoute();
|
||||
const slip = useBetSlipStore();
|
||||
const match = ref<MatchDetail | null>(null);
|
||||
|
||||
interface MatchDetail {
|
||||
id: string;
|
||||
homeTeamName: string;
|
||||
awayTeamName: string;
|
||||
startTime: string;
|
||||
markets: Market[];
|
||||
}
|
||||
|
||||
interface Market {
|
||||
id: string;
|
||||
marketType: string;
|
||||
period: string;
|
||||
lineValue?: string;
|
||||
selections: Selection[];
|
||||
}
|
||||
|
||||
interface Selection {
|
||||
id: string;
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
odds: string;
|
||||
oddsVersion: string;
|
||||
}
|
||||
|
||||
const marketLabels: Record<string, string> = {
|
||||
FT_1X2: '全场独赢',
|
||||
FT_HANDICAP: '全场让球',
|
||||
FT_OVER_UNDER: '全场大小',
|
||||
FT_ODD_EVEN: '全场单双',
|
||||
HT_1X2: '半场独赢',
|
||||
FT_CORRECT_SCORE: '波胆',
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await api.get(`/player/matches/${route.params.id}`);
|
||||
match.value = data.data;
|
||||
});
|
||||
|
||||
function isSelected(id: string) {
|
||||
return slip.items.some((i) => i.selectionId === id);
|
||||
}
|
||||
|
||||
function toggleSelection(sel: Selection, market: Market) {
|
||||
if (!match.value) return;
|
||||
slip.addItem({
|
||||
selectionId: sel.id,
|
||||
oddsVersion: sel.oddsVersion,
|
||||
matchId: match.value.id,
|
||||
matchName: `${match.value.homeTeamName} vs ${match.value.awayTeamName}`,
|
||||
selectionName: sel.selectionName,
|
||||
odds: parseFloat(sel.odds),
|
||||
marketType: market.marketType,
|
||||
});
|
||||
}
|
||||
|
||||
const groupedMarkets = computed(() => {
|
||||
if (!match.value) return [];
|
||||
return match.value.markets;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="match">
|
||||
<div class="match-header card">
|
||||
<h2>{{ match.homeTeamName }} vs {{ match.awayTeamName }}</h2>
|
||||
<p class="time">{{ new Date(match.startTime).toLocaleString() }}</p>
|
||||
</div>
|
||||
|
||||
<div v-for="market in groupedMarkets" :key="market.id" class="card market-group">
|
||||
<h3>{{ marketLabels[market.marketType] || market.marketType }}</h3>
|
||||
<div class="selections">
|
||||
<button
|
||||
v-for="sel in market.selections"
|
||||
:key="sel.id"
|
||||
class="odds-btn"
|
||||
:class="{ selected: isSelected(sel.id) }"
|
||||
@click="toggleSelection(sel, market)"
|
||||
>
|
||||
<div class="label">{{ sel.selectionName }}</div>
|
||||
<div class="value">{{ sel.odds }}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.match-header h2 { font-size: 18px; margin-bottom: 4px; }
|
||||
.time { color: var(--text-muted); font-size: 13px; }
|
||||
.market-group h3 { font-size: 14px; margin-bottom: 12px; color: var(--text-muted); }
|
||||
.selections { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user