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:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { ref, onMounted, computed, watch, reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
@@ -37,11 +37,13 @@ import {
|
||||
formatAmountFull,
|
||||
shouldCompactAmount as shouldCompact,
|
||||
} from '../utils/format-amount';
|
||||
import { formatAgentLevelNumeral } from '../utils/agent-level-label';
|
||||
import {
|
||||
shouldToggleExpandOnRowClick,
|
||||
expandableTableRowClassName,
|
||||
} from '../utils/expandable-table';
|
||||
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
|
||||
import PlayerWalletLedgerDialog from '../components/PlayerWalletLedgerDialog.vue';
|
||||
import WalletTransferContext from '../components/WalletTransferContext.vue';
|
||||
import AgentCreditContext from '../components/AgentCreditContext.vue';
|
||||
import { useAdminPlayerTransfer } from '../composables/useAdminPlayerTransfer';
|
||||
@@ -59,15 +61,69 @@ const tier1PageSize = ref(20);
|
||||
const tier1Keyword = ref('');
|
||||
const tier1FilterStatus = ref('');
|
||||
|
||||
/* ─── Tier-2 agent list ─── */
|
||||
const tier2Agents = ref<AgentRow[]>([]);
|
||||
const tier2Total = ref(0);
|
||||
const tier2Page = ref(1);
|
||||
const tier2PageSize = ref(20);
|
||||
const tier2Keyword = ref('');
|
||||
const tier2FilterStatus = ref('');
|
||||
/* ─── Sub-agent lists by level (L2, L3, …) ─── */
|
||||
type SubAgentLevelState = {
|
||||
agents: AgentRow[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
keyword: string;
|
||||
filterStatus: string;
|
||||
};
|
||||
|
||||
/* ─── View tab: players | tier1Agents | tier2Agents ─── */
|
||||
const subAgentLevelState = reactive<Record<number, SubAgentLevelState>>({});
|
||||
const agentLevelCounts = ref<Record<number, number>>({});
|
||||
const subAgentTableRefs = ref<Record<number, { toggleRowExpansion: (row: AgentRow) => void } | null>>({});
|
||||
|
||||
function ensureSubAgentState(level: number): SubAgentLevelState {
|
||||
if (!subAgentLevelState[level]) {
|
||||
subAgentLevelState[level] = {
|
||||
agents: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
keyword: '',
|
||||
filterStatus: '',
|
||||
};
|
||||
}
|
||||
return subAgentLevelState[level];
|
||||
}
|
||||
|
||||
function agentLevelTabName(level: number) {
|
||||
return `agentLevel-${level}`;
|
||||
}
|
||||
|
||||
function lvlLabel(level: number) {
|
||||
return formatAgentLevelNumeral(level, localeTag.value);
|
||||
}
|
||||
|
||||
function agentTierName(level: number) {
|
||||
return t('agent.level_name', { level: lvlLabel(level) });
|
||||
}
|
||||
|
||||
function agentLevelTabLabel(level: number) {
|
||||
const count = agentLevelCounts.value[level] ?? 0;
|
||||
return `${agentTierName(level)} (${count})`;
|
||||
}
|
||||
|
||||
const visibleSubAgentTabLevels = computed(() => {
|
||||
const counts = agentLevelCounts.value;
|
||||
const max = hierarchySettings.value.maxAgentLevel;
|
||||
const levels = new Set<number>([2]);
|
||||
if (max === 0) {
|
||||
for (const [lvl, cnt] of Object.entries(counts)) {
|
||||
const n = Number(lvl);
|
||||
if (n >= 3 && cnt > 0) levels.add(n);
|
||||
}
|
||||
} else {
|
||||
for (let n = 3; n <= max; n++) {
|
||||
if ((counts[n] ?? 0) > 0) levels.add(n);
|
||||
}
|
||||
}
|
||||
return [...levels].sort((a, b) => a - b);
|
||||
});
|
||||
|
||||
/* ─── View tab: players | tier1Agents | agentLevel-N ─── */
|
||||
const activeViewTab = ref('players');
|
||||
|
||||
/* ─── All players list ─── */
|
||||
@@ -86,7 +142,8 @@ const expandedSet = ref(new Set<string>());
|
||||
const agentPlayersMap = ref<Record<string, PlayerRow[]>>({});
|
||||
const expandLoading = ref<Record<string, boolean>>({});
|
||||
const tier1AgentTableRef = ref();
|
||||
const tier2AgentTableRef = ref();
|
||||
|
||||
const createToolbarChildLevel = ref<number | null>(null);
|
||||
|
||||
/* ─── Dialogs ─── */
|
||||
const createVisible = ref(false);
|
||||
@@ -132,11 +189,16 @@ const bettingLimits = ref({
|
||||
});
|
||||
const settingsSaving = ref(false);
|
||||
const limitsSaving = ref(false);
|
||||
const agentSuspendSettings = ref({
|
||||
suspendFreezeDirectPlayers: false,
|
||||
suspendBlockPlayerLogin: false,
|
||||
const hierarchySettings = ref({ maxAgentLevel: 0, defaultSubAgentCreditRatio: 50 });
|
||||
const freezeAgentVisible = ref(false);
|
||||
const freezeAgentLoading = ref(false);
|
||||
const freezeAgentTarget = ref<AgentRow | null>(null);
|
||||
const freezeAgentForm = ref({
|
||||
freezeDirectPlayers: false,
|
||||
blockDirectPlayerLogin: false,
|
||||
unfreezeDirectPlayers: false,
|
||||
});
|
||||
const agentSuspendSaving = ref(false);
|
||||
const hierarchySaving = ref(false);
|
||||
const resetAllowed = ref(false);
|
||||
const resetLoading = ref(false);
|
||||
const resetConfirmPhrase = ref('');
|
||||
@@ -144,27 +206,161 @@ const settingsCollapseOpen = ref<string[]>([]);
|
||||
|
||||
const createDialogTitle = computed(() => {
|
||||
if (createAccountMode.value === 1) return t('agent.dialog.create');
|
||||
if (createAccountMode.value === 2) return t('agent_portal.create_sub_agent_dialog');
|
||||
if (createAccountMode.value === 2) {
|
||||
const lvl = createTargetAgentLevel.value;
|
||||
if (lvl === 2) return t('agent_portal.create_sub_agent_dialog');
|
||||
if (lvl != null) return t('agent.dialog.create_level_agent', { level: lvlLabel(lvl) });
|
||||
return t('agent.dialog.create_child_agent');
|
||||
}
|
||||
return t('user.dialog.create');
|
||||
});
|
||||
|
||||
const createSubAgentLevelPreview = computed(() => {
|
||||
if (createAccountMode.value !== 2 || !createParentAgentId.value) return null;
|
||||
const parentLevel = resolveParentAgentLevel(createParentAgentId.value);
|
||||
return parentLevel != null ? parentLevel + 1 : null;
|
||||
});
|
||||
|
||||
const createTargetAgentLevel = computed(() => {
|
||||
if (createAccountMode.value !== 2) return null;
|
||||
if (createToolbarChildLevel.value != null) return createToolbarChildLevel.value;
|
||||
return createSubAgentLevelPreview.value;
|
||||
});
|
||||
|
||||
function resolveParentAgentLevel(agentId: string): number | null {
|
||||
const opt = agentOptions.value.find((a) => a.id === agentId);
|
||||
if (opt) return opt.level;
|
||||
const row = findAgentRowByUserId(agentId);
|
||||
if (row) return row.level;
|
||||
return null;
|
||||
}
|
||||
|
||||
function parentOptionsForChildLevel(childLevel: number) {
|
||||
if (childLevel < 2) return [];
|
||||
const requiredParentLevel = childLevel - 1;
|
||||
return parentAgentOptionsForCreate.value.filter((a) => a.level === requiredParentLevel);
|
||||
}
|
||||
|
||||
function childAgentActionLabel(parentLevel: number) {
|
||||
return t('agent.create_level_agent', { level: lvlLabel(parentLevel + 1) });
|
||||
}
|
||||
|
||||
const createParentCreditCache = ref<Record<string, string>>({});
|
||||
|
||||
function findAgentRowByUserId(userId: string): AgentRow | undefined {
|
||||
const tier1 = tier1Agents.value.find((a) => a.userId === userId);
|
||||
if (tier1) return tier1;
|
||||
for (const lvl of visibleSubAgentTabLevels.value) {
|
||||
const hit = subAgentLevelState[lvl]?.agents.find((a) => a.userId === userId);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function ensureCreateParentCredit(agentId: string) {
|
||||
const hit = findAgentRowByUserId(agentId);
|
||||
if (hit) {
|
||||
createParentCreditCache.value[agentId] = hit.availableCredit;
|
||||
return;
|
||||
}
|
||||
if (createParentCreditCache.value[agentId]) return;
|
||||
try {
|
||||
const { data } = await api.get(`/admin/agents/${agentId}`);
|
||||
createParentCreditCache.value[agentId] = String(data.data.availableCredit ?? '0');
|
||||
} catch {
|
||||
createParentCreditCache.value[agentId] = '0';
|
||||
}
|
||||
}
|
||||
|
||||
const createParentAvailableCredit = computed(() => {
|
||||
if (createAccountMode.value !== 2 || !createParentAgentId.value) return null;
|
||||
const id = createParentAgentId.value;
|
||||
const hit = findAgentRowByUserId(id);
|
||||
if (hit) return hit.availableCredit;
|
||||
return createParentCreditCache.value[id] ?? null;
|
||||
});
|
||||
|
||||
const createSubCreditMax = computed(() => {
|
||||
const n = Number(createParentAvailableCredit.value ?? 0);
|
||||
return Number.isFinite(n) ? Math.max(0, n) : 0;
|
||||
});
|
||||
|
||||
function computeSubAgentCreditByRatio(available: number, ratioPercent: number): number {
|
||||
if (available <= 0) return 0;
|
||||
const pct = Math.min(100, Math.max(1, ratioPercent)) / 100;
|
||||
const raw = available * pct;
|
||||
const rounded = pct >= 1 ? available : Math.floor(raw / 100) * 100;
|
||||
return Math.min(available, Math.max(0, rounded));
|
||||
}
|
||||
|
||||
const creditQuickRatios = [10, 15, 20, 30] as const;
|
||||
|
||||
function computeDefaultSubAgentCreditLimit(available: number): number {
|
||||
return computeSubAgentCreditByRatio(available, hierarchySettings.value.defaultSubAgentCreditRatio ?? 50);
|
||||
}
|
||||
|
||||
function applyCreateSubAgentCreditRatio(ratioPercent: number) {
|
||||
createForm.value.creditLimit = computeSubAgentCreditByRatio(createSubCreditMax.value, ratioPercent);
|
||||
}
|
||||
|
||||
function applyCreateSubAgentDefaultCredit() {
|
||||
if (createAccountMode.value !== 2) return;
|
||||
createForm.value.creditLimit = computeDefaultSubAgentCreditLimit(createSubCreditMax.value);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [createVisible.value, createAccountMode.value, createParentAgentId.value] as const,
|
||||
([visible, mode, parentId]) => {
|
||||
if (!visible || mode !== 2 || !parentId) return;
|
||||
void ensureCreateParentCredit(parentId).then(() => {
|
||||
applyCreateSubAgentDefaultCredit();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
function canAgentCreateSub(row: Pick<AgentRow, 'level'>) {
|
||||
const max = hierarchySettings.value.maxAgentLevel;
|
||||
if (max === 0) return true;
|
||||
return row.level < max;
|
||||
}
|
||||
|
||||
function parentChainLabel(row: Pick<AgentRow, 'parentChainLabel' | 'parentUsername'>) {
|
||||
if (row.parentChainLabel) return row.parentChainLabel;
|
||||
return row.parentUsername ?? '—';
|
||||
}
|
||||
|
||||
const tier1AgentOptions = computed(() => agentOptions.value.filter((a) => a.level === 1));
|
||||
|
||||
const parentAgentOptionsForCreate = computed(() => {
|
||||
const max = hierarchySettings.value.maxAgentLevel;
|
||||
return agentOptions.value.filter((a) => max === 0 || a.level < max);
|
||||
});
|
||||
|
||||
const createParentSelectOptions = computed(() => {
|
||||
const childLevel = createToolbarChildLevel.value;
|
||||
if (childLevel == null || childLevel < 2) return [];
|
||||
return parentOptionsForChildLevel(childLevel);
|
||||
});
|
||||
|
||||
const createParentSelectPlaceholder = computed(() => {
|
||||
const childLevel = createToolbarChildLevel.value;
|
||||
if (childLevel == null || childLevel < 2) return t('user.filter.agent_ph');
|
||||
return t('agent.hint.select_parent_for_level', { level: lvlLabel(childLevel - 1) });
|
||||
});
|
||||
|
||||
function agentOptionLabel(a: { username: string; id: string; level: number; parentUsername?: string | null }) {
|
||||
if (a.level === 2 && a.parentUsername) {
|
||||
return `${a.parentUsername} / ${a.username} (#${a.id})`;
|
||||
}
|
||||
return `${a.username} (#${a.id})`;
|
||||
const chain = a.parentUsername ? `${a.parentUsername} / ${a.username}` : a.username;
|
||||
return `L${a.level} ${chain} (#${a.id})`;
|
||||
}
|
||||
|
||||
function resolveCreateParentLabel(agentId: string) {
|
||||
const hit = agentOptions.value.find((a) => a.id === agentId);
|
||||
if (hit) return agentOptionLabel(hit);
|
||||
const opt = agentOptions.value.find((a) => a.id === agentId);
|
||||
if (opt) return agentOptionLabel(opt);
|
||||
const tier1 = tier1Agents.value.find((a) => a.userId === agentId);
|
||||
if (tier1) return tier1.username;
|
||||
const tier2 = tier2Agents.value.find((a) => a.userId === agentId);
|
||||
if (tier2) {
|
||||
return tier2.parentUsername ? `${tier2.parentUsername} / ${tier2.username}` : tier2.username;
|
||||
const row = findAgentRowByUserId(agentId);
|
||||
if (row) {
|
||||
return row.parentUsername ? `${row.parentUsername} / ${row.username}` : row.username;
|
||||
}
|
||||
return agentId;
|
||||
}
|
||||
@@ -173,12 +369,16 @@ function resolveCreateParentLabel(agentId: string) {
|
||||
onMounted(() => {
|
||||
loadPlayerSettings();
|
||||
loadBettingLimits();
|
||||
loadAgentSuspendSettings();
|
||||
loadHierarchySettings();
|
||||
loadResetDatabaseStatus();
|
||||
loadAgentOptions();
|
||||
loadAllPlayers();
|
||||
loadTier1Agents();
|
||||
loadTier2Agents();
|
||||
loadAgentLevelCounts().then(() => {
|
||||
for (const lvl of visibleSubAgentTabLevels.value) {
|
||||
loadSubAgentsAtLevel(lvl);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ─── Load tier-1 agents ─── */
|
||||
@@ -212,40 +412,63 @@ function searchTier1Agents() {
|
||||
loadTier1Agents();
|
||||
}
|
||||
|
||||
/* ─── Load tier-2 agents ─── */
|
||||
async function loadTier2Agents() {
|
||||
async function loadAgentLevelCounts() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/agents/level-counts');
|
||||
const raw = data.data ?? {};
|
||||
const normalized: Record<number, number> = {};
|
||||
for (const [lvl, cnt] of Object.entries(raw)) {
|
||||
normalized[Number(lvl)] = Number(cnt) || 0;
|
||||
}
|
||||
agentLevelCounts.value = normalized;
|
||||
} catch {
|
||||
agentLevelCounts.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubAgentsAtLevel(level: number) {
|
||||
const st = ensureSubAgentState(level);
|
||||
const { data } = await api.get('/admin/agents', {
|
||||
params: {
|
||||
page: tier2Page.value,
|
||||
pageSize: tier2PageSize.value,
|
||||
keyword: tier2Keyword.value.trim() || undefined,
|
||||
status: tier2FilterStatus.value || undefined,
|
||||
level: 2,
|
||||
page: st.page,
|
||||
pageSize: st.pageSize,
|
||||
keyword: st.keyword.trim() || undefined,
|
||||
status: st.filterStatus || undefined,
|
||||
level,
|
||||
},
|
||||
});
|
||||
tier2Agents.value = data.data.items as AgentRow[];
|
||||
tier2Total.value = data.data.total;
|
||||
st.agents = data.data.items as AgentRow[];
|
||||
st.total = data.data.total;
|
||||
}
|
||||
|
||||
function onTier2PageChange(p: number) {
|
||||
tier2Page.value = p;
|
||||
loadTier2Agents();
|
||||
function onSubAgentPageChange(level: number, p: number) {
|
||||
ensureSubAgentState(level).page = p;
|
||||
loadSubAgentsAtLevel(level);
|
||||
}
|
||||
|
||||
function onTier2SizeChange(size: number) {
|
||||
tier2PageSize.value = size;
|
||||
tier2Page.value = 1;
|
||||
loadTier2Agents();
|
||||
function onSubAgentSizeChange(level: number, size: number) {
|
||||
const st = ensureSubAgentState(level);
|
||||
st.pageSize = size;
|
||||
st.page = 1;
|
||||
loadSubAgentsAtLevel(level);
|
||||
}
|
||||
|
||||
function searchTier2Agents() {
|
||||
tier2Page.value = 1;
|
||||
loadTier2Agents();
|
||||
function searchSubAgentsAtLevel(level: number) {
|
||||
ensureSubAgentState(level).page = 1;
|
||||
loadSubAgentsAtLevel(level);
|
||||
}
|
||||
|
||||
function reloadSubAgentTabs() {
|
||||
return loadAgentLevelCounts().then(() => {
|
||||
for (const lvl of visibleSubAgentTabLevels.value) {
|
||||
loadSubAgentsAtLevel(lvl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reloadAgentLists() {
|
||||
loadTier1Agents();
|
||||
loadTier2Agents();
|
||||
void reloadSubAgentTabs();
|
||||
}
|
||||
|
||||
/* ─── Load main agent list ─── */
|
||||
@@ -313,10 +536,24 @@ function onTier1AgentRowClick(row: AgentRow, _column: unknown, event: MouseEvent
|
||||
onAgentRowClick(row, tier1AgentTableRef, _column, event);
|
||||
}
|
||||
|
||||
function onTier2AgentRowClick(row: AgentRow, _column: unknown, event: MouseEvent) {
|
||||
onAgentRowClick(row, tier2AgentTableRef, _column, event);
|
||||
function onSubAgentRowClick(level: number, row: AgentRow, _column: unknown, event: MouseEvent) {
|
||||
const tableRef = { value: subAgentTableRefs.value[level] };
|
||||
onAgentRowClick(row, tableRef, _column, event);
|
||||
}
|
||||
|
||||
watch(activeViewTab, (tab) => {
|
||||
const m = /^agentLevel-(\d+)$/.exec(tab);
|
||||
if (m) loadSubAgentsAtLevel(Number(m[1]));
|
||||
});
|
||||
|
||||
watch(visibleSubAgentTabLevels, (levels, prev) => {
|
||||
for (const lvl of levels) {
|
||||
if (!prev?.includes(lvl) || !subAgentLevelState[lvl]?.agents.length) {
|
||||
loadSubAgentsAtLevel(lvl);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* ─── Expansion ─── */
|
||||
async function onExpandChange(row: DisplayAgentRow, expandedRows: DisplayAgentRow[]) {
|
||||
expandedSet.value = new Set(expandedRows.map((r) => r.userId));
|
||||
@@ -442,30 +679,40 @@ async function savePlayerSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAgentSuspendSettings() {
|
||||
async function loadHierarchySettings() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/agents/settings/suspend');
|
||||
agentSuspendSettings.value = data.data;
|
||||
const { data } = await api.get('/admin/agents/settings/hierarchy');
|
||||
hierarchySettings.value = data.data;
|
||||
} catch {
|
||||
/* defaults */
|
||||
hierarchySettings.value = { maxAgentLevel: 0, defaultSubAgentCreditRatio: 50 };
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAgentSuspendSettings() {
|
||||
agentSuspendSaving.value = true;
|
||||
async function saveHierarchySettings() {
|
||||
hierarchySaving.value = true;
|
||||
try {
|
||||
const { data } = await api.put('/admin/agents/settings/suspend', agentSuspendSettings.value);
|
||||
agentSuspendSettings.value = data.data;
|
||||
const { data } = await api.put('/admin/agents/settings/hierarchy', hierarchySettings.value);
|
||||
hierarchySettings.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'));
|
||||
loadAgentSuspendSettings();
|
||||
loadHierarchySettings();
|
||||
} finally {
|
||||
agentSuspendSaving.value = false;
|
||||
hierarchySaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* ─── Create (unified) ─── */
|
||||
function openCreateAccount() {
|
||||
createForm.value = emptyPlayerCreateForm();
|
||||
@@ -500,15 +747,19 @@ function openCreateSubAgent(parentAgentUserId: string) {
|
||||
createForm.value.asTier1Agent = false;
|
||||
createParentAgentId.value = parentAgentUserId;
|
||||
createParentLocked.value = true;
|
||||
const parentLevel = resolveParentAgentLevel(parentAgentUserId);
|
||||
createToolbarChildLevel.value = parentLevel != null ? parentLevel + 1 : null;
|
||||
createAccountMode.value = 2;
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
function openCreateSubAgentFromToolbar() {
|
||||
function openCreateSubAgentFromToolbar(childLevel: number) {
|
||||
createForm.value = emptyPlayerCreateForm();
|
||||
createForm.value.asTier1Agent = false;
|
||||
createParentAgentId.value = '';
|
||||
createParentLocked.value = false;
|
||||
createToolbarChildLevel.value = childLevel;
|
||||
const parents = parentOptionsForChildLevel(childLevel);
|
||||
createParentAgentId.value = parents.length === 1 ? parents[0].id : '';
|
||||
createAccountMode.value = 2;
|
||||
createVisible.value = true;
|
||||
}
|
||||
@@ -519,10 +770,20 @@ async function submitCreate() {
|
||||
let payload: Record<string, unknown>;
|
||||
try {
|
||||
if (isSubAgent) {
|
||||
if (!createParentAgentId.value) throw new Error(t('agent.hint.sub_agent_parent'));
|
||||
if (!createParentAgentId.value) {
|
||||
throw new Error(t('agent.hint.select_parent_for_level', { level: lvlLabel((createToolbarChildLevel.value ?? 2) - 1) }));
|
||||
}
|
||||
const parentLevel = resolveParentAgentLevel(createParentAgentId.value);
|
||||
const targetLevel = createTargetAgentLevel.value;
|
||||
if (targetLevel == null || parentLevel == null || parentLevel !== targetLevel - 1) {
|
||||
throw new Error(t('agent.err.parent_level_mismatch', { level: lvlLabel(targetLevel ?? 0) }));
|
||||
}
|
||||
if (!createForm.value.username.trim()) throw new Error(t('err.username_required'));
|
||||
if (createForm.value.password.length < 8) throw new Error(t('err.password_min'));
|
||||
if (createForm.value.password !== createForm.value.confirmPassword) throw new Error(t('err.password_mismatch'));
|
||||
if (createForm.value.creditLimit > createSubCreditMax.value) {
|
||||
throw new Error(t('err.insufficient_credit'));
|
||||
}
|
||||
payload = {
|
||||
username: createForm.value.username.trim(),
|
||||
password: createForm.value.password,
|
||||
@@ -555,8 +816,12 @@ async function submitCreate() {
|
||||
: t('user.msg.created_with_password', { password: createForm.value.password }),
|
||||
);
|
||||
createVisible.value = false;
|
||||
const createdLevel = isSubAgent ? createTargetAgentLevel.value : null;
|
||||
load();
|
||||
refreshExpandedParents();
|
||||
if (createdLevel != null && createdLevel >= 2) {
|
||||
activeViewTab.value = agentLevelTabName(createdLevel);
|
||||
}
|
||||
const parentId = createParentAgentId.value || createForm.value.parentId;
|
||||
if (parentId) {
|
||||
await loadExpansionData(parentId);
|
||||
@@ -752,87 +1017,58 @@ async function toggleFreezePlayer(row: PlayerRow) {
|
||||
}
|
||||
|
||||
/* ─── Freeze / Unfreeze Agent ─── */
|
||||
async function toggleFreezeAgent(row: AgentRow) {
|
||||
const accountStatus = subAgentAccountStatus(row);
|
||||
const freeze = accountStatus === 'ACTIVE';
|
||||
const action = freeze ? t('common.freeze') : t('common.unfreeze');
|
||||
const freezeAgentIsSuspend = computed(() => {
|
||||
if (!freezeAgentTarget.value) return true;
|
||||
return subAgentAccountStatus(freezeAgentTarget.value) === 'ACTIVE';
|
||||
});
|
||||
|
||||
if (!freeze) {
|
||||
// Unfreeze: simple confirm
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('agent.freeze.confirm_unfreeze_body', { name: row.username }),
|
||||
t('agent.freeze.confirm_freeze_title'),
|
||||
{ type: 'info', confirmButtonText: action, cancelButtonText: t('common.cancel') },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.put(`/admin/agents/${row.userId}`, { status: 'ACTIVE' });
|
||||
ElMessage.success(t('agent.msg.freeze_done', { action }));
|
||||
load();
|
||||
refreshExpandedParents();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.freeze_failed', { action }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
function toggleFreezeAgent(row: AgentRow) {
|
||||
freezeAgentTarget.value = row;
|
||||
freezeAgentForm.value = {
|
||||
freezeDirectPlayers: false,
|
||||
blockDirectPlayerLogin: false,
|
||||
unfreezeDirectPlayers: false,
|
||||
};
|
||||
freezeAgentVisible.value = true;
|
||||
}
|
||||
|
||||
// Freeze: offer cascade option via a custom dialog
|
||||
let freezeDirectPlayers = false;
|
||||
async function submitFreezeAgent() {
|
||||
const row = freezeAgentTarget.value;
|
||||
if (!row) return;
|
||||
const suspend = freezeAgentIsSuspend.value;
|
||||
const action = suspend ? t('common.freeze') : t('common.unfreeze');
|
||||
freezeAgentLoading.value = true;
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('agent.freeze.confirm_freeze_body', { name: row.username }),
|
||||
t('agent.freeze.confirm_freeze_title'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: action,
|
||||
cancelButtonText: t('common.cancel'),
|
||||
distinguishCancelAndClose: true,
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// After confirming freeze, ask about cascade (only when enabled in settings)
|
||||
if (row.directPlayerCount > 0 && agentSuspendSettings.value.suspendFreezeDirectPlayers) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
t('agent.freeze.cascade_hint'),
|
||||
t('agent.freeze.cascade_label'),
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: t('common.yes') || '是',
|
||||
cancelButtonText: t('common.no') || '否',
|
||||
distinguishCancelAndClose: true,
|
||||
},
|
||||
if (suspend) {
|
||||
await api.put(`/admin/agents/${row.userId}`, {
|
||||
status: 'SUSPENDED',
|
||||
freezeDirectPlayers: freezeAgentForm.value.freezeDirectPlayers,
|
||||
blockDirectPlayerLogin: freezeAgentForm.value.blockDirectPlayerLogin,
|
||||
});
|
||||
ElMessage.success(
|
||||
freezeAgentForm.value.freezeDirectPlayers
|
||||
? t('agent.msg.cascade_freeze_done')
|
||||
: t('agent.msg.freeze_done', { action }),
|
||||
);
|
||||
} else {
|
||||
await api.put(`/admin/agents/${row.userId}`, {
|
||||
status: 'ACTIVE',
|
||||
unfreezeDirectPlayers: freezeAgentForm.value.unfreezeDirectPlayers,
|
||||
});
|
||||
ElMessage.success(
|
||||
freezeAgentForm.value.unfreezeDirectPlayers
|
||||
? t('agent.msg.cascade_unfreeze_done')
|
||||
: t('agent.msg.freeze_done', { action }),
|
||||
);
|
||||
freezeDirectPlayers = true;
|
||||
} catch {
|
||||
freezeDirectPlayers = false;
|
||||
}
|
||||
} else if (row.directPlayerCount > 0 && !agentSuspendSettings.value.suspendFreezeDirectPlayers) {
|
||||
ElMessage.info(t('agent.suspend.cascade_disabled_hint'));
|
||||
}
|
||||
|
||||
try {
|
||||
await api.put(`/admin/agents/${row.userId}`, {
|
||||
status: 'SUSPENDED',
|
||||
freezeDirectPlayers,
|
||||
});
|
||||
ElMessage.success(
|
||||
freezeDirectPlayers
|
||||
? t('agent.msg.cascade_freeze_done')
|
||||
: t('agent.msg.freeze_done', { action }),
|
||||
);
|
||||
freezeAgentVisible.value = false;
|
||||
load();
|
||||
refreshExpandedParents();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.freeze_failed', { action }));
|
||||
} finally {
|
||||
freezeAgentLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -930,22 +1166,31 @@ function creditTypeLabel(type: string) {
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="list-settings-block">
|
||||
<p class="list-settings-title">{{ t('agent.suspend.settings_title') }}</p>
|
||||
<p class="list-settings-hint">{{ t('agent.suspend.settings_hint') }}</p>
|
||||
<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.suspend.freeze_direct_players')">
|
||||
<el-switch
|
||||
v-model="agentSuspendSettings.suspendFreezeDirectPlayers"
|
||||
:loading="agentSuspendSaving"
|
||||
@change="saveAgentSuspendSettings"
|
||||
<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 :label="t('agent.suspend.block_player_login')">
|
||||
<el-switch
|
||||
v-model="agentSuspendSettings.suspendBlockPlayerLogin"
|
||||
:loading="agentSuspendSaving"
|
||||
@change="saveAgentSuspendSettings"
|
||||
<el-form-item :label="t('agent.hierarchy.default_sub_credit_ratio')">
|
||||
<el-input-number
|
||||
v-model="hierarchySettings.defaultSubAgentCreditRatio"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="5"
|
||||
controls-position="right"
|
||||
:disabled="hierarchySaving"
|
||||
/>
|
||||
<span class="list-settings-unit">%</span>
|
||||
</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>
|
||||
@@ -1080,10 +1325,11 @@ function creditTypeLabel(type: string) {
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.actions')" width="340" fixed="right" align="center">
|
||||
<el-table-column :label="t('common.actions')" width="420" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-btns">
|
||||
<el-button size="small" link type="primary" @click="openDetailPlayer(row.id)">{{ t('common.detail') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openPlayerWalletLedger(row.id, row.username)">{{ t('user.action.view_wallet_ledger') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openEditPlayer(row.id)">{{ t('common.edit') }}</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>
|
||||
@@ -1110,7 +1356,7 @@ function creditTypeLabel(type: string) {
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ─── Tab: 一级代理 ─── -->
|
||||
<el-tab-pane :label="`${t('user.type.tier1_agent')} (${tier1Total})`" name="tier1Agents">
|
||||
<el-tab-pane :label="`${agentTierName(1)} (${tier1Total})`" name="tier1Agents">
|
||||
<section class="list-panel agent-list-panel">
|
||||
<div class="list-panel-toolbar">
|
||||
<el-form inline class="list-chrome__grow">
|
||||
@@ -1138,7 +1384,7 @@ function creditTypeLabel(type: string) {
|
||||
stripe
|
||||
row-key="userId"
|
||||
:row-class-name="expandableTableRowClassName"
|
||||
class="expandable-table"
|
||||
class="expandable-table compact-agent-table"
|
||||
@expand-change="onExpandChange"
|
||||
@row-click="onTier1AgentRowClick"
|
||||
>
|
||||
@@ -1206,25 +1452,25 @@ function creditTypeLabel(type: string) {
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="userId" label="ID" width="72" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="140" />
|
||||
<el-table-column :label="t('common.status')" width="88">
|
||||
<el-table-column prop="userId" label="ID" width="64" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" width="100" show-overflow-tooltip />
|
||||
<el-table-column :label="t('common.status')" width="72">
|
||||
<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="t('agent.col.level')" width="60" align="center">
|
||||
<el-table-column prop="level" :label="t('agent.col.level')" width="52" align="center">
|
||||
<template #default="{ row }">L{{ row.level }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('agent.col.credit')" min-width="168" align="right">
|
||||
<el-table-column :label="t('agent.col.credit')" width="132" 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="t('agent.col.direct_players')" width="80" align="center" />
|
||||
<el-table-column prop="childAgentCount" :label="t('agent.col.sub_agents')" width="80" align="center" />
|
||||
<el-table-column prop="directPlayerCount" :label="t('agent.col.direct_players')" width="72" align="center" />
|
||||
<el-table-column prop="childAgentCount" :label="t('agent.col.sub_agents')" width="72" align="center" />
|
||||
<el-table-column :label="t('agent.col.cashback')" width="80" align="right">
|
||||
<template #default="{ row }">{{ row.cashbackRate }}</template>
|
||||
</el-table-column>
|
||||
@@ -1234,7 +1480,7 @@ function creditTypeLabel(type: string) {
|
||||
<el-button size="small" link type="primary" @click="openDetailAgent(row.userId)">{{ t('common.detail') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openEditAgent(row.userId)">{{ t('common.edit') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openCredit(row.userId)">{{ t('common.adjust_credit') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openCreateSubAgent(row.userId)">{{ t('agent.create_sub') }}</el-button>
|
||||
<el-button v-if="canAgentCreateSub(row)" size="small" link type="primary" @click="openCreateSubAgent(row.userId)">{{ childAgentActionLabel(row.level) }}</el-button>
|
||||
<el-button v-if="row.status === 'ACTIVE'" size="small" link type="warning" @click="toggleFreezeAgent(row)">{{ t('common.freeze') }}</el-button>
|
||||
<el-button v-else size="small" link type="primary" @click="toggleFreezeAgent(row)">{{ t('common.unfreeze') }}</el-button>
|
||||
</div>
|
||||
@@ -1257,38 +1503,56 @@ function creditTypeLabel(type: string) {
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ─── Tab: 二级代理 ─── -->
|
||||
<el-tab-pane :label="`${t('user.type.sub_agent')} (${tier2Total})`" name="tier2Agents">
|
||||
<!-- ─── Tab: L2+ 各级代理 ─── -->
|
||||
<el-tab-pane
|
||||
v-for="agentLevel in visibleSubAgentTabLevels"
|
||||
:key="agentLevel"
|
||||
:label="agentLevelTabLabel(agentLevel)"
|
||||
:name="agentLevelTabName(agentLevel)"
|
||||
>
|
||||
<section class="list-panel agent-list-panel">
|
||||
<div class="list-panel-toolbar">
|
||||
<el-form inline class="list-chrome__grow">
|
||||
<el-form-item :label="t('common.keyword')">
|
||||
<el-input v-model="tier2Keyword" :placeholder="t('agent.filter.username_ph')" clearable style="width: 180px" @keyup.enter="searchTier2Agents" />
|
||||
<el-input
|
||||
v-model="ensureSubAgentState(agentLevel).keyword"
|
||||
:placeholder="t('agent.filter.username_ph')"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter="searchSubAgentsAtLevel(agentLevel)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('common.status')">
|
||||
<el-select v-model="tier2FilterStatus" :placeholder="t('common.all')" clearable style="width: 120px">
|
||||
<el-select
|
||||
v-model="ensureSubAgentState(agentLevel).filterStatus"
|
||||
:placeholder="t('common.all')"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option :label="t('user.status.ACTIVE')" value="ACTIVE" />
|
||||
<el-option :label="t('user.status.SUSPENDED')" value="SUSPENDED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="searchTier2Agents">{{ t('common.search') }}</el-button>
|
||||
<el-button type="primary" @click="searchSubAgentsAtLevel(agentLevel)">{{ t('common.search') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="list-chrome__actions">
|
||||
<el-button type="primary" @click="openCreateSubAgentFromToolbar">{{ t('agent.create_sub_btn') }}</el-button>
|
||||
<el-button type="primary" @click="openCreateSubAgentFromToolbar(agentLevel)">
|
||||
{{ agentLevel === 2 ? t('agent.create_sub_btn') : t('agent.create_level_agent_btn', { level: lvlLabel(agentLevel) }) }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<el-table
|
||||
ref="tier2AgentTableRef"
|
||||
:data="tier2Agents"
|
||||
:ref="(el) => { subAgentTableRefs[agentLevel] = el as { toggleRowExpansion: (row: AgentRow) => void } | null }"
|
||||
:data="ensureSubAgentState(agentLevel).agents"
|
||||
stripe
|
||||
row-key="userId"
|
||||
:row-class-name="expandableTableRowClassName"
|
||||
class="expandable-table"
|
||||
class="expandable-table compact-agent-table"
|
||||
@expand-change="onExpandChange"
|
||||
@row-click="onTier2AgentRowClick"
|
||||
@row-click="(row, column, event) => onSubAgentRowClick(agentLevel, row, column, event)"
|
||||
>
|
||||
<template #empty>
|
||||
<AdminTableEmpty />
|
||||
@@ -1331,30 +1595,31 @@ function creditTypeLabel(type: string) {
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="userId" label="ID" width="72" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="120" />
|
||||
<el-table-column :label="t('user.type.tier1_agent')" min-width="120">
|
||||
<template #default="{ row }">{{ row.parentUsername ?? '—' }}</template>
|
||||
<el-table-column prop="userId" label="ID" width="64" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" width="100" show-overflow-tooltip />
|
||||
<el-table-column :label="t('agent.col.parent_chain')" width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ parentChainLabel(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.status')" width="88">
|
||||
<el-table-column :label="t('common.status')" width="72">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(subAgentAccountStatus(row))" size="small">{{ statusLabel(subAgentAccountStatus(row)) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('agent.col.credit')" min-width="168" align="right">
|
||||
<el-table-column :label="t('agent.col.credit')" width="132" 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="t('agent.col.direct_players')" width="80" align="center" />
|
||||
<el-table-column :label="t('common.actions')" width="340" fixed="right" align="center">
|
||||
<el-table-column prop="directPlayerCount" :label="t('agent.col.direct_players')" width="72" align="center" />
|
||||
<el-table-column :label="t('common.actions')" width="400" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="action-btns" @click.stop>
|
||||
<el-button size="small" link type="primary" @click="openDetailAgent(row.userId)">{{ t('common.detail') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openEditAgent(row.userId)">{{ t('common.edit') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openCredit(row.userId)">{{ t('common.adjust_credit') }}</el-button>
|
||||
<el-button v-if="canAgentCreateSub(row)" size="small" link type="primary" @click="openCreateSubAgent(row.userId)">{{ childAgentActionLabel(row.level) }}</el-button>
|
||||
<el-button v-if="subAgentAccountStatus(row) === 'ACTIVE'" size="small" link type="warning" @click="toggleFreezeAgent(row)">{{ t('common.freeze') }}</el-button>
|
||||
<el-button v-else size="small" link type="primary" @click="toggleFreezeAgent(row)">{{ t('common.unfreeze') }}</el-button>
|
||||
</div>
|
||||
@@ -1364,14 +1629,14 @@ function creditTypeLabel(type: string) {
|
||||
</div>
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="tier2Page"
|
||||
v-model:page-size="tier2PageSize"
|
||||
:total="tier2Total"
|
||||
:current-page="ensureSubAgentState(agentLevel).page"
|
||||
:page-size="ensureSubAgentState(agentLevel).pageSize"
|
||||
:total="ensureSubAgentState(agentLevel).total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@current-change="onTier2PageChange"
|
||||
@size-change="onTier2SizeChange"
|
||||
@current-change="(p) => onSubAgentPageChange(agentLevel, p)"
|
||||
@size-change="(size) => onSubAgentSizeChange(agentLevel, size)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1380,92 +1645,203 @@ function creditTypeLabel(type: string) {
|
||||
|
||||
<!-- ═══════════ DIALOGS ═══════════ -->
|
||||
|
||||
<!-- ── Freeze / Unfreeze Agent ── -->
|
||||
<el-dialog
|
||||
v-model="freezeAgentVisible"
|
||||
:title="freezeAgentIsSuspend ? t('agent.freeze.confirm_freeze_title') : t('agent.unfreeze.confirm_title')"
|
||||
width="480px"
|
||||
destroy-on-close
|
||||
>
|
||||
<template v-if="freezeAgentTarget">
|
||||
<p class="freeze-agent-intro">
|
||||
{{
|
||||
freezeAgentIsSuspend
|
||||
? t('agent.freeze.confirm_freeze_body', { name: freezeAgentTarget.username })
|
||||
: t('agent.freeze.confirm_unfreeze_body', { name: freezeAgentTarget.username })
|
||||
}}
|
||||
</p>
|
||||
<div v-if="freezeAgentIsSuspend" class="freeze-agent-options">
|
||||
<el-checkbox
|
||||
v-if="freezeAgentTarget.directPlayerCount > 0"
|
||||
v-model="freezeAgentForm.freezeDirectPlayers"
|
||||
>
|
||||
{{ t('agent.freeze.opt_freeze_direct_players') }}
|
||||
</el-checkbox>
|
||||
<el-checkbox v-model="freezeAgentForm.blockDirectPlayerLogin">
|
||||
{{ t('agent.freeze.opt_block_player_login') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div v-else class="freeze-agent-options">
|
||||
<el-checkbox
|
||||
v-if="freezeAgentTarget.directPlayerCount > 0"
|
||||
v-model="freezeAgentForm.unfreezeDirectPlayers"
|
||||
>
|
||||
{{ t('agent.unfreeze.opt_unfreeze_direct_players') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button @click="freezeAgentVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button
|
||||
:type="freezeAgentIsSuspend ? 'warning' : 'primary'"
|
||||
:loading="freezeAgentLoading"
|
||||
@click="submitFreezeAgent"
|
||||
>
|
||||
{{ freezeAgentIsSuspend ? t('common.freeze') : t('common.unfreeze') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- ── Create (unified) ── -->
|
||||
<el-dialog v-model="createVisible" :title="createDialogTitle" width="520px" destroy-on-close class="create-account-dialog">
|
||||
<el-form label-width="100px">
|
||||
<el-dialog
|
||||
v-model="createVisible"
|
||||
:title="createDialogTitle"
|
||||
width="460px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
class="create-account-dialog"
|
||||
>
|
||||
<div
|
||||
v-if="createAccountMode === 2 && createParentAgentId && createParentLocked"
|
||||
class="create-meta-bar"
|
||||
>
|
||||
<div class="create-meta-row">
|
||||
<span class="create-meta-label">{{ t('agent.field.parent_agent') }}</span>
|
||||
<span class="create-meta-value">{{ resolveCreateParentLabel(createParentAgentId) }}</span>
|
||||
<el-tag v-if="createTargetAgentLevel" size="small" type="info">L{{ createTargetAgentLevel }}</el-tag>
|
||||
</div>
|
||||
<div class="create-meta-row">
|
||||
<span class="create-meta-label">{{ t('agent.field.available_credit') }}</span>
|
||||
<span class="create-meta-value c-green">{{ formatAmount(createParentAvailableCredit ?? '0') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form label-width="100px" size="small" class="compact-create-form">
|
||||
<template v-if="createAccountMode === 2 && !createParentLocked">
|
||||
<el-form-item :label="t('agent.field.parent_agent')" required>
|
||||
<el-select
|
||||
v-model="createParentAgentId"
|
||||
:placeholder="createParentSelectPlaceholder"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="a in createParentSelectOptions"
|
||||
:key="a.id"
|
||||
:label="agentOptionLabel(a)"
|
||||
:value="a.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div v-if="createTargetAgentLevel" class="field-hint">
|
||||
{{ t('agent.hierarchy.create_level_hint', { n: lvlLabel(createTargetAgentLevel) }) }}
|
||||
· {{ t('agent.hint.select_parent_for_level', { level: lvlLabel(createTargetAgentLevel - 1) }) }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div v-if="createParentAgentId" class="create-meta-bar create-meta-bar--inline">
|
||||
<div class="create-meta-row">
|
||||
<span class="create-meta-label">{{ t('agent.field.available_credit') }}</span>
|
||||
<span class="create-meta-value c-green">{{ formatAmount(createParentAvailableCredit ?? '0') }}</span>
|
||||
<el-tag v-if="createTargetAgentLevel" size="small" type="info">L{{ createTargetAgentLevel }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form-item :label="t('user.col.username')" required>
|
||||
<el-input
|
||||
v-model="createForm.username"
|
||||
:placeholder="createAccountMode === 0 ? t('user.ph.username_player') : t('user.ph.username_unique')"
|
||||
/>
|
||||
<div v-if="createAccountMode === 0" class="field-hint">{{ t('user.hint.username_player') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.password')" required>
|
||||
<el-input v-model="createForm.password" type="text" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.confirm_password')" required>
|
||||
<el-input v-model="createForm.confirmPassword" type="text" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<div class="create-form-pair">
|
||||
<el-form-item :label="t('user.field.password')" required>
|
||||
<el-input v-model="createForm.password" type="text" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.confirm_password')" required>
|
||||
<el-input v-model="createForm.confirmPassword" type="text" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 玩家:从代理行/展开区进入时锁定所属代理 -->
|
||||
<el-form-item v-if="createAccountMode === 0 && createParentLocked" :label="t('user.filter.agent')">
|
||||
<el-tag type="info" class="account-type-tag">{{ resolveCreateParentLabel(createParentAgentId) }}</el-tag>
|
||||
<div class="field-hint">{{ t('agent.hint.creating_under_agent') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 玩家:从玩家 Tab 进入时可选择一级/二级代理 -->
|
||||
<template v-if="createAccountMode === 0 && !createParentLocked">
|
||||
<el-form-item :label="t('user.filter.agent')">
|
||||
<el-select v-model="createForm.parentId" :placeholder="t('user.ph.no_agent')" clearable style="width: 100%">
|
||||
<el-option v-for="a in agentOptions" :key="a.id" :label="agentOptionLabel(a)" :value="a.id" />
|
||||
</el-select>
|
||||
<div class="field-hint">{{ t('user.hint.no_agent') }}</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 二级代理:从一级代理行进入时锁定上级 -->
|
||||
<el-form-item v-if="createAccountMode === 2 && createParentLocked" :label="t('user.type.tier1_agent')">
|
||||
<el-tag type="info" class="account-type-tag">{{ resolveCreateParentLabel(createParentAgentId) }}</el-tag>
|
||||
<div class="field-hint">{{ t('agent.hint.creating_under_agent') }}</div>
|
||||
<el-form-item v-if="createAccountMode === 0 && !createParentLocked" :label="t('user.filter.agent')">
|
||||
<el-select v-model="createForm.parentId" :placeholder="t('user.ph.no_agent')" clearable style="width: 100%">
|
||||
<el-option v-for="a in agentOptions" :key="a.id" :label="agentOptionLabel(a)" :value="a.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 二级代理:从二级 Tab 进入时选择一级代理 -->
|
||||
<template v-if="createAccountMode === 2 && !createParentLocked">
|
||||
<el-form-item :label="t('user.type.tier1_agent')" required>
|
||||
<el-select v-model="createParentAgentId" :placeholder="t('user.filter.agent_ph')" style="width: 100%">
|
||||
<el-option v-for="a in tier1AgentOptions" :key="a.id" :label="agentOptionLabel(a)" :value="a.id" />
|
||||
</el-select>
|
||||
<div class="field-hint">{{ t('agent.hint.sub_agent_parent') }}</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 代理字段(一级 / 二级) -->
|
||||
<template v-if="createAccountMode === 1 || createAccountMode === 2">
|
||||
<el-form-item :label="t('agent.field.credit_limit')" required>
|
||||
<el-input-number v-model="createForm.creditLimit" :min="0" :step="10000" style="width: 100%" />
|
||||
<div class="field-hint">{{ t('agent.hint.credit_limit') }}</div>
|
||||
<el-input-number
|
||||
v-model="createForm.creditLimit"
|
||||
:min="0"
|
||||
:max="createAccountMode === 2 ? createSubCreditMax : undefined"
|
||||
:step="1000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div v-if="createAccountMode === 2 && createParentAgentId" class="credit-quick-row">
|
||||
<div class="field-hint">
|
||||
{{ t('agent.hierarchy.create_credit_quick_hint', { amount: formatAmount(createParentAvailableCredit ?? '0') }) }}
|
||||
</div>
|
||||
<div class="credit-quick-btns">
|
||||
<el-button
|
||||
v-for="ratio in creditQuickRatios"
|
||||
:key="ratio"
|
||||
size="small"
|
||||
:disabled="createSubCreditMax <= 0"
|
||||
@click="applyCreateSubAgentCreditRatio(ratio)"
|
||||
>
|
||||
{{ ratio }}%
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="field-hint">{{ t('agent.hint.credit_limit') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('agent.field.cashback_rate')">
|
||||
<el-input-number v-model="createForm.cashbackRate" :min="0" :max="1" :step="0.001" :precision="4" style="width: 100%" />
|
||||
<div class="field-hint">{{ t('agent.hint.cashback_example') }}</div>
|
||||
<el-input-number
|
||||
v-model="createForm.cashbackRate"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.001"
|
||||
:precision="4"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('agent.field.max_single_deposit')">
|
||||
<el-input-number v-model="createForm.maxSingleDeposit" :min="0" :step="100" style="width: 100%" />
|
||||
<div class="field-hint">{{ t('agent.hint.deposit_limit_empty') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('agent.field.max_daily_deposit')">
|
||||
<el-input-number v-model="createForm.maxDailyDeposit" :min="0" :step="1000" style="width: 100%" />
|
||||
<div class="field-hint">{{ t('agent.hint.deposit_limit_empty') }}</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item :label="t('user.field.phone')">
|
||||
<el-input v-model="createForm.phone" :placeholder="t('common.optional')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.email')">
|
||||
<el-input v-model="createForm.email" :placeholder="t('common.optional')" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- Player-only: initial balance -->
|
||||
<template v-if="createAccountMode === 0">
|
||||
<div class="create-form-pair">
|
||||
<el-form-item :label="t('user.field.phone')">
|
||||
<el-input v-model="createForm.phone" :placeholder="t('common.optional')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.email')">
|
||||
<el-input v-model="createForm.email" :placeholder="t('common.optional')" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item :label="t('user.field.initial_balance')">
|
||||
<el-input-number v-model="createForm.initialDeposit" :min="0" :step="100" style="width: 100%" />
|
||||
<div class="field-hint">{{ t('user.hint.initial_balance') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.deposit_remark')">
|
||||
<el-input v-model="createForm.remark" :placeholder="t('user.ph.remark_initial')" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<template v-else-if="createAccountMode === 1">
|
||||
<div class="create-form-pair">
|
||||
<el-form-item :label="t('user.field.phone')">
|
||||
<el-input v-model="createForm.phone" :placeholder="t('common.optional')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.email')">
|
||||
<el-input v-model="createForm.email" :placeholder="t('common.optional')" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
@@ -1682,6 +2058,11 @@ function creditTypeLabel(type: string) {
|
||||
<el-descriptions-item :label="t('user.field.login_fail')">{{ t('user.login_fail_value', { n: playerDetail.loginFailCount }) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('user.field.registered_at')" :span="2">{{ formatTime(playerDetail.createdAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="detail-actions">
|
||||
<el-button type="primary" link @click="openPlayerWalletLedger(playerDetail.id, playerDetail.username)">
|
||||
{{ t('user.action.view_wallet_ledger') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -1756,6 +2137,12 @@ function creditTypeLabel(type: string) {
|
||||
</el-table>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<PlayerWalletLedgerDialog
|
||||
v-model="walletLedgerVisible"
|
||||
:player-id="walletLedgerPlayerId"
|
||||
:player-username="walletLedgerPlayerUsername"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1792,6 +2179,18 @@ function creditTypeLabel(type: string) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.freeze-agent-intro {
|
||||
margin: 0 0 16px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.freeze-agent-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ─── Table toolbar ─── */
|
||||
.list-panel-toolbar {
|
||||
flex-shrink: 0;
|
||||
@@ -1827,7 +2226,6 @@ function creditTypeLabel(type: string) {
|
||||
padding: 12px 14px 14px;
|
||||
background: #141414;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-left: 3px solid rgba(47, 181, 106, 0.45);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.expand-loading {
|
||||
@@ -1850,6 +2248,24 @@ function creditTypeLabel(type: string) {
|
||||
.expandable-table :deep(.row-expandable) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.compact-agent-table :deep(.el-table__header .el-table__cell),
|
||||
.compact-agent-table :deep(.el-table__body .el-table__cell) {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.compact-agent-table :deep(.el-table__header .cell) {
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
.compact-agent-table :deep(.el-table__body .cell) {
|
||||
padding: 0;
|
||||
line-height: 1.35;
|
||||
font-size: 13px;
|
||||
}
|
||||
.compact-agent-table :deep(.el-tag) {
|
||||
max-width: 100%;
|
||||
}
|
||||
.nested-table {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -1891,9 +2307,6 @@ function creditTypeLabel(type: string) {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.expandable-table :deep(.row-expandable) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.action-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1909,6 +2322,71 @@ function creditTypeLabel(type: string) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
.field-hint { font-size: 12px; color: #888; margin-top: 4px; }
|
||||
:deep(.create-account-dialog .el-dialog__body) {
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
padding: 14px 20px 6px;
|
||||
}
|
||||
:deep(.create-account-dialog .el-dialog__footer) {
|
||||
padding-top: 4px;
|
||||
}
|
||||
.compact-create-form :deep(.el-form-item) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.compact-create-form :deep(.el-form-item__label) {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.credit-quick-row {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.credit-quick-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.create-meta-bar {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
.create-meta-bar--inline {
|
||||
margin-top: -4px;
|
||||
}
|
||||
.create-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.create-meta-row + .create-meta-row {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.create-meta-label {
|
||||
flex-shrink: 0;
|
||||
color: #888;
|
||||
}
|
||||
.create-meta-value {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #ddd;
|
||||
font-weight: 600;
|
||||
}
|
||||
.create-meta-row .c-green {
|
||||
font-size: 14px;
|
||||
}
|
||||
.create-form-pair {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0 10px;
|
||||
}
|
||||
.c-green { color: #2fb56a; }
|
||||
.amount-compact { white-space: nowrap; font-variant-numeric: tabular-nums; cursor: default; }
|
||||
.affiliation-tag {
|
||||
max-width: 100%;
|
||||
@@ -1963,6 +2441,11 @@ function creditTypeLabel(type: string) {
|
||||
margin: 0 0 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.list-settings-unit {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
.reset-db-alert { margin-bottom: 10px; }
|
||||
.detail-block { margin-bottom: 16px; }
|
||||
.section-title {
|
||||
|
||||
Reference in New Issue
Block a user