feat(admin,api,player): 代理层级管理、额度上下分与玩家钱包详情

新增代理管理器与二级代理体系,完善信用额度/上下分上下文与冻结策略;代理端玩家与子代理管理增强;玩家端新增钱包详情页与交易筛选优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-08 15:34:12 +08:00
parent b2216abd0c
commit 414998ce36
54 changed files with 6641 additions and 481 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useAdminLocale } from '../composables/useAdminLocale';
@@ -25,6 +25,8 @@ import {
shouldCompactAmount as shouldCompact,
} from '../utils/format-amount';
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
import WalletTransferContext from '../components/WalletTransferContext.vue';
import { useAdminPlayerTransfer } from '../composables/useAdminPlayerTransfer';
const users = ref<PlayerRow[]>([]);
const total = ref(0);
@@ -39,17 +41,30 @@ const agentOptions = ref<{ id: string; username: string }[]>([]);
const createVisible = ref(false);
const editVisible = ref(false);
const detailVisible = ref(false);
const depositVisible = ref(false);
const createLoading = ref(false);
const editLoading = ref(false);
const depositLoading = ref(false);
const createForm = ref<PlayerCreateForm>(emptyPlayerCreateForm());
const editForm = ref<PlayerEditForm>(emptyPlayerEditForm());
const detail = ref<PlayerDetail | null>(null);
const editingId = ref('');
const depositForm = ref({ userId: '', amount: 100, remark: '' });
const {
transferVisible,
transferLoading,
transferType,
transferTarget,
transferAmount,
transferRemark,
transferContext,
transferContextLoading,
transferAmountRange,
transferAmountDisabled,
transferTitle,
openTransfer,
submitTransfer,
} = useAdminPlayerTransfer(() => load());
const playerSettings = ref({ allowPasswordChange: true, allowUsernameChange: false });
const bettingLimits = ref({
minStake: 1,
@@ -219,11 +234,6 @@ async function openEdit(id: string) {
editVisible.value = true;
}
function openDeposit(row: PlayerRow) {
depositForm.value = { userId: row.id, amount: 100, remark: t('user.deposit_remark_default') };
depositVisible.value = true;
}
async function submitCreate() {
let payload: ReturnType<typeof buildCreatePlayerPayload>;
try {
@@ -311,30 +321,6 @@ async function submitEdit() {
}
}
async function submitDeposit() {
if (depositForm.value.amount <= 0) {
ElMessage.warning(t('msg.amount_gt_zero'));
return;
}
depositLoading.value = true;
try {
await api.post('/admin/wallet/deposit', {
userId: depositForm.value.userId,
amount: depositForm.value.amount,
remark: depositForm.value.remark,
requestId: `dep-${depositForm.value.userId}-${Date.now()}`,
});
ElMessage.success(t('msg.topup_ok'));
depositVisible.value = false;
load();
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? t('msg.topup_failed'));
} finally {
depositLoading.value = false;
}
}
function formatTime(v: string) {
if (!v) return '—';
return new Date(v).toLocaleString(localeTag.value);
@@ -554,11 +540,12 @@ function statusLabel(s: string) {
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('common.actions')" width="300" fixed="right" align="center">
<el-table-column :label="t('common.actions')" width="340" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" link type="primary" @click="openDetail(row.id)">{{ t('common.detail') }}</el-button>
<el-button size="small" link type="primary" @click="openEdit(row.id)">{{ t('common.edit') }}</el-button>
<el-button size="small" link type="primary" @click="openDeposit(row)">{{ t('common.topup') }}</el-button>
<el-button size="small" link type="success" @click="openTransfer('deposit', row)">{{ t('common.topup') }}</el-button>
<el-button size="small" link type="warning" @click="openTransfer('withdraw', row)">{{ t('agent_portal.withdraw_btn_label') }}</el-button>
<el-button
v-if="row.status === 'ACTIVE'"
size="small"
@@ -760,21 +747,41 @@ function statusLabel(s: string) {
</template>
</el-dialog>
<el-dialog v-model="depositVisible" :title="t('user.dialog.deposit')" width="400px" destroy-on-close>
<el-form label-width="80px">
<el-form-item :label="t('user.field.player_id')">
<el-input :model-value="depositForm.userId" disabled />
<el-dialog v-model="transferVisible" :title="transferTitle()" width="520px" destroy-on-close>
<WalletTransferContext
:context="transferContext"
:mode="transferType"
:loading="transferContextLoading"
/>
<el-form label-width="88px">
<el-form-item :label="t('common.col_id')">
<span>{{ transferTarget?.id }}</span>
</el-form-item>
<el-form-item :label="t('user.field.amount')">
<el-input-number v-model="depositForm.amount" :min="0.01" :step="10" style="width: 100%" />
<el-input-number
v-model="transferAmount"
:min="transferAmountRange.min"
:max="transferAmountRange.max"
:disabled="transferAmountDisabled"
:step="10"
:precision="2"
style="width: 100%"
/>
</el-form-item>
<el-form-item :label="t('user.field.remark')">
<el-input v-model="depositForm.remark" />
<el-input v-model="transferRemark" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="depositVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" :loading="depositLoading" @click="submitDeposit">{{ t('user.btn.confirm_deposit') }}</el-button>
<el-button @click="transferVisible = false">{{ t('common.cancel') }}</el-button>
<el-button
type="primary"
:loading="transferLoading"
:disabled="transferAmountDisabled"
@click="submitTransfer"
>
{{ t('common.confirm') }}
</el-button>
</template>
</el-dialog>