feat(admin,api,player): 结算预览分页、统计图表与返水限额
完善结算计算与预览 API(含后端分页),加强管理端结算/返水/权限,并优化玩家端投注单与队徽展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,28 +1,90 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import type { TableColumnCtx } from 'element-plus';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import { formatAmount, formatAmountFull } from '../utils/format-amount';
|
||||
import api from '../api';
|
||||
|
||||
interface CashbackBatch {
|
||||
id: string;
|
||||
batchNo: string;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
playerCount: number;
|
||||
totalAmount: string;
|
||||
}
|
||||
|
||||
interface CashbackPreviewItem {
|
||||
userId: string;
|
||||
username: string;
|
||||
agentUsername: string | null;
|
||||
effectiveStake: string;
|
||||
rate: string;
|
||||
amount: string;
|
||||
}
|
||||
|
||||
interface CashbackPreview {
|
||||
batch: CashbackBatch;
|
||||
items: CashbackPreviewItem[];
|
||||
totalAmount: string;
|
||||
}
|
||||
|
||||
const { t } = useAdminLocale();
|
||||
|
||||
const preview = ref<Record<string, unknown> | null>(null);
|
||||
const preview = ref<CashbackPreview | null>(null);
|
||||
const rulesVisible = ref(false);
|
||||
const loading = ref(false);
|
||||
const period = ref({
|
||||
start: new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10),
|
||||
end: new Date().toISOString().slice(0, 10),
|
||||
});
|
||||
|
||||
async function generatePreview() {
|
||||
const { data } = await api.post('/admin/cashbacks/preview', {
|
||||
periodStart: period.value.start,
|
||||
periodEnd: period.value.end,
|
||||
const previewItems = computed(() => preview.value?.items ?? []);
|
||||
|
||||
function formatRate(value: string | number | null | undefined) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) return '—';
|
||||
return `${(n * 100).toFixed(2)}%`;
|
||||
}
|
||||
|
||||
function formatPeriodDate(value: string) {
|
||||
if (!value) return '—';
|
||||
return value.slice(0, 10);
|
||||
}
|
||||
|
||||
function tableSummary(param: {
|
||||
columns: TableColumnCtx<CashbackPreviewItem>[];
|
||||
data: CashbackPreviewItem[];
|
||||
}) {
|
||||
const { columns, data } = param;
|
||||
const stakeSum = data.reduce((s, row) => s + Number(row.effectiveStake), 0);
|
||||
const amountSum = data.reduce((s, row) => s + Number(row.amount), 0);
|
||||
|
||||
return columns.map((col, index) => {
|
||||
if (index === 0) return t('cashback.table_total');
|
||||
if (col.property === 'effectiveStake') return formatAmount(stakeSum);
|
||||
if (col.property === 'amount') return formatAmount(amountSum);
|
||||
return '';
|
||||
});
|
||||
preview.value = data.data;
|
||||
}
|
||||
|
||||
async function generatePreview() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await api.post('/admin/cashbacks/preview', {
|
||||
periodStart: period.value.start,
|
||||
periodEnd: period.value.end,
|
||||
});
|
||||
preview.value = data.data as CashbackPreview;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
if (!preview.value?.batch) return;
|
||||
await api.post(`/admin/cashbacks/${(preview.value.batch as { id: string }).id}/confirm`);
|
||||
await api.post(`/admin/cashbacks/${preview.value.batch.id}/confirm`);
|
||||
ElMessage.success(t('msg.cashback_issued'));
|
||||
preview.value = null;
|
||||
}
|
||||
@@ -30,49 +92,261 @@ async function confirm() {
|
||||
|
||||
<template>
|
||||
<div class="page-scroll">
|
||||
<el-card class="tool-card" shadow="never">
|
||||
<div class="filter-row">
|
||||
<el-form inline>
|
||||
<el-form-item :label="t('cashback.start_date')">
|
||||
<el-date-picker v-model="period.start" type="date" value-format="YYYY-MM-DD" style="width: 150px" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('cashback.end_date')">
|
||||
<el-date-picker v-model="period.end" type="date" value-format="YYYY-MM-DD" style="width: 150px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="generatePreview">{{ t('cashback.preview_btn') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="tool-card" shadow="never">
|
||||
<div class="filter-row">
|
||||
<el-form inline>
|
||||
<el-form-item :label="t('cashback.start_date')">
|
||||
<el-date-picker v-model="period.start" type="date" value-format="YYYY-MM-DD" style="width: 150px" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('cashback.end_date')">
|
||||
<el-date-picker v-model="period.end" type="date" value-format="YYYY-MM-DD" style="width: 150px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="generatePreview">
|
||||
{{ t('cashback.preview_btn') }}
|
||||
</el-button>
|
||||
<button
|
||||
type="button"
|
||||
class="rules-help-btn"
|
||||
:aria-label="t('cashback.rules_title')"
|
||||
:title="t('cashback.rules_title')"
|
||||
@click="rulesVisible = true"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="preview" class="preview-card" shadow="never">
|
||||
<div class="preview-title">{{ t('cashback.preview_title') }}</div>
|
||||
<el-row :gutter="20" class="preview-stats">
|
||||
<el-col :span="8">
|
||||
<div class="pstat">
|
||||
<div class="pstat-value">{{ (preview.batch as { playerCount: number })?.playerCount ?? 0 }}</div>
|
||||
<div class="pstat-label">{{ t('cashback.stat.players') }}</div>
|
||||
<el-dialog
|
||||
v-model="rulesVisible"
|
||||
:title="t('cashback.rules_title')"
|
||||
width="560px"
|
||||
destroy-on-close
|
||||
>
|
||||
<ul class="rules-list">
|
||||
<li>{{ t('cashback.rule_period') }}</li>
|
||||
<li>{{ t('cashback.rule_eligible') }}</li>
|
||||
<li>{{ t('cashback.rule_formula') }}</li>
|
||||
<li>{{ t('cashback.rule_rate') }}</li>
|
||||
<li>{{ t('cashback.rule_flow') }}</li>
|
||||
<li class="rules-note">{{ t('cashback.rule_note_zero') }}</li>
|
||||
</ul>
|
||||
</el-dialog>
|
||||
|
||||
<template v-if="preview">
|
||||
<el-card class="preview-card" shadow="never">
|
||||
<div class="preview-head">
|
||||
<div>
|
||||
<div class="preview-title">{{ t('cashback.preview_title') }}</div>
|
||||
<div class="preview-meta">
|
||||
<span>{{ t('cashback.batch_no') }}:{{ preview.batch.batchNo }}</span>
|
||||
<span class="meta-sep">·</span>
|
||||
<span>
|
||||
{{ formatPeriodDate(preview.batch.periodStart) }}
|
||||
—
|
||||
{{ formatPeriodDate(preview.batch.periodEnd) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="pstat">
|
||||
<div class="pstat-value">{{ preview.totalAmount }}</div>
|
||||
<div class="pstat-label">{{ t('cashback.stat.total') }}</div>
|
||||
|
||||
<el-row :gutter="20" class="preview-stats">
|
||||
<el-col :span="8">
|
||||
<div class="pstat">
|
||||
<div class="pstat-value">{{ preview.batch.playerCount ?? 0 }}</div>
|
||||
<div class="pstat-label">{{ t('cashback.stat.players') }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="pstat">
|
||||
<div class="pstat-value pstat-green">{{ formatAmount(preview.totalAmount) }}</div>
|
||||
<div class="pstat-label">{{ t('cashback.stat.total') }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="pstat">
|
||||
<div class="pstat-value">{{ previewItems.length }}</div>
|
||||
<div class="pstat-label">{{ t('cashback.stat.lines') }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<el-card class="data-card" shadow="never">
|
||||
<div class="table-title">{{ t('cashback.table_title') }}</div>
|
||||
<div class="table-wrap">
|
||||
<el-table
|
||||
:data="previewItems"
|
||||
stripe
|
||||
class="cashback-table"
|
||||
show-summary
|
||||
:summary-method="tableSummary"
|
||||
:empty-text="t('cashback.empty_items')"
|
||||
>
|
||||
<el-table-column type="index" :label="t('cashback.col.index')" width="56" align="center" />
|
||||
<el-table-column
|
||||
prop="username"
|
||||
:label="t('cashback.col.player')"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="agentUsername"
|
||||
:label="t('cashback.col.agent')"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.agentUsername || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="effectiveStake"
|
||||
:label="t('cashback.col.effective_stake')"
|
||||
min-width="120"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="formatAmountFull(row.effectiveStake)" placement="top">
|
||||
<span>{{ formatAmount(row.effectiveStake) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rate" :label="t('cashback.col.rate')" width="96" align="right">
|
||||
<template #default="{ row }">{{ formatRate(row.rate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" :label="t('cashback.col.amount')" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="formatAmountFull(row.amount)" placement="top">
|
||||
<span class="amount-cell">{{ formatAmount(row.amount) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-button type="success" @click="confirm" style="margin-top: 20px">{{ t('cashback.confirm_issue') }}</el-button>
|
||||
</el-card>
|
||||
|
||||
<el-button type="success" class="confirm-btn" :disabled="previewItems.length === 0" @click="confirm">
|
||||
{{ t('cashback.confirm_issue') }}
|
||||
</el-button>
|
||||
</el-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.tool-card { margin-bottom: 16px; border-radius: 12px; }
|
||||
.preview-card { border-radius: 12px; }
|
||||
.preview-title { font-size: 15px; font-weight: 600; color: #e0e0e0; margin-bottom: 16px; }
|
||||
.preview-stats { }
|
||||
.pstat { padding: 16px; background: #f9f9fb; border-radius: 10px; text-align: center; }
|
||||
.pstat-value { font-size: 26px; font-weight: 700; color: var(--green-glow); }
|
||||
.pstat-label { font-size: 12px; color: #3a3a3a; margin-top: 4px; }
|
||||
.tool-card { margin-bottom: 16px; border-radius: 12px; }
|
||||
|
||||
.rules-help-btn {
|
||||
margin-left: 10px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #3a3a3a;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: #aaa;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.rules-help-btn:hover {
|
||||
color: var(--green-glow);
|
||||
border-color: rgba(47, 181, 106, 0.45);
|
||||
background: rgba(47, 181, 106, 0.08);
|
||||
}
|
||||
|
||||
.rules-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
font-size: 13px;
|
||||
line-height: 1.75;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.rules-note {
|
||||
color: #888;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.preview-card,
|
||||
.data-card {
|
||||
border-radius: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.preview-meta {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.meta-sep {
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.preview-stats {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pstat {
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pstat-value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.pstat-green {
|
||||
color: var(--green-glow);
|
||||
text-shadow: 0 0 20px rgba(47, 181, 106, 0.35);
|
||||
}
|
||||
|
||||
.pstat-label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.amount-cell {
|
||||
color: var(--green-glow);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user