sync(theme-3): 同步 main 最新 API/Admin 与玩家端逻辑
从 main 同步站内信手动发送、媒体库选择、列表子路由重构等 API/Admin 改动;玩家端仅合并 ADMIN_CUSTOM 富文本、消息预览与充值截图压缩逻辑,保留 theme-3 样式。
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { ref, computed, onMounted, defineAsyncComponent, watch, nextTick } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { formatApiErrorMessage, isApiErrorCode } from '@thebet365/shared';
|
||||
import api from '../api';
|
||||
@@ -75,6 +75,12 @@ const { hasPermission } = usePermissions();
|
||||
const canResettle = computed(() => hasPermission(AdminPerm.resettle));
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function settlementReturnTo(isOutright = false) {
|
||||
const q = route.query.returnTo;
|
||||
if (typeof q === 'string' && q.startsWith('/')) return q;
|
||||
return isOutright ? '/matches/outrights' : '/matches';
|
||||
}
|
||||
const STAT_FACT_LABELS = {
|
||||
homeCorners: {
|
||||
'zh-CN': '主队角球',
|
||||
@@ -115,14 +121,14 @@ const matchStats = ref<{
|
||||
|
||||
function emptyMatchStats() {
|
||||
return {
|
||||
homeCorners: null,
|
||||
awayCorners: null,
|
||||
homeYellowCards: null,
|
||||
awayYellowCards: null,
|
||||
homeRedCards: null,
|
||||
awayRedCards: null,
|
||||
homeCards: null,
|
||||
awayCards: null,
|
||||
homeCorners: 0,
|
||||
awayCorners: 0,
|
||||
homeYellowCards: 0,
|
||||
awayYellowCards: 0,
|
||||
homeRedCards: 0,
|
||||
awayRedCards: 0,
|
||||
homeCards: 0,
|
||||
awayCards: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,8 +161,29 @@ const winnerTeamId = ref('');
|
||||
const outrightSelections = ref<
|
||||
Array<{ teamId: string; teamCode: string; teamZh: string; teamEn: string }>
|
||||
>([]);
|
||||
interface ResettlePreviewItem {
|
||||
betId: string;
|
||||
betNo: string;
|
||||
oldPayout: string;
|
||||
newPayout: string;
|
||||
delta: string;
|
||||
oldStatus: string;
|
||||
newStatus: string;
|
||||
}
|
||||
|
||||
interface ResettlePreview {
|
||||
batch: {
|
||||
id: string;
|
||||
batchNo: string;
|
||||
};
|
||||
affectedCount: number;
|
||||
totalTopup: string;
|
||||
totalClawback: string;
|
||||
items: ResettlePreviewItem[];
|
||||
}
|
||||
|
||||
const preview = ref<Record<string, unknown> | null>(null);
|
||||
const resettlePreview = ref<Record<string, unknown> | null>(null);
|
||||
const resettlePreview = ref<ResettlePreview | null>(null);
|
||||
const resettleReason = ref('');
|
||||
const statsSummary = ref<Pick<SettlementBetStats, 'summary' | 'bySelection'> | null>(null);
|
||||
const betsList = ref<SettlementBetStats['bets'] | null>(null);
|
||||
@@ -171,12 +198,71 @@ const betPage = ref(1);
|
||||
const betPageSize = ref(10);
|
||||
const previewPage = ref(1);
|
||||
const previewPageSize = ref(10);
|
||||
const previewDialogVisible = ref(false);
|
||||
const resettleDialogVisible = ref(false);
|
||||
const confirmLoading = ref(false);
|
||||
const resettleConfirmLoading = ref(false);
|
||||
|
||||
const resettlePage = ref(1);
|
||||
const resettlePageSize = ref(10);
|
||||
|
||||
const resettleItemsPage = computed(() => {
|
||||
if (!resettlePreview.value?.items) return [];
|
||||
const start = (resettlePage.value - 1) * resettlePageSize.value;
|
||||
const end = start + resettlePageSize.value;
|
||||
return resettlePreview.value.items.slice(start, end);
|
||||
});
|
||||
|
||||
const isProgrammaticChange = ref(false);
|
||||
|
||||
interface SettlementHistoryRecord {
|
||||
id: string;
|
||||
batchNo: string;
|
||||
htHomeScore: number | null;
|
||||
htAwayScore: number | null;
|
||||
ftHomeScore: number | null;
|
||||
ftAwayScore: number | null;
|
||||
homeCorners: number | null;
|
||||
awayCorners: number | null;
|
||||
homeYellowCards: number | null;
|
||||
awayYellowCards: number | null;
|
||||
homeRedCards: number | null;
|
||||
awayRedCards: number | null;
|
||||
homeCards: number | null;
|
||||
awayCards: number | null;
|
||||
totalBets: number;
|
||||
totalPayout: string;
|
||||
totalRefund: string;
|
||||
confirmedAt: string | null;
|
||||
isResettle: boolean;
|
||||
reason: string | null;
|
||||
operatorUsername: string;
|
||||
}
|
||||
|
||||
const settlementHistory = ref<SettlementHistoryRecord[]>([]);
|
||||
const historyLoading = ref(false);
|
||||
const activeTab = ref<'bets' | 'history'>('bets');
|
||||
|
||||
async function loadSettlementHistory() {
|
||||
if (!matchId.value) return;
|
||||
historyLoading.value = true;
|
||||
try {
|
||||
const { data } = await api.get(`/admin/matches/${matchId.value}/settlement/history`);
|
||||
settlementHistory.value = data.data as SettlementHistoryRecord[];
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 智能比分推荐已暂时关闭(后端 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 '';
|
||||
@@ -372,6 +458,29 @@ function formatTime(v: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function formatScorePair(h: number | null, a: number | null) {
|
||||
if (h == null || a == null) return '—';
|
||||
return `${h}-${a}`;
|
||||
}
|
||||
|
||||
function formatHistoryScore(row: SettlementHistoryRecord) {
|
||||
const ht = formatScorePair(row.htHomeScore, row.htAwayScore);
|
||||
const ft = formatScorePair(row.ftHomeScore, row.ftAwayScore);
|
||||
if (ht === '—' && ft === '—') return '—';
|
||||
return `${ht} / ${ft}`;
|
||||
}
|
||||
|
||||
function formatHomeAwayPair(h: number | null, a: number | null) {
|
||||
if (h == null && a == null) return '—';
|
||||
return `${h ?? '—'} / ${a ?? '—'}`;
|
||||
}
|
||||
|
||||
function formatHistoryCards(row: SettlementHistoryRecord) {
|
||||
const { homeYellowCards: yH, awayYellowCards: yA, homeRedCards: rH, awayRedCards: rA } = row;
|
||||
if (yH == null && yA == null && rH == null && rA == null) return '—';
|
||||
return `Y:${yH ?? '—'}/${yA ?? '—'} R:${rH ?? '—'}/${rA ?? '—'}`;
|
||||
}
|
||||
|
||||
function matchBetSelectionSummary(
|
||||
row: SettlementBetStats['bets']['items'][number],
|
||||
) {
|
||||
@@ -436,6 +545,7 @@ function onBetPageSizeChange(size: number) {
|
||||
async function loadMatch() {
|
||||
if (!matchId.value) return;
|
||||
loading.value = true;
|
||||
isProgrammaticChange.value = true;
|
||||
try {
|
||||
const { data } = await api.get(`/admin/matches/${matchId.value}`);
|
||||
const detail = data.data as AdminMatchDetail;
|
||||
@@ -445,7 +555,7 @@ async function loadMatch() {
|
||||
detail.status === 'SETTLED';
|
||||
if (!settleable) {
|
||||
ElMessage.warning(t('settlement.must_close_first'));
|
||||
router.replace(detail.isOutright ? '/matches/outrights' : '/matches');
|
||||
router.replace(settlementReturnTo(detail.isOutright));
|
||||
return;
|
||||
}
|
||||
match.value = detail;
|
||||
@@ -457,14 +567,14 @@ async function loadMatch() {
|
||||
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,
|
||||
homeCorners: detail.score.homeCorners ?? 0,
|
||||
awayCorners: detail.score.awayCorners ?? 0,
|
||||
homeYellowCards: detail.score.homeYellowCards ?? 0,
|
||||
awayYellowCards: detail.score.awayYellowCards ?? 0,
|
||||
homeRedCards: detail.score.homeRedCards ?? 0,
|
||||
awayRedCards: detail.score.awayRedCards ?? 0,
|
||||
homeCards: detail.score.homeCards ?? 0,
|
||||
awayCards: detail.score.awayCards ?? 0,
|
||||
};
|
||||
winnerTeamId.value = detail.score.winnerTeamId ?? '';
|
||||
} else {
|
||||
@@ -495,11 +605,28 @@ async function loadMatch() {
|
||||
}
|
||||
betPage.value = 1;
|
||||
await loadStats();
|
||||
if (detail.status === 'PENDING_SETTLEMENT') {
|
||||
try {
|
||||
const previewRes = await api.get(`/admin/matches/${matchId.value}/settlement/preview`, {
|
||||
params: { page: 1, pageSize: previewPageSize.value }
|
||||
});
|
||||
if (previewRes.data.data) {
|
||||
preview.value = previewRes.data.data;
|
||||
const itemsPage = previewRes.data.data.items as PreviewItemsPage;
|
||||
previewPage.value = itemsPage.page;
|
||||
previewPageSize.value = itemsPage.pageSize;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load active settlement preview', e);
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
await nextTick();
|
||||
isProgrammaticChange.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,19 +635,34 @@ 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;
|
||||
try {
|
||||
const { data } = await api.post(`/admin/matches/${matchId.value}/resettle/preview`, {
|
||||
...payload,
|
||||
reason: resettleReason.value.trim() || undefined,
|
||||
});
|
||||
resettlePage.value = 1;
|
||||
resettlePreview.value = data.data;
|
||||
resettleDialogVisible.value = true;
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
resettleConfirmLoading.value = true;
|
||||
try {
|
||||
await api.post(`/admin/resettle/${(resettlePreview.value.batch as { id: string }).id}/confirm`);
|
||||
ElMessage.success(t('msg.resettle_confirmed'));
|
||||
resettlePreview.value = null;
|
||||
resettleDialogVisible.value = false;
|
||||
await loadMatch();
|
||||
void loadSettlementHistory();
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
|
||||
} finally {
|
||||
resettleConfirmLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function settlementApiError(e: unknown, fallback: string) {
|
||||
@@ -582,6 +724,7 @@ async function previewSettlement() {
|
||||
const itemsPage = data.data.items as PreviewItemsPage;
|
||||
previewPage.value = itemsPage.page;
|
||||
previewPageSize.value = itemsPage.pageSize;
|
||||
previewDialogVisible.value = true;
|
||||
await loadMatch();
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
|
||||
@@ -603,14 +746,41 @@ function onPreviewPageSizeChange(size: number) {
|
||||
|
||||
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();
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
await api.post(`/admin/settlement/${(preview.value.batch as { id: string }).id}/confirm`);
|
||||
ElMessage.success(t('msg.settlement_confirmed'));
|
||||
preview.value = null;
|
||||
previewDialogVisible.value = false;
|
||||
await loadMatch();
|
||||
void loadSettlementHistory();
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePreviewClick() {
|
||||
if (preview.value) {
|
||||
previewDialogVisible.value = true;
|
||||
} else {
|
||||
void previewSettlement();
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[score, matchStats, winnerTeamId],
|
||||
() => {
|
||||
if (isProgrammaticChange.value) return;
|
||||
preview.value = null;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
void loadMatch();
|
||||
void loadSettlementHistory();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -766,9 +936,9 @@ onMounted(() => {
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="previewing"
|
||||
@click="previewSettlement"
|
||||
@click="handlePreviewClick"
|
||||
>
|
||||
{{ t('settlement.preview_btn') }}
|
||||
{{ preview ? t('settlement.view_preview_btn') : t('settlement.preview_btn') }}
|
||||
</el-button>
|
||||
<span class="preview-hint">{{
|
||||
isOutright ? t('settlement.outright.preview_hint') : t('settlement.preview_hint')
|
||||
@@ -791,37 +961,94 @@ onMounted(() => {
|
||||
|
||||
<!-- 智能比分弹窗已关闭(见 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-dialog
|
||||
v-model="resettleDialogVisible"
|
||||
:title="t('settlement.resettle_preview_title')"
|
||||
width="850px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="resettlePreview">
|
||||
<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-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 v-if="resettlePreview.items && resettlePreview.items.length > 0" class="preview-items-wrap" style="margin-top: 20px;">
|
||||
<div class="preview-items-head" style="margin-bottom: 10px;">
|
||||
<span class="preview-items-title" style="font-size: 14px; font-weight: 600; color: var(--text);">
|
||||
{{ t('settlement.resettle_affected_list') }} ({{ resettlePreview.items.length }})
|
||||
</span>
|
||||
</div>
|
||||
<el-table :data="resettleItemsPage" size="small" stripe class="preview-items-table" style="max-height: 400px; overflow-y: auto;">
|
||||
<el-table-column type="index" :index="(i: number) => (resettlePage - 1) * resettlePageSize + 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('settlement.resettle_col.old_result')" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="betStatusTagType(row.oldStatus)" style="margin-right: 6px;">{{ betStatusLabel(row.oldStatus) }}</el-tag>
|
||||
<span class="old-payout" style="color: var(--text-muted); font-size: 11px;">{{ formatAmount(row.oldPayout) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('settlement.resettle_col.new_result')" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="betStatusTagType(row.newStatus)" style="margin-right: 6px;">{{ betStatusLabel(row.newStatus) }}</el-tag>
|
||||
<span class="new-payout" style="color: var(--text); font-size: 11px;">{{ formatAmount(row.newPayout) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('settlement.resettle_col.adjust')" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span :class="Number(row.delta) > 0 ? 'pstat-green' : Number(row.delta) < 0 ? 'pstat-orange' : ''" style="font-weight: bold;">
|
||||
{{ Number(row.delta) > 0 ? '+' : '' }}{{ formatAmount(row.delta) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="preview-pager bet-pager"
|
||||
size="small"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:total="resettlePreview.items.length"
|
||||
v-model:current-page="resettlePage"
|
||||
v-model:page-size="resettlePageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
style="margin-top: 12px; justify-content: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="resettleDialogVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="warning" :loading="resettleConfirmLoading" @click="confirmResettle">
|
||||
{{ t('settlement.resettle_confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 结算预览弹窗 -->
|
||||
<el-dialog
|
||||
v-model="previewDialogVisible"
|
||||
:title="t('settlement.preview_title')"
|
||||
width="850px"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="preview">
|
||||
<div class="preview-metrics" style="margin-bottom: 16px;">
|
||||
<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>
|
||||
@@ -839,147 +1066,201 @@ onMounted(() => {
|
||||
<span class="preview-metric-label">{{ t('settlement.refund_amount') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="success" size="small" @click="confirm">
|
||||
<p v-if="previewZeroHint" class="preview-zero-hint" style="margin-bottom: 16px;">{{ 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>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="previewDialogVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="success" :loading="confirmLoading" @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>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<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>
|
||||
<el-card v-loading="statsLoading && activeTab === 'bets'" class="stats-card" shadow="never">
|
||||
<el-tabs v-model="activeTab" class="settlement-tabs">
|
||||
<el-tab-pane name="bets" :label="t('settlement.history_tab_bets')">
|
||||
<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 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 class="stats-tables">
|
||||
<div class="stats-table-block">
|
||||
<div class="subsection-title">{{ t('settlement.stats_by_market') }}</div>
|
||||
<div class="table-wrap">
|
||||
<el-table
|
||||
v-if="stats.bySelection.length"
|
||||
:data="stats.bySelection"
|
||||
size="small"
|
||||
stripe
|
||||
class="stats-table"
|
||||
>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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-tab-pane>
|
||||
|
||||
<el-tab-pane name="history" :label="t('settlement.history_tab')">
|
||||
<div v-loading="historyLoading" class="history-body">
|
||||
<div class="table-wrap">
|
||||
<el-table
|
||||
:data="settlementHistory"
|
||||
size="small"
|
||||
stripe
|
||||
class="stats-table history-table"
|
||||
:empty-text="t('settlement.history.no_records')"
|
||||
>
|
||||
<el-table-column prop="batchNo" :label="t('settlement.history.col.batch_no')" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column :label="t('settlement.history.col.type')" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.isResettle ? 'warning' : 'success'">
|
||||
{{ row.isResettle ? t('settlement.history.type.resettle') : t('settlement.history.type.initial') }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('settlement.history.col.score')" min-width="120">
|
||||
<template #default="{ row }">{{ formatHistoryScore(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('settlement.history.col.corners')" width="100">
|
||||
<template #default="{ row }">{{ formatHomeAwayPair(row.homeCorners, row.awayCorners) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('settlement.history.col.cards')" min-width="120">
|
||||
<template #default="{ row }">{{ formatHistoryCards(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalBets" :label="t('settlement.history.col.total_bets')" width="96" align="right" />
|
||||
<el-table-column :label="t('settlement.history.col.total_payout')" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatAmount(row.totalPayout) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('settlement.history.col.total_refund')" width="100" align="right">
|
||||
<template #default="{ row }">{{ formatAmount(row.totalRefund) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operatorUsername" :label="t('settlement.history.col.operator')" width="96" show-overflow-tooltip />
|
||||
<el-table-column :label="t('settlement.history.col.time')" width="120">
|
||||
<template #default="{ row }">{{ row.confirmedAt ? formatTime(row.confirmedAt) : '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('settlement.history.col.reason')" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.reason || '—' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1031,6 +1312,72 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settlement-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settlement-tabs :deep(.el-tabs__header) {
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settlement-tabs :deep(.el-tabs__nav-wrap) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.settlement-tabs :deep(.el-tabs__item) {
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.settlement-tabs :deep(.el-tabs__item.is-active) {
|
||||
color: #d4fde5;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.settlement-tabs :deep(.el-tabs__active-bar) {
|
||||
background-color: var(--gold-bright);
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.settlement-tabs :deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settlement-tabs :deep(.el-tab-pane) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.history-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.history-body .table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.history-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settle-score-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -1225,7 +1572,7 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-table-block--bets .table-wrap {
|
||||
.stats-table-block .table-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
@@ -1487,7 +1834,7 @@ onMounted(() => {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stats-table-block--bets .table-wrap {
|
||||
.stats-table-block .table-wrap {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user