feat: refactor agent manager, media library, and player UX
- Split admin users page into player/tier-1/tier-2 tabs with affiliation labels and context-specific create dialogs - Add media library with uploaded_files migration, list/delete unused files API, and admin nav route - Enforce player username format (alphanumeric 3-32) on frontend and backend via shared package - Improve admin dialog/panel styling; refine player parlay and match bet card kickoff display Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,8 @@ import {
|
||||
type PlayerDetail,
|
||||
type PlayerCreateForm,
|
||||
type PlayerEditForm,
|
||||
formatPlayerAffiliationLabel,
|
||||
assertPlayerUsername,
|
||||
} from './user-form';
|
||||
import {
|
||||
emptyAgentEditForm,
|
||||
@@ -28,6 +30,8 @@ import {
|
||||
type AgentEditForm,
|
||||
} from './agent-form';
|
||||
import { subAgentAccountStatus } from './agent/agent-sub-agent-form';
|
||||
|
||||
type DisplayAgentRow = AgentRow;
|
||||
import {
|
||||
formatAmount,
|
||||
formatAmountFull,
|
||||
@@ -47,26 +51,42 @@ import {
|
||||
type AgentCreditAdjustContext,
|
||||
} from '../utils/agent-credit-context';
|
||||
|
||||
/* ─── Main agent list ─── */
|
||||
const agents = ref<AgentRow[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const keyword = ref('');
|
||||
const filterStatus = ref('');
|
||||
/* ─── Tier-1 agent list ─── */
|
||||
const tier1Agents = ref<AgentRow[]>([]);
|
||||
const tier1Total = ref(0);
|
||||
const tier1Page = ref(1);
|
||||
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('');
|
||||
|
||||
/* ─── View tab: players | tier1Agents | tier2Agents ─── */
|
||||
const activeViewTab = ref('players');
|
||||
|
||||
/* ─── All players list ─── */
|
||||
const allPlayers = ref<PlayerRow[]>([]);
|
||||
const playerTotal = ref(0);
|
||||
const playerPage = ref(1);
|
||||
const playerPageSize = ref(20);
|
||||
const playerKeyword = ref('');
|
||||
const playerFilterStatus = ref('');
|
||||
const playerFilterAgent = ref('');
|
||||
const playerLoading = ref(false);
|
||||
const agentOptions = ref<{ id: string; username: string; level: number; parentUsername?: string | null }[]>([]);
|
||||
|
||||
/* ─── Expansion state ─── */
|
||||
const expandedSet = ref(new Set<string>());
|
||||
const agentPlayersMap = ref<Record<string, PlayerRow[]>>({});
|
||||
const agentSubAgentsMap = ref<Record<string, AgentRow[]>>({});
|
||||
const expandLoading = ref<Record<string, boolean>>({});
|
||||
const innerTabMap = ref<Record<string, string>>({});
|
||||
const agentTableRef = ref();
|
||||
|
||||
/* Sub-agent nested expansion (players under tier-2 agents) */
|
||||
const subAgentExpandedKeys = ref<string[]>([]);
|
||||
const subAgentPlayersMap = ref<Record<string, PlayerRow[]>>({});
|
||||
const subAgentExpandLoading = ref<Record<string, boolean>>({});
|
||||
const tier1AgentTableRef = ref();
|
||||
const tier2AgentTableRef = ref();
|
||||
|
||||
/* ─── Dialogs ─── */
|
||||
const createVisible = ref(false);
|
||||
@@ -82,7 +102,8 @@ const creditLoading = ref(false);
|
||||
|
||||
/* ─── Create form (unified) ─── */
|
||||
const createForm = ref<PlayerCreateForm>(emptyPlayerCreateForm());
|
||||
const createParentAgentId = ref(''); // set when creating from expansion
|
||||
const createParentAgentId = ref('');
|
||||
const createParentLocked = ref(false);
|
||||
const createAccountMode = ref(0); // 0=player, 1=tier1Agent, 2=subAgent
|
||||
|
||||
/* ─── Edit forms ─── */
|
||||
@@ -121,74 +142,201 @@ const resetLoading = ref(false);
|
||||
const resetConfirmPhrase = ref('');
|
||||
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');
|
||||
return t('user.dialog.create');
|
||||
});
|
||||
|
||||
const tier1AgentOptions = computed(() => agentOptions.value.filter((a) => a.level === 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})`;
|
||||
}
|
||||
|
||||
function resolveCreateParentLabel(agentId: string) {
|
||||
const hit = agentOptions.value.find((a) => a.id === agentId);
|
||||
if (hit) return agentOptionLabel(hit);
|
||||
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;
|
||||
}
|
||||
return agentId;
|
||||
}
|
||||
|
||||
/* ─── Init ─── */
|
||||
onMounted(() => {
|
||||
loadPlayerSettings();
|
||||
loadBettingLimits();
|
||||
loadAgentSuspendSettings();
|
||||
loadResetDatabaseStatus();
|
||||
load();
|
||||
loadAgentOptions();
|
||||
loadAllPlayers();
|
||||
loadTier1Agents();
|
||||
loadTier2Agents();
|
||||
});
|
||||
|
||||
/* ─── Load tier-1 agents ─── */
|
||||
async function loadTier1Agents() {
|
||||
const { data } = await api.get('/admin/agents', {
|
||||
params: {
|
||||
page: tier1Page.value,
|
||||
pageSize: tier1PageSize.value,
|
||||
keyword: tier1Keyword.value.trim() || undefined,
|
||||
status: tier1FilterStatus.value || undefined,
|
||||
level: 1,
|
||||
},
|
||||
});
|
||||
tier1Agents.value = data.data.items as AgentRow[];
|
||||
tier1Total.value = data.data.total;
|
||||
}
|
||||
|
||||
function onTier1PageChange(p: number) {
|
||||
tier1Page.value = p;
|
||||
loadTier1Agents();
|
||||
}
|
||||
|
||||
function onTier1SizeChange(size: number) {
|
||||
tier1PageSize.value = size;
|
||||
tier1Page.value = 1;
|
||||
loadTier1Agents();
|
||||
}
|
||||
|
||||
function searchTier1Agents() {
|
||||
tier1Page.value = 1;
|
||||
loadTier1Agents();
|
||||
}
|
||||
|
||||
/* ─── Load tier-2 agents ─── */
|
||||
async function loadTier2Agents() {
|
||||
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,
|
||||
},
|
||||
});
|
||||
tier2Agents.value = data.data.items as AgentRow[];
|
||||
tier2Total.value = data.data.total;
|
||||
}
|
||||
|
||||
function onTier2PageChange(p: number) {
|
||||
tier2Page.value = p;
|
||||
loadTier2Agents();
|
||||
}
|
||||
|
||||
function onTier2SizeChange(size: number) {
|
||||
tier2PageSize.value = size;
|
||||
tier2Page.value = 1;
|
||||
loadTier2Agents();
|
||||
}
|
||||
|
||||
function searchTier2Agents() {
|
||||
tier2Page.value = 1;
|
||||
loadTier2Agents();
|
||||
}
|
||||
|
||||
function reloadAgentLists() {
|
||||
loadTier1Agents();
|
||||
loadTier2Agents();
|
||||
}
|
||||
|
||||
/* ─── Load main agent list ─── */
|
||||
async function load() {
|
||||
const { data } = await api.get('/admin/agents', {
|
||||
params: {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: keyword.value.trim() || undefined,
|
||||
},
|
||||
});
|
||||
agents.value = data.data.items as AgentRow[];
|
||||
total.value = data.data.total;
|
||||
reloadAgentLists();
|
||||
}
|
||||
|
||||
function onPageChange(p: number) {
|
||||
page.value = p;
|
||||
load();
|
||||
async function loadAgentOptions() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/agents/options');
|
||||
agentOptions.value = data.data;
|
||||
} catch {
|
||||
agentOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function onSizeChange(size: number) {
|
||||
pageSize.value = size;
|
||||
page.value = 1;
|
||||
load();
|
||||
async function loadAllPlayers() {
|
||||
playerLoading.value = true;
|
||||
try {
|
||||
const params: Record<string, unknown> = {
|
||||
page: playerPage.value,
|
||||
pageSize: playerPageSize.value,
|
||||
keyword: playerKeyword.value.trim() || undefined,
|
||||
status: playerFilterStatus.value || undefined,
|
||||
};
|
||||
if (playerFilterAgent.value) {
|
||||
params.parentId = playerFilterAgent.value;
|
||||
}
|
||||
const { data } = await api.get('/admin/users', { params });
|
||||
allPlayers.value = data.data.items as PlayerRow[];
|
||||
playerTotal.value = data.data.total;
|
||||
} catch {
|
||||
allPlayers.value = [];
|
||||
playerTotal.value = 0;
|
||||
} finally {
|
||||
playerLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function searchPlayers() {
|
||||
playerPage.value = 1;
|
||||
loadAllPlayers();
|
||||
}
|
||||
|
||||
function onPlayerPageChange(p: number) {
|
||||
playerPage.value = p;
|
||||
loadAllPlayers();
|
||||
}
|
||||
|
||||
function onPlayerSizeChange(size: number) {
|
||||
playerPageSize.value = size;
|
||||
playerPage.value = 1;
|
||||
loadAllPlayers();
|
||||
}
|
||||
|
||||
function affiliationLabel(row: PlayerRow) {
|
||||
return formatPlayerAffiliationLabel(row, t('user.type.player'), t('agent.platform_row_name'));
|
||||
}
|
||||
|
||||
function directPlayersTabLabel(ownerName: string, count: number) {
|
||||
return `${t('agent.direct_players_title', { name: ownerName })} (${count})`;
|
||||
}
|
||||
|
||||
function 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);
|
||||
}
|
||||
|
||||
/* ─── Expansion ─── */
|
||||
function getInnerTab(agentId: string) {
|
||||
return innerTabMap.value[agentId] || 'players';
|
||||
}
|
||||
|
||||
function setInnerTab(agentId: string, tab: string) {
|
||||
innerTabMap.value[agentId] = tab;
|
||||
}
|
||||
|
||||
async function onExpandChange(row: AgentRow, expandedRows: AgentRow[]) {
|
||||
// Track expanded rows
|
||||
expandedSet.value = new Set(expandedRows.map(r => r.userId));
|
||||
// Load data if newly expanded
|
||||
async function onExpandChange(row: DisplayAgentRow, expandedRows: DisplayAgentRow[]) {
|
||||
expandedSet.value = new Set(expandedRows.map((r) => r.userId));
|
||||
if (expandedSet.value.has(row.userId) && !agentPlayersMap.value[row.userId]) {
|
||||
await loadExpansionData(row.userId);
|
||||
}
|
||||
}
|
||||
|
||||
function onAgentRowClick(row: AgentRow, _column: unknown, event: MouseEvent) {
|
||||
function onAgentRowClick(row: AgentRow, tableRef: typeof tier1AgentTableRef, _column: unknown, event: MouseEvent) {
|
||||
if (!shouldToggleExpandOnRowClick(event)) return;
|
||||
agentTableRef.value?.toggleRowExpansion(row);
|
||||
tableRef.value?.toggleRowExpansion(row);
|
||||
}
|
||||
|
||||
async function loadExpansionData(agentId: string) {
|
||||
expandLoading.value[agentId] = true;
|
||||
try {
|
||||
const [playersRes, subAgentsRes] = await Promise.all([
|
||||
api.get('/admin/users', { params: { parentId: agentId, pageSize: 100 } }),
|
||||
api.get('/admin/agents', { params: { parentAgentId: agentId, pageSize: 100 } }),
|
||||
]);
|
||||
agentPlayersMap.value[agentId] = playersRes.data.data.items;
|
||||
agentSubAgentsMap.value[agentId] = subAgentsRes.data.data.items;
|
||||
const { data } = await api.get('/admin/users', { params: { parentId: agentId, pageSize: 100 } });
|
||||
agentPlayersMap.value[agentId] = data.data.items as PlayerRow[];
|
||||
} catch {
|
||||
agentPlayersMap.value[agentId] = [];
|
||||
agentSubAgentsMap.value[agentId] = [];
|
||||
} finally {
|
||||
expandLoading.value[agentId] = false;
|
||||
}
|
||||
@@ -198,51 +346,9 @@ function getPlayers(agentId: string) {
|
||||
return agentPlayersMap.value[agentId] || [];
|
||||
}
|
||||
|
||||
function getSubAgents(agentId: string) {
|
||||
return agentSubAgentsMap.value[agentId] || [];
|
||||
}
|
||||
|
||||
async function loadSubAgentPlayers(subAgentUserId: string) {
|
||||
subAgentExpandLoading.value[subAgentUserId] = true;
|
||||
try {
|
||||
const { data } = await api.get('/admin/users', {
|
||||
params: { parentId: subAgentUserId, pageSize: 100 },
|
||||
});
|
||||
subAgentPlayersMap.value[subAgentUserId] = data.data.items as PlayerRow[];
|
||||
} catch {
|
||||
subAgentPlayersMap.value[subAgentUserId] = [];
|
||||
} finally {
|
||||
subAgentExpandLoading.value[subAgentUserId] = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getSubAgentPlayers(subAgentUserId: string) {
|
||||
return subAgentPlayersMap.value[subAgentUserId] || [];
|
||||
}
|
||||
|
||||
function onSubAgentExpand(_parentAgentId: string, row: AgentRow, expandedRows: AgentRow[]) {
|
||||
subAgentExpandedKeys.value = expandedRows.map((r) => r.userId);
|
||||
if (!subAgentPlayersMap.value[row.userId]) {
|
||||
loadSubAgentPlayers(row.userId);
|
||||
}
|
||||
}
|
||||
|
||||
function onSubAgentRowClick(_parentAgentId: string, row: AgentRow, _column: unknown, event: MouseEvent) {
|
||||
if (!shouldToggleExpandOnRowClick(event)) return;
|
||||
const id = row.userId;
|
||||
if (subAgentExpandedKeys.value.includes(id)) {
|
||||
subAgentExpandedKeys.value = subAgentExpandedKeys.value.filter((k) => k !== id);
|
||||
} else {
|
||||
subAgentExpandedKeys.value = [...subAgentExpandedKeys.value, id];
|
||||
if (!subAgentPlayersMap.value[id]) {
|
||||
loadSubAgentPlayers(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refreshExpandedSubAgentPlayers() {
|
||||
for (const subId of subAgentExpandedKeys.value) {
|
||||
loadSubAgentPlayers(subId);
|
||||
function refreshExpandedAgentPlayers() {
|
||||
for (const agentId of expandedSet.value) {
|
||||
loadExpansionData(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,18 +467,30 @@ async function saveAgentSuspendSettings() {
|
||||
}
|
||||
|
||||
/* ─── Create (unified) ─── */
|
||||
function openCreateGlobal() {
|
||||
function openCreateAccount() {
|
||||
createForm.value = emptyPlayerCreateForm();
|
||||
createForm.value.asTier1Agent = false;
|
||||
createParentAgentId.value = '';
|
||||
createParentLocked.value = false;
|
||||
createAccountMode.value = 0;
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
function openCreateTier1Agent() {
|
||||
createForm.value = emptyPlayerCreateForm();
|
||||
createForm.value.asTier1Agent = true;
|
||||
createParentAgentId.value = '';
|
||||
createParentLocked.value = false;
|
||||
createAccountMode.value = 1;
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
function openCreatePlayer(parentAgentUserId: string) {
|
||||
createForm.value = emptyPlayerCreateForm();
|
||||
createForm.value.asTier1Agent = false;
|
||||
createForm.value.parentId = parentAgentUserId;
|
||||
createParentAgentId.value = parentAgentUserId;
|
||||
createParentLocked.value = true;
|
||||
createAccountMode.value = 0;
|
||||
createVisible.value = true;
|
||||
}
|
||||
@@ -381,27 +499,18 @@ function openCreateSubAgent(parentAgentUserId: string) {
|
||||
createForm.value = emptyPlayerCreateForm();
|
||||
createForm.value.asTier1Agent = false;
|
||||
createParentAgentId.value = parentAgentUserId;
|
||||
createParentLocked.value = true;
|
||||
createAccountMode.value = 2;
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
function onAccountModeChange(mode: number) {
|
||||
createAccountMode.value = mode;
|
||||
if (mode === 0) {
|
||||
// Player
|
||||
createForm.value.asTier1Agent = false;
|
||||
if (createParentAgentId.value) {
|
||||
createForm.value.parentId = createParentAgentId.value;
|
||||
}
|
||||
} else if (mode === 1) {
|
||||
// Tier-1 agent
|
||||
createForm.value.asTier1Agent = true;
|
||||
createForm.value.parentId = '';
|
||||
} else if (mode === 2) {
|
||||
// Sub-agent
|
||||
createForm.value.asTier1Agent = false;
|
||||
createForm.value.parentId = '';
|
||||
}
|
||||
function openCreateSubAgentFromToolbar() {
|
||||
createForm.value = emptyPlayerCreateForm();
|
||||
createForm.value.asTier1Agent = false;
|
||||
createParentAgentId.value = '';
|
||||
createParentLocked.value = false;
|
||||
createAccountMode.value = 2;
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitCreate() {
|
||||
@@ -410,7 +519,7 @@ async function submitCreate() {
|
||||
let payload: Record<string, unknown>;
|
||||
try {
|
||||
if (isSubAgent) {
|
||||
// Sub-agent creation
|
||||
if (!createParentAgentId.value) throw new Error(t('agent.hint.sub_agent_parent'));
|
||||
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'));
|
||||
@@ -450,7 +559,7 @@ async function submitCreate() {
|
||||
refreshExpandedParents();
|
||||
const parentId = createParentAgentId.value || createForm.value.parentId;
|
||||
if (parentId) {
|
||||
await loadSubAgentPlayers(parentId);
|
||||
await loadExpansionData(parentId);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
@@ -474,12 +583,17 @@ async function submitEditPlayer() {
|
||||
ElMessage.warning(t('err.password_min'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
assertPlayerUsername(editPlayerForm.value.username);
|
||||
} catch (e) {
|
||||
ElMessage.warning(resolveFormError(e, t));
|
||||
return;
|
||||
}
|
||||
editPlayerLoading.value = true;
|
||||
try {
|
||||
const newPwd = editPlayerForm.value.newPassword.trim();
|
||||
const { data } = await api.put(`/admin/users/${editingId.value}`, {
|
||||
username: editPlayerForm.value.username.trim(),
|
||||
parentId: editPlayerForm.value.parentId || '',
|
||||
phone: editPlayerForm.value.phone.trim() || undefined,
|
||||
email: editPlayerForm.value.email.trim() || undefined,
|
||||
password: newPwd || undefined,
|
||||
@@ -724,10 +838,9 @@ async function toggleFreezeAgent(row: AgentRow) {
|
||||
|
||||
/* ─── Helpers ─── */
|
||||
function refreshExpandedParents() {
|
||||
for (const agentId of expandedSet.value) {
|
||||
loadExpansionData(agentId);
|
||||
}
|
||||
refreshExpandedSubAgentPlayers();
|
||||
loadAllPlayers();
|
||||
reloadAgentLists();
|
||||
refreshExpandedAgentPlayers();
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -878,41 +991,156 @@ function creditTypeLabel(type: string) {
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
<!-- ─── Filter bar ─── -->
|
||||
<div class="list-chrome">
|
||||
<div class="list-chrome__row">
|
||||
<el-tabs v-model="activeViewTab" class="mgr-top-tabs">
|
||||
<!-- ─── Tab: 全部玩家(默认) ─── -->
|
||||
<el-tab-pane :label="`${t('user.type.player')} (${playerTotal})`" name="players">
|
||||
<section class="list-panel player-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="playerKeyword"
|
||||
:placeholder="t('user.filter.username_ph')"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter="searchPlayers"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.filter.agent')">
|
||||
<el-select
|
||||
v-model="playerFilterAgent"
|
||||
:placeholder="t('user.filter.agent_ph')"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
>
|
||||
<el-option
|
||||
v-for="a in agentOptions"
|
||||
:key="a.id"
|
||||
:label="agentOptionLabel(a)"
|
||||
:value="a.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('common.status')">
|
||||
<el-select v-model="playerFilterStatus" :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="searchPlayers">{{ t('common.search') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="list-chrome__actions">
|
||||
<el-button type="primary" @click="openCreateAccount">{{ t('user.create_btn') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<el-table v-loading="playerLoading" :data="allPlayers" stripe>
|
||||
<template #empty>
|
||||
<AdminTableEmpty />
|
||||
</template>
|
||||
<el-table-column prop="id" label="ID" width="72" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="120" />
|
||||
<el-table-column :label="t('common.status')" width="88">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.col.agent')" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" type="info" class="affiliation-tag">{{ affiliationLabel(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.col.balance')" min-width="128" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="`${formatAmountFull(row.availableBalance)} / ${formatAmountFull(row.frozenBalance)}`" placement="top">
|
||||
<span class="amount-compact">{{ formatAmount(row.availableBalance) }} / {{ formatAmount(row.frozenBalance) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="betCount" :label="t('user.col.bets')" width="64" align="center" />
|
||||
<el-table-column :label="t('user.col.stake_payout')" min-width="108" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="amount-compact">{{ formatAmount(row.totalStake) }} / {{ formatAmount(row.totalReturn) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.col.last_login')" width="108">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.lastLoginAt" :content="formatTime(row.lastLoginAt)" placement="top">
|
||||
<span>{{ formatLastLogin(row.lastLoginAt) }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="text-muted">{{ t('common.never_login') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.col.created')" width="108">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="formatTime(row.createdAt)" placement="top">
|
||||
<span>{{ formatLastLogin(row.createdAt) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.actions')" width="340" 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="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>
|
||||
<el-button v-if="row.status === 'ACTIVE'" size="small" link type="warning" @click="toggleFreezePlayer(row)">{{ t('common.freeze') }}</el-button>
|
||||
<el-button v-else size="small" link type="primary" @click="toggleFreezePlayer(row)">{{ t('common.unfreeze') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="playerPage"
|
||||
v-model:page-size="playerPageSize"
|
||||
:total="playerTotal"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@current-change="onPlayerPageChange"
|
||||
@size-change="onPlayerSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ─── Tab: 一级代理 ─── -->
|
||||
<el-tab-pane :label="`${t('user.type.tier1_agent')} (${tier1Total})`" name="tier1Agents">
|
||||
<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="keyword" :placeholder="t('agent.filter.username_ph')" clearable style="width: 180px" @keyup.enter="load" />
|
||||
<el-input v-model="tier1Keyword" :placeholder="t('agent.filter.username_ph')" clearable style="width: 180px" @keyup.enter="searchTier1Agents" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('common.status')">
|
||||
<el-select v-model="filterStatus" :placeholder="t('common.all')" clearable style="width: 120px">
|
||||
<el-select v-model="tier1FilterStatus" :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="load">{{ t('common.search') }}</el-button>
|
||||
<el-button type="primary" @click="searchTier1Agents">{{ t('common.search') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="list-chrome__actions">
|
||||
<el-button type="primary" @click="openCreateGlobal">{{ t('agent.create_btn') }}</el-button>
|
||||
<el-button type="primary" @click="openCreateTier1Agent">{{ t('agent.create_btn') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── Agent table ─── -->
|
||||
<section class="list-panel">
|
||||
<div class="table-wrap">
|
||||
<el-table
|
||||
ref="agentTableRef"
|
||||
:data="agents"
|
||||
ref="tier1AgentTableRef"
|
||||
:data="tier1Agents"
|
||||
stripe
|
||||
row-key="userId"
|
||||
:row-class-name="expandableTableRowClassName"
|
||||
class="expandable-table"
|
||||
@expand-change="onExpandChange"
|
||||
@row-click="onAgentRowClick"
|
||||
@row-click="onTier1AgentRowClick"
|
||||
>
|
||||
<template #empty>
|
||||
<AdminTableEmpty />
|
||||
@@ -925,16 +1153,12 @@ function creditTypeLabel(type: string) {
|
||||
<div v-if="expandLoading[row.userId]" class="expand-loading">
|
||||
{{ t('common.loading') || '加载中...' }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<el-tabs :model-value="getInnerTab(row.userId)" @update:model-value="setInnerTab(row.userId, $event)" class="inner-tabs">
|
||||
<!-- ── Players tab ── -->
|
||||
<el-tab-pane :label="`${t('nav.players')} (${getPlayers(row.userId).length})`" name="players">
|
||||
<div class="inner-toolbar">
|
||||
<el-button type="primary" size="small" @click="openCreatePlayer(row.userId)">
|
||||
+ {{ t('user.create_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="getPlayers(row.userId)" stripe size="small" class="inner-table">
|
||||
<div v-else class="expand-panel-body">
|
||||
<div class="expand-section-header">
|
||||
<div class="expand-section-title">{{ directPlayersTabLabel(row.username, getPlayers(row.userId).length) }}</div>
|
||||
<el-button type="primary" size="small" @click="openCreatePlayer(row.userId)">{{ t('user.create_btn') }}</el-button>
|
||||
</div>
|
||||
<el-table :data="getPlayers(row.userId)" stripe class="inner-table">
|
||||
<template #empty><AdminTableEmpty /></template>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" />
|
||||
@@ -977,122 +1201,13 @@ function creditTypeLabel(type: string) {
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ── Sub-agents tab ── -->
|
||||
<el-tab-pane :label="`${t('nav.subAgents')} (${getSubAgents(row.userId).length})`" name="subAgents">
|
||||
<div class="inner-toolbar">
|
||||
<el-button type="primary" size="small" @click="openCreateSubAgent(row.userId)">
|
||||
+ {{ t('agent.create_sub') || '创建二级代理' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
:data="getSubAgents(row.userId)"
|
||||
stripe
|
||||
size="small"
|
||||
row-key="userId"
|
||||
:expand-row-keys="subAgentExpandedKeys"
|
||||
:row-class-name="expandableTableRowClassName"
|
||||
class="inner-table expandable-table"
|
||||
@expand-change="(sub: AgentRow, rows: AgentRow[]) => onSubAgentExpand(row.userId, sub, rows)"
|
||||
@row-click="(sub: AgentRow, col: unknown, e: MouseEvent) => onSubAgentRowClick(row.userId, sub, col, e)"
|
||||
>
|
||||
<template #empty><AdminTableEmpty /></template>
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row: sub }">
|
||||
<div class="expand-panel">
|
||||
<div v-if="subAgentExpandLoading[sub.userId]" class="expand-loading">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="expand-section-header">
|
||||
<div class="expand-section-title">
|
||||
{{ t('nav.players') }} ({{ getSubAgentPlayers(sub.userId).length }})
|
||||
</div>
|
||||
<el-button type="primary" size="small" @click.stop="openCreatePlayer(sub.userId)">
|
||||
+ {{ t('agent_portal.create_player_btn').replace(/^\+ /, '') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="getSubAgentPlayers(sub.userId)" stripe size="small" class="inner-table nested-table">
|
||||
<template #empty><AdminTableEmpty /></template>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" />
|
||||
<el-table-column :label="t('common.status')" width="80">
|
||||
<template #default="{ row: player }">
|
||||
<el-tag :type="statusTagType(player.status)" size="small">
|
||||
{{ statusLabel(player.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.field.available')" min-width="100" align="right">
|
||||
<template #default="{ row: player }">
|
||||
<span class="amount-compact">{{ formatAmount(player.availableBalance) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.actions')" width="340" fixed="right" align="center">
|
||||
<template #default="{ row: player }">
|
||||
<div class="action-btns" @click.stop>
|
||||
<el-button size="small" link type="primary" @click="openDetailPlayer(player.id)">{{ t('common.detail') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openEditPlayer(player.id)">{{ t('common.edit') }}</el-button>
|
||||
<el-button size="small" link type="success" @click="openTransfer('deposit', player)">{{ t('common.topup') }}</el-button>
|
||||
<el-button size="small" link type="warning" @click="openTransfer('withdraw', player)">{{ t('agent_portal.withdraw_btn_label') }}</el-button>
|
||||
<el-button v-if="player.status === 'ACTIVE'" size="small" link type="warning" @click="toggleFreezePlayer(player)">{{ t('common.freeze') }}</el-button>
|
||||
<el-button v-else size="small" link type="primary" @click="toggleFreezePlayer(player)">{{ t('common.unfreeze') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="userId" label="ID" width="60" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" />
|
||||
<el-table-column :label="t('common.status')" width="80">
|
||||
<template #default="{ row: sub }">
|
||||
<el-tag :type="statusTagType(subAgentAccountStatus(sub))" size="small">
|
||||
{{ statusLabel(subAgentAccountStatus(sub)) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('agent.col.credit')" min-width="150" align="right">
|
||||
<template #default="{ row: sub }">
|
||||
<el-tooltip :content="creditLineFull(sub)" placement="top">
|
||||
<span class="amount-compact">{{ creditLine(sub) }}</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('agent.col.cashback')" width="70" align="right">
|
||||
<template #default="{ row: sub }">{{ sub.cashbackRate }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.actions')" width="300" fixed="right" align="center">
|
||||
<template #default="{ row: sub }">
|
||||
<div class="action-btns">
|
||||
<el-button size="small" link type="primary" @click="openDetailAgent(sub.userId)">{{ t('common.detail') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openEditAgent(sub.userId)">{{ t('common.edit') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openCredit(sub.userId)">{{ t('common.adjust_credit') }}</el-button>
|
||||
<el-button
|
||||
v-if="subAgentAccountStatus(sub) === 'ACTIVE'"
|
||||
size="small"
|
||||
link
|
||||
type="warning"
|
||||
@click="toggleFreezeAgent(sub)"
|
||||
>{{ t('common.freeze') }}</el-button>
|
||||
<el-button v-else size="small" link type="primary" @click="toggleFreezeAgent(sub)">{{ t('common.unfreeze') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</div>
|
||||
</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 prop="username" :label="t('user.col.username')" min-width="140" />
|
||||
<el-table-column :label="t('common.status')" width="88">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
@@ -1113,12 +1228,13 @@ function creditTypeLabel(type: string) {
|
||||
<el-table-column :label="t('agent.col.cashback')" width="80" align="right">
|
||||
<template #default="{ row }">{{ row.cashbackRate }}</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="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 size="small" link type="primary" @click="openCreateSubAgent(row.userId)">{{ t('agent.create_sub') }}</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>
|
||||
@@ -1128,25 +1244,151 @@ function creditTypeLabel(type: string) {
|
||||
</div>
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
v-model:current-page="tier1Page"
|
||||
v-model:page-size="tier1PageSize"
|
||||
:total="tier1Total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@current-change="onPageChange"
|
||||
@size-change="onSizeChange"
|
||||
@current-change="onTier1PageChange"
|
||||
@size-change="onTier1SizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ─── Tab: 二级代理 ─── -->
|
||||
<el-tab-pane :label="`${t('user.type.sub_agent')} (${tier2Total})`" name="tier2Agents">
|
||||
<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-form-item>
|
||||
<el-form-item :label="t('common.status')">
|
||||
<el-select v-model="tier2FilterStatus" :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-form-item>
|
||||
</el-form>
|
||||
<div class="list-chrome__actions">
|
||||
<el-button type="primary" @click="openCreateSubAgentFromToolbar">{{ t('agent.create_sub_btn') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<el-table
|
||||
ref="tier2AgentTableRef"
|
||||
:data="tier2Agents"
|
||||
stripe
|
||||
row-key="userId"
|
||||
:row-class-name="expandableTableRowClassName"
|
||||
class="expandable-table"
|
||||
@expand-change="onExpandChange"
|
||||
@row-click="onTier2AgentRowClick"
|
||||
>
|
||||
<template #empty>
|
||||
<AdminTableEmpty />
|
||||
</template>
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="expand-panel">
|
||||
<div v-if="expandLoading[row.userId]" class="expand-loading">{{ t('common.loading') }}</div>
|
||||
<div v-else class="expand-panel-body">
|
||||
<div class="expand-section-header">
|
||||
<div class="expand-section-title">{{ directPlayersTabLabel(row.username, getPlayers(row.userId).length) }}</div>
|
||||
<el-button type="primary" size="small" @click="openCreatePlayer(row.userId)">{{ t('user.create_btn') }}</el-button>
|
||||
</div>
|
||||
<el-table :data="getPlayers(row.userId)" stripe class="inner-table">
|
||||
<template #empty><AdminTableEmpty /></template>
|
||||
<el-table-column prop="id" label="ID" width="60" />
|
||||
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" />
|
||||
<el-table-column :label="t('common.status')" width="80">
|
||||
<template #default="{ row: player }">
|
||||
<el-tag :type="statusTagType(player.status)" size="small">{{ statusLabel(player.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.col.balance')" min-width="120" align="right">
|
||||
<template #default="{ row: player }">
|
||||
<span class="amount-compact">{{ formatAmount(player.availableBalance) }} / {{ formatAmount(player.frozenBalance) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('common.actions')" width="340" fixed="right" align="center">
|
||||
<template #default="{ row: player }">
|
||||
<div class="action-btns">
|
||||
<el-button size="small" link type="primary" @click="openDetailPlayer(player.id)">{{ t('common.detail') }}</el-button>
|
||||
<el-button size="small" link type="primary" @click="openEditPlayer(player.id)">{{ t('common.edit') }}</el-button>
|
||||
<el-button size="small" link type="success" @click="openTransfer('deposit', player)">{{ t('common.topup') }}</el-button>
|
||||
<el-button size="small" link type="warning" @click="openTransfer('withdraw', player)">{{ t('agent_portal.withdraw_btn_label') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</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>
|
||||
<el-table-column :label="t('common.status')" width="88">
|
||||
<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">
|
||||
<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">
|
||||
<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="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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="pager">
|
||||
<el-pagination
|
||||
v-model:current-page="tier2Page"
|
||||
v-model:page-size="tier2PageSize"
|
||||
:total="tier2Total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@current-change="onTier2PageChange"
|
||||
@size-change="onTier2SizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- ═══════════ DIALOGS ═══════════ -->
|
||||
|
||||
<!-- ── Create (unified) ── -->
|
||||
<el-dialog v-model="createVisible" :title="t('user.dialog.create')" width="520px" destroy-on-close>
|
||||
<el-dialog v-model="createVisible" :title="createDialogTitle" width="520px" destroy-on-close class="create-account-dialog">
|
||||
<el-form label-width="100px">
|
||||
<el-form-item :label="t('user.col.username')" required>
|
||||
<el-input v-model="createForm.username" :placeholder="t('user.ph.username_unique')" />
|
||||
<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" />
|
||||
@@ -1154,31 +1396,40 @@ function creditTypeLabel(type: string) {
|
||||
<el-form-item :label="t('user.field.confirm_password')" required>
|
||||
<el-input v-model="createForm.confirmPassword" type="text" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.account_type')">
|
||||
<el-radio-group :model-value="createAccountMode" @update:model-value="onAccountModeChange">
|
||||
<el-radio :value="0">{{ t('user.type.player') }}</el-radio>
|
||||
<el-radio :value="1" :disabled="!!createParentAgentId">{{ t('user.type.tier1_agent') }}</el-radio>
|
||||
<el-radio :value="2" :disabled="!createParentAgentId">{{ t('user.type.sub_agent') }}</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="field-hint">
|
||||
<template v-if="createParentAgentId">
|
||||
{{ t('agent.hint.creating_under_agent') }}
|
||||
</template>
|
||||
<template v-else>{{ t('user.hint.account_type') }}</template>
|
||||
</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>
|
||||
|
||||
<!-- Player fields (when mode is player and no parent agent context) -->
|
||||
<template v-if="createAccountMode === 0 && !createParentAgentId">
|
||||
<!-- 玩家:从玩家 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 agents" :key="a.userId" :label="`${a.username} (#${a.userId})`" :value="a.userId" />
|
||||
<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>
|
||||
|
||||
<!-- Agent fields (tier1 or sub-agent) -->
|
||||
<!-- 二级代理:从一级代理行进入时锁定上级 -->
|
||||
<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>
|
||||
|
||||
<!-- 二级代理:从二级 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%" />
|
||||
@@ -1230,7 +1481,8 @@ function creditTypeLabel(type: string) {
|
||||
<el-tag :type="statusTagType(editPlayerForm.status)" size="small">{{ statusLabel(editPlayerForm.status) }}</el-tag>
|
||||
</div>
|
||||
<el-form-item :label="t('user.col.username')">
|
||||
<el-input v-model="editPlayerForm.username" :placeholder="t('user.ph.username_unique')" />
|
||||
<el-input v-model="editPlayerForm.username" :placeholder="t('user.ph.username_player')" />
|
||||
<div class="field-hint">{{ t('user.hint.username_player') }}</div>
|
||||
</el-form-item>
|
||||
<div class="password-mgmt-block">
|
||||
<div class="block-title">{{ t('user.section.password_mgmt') }}</div>
|
||||
@@ -1243,10 +1495,9 @@ function creditTypeLabel(type: string) {
|
||||
<el-input v-model="editPlayerForm.newPassword" type="text" autocomplete="off" :placeholder="t('user.ph.reset_password_short')" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item :label="t('user.filter.agent')">
|
||||
<el-select v-model="editPlayerForm.parentId" :placeholder="t('user.ph.no_agent')" clearable style="width: 100%">
|
||||
<el-option v-for="a in agents" :key="a.userId" :label="`${a.username} (#${a.userId})`" :value="a.userId" />
|
||||
</el-select>
|
||||
<el-form-item :label="t('user.col.agent')">
|
||||
<el-tag size="small" type="info" class="affiliation-tag">{{ affiliationLabel(editPlayerForm) }}</el-tag>
|
||||
<div class="field-hint">{{ t('user.hint.agent_readonly') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('user.field.phone')">
|
||||
<el-input v-model="editPlayerForm.phone" :placeholder="t('common.optional')" />
|
||||
@@ -1411,7 +1662,7 @@ function creditTypeLabel(type: string) {
|
||||
<el-descriptions-item :label="t('common.status')">
|
||||
<el-tag :type="statusTagType(playerDetail.status)" size="small">{{ statusLabel(playerDetail.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('user.col.agent')">{{ playerDetail.parentUsername ?? t('common.platform_direct') }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('user.col.agent')">{{ affiliationLabel(playerDetail) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('user.field.available')">
|
||||
{{ formatAmount(playerDetail.availableBalance) }}
|
||||
<span v-if="shouldCompact(playerDetail.availableBalance)" class="amount-full-hint">({{ formatAmountFull(playerDetail.availableBalance) }})</span>
|
||||
@@ -1509,9 +1760,75 @@ function creditTypeLabel(type: string) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agent-mgr-page > .agent-list-panel,
|
||||
.agent-mgr-page > .mgr-top-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.agent-mgr-page > .mgr-top-tabs :deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.agent-mgr-page > .mgr-top-tabs :deep(.el-tab-pane) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.player-list-panel,
|
||||
.agent-list-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mgr-top-tabs :deep(.el-tabs__header) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.mgr-top-tabs :deep(.el-tabs__item) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ─── Table toolbar ─── */
|
||||
.list-panel-toolbar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 0 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
--list-chrome-control-h: 32px;
|
||||
--el-component-size: 32px;
|
||||
}
|
||||
.list-panel-toolbar .list-chrome__actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.list-panel-toolbar :deep(.el-form-item) {
|
||||
margin-bottom: 0 !important;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.list-panel-toolbar :deep(.el-input__wrapper),
|
||||
.list-panel-toolbar :deep(.el-select__wrapper) {
|
||||
height: var(--list-chrome-control-h) !important;
|
||||
min-height: var(--list-chrome-control-h) !important;
|
||||
}
|
||||
.list-panel-toolbar :deep(.el-button:not(.is-link)) {
|
||||
height: var(--list-chrome-control-h) !important;
|
||||
min-height: var(--list-chrome-control-h) !important;
|
||||
}
|
||||
|
||||
/* ─── Expansion ─── */
|
||||
.expand-panel {
|
||||
padding: 4px 16px 8px;
|
||||
margin: 4px 0 8px;
|
||||
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 {
|
||||
text-align: center;
|
||||
@@ -1546,22 +1863,34 @@ function creditTypeLabel(type: string) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.inner-tabs :deep(.el-tabs__item) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
padding: 0 14px;
|
||||
}
|
||||
.inner-tabs :deep(.el-tabs__item.is-active) {
|
||||
color: #d4fde5;
|
||||
font-weight: 700;
|
||||
}
|
||||
.inner-tabs :deep(.el-tabs__active-bar) {
|
||||
background-color: var(--green-bright);
|
||||
height: 2px;
|
||||
}
|
||||
.inner-tabs :deep(.el-tabs__content) {
|
||||
padding: 0;
|
||||
}
|
||||
.inner-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.inner-table {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.inner-table :deep(.el-table__cell) {
|
||||
font-size: 13px;
|
||||
}
|
||||
.account-type-tag {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.expandable-table :deep(.row-expandable) {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1581,6 +1910,11 @@ function creditTypeLabel(type: string) {
|
||||
}
|
||||
.field-hint { font-size: 12px; color: #888; margin-top: 4px; }
|
||||
.amount-compact { white-space: nowrap; font-variant-numeric: tabular-nums; cursor: default; }
|
||||
.affiliation-tag {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.amount-full-hint { font-size: 11px; color: #666; margin-left: 4px; }
|
||||
.text-muted { color: #666; font-size: 12px; }
|
||||
.password-mgmt-block {
|
||||
|
||||
Reference in New Issue
Block a user