feat(player): 完善 H5 投注端与 API 演示数据

- 球赛/串关/优胜冠军、赛事详情、历史投注与个人资料编辑
- 固定顶栏、公告与底栏,仅内容区滚动
- 底部导航与站点 favicon 使用 logo,登录页精简
- API 种子、冠军盘与历史注单增强

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-02 17:18:11 +08:00
parent 7af2e418c3
commit b5dca1bfb1
75 changed files with 7077 additions and 384 deletions

View File

@@ -0,0 +1,169 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { groupCorrectScoreSelections, type CsSelection } from '../../utils/correctScoreLayout';
const props = defineProps<{
marketType: string;
homeTeamName: string;
awayTeamName: string;
selections: Array<{
id: string;
selectionCode: string;
selectionName: string;
odds: string;
oddsVersion: string;
}>;
stakes: Record<string, number>;
}>();
const emit = defineEmits<{
'update:stakes': [Record<string, number>];
}>();
const { t } = useI18n();
const columns = computed(() => groupCorrectScoreSelections(props.selections, props.marketType));
function setStake(sel: CsSelection, raw: string) {
const n = Math.max(0, Number(raw) || 0);
emit('update:stakes', { ...props.stakes, [sel.id]: n });
}
function formatOdds(odds: string) {
const n = parseFloat(odds);
return Number.isFinite(n) ? n.toFixed(2) : odds;
}
</script>
<template>
<div class="cs-panel">
<div class="teams-bar">
<div class="team-box">{{ homeTeamName }}</div>
<div class="team-box">{{ awayTeamName }}</div>
</div>
<div class="cols-head">
<span>{{ t('bet.col_home') }}</span>
<span>{{ t('bet.col_draw') }}</span>
<span>{{ t('bet.col_away') }}</span>
</div>
<div class="cols-grid">
<div v-for="colKey in ['home', 'draw', 'away'] as const" :key="colKey" class="col">
<div
v-for="sel in columns[colKey]"
:key="sel.id"
class="score-card"
>
<div class="score-line">{{ sel.scoreDisplay }}</div>
<div class="odds-box">{{ formatOdds(sel.odds) }}</div>
<input
type="number"
class="stake-input"
min="0"
step="1"
inputmode="decimal"
:value="stakes[sel.id] ?? 0"
@input="setStake(sel, ($event.target as HTMLInputElement).value)"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.cs-panel {
padding: 0 10px 14px;
}
.teams-bar {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 12px;
}
.team-box {
padding: 10px 8px;
text-align: center;
font-size: 14px;
font-weight: 800;
color: var(--primary-light);
background: #0d0d0d;
border: 1px solid #333;
border-radius: 4px;
}
.cols-head {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
margin-bottom: 8px;
text-align: center;
font-size: 13px;
font-weight: 800;
color: var(--primary-light);
}
.cols-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
align-items: start;
}
.col {
display: flex;
flex-direction: column;
gap: 8px;
}
.score-card {
background: #1c1c1c;
border: 1px solid #333;
border-radius: 4px;
padding: 8px 6px 6px;
text-align: center;
}
.score-line {
font-size: 15px;
font-weight: 800;
color: var(--text);
margin-bottom: 6px;
}
.odds-box {
display: inline-block;
min-width: 52px;
padding: 3px 8px;
margin-bottom: 6px;
border: 1px solid var(--primary);
border-radius: 3px;
font-size: 12px;
font-weight: 800;
color: var(--primary-light);
background: rgba(212, 175, 55, 0.08);
}
.stake-input {
width: 100%;
padding: 6px 4px;
border-radius: 3px;
border: none;
background: #f2f2f2;
color: #111;
font-size: 14px;
font-weight: 700;
text-align: center;
-moz-appearance: textfield;
}
.stake-input::-webkit-outer-spin-button,
.stake-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>

View File

