feat(admin): 管理端列表分页、控制台图表与赛事导入
- 玩家/代理/赛事/注单/审计列表分页,默认每页 10 条,无页面滚动条布局 - ECharts 控制台概览、注单管理中文化与列宽优化 - zhibo 赛事字段迁移与导入,玩家编辑可改所属代理 - 管理端 API 分页与 dashboard 统计接口 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user