feat: multi-tier agent hierarchy, wallet ledger, and player UX polish
Add configurable agent max level and default sub-agent credit ratio, per-agent block direct player login on suspend, admin/agent wallet transaction views, and match detail my-bets section with refreshed player card styling. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api from '../api';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
import { useBetSlipStore } from '../stores/betSlip';
|
||||
import TeamEmblem from '../components/TeamEmblem.vue';
|
||||
import { DETAIL_MARKET_TYPES, MARKET_I18N_KEY } from '../utils/marketCatalog';
|
||||
@@ -82,6 +83,53 @@ const csMessage = ref('');
|
||||
const showCsSuccess = ref(false);
|
||||
const csConfirmOpen = ref(false);
|
||||
const csConfirmMarketType = ref<string | null>(null);
|
||||
|
||||
interface MyBet {
|
||||
betNo: string;
|
||||
betType: string;
|
||||
stake: string;
|
||||
totalOdds: string;
|
||||
potentialReturn: string;
|
||||
actualReturn: string;
|
||||
status: string;
|
||||
placedAt: string;
|
||||
pickLabel: string;
|
||||
matchTitle: string;
|
||||
}
|
||||
const myBets = ref<MyBet[]>([]);
|
||||
const loadingMyBets = ref(false);
|
||||
|
||||
async function loadMyBets() {
|
||||
if (!match.value) return;
|
||||
loadingMyBets.value = true;
|
||||
try {
|
||||
const { data } = await api.get('/player/bets?page=1');
|
||||
const items = (data.data?.items ?? data.data ?? []) as MyBet[];
|
||||
const matchTitle = `${match.value.homeTeamName} vs ${match.value.awayTeamName}`;
|
||||
myBets.value = items.filter(
|
||||
(b) => b.matchTitle === matchTitle || b.matchTitle === `${match.value!.awayTeamName} vs ${match.value!.homeTeamName}`,
|
||||
);
|
||||
} catch {
|
||||
myBets.value = [];
|
||||
} finally {
|
||||
loadingMyBets.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const statusLabel = (status: string) => {
|
||||
const s = status.toUpperCase();
|
||||
if (s === 'WON' || s === 'WIN') return t('history.status_won');
|
||||
if (s === 'LOST' || s === 'LOSE') return t('history.status_lost');
|
||||
if (s === 'PUSH' || s === 'VOID' || s === 'CANCELLED') return t('history.status_push');
|
||||
return t('history.status_pending');
|
||||
};
|
||||
|
||||
const statusClass = (status: string) => {
|
||||
const s = status.toUpperCase();
|
||||
if (s === 'WON' || s === 'WIN') return 'bet-status-won';
|
||||
if (s === 'LOST' || s === 'LOSE') return 'bet-status-lost';
|
||||
return 'bet-status-pending';
|
||||
};
|
||||
const marketsByType = computed(() => {
|
||||
const map = new Map<string, Market>();
|
||||
for (const m of match.value?.markets ?? []) {
|
||||
@@ -237,6 +285,7 @@ async function loadMatch() {
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
loadMyBets();
|
||||
}
|
||||
|
||||
useOnLocaleChange(loadMatch);
|
||||
@@ -384,6 +433,25 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
<p v-if="liveScoreText" class="live-score">{{ liveScoreText }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 我的投注 -->
|
||||
<section v-if="myBets.length" class="my-bets-section">
|
||||
<h3 class="my-bets-title">{{ t('history.my_bets') || '我的投注' }}</h3>
|
||||
<div class="my-bets-list">
|
||||
<div v-for="bet in myBets" :key="bet.betNo" class="my-bet-card" @click="router.push(`/bets/${bet.betNo}`)">
|
||||
<div class="bet-header">
|
||||
<span class="bet-type">{{ bet.betType === 'PARLAY' ? t('history.parlay_league') : bet.pickLabel }}</span>
|
||||
<span class="bet-status" :class="statusClass(bet.status)">{{ statusLabel(bet.status) }}</span>
|
||||
</div>
|
||||
<div class="bet-footer">
|
||||
<span class="bet-stake">{{ t('history.stake') }} {{ formatMoney(bet.stake, locale) }}</span>
|
||||
<span class="bet-return" :class="statusClass(bet.status)">
|
||||
{{ statusClass(bet.status) === 'bet-status-won' ? '+' : '' }}{{ formatMoney(bet.actualReturn || bet.potentialReturn, locale) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="markets-section">
|
||||
<CorrectScoreConfirmModal
|
||||
:open="csConfirmOpen"
|
||||
@@ -777,4 +845,93 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
padding: 2px 0 6px;
|
||||
}
|
||||
|
||||
/* ── 我的投注 ── */
|
||||
.my-bets-section {
|
||||
padding: 0 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.my-bets-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.my-bets-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.my-bet-card {
|
||||
background: #1c1c1c;
|
||||
border: 1px solid #2e2e2e;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.my-bet-card:active {
|
||||
background: #252525;
|
||||
}
|
||||
|
||||
.bet-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.bet-type {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.bet-status {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bet-status-won {
|
||||
background: rgba(61, 184, 101, 0.15);
|
||||
color: #3db865;
|
||||
}
|
||||
|
||||
.bet-status-lost {
|
||||
background: rgba(224, 80, 80, 0.15);
|
||||
color: #e05050;
|
||||
}
|
||||
|
||||
.bet-status-pending {
|
||||
background: rgba(232, 200, 74, 0.15);
|
||||
color: #e8c84a;
|
||||
}
|
||||
|
||||
.bet-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bet-stake {
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.bet-return {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user