Files
thebet365/apps/player/src/components/BetSlipDrawer.vue
Mars b5dca1bfb1 feat(player): 完善 H5 投注端与 API 演示数据
- 球赛/串关/优胜冠军、赛事详情、历史投注与个人资料编辑
- 固定顶栏、公告与底栏,仅内容区滚动
- 底部导航与站点 favicon 使用 logo,登录页精简
- API 种子、冠军盘与历史注单增强

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 17:18:11 +08:00

151 lines
5.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useBetSlipStore } from '../stores/betSlip';
import api from '../api';
const props = defineProps<{ modelValue: boolean }>();
const emit = defineEmits<{ 'update:modelValue': [boolean] }>();
const { t } = useI18n();
const slip = useBetSlipStore();
const show = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
});
const loading = ref(false);
const error = ref('');
const success = ref('');
function genId() {
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
async function placeBet() {
if (!slip.items.length) return;
loading.value = true;
error.value = '';
success.value = '';
try {
const requestId = genId();
if (slip.mode === 'parlay' && slip.items.length >= 2) {
if (slip.hasSameMatch) {
error.value = t('bet.parlay_same_match');
return;
}
await api.post('/player/bets/parlay', {
legs: slip.items.map((i) => ({
selectionId: i.selectionId,
oddsVersion: i.oddsVersion,
})),
stake: slip.stake,
requestId,
});
} else if (slip.items.length === 1) {
const item = slip.items[0];
await api.post('/player/bets/single', {
selectionId: item.selectionId,
oddsVersion: item.oddsVersion,
stake: slip.stake,
requestId,
});
} else {
error.value = t('bet.parlay_need_more');
return;
}
success.value = '下注成功!';
slip.clear();
setTimeout(() => { show.value = false; success.value = ''; }, 1500);
} catch (e: unknown) {
error.value = (e as { response?: { data?: { error?: string } } })?.response?.data?.error || '下注失败';
} finally {
loading.value = false;
}
}
</script>
<template>
<div v-if="show" class="overlay" @click.self="show = false">
<div class="drawer">
<div class="drawer-header">
<h3>{{ t('bet.bet_slip') }} <span class="count">({{ slip.count }})</span></h3>
<button class="close-btn" @click="show = false"></button>
</div>
<div v-if="!slip.items.length" class="empty">点击赔率添加投注</div>
<div v-for="item in slip.items" :key="item.selectionId" class="slip-item">
<div class="item-name">{{ item.matchName }}</div>
<div class="item-sel">{{ item.selectionName }} @ <span class="odds">{{ item.odds }}</span></div>
<button class="remove" @click="slip.removeItem(item.selectionId)">移除</button>
</div>
<div v-if="slip.hasSameMatch" class="warn">同场比赛不能串关可作为单关分别投注</div>
<div v-if="slip.items.length" class="stake-area">
<label>{{ t('bet.stake') }}</label>
<input v-model.number="slip.stake" type="number" min="1" />
<div v-if="slip.isParlay" class="mode-tag">{{ t('bet.parlay') }} · 赔率 {{ slip.totalOdds.toFixed(2) }}</div>
<div class="return">预计返还: <strong>{{ slip.potentialReturn.toFixed(2) }}</strong></div>
</div>
<p v-if="error" class="error">{{ error }}</p>
<p v-if="success" class="success">{{ success }}</p>
<button class="btn-primary" :disabled="loading || !slip.items.length" @click="placeBet">
{{ t('bet.place_bet') }}
</button>
</div>
</div>
</template>
<style scoped>
.overlay {
position: fixed; inset: 0;
background: rgba(0, 0, 0, 0.75);
z-index: 200;
display: flex; align-items: flex-end;
}
.drawer {
background: linear-gradient(180deg, #222 0%, #141414 100%);
width: 100%;
max-height: 80vh;
border-radius: 20px 20px 0 0;
padding: 20px 16px calc(20px + env(safe-area-inset-bottom, 0));
overflow-y: auto;
border-top: 1px solid var(--border);
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.35);
}
.drawer-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.drawer-header h3 { font-size: 18px; font-weight: 800; letter-spacing: 0.04em; }
.count { color: var(--primary-light); font-size: 20px; font-weight: 800; }
.close-btn { background: none; color: var(--text-muted); font-size: 22px; padding: 4px; font-weight: 700; }
.slip-item {
padding: 14px;
background: #111;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
margin-bottom: 10px;
}
.item-name { font-size: 14px; font-weight: 800; }
.item-sel { font-size: 13px; color: var(--text-muted); margin-top: 6px; font-weight: 500; }
.odds { color: var(--primary-light); font-weight: 800; }
.remove { background: none; color: var(--danger); font-size: 13px; margin-top: 8px; font-weight: 700; }
.stake-area { margin: 18px 0; }
.stake-area label { font-size: 13px; color: var(--primary-light); display: block; margin-bottom: 8px; font-weight: 700; letter-spacing: 0.04em; }
.return { font-size: 15px; color: var(--text-muted); margin-top: 12px; font-weight: 600; }
.return strong {
color: var(--primary-light);
font-size: 22px;
font-weight: 800;
text-shadow: none;
}
.mode-tag { font-size: 13px; color: var(--primary-light); font-weight: 700; margin-top: 8px; }
.warn { color: var(--primary-light); font-size: 13px; margin-bottom: 10px; font-weight: 600; }
.error { color: var(--danger); font-size: 14px; margin-bottom: 8px; font-weight: 600; }
.success { color: var(--primary-light); font-size: 14px; font-weight: 700; margin-bottom: 8px; }
.empty { text-align: center; color: var(--text-muted); padding: 32px; font-weight: 600; }
</style>