1508 lines
42 KiB
Vue
1508 lines
42 KiB
Vue
<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';
|
||
import { usePermissions } from '../composables/usePermissions';
|
||
import { AdminPerm } from '../constants/permissions';
|
||
|
||
const VChart = defineAsyncComponent(() =>
|
||
import('../components/dashboard/echarts-setup').then((m) => m.VChart),
|
||
);
|
||
import { formatAmount } from '../utils/format-amount';
|
||
import {
|
||
buildBetTypePieOption,
|
||
buildSelectionStakeBarOption,
|
||
buildStatusPieOption,
|
||
} from '../utils/settlement-charts';
|
||
import {
|
||
betStatusLabel,
|
||
betStatusTagType,
|
||
betTypeLabel,
|
||
betResultLabel,
|
||
} from '../utils/bet-labels';
|
||
import { adminSelectionLabel } from '../utils/adminSelectionLabel';
|
||
import type { AdminMatchDetail } from './match-form';
|
||
import AdminSubNav from '../components/AdminSubNav.vue';
|
||
|
||
interface SettlementBetStats {
|
||
summary: {
|
||
totalBets: number;
|
||
singleBets: number;
|
||
parlayBets: number;
|
||
totalStake: string;
|
||
totalPotentialReturn: string;
|
||
statusCounts: Record<string, number>;
|
||
legCount: number;
|
||
};
|
||
bySelection: Array<{
|
||
marketType: string;
|
||
period: string | null;
|
||
selectionName: string;
|
||
selectionId: string;
|
||
legCount: number;
|
||
singleStake: string;
|
||
parlayLegCount: number;
|
||
}>;
|
||
bets: {
|
||
items: Array<{
|
||
id: string;
|
||
betNo: string;
|
||
username: string;
|
||
betType: string;
|
||
status: string;
|
||
stake: string;
|
||
potentialReturn: string | null;
|
||
placedAt: string;
|
||
legCountOnMatch: number;
|
||
selections: Array<{
|
||
marketType: string;
|
||
period: string | null;
|
||
selectionName: string;
|
||
odds: string;
|
||
}>;
|
||
}>;
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
};
|
||
}
|
||
|
||
const { t, locale, localeTag } = useAdminLocale();
|
||
const { hasPermission } = usePermissions();
|
||
const canResettle = computed(() => hasPermission(AdminPerm.resettle));
|
||
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 }>
|
||
>([]);
|
||
const preview = ref<Record<string, unknown> | null>(null);
|
||
const resettlePreview = ref<Record<string, unknown> | null>(null);
|
||
const resettleReason = ref('');
|
||
const statsSummary = ref<Pick<SettlementBetStats, 'summary' | 'bySelection'> | null>(null);
|
||
const betsList = ref<SettlementBetStats['bets'] | null>(null);
|
||
const statsLoading = ref(false);
|
||
const betsLoading = ref(false);
|
||
|
||
const stats = computed((): SettlementBetStats | null => {
|
||
if (!statsSummary.value || !betsList.value) return null;
|
||
return { ...statsSummary.value, bets: betsList.value };
|
||
});
|
||
const betPage = ref(1);
|
||
const betPageSize = ref(10);
|
||
const previewPage = ref(1);
|
||
const previewPageSize = ref(10);
|
||
|
||
// 智能比分推荐已暂时关闭(后端 smart-score.solver.ts 保留,恢复时接回 UI 与 POST /settlement/smart-score)
|
||
|
||
const matchId = computed(() => String(route.params.id ?? ''));
|
||
const isOutright = computed(() => match.value?.isOutright === true);
|
||
|
||
const outrightTitle = computed(() => {
|
||
const m = match.value;
|
||
if (!m) return '';
|
||
const name = m.matchName?.trim();
|
||
if (name) return name;
|
||
if (locale.value === 'en-US') return m.leagueEn || m.leagueZh;
|
||
if (locale.value === 'ms-MY') return m.leagueMs || m.leagueEn || m.leagueZh;
|
||
return m.leagueZh || m.leagueEn;
|
||
});
|
||
|
||
const settlementPageTitle = computed(() =>
|
||
isOutright.value ? t('settlement.outright.page_title') : t('page.settlement.title'),
|
||
);
|
||
|
||
function outrightTeamLabel(row: { teamZh: string; teamEn: string; teamCode: string }) {
|
||
const name =
|
||
locale.value === 'en-US'
|
||
? row.teamEn || row.teamZh
|
||
: locale.value === 'ms-MY'
|
||
? row.teamEn || row.teamZh
|
||
: row.teamZh || row.teamEn;
|
||
return `${name} (${row.teamCode})`;
|
||
}
|
||
|
||
function buildSettlementPayload(): Record<string, unknown> | null {
|
||
if (isOutright.value) {
|
||
if (!winnerTeamId.value) {
|
||
ElMessage.warning(t('settlement.outright.winner_required'));
|
||
return null;
|
||
}
|
||
return { winnerTeamId: Number(winnerTeamId.value) };
|
||
}
|
||
return { ...score.value, ...settlementStatsPayload() };
|
||
}
|
||
type PreviewItem = {
|
||
betNo: string;
|
||
betType: string;
|
||
result: string;
|
||
payout: string;
|
||
note?: string;
|
||
};
|
||
|
||
type PreviewItemsPage = {
|
||
items: PreviewItem[];
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
};
|
||
|
||
const previewItemsPage = computed(
|
||
() => (preview.value?.items as PreviewItemsPage | undefined) ?? { items: [], total: 0, page: 1, pageSize: 10 },
|
||
);
|
||
|
||
const previewZeroHint = computed(() => {
|
||
const p = preview.value;
|
||
if (!p) return '';
|
||
const payout = Number(p.totalPayout ?? 0);
|
||
const refund = Number(p.totalRefund ?? 0);
|
||
if (payout > 0 || refund > 0) return '';
|
||
const pending = Number(p.pendingOtherMatches ?? 0);
|
||
const wonLegs = Number(p.wonLegsOnMatch ?? 0);
|
||
if (pending > 0) {
|
||
return t('settlement.preview_zero_parlay_hint', { pending: String(pending), legs: String(wonLegs) });
|
||
}
|
||
if (Number(p.lostOnThisMatch ?? 0) > 0) {
|
||
return t('settlement.preview_zero_lost_hint', { count: String(p.lostOnThisMatch) });
|
||
}
|
||
return t('settlement.preview_zero_default_hint');
|
||
});
|
||
|
||
function previewResultLabel(result: string) {
|
||
const key = `settlement.preview.result.${result}`;
|
||
const label = t(key);
|
||
return label !== key ? label : betResultLabel(result);
|
||
}
|
||
|
||
const betTypeChartOption = computed(() => {
|
||
const s = stats.value?.summary;
|
||
if (!s) return null;
|
||
return buildBetTypePieOption({
|
||
title: t('settlement.chart.bet_type'),
|
||
singleBets: s.singleBets,
|
||
parlayBets: s.parlayBets,
|
||
totalBets: s.totalBets,
|
||
legCount: s.legCount,
|
||
labels: {
|
||
single: t('settlement.stats_single'),
|
||
parlay: t('settlement.stats_parlay'),
|
||
},
|
||
subtext: `${s.singleBets} ${t('settlement.stats_single')} · ${s.parlayBets} ${t('settlement.stats_parlay')} · ${s.legCount} ${t('settlement.col.legs')}`,
|
||
centerLabel: t('settlement.stats_total_bets'),
|
||
});
|
||
});
|
||
|
||
const statusChartOption = computed(() => {
|
||
const s = stats.value?.summary;
|
||
if (!s) return null;
|
||
const parts = ['PENDING', 'WON', 'LOST', 'VOID', 'PUSH']
|
||
.filter((st) => (s.statusCounts[st] ?? 0) > 0)
|
||
.map((st) => `${betStatusLabel(st)} ${s.statusCounts[st]}`);
|
||
return buildStatusPieOption({
|
||
title: t('settlement.chart.status'),
|
||
statusCounts: s.statusCounts,
|
||
labelFor: (st) => betStatusLabel(st),
|
||
subtext: parts.join(' · ') || t('settlement.no_bets'),
|
||
});
|
||
});
|
||
|
||
const selectionStakeChartOption = computed(() => {
|
||
const s = stats.value?.summary;
|
||
const rows = stats.value?.bySelection ?? [];
|
||
if (!s) return null;
|
||
const top = [...rows]
|
||
.sort((a, b) => Number(b.singleStake) - Number(a.singleStake))
|
||
.slice(0, 6);
|
||
const labels = top.map((row) => {
|
||
const name = selectionDisplay(row);
|
||
return name.length > 8 ? `${name.slice(0, 8)}…` : name;
|
||
});
|
||
return buildSelectionStakeBarOption({
|
||
title: t('settlement.chart.stake_by_selection'),
|
||
subtext: `${t('settlement.stats_total_stake')} ${formatAmount(s.totalStake)} · ${t('settlement.stats_potential')} ${formatAmount(s.totalPotentialReturn)}`,
|
||
labels: labels.length ? labels : [t('settlement.no_bets')],
|
||
stakes: top.length ? top.map((row) => Number(row.singleStake)) : [0],
|
||
seriesName: t('settlement.col.single_stake'),
|
||
});
|
||
});
|
||
|
||
const leagueLabel = computed(() => {
|
||
const m = match.value;
|
||
if (!m) return '';
|
||
if (locale.value === 'en-US') return m.leagueEn || m.leagueZh;
|
||
if (locale.value === 'ms-MY') return m.leagueMs || m.leagueEn || m.leagueZh;
|
||
return m.leagueZh || m.leagueEn;
|
||
});
|
||
|
||
const homeLabel = computed(() => {
|
||
const m = match.value;
|
||
if (!m) return '';
|
||
if (locale.value === 'en-US') return m.homeTeamEn || m.homeTeamZh;
|
||
if (locale.value === 'ms-MY') return m.homeTeamMs || m.homeTeamEn || m.homeTeamZh;
|
||
return m.homeTeamZh || m.homeTeamEn;
|
||
});
|
||
|
||
const awayLabel = computed(() => {
|
||
const m = match.value;
|
||
if (!m) return '';
|
||
if (locale.value === 'en-US') return m.awayTeamEn || m.awayTeamZh;
|
||
if (locale.value === 'ms-MY') return m.awayTeamMs || m.awayTeamEn || m.awayTeamZh;
|
||
return m.awayTeamZh || m.awayTeamEn;
|
||
});
|
||
|
||
const kickoffLabel = computed(() => {
|
||
const iso = match.value?.startTime;
|
||
if (!iso) return '—';
|
||
const d = new Date(iso);
|
||
if (Number.isNaN(d.getTime())) return iso;
|
||
return d.toLocaleString(locale.value === 'en-US' ? 'en-GB' : 'zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
});
|
||
|
||
const statusLabel = computed(() => {
|
||
const s = match.value?.status;
|
||
if (!s) return '—';
|
||
const key = `match.status.${s}`;
|
||
const translated = t(key);
|
||
return translated === key ? s : translated;
|
||
});
|
||
|
||
function marketLabel(marketType: string) {
|
||
const key = `matchEditor.market.${marketType}`;
|
||
const label = t(key);
|
||
return label === key ? marketType : label;
|
||
}
|
||
|
||
function selectionDisplay(row: { selectionName: string; period?: string | null }) {
|
||
return adminSelectionLabel(t, row.selectionName, {
|
||
period: row.period ?? undefined,
|
||
});
|
||
}
|
||
|
||
function formatTime(v: string) {
|
||
return new Date(v).toLocaleString(localeTag.value, {
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
}
|
||
|
||
function matchBetSelectionSummary(
|
||
row: SettlementBetStats['bets']['items'][number],
|
||
) {
|
||
return row.selections
|
||
.map((leg) => {
|
||
const market = marketLabel(leg.marketType);
|
||
const sel = selectionDisplay(leg);
|
||
return `${market} ${sel} @${leg.odds}`;
|
||
})
|
||
.join(' · ');
|
||
}
|
||
|
||
async function loadStatsSummary() {
|
||
if (!matchId.value) return;
|
||
statsLoading.value = true;
|
||
try {
|
||
const { data } = await api.get(`/admin/matches/${matchId.value}/settlement/summary`);
|
||
statsSummary.value = data.data as Pick<SettlementBetStats, 'summary' | 'bySelection'>;
|
||
} catch (e: unknown) {
|
||
const err = e as { response?: { data?: { error?: string } } };
|
||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||
} finally {
|
||
statsLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function loadBets() {
|
||
if (!matchId.value) return;
|
||
betsLoading.value = true;
|
||
try {
|
||
const { data } = await api.get(`/admin/matches/${matchId.value}/settlement/bets`, {
|
||
params: { page: betPage.value, pageSize: betPageSize.value },
|
||
});
|
||
const payload = data.data as SettlementBetStats['bets'];
|
||
betsList.value = payload;
|
||
betPage.value = payload.page;
|
||
betPageSize.value = payload.pageSize;
|
||
} catch (e: unknown) {
|
||
const err = e as { response?: { data?: { error?: string } } };
|
||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||
} finally {
|
||
betsLoading.value = false;
|
||
}
|
||
}
|
||
|
||
async function loadStats() {
|
||
betPage.value = 1;
|
||
await Promise.all([loadStatsSummary(), loadBets()]);
|
||
}
|
||
|
||
function onBetPageChange(page: number) {
|
||
betPage.value = page;
|
||
void loadBets();
|
||
}
|
||
|
||
function onBetPageSizeChange(size: number) {
|
||
betPageSize.value = size;
|
||
betPage.value = 1;
|
||
void loadBets();
|
||
}
|
||
|
||
async function loadMatch() {
|
||
if (!matchId.value) return;
|
||
loading.value = true;
|
||
try {
|
||
const { data } = await api.get(`/admin/matches/${matchId.value}`);
|
||
const detail = data.data as AdminMatchDetail;
|
||
const settleable =
|
||
detail.status === 'CLOSED' ||
|
||
detail.status === 'PENDING_SETTLEMENT' ||
|
||
detail.status === 'SETTLED';
|
||
if (!settleable) {
|
||
ElMessage.warning(t('settlement.must_close_first'));
|
||
router.replace(detail.isOutright ? '/matches/outrights' : '/matches');
|
||
return;
|
||
}
|
||
match.value = detail;
|
||
if (detail.score) {
|
||
score.value = {
|
||
htHome: detail.score.htHome,
|
||
htAway: detail.score.htAway,
|
||
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) {
|
||
const outrightRes = await api.get(`/admin/outrights/${matchId.value}`);
|
||
const payload = outrightRes.data.data as {
|
||
selections: Array<{
|
||
teamId: string | null;
|
||
teamCode: string;
|
||
teamZh: string;
|
||
teamEn: string;
|
||
status: string;
|
||
}>;
|
||
};
|
||
outrightSelections.value = (payload.selections ?? [])
|
||
.filter((s) => s.teamId)
|
||
.map((s) => ({
|
||
teamId: s.teamId as string,
|
||
teamCode: s.teamCode,
|
||
teamZh: s.teamZh,
|
||
teamEn: s.teamEn,
|
||
}));
|
||
} else {
|
||
outrightSelections.value = [];
|
||
}
|
||
betPage.value = 1;
|
||
await loadStats();
|
||
} catch (e: unknown) {
|
||
const err = e as { response?: { data?: { error?: string } } };
|
||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
const isSettled = computed(() => match.value?.status === 'SETTLED');
|
||
|
||
async function previewResettlement() {
|
||
const payload = buildSettlementPayload();
|
||
if (!payload) return;
|
||
const { data } = await api.post(`/admin/matches/${matchId.value}/resettle/preview`, {
|
||
...payload,
|
||
reason: resettleReason.value.trim() || undefined,
|
||
});
|
||
resettlePreview.value = data.data;
|
||
}
|
||
|
||
async function confirmResettle() {
|
||
if (!resettlePreview.value?.batch) return;
|
||
await api.post(`/admin/resettle/${(resettlePreview.value.batch as { id: string }).id}/confirm`);
|
||
ElMessage.success(t('msg.resettle_confirmed'));
|
||
resettlePreview.value = null;
|
||
await loadMatch();
|
||
}
|
||
|
||
function settlementApiError(e: unknown, fallback: 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');
|
||
}
|
||
if (raw === 'SETTLEMENT_WINNER_REQUIRED') {
|
||
return t('settlement.outright.winner_required');
|
||
}
|
||
return raw;
|
||
}
|
||
|
||
async function loadPreviewItems(page = previewPage.value, pageSize = previewPageSize.value) {
|
||
const batch = preview.value?.batch as { id: string } | undefined;
|
||
if (!batch) return;
|
||
try {
|
||
const { data } = await api.get(`/admin/settlement/${batch.id}/preview-items`, {
|
||
params: { page, pageSize },
|
||
});
|
||
if (preview.value) {
|
||
preview.value = { ...preview.value, items: data.data as PreviewItemsPage };
|
||
}
|
||
previewPage.value = (data.data as PreviewItemsPage).page;
|
||
previewPageSize.value = (data.data as PreviewItemsPage).pageSize;
|
||
} catch (e: unknown) {
|
||
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
|
||
}
|
||
}
|
||
|
||
async function previewSettlement() {
|
||
const payload = buildSettlementPayload();
|
||
if (!payload) return;
|
||
preview.value = null;
|
||
previewPage.value = 1;
|
||
previewing.value = true;
|
||
try {
|
||
const { data } = await api.post(`/admin/matches/${matchId.value}/settlement/preview`, {
|
||
...payload,
|
||
page: 1,
|
||
pageSize: previewPageSize.value,
|
||
});
|
||
preview.value = data.data;
|
||
const itemsPage = data.data.items as PreviewItemsPage;
|
||
previewPage.value = itemsPage.page;
|
||
previewPageSize.value = itemsPage.pageSize;
|
||
await loadMatch();
|
||
} catch (e: unknown) {
|
||
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
|
||
} finally {
|
||
previewing.value = false;
|
||
}
|
||
}
|
||
|
||
function onPreviewPageChange(page: number) {
|
||
previewPage.value = page;
|
||
void loadPreviewItems(page, previewPageSize.value);
|
||
}
|
||
|
||
function onPreviewPageSizeChange(size: number) {
|
||
previewPageSize.value = size;
|
||
previewPage.value = 1;
|
||
void loadPreviewItems(1, size);
|
||
}
|
||
|
||
async function confirm() {
|
||
if (!preview.value?.batch) return;
|
||
await api.post(`/admin/settlement/${(preview.value.batch as { id: string }).id}/confirm`);
|
||
ElMessage.success(t('msg.settlement_confirmed'));
|
||
preview.value = null;
|
||
await loadMatch();
|
||
}
|
||
|
||
onMounted(() => {
|
||
void loadMatch();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div v-loading="loading" class="settlement-page">
|
||
<AdminSubNav
|
||
:title="settlementPageTitle"
|
||
:subtitle="`#${matchId}`"
|
||
>
|
||
<template #extra>
|
||
<el-tag v-if="match" size="small" type="info">{{ statusLabel }}</el-tag>
|
||
</template>
|
||
</AdminSubNav>
|
||
|
||
<el-card v-if="match" class="settle-top-card" shadow="never">
|
||
<p v-if="leagueLabel" class="match-league">{{ leagueLabel }}</p>
|
||
<div v-if="isOutright" class="outright-settle-head">
|
||
<p class="outright-settle-title">{{ outrightTitle }}</p>
|
||
<span class="kickoff-inline">
|
||
<span class="meta-k">{{ t('settlement.outright.title') }}</span>
|
||
</span>
|
||
</div>
|
||
<div v-else class="match-inline">
|
||
<div class="team-chip">
|
||
<img
|
||
v-if="match.homeTeamLogoUrl"
|
||
:src="match.homeTeamLogoUrl"
|
||
alt=""
|
||
class="team-logo-sm"
|
||
loading="lazy"
|
||
/>
|
||
<span class="team-name-inline">{{ homeLabel }}</span>
|
||
</div>
|
||
<span class="vs">VS</span>
|
||
<div class="team-chip">
|
||
<img
|
||
v-if="match.awayTeamLogoUrl"
|
||
:src="match.awayTeamLogoUrl"
|
||
alt=""
|
||
class="team-logo-sm"
|
||
loading="lazy"
|
||
/>
|
||
<span class="team-name-inline">{{ awayLabel }}</span>
|
||
</div>
|
||
<span class="kickoff-inline">
|
||
<span class="meta-k">{{ t('settlement.kickoff') }}</span>
|
||
{{ kickoffLabel }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="settle-score-row">
|
||
<div v-if="isOutright" class="outright-winner-row">
|
||
<span class="score-title">{{ t('settlement.outright.winner') }}</span>
|
||
<el-select
|
||
v-model="winnerTeamId"
|
||
filterable
|
||
clearable
|
||
class="outright-winner-select"
|
||
:placeholder="t('settlement.outright.winner_ph')"
|
||
>
|
||
<el-option
|
||
v-for="row in outrightSelections"
|
||
:key="row.teamId"
|
||
:label="outrightTeamLabel(row)"
|
||
:value="row.teamId"
|
||
/>
|
||
</el-select>
|
||
</div>
|
||
<div v-else class="score-inline-group">
|
||
<div class="score-block compact">
|
||
<span class="score-title">{{ t('settlement.ht_score') }}</span>
|
||
<div class="score-inputs">
|
||
<el-input-number v-model="score.htHome" :min="0" controls-position="right" style="width: 88px" />
|
||
<span class="score-sep">—</span>
|
||
<el-input-number v-model="score.htAway" :min="0" controls-position="right" style="width: 88px" />
|
||
</div>
|
||
</div>
|
||
<div class="score-block compact">
|
||
<span class="score-title">{{ t('settlement.ft_score') }}</span>
|
||
<div class="score-inputs">
|
||
<el-input-number v-model="score.ftHome" :min="0" controls-position="right" style="width: 88px" />
|
||
<span class="score-sep">—</span>
|
||
<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">
|
||
<template v-if="!isSettled">
|
||
<el-button
|
||
type="primary"
|
||
:loading="previewing"
|
||
@click="previewSettlement"
|
||
>
|
||
{{ t('settlement.preview_btn') }}
|
||
</el-button>
|
||
<span class="preview-hint">{{
|
||
isOutright ? t('settlement.outright.preview_hint') : t('settlement.preview_hint')
|
||
}}</span>
|
||
</template>
|
||
<template v-else-if="canResettle">
|
||
<el-input
|
||
v-model="resettleReason"
|
||
size="small"
|
||
:placeholder="t('settlement.resettle_reason')"
|
||
class="settle-resettle-reason"
|
||
/>
|
||
<el-button type="warning" plain @click="previewResettlement">
|
||
{{ t('settlement.resettle_preview') }}
|
||
</el-button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<!-- 智能比分弹窗已关闭(见 Settlement.vue git 历史) -->
|
||
|
||
<el-card v-if="canResettle && resettlePreview" class="preview-card" shadow="never">
|
||
<div class="preview-title">{{ t('settlement.resettle_preview_title') }}</div>
|
||
<el-row :gutter="20">
|
||
<el-col :span="8">
|
||
<div class="pstat">
|
||
<div class="pstat-value">{{ resettlePreview.affectedCount }}</div>
|
||
<div class="pstat-label">{{ t('settlement.resettle_affected') }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="pstat">
|
||
<div class="pstat-value pstat-green">{{ resettlePreview.totalTopup }}</div>
|
||
<div class="pstat-label">{{ t('settlement.resettle_topup') }}</div>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<div class="pstat">
|
||
<div class="pstat-value pstat-orange">{{ resettlePreview.totalClawback }}</div>
|
||
<div class="pstat-label">{{ t('settlement.resettle_clawback') }}</div>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<el-button type="warning" class="confirm-btn" @click="confirmResettle">
|
||
{{ t('settlement.resettle_confirm') }}
|
||
</el-button>
|
||
</el-card>
|
||
|
||
<el-card v-if="preview" class="preview-card preview-card--compact" shadow="never">
|
||
<div class="preview-bar">
|
||
<span class="preview-bar-title">{{ t('settlement.preview_title') }}</span>
|
||
<div class="preview-metrics">
|
||
<div class="preview-metric">
|
||
<span class="preview-metric-value">{{ preview.pendingBetCount ?? preview.singleBetCount }}</span>
|
||
<span class="preview-metric-label">{{ t('settlement.preview_pending_bets') }}</span>
|
||
</div>
|
||
<div class="preview-metric">
|
||
<span class="preview-metric-value">{{ preview.singleBetCount }} / {{ preview.parlayBetCount }}</span>
|
||
<span class="preview-metric-label">{{ t('settlement.preview_bet_mix') }}</span>
|
||
</div>
|
||
<div class="preview-metric">
|
||
<span class="preview-metric-value preview-metric-green">{{ formatAmount(String(preview.totalPayout ?? 0)) }}</span>
|
||
<span class="preview-metric-label">{{ t('settlement.est_payout') }}</span>
|
||
</div>
|
||
<div class="preview-metric">
|
||
<span class="preview-metric-value preview-metric-orange">{{ formatAmount(String(preview.totalRefund ?? 0)) }}</span>
|
||
<span class="preview-metric-label">{{ t('settlement.refund_amount') }}</span>
|
||
</div>
|
||
</div>
|
||
<el-button type="success" size="small" @click="confirm">
|
||
{{ t('settlement.confirm_btn') }}
|
||
</el-button>
|
||
</div>
|
||
<p v-if="previewZeroHint" class="preview-zero-hint">{{ previewZeroHint }}</p>
|
||
<div v-if="previewItemsPage.total > 0" class="preview-items-wrap">
|
||
<div class="preview-items-head">
|
||
<span class="preview-items-title">
|
||
{{ t('settlement.preview_items_title', { n: previewItemsPage.total }) }}
|
||
</span>
|
||
</div>
|
||
<el-table :data="previewItemsPage.items" size="small" stripe class="preview-items-table">
|
||
<el-table-column type="index" :index="(i: number) => (previewItemsPage.page - 1) * previewItemsPage.pageSize + i + 1" :label="t('common.seq')" width="55" align="center" />
|
||
<el-table-column :label="t('bet.col.bet_no')" prop="betNo" min-width="140" show-overflow-tooltip />
|
||
<el-table-column :label="t('common.type')" width="68">
|
||
<template #default="{ row }">{{ betTypeLabel(row.betType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('settlement.preview_col.result')" width="108">
|
||
<template #default="{ row }">{{ previewResultLabel(row.result) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('settlement.est_payout')" width="92" align="right">
|
||
<template #default="{ row }">{{ formatAmount(row.payout) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('user.field.remark')" min-width="160" show-overflow-tooltip prop="note" />
|
||
</el-table>
|
||
<el-pagination
|
||
v-if="previewItemsPage.total > 0"
|
||
class="preview-pager bet-pager"
|
||
size="small"
|
||
background
|
||
layout="total, sizes, prev, pager, next"
|
||
:total="previewItemsPage.total"
|
||
:current-page="previewItemsPage.page"
|
||
:page-size="previewItemsPage.pageSize"
|
||
:page-sizes="[10, 20, 50]"
|
||
@current-change="onPreviewPageChange"
|
||
@size-change="onPreviewPageSizeChange"
|
||
/>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-card v-loading="statsLoading" class="stats-card" shadow="never">
|
||
<div v-if="stats" class="stats-body">
|
||
<div class="stats-charts">
|
||
<div v-if="betTypeChartOption" class="mini-chart">
|
||
<VChart class="mini-chart-canvas" :option="betTypeChartOption" autoresize />
|
||
</div>
|
||
<div v-if="statusChartOption" class="mini-chart">
|
||
<VChart class="mini-chart-canvas" :option="statusChartOption" autoresize />
|
||
</div>
|
||
<div v-if="selectionStakeChartOption" class="mini-chart">
|
||
<VChart class="mini-chart-canvas" :option="selectionStakeChartOption" autoresize />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats-tables">
|
||
<div class="stats-table-block">
|
||
<div class="subsection-title">{{ t('settlement.stats_by_market') }}</div>
|
||
<el-table
|
||
v-if="stats.bySelection.length"
|
||
:data="stats.bySelection"
|
||
size="small"
|
||
stripe
|
||
class="stats-table"
|
||
height="100%"
|
||
>
|
||
<el-table-column :label="t('settlement.col.market')" min-width="120">
|
||
<template #default="{ row }">
|
||
{{ marketLabel(row.marketType) }}
|
||
<span v-if="row.period" class="period-tag">{{ row.period }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('settlement.col.selection')" min-width="120">
|
||
<template #default="{ row }">{{ selectionDisplay(row) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('settlement.col.legs')" width="72" align="center" prop="legCount" />
|
||
<el-table-column :label="t('settlement.col.single_stake')" width="100" align="right">
|
||
<template #default="{ row }">{{ formatAmount(row.singleStake) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('settlement.col.parlay_legs')" width="88" align="center" prop="parlayLegCount" />
|
||
</el-table>
|
||
<p v-else class="empty-hint">{{ t('settlement.no_bets') }}</p>
|
||
</div>
|
||
|
||
<div class="stats-table-block stats-table-block--bets">
|
||
<div class="subsection-title">
|
||
{{ t('settlement.bet_list') }} ({{ stats.bets.total }})
|
||
<span class="subsection-hint">{{ t('settlement.bet_list_hint') }}</span>
|
||
</div>
|
||
<div v-loading="betsLoading" class="table-wrap">
|
||
<el-table
|
||
v-if="stats.bets.items.length"
|
||
:data="stats.bets.items"
|
||
size="small"
|
||
stripe
|
||
class="stats-table"
|
||
>
|
||
<el-table-column type="index" :index="(i: number) => ((stats?.bets.page ?? 1) - 1) * (stats?.bets.pageSize ?? 10) + i + 1" :label="t('common.seq')" width="55" align="center" />
|
||
<el-table-column prop="betNo" :label="t('bet.col.bet_no')" width="140" />
|
||
<el-table-column prop="username" :label="t('bet.col.player')" width="96" />
|
||
<el-table-column :label="t('common.type')" width="72">
|
||
<template #default="{ row }">
|
||
{{ betTypeLabel(row.betType) }}
|
||
<span v-if="row.legCountOnMatch > 1" class="leg-badge">×{{ row.legCountOnMatch }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('bet.col.content')" min-width="200">
|
||
<template #default="{ row }">
|
||
<span class="bet-content-cell">{{ matchBetSelectionSummary(row) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('bet.col.stake')" width="88" align="right">
|
||
<template #default="{ row }">{{ formatAmount(row.stake) }}</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('common.status')" width="88">
|
||
<template #default="{ row }">
|
||
<el-tag size="small" :type="betStatusTagType(row.status)">{{ betStatusLabel(row.status) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column :label="t('bet.col.placed_at')" width="120">
|
||
<template #default="{ row }">{{ formatTime(row.placedAt) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<p v-else class="empty-hint">{{ t('settlement.no_bets') }}</p>
|
||
</div>
|
||
<el-pagination
|
||
v-if="stats.bets.total > 0"
|
||
class="bet-pager"
|
||
size="small"
|
||
background
|
||
layout="total, sizes, prev, pager, next"
|
||
:total="stats.bets.total"
|
||
:current-page="stats.bets.page"
|
||
:page-size="stats.bets.pageSize"
|
||
:page-sizes="[10, 20, 50]"
|
||
@current-change="onBetPageChange"
|
||
@size-change="onBetPageSizeChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.settlement-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
height: 100%;
|
||
min-height: 0;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.settle-top-card,
|
||
.preview-card {
|
||
flex-shrink: 0;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.settle-top-card :deep(.el-card__body) {
|
||
padding: 14px 16px;
|
||
}
|
||
|
||
.stats-card {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.stats-card :deep(.el-card__body) {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 10px 12px 12px;
|
||
}
|
||
|
||
.stats-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.settle-score-row {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
gap: 16px 24px;
|
||
flex-wrap: wrap;
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.outright-settle-head {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: baseline;
|
||
gap: 8px 16px;
|
||
}
|
||
|
||
.outright-settle-title {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: var(--success-text);
|
||
}
|
||
|
||
.outright-winner-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
min-width: 240px;
|
||
flex: 1;
|
||
}
|
||
|
||
.outright-winner-select {
|
||
width: min(100%, 360px);
|
||
}
|
||
|
||
.settle-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.settle-resettle-reason {
|
||
width: 220px;
|
||
}
|
||
|
||
.match-inline {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 12px 16px;
|
||
flex: 1;
|
||
min-width: 280px;
|
||
}
|
||
|
||
.team-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.team-logo-sm {
|
||
width: 36px;
|
||
height: 36px;
|
||
object-fit: contain;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.team-name-inline {
|
||
font-size: 16px;
|
||
font-weight: 750;
|
||
color: var(--text);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.kickoff-inline {
|
||
font-size: 13px;
|
||
color: var(--text-muted);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.kickoff-inline .meta-k {
|
||
color: #aaa49a;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.score-inline-group {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
flex-wrap: wrap;
|
||
gap: 16px 20px;
|
||
}
|
||
|
||
.score-block.compact {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.score-block.compact .score-title {
|
||
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;
|
||
color: #e0e0e0;
|
||
margin-bottom: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.subsection-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #aaa;
|
||
margin: 0 0 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.subsection-hint {
|
||
margin-left: 8px;
|
||
font-size: 10px;
|
||
font-weight: 400;
|
||
color: #666;
|
||
}
|
||
|
||
.leg-badge {
|
||
margin-left: 4px;
|
||
font-size: 10px;
|
||
color: var(--warning-text);
|
||
}
|
||
|
||
.bet-content-cell {
|
||
font-size: 12px;
|
||
color: #bbb;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.stats-charts {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.mini-chart {
|
||
border: 1px solid #2a2a2a;
|
||
border-radius: 10px;
|
||
background: rgba(255, 255, 255, 0.02);
|
||
padding: 4px 6px 0;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.mini-chart-canvas {
|
||
height: 148px;
|
||
width: 100%;
|
||
}
|
||
|
||
.stats-tables {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1.4fr;
|
||
gap: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.stats-table-block {
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.stats-table-block--bets .table-wrap {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: auto;
|
||
}
|
||
|
||
.bet-pager {
|
||
flex-shrink: 0;
|
||
margin-top: 8px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.stats-table {
|
||
width: 100%;
|
||
}
|
||
|
||
.period-tag {
|
||
margin-left: 6px;
|
||
font-size: 11px;
|
||
color: #666;
|
||
}
|
||
|
||
.empty-hint {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
color: #666;
|
||
text-align: center;
|
||
padding: 16px 0;
|
||
}
|
||
|
||
.match-league {
|
||
margin: 0 0 10px;
|
||
font-size: 13px;
|
||
color: #888;
|
||
}
|
||
|
||
.vs {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--warning-text);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.score-title {
|
||
font-size: 13px;
|
||
color: #999;
|
||
margin-bottom: 8px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.score-inputs {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.score-sep {
|
||
font-size: 18px;
|
||
color: #666;
|
||
font-weight: 300;
|
||
}
|
||
|
||
.preview-hint {
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
.preview-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #e0e0e0;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.preview-card--compact :deep(.el-card__body) {
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
.preview-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.preview-bar-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #e0e0e0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.preview-metrics {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
flex: 1;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.preview-metric {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
}
|
||
|
||
.preview-metric-value {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.preview-metric-green {
|
||
color: var(--green-glow);
|
||
}
|
||
|
||
.preview-metric-orange {
|
||
color: #e8a040;
|
||
}
|
||
|
||
.preview-metric-label {
|
||
font-size: 12px;
|
||
color: #888;
|
||
}
|
||
|
||
.preview-zero-hint {
|
||
margin: 10px 0 0;
|
||
padding: 8px 10px;
|
||
border-radius: 6px;
|
||
background: rgba(232, 160, 64, 0.12);
|
||
border: 1px solid rgba(232, 160, 64, 0.25);
|
||
color: #e8c080;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.preview-items-wrap {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.preview-items-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.preview-items-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #aaa;
|
||
}
|
||
|
||
.preview-items-table {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.preview-pager {
|
||
margin-top: 8px;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.pstat {
|
||
padding: 12px;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border: 1px solid #2a2a2a;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
}
|
||
|
||
.pstat-value {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: #e0e0e0;
|
||
}
|
||
|
||
.pstat-green {
|
||
color: var(--green-glow);
|
||
text-shadow: none;
|
||
}
|
||
|
||
.pstat-orange {
|
||
color: #e8a040;
|
||
}
|
||
|
||
.pstat-label {
|
||
font-size: 12px;
|
||
color: #888;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.confirm-btn {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
@media (max-width: 1100px) {
|
||
.stats-charts {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.stats-tables {
|
||
grid-template-columns: 1fr;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.stats-table-block {
|
||
min-height: 200px;
|
||
}
|
||
|
||
}
|
||
|
||
.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>
|