feat(admin,api,player): 结算预览分页、统计图表与返水限额
完善结算计算与预览 API(含后端分页),加强管理端结算/返水/权限,并优化玩家端投注单与队徽展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,19 +30,11 @@ async function placeBet() {
|
||||
success.value = '';
|
||||
|
||||
try {
|
||||
if (slip.mode === 'parlay' && slip.items.length >= PARLAY_MIN_LEGS) {
|
||||
if (slip.hasSameMatch) {
|
||||
error.value = t('bet.parlay_same_match');
|
||||
return;
|
||||
}
|
||||
if (slip.canPlaceParlay) {
|
||||
if (slip.items.length > PARLAY_MAX_LEGS) {
|
||||
error.value = t('bet.parlay_max_legs');
|
||||
return;
|
||||
}
|
||||
if (!slip.canPlaceParlay) {
|
||||
error.value = t('bet.parlay_need_more');
|
||||
return;
|
||||
}
|
||||
await api.post('/player/bets/parlay', {
|
||||
legs: slip.items.map((i) => ({
|
||||
selectionId: i.selectionId,
|
||||
@@ -51,15 +43,17 @@ async function placeBet() {
|
||||
stake: slip.stake,
|
||||
requestId: genId(),
|
||||
});
|
||||
} 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: genId(),
|
||||
});
|
||||
} else if (slip.canPlaceBatchSingles) {
|
||||
for (const item of slip.items) {
|
||||
await api.post('/player/bets/single', {
|
||||
selectionId: item.selectionId,
|
||||
oddsVersion: item.oddsVersion,
|
||||
stake: slip.stake,
|
||||
requestId: genId(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
error.value = t('bet.parlay_need_more');
|
||||
return;
|
||||
}
|
||||
success.value = t('bet.place_success');
|
||||
@@ -100,12 +94,19 @@ async function placeBet() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="slip.isParlay" class="mode-hint mode-hint--parlay">
|
||||
<p v-if="slip.canPlaceParlay" class="mode-hint mode-hint--parlay">
|
||||
{{ t('bet.parlay') }} · {{ t('bet.slip_parlay_odds', { odds: slip.totalOdds.toFixed(2) }) }}
|
||||
</p>
|
||||
<p v-else-if="slip.canPlaceBatchSingles && slip.count > 1" class="mode-hint">
|
||||
{{ t('bet.slip_singles_hint', { n: slip.count }) }}
|
||||
</p>
|
||||
|
||||
<div v-if="slip.items.length" class="stake-area">
|
||||
<label>{{ t('bet.stake') }}</label>
|
||||
<label>{{
|
||||
slip.canPlaceBatchSingles && slip.count > 1
|
||||
? t('bet.slip_stake_per_bet')
|
||||
: t('bet.stake')
|
||||
}}</label>
|
||||
<input v-model.number="slip.stake" type="number" min="1" />
|
||||
<div class="return">
|
||||
{{ t('bet.slip_est_return') }}:
|
||||
@@ -119,7 +120,7 @@ async function placeBet() {
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary"
|
||||
:disabled="loading || !slip.items.length || (slip.mode === 'parlay' && !slip.canPlaceParlay)"
|
||||
:disabled="loading || !slip.canSubmit"
|
||||
@click="placeBet"
|
||||
>
|
||||
{{ loading ? t('bet.placing') : t('bet.place_bet') }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { teamFlagUrl } from '../utils/teamFlag';
|
||||
import TeamEmblem from './TeamEmblem.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
match: {
|
||||
@@ -31,12 +31,6 @@ const kickoff = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const homeFlag = computed(() =>
|
||||
teamFlagUrl(props.match.homeTeamCode, props.match.homeTeamName, props.match.homeTeamLogoUrl),
|
||||
);
|
||||
const awayFlag = computed(() =>
|
||||
teamFlagUrl(props.match.awayTeamCode, props.match.awayTeamName, props.match.awayTeamLogoUrl),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -44,12 +38,22 @@ const awayFlag = computed(() =>
|
||||
<div class="kickoff">{{ kickoff }}</div>
|
||||
<div class="teams-stack">
|
||||
<div class="side">
|
||||
<img v-if="homeFlag" :src="homeFlag" alt="" class="flag" />
|
||||
<TeamEmblem
|
||||
size="sm"
|
||||
:team-code="match.homeTeamCode"
|
||||
:team-name="match.homeTeamName"
|
||||
:logo-url="match.homeTeamLogoUrl"
|
||||
/>
|
||||
<span class="name">{{ match.homeTeamName }}</span>
|
||||
</div>
|
||||
<span class="vs">VS</span>
|
||||
<div class="side">
|
||||
<img v-if="awayFlag" :src="awayFlag" alt="" class="flag" />
|
||||
<TeamEmblem
|
||||
size="sm"
|
||||
:team-code="match.awayTeamCode"
|
||||
:team-name="match.awayTeamName"
|
||||
:logo-url="match.awayTeamLogoUrl"
|
||||
/>
|
||||
<span class="name">{{ match.awayTeamName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,14 +102,6 @@ const awayFlag = computed(() =>
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.flag {
|
||||
width: 22px;
|
||||
height: 15px;
|
||||
object-fit: cover;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 100%;
|
||||
font-size: 10px;
|
||||
|
||||
121
apps/player/src/components/TeamEmblem.vue
Normal file
121
apps/player/src/components/TeamEmblem.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { teamFlagUrl } from '../utils/teamFlag';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
teamCode?: string;
|
||||
teamName?: string;
|
||||
logoUrl?: string | null;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}>(),
|
||||
{ size: 'md' },
|
||||
);
|
||||
|
||||
const useCustomLogo = computed(() => Boolean(props.logoUrl?.trim()));
|
||||
const src = computed(() =>
|
||||
teamFlagUrl(props.teamCode, props.teamName, props.logoUrl),
|
||||
);
|
||||
const failed = ref(false);
|
||||
|
||||
function onError() {
|
||||
failed.value = true;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.teamCode, props.teamName, props.logoUrl] as const,
|
||||
() => {
|
||||
failed.value = false;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
v-if="src && !failed"
|
||||
:src="src"
|
||||
alt=""
|
||||
class="team-emblem"
|
||||
:class="[`team-emblem--${size}`, { 'team-emblem--logo': useCustomLogo }]"
|
||||
loading="lazy"
|
||||
@error="onError"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="team-emblem team-emblem--placeholder"
|
||||
:class="`team-emblem--${size}`"
|
||||
aria-hidden="true"
|
||||
>⚽</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.team-emblem {
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.team-emblem--sm {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.team-emblem--md {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.team-emblem--lg {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
/* 国旗:横向比例 + 铺满 */
|
||||
.team-emblem:not(.team-emblem--logo) {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.team-emblem--sm:not(.team-emblem--logo) {
|
||||
width: 28px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.team-emblem--md:not(.team-emblem--logo) {
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.team-emblem--lg:not(.team-emblem--logo) {
|
||||
width: 54px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
/* 队徽:正方形容器 + 完整显示 */
|
||||
.team-emblem--logo {
|
||||
object-fit: contain;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.team-emblem--placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.55;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.team-emblem--sm.team-emblem--placeholder {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.team-emblem--md.team-emblem--placeholder {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.team-emblem--lg.team-emblem--placeholder {
|
||||
font-size: 22px;
|
||||
}
|
||||
</style>
|
||||
@@ -159,6 +159,10 @@ function openSlip() {
|
||||
}
|
||||
|
||||
const showParlayFoot = computed(() => slip.mode === 'parlay' && slip.count > 0);
|
||||
|
||||
const footConfirmLabel = computed(() =>
|
||||
slip.canPlaceParlay ? t('bet.parlay_confirm_parlay') : t('bet.cs_confirm_cell'),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -247,7 +251,7 @@ const showParlayFoot = computed(() => slip.mode === 'parlay' && slip.count > 0);
|
||||
:disabled="!slip.canPlaceParlay"
|
||||
@click="openSlip"
|
||||
>
|
||||
{{ t('bet.cs_confirm_cell') }}
|
||||
{{ footConfirmLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
@@ -290,10 +294,15 @@ const showParlayFoot = computed(() => slip.mode === 'parlay' && slip.count > 0);
|
||||
|
||||
.foot-hint--warn {
|
||||
color: var(--danger);
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.foot-hint--info {
|
||||
color: var(--primary-light);
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.foot-meta {
|
||||
color: var(--primary-light);
|
||||
font-weight: 600;
|
||||
|
||||
@@ -105,7 +105,7 @@ const i18n = createI18n({
|
||||
parlay_guide_help: '查看串关说明',
|
||||
parlay_desc: '选择 2–5 场赛前赛事组合串关(2 串 1 至 5 串 1)。赔率相乘,不含滚球、冠军盘与四分盘让球/大小。',
|
||||
parlay_guide_1: '在列表中点击各场赔率,选中项显示金边;再点同一项可取消',
|
||||
parlay_guide_2: '须选 2–5 场不同赛事,不可同场串关;冠军盘与四分盘让球/大小不可选',
|
||||
parlay_guide_2: '须选 2–5 项(可同场多项);冠军盘与四分盘让球/大小不可选',
|
||||
parlay_guide_3: '选好后点底部「确认下单」打开投注单,填写金额并提交',
|
||||
parlay_max_legs: '串关最多 5 项',
|
||||
parlay_block_outright: '冠军盘不可串关',
|
||||
@@ -114,6 +114,9 @@ const i18n = createI18n({
|
||||
parlay_filter_all: '全部',
|
||||
parlay_empty: '暂无可用串关赛事',
|
||||
parlay_same_match: '同一场比赛不能串关',
|
||||
parlay_same_match_singles: '已选 {n} 项,将分 {n} 笔单关下单',
|
||||
parlay_confirm_singles: '确认下单({n}笔单关)',
|
||||
parlay_confirm_parlay: '确认串关下单',
|
||||
parlay_need_more: '请至少选择 2 项进行串关',
|
||||
back: '返回',
|
||||
refresh: '刷新',
|
||||
@@ -229,7 +232,7 @@ const i18n = createI18n({
|
||||
password_disabled: '当前账号不允许自行修改密码,请联系客服',
|
||||
rules_title: '投注规则',
|
||||
rules_p1: '本平台第一版仅支持足球赛前盘,不含滚球、Cash Out、改单及系统串关。',
|
||||
rules_p2: '串关为 2 串 1 至 5 串 1,不可同场串关;冠军盘、四分盘让球/大小不可进入串关。',
|
||||
rules_p2: '串关为 2 串 1 至 5 串 1,同场可多选;冠军盘、四分盘让球/大小不可进入串关。',
|
||||
rules_p3: '赛果由平台根据官方录入的半场/全场比分结算,结算预览经确认后入账。',
|
||||
rules_p4: '若本说明与后台公告冲突,以最新公告及实际盘口规则为准。',
|
||||
rules_p5: '操作步骤:进入任意赛事详情,点右上角「?」查看玩法说明。',
|
||||
@@ -330,7 +333,7 @@ const i18n = createI18n({
|
||||
parlay_guide_help: 'Parlay help',
|
||||
parlay_desc: 'Combine 2–5 pre-match legs (2-fold to 5-fold). No live, outright, or quarter-ball HDP/O-U in parlay.',
|
||||
parlay_guide_1: 'Tap odds in the list; selected cells show a gold border. Tap again to remove',
|
||||
parlay_guide_2: 'Pick 2–5 different matches only. No same match, outright, or quarter-ball HDP/O-U',
|
||||
parlay_guide_2: 'Pick 2–5 legs (same match allowed). No outright or quarter-ball HDP/O-U',
|
||||
parlay_guide_3: 'Tap Confirm order at the bottom, enter stake in the bet slip, and submit',
|
||||
parlay_max_legs: 'Parlay allows up to 5 legs',
|
||||
parlay_block_outright: 'Outright cannot be parlayed',
|
||||
@@ -339,6 +342,9 @@ const i18n = createI18n({
|
||||
parlay_filter_all: 'All',
|
||||
parlay_empty: 'No matches available for parlay betting',
|
||||
parlay_same_match: 'Cannot parlay selections from the same match',
|
||||
parlay_same_match_singles: '{n} selection(s) → {n} separate single bet(s)',
|
||||
parlay_confirm_singles: 'Place {n} single bet(s)',
|
||||
parlay_confirm_parlay: 'Place parlay',
|
||||
parlay_need_more: 'Select at least 2 legs for parlay',
|
||||
back: 'Back',
|
||||
refresh: 'Refresh',
|
||||
@@ -454,7 +460,7 @@ const i18n = createI18n({
|
||||
password_disabled: 'Password change is disabled for this account; contact support',
|
||||
rules_title: 'Betting Rules',
|
||||
rules_p1: 'Football pre-match only in v1. No live betting, Cash Out, bet edits, or system parlays.',
|
||||
rules_p2: 'Parlays: 2–5 legs, different matches only. Outright and quarter-ball HDP/O-U are excluded from parlays.',
|
||||
rules_p2: 'Parlays: 2–5 legs, same-match multi-select allowed. Outright and quarter-ball HDP/O-U are excluded.',
|
||||
rules_p3: 'Results use admin-entered half-time and full-time scores; payouts apply after settlement preview is confirmed.',
|
||||
rules_p4: 'If this text conflicts with site notices, the latest notice and market rules prevail.',
|
||||
rules_p5: 'How to bet: open any match, tap the ? icon on the top right.',
|
||||
@@ -561,7 +567,7 @@ const i18n = createI18n({
|
||||
parlay_guide_help: 'Bantuan parlay',
|
||||
parlay_desc: 'Gabung 2–5 perlawanan pra-perlawanan (2 hingga 5 liputan). Tiada live, outright atau suku bola HDP/O-U.',
|
||||
parlay_guide_1: 'Ketik odds dalam senarai; pilihan dipilih ada sempadan emas. Ketik lagi untuk batal',
|
||||
parlay_guide_2: 'Pilih 2–5 perlawanan berbeza. Tiada perlawanan sama, outright atau suku bola HDP/O-U',
|
||||
parlay_guide_2: 'Pilih 2–5 pilihan (boleh perlawanan sama). Tiada outright atau suku bola HDP/O-U',
|
||||
parlay_guide_3: 'Ketik Sahkan pesanan di bawah, isi pegangan dalam slip, dan hantar',
|
||||
parlay_max_legs: 'Maksimum 5 pilihan parlay',
|
||||
parlay_block_outright: 'Outright tidak boleh parlay',
|
||||
@@ -570,6 +576,9 @@ const i18n = createI18n({
|
||||
parlay_filter_all: 'Semua',
|
||||
parlay_empty: 'Tiada perlawanan untuk pertaruhan berganda',
|
||||
parlay_same_match: 'Perlawanan sama tidak boleh berganda',
|
||||
parlay_same_match_singles: '{n} pilihan → {n} pertaruhan tunggal berasingan',
|
||||
parlay_confirm_singles: 'Sahkan {n} pertaruhan tunggal',
|
||||
parlay_confirm_parlay: 'Sahkan parlay',
|
||||
parlay_need_more: 'Pilih sekurang-kurangnya 2 pilihan',
|
||||
back: 'Kembali',
|
||||
refresh: 'Muat semula',
|
||||
@@ -685,7 +694,7 @@ const i18n = createI18n({
|
||||
password_disabled: 'Akaun ini tidak dibenarkan tukar kata laluan; hubungi sokongan',
|
||||
rules_title: 'Peraturan Pertaruhan',
|
||||
rules_p1: 'Versi pertama: hanya bola sepak pra-perlawanan. Tiada live, Cash Out, edit pertaruhan atau parlay sistem.',
|
||||
rules_p2: 'Parlay 2–5 perlawanan, bukan perlawanan sama. Outright dan suku bola HDP/O-U tidak boleh parlay.',
|
||||
rules_p2: 'Parlay 2–5 pilihan, boleh pilih berbilang dari perlawanan sama. Outright dan suku bola HDP/O-U tidak boleh parlay.',
|
||||
rules_p3: 'Keputusan berdasarkan skor separuh masa/penuh yang dimasukkan admin; bayaran selepas pratonton disahkan.',
|
||||
rules_p4: 'Jika bercanggah dengan notis laman, ikut notis terkini dan peraturan pasaran.',
|
||||
rules_p5: 'Langkah operasi: buka butiran perlawanan, ketik ikon ? di atas kanan.',
|
||||
|
||||
@@ -34,7 +34,7 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
/** 球赛/详情页:仅单关,且投注单同时只能有 1 项 */
|
||||
/** 球赛/详情页:同场可多选,分笔单关;再点同一项可取消 */
|
||||
function addItem(item: SlipItem) {
|
||||
if (mode.value === 'parlay') items.value = [];
|
||||
mode.value = 'single';
|
||||
@@ -44,13 +44,13 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
(i) => i.selectionId === item.selectionId,
|
||||
);
|
||||
if (existing >= 0) {
|
||||
items.value = [];
|
||||
items.value.splice(existing, 1);
|
||||
return;
|
||||
}
|
||||
items.value = [item];
|
||||
items.value.push(item);
|
||||
}
|
||||
|
||||
/** 串关页专用:跨场组合,同场只保留一个选项 */
|
||||
/** 串关页:同场/跨场均可多选,合成一张串关单 */
|
||||
function addParlayLeg(item: SlipItem): ParlayRejectReason | 'MAX_LEGS' | null {
|
||||
if (mode.value === 'single') items.value = [];
|
||||
mode.value = 'parlay';
|
||||
@@ -77,7 +77,6 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
return 'MAX_LEGS';
|
||||
}
|
||||
|
||||
items.value = items.value.filter((i) => i.matchId !== item.matchId);
|
||||
items.value.push(item);
|
||||
lastParlayError.value = null;
|
||||
return null;
|
||||
@@ -101,10 +100,10 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
|
||||
const potentialReturn = computed(() => {
|
||||
if (!items.value.length) return 0;
|
||||
if (mode.value === 'parlay' && items.value.length >= PARLAY_MIN_LEGS) {
|
||||
if (mode.value === 'parlay' && canPlaceParlay.value) {
|
||||
return stake.value * totalOdds.value;
|
||||
}
|
||||
return stake.value * items.value[0].odds;
|
||||
return items.value.reduce((acc, i) => acc + stake.value * i.odds, 0);
|
||||
});
|
||||
|
||||
const hasSameMatch = computed(() => {
|
||||
@@ -116,8 +115,16 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
() =>
|
||||
mode.value === 'parlay' &&
|
||||
items.value.length >= PARLAY_MIN_LEGS &&
|
||||
items.value.length <= PARLAY_MAX_LEGS &&
|
||||
!hasSameMatch.value,
|
||||
items.value.length <= PARLAY_MAX_LEGS,
|
||||
);
|
||||
|
||||
/** 详情页等同场多笔单关(串关模式不走此路径) */
|
||||
const canPlaceBatchSingles = computed(
|
||||
() => mode.value === 'single' && items.value.length >= 1,
|
||||
);
|
||||
|
||||
const canSubmit = computed(
|
||||
() => canPlaceParlay.value || canPlaceBatchSingles.value,
|
||||
);
|
||||
|
||||
const drawerOpen = ref(false);
|
||||
@@ -140,6 +147,8 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
potentialReturn,
|
||||
hasSameMatch,
|
||||
canPlaceParlay,
|
||||
canPlaceBatchSingles,
|
||||
canSubmit,
|
||||
lastParlayError,
|
||||
drawerOpen,
|
||||
addItem,
|
||||
|
||||
@@ -6,7 +6,7 @@ import vsImg from '../assets/images/vs.png';
|
||||
import cardBg from '../assets/images/卡片.png';
|
||||
import BannerCarousel from '../components/BannerCarousel.vue';
|
||||
import { usePlayerHome } from '../composables/usePlayerHome';
|
||||
import { teamFlagUrl } from '../utils/teamFlag';
|
||||
import TeamEmblem from '../components/TeamEmblem.vue';
|
||||
|
||||
const matchCardBg = `url(${cardBg})`;
|
||||
const { t, locale } = useI18n();
|
||||
@@ -28,13 +28,6 @@ function formatKickoff(startTime: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function homeFlag(match: (typeof hotMatches.value)[number]) {
|
||||
return teamFlagUrl(match.homeTeamCode, match.homeTeamName);
|
||||
}
|
||||
|
||||
function awayFlag(match: (typeof hotMatches.value)[number]) {
|
||||
return teamFlagUrl(match.awayTeamCode, match.awayTeamName);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -53,8 +46,12 @@ function awayFlag(match: (typeof hotMatches.value)[number]) {
|
||||
<div class="match-time">{{ formatKickoff(match.startTime) }}</div>
|
||||
</div>
|
||||
<div class="match-flags" aria-hidden="true">
|
||||
<img v-if="homeFlag(match)" :src="homeFlag(match)" alt="" class="flag" />
|
||||
<span v-else class="flag-ph">⚽</span>
|
||||
<TeamEmblem
|
||||
size="md"
|
||||
:team-code="match.homeTeamCode"
|
||||
:team-name="match.homeTeamName"
|
||||
:logo-url="match.homeTeamLogoUrl"
|
||||
/>
|
||||
<div class="vs-arena">
|
||||
<svg class="hz-lightning" viewBox="0 0 72 28" aria-hidden="true">
|
||||
<defs>
|
||||
@@ -80,8 +77,12 @@ function awayFlag(match: (typeof hotMatches.value)[number]) {
|
||||
<span class="hz-beam" aria-hidden="true" />
|
||||
<img :src="vsImg" alt="" class="vs-img" />
|
||||
</div>
|
||||
<img v-if="awayFlag(match)" :src="awayFlag(match)" alt="" class="flag" />
|
||||
<span v-else class="flag-ph">⚽</span>
|
||||
<TeamEmblem
|
||||
size="md"
|
||||
:team-code="match.awayTeamCode"
|
||||
:team-name="match.awayTeamName"
|
||||
:logo-url="match.awayTeamLogoUrl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -155,32 +156,15 @@ function awayFlag(match: (typeof hotMatches.value)[number]) {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.flag {
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.flag-ph {
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
opacity: 0.45;
|
||||
gap: 4px;
|
||||
max-width: 46%;
|
||||
}
|
||||
|
||||
.vs-arena {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 72px;
|
||||
height: 58px;
|
||||
width: 64px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -240,7 +224,7 @@ function awayFlag(match: (typeof hotMatches.value)[number]) {
|
||||
.vs-img {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 58px;
|
||||
width: 48px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
animation: vs-glow 2.4s ease-in-out infinite;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api from '../api';
|
||||
import { useBetSlipStore } from '../stores/betSlip';
|
||||
import { teamFlagUrl } from '../utils/teamFlag';
|
||||
import TeamEmblem from '../components/TeamEmblem.vue';
|
||||
import { DETAIL_MARKET_TYPES, MARKET_I18N_KEY } from '../utils/marketCatalog';
|
||||
import MatchBetGuide from '../components/match-detail/MatchBetGuide.vue';
|
||||
import MarketTypeTile from '../components/match-detail/MarketTypeTile.vue';
|
||||
@@ -75,13 +75,6 @@ const marketsByType = computed(() => {
|
||||
return map;
|
||||
});
|
||||
|
||||
const homeFlag = computed(() =>
|
||||
teamFlagUrl(match.value?.homeTeamCode, match.value?.homeTeamName, match.value?.homeTeamLogoUrl),
|
||||
);
|
||||
const awayFlag = computed(() =>
|
||||
teamFlagUrl(match.value?.awayTeamCode, match.value?.awayTeamName, match.value?.awayTeamLogoUrl),
|
||||
);
|
||||
|
||||
function marketPromoLabel(marketType: string) {
|
||||
const m = marketsByType.value.get(marketType);
|
||||
return m?.promoLabel?.trim() || '';
|
||||
@@ -249,11 +242,12 @@ function openBetSlipDrawer() {
|
||||
slip.openDrawer();
|
||||
}
|
||||
|
||||
/** 当前玩法是否已选入投注单(单关、仅一项) */
|
||||
/** 当前玩法是否已有选项加入投注单 */
|
||||
function hasSlipPickForMarket(marketType: string) {
|
||||
if (!match.value || slip.mode !== 'single' || slip.items.length !== 1) return false;
|
||||
const item = slip.items[0];
|
||||
return item.matchId === match.value.id && item.marketType === marketType;
|
||||
if (!match.value || slip.mode !== 'single') return false;
|
||||
return slip.items.some(
|
||||
(item) => item.matchId === match.value!.id && item.marketType === marketType,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -284,8 +278,12 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
<div class="hero-teams">
|
||||
<!-- home -->
|
||||
<div class="hero-team">
|
||||
<img v-if="homeFlag" :src="homeFlag" alt="" class="hero-flag" />
|
||||
<span v-else class="hero-flag-ph">⚽</span>
|
||||
<TeamEmblem
|
||||
size="lg"
|
||||
:team-code="match.homeTeamCode"
|
||||
:team-name="match.homeTeamName"
|
||||
:logo-url="match.homeTeamLogoUrl"
|
||||
/>
|
||||
<span class="hero-name">{{ match.homeTeamName }}</span>
|
||||
</div>
|
||||
|
||||
@@ -318,8 +316,12 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
|
||||
<!-- away -->
|
||||
<div class="hero-team">
|
||||
<img v-if="awayFlag" :src="awayFlag" alt="" class="hero-flag" />
|
||||
<span v-else class="hero-flag-ph">⚽</span>
|
||||
<TeamEmblem
|
||||
size="lg"
|
||||
:team-code="match.awayTeamCode"
|
||||
:team-name="match.awayTeamName"
|
||||
:logo-url="match.awayTeamLogoUrl"
|
||||
/>
|
||||
<span class="hero-name">{{ match.awayTeamName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -487,24 +489,6 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero-flag {
|
||||
width: 54px;
|
||||
height: 36px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.hero-flag-ph {
|
||||
width: 54px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.hero-name {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
|
||||
Reference in New Issue
Block a user