feat(admin): 管理端列表分页、控制台图表与赛事导入

- 玩家/代理/赛事/注单/审计列表分页,默认每页 10 条,无页面滚动条布局

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

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 13:49:31 +08:00
parent 2c356b2048
commit 80adc0e928
45 changed files with 6564 additions and 499 deletions

View File

@@ -2,51 +2,468 @@
import { ref, onMounted } from 'vue';
import api from '../api';
import { ElMessage } from 'element-plus';
import {
emptyAgentCreateForm,
emptyAgentEditForm,
editFormFromAgentDetail,
buildCreateAgentPayload,
type AgentRow,
type AgentDetail,
type AgentCreateForm,
type AgentEditForm,
} from './agent-form';
import {
formatAmount,
formatAmountFull,
shouldCompactAmount as shouldCompact,
} from '../utils/format-amount';
const agents = ref<unknown[]>([]);
const form = ref({ username: '', password: 'Agent@123', creditLimit: 50000 });
const agents = ref<AgentRow[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(10);
const keyword = ref('');
function creditLine(row: AgentRow) {
return `${formatAmount(row.creditLimit)} / ${formatAmount(row.usedCredit)} / ${formatAmount(row.availableCredit)}`;
}
function creditLineFull(row: AgentRow) {
return `${formatAmountFull(row.creditLimit)} / ${formatAmountFull(row.usedCredit)} / ${formatAmountFull(row.availableCredit)}`;
}
const createVisible = ref(false);
const editVisible = ref(false);
const detailVisible = ref(false);
const creditVisible = ref(false);
const createLoading = ref(false);
const editLoading = ref(false);
const creditLoading = ref(false);
const createForm = ref<AgentCreateForm>(emptyAgentCreateForm());
const editForm = ref<AgentEditForm>(emptyAgentEditForm());
const detail = ref<AgentDetail | null>(null);
const editingId = ref('');
const creditForm = ref({ amount: 10000, remark: '' });
onMounted(load);
async function load() {
const { data } = await api.get('/admin/agents');
agents.value = data.data;
}
async function create() {
await api.post('/admin/agents', form.value);
ElMessage.success('创建成功');
load();
}
async function adjustCredit(agent: { userId: string }, amount: number) {
await api.post(`/admin/agents/${agent.userId}/credit`, {
amount,
requestId: `credit-${Date.now()}`,
const { data } = await api.get('/admin/agents', {
params: {
page: page.value,
pageSize: pageSize.value,
keyword: keyword.value.trim() || undefined,
},
});
ElMessage.success('额度已调整');
agents.value = data.data.items as AgentRow[];
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 openCreate() {
createForm.value = emptyAgentCreateForm();
createVisible.value = true;
}
async function openDetail(userId: string) {
const { data } = await api.get(`/admin/agents/${userId}`);
detail.value = data.data as AgentDetail;
detailVisible.value = true;
}
async function openEdit(userId: string) {
const { data } = await api.get(`/admin/agents/${userId}`);
const d = data.data as AgentDetail;
editingId.value = userId;
editForm.value = editFormFromAgentDetail(d);
editVisible.value = true;
}
function openCredit(row: AgentRow) {
editingId.value = row.userId;
creditForm.value = { amount: 10000, remark: '' };
creditVisible.value = true;
}
async function submitCreate() {
let payload: ReturnType<typeof buildCreateAgentPayload>;
try {
payload = buildCreateAgentPayload(createForm.value);
} catch (e) {
ElMessage.warning(e instanceof Error ? e.message : '请检查表单');
return;
}
createLoading.value = true;
try {
await api.post('/admin/agents', payload);
ElMessage.success('一级代理已创建');
createVisible.value = false;
load();
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? '创建失败');
} finally {
createLoading.value = false;
}
}
async function submitEdit() {
editLoading.value = true;
try {
await api.put(`/admin/agents/${editingId.value}`, {
status: editForm.value.status,
phone: editForm.value.phone.trim() || undefined,
email: editForm.value.email.trim() || undefined,
cashbackRate: editForm.value.cashbackRate,
});
ElMessage.success('已保存');
editVisible.value = false;
load();
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? '保存失败');
} finally {
editLoading.value = false;
}
}
async function submitCredit() {
if (creditForm.value.amount === 0) {
ElMessage.warning('调整金额不能为 0');
return;
}
creditLoading.value = true;
try {
await api.post(`/admin/agents/${editingId.value}/credit`, {
amount: creditForm.value.amount,
requestId: `credit-${editingId.value}-${Date.now()}`,
remark: creditForm.value.remark || undefined,
});
ElMessage.success('授信已调整');
creditVisible.value = false;
load();
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? '调整失败');
} finally {
creditLoading.value = false;
}
}
function formatTime(v: string) {
if (!v) return '—';
return new Date(v).toLocaleString('zh-CN');
}
function statusTagType(s: string) {
return s === 'ACTIVE' ? 'success' : 'warning';
}
function statusLabel(s: string) {
return s === 'ACTIVE' ? '正常' : s === 'SUSPENDED' ? '停用' : s;
}
function creditTypeLabel(t: string) {
if (t === 'CREDIT_INCREASE') return '增加';
if (t === 'CREDIT_DECREASE') return '减少';
return t;
}
</script>
<template>
<h2>代理管理</h2>
<el-form inline style="margin: 16px 0">
<el-input v-model="form.username" placeholder="用户名" style="width: 120px" />
<el-input-number v-model="form.creditLimit" placeholder="额度" />
<el-button type="primary" @click="create">创建一级代理</el-button>
</el-form>
<el-table :data="agents">
<el-table-column label="用户名">
<template #default="{ row }">{{ (row as { user?: { username: string } }).user?.username }}</template>
</el-table-column>
<el-table-column prop="level" label="层级" />
<el-table-column prop="creditLimit" label="授信额度" />
<el-table-column prop="usedCredit" label="已用额度" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button size="small" @click="adjustCredit(row as { userId: string }, 10000)">+10000</el-button>
</template>
</el-table-column>
</el-table>
<div class="admin-list-page">
<div class="page-header">
<div>
<h2 class="page-title">代理管理</h2>
<span class="page-desc">创建一级代理调整授信额度查看直属玩家与额度占用</span>
</div>
<el-button type="primary" @click="openCreate">+ 新建一级代理</el-button>
</div>
<el-card class="filter-card" shadow="never">
<el-form inline>
<el-form-item label="关键词">
<el-input
v-model="keyword"
placeholder="用户名"
clearable
style="width: 180px"
@keyup.enter="load"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="data-card" shadow="never">
<div class="table-wrap">
<el-table :data="agents" stripe>
<el-table-column prop="userId" label="ID" width="72" />
<el-table-column prop="username" label="用户名" min-width="120" />
<el-table-column label="状态" width="88">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">
{{ statusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="level" label="层级" width="72" align="center" />
<el-table-column label="授信 / 已用 / 可用" min-width="168" align="right">
<template #default="{ row }">
<el-tooltip :content="creditLineFull(row)" placement="top">
<span class="amount-compact">{{ creditLine(row) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="directPlayerCount" label="直属玩家" width="96" align="center" />
<el-table-column label="返水率" width="88" align="right">
<template #default="{ row }">{{ row.cashbackRate }}</template>
</el-table-column>
<el-table-column prop="phone" label="手机" min-width="110">
<template #default="{ row }">{{ row.phone ?? '—' }}</template>
</el-table-column>
<el-table-column label="创建时间" min-width="158">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openDetail(row.userId)">详情</el-button>
<el-button size="small" link type="primary" @click="openEdit(row.userId)">编辑</el-button>
<el-button size="small" link type="primary" @click="openCredit(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="createVisible" title="新建一级代理" width="520px" destroy-on-close>
<el-form label-width="100px">
<el-form-item label="用户名" required>
<el-input v-model="createForm.username" placeholder="登录用户名,唯一" />
</el-form-item>
<el-form-item label="登录密码" required>
<el-input v-model="createForm.password" type="text" autocomplete="off" />
</el-form-item>
<el-form-item label="确认密码" required>
<el-input v-model="createForm.confirmPassword" type="text" autocomplete="off" />
</el-form-item>
<el-form-item label="授信额度" required>
<el-input-number
v-model="createForm.creditLimit"
:min="0"
:step="10000"
style="width: 100%"
/>
<div class="field-hint">代理可向直属玩家上分的总额度上限</div>
</el-form-item>
<el-form-item label="返水比例">
<el-input-number
v-model="createForm.cashbackRate"
:min="0"
:max="1"
:step="0.001"
:precision="4"
style="width: 100%"
/>
<div class="field-hint">例如 0.01 表示 1%</div>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="createForm.phone" placeholder="选填" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="createForm.email" placeholder="选填" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="createLoading" @click="submitCreate">创建</el-button>
</template>
</el-dialog>
<el-dialog v-model="editVisible" title="编辑代理" width="480px" destroy-on-close>
<el-form label-width="88px">
<el-form-item label="账号状态">
<el-radio-group v-model="editForm.status">
<el-radio value="ACTIVE">正常</el-radio>
<el-radio value="SUSPENDED">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="返水比例">
<el-input-number
v-model="editForm.cashbackRate"
:min="0"
:max="1"
:step="0.001"
:precision="4"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="editForm.phone" />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="editForm.email" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" :loading="editLoading" @click="submitEdit">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="creditVisible" title="调整授信额度" width="420px" destroy-on-close>
<el-form label-width="88px">
<el-form-item label="代理 ID">
<el-input :model-value="editingId" disabled />
</el-form-item>
<el-form-item label="调整金额">
<el-input-number v-model="creditForm.amount" :step="1000" style="width: 100%" />
<div class="field-hint">正数为增加授信负数为减少</div>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="creditForm.remark" placeholder="选填,写入额度流水" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="creditVisible = false">取消</el-button>
<el-button type="primary" :loading="creditLoading" @click="submitCredit">确认调整</el-button>
</template>
</el-dialog>
<el-dialog v-model="detailVisible" title="代理详情" width="640px" destroy-on-close>
<template v-if="detail">
<el-descriptions :column="2" border size="small" class="detail-block">
<el-descriptions-item label="ID">{{ detail.userId }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ detail.username }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusTagType(detail.status)" size="small">
{{ statusLabel(detail.status) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="层级">L{{ detail.level }}</el-descriptions-item>
<el-descriptions-item label="授信额度">
{{ formatAmount(detail.creditLimit) }}
<span v-if="shouldCompact(detail.creditLimit)" class="amount-full-hint">
{{ formatAmountFull(detail.creditLimit) }}
</span>
</el-descriptions-item>
<el-descriptions-item label="已用额度">
{{ formatAmount(detail.usedCredit) }}
<span v-if="shouldCompact(detail.usedCredit)" class="amount-full-hint">
{{ formatAmountFull(detail.usedCredit) }}
</span>
</el-descriptions-item>
<el-descriptions-item label="可用授信">
{{ formatAmount(detail.availableCredit) }}
<span v-if="shouldCompact(detail.availableCredit)" class="amount-full-hint">
{{ formatAmountFull(detail.availableCredit) }}
</span>
</el-descriptions-item>
<el-descriptions-item label="直属玩家">{{ detail.directPlayerCount }} </el-descriptions-item>
<el-descriptions-item label="玩家负债">
{{ formatAmount(detail.directPlayerLiability) }}
</el-descriptions-item>
<el-descriptions-item label="下级代理敞口">
{{ formatAmount(detail.childAgentExposure) }}
</el-descriptions-item>
<el-descriptions-item label="返水率">{{ detail.cashbackRate }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ detail.phone ?? '—' }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ detail.email ?? '—' }}</el-descriptions-item>
<el-descriptions-item label="最后登录" :span="2">
{{ detail.lastLoginAt ? formatTime(detail.lastLoginAt) : '从未登录' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ formatTime(detail.createdAt) }}
</el-descriptions-item>
</el-descriptions>
<div class="section-title">最近额度变动</div>
<el-table
:data="detail.recentCreditTransactions"
size="small"
stripe
empty-text="暂无记录"
>
<el-table-column label="类型" width="80">
<template #default="{ row }">{{ creditTypeLabel(row.transactionType) }}</template>
</el-table-column>
<el-table-column label="变动" width="96" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.amount)" placement="top">
<span>{{ formatAmount(row.amount) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="变动后" width="96" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.creditAfter)" placement="top">
<span>{{ formatAmount(row.creditAfter) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column label="时间" min-width="150">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
</el-table>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20px;
}
.page-title { font-size: 20px; font-weight: 700; color: #e0e0e0; margin: 0 0 4px; }
.page-desc { font-size: 13px; color: #666; }
.filter-card { border-radius: 12px; }
.data-card { border-radius: 12px; }
.field-hint { font-size: 12px; color: #888; margin-top: 4px; }
.detail-block { margin-bottom: 16px; }
.section-title {
font-size: 13px;
font-weight: 600;
color: #666;
margin-bottom: 8px;
}
.amount-compact {
white-space: nowrap;
font-variant-numeric: tabular-nums;
cursor: default;
}
.amount-full-hint {
font-size: 11px;
color: #666;
margin-left: 4px;
}
</style>