Files
thebet365/apps/admin/src/views/Bets.vue
Mars 80adc0e928 feat(admin): 管理端列表分页、控制台图表与赛事导入
- 玩家/代理/赛事/注单/审计列表分页,默认每页 10 条,无页面滚动条布局

- ECharts 控制台概览、注单管理中文化与列宽优化

- zhibo 赛事字段迁移与导入,玩家编辑可改所属代理

- 管理端 API 分页与 dashboard 统计接口

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 13:49:31 +08:00

320 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import api from '../api';
import { formatAmount, formatAmountFull } from '../utils/format-amount';
import {
betStatusLabel,
betStatusTagType,
betTypeLabel,
betSettlementLabel,
BET_STATUS_OPTIONS,
BET_TYPE_OPTIONS,
} from '../utils/bet-labels';
import type { BetListRow, BetDetail } from './bet-form';
const bets = ref<BetListRow[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(10);
const keyword = ref('');
const filterStatus = ref('');
const filterBetType = ref('');
const placedFrom = ref('');
const placedTo = ref('');
const detailVisible = ref(false);
const detail = ref<BetDetail | null>(null);
const detailLoading = ref(false);
onMounted(load);
async function load() {
const { data } = await api.get('/admin/bets', {
params: {
page: page.value,
pageSize: pageSize.value,
keyword: keyword.value.trim() || undefined,
status: filterStatus.value || undefined,
betType: filterBetType.value || undefined,
placedFrom: placedFrom.value || undefined,
placedTo: placedTo.value || undefined,
},
});
bets.value = data.data.items as BetListRow[];
total.value = data.data.total;
}
function onPageChange(p: number) {
page.value = p;
load();
}
function onSizeChange(size: number) {
pageSize.value = size;
page.value = 1;
load();
}
function resetFilters() {
keyword.value = '';
filterStatus.value = '';
filterBetType.value = '';
placedFrom.value = '';
placedTo.value = '';
page.value = 1;
load();
}
function parentLabel(row: BetListRow) {
return row.parentUsername ?? '平台直属';
}
function formatTime(v: string | null | undefined) {
if (!v) return '—';
return new Date(v).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
}
function resultStatusLabel(s: string | null | undefined) {
if (!s) return '—';
const map: Record<string, string> = {
WON: '赢',
LOST: '输',
VOID: '走水',
PUSH: '走盘',
HALF_WON: '半赢',
HALF_LOST: '半输',
};
return map[s] ?? s;
}
async function openDetail(row: BetListRow) {
detailLoading.value = true;
detailVisible.value = true;
detail.value = null;
try {
const { data } = await api.get(`/admin/bets/${row.id}`);
detail.value = data.data as BetDetail;
} finally {
detailLoading.value = false;
}
}
</script>
<template>
<div class="admin-list-page">
<div class="page-header">
<h2 class="page-title">注单管理</h2>
<span class="page-desc">筛选分页查看全平台注单支持详情与投注项</span>
</div>
<el-card class="filter-card" shadow="never">
<el-form inline>
<el-form-item label="关键词">
<el-input
v-model="keyword"
placeholder="流水编号 / 玩家用户名"
clearable
style="width: 200px"
@keyup.enter="load"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filterStatus" placeholder="全部" clearable style="width: 120px">
<el-option
v-for="o in BET_STATUS_OPTIONS.filter((x) => x.value !== '')"
:key="o.value"
:label="o.label"
:value="o.value"
/>
</el-select>
</el-form-item>
<el-form-item label="类型">
<el-select v-model="filterBetType" placeholder="全部" clearable style="width: 100px">
<el-option
v-for="o in BET_TYPE_OPTIONS.filter((x) => x.value !== '')"
:key="o.value"
:label="o.label"
:value="o.value"
/>
</el-select>
</el-form-item>
<el-form-item label="投注日起">
<el-date-picker
v-model="placedFrom"
type="date"
value-format="YYYY-MM-DD"
placeholder="开始"
style="width: 140px"
/>
</el-form-item>
<el-form-item label="止">
<el-date-picker
v-model="placedTo"
type="date"
value-format="YYYY-MM-DD"
placeholder="结束"
style="width: 140px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="data-card" shadow="never">
<div class="table-wrap">
<el-table :data="bets" stripe>
<el-table-column prop="id" label="单号" width="56" align="center" />
<el-table-column prop="betNo" label="流水编号" width="168" show-overflow-tooltip>
<template #default="{ row }">
<span class="bet-no">{{ row.betNo }}</span>
</template>
</el-table-column>
<el-table-column prop="username" label="玩家" width="100" show-overflow-tooltip />
<el-table-column label="所属代理" width="100" show-overflow-tooltip>
<template #default="{ row }">{{ parentLabel(row) }}</template>
</el-table-column>
<el-table-column label="类型" width="72" align="center">
<template #default="{ row }">
<el-tag type="info" size="small" effect="plain">{{ betTypeLabel(row.betType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="选项" width="52" align="center">
<template #default="{ row }">{{ row.selectionCount }}</template>
</el-table-column>
<el-table-column label="投注额" width="96" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.stake)" placement="top">
<span>{{ formatAmount(row.stake) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="赔率" width="72" align="right">
<template #default="{ row }">{{ row.totalOdds ?? '—' }}</template>
</el-table-column>
<el-table-column label="派彩" width="96" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.actualReturn)" placement="top">
<span>{{ formatAmount(row.actualReturn) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="状态" width="88" align="center">
<template #default="{ row }">
<el-tag :type="betStatusTagType(row.status)" size="small">
{{ betStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="投注时间" width="160">
<template #default="{ row }">{{ formatTime(row.placedAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="88" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="openDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="pager">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
background
@current-change="onPageChange"
@size-change="onSizeChange"
/>
</div>
</el-card>
<el-dialog v-model="detailVisible" title="注单详情" width="720px" destroy-on-close>
<div v-loading="detailLoading">
<template v-if="detail">
<el-descriptions :column="2" border size="small" class="detail-desc">
<el-descriptions-item label="单号">{{ detail.id }}</el-descriptions-item>
<el-descriptions-item label="流水编号">{{ detail.betNo }}</el-descriptions-item>
<el-descriptions-item label="玩家">{{ detail.username }}</el-descriptions-item>
<el-descriptions-item label="所属代理">{{ parentLabel(detail) }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ betTypeLabel(detail.betType) }}</el-descriptions-item>
<el-descriptions-item label="币种">{{ detail.currency }}</el-descriptions-item>
<el-descriptions-item label="投注额">
{{ formatAmountFull(detail.stake) }}
</el-descriptions-item>
<el-descriptions-item label="总赔率">{{ detail.totalOdds ?? '—' }}</el-descriptions-item>
<el-descriptions-item label="可赢额">
{{ detail.potentialReturn ? formatAmountFull(detail.potentialReturn) : '—' }}
</el-descriptions-item>
<el-descriptions-item label="实际派彩">
{{ formatAmountFull(detail.actualReturn) }}
</el-descriptions-item>
<el-descriptions-item label="注单状态">
<el-tag :type="betStatusTagType(detail.status)" size="small">
{{ betStatusLabel(detail.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="结算状态">
{{ betSettlementLabel(detail.settlementStatus) }}
</el-descriptions-item>
<el-descriptions-item label="投注时间">{{ formatTime(detail.placedAt) }}</el-descriptions-item>
<el-descriptions-item label="结算时间">{{ formatTime(detail.settledAt) }}</el-descriptions-item>
<el-descriptions-item label="请求 ID" :span="2">{{ detail.requestId }}</el-descriptions-item>
</el-descriptions>
<div class="selections-title">投注项{{ detail.selections.length }}</div>
<el-table :data="detail.selections" size="small" stripe border>
<el-table-column type="index" label="#" width="44" />
<el-table-column prop="selectionName" label="选项" min-width="120" show-overflow-tooltip />
<el-table-column prop="marketType" label="玩法" width="100" />
<el-table-column prop="period" label="时段" width="72">
<template #default="{ row }">{{ row.period ?? '—' }}</template>
</el-table-column>
<el-table-column prop="odds" label="赔率" width="72" align="right" />
<el-table-column label="盘口" width="88">
<template #default="{ row }">
{{ row.handicapLine ?? row.totalLine ?? '—' }}
</template>
</el-table-column>
<el-table-column label="赛果" width="72" align="center">
<template #default="{ row }">{{ resultStatusLabel(row.resultStatus) }}</template>
</el-table-column>
</el-table>
</template>
</div>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.page-header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 20px; }
.page-title { font-size: 20px; font-weight: 700; color: #e0e0e0; }
.page-desc { font-size: 13px; color: #666; }
.filter-card { margin-bottom: 16px; border-radius: 12px; }
.data-card { border-radius: 12px; }
.bet-no { font-size: 12px; color: #ccc; font-family: ui-monospace, monospace; }
.pager { display: flex; justify-content: flex-end; margin-top: 16px; }
.detail-desc { margin-bottom: 16px; }
.selections-title {
font-size: 13px;
font-weight: 700;
color: #aaa;
margin-bottom: 10px;
}
</style>