@@ -0,0 +1,160 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const props = defineProps<{
label: string;
expanded: boolean;
hasMarket: boolean;
rewardActive?: boolean;
}>();
const emit = defineEmits<{
bet: [];
expand: [];
collapse: [];
}>();
const { t } = useI18n();
function onBet() {
if (!props.hasMarket) return;
emit('bet');
}
function onExpand() {
if (!props.hasMarket) return;
emit('expand');
}
function onCollapse() {
emit('collapse');
}
</script>
<template>
<div class="featured-wrap" :class="{ expanded, disabled: !hasMarket }">
<div class="featured-row">
<div class="featured-main">
<span class="market-name">{{ label }}</span>
<span v-if="rewardActive && hasMarket" class="reward-badge">
🏆 {{ t('bet.reward_active') }}
</span>
<span v-else-if="!hasMarket" class="closed-tag">{{ t('bet.market_closed') }}</span>
</div>
<div class="featured-actions">
<button type="button" class="btn-bet btn-gold-outline" :disabled="!hasMarket" @click="onBet">
{{ t('bet.place_bet_short') }}
</button>
<button
type="button"
class="btn-icon btn-gold-outline"
:disabled="!hasMarket"
:aria-label="expanded ? t('bet.collapse_market') : t('bet.expand_market')"
@click="expanded ? onCollapse() : onExpand()"
>
{{ expanded ? '' : '+' }}
</button>
<button
type="button"
class="btn-icon btn-gold-outline"
:aria-label="t('bet.collapse_market')"
@click="onCollapse"
>
×
</button>
</div>
</div>
<div v-if="expanded && hasMarket" class="panel-slot">
<slot />
</div>
</div>
</template>
<style scoped>
.featured-wrap {
margin-bottom: 10px;
border-radius: 6px;
border: 1px solid #3a3218;
background: #121212;
box-shadow: 0 0 0 1px rgba(212, 175, 55, 0.12);
}
.featured-wrap.expanded {
box-shadow:
0 0 0 1px rgba(212, 175, 55, 0.35),
0 0 12px rgba(212, 175, 55, 0.15);
}
.featured-wrap.disabled {
opacity: 0.55;
}
.featured-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 10px;
}
.featured-main {
flex: 1;
min-width: 0;
}
.market-name {
display: block;
font-size: 15px;
font-weight: 800;
color: var(--primary-light);
margin-bottom: 4px;
}
.reward-badge {
display: inline-block;
font-size: 11px;
font-weight: 700;
color: #e8c84a;
background: rgba(212, 175, 55, 0.12);
padding: 2px 8px;
border-radius: 4px;
}
.closed-tag {
font-size: 11px;
color: var(--text-muted);
}
.featured-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.btn-bet {
padding: 8px 14px;
border-radius: 6px;
font-size: 13px;
}
.btn-bet:disabled {
opacity: 0.45;
}
.btn-icon {
width: 32px;
height: 32px;
border-radius: 6px;
font-size: 18px;
font-weight: 900;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon:disabled {
opacity: 0.45;
}
</style>

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
defineProps<{
selections: {
id: string;
selectionName: string;
odds: string;
}[];
isSelected: (id: string) => boolean;
}>();
const emit = defineEmits<{ pick: [id: string] }>();
</script>
<template>
<div class="panel">
<button
v-for="sel in selections"
:key="sel.id"
type="button"
class="odds-btn"
:class="{ selected: isSelected(sel.id) }"
@click="emit('pick', sel.id)"
>
<span class="label">{{ sel.selectionName }}</span>
<span class="odds">{{ sel.odds }}</span>
</button>
</div>
</template>
<style scoped>
.panel {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0 10px 12px;
}
.odds-btn {
min-width: calc(33.33% - 6px);
flex: 1 1 calc(33.33% - 6px);
max-width: calc(50% - 4px);
padding: 10px 8px;
border-radius: 6px;
background: #0d0d0d;
border: 1px solid #333;
text-align: center;
}
.odds-btn.selected {
border-color: var(--primary);
background: rgba(212, 175, 55, 0.12);
}
.label {
display: block;
font-size: 11px;
color: var(--text-muted);
margin-bottom: 4px;
}
.odds {
font-size: 15px;
font-weight: 800;
color: var(--primary-light);
}
</style>

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
defineProps<{
label: string;
active: boolean;
hasMarket: boolean;
}>();
const emit = defineEmits<{ expand: []; collapse: [] }>();
const { t } = useI18n();
</script>
<template>
<div class="tile" :class="{ active, disabled: !hasMarket }">
<span class="tile-label">{{ label }}</span>
<div class="tile-actions">
<button
type="button"
class="btn-icon btn-gold-outline"
:disabled="!hasMarket"
:aria-label="t('bet.expand_market')"
@click="emit('expand')"
>
+
</button>
<button
type="button"
class="btn-icon btn-gold-outline"
:aria-label="t('bet.collapse_market')"
@click="emit('collapse')"
>
×
</button>
</div>
</div>
</template>
<style scoped>
.tile {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
padding: 12px 10px;
background: #141414;
border: 1px solid #2a2a2a;
border-radius: 6px;
min-height: 48px;
}
.tile.active {
border-color: var(--primary);
box-shadow: 0 0 8px rgba(212, 175, 55, 0.2);
}
.tile.disabled {
opacity: 0.5;
}
.tile-label {
font-size: 12px;
font-weight: 800;
color: var(--primary-light);
line-height: 1.25;
flex: 1;
min-width: 0;
}
.tile-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.btn-icon {
width: 26px;
height: 26px;
border-radius: 5px;
font-size: 16px;
font-weight: 900;
line-height: 1;
}
</style>