This commit is contained in:
wchino
2026-06-13 17:38:25 +08:00
parent e7e938f261
commit 7b33d9f9fa
190 changed files with 23222 additions and 4336 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, defineAsyncComponent } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { formatApiErrorMessage, isApiErrorCode } from '@thebet365/shared';
import api from '../api';
import { ElMessage } from 'element-plus';
import { useAdminLocale } from '../composables/useAdminLocale';
@@ -70,11 +71,82 @@ interface SettlementBetStats {
const { t, locale, localeTag } = useAdminLocale();
const route = useRoute();
const router = useRouter();
const STAT_FACT_LABELS = {
homeCorners: {
'zh-CN': '主队角球',
'en-US': 'Home corners',
'ms-MY': 'Sepakan sudut tuan rumah',
},
awayCorners: {
'zh-CN': '客队角球',
'en-US': 'Away corners',
'ms-MY': 'Sepakan sudut pelawat',
},
homeCards: {
'zh-CN': '主队黄牌/红牌',
'en-US': 'Home yellow/red cards',
'ms-MY': 'Kad kuning/merah tuan rumah',
},
awayCards: {
'zh-CN': '客队黄牌/红牌',
'en-US': 'Away yellow/red cards',
'ms-MY': 'Kad kuning/merah pelawat',
},
} as const;
const loading = ref(false);
const previewing = ref(false);
const match = ref<AdminMatchDetail | null>(null);
const score = ref({ htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 });
const matchStats = ref<{
homeCorners: number | null;
awayCorners: number | null;
homeYellowCards: number | null;
awayYellowCards: number | null;
homeRedCards: number | null;
awayRedCards: number | null;
homeCards: number | null;
awayCards: number | null;
}>(emptyMatchStats());
function emptyMatchStats() {
return {
homeCorners: null,
awayCorners: null,
homeYellowCards: null,
awayYellowCards: null,
homeRedCards: null,
awayRedCards: null,
homeCards: null,
awayCards: null,
};
}
function cardTotalFromBreakdown(
yellow: number | null,
red: number | null,
fallback: number | null,
) {
const hasBreakdown = yellow != null || red != null;
if (!hasBreakdown) return fallback ?? null;
if (yellow == null || red == null) return null;
return yellow + red;
}
function settlementStatsPayload() {
const s = matchStats.value;
return {
...s,
homeCards: cardTotalFromBreakdown(s.homeYellowCards, s.homeRedCards, s.homeCards),
awayCards: cardTotalFromBreakdown(s.awayYellowCards, s.awayRedCards, s.awayCards),
};
}
function resetSettlementInputs() {
score.value = { htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 };
matchStats.value = emptyMatchStats();
}
const winnerTeamId = ref('');
const outrightSelections = ref<
Array<{ teamId: string; teamCode: string; teamZh: string; teamEn: string }>
@@ -133,7 +205,7 @@ function buildSettlementPayload(): Record<string, unknown> | null {
}
return { winnerTeamId: Number(winnerTeamId.value) };
}
return { ...score.value };
return { ...score.value, ...settlementStatsPayload() };
}
type PreviewItem = {
betNo: string;
@@ -162,7 +234,7 @@ const previewZeroHint = computed(() => {
if (payout > 0 || refund > 0) return '';
const pending = Number(p.pendingOtherMatches ?? 0);
const wonLegs = Number(p.wonLegsOnMatch ?? 0);
if (pending > 0 && wonLegs > 0) {
if (pending > 0) {
return t('settlement.preview_zero_parlay_hint', { pending: String(pending), legs: String(wonLegs) });
}
if (Number(p.lostOnThisMatch ?? 0) > 0) {
@@ -380,8 +452,19 @@ async function loadMatch() {
ftHome: detail.score.ftHome,
ftAway: detail.score.ftAway,
};
matchStats.value = {
homeCorners: detail.score.homeCorners ?? null,
awayCorners: detail.score.awayCorners ?? null,
homeYellowCards: detail.score.homeYellowCards ?? null,
awayYellowCards: detail.score.awayYellowCards ?? null,
homeRedCards: detail.score.homeRedCards ?? null,
awayRedCards: detail.score.awayRedCards ?? null,
homeCards: detail.score.homeCards ?? null,
awayCards: detail.score.awayCards ?? null,
};
winnerTeamId.value = detail.score.winnerTeamId ?? '';
} else {
resetSettlementInputs();
winnerTeamId.value = '';
}
if (detail.isOutright) {
@@ -437,7 +520,21 @@ async function confirmResettle() {
}
function settlementApiError(e: unknown, fallback: string) {
const err = e as { response?: { data?: { error?: string; message?: string } } };
const err = e as { response?: { data?: { code?: unknown; params?: Record<string, string | number>; error?: string; message?: string } } };
const code = err.response?.data?.code;
if (isApiErrorCode(code)) {
const params = { ...(err.response?.data?.params ?? {}) };
if (code === 'SETTLEMENT_FACTS_REQUIRED' && typeof params.fields === 'string') {
params.fields = params.fields
.split(',')
.map((field) => {
const labels = STAT_FACT_LABELS[field as keyof typeof STAT_FACT_LABELS];
return labels?.[locale.value as keyof typeof labels] ?? field;
})
.join(locale.value === 'zh-CN' ? '、' : ', ');
}
return formatApiErrorMessage(code, locale.value, params);
}
const raw = err.response?.data?.error ?? err.response?.data?.message ?? fallback;
if (raw === 'Score not recorded' || raw === 'Score not found') {
return t('settlement.err_score_not_recorded');
@@ -595,6 +692,69 @@ onMounted(() => {
<el-input-number v-model="score.ftAway" :min="0" controls-position="right" style="width: 88px" />
</div>
</div>
<div class="score-block compact stat-block">
<span class="score-title">{{ t('settlement.corners') }}</span>
<div class="stat-inputs">
<span class="stat-side">{{ t('settlement.home_stat') }}</span>
<el-input-number
v-model="matchStats.homeCorners"
:min="0"
:precision="0"
controls-position="right"
style="width: 80px"
/>
<span class="stat-side">{{ t('settlement.away_stat') }}</span>
<el-input-number
v-model="matchStats.awayCorners"
:min="0"
:precision="0"
controls-position="right"
style="width: 80px"
/>
</div>
</div>
<div class="score-block compact stat-block">
<span class="score-title">{{ t('settlement.yellow_cards') }}</span>
<div class="stat-inputs">
<span class="stat-side">{{ t('settlement.home_stat') }}</span>
<el-input-number
v-model="matchStats.homeYellowCards"
:min="0"
:precision="0"
controls-position="right"
style="width: 80px"
/>
<span class="stat-side">{{ t('settlement.away_stat') }}</span>
<el-input-number
v-model="matchStats.awayYellowCards"
:min="0"
:precision="0"
controls-position="right"
style="width: 80px"
/>
</div>
</div>
<div class="score-block compact stat-block">
<span class="score-title">{{ t('settlement.red_cards') }}</span>
<div class="stat-inputs">
<span class="stat-side">{{ t('settlement.home_stat') }}</span>
<el-input-number
v-model="matchStats.homeRedCards"
:min="0"
:precision="0"
controls-position="right"
style="width: 80px"
/>
<span class="stat-side">{{ t('settlement.away_stat') }}</span>
<el-input-number
v-model="matchStats.awayRedCards"
:min="0"
:precision="0"
controls-position="right"
style="width: 80px"
/>
</div>
</div>
</div>
<div class="settle-actions">
@@ -873,7 +1033,7 @@ onMounted(() => {
flex-wrap: wrap;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #2a2a2a;
border-top: 1px solid var(--border);
}
.outright-settle-head {
@@ -887,7 +1047,7 @@ onMounted(() => {
margin: 0;
font-size: 16px;
font-weight: 700;
color: var(--green-text);
color: var(--success-text);
}
.outright-winner-row {
@@ -938,19 +1098,19 @@ onMounted(() => {
.team-name-inline {
font-size: 16px;
font-weight: 600;
color: #eee;
font-weight: 750;
color: var(--text);
white-space: nowrap;
}
.kickoff-inline {
font-size: 13px;
color: #aaa;
color: var(--text-muted);
white-space: nowrap;
}
.kickoff-inline .meta-k {
color: #777;
color: #aaa49a;
margin-right: 4px;
}
@@ -971,6 +1131,22 @@ onMounted(() => {
margin-bottom: 0;
}
.stat-block {
padding-left: 2px;
}
.stat-inputs {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.stat-side {
font-size: 11px;
color: var(--warning-text);
}
.section-title {
font-size: 14px;
font-weight: 600;
@@ -997,7 +1173,7 @@ onMounted(() => {
.leg-badge {
margin-left: 4px;
font-size: 10px;
color: var(--gold-text, #dcc078);
color: var(--warning-text);
}
.bet-content-cell {
@@ -1083,7 +1259,7 @@ onMounted(() => {
.vs {
font-size: 13px;
font-weight: 700;
color: var(--gold-text, #dcc078);
color: var(--warning-text);
flex-shrink: 0;
}
@@ -1256,7 +1432,70 @@ onMounted(() => {
}
/* 智能比分弹窗样式(功能已关闭,保留便于恢复)
.smart-hint { ... }
*/
.section-title,
.preview-title,
.preview-bar-title,
.pstat-value {
color: var(--text);
}
.subsection-title,
.bet-content-cell,
.match-league,
.score-title,
.preview-metric-label,
.preview-items-title,
.period-tag,
.empty-hint,
.subsection-hint {
color: var(--text-muted);
}
.stat-side,
.leg-badge,
.vs,
.preview-metric-orange,
.pstat-orange {
color: var(--warning-text);
}
.preview-metric-green,
.pstat-green {
color: var(--success-text);
}
.mini-chart,
.pstat {
border-color: var(--border);
background: #ffffff;
box-shadow: var(--shadow);
}
.preview-zero-hint {
border-color: var(--warning-border);
background: var(--warning-bg);
color: var(--warning-text);
}
.score-sep {
color: var(--text-muted);
}
.stats-table-block--bets .table-wrap {
border-color: var(--border);
background: #ffffff;
}
@media (max-width: 760px) {
.score-row,
.preview-bar,
.preview-metrics {
align-items: stretch;
}
.preview-metric {
justify-content: space-between;
width: 100%;
}
}
</style>