feat: multi-tier agent hierarchy, wallet ledger, and player UX polish

Add configurable agent max level and default sub-agent credit ratio, per-agent block direct player login on suspend, admin/agent wallet transaction views, and match detail my-bets section with refreshed player card styling.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-10 16:15:34 +08:00
parent 641c92a5f5
commit ef6b15f119
39 changed files with 2398 additions and 410 deletions

View File

@@ -32,6 +32,7 @@ import {
import type { TableInstance } from 'element-plus';
import WalletTransferContext from '../../components/WalletTransferContext.vue';
import AgentCreditContext from '../../components/AgentCreditContext.vue';
import PlayerWalletLedgerDialog from '../../components/PlayerWalletLedgerDialog.vue';
import {
depositAmountCap,
parsePlayerAvailable,
@@ -45,11 +46,16 @@ import {
const { t, localeTag } = useAdminLocale();
const auth = useAuthStore();
/* L1 agents can manage sub-agents; L2 cannot */
const isTier1 = computed(() => auth.isTier1Agent.value);
const profile = ref<{
creditLimit?: string;
usedCredit?: string;
availableCredit?: string;
canManageSubAgents?: boolean;
}>({});
/* ─── Credit profile ─── */
const profile = ref<{ creditLimit?: string; usedCredit?: string; availableCredit?: string }>({});
const canManageSubAgents = computed(
() => profile.value.canManageSubAgents === true || auth.canManageSubAgents.value,
);
/* ─── Top-level tab: players | subAgents ─── */
const activeTab = ref('players');
@@ -161,7 +167,7 @@ const transferAmountCapError = computed(() => {
onMounted(async () => {
await loadProfile();
await loadPlayers();
if (isTier1.value) {
if (canManageSubAgents.value) {
await loadSubAgents();
}
});
@@ -183,6 +189,16 @@ async function loadPlayers() {
}
}
const walletLedgerVisible = ref(false);
const walletLedgerPlayerId = ref('');
const walletLedgerPlayerUsername = ref<string | null>(null);
function openPlayerWalletLedger(playerId: string, playerUsername?: string | null) {
walletLedgerPlayerId.value = playerId;
walletLedgerPlayerUsername.value = playerUsername ?? null;
walletLedgerVisible.value = true;
}
async function loadSubAgents() {
loadingAgents.value = true;
try {
@@ -622,10 +638,11 @@ function statusTagType(s: string) {
<el-table-column :label="t('user.col.created')" min-width="148">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column :label="t('common.actions')" width="300" align="center" fixed="right">
<el-table-column :label="t('common.actions')" width="380" align="center" fixed="right">
<template #default="{ row }">
<div class="action-btns">
<el-button size="small" type="primary" link @click="openEdit(row)">{{ t('common.edit') }}</el-button>
<el-button size="small" type="primary" link @click="openPlayerWalletLedger(row.id, row.username)">{{ t('user.action.view_wallet_ledger') }}</el-button>
<el-button size="small" type="success" link @click="openTransfer('deposit', row)">{{ t('common.topup') }}</el-button>
<el-button size="small" type="warning" link @click="openTransfer('withdraw', row)">{{ t('agent_portal.withdraw_btn_label') }}</el-button>
<el-button v-if="row.status === 'ACTIVE'" size="small" link type="warning" @click="toggleFreeze(row)">{{ t('common.freeze') }}</el-button>
@@ -637,7 +654,7 @@ function statusTagType(s: string) {
</el-tab-pane>
<!-- Tab: 下级代理 (仅一级代理可见) -->
<el-tab-pane v-if="isTier1" :label="`${t('nav.subAgents')} (${subAgents.length})`" name="subAgents">
<el-tab-pane v-if="canManageSubAgents" :label="`${t('nav.subAgents')} (${subAgents.length})`" name="subAgents">
<div class="inner-toolbar">
<el-form inline size="small" style="flex: 1">
<el-form-item :label="t('common.search')">
@@ -955,6 +972,12 @@ function statusTagType(s: string) {
<el-button type="primary" :loading="creditLoading" @click="submitCreditSub">{{ t('common.confirm') }}</el-button>
</template>
</el-dialog>
<PlayerWalletLedgerDialog
v-model="walletLedgerVisible"
:player-id="walletLedgerPlayerId"
:player-username="walletLedgerPlayerUsername"
/>
</div>
</template>