feat(admin): 管理端列表分页、控制台图表与赛事导入
- 玩家/代理/赛事/注单/审计列表分页,默认每页 10 条,无页面滚动条布局 - ECharts 控制台概览、注单管理中文化与列宽优化 - zhibo 赛事字段迁移与导入,玩家编辑可改所属代理 - 管理端 API 分页与 dashboard 统计接口 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BIN
apps/player/src/assets/images/足球赛事下注背景框.png
Normal file
BIN
apps/player/src/assets/images/足球赛事下注背景框.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
@@ -3,6 +3,7 @@ import { ref, computed, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api from '../../api';
|
||||
import { formatMoney, parseAmount } from '../../utils/localeDisplay';
|
||||
import { teamFlagUrl } from '../../utils/teamFlag';
|
||||
|
||||
export interface OutrightPick {
|
||||
selectionId: string;
|
||||
@@ -30,8 +31,26 @@ const balance = ref(0);
|
||||
const successBalance = ref(0);
|
||||
const successStake = ref(0);
|
||||
|
||||
const flagUrl = computed(() =>
|
||||
props.pick ? teamFlagUrl(props.pick.teamCode, props.pick.teamName) : null,
|
||||
);
|
||||
|
||||
const balanceText = computed(() => formatMoney(balance.value, locale.value));
|
||||
|
||||
const oddsNum = computed(() => {
|
||||
if (!props.pick) return 0;
|
||||
const n = parseFloat(props.pick.odds);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
});
|
||||
|
||||
const estReturn = computed(() => {
|
||||
const s = Number(stake.value);
|
||||
if (!s || s <= 0 || !oddsNum.value) return 0;
|
||||
return s * oddsNum.value;
|
||||
});
|
||||
|
||||
const estReturnText = computed(() => formatMoney(estReturn.value, locale.value));
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
async (v) => {
|
||||
@@ -56,8 +75,20 @@ function genRequestId() {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
function setStake(amount: number) {
|
||||
stake.value = Math.max(0.01, Math.round(amount * 100) / 100);
|
||||
}
|
||||
|
||||
function setMaxStake() {
|
||||
if (balance.value > 0) setStake(balance.value);
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!props.pick || stake.value <= 0) return;
|
||||
if (stake.value > balance.value) {
|
||||
error.value = t('bet.outright_insufficient');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
@@ -88,13 +119,26 @@ function formatOdds(odds: string) {
|
||||
|
||||
<template>
|
||||
<div v-if="open && pick" class="overlay" @click.self="close">
|
||||
<div class="modal">
|
||||
<div class="modal" role="dialog" aria-modal="true">
|
||||
<button type="button" class="close-x" :aria-label="t('bet.cancel')" @click="close">✕</button>
|
||||
|
||||
<template v-if="step === 'form'">
|
||||
<h3 class="title">{{ t('bet.outright_enter_stake') }}</h3>
|
||||
<p class="team">{{ pick.teamName }}</p>
|
||||
<span class="odds-badge">{{ formatOdds(pick.odds) }}</span>
|
||||
<p class="balance-line">{{ t('bet.outright_balance') }}:{{ balanceText }}</p>
|
||||
<p class="hint">{{ t('bet.outright_enter_stake') }}</p>
|
||||
|
||||
<div class="hero">
|
||||
<img v-if="flagUrl" :src="flagUrl" alt="" class="flag" />
|
||||
<p class="team">{{ pick.teamName }}</p>
|
||||
<span class="odds-badge">@ {{ formatOdds(pick.odds) }}</span>
|
||||
</div>
|
||||
|
||||
<p class="event-title">{{ pick.eventTitle }}</p>
|
||||
|
||||
<div class="balance-row">
|
||||
<span class="balance-label">{{ t('bet.outright_balance') }}</span>
|
||||
<span class="balance-value">{{ balanceText }}</span>
|
||||
</div>
|
||||
|
||||
<label class="stake-label">{{ t('bet.stake_label') }}</label>
|
||||
<input
|
||||
v-model.number="stake"
|
||||
type="number"
|
||||
@@ -102,32 +146,63 @@ function formatOdds(odds: string) {
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
inputmode="decimal"
|
||||
:placeholder="t('bet.stake_placeholder')"
|
||||
/>
|
||||
|
||||
<div class="quick-stakes">
|
||||
<button type="button" class="chip" @click="setStake(50)">50</button>
|
||||
<button type="button" class="chip" @click="setStake(100)">100</button>
|
||||
<button type="button" class="chip" @click="setStake(500)">500</button>
|
||||
<button type="button" class="chip" @click="setMaxStake">{{ t('bet.stake_max') }}</button>
|
||||
</div>
|
||||
|
||||
<p class="est-return">
|
||||
{{ t('history.est_return') }}:<span class="est-value">{{ estReturnText }}</span>
|
||||
</p>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="btn-confirm" :disabled="loading" @click="submit">
|
||||
{{ t('bet.place_bet_short') }}
|
||||
</button>
|
||||
<button type="button" class="btn-cancel" :disabled="loading" @click="close">
|
||||
{{ t('bet.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-confirm btn-gold-outline"
|
||||
:disabled="loading || stake <= 0"
|
||||
@click="submit"
|
||||
>
|
||||
{{ loading ? t('bet.placing') : t('bet.place_bet_short') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="success-icon">✓</div>
|
||||
<h3 class="title success-title">{{ t('bet.outright_success') }}</h3>
|
||||
<p class="team">{{ pick.teamName }}</p>
|
||||
<span class="odds-badge">{{ formatOdds(pick.odds) }}</span>
|
||||
<p class="balance-line">
|
||||
{{ t('bet.outright_stake_amount') }} : {{ successStake.toFixed(2) }}
|
||||
</p>
|
||||
<p class="balance-line">
|
||||
{{ t('bet.outright_balance') }} : {{ formatMoney(successBalance, locale) }}
|
||||
</p>
|
||||
<h3 class="success-title">{{ t('bet.outright_success') }}</h3>
|
||||
|
||||
<div class="hero hero--compact">
|
||||
<img v-if="flagUrl" :src="flagUrl" alt="" class="flag" />
|
||||
<p class="team">{{ pick.teamName }}</p>
|
||||
<span class="odds-badge">@ {{ formatOdds(pick.odds) }}</span>
|
||||
</div>
|
||||
|
||||
<p class="event-title">{{ pick.eventTitle }}</p>
|
||||
<button type="button" class="btn-share" disabled>Share</button>
|
||||
<button type="button" class="btn-done" @click="close">{{ t('bet.outright_done') }}</button>
|
||||
|
||||
<div class="summary">
|
||||
<div class="summary-row">
|
||||
<span>{{ t('bet.outright_stake_amount') }}</span>
|
||||
<span>{{ formatMoney(successStake, locale) }}</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span>{{ t('bet.outright_balance') }}</span>
|
||||
<span class="balance-value">{{ formatMoney(successBalance, locale) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-done btn-gold-outline" @click="close">
|
||||
{{ t('bet.outright_done') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,137 +213,266 @@ function formatOdds(odds: string) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 20px 18px 16px;
|
||||
max-width: 300px;
|
||||
background: linear-gradient(165deg, #1a1810 0%, #121212 45%, #0a0a0a 100%);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px 16px 16px;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
box-shadow: var(--shadow), 0 0 24px rgba(212, 175, 55, 0.08);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: #b8942b;
|
||||
.close-x {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
margin-top: 8px;
|
||||
.hero--compact {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.flag {
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.team {
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
color: #c9a227;
|
||||
margin-bottom: 8px;
|
||||
color: var(--primary-light);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.odds-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
background: #c62828;
|
||||
padding: 3px 10px;
|
||||
background: rgba(198, 40, 40, 0.85);
|
||||
border: 1px solid rgba(255, 120, 120, 0.35);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.balance-line {
|
||||
font-size: 13px;
|
||||
color: #444;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.balance-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 12px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin: 8px 0 14px;
|
||||
line-height: 1.35;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.stake-label {
|
||||
display: block;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.stake-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #5b9bd5;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
padding: 11px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #0a0a0a;
|
||||
color: var(--text);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.stake-input:focus {
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: 0 0 0 1px rgba(212, 175, 55, 0.15);
|
||||
}
|
||||
|
||||
.stake-input::placeholder {
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.quick-stakes {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
flex: 1;
|
||||
padding: 6px 4px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #141414;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.chip:active {
|
||||
border-color: var(--border-gold-soft);
|
||||
color: var(--primary-light);
|
||||
background: rgba(212, 175, 55, 0.08);
|
||||
}
|
||||
|
||||
.est-return {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.est-value {
|
||||
color: var(--primary-light);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #c62828;
|
||||
color: var(--danger);
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background: #2e9e5e;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background: #555;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
border-radius: 6px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 4px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #2e9e5e;
|
||||
color: #2e9e5e;
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
line-height: 42px;
|
||||
.btn-confirm:disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.btn-share {
|
||||
width: 100%;
|
||||
margin: 12px 0 8px;
|
||||
.success-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto 8px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(46, 158, 94, 0.8);
|
||||
background: rgba(46, 158, 94, 0.12);
|
||||
color: #4ade80;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
margin: 12px 0 14px;
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
background: #1877f2;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
opacity: 0.5;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.summary-row + .summary-row {
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-done {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
background: #1aa89a;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
padding: 11px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
</style>
|
||||
|
||||
2
apps/player/src/router/index.js
Normal file
2
apps/player/src/router/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// 兼容旧缓存/错误解析:转发到 TypeScript 源文件
|
||||
export { default } from './index.ts';
|
||||
Reference in New Issue
Block a user