feat(admin+api): 列表页子路由重构、结算历史与充值截图清理
赛事/代理管理从表格展开改为子路由详情页;结算页增加预览弹窗与历史记录;媒体库支持截图存储统计与自动/手动清理;Player 端充值截图压缩为 WebP 300KB。
This commit is contained in:
@@ -1,18 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onActivated, computed, watch, reactive, h } from 'vue';
|
||||
import { ref, onMounted, onActivated, computed, watch, reactive, h, provide } from 'vue';
|
||||
|
||||
defineOptions({ name: 'AdminAgentManager' });
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import { resolveFormError, resolveApiError } from '../i18n/form-validation';
|
||||
import api from '../api';
|
||||
import { clearStaffSession } from '../stores/auth';
|
||||
|
||||
import { usePermissions } from '../composables/usePermissions';
|
||||
import { AdminPerm } from '../constants/permissions';
|
||||
|
||||
const { t, localeTag } = useAdminLocale();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { hasPermission, role: staffRole } = usePermissions();
|
||||
|
||||
@@ -66,9 +65,9 @@ import {
|
||||
} from '../utils/format-amount';
|
||||
import { formatAgentLevelNumeral } from '../utils/agent-level-label';
|
||||
import {
|
||||
shouldToggleExpandOnRowClick,
|
||||
expandableTableRowClassName,
|
||||
} from '../utils/expandable-table';
|
||||
agentDirectPlayersReloadKey,
|
||||
agentPlayerActionsKey,
|
||||
} from '../composables/agent-direct-players-context';
|
||||
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
|
||||
import PlayerWalletLedgerDialog from '../components/PlayerWalletLedgerDialog.vue';
|
||||
import WalletTransferContext from '../components/WalletTransferContext.vue';
|
||||
@@ -113,6 +112,7 @@ type SubAgentLevelState = {
|
||||
|
||||
const subAgentLevelState = reactive<Record<number, SubAgentLevelState>>({});
|
||||
const agentLevelCounts = ref<Record<number, number>>({});
|
||||
const hierarchySettings = ref({ maxAgentLevel: 0 });
|
||||
|
||||
function ensureSubAgentState(level: number): SubAgentLevelState {
|
||||
if (!subAgentLevelState[level]) {
|
||||
@@ -176,12 +176,12 @@ const playerFilterAgent = ref('');
|
||||
const playerLoading = ref(false);
|
||||
const agentOptions = ref<{ id: string; username: string; level: number; parentUsername?: string | null }[]>([]);
|
||||
|
||||
/* ─── Expansion state ─── */
|
||||
const expandedSet = ref(new Set<string>());
|
||||
const agentPlayersMap = ref<Record<string, PlayerRow[]>>({});
|
||||
const expandLoading = ref<Record<string, boolean>>({});
|
||||
const directPlayersReload = ref<(() => void) | null>(null);
|
||||
provide(agentDirectPlayersReloadKey, directPlayersReload);
|
||||
|
||||
const expandedRowKeys = computed(() => Array.from(expandedSet.value));
|
||||
const isAgentChildRoute = computed(() =>
|
||||
/^\/users\/agents\/[^/]+\/players/.test(route.path) || route.path === '/users/settings',
|
||||
);
|
||||
|
||||
const createToolbarChildLevel = ref<number | null>(null);
|
||||
|
||||
@@ -217,19 +217,9 @@ const creditForm = ref({ amount: 10000, remark: '' });
|
||||
const creditContext = ref<AgentCreditAdjustContext | null>(null);
|
||||
const creditContextLoading = ref(false);
|
||||
|
||||
/* ─── Global settings ─── */
|
||||
const playerSettings = ref({ allowPasswordChange: true, allowUsernameChange: false });
|
||||
const bettingLimits = ref({
|
||||
minStake: 1,
|
||||
maxStakeSingle: 50000,
|
||||
maxStakeParlay: 20000,
|
||||
maxPayoutSingle: 500000,
|
||||
maxPayoutParlay: 1000000,
|
||||
dailyStakeLimit: 200000,
|
||||
});
|
||||
const settingsSaving = ref(false);
|
||||
const limitsSaving = ref(false);
|
||||
const hierarchySettings = ref({ maxAgentLevel: 0 });
|
||||
/* ─── Init ─── */
|
||||
let pageInitPromise: Promise<void> | null = null;
|
||||
const pageInitLoaded = ref(false);
|
||||
const DEFAULT_SUB_AGENT_CREDIT_RATIO = 50;
|
||||
const freezeAgentVisible = ref(false);
|
||||
const freezeAgentLoading = ref(false);
|
||||
@@ -239,18 +229,7 @@ const freezeAgentForm = ref({
|
||||
blockDirectPlayerLogin: false,
|
||||
unfreezeDirectPlayers: false,
|
||||
});
|
||||
const hierarchySaving = ref(false);
|
||||
const platformDirectRate = ref(0);
|
||||
const adminInviteRate = ref(0);
|
||||
const platformDirectSaving = ref(false);
|
||||
const resetAllowed = ref(false);
|
||||
const resetLoading = ref(false);
|
||||
const resetConfirmPhrase = ref('');
|
||||
const settingsCollapseOpen = ref<string[]>([]);
|
||||
const settingsLoaded = ref(false);
|
||||
const resetDbStatusLoaded = ref(false);
|
||||
const agentOptionsLoading = ref(false);
|
||||
const MAX_EXPANDED_AGENT_ROWS = 2;
|
||||
|
||||
const createDialogTitle = computed(() => {
|
||||
if (createAccountMode.value === 1) return t('agent.dialog.create');
|
||||
@@ -413,11 +392,8 @@ function resolveCreateParentLabel(agentId: string) {
|
||||
return agentId;
|
||||
}
|
||||
|
||||
/* ─── Init ─── */
|
||||
let pageInitPromise: Promise<void> | null = null;
|
||||
|
||||
function ensurePageInit(): Promise<void> {
|
||||
if (settingsLoaded.value) return Promise.resolve();
|
||||
if (pageInitLoaded.value) return Promise.resolve();
|
||||
if (!pageInitPromise) {
|
||||
pageInitPromise = loadUsersPageInit().finally(() => {
|
||||
pageInitPromise = null;
|
||||
@@ -444,10 +420,12 @@ function loadActiveViewTabData() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void ensurePageInit();
|
||||
loadActiveViewTabData();
|
||||
});
|
||||
// KeepAlive 激活时静默刷新当前 tab 列表(不重复 page-init)
|
||||
onActivated(() => {
|
||||
void ensurePageInit();
|
||||
const tab = activeViewTab.value;
|
||||
if (tab === 'players' && canViewUsers.value && allPlayers.value.length > 0) void loadAllPlayers();
|
||||
else if (tab === 'tier1Agents' && canViewAgents.value && tier1Agents.value.length > 0) void loadTier1Agents();
|
||||
@@ -465,44 +443,29 @@ async function loadUsersPageInit() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/users/page-init');
|
||||
const payload = data.data as {
|
||||
playerSettings?: typeof playerSettings.value;
|
||||
bettingLimits?: typeof bettingLimits.value;
|
||||
hierarchySettings?: { maxAgentLevel: number };
|
||||
platformDirect?: { platformDirectRate?: number | string; adminInviteRate?: number | string };
|
||||
agentLevelCounts?: Record<number, number>;
|
||||
};
|
||||
if (payload.playerSettings) playerSettings.value = payload.playerSettings;
|
||||
if (payload.bettingLimits) bettingLimits.value = payload.bettingLimits;
|
||||
if (payload.hierarchySettings) {
|
||||
hierarchySettings.value = {
|
||||
maxAgentLevel: payload.hierarchySettings.maxAgentLevel ?? 0,
|
||||
};
|
||||
}
|
||||
if (payload.platformDirect) {
|
||||
platformDirectRate.value = decimalRateToPercent(payload.platformDirect.platformDirectRate ?? 0);
|
||||
adminInviteRate.value = decimalRateToPercent(
|
||||
payload.platformDirect.adminInviteRate ?? payload.platformDirect.platformDirectRate ?? 0,
|
||||
);
|
||||
}
|
||||
if (payload.agentLevelCounts) {
|
||||
agentLevelCounts.value = payload.agentLevelCounts;
|
||||
if (payload.agentLevelCounts[1] !== undefined) {
|
||||
tier1Total.value = payload.agentLevelCounts[1];
|
||||
}
|
||||
}
|
||||
settingsLoaded.value = true;
|
||||
pageInitLoaded.value = true;
|
||||
} catch {
|
||||
/* keep defaults */
|
||||
}
|
||||
}
|
||||
|
||||
watch(settingsCollapseOpen, (open) => {
|
||||
if (!open.includes('settings')) return;
|
||||
if (!resetDbStatusLoaded.value) {
|
||||
resetDbStatusLoaded.value = true;
|
||||
void loadResetDatabaseStatus();
|
||||
}
|
||||
if (!settingsLoaded.value) {
|
||||
void loadUsersPageInit();
|
||||
}
|
||||
});
|
||||
function openGlobalSettings() {
|
||||
void router.push('/users/settings');
|
||||
}
|
||||
|
||||
/* ─── Load tier-1 agents ─── */
|
||||
async function loadTier1Agents() {
|
||||
@@ -550,6 +513,9 @@ async function loadAgentLevelCounts() {
|
||||
normalized[Number(lvl)] = Number(cnt) || 0;
|
||||
}
|
||||
agentLevelCounts.value = normalized;
|
||||
if (normalized[1] !== undefined) {
|
||||
tier1Total.value = normalized[1];
|
||||
}
|
||||
} catch {
|
||||
agentLevelCounts.value = {};
|
||||
}
|
||||
@@ -683,16 +649,28 @@ function affiliationLabel(row: Pick<PlayerRow, 'affiliationAgents'>) {
|
||||
return formatPlayerAffiliationLabel(row, t('user.type.player'), t('agent.platform_row_name'));
|
||||
}
|
||||
|
||||
function directPlayersTabLabel(ownerName: string, count: number) {
|
||||
return `${t('agent.direct_players_title', { name: ownerName })} (${count})`;
|
||||
function eventClickElement(event: Event): Element | null {
|
||||
const target = event.target;
|
||||
if (target instanceof Element) return target;
|
||||
if (target instanceof Node) return target.parentElement;
|
||||
return null;
|
||||
}
|
||||
|
||||
function onTier1AgentRowClick(row: AgentRow, _column: unknown, event: MouseEvent) {
|
||||
onAgentRowClick(row, event);
|
||||
function openAgentDirectPlayers(row: AgentRow, _column: unknown, event: Event) {
|
||||
const el = eventClickElement(event);
|
||||
if (!el) return;
|
||||
if (el.closest('button') || el.closest('.el-button') || el.closest('.admin-agent-row-actions')) return;
|
||||
void router.push({
|
||||
path: `/users/agents/${row.userId}/players`,
|
||||
query: {
|
||||
username: row.username,
|
||||
fromTab: activeViewTab.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onSubAgentRowClick(row: AgentRow, _column: unknown, event: MouseEvent) {
|
||||
onAgentRowClick(row, event);
|
||||
function agentRowClassName() {
|
||||
return 'row-navigable';
|
||||
}
|
||||
|
||||
watch(activeViewTab, (tab) => {
|
||||
@@ -716,199 +694,6 @@ watch(visibleSubAgentTabLevels, (levels) => {
|
||||
}
|
||||
});
|
||||
|
||||
/* ─── Expansion ─── */
|
||||
async function onExpandChange(row: DisplayAgentRow, expandedRows: DisplayAgentRow[]) {
|
||||
expandedSet.value = new Set(expandedRows.map((r) => r.userId));
|
||||
if (expandedSet.value.has(row.userId) && !agentPlayersMap.value[row.userId]) {
|
||||
await loadExpansionData(row.userId);
|
||||
}
|
||||
}
|
||||
|
||||
function onAgentRowClick(row: AgentRow, event: MouseEvent) {
|
||||
if (!shouldToggleExpandOnRowClick(event)) return;
|
||||
const userId = row.userId;
|
||||
const next = new Set(expandedSet.value);
|
||||
if (next.has(userId)) {
|
||||
next.delete(userId);
|
||||
} else {
|
||||
if (next.size >= MAX_EXPANDED_AGENT_ROWS) {
|
||||
const [first] = next;
|
||||
if (first) next.delete(first);
|
||||
}
|
||||
next.add(userId);
|
||||
if (!agentPlayersMap.value[userId]) void loadExpansionData(userId);
|
||||
}
|
||||
expandedSet.value = next;
|
||||
}
|
||||
|
||||
async function loadExpansionData(agentId: string) {
|
||||
expandLoading.value[agentId] = true;
|
||||
try {
|
||||
const { data } = await api.get('/admin/users', { params: { parentId: agentId, pageSize: 100 } });
|
||||
agentPlayersMap.value[agentId] = data.data.items as PlayerRow[];
|
||||
} catch {
|
||||
agentPlayersMap.value[agentId] = [];
|
||||
} finally {
|
||||
expandLoading.value[agentId] = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getPlayers(agentId: string) {
|
||||
return agentPlayersMap.value[agentId] || [];
|
||||
}
|
||||
|
||||
function refreshExpandedAgentPlayers() {
|
||||
for (const agentId of expandedSet.value) {
|
||||
loadExpansionData(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Global settings ─── */
|
||||
async function loadResetDatabaseStatus() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/system/reset-database');
|
||||
resetAllowed.value = !!data.data?.allowed;
|
||||
} catch {
|
||||
resetAllowed.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetDatabase() {
|
||||
if (resetConfirmPhrase.value !== 'RESET') {
|
||||
ElMessage.warning(t('user.reset_database_confirm_label'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(t('user.reset_database_hint'), t('user.reset_database'), {
|
||||
type: 'warning',
|
||||
confirmButtonText: t('user.reset_database_btn'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
resetLoading.value = true;
|
||||
try {
|
||||
const { data } = await api.post('/admin/system/reset-database', { confirmPhrase: 'RESET' });
|
||||
const accounts: string[] = data.data?.demoAccounts ?? [];
|
||||
ElMessage.success({
|
||||
message: `${t('user.reset_database_success')}\n${t('user.reset_database_accounts')}: ${accounts.join(' · ')}`,
|
||||
duration: 8000,
|
||||
});
|
||||
clearStaffSession();
|
||||
await router.push('/login');
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
resetLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBettingLimits() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/settings/betting-limits');
|
||||
bettingLimits.value = data.data;
|
||||
} catch {
|
||||
/* defaults */
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBettingLimits() {
|
||||
limitsSaving.value = true;
|
||||
try {
|
||||
const { data } = await api.put('/admin/settings/betting-limits', bettingLimits.value);
|
||||
bettingLimits.value = data.data;
|
||||
ElMessage.success(t('msg.saved'));
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
loadBettingLimits();
|
||||
} finally {
|
||||
limitsSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPlayerSettings() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/users/settings/account');
|
||||
playerSettings.value = data.data;
|
||||
} catch {
|
||||
/* defaults */
|
||||
}
|
||||
}
|
||||
|
||||
async function savePlayerSettings() {
|
||||
settingsSaving.value = true;
|
||||
try {
|
||||
const { data } = await api.put('/admin/users/settings/account', playerSettings.value);
|
||||
playerSettings.value = data.data;
|
||||
ElMessage.success(t('msg.saved'));
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
loadPlayerSettings();
|
||||
} finally {
|
||||
settingsSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHierarchySettings() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/agents/settings/hierarchy');
|
||||
hierarchySettings.value = { maxAgentLevel: data.data?.maxAgentLevel ?? 0 };
|
||||
} catch {
|
||||
hierarchySettings.value = { maxAgentLevel: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHierarchySettings() {
|
||||
hierarchySaving.value = true;
|
||||
try {
|
||||
const { data } = await api.put('/admin/agents/settings/hierarchy', hierarchySettings.value);
|
||||
hierarchySettings.value = { maxAgentLevel: data.data?.maxAgentLevel ?? hierarchySettings.value.maxAgentLevel };
|
||||
ElMessage.success(t('msg.saved'));
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
loadHierarchySettings();
|
||||
} finally {
|
||||
hierarchySaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPlatformDirectSettings() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/settings/cashback/platform-direct');
|
||||
const payload = data.data as { platformDirectRate?: number | string; adminInviteRate?: number | string };
|
||||
platformDirectRate.value = decimalRateToPercent(payload?.platformDirectRate ?? 0);
|
||||
adminInviteRate.value = decimalRateToPercent(payload?.adminInviteRate ?? payload?.platformDirectRate ?? 0);
|
||||
} catch {
|
||||
platformDirectRate.value = 0;
|
||||
adminInviteRate.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePlatformDirectSettings() {
|
||||
platformDirectSaving.value = true;
|
||||
try {
|
||||
const { data } = await api.put('/admin/settings/cashback/platform-direct', {
|
||||
platformDirectRate: percentToDecimalRate(platformDirectRate.value),
|
||||
adminInviteRate: percentToDecimalRate(adminInviteRate.value),
|
||||
});
|
||||
const payload = data.data as { platformDirectRate?: number | string; adminInviteRate?: number | string };
|
||||
platformDirectRate.value = decimalRateToPercent(payload?.platformDirectRate ?? 0);
|
||||
adminInviteRate.value = decimalRateToPercent(payload?.adminInviteRate ?? payload?.platformDirectRate ?? 0);
|
||||
ElMessage.success(t('msg.saved'));
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
loadPlatformDirectSettings();
|
||||
} finally {
|
||||
platformDirectSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const walletLedgerVisible = ref(false);
|
||||
const walletLedgerPlayerId = ref('');
|
||||
const walletLedgerPlayerUsername = ref<string | null>(null);
|
||||
@@ -1030,7 +815,7 @@ async function submitCreate() {
|
||||
}
|
||||
const parentId = createParentAgentId.value || createForm.value.parentId;
|
||||
if (parentId) {
|
||||
await loadExpansionData(parentId);
|
||||
directPlayersReload.value?.();
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
@@ -1374,7 +1159,7 @@ async function submitFreezeAgent() {
|
||||
function refreshExpandedParents() {
|
||||
loadAllPlayers();
|
||||
reloadAgentLists();
|
||||
refreshExpandedAgentPlayers();
|
||||
directPlayersReload.value?.();
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -1445,107 +1230,40 @@ function creditTypeLabel(type: string) {
|
||||
if (type === 'CREDIT_DECREASE') return t('agent.credit.decrease');
|
||||
return type;
|
||||
}
|
||||
|
||||
provide(agentPlayerActionsKey, {
|
||||
get canCreatePlayer() {
|
||||
return canCreateUsers.value;
|
||||
},
|
||||
get playerActionFlags() {
|
||||
return playerActionFlags.value;
|
||||
},
|
||||
openCreatePlayer,
|
||||
openDetailPlayer,
|
||||
openEditPlayer,
|
||||
openTransfer,
|
||||
toggleFreezePlayer,
|
||||
deletePlayer,
|
||||
openPlayerWalletLedger,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="admin-list-page agent-mgr-page">
|
||||
<!-- ─── Global settings collapse ─── -->
|
||||
<el-collapse v-if="canManageSettings" v-model="settingsCollapseOpen" class="list-settings">
|
||||
<el-collapse-item :title="t('user.page_settings')" name="settings">
|
||||
<div class="list-settings-block">
|
||||
<p class="list-settings-title">{{ t('user.global_settings') }}</p>
|
||||
<el-form inline size="small" class="settings-form">
|
||||
<el-form-item :label="t('user.field.allow_password_change')">
|
||||
<el-switch v-model="playerSettings.allowPasswordChange" :loading="settingsSaving" @change="savePlayerSettings" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.allow_username_change')">
|
||||
<el-switch v-model="playerSettings.allowUsernameChange" :loading="settingsSaving" @change="savePlayerSettings" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="list-settings-block">
|
||||
<p class="list-settings-title">{{ t('agent.hierarchy.settings_title') }}</p>
|
||||
<p class="list-settings-hint">{{ t('agent.hierarchy.settings_hint') }}</p>
|
||||
<el-form inline size="small" class="settings-form">
|
||||
<el-form-item :label="t('agent.hierarchy.max_level')">
|
||||
<el-input-number
|
||||
v-model="hierarchySettings.maxAgentLevel"
|
||||
:min="0"
|
||||
:step="1"
|
||||
controls-position="right"
|
||||
:disabled="hierarchySaving"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="hierarchySaving" @click="saveHierarchySettings">{{ t('common.save') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="list-settings-block">
|
||||
<p class="list-settings-title">{{ t('cashback.settings_title') }}</p>
|
||||
<el-form inline size="small" class="settings-form">
|
||||
<el-form-item :label="t('cashback.platform_direct_default_rate')">
|
||||
<RatePercentInput v-model="platformDirectRate" />
|
||||
</el-form-item>
|
||||
<p class="list-settings-hint block-hint">{{ t('cashback.platform_direct_default_hint') }}</p>
|
||||
<el-form-item :label="t('cashback.admin_invite_default_rate')">
|
||||
<RatePercentInput v-model="adminInviteRate" />
|
||||
</el-form-item>
|
||||
<p class="list-settings-hint block-hint">{{ t('cashback.admin_invite_default_hint') }}</p>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="platformDirectSaving" @click="savePlatformDirectSettings">{{ t('common.save') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="list-settings-block">
|
||||
<p class="list-settings-title">{{ t('user.betting_limits') }}</p>
|
||||
<el-form inline size="small" class="settings-form limits-form">
|
||||
<el-form-item :label="t('user.limit.min_stake')">
|
||||
<el-input-number v-model="bettingLimits.minStake" :min="0" :step="1" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.limit.max_stake_single')">
|
||||
<el-input-number v-model="bettingLimits.maxStakeSingle" :min="0" :step="100" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.limit.max_stake_parlay')">
|
||||
<el-input-number v-model="bettingLimits.maxStakeParlay" :min="0" :step="100" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.limit.max_payout_single')">
|
||||
<el-input-number v-model="bettingLimits.maxPayoutSingle" :min="0" :step="1000" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.limit.max_payout_parlay')">
|
||||
<el-input-number v-model="bettingLimits.maxPayoutParlay" :min="0" :step="1000" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.limit.daily_stake')">
|
||||
<el-input-number v-model="bettingLimits.dailyStakeLimit" :min="0" :step="1000" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="limitsSaving" @click="saveBettingLimits">{{ t('common.save') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="list-settings-block list-settings-block--danger">
|
||||
<p class="list-settings-title">{{ t('user.reset_database') }}</p>
|
||||
<p class="list-settings-hint">{{ t('user.reset_database_hint') }}</p>
|
||||
<el-alert v-if="!resetAllowed" type="warning" :closable="false" show-icon class="reset-db-alert" :title="t('user.reset_database_disabled_prod')" />
|
||||
<el-form inline size="small" class="settings-form reset-db-form">
|
||||
<el-form-item :label="t('user.reset_database_confirm_label')">
|
||||
<el-input v-model="resetConfirmPhrase" :placeholder="t('user.reset_database_confirm_ph')" style="width: 160px" :disabled="!resetAllowed" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="danger" plain :loading="resetLoading" :disabled="!resetAllowed || resetConfirmPhrase !== 'RESET'" @click="resetDatabase">{{ t('user.reset_database_btn') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
<div class="agent-mgr-shell">
|
||||
<router-view v-if="isAgentChildRoute" />
|
||||
<div v-else class="admin-list-page agent-mgr-page">
|
||||
<InviteManageDialog v-model="inviteDialogOpen" />
|
||||
|
||||
<div class="mgr-tabs-shell">
|
||||
<el-button v-if="canManageSettings" type="primary" class="invite-prominent-btn" @click="inviteDialogOpen = true">
|
||||
{{ t('invite.menu_btn') }}
|
||||
</el-button>
|
||||
<el-tabs v-model="activeViewTab" class="mgr-top-tabs" :class="{ 'mgr-top-tabs--with-invite': canManageSettings }">
|
||||
<div v-if="canManageSettings" class="mgr-toolbar-actions">
|
||||
<el-button class="settings-toolbar-btn" @click="openGlobalSettings">
|
||||
{{ t('user.page_settings') }}
|
||||
</el-button>
|
||||
<el-button type="primary" class="invite-prominent-btn" @click="inviteDialogOpen = true">
|
||||
{{ t('invite.menu_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-tabs v-model="activeViewTab" class="mgr-top-tabs" :class="{ 'mgr-top-tabs--with-actions': canManageSettings }">
|
||||
<!-- ─── Tab: 全部玩家(默认) ─── -->
|
||||
<el-tab-pane v-if="canViewUsers" :label="`${t('user.type.player')} (${playerTotal})`" name="players">
|
||||
<section class="list-panel player-list-panel">
|
||||
@@ -1685,81 +1403,20 @@ function creditTypeLabel(type: string) {
|
||||
<el-button type="primary" @click="openCreateTier1Agent">{{ t('agent.create_btn') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="list-hint">{{ t('agent.open_agent_hint') }}</p>
|
||||
<AdminTableWrap>
|
||||
<el-table
|
||||
:data="tier1Agents"
|
||||
stripe
|
||||
row-key="userId"
|
||||
:expand-row-keys="expandedRowKeys"
|
||||
:row-class-name="expandableTableRowClassName"
|
||||
class="expandable-table compact-agent-table"
|
||||
@expand-change="onExpandChange"
|
||||
@row-click="onTier1AgentRowClick"
|
||||
:row-class-name="agentRowClassName"
|
||||
class="compact-agent-table"
|
||||
@row-click="openAgentDirectPlayers"
|
||||
>
|
||||
<template #empty>
|
||||
<AdminTableEmpty />
|
||||
</template>
|
||||
|
||||
<!-- Built-in expand column -->
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="expand-panel">
|
||||
<div v-if="expandLoading[row.userId]" class="expand-loading">
|
||||
{{ t('common.loading') || '加载中...' }}
|
||||
</div>
|
||||
<div v-else class="expand-panel-body">
|
||||
<div class="expand-section-header">
|
||||
<div class="expand-section-title">{{ directPlayersTabLabel(row.username, getPlayers(row.userId).length) }}</div>
|
||||
<el-button type="primary" size="small" @click="openCreatePlayer(row.userId)">{{ t('user.create_btn') }}</el-button>
|
||||
</div>
|
||||
<el-table :data="getPlayers(row.userId)" stripe class="inner-table">
|
||||
<template #empty><AdminTableEmpty /></template>
|
||||
<el-table-column type="index" :label="t('common.seq')" width="55" align="center" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" />
|
||||
<el-table-column :label="t('common.status')" min-width="120">
|
||||
<template #default="{ row: player }">
|
||||
<AdminPlayerStatusCell :status="player.status" :is-online="player.isOnline" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.col.invite_code')" min-width="96" show-overflow-tooltip>
|
||||
<template #default="{ row: player }">
|
||||
<code v-if="player.inviteCode" class="invite-code-cell">{{ player.inviteCode }}</code>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.col.balance')" min-width="120" align="right">
|
||||
<template #default="{ row: player }">
|
||||
<el-tooltip :content="`${formatAmountFull(player.availableBalance)} / ${formatAmountFull(player.frozenBalance)}`" placement="top">
|
||||
<span class="amount-compact">{{ formatAmount(player.availableBalance) }} / {{ formatAmount(player.frozenBalance) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="betCount" :label="t('user.col.bets')" width="56" align="center" />
|
||||
<el-table-column :label="t('user.col.stake_payout')" min-width="100" align="right">
|
||||
<template #default="{ row: player }">
|
||||
<span class="amount-compact">{{ formatAmount(player.totalStake) }} / {{ formatAmount(player.totalReturn) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.actions')" min-width="320" align="center">
|
||||
<template #default="{ row: player }">
|
||||
<AdminPlayerRowActions
|
||||
v-bind="playerActionFlags"
|
||||
:row="player"
|
||||
@detail="openDetailPlayer(player.id)"
|
||||
@ledger="openPlayerWalletLedger(player.id, player.username)"
|
||||
@edit="openEditPlayer(player.id)"
|
||||
@deposit="openTransfer('deposit', player)"
|
||||
@withdraw="openTransfer('withdraw', player)"
|
||||
@freeze="toggleFreezePlayer(player)"
|
||||
@delete="deletePlayer(player)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="index" :index="(i: number) => (tier1Page - 1) * tier1PageSize + i + 1" :label="t('common.seq')" width="70" align="center" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column :label="t('common.status')" min-width="72">
|
||||
@@ -1854,69 +1511,19 @@ function creditTypeLabel(type: string) {
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="list-hint">{{ t('agent.open_agent_hint') }}</p>
|
||||
<AdminTableWrap>
|
||||
<el-table
|
||||
:data="ensureSubAgentState(agentLevel).agents"
|
||||
stripe
|
||||
row-key="userId"
|
||||
:expand-row-keys="expandedRowKeys"
|
||||
:row-class-name="expandableTableRowClassName"
|
||||
class="expandable-table compact-agent-table"
|
||||
@expand-change="onExpandChange"
|
||||
@row-click="onSubAgentRowClick"
|
||||
:row-class-name="agentRowClassName"
|
||||
class="compact-agent-table"
|
||||
@row-click="openAgentDirectPlayers"
|
||||
>
|
||||
<template #empty>
|
||||
<AdminTableEmpty />
|
||||
</template>
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="expand-panel">
|
||||
<div v-if="expandLoading[row.userId]" class="expand-loading">{{ t('common.loading') }}</div>
|
||||
<div v-else class="expand-panel-body">
|
||||
<div class="expand-section-header">
|
||||
<div class="expand-section-title">{{ directPlayersTabLabel(row.username, getPlayers(row.userId).length) }}</div>
|
||||
<el-button type="primary" size="small" @click="openCreatePlayer(row.userId)">{{ t('user.create_btn') }}</el-button>
|
||||
</div>
|
||||
<el-table :data="getPlayers(row.userId)" stripe class="inner-table">
|
||||
<template #empty><AdminTableEmpty /></template>
|
||||
<el-table-column type="index" :label="t('common.seq')" width="55" align="center" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" />
|
||||
<el-table-column :label="t('common.status')" min-width="120">
|
||||
<template #default="{ row: player }">
|
||||
<AdminPlayerStatusCell :status="player.status" :is-online="player.isOnline" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.col.invite_code')" min-width="96" show-overflow-tooltip>
|
||||
<template #default="{ row: player }">
|
||||
<code v-if="player.inviteCode" class="invite-code-cell">{{ player.inviteCode }}</code>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.col.balance')" min-width="120" align="right">
|
||||
<template #default="{ row: player }">
|
||||
<span class="amount-compact">{{ formatAmount(player.availableBalance) }} / {{ formatAmount(player.frozenBalance) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.actions')" min-width="280" align="center">
|
||||
<template #default="{ row: player }">
|
||||
<AdminPlayerRowActions
|
||||
v-bind="playerActionFlags"
|
||||
:row="player"
|
||||
@detail="openDetailPlayer(player.id)"
|
||||
@ledger="openPlayerWalletLedger(player.id, player.username)"
|
||||
@edit="openEditPlayer(player.id)"
|
||||
@deposit="openTransfer('deposit', player)"
|
||||
@withdraw="openTransfer('withdraw', player)"
|
||||
@freeze="toggleFreezePlayer(player)"
|
||||
@delete="deletePlayer(player)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="index" :index="(i: number) => (ensureSubAgentState(agentLevel).page - 1) * ensureSubAgentState(agentLevel).pageSize + i + 1" :label="t('common.seq')" width="70" align="center" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column :label="t('agent.col.parent_chain')" min-width="120" show-overflow-tooltip>
|
||||
@@ -1968,6 +1575,7 @@ function creditTypeLabel(type: string) {
|
||||
</template>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════ DIALOGS ═══════════ -->
|
||||
|
||||
@@ -2534,6 +2142,23 @@ function creditTypeLabel(type: string) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agent-mgr-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.agent-mgr-shell > :deep(.agent-direct-players-page),
|
||||
.agent-mgr-shell > :deep(.global-settings-page) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.compact-agent-table :deep(.row-navigable) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mgr-tabs-shell {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
@@ -2542,15 +2167,31 @@ function creditTypeLabel(type: string) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mgr-top-tabs--with-invite :deep(.el-tabs__header) {
|
||||
padding-right: 108px;
|
||||
.mgr-top-tabs--with-actions :deep(.el-tabs__header) {
|
||||
padding-right: 248px;
|
||||
}
|
||||
|
||||
.invite-prominent-btn {
|
||||
.mgr-toolbar-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-toolbar-btn {
|
||||
min-width: 96px;
|
||||
height: 38px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.invite-prominent-btn {
|
||||
position: static;
|
||||
min-width: 96px;
|
||||
height: 38px;
|
||||
padding: 0 22px;
|
||||
@@ -2756,11 +2397,11 @@ function creditTypeLabel(type: string) {
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.mgr-top-tabs--with-invite :deep(.el-tabs__header) {
|
||||
.mgr-top-tabs--with-actions :deep(.el-tabs__header) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.invite-prominent-btn {
|
||||
.mgr-toolbar-actions {
|
||||
position: static;
|
||||
align-self: flex-end;
|
||||
margin: 0 0 8px;
|
||||
|
||||
Reference in New Issue
Block a user