@@ -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 agentSuspend Settings = ref ( {
suspendFreezeDirectPlayers : false ,
suspendBlockPlayerLogin : false ,
const hierarchy Settings = 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 agentSuspend Saving = ref ( false ) ;
const hierarchy Saving = 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 hi t = agentOptions . value . find ( ( a ) => a . id === agentId ) ;
if ( hi t) return agentOptionLabel ( hi t) ;
const op t = agentOptions . value . find ( ( a ) => a . id === agentId ) ;
if ( op t) return agentOptionLabel ( op t) ;
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 ( ) ;
loadAgentSuspend Settings ( ) ;
loadHierarchy Settings ( ) ;
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 . valu e,
pageSize : tier2PageSize . valu e,
keyword : tier2Keyword . value . trim ( ) || undefined ,
status : tier2F ilterStatus. value || undefined ,
level : 2 ,
page : s t. pag e,
pageSize : st . pageSiz e,
keyword : st . keyword . trim ( ) || undefined ,
status : st . f ilterStatus || 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 onTier2 PageChange ( p : number ) {
tier2Page . valu e = p ;
loadTier2 Agents( ) ;
function onSubAgent PageChange ( level : number , p : number ) {
ensureSubAgentState ( level ) . pag e = p ;
loadSub AgentsAtLevel ( level ) ;
}
function onTier2 SizeChange ( size : number ) {
tier2PageSiz e. value = size ;
tier2Page . valu e = 1 ;
loadTier2Agents ( ) ;
function onSubAgent SizeChange ( level : number , size : number ) {
const st = ensureSubAgentStat e( level ) ;
st . pageSiz e = size ;
st . page = 1 ;
loadSubAgentsAtLevel ( level ) ;
}
function searchTier2 Agents( ) {
tier2Page . valu e = 1 ;
loadTier2 Agents( ) ;
function searchSub AgentsAtLevel ( level : number ) {
ensureSubAgentState ( level ) . pag e = 1 ;
loadSub AgentsAtLevel ( level ) ;
}
function reloadSubAgentTabs ( ) {
return loadAgentLevelCounts ( ) . then ( ( ) => {
for ( const lvl of visibleSubAgentTabLevels . value ) {
loadSubAgentsAtLevel ( lvl ) ;
}
} ) ;
}
function reloadAgentLists ( ) {
loadTier1Agents ( ) ;
loadTier2 Agents( ) ;
void reloadSub AgentTab s( ) ;
}
/* ─── Load main agent list ─── */
@@ -313,10 +536,24 @@ function onTier1AgentRowClick(row: AgentRow, _column: unknown, event: MouseEvent
onAgentRowClick ( row , tier1AgentTableRef , _column , event ) ;
}
function onTier2 AgentRowClick ( row : AgentRow , _column : unknown , event : MouseEvent ) {
onAgentRowClick ( row , tier2 AgentTableRef, _column , event ) ;
function onSub AgentRowClick ( level : number , row : AgentRow , _column : unknown , event : MouseEvent ) {
const tableRef = { value : sub AgentTableRefs . 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 loadAgentSuspend Settings ( ) {
async function loadHierarchy Settings ( ) {
try {
const { data } = await api . get ( '/admin/agents/settings/suspend ' ) ;
agentSuspend Settings. value = data . data ;
const { data } = await api . get ( '/admin/agents/settings/hierarchy ' ) ;
hierarchy Settings. value = data . data ;
} catch {
/* defaults */
hierarchySettings . value = { maxAgentLevel : 0 , defaultSubAgentCreditRatio : 50 } ;
}
}
async function saveAgentSuspend Settings ( ) {
agentSuspend Saving. value = true ;
async function saveHierarchy Settings ( ) {
hierarchy Saving. value = true ;
try {
const { data } = await api . put ( '/admin/agents/settings/suspend' , agentSuspend Settings . value ) ;
agentSuspend Settings. value = data . data ;
const { data } = await api . put ( '/admin/agents/settings/hierarchy' , hierarchy Settings . value ) ;
hierarchy Settings. 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' ) ) ;
loadAgentSuspend Settings ( ) ;
loadHierarchy Settings ( ) ;
} finally {
agentSuspend Saving. value = false ;
hierarchy Saving. 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 = f als e;
async function submitFreezeAgent ( ) {
const row = freezeAgentTarget . v alu e ;
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 "
:load ing = "agentSuspendSaving "
@change ="saveAgentSuspendSettings"
< el-form-item :label = "t('agent.hierarchy.max_level ')" >
< el-input-number
v-model = "hierarchySettings.maxAgentLevel "
:m in = "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 "
:load ing = "agentSuspendSaving "
@change ="saveAgentSuspendSettings"
< el-form-item :label = "t('agent.hierarchy.default_sub_credit_ratio ')" >
< el-input-number
v-model = "hierarchySettings.defaultSubAgentCreditRatio "
:m in = "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 = "3 40" fixed = "right" align = "center" >
< el-table-column :label = "t('common.actions')" width = "42 0" 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 = "tier2A gents"
: ref = "(el) => { subAgentTableRefs[agentLevel] = el as { toggleRowExpansion: (row: AgentRow) => void } | null } "
:data = "ensureSubAgentState(agentLevel).a gents"
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 = "tier2P ageSize"
:total= "tier2Total"
:current-page=" ensureSubAgentState ( agentLevel ) . page "
:page-size=" ensureSubAgentState ( agentLevel ) . p ageSize"
: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=" 480 px "
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=" 460 px "
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=" 100 px " 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_l imit' ) } } < / div >
< el-input-number
v-model=" createForm . creditL imit"
: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 16 px ;
line - height : 1.5 ;
color : var ( -- el - text - color - regular ) ;
}
. freeze - agent - options {
display : flex ;
flex - direction : column ;
gap : 10 px ;
}
/* ─── Table toolbar ─── */
. list - panel - toolbar {
flex - shrink : 0 ;
@@ -1827,7 +2226,6 @@ function creditTypeLabel(type: string) {
padding : 12 px 14 px 14 px ;
background : # 141414 ;
border : 1 px solid # 2 a2a2a ;
border - left : 3 px solid rgba ( 47 , 181 , 106 , 0.45 ) ;
border - radius : 8 px ;
}
. 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 : 6 px 8 px ;
}
. compact - agent - table : deep ( . el - table _ _header . cell ) {
line - height : 1.25 ;
white - space : nowrap ;
font - size : 12 px ;
padding : 0 ;
}
. compact - agent - table : deep ( . el - table _ _body . cell ) {
padding : 0 ;
line - height : 1.35 ;
font - size : 13 px ;
}
. compact - agent - table : deep ( . el - tag ) {
max - width : 100 % ;
}
. nested - table {
margin - bottom : 4 px ;
}
@@ -1891,9 +2307,6 @@ function creditTypeLabel(type: string) {
font - size : 13 px ;
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 : 12 px ;
}
. field - hint { font - size : 12 px ; color : # 888 ; margin - top : 4 px ; }
: deep ( . create - account - dialog . el - dialog _ _body ) {
max - height : none ! important ;
overflow : visible ! important ;
padding : 14 px 20 px 6 px ;
}
: deep ( . create - account - dialog . el - dialog _ _footer ) {
padding - top : 4 px ;
}
. compact - create - form : deep ( . el - form - item ) {
margin - bottom : 10 px ;
}
. compact - create - form : deep ( . el - form - item _ _label ) {
font - size : 13 px ;
white - space : nowrap ;
}
. credit - quick - row {
margin - top : 2 px ;
}
. credit - quick - btns {
display : flex ;
flex - wrap : wrap ;
gap : 6 px ;
margin - top : 6 px ;
}
. create - meta - bar {
margin - bottom : 12 px ;
padding : 10 px 12 px ;
border : 1 px solid # 2 a2a2a ;
border - radius : 8 px ;
background : rgba ( 255 , 255 , 255 , 0.02 ) ;
}
. create - meta - bar -- inline {
margin - top : - 4 px ;
}
. create - meta - row {
display : flex ;
align - items : center ;
gap : 8 px ;
font - size : 12 px ;
line - height : 1.4 ;
}
. create - meta - row + . create - meta - row {
margin - top : 6 px ;
}
. 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 : 14 px ;
}
. create - form - pair {
display : grid ;
grid - template - columns : 1 fr 1 fr ;
gap : 0 10 px ;
}
. c - green { color : # 2 fb56a ; }
. 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 10 px ;
line - height : 1.5 ;
}
. list - settings - unit {
margin - left : 4 px ;
font - size : 12 px ;
color : # 888 ;
}
. reset - db - alert { margin - bottom : 10 px ; }
. detail - block { margin - bottom : 16 px ; }
. section - title {