diff --git a/.gitignore b/.gitignore
index 84576aa..007e23a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
node_modules/
dist/
+.pnpm-store/
+release/
.claude/
*.log
.DS_Store
diff --git a/apps/admin/src/App.vue b/apps/admin/src/App.vue
index 9a78bba..625b410 100644
--- a/apps/admin/src/App.vue
+++ b/apps/admin/src/App.vue
@@ -659,8 +659,7 @@ body {
border-color: #2a2a2a !important;
}
.user-edit-dialog .el-dialog__body,
-.agent-edit-dialog .el-dialog__body,
-.create-account-dialog .el-dialog__body {
+.agent-edit-dialog .el-dialog__body {
max-height: min(70vh, 640px);
overflow-y: auto;
}
diff --git a/apps/admin/src/components/PlayerWalletLedgerDialog.vue b/apps/admin/src/components/PlayerWalletLedgerDialog.vue
new file mode 100644
index 0000000..871bfb4
--- /dev/null
+++ b/apps/admin/src/components/PlayerWalletLedgerDialog.vue
@@ -0,0 +1,285 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('common.search') }}
+
+
+
+
+
+
+
+
+
+ {{ formatTime(row.createdAt) }}
+
+
+ {{ row.transactionId }}
+
+
+ {{ walletTypeLabel(row.transactionType) }}
+
+
+
+
+
+ {{ formatAmount(row.amount) }}
+
+
+
+
+
+
+
+ {{ formatAmount(row.balanceBefore) }}
+
+
+
+
+
+
+ {{ formatAmount(row.balanceAfter) }}
+
+
+
+
+
+
+ {{ formatAmount(row.frozenBefore) }}
+
+
+
+
+
+
+ {{ formatAmount(row.frozenAfter) }}
+
+
+
+
+
+
+ {{ row.betNo }}
+
+ —
+
+
+
+ {{ row.operatorUsername ?? '—' }}
+
+
+ {{ row.remark ?? '—' }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/admin/src/i18n/admin-messages.ts b/apps/admin/src/i18n/admin-messages.ts
index f851ca0..1a5ae7b 100644
--- a/apps/admin/src/i18n/admin-messages.ts
+++ b/apps/admin/src/i18n/admin-messages.ts
@@ -47,7 +47,7 @@ const zh: Record = {
'nav.smoke_tests': '自动化测试',
'nav.media': '媒体库',
'nav.players': '直属玩家',
- 'nav.subAgents': '下级代理',
+ 'nav.subAgents': '二级代理',
'nav.myBets': '注单查询',
'nav.open_menu': '打开菜单',
'nav.close_menu': '关闭菜单',
@@ -174,7 +174,7 @@ const zh: Record = {
'agent_dash.liability_child': '下级代理占用',
'page.agent_players.title': '直属玩家',
'page.agent_players.desc': '管理你名下的直属玩家',
- 'page.agent_sub.title': '下级代理',
+ 'page.agent_sub.title': '二级代理',
'page.agent_sub.desc': '管理二级代理账号与授信分配',
'page.agent_bets.title': '注单查询',
'page.agent_bets.desc': '下级玩家的全部投注记录',
@@ -247,7 +247,7 @@ const en: Record = {
'nav.smoke_tests': 'Smoke tests',
'nav.media': 'Media Library',
'nav.players': 'My Players',
- 'nav.subAgents': 'Sub-Agents',
+ 'nav.subAgents': 'Tier-2 agents',
'nav.myBets': 'Bet Search',
'nav.open_menu': 'Open menu',
'nav.close_menu': 'Close menu',
@@ -374,7 +374,7 @@ const en: Record = {
'agent_dash.liability_child': 'Sub-agent exposure',
'page.agent_players.title': 'My players',
'page.agent_players.desc': 'Players under your account',
- 'page.agent_sub.title': 'Sub-agents',
+ 'page.agent_sub.title': 'Tier-2 agents',
'page.agent_sub.desc': 'Manage tier-2 agents and credit allocation',
'page.agent_bets.title': 'Bet search',
'page.agent_bets.desc': 'All bets from downstream players',
@@ -447,7 +447,7 @@ const ms: Record = {
'nav.smoke_tests': 'Ujian asap',
'nav.media': 'Perpustakaan Media',
'nav.players': 'Pemain saya',
- 'nav.subAgents': 'Sub-ejen',
+ 'nav.subAgents': 'Ejen peringkat 2',
'nav.myBets': 'Carian pertaruhan',
'nav.open_menu': 'Buka menu',
'nav.close_menu': 'Tutup menu',
@@ -574,7 +574,7 @@ const ms: Record = {
'agent_dash.liability_child': 'Pendedahan ejen bawahan',
'page.agent_players.title': 'Pemain saya',
'page.agent_players.desc': 'Pemain di bawah akaun anda',
- 'page.agent_sub.title': 'Sub-ejen',
+ 'page.agent_sub.title': 'Ejen peringkat 2',
'page.agent_sub.desc': 'Urus ejen peringkat 2 dan peruntukan kredit',
'page.agent_bets.title': 'Carian pertaruhan',
'page.agent_bets.desc': 'Semua pertaruhan pemain hiliran',
diff --git a/apps/admin/src/i18n/admin-pages-ms.ts b/apps/admin/src/i18n/admin-pages-ms.ts
index b142bd3..7927bb3 100644
--- a/apps/admin/src/i18n/admin-pages-ms.ts
+++ b/apps/admin/src/i18n/admin-pages-ms.ts
@@ -100,13 +100,21 @@ export const adminPagesMs: Record = {
'user.field.account_type': 'Jenis akaun',
'user.type.player': 'Pemain',
'user.type.tier1_agent': 'Ejen peringkat 1',
- 'user.type.sub_agent': 'Sub-ejen',
+ 'user.type.sub_agent': 'Ejen peringkat 2',
'user.hint.account_type': 'Ejen guna had kredit; pemain boleh di bawah ejen',
'agent.create_btn': '+ Ejen peringkat 1 baharu',
'agent.create_sub_btn': '+ Ejen peringkat 2 baharu',
- 'agent.create_sub': 'Cipta sub-ejen',
- 'agent.hint.sub_agent_parent': 'Ejen peringkat 2 mesti di bawah ejen peringkat 1',
+ 'agent.create_sub': 'Cipta ejen peringkat 2',
+ 'agent.create_child_btn': '+ Sub-ejen baharu',
+ 'agent.dialog.create_child_agent': 'Sub-ejen baharu',
+ 'agent.create_level_agent': 'Cipta ejen peringkat {level}',
+ 'agent.create_level_agent_btn': '+ Ejen peringkat {level} baharu',
+ 'agent.level_name': 'Ejen peringkat {level}',
+ 'agent.level_tab': 'Ejen peringkat {level}',
+ 'agent.dialog.create_level_agent': 'Ejen peringkat {level} baharu',
+ 'agent.hint.select_parent_for_level': 'Pilih ejen peringkat {level} sebagai induk',
+ 'agent.err.parent_level_mismatch': 'Peringkat induk tidak sah untuk cipta ejen peringkat {level}',
'agent.hint.creating_under_agent': 'Cipta akaun di bawah ejen ini',
'agent.filter.username_ph': 'Nama pengguna',
'agent_mgr.tab.players': 'Pemain',
@@ -150,6 +158,37 @@ export const adminPagesMs: Record = {
'agent.credit_tx.view_all': 'Lihat semua lejar kredit',
'finance.tab.credit': 'Lejar kredit',
'finance.tab.transfer': 'Lejar pemindahan',
+ 'finance.tab.wallet': 'Lejar dompet',
+ 'finance.filter.type_category': 'Jenis transaksi',
+ 'finance.filter.type_category_all': 'Semua',
+ 'finance.filter.type_category_deposit': 'Pemindahan',
+ 'finance.filter.type_category_bet': 'Pertaruhan',
+ 'finance.filter.type_category_cashback': 'Rebat',
+ 'finance.col.frozen_before': 'Beku sebelum',
+ 'finance.col.frozen_after': 'Beku selepas',
+ 'finance.col.reference': 'Pertaruhan berkaitan',
+ 'finance.tx.adjust': 'Pelarasan baki',
+ 'finance.tx.bet_freeze': 'Beku pertaruhan',
+ 'finance.tx.bet_deduct': 'Potong pertaruhan',
+ 'finance.tx.bet_win': 'Bayaran pertaruhan',
+ 'finance.tx.bet_lose': 'Penyelesaian pertaruhan',
+ 'finance.tx.bet_push': 'Refund seri',
+ 'finance.tx.bet_refund': 'Refund pertaruhan',
+ 'finance.tx.bet_void': 'Pertaruhan batal',
+ 'finance.tx.cashback': 'Rebat',
+ 'finance.tx.resettle': 'Penyelesaian semula',
+ 'user.action.view_wallet_ledger': 'Lihat lejar dompet',
+ 'user.wallet_ledger_dialog_title': 'Lejar dompet — {name}',
+ 'agent.hierarchy.settings_title': 'Hierarki ejen',
+ 'agent.hierarchy.settings_hint': '0 bermaksud tanpa had. Ejen di had atas tidak boleh cipta sub-ejen.',
+ 'agent.hierarchy.max_level': 'Tahap ejen maksimum',
+ 'agent.hierarchy.default_sub_credit_ratio': 'Nisbah kredit sub-ejen lalai',
+ 'agent.hierarchy.create_credit_default_hint': 'Lalai {ratio}% ({amount}), tidak melebihi kredit induk; boleh diselaraskan',
+ 'agent.hierarchy.create_credit_quick_hint': 'Kredit induk tersedia {amount} — klik nisbah untuk isi',
+ 'agent.hierarchy.create_level_hint': 'Akan dicipta sebagai ejen peringkat {n}',
+ 'agent.field.parent_agent': 'Ejen induk',
+ 'agent.col.parent_chain': 'Rantaian induk',
+ 'role.agent_level': 'Ejen peringkat {n}',
'finance.filter.date_range': 'Julat tarikh',
'finance.filter.player_ph': 'Nama pengguna pemain',
'finance.filter.parent_agent_ph': 'Nama/ID ejen induk',
diff --git a/apps/admin/src/i18n/admin-pages.ts b/apps/admin/src/i18n/admin-pages.ts
index 5bb97bd..f74a883 100644
--- a/apps/admin/src/i18n/admin-pages.ts
+++ b/apps/admin/src/i18n/admin-pages.ts
@@ -106,13 +106,21 @@ export const adminPagesZh: Record = {
'agent.create_btn': '+ 新建一级代理',
'agent.create_sub_btn': '+ 新建二级代理',
'agent.create_sub': '创建二级代理',
- 'agent.hint.sub_agent_parent': '二级代理必须挂靠在一级代理名下',
+ 'agent.create_child_btn': '+ 新建下级代理',
+ 'agent.dialog.create_child_agent': '新建下级代理',
+ 'agent.create_level_agent': '创建{level}级代理',
+ 'agent.create_level_agent_btn': '+ 新建{level}级代理',
+ 'agent.level_name': '{level}级代理',
+ 'agent.level_tab': '{level}级代理',
+ 'agent.dialog.create_level_agent': '新建{level}级代理',
+ 'agent.hint.select_parent_for_level': '请选择 {level} 级代理作为上级',
+ 'agent.err.parent_level_mismatch': '上级代理层级不正确,无法创建 {level} 级代理',
'agent.hint.creating_under_agent': '在此代理下创建账号',
'agent.filter.username_ph': '用户名',
'agent_mgr.tab.players': '玩家',
'agent_mgr.tab.agents': '代理',
'agent.col.level': '层级',
- 'agent.col.credit': '授信 / 已用 / 可用',
+ 'agent.col.credit': '授信/已用/可用',
'agent.col.direct_players': '直属玩家',
'agent.direct_players_title': '直属玩家 · {name}',
'agent.platform_row_name': '平台',
@@ -153,6 +161,38 @@ export const adminPagesZh: Record = {
'agent.credit_tx.view_all': '查看全部额度流水',
'finance.tab.credit': '额度流水',
'finance.tab.transfer': '上下分流水',
+ 'finance.tab.wallet': '钱包流水',
+ 'finance.filter.type_category': '流水类型',
+ 'finance.filter.type_category_all': '全部',
+ 'finance.filter.type_category_deposit': '上下分',
+ 'finance.filter.type_category_bet': '投注',
+ 'finance.filter.type_category_cashback': '返水',
+ 'finance.col.frozen_before': '变动前冻结',
+ 'finance.col.frozen_after': '变动后冻结',
+ 'finance.col.reference': '关联注单',
+ 'finance.tx.adjust': '余额调整',
+ 'finance.tx.bet_freeze': '投注冻结',
+ 'finance.tx.bet_deduct': '投注扣款',
+ 'finance.tx.bet_win': '投注派彩',
+ 'finance.tx.bet_lose': '投注结算',
+ 'finance.tx.bet_push': '走水返还',
+ 'finance.tx.bet_refund': '投注退款',
+ 'finance.tx.bet_void': '注单作废',
+ 'finance.tx.cashback': '返水',
+ 'finance.tx.resettle': '重结算调整',
+ 'user.action.view_wallet_ledger': '查看资金流水',
+ 'user.wallet_ledger_dialog_title': '{name} 的资金流水',
+ 'agent.hierarchy.settings_title': '代理层级设置',
+ 'agent.hierarchy.settings_hint': '0 表示不限制代理层级;达到上限的代理将无法创建下级。下级默认授信比例用于创建下级代理时的预填额度。',
+ 'agent.hierarchy.max_level': '最大代理层级',
+ 'agent.hierarchy.default_sub_credit_ratio': '下级默认授信比例',
+ 'agent.hierarchy.default_sub_credit_ratio_hint': '创建下级代理时,授信额度默认预填为上级可用授信 × 此比例',
+ 'agent.hierarchy.create_credit_default_hint': '默认 {ratio}%({amount}),不超过上级可用授信,可手动调整',
+ 'agent.hierarchy.create_credit_quick_hint': '上级可用授信 {amount},点击比例快速填入',
+ 'agent.hierarchy.create_level_hint': '将创建为 {n} 级代理',
+ 'agent.field.parent_agent': '上级代理',
+ 'agent.col.parent_chain': '上级链路',
+ 'role.agent_level': '{n}级代理',
'finance.filter.date_range': '时间范围',
'finance.filter.player_ph': '玩家用户名',
'finance.filter.parent_agent_ph': '上级代理用户名或 ID',
@@ -176,17 +216,15 @@ export const adminPagesZh: Record = {
'agent.field.select_user': '选择用户',
'agent.ph.select_user': '搜索玩家用户名',
'agent.hint.select_user': '从已有玩家账号中选择,将其设为一级代理(不新建登录账号)',
- 'agent.suspend.settings_title': '代理停用策略',
- 'agent.suspend.settings_hint': 'MVP 默认仅停用代理操作权限,不自动冻结或禁止其直属玩家登录。',
- 'agent.suspend.freeze_direct_players': '停用时允许级联冻结直属玩家',
- 'agent.suspend.block_player_login': '上级代理停用时禁止直属玩家登录',
- 'agent.suspend.cascade_disabled_hint': '未开启级联冻结,仅停用该代理操作权限,直属玩家不受影响。',
'agent.freeze.confirm_freeze_title': '确认停用代理',
'agent.freeze.confirm_freeze_body': '确定停用代理「{name}」?停用后该代理无法登录代理端。',
'agent.freeze.confirm_unfreeze_body': '确定恢复代理「{name}」为正常状态?',
- 'agent.freeze.cascade_hint': '是否同时冻结该代理名下所有直属玩家账号?',
- 'agent.freeze.cascade_label': '级联冻结直属玩家',
+ 'agent.freeze.opt_freeze_direct_players': '同时冻结直属玩家',
+ 'agent.freeze.opt_block_player_login': '禁止直属玩家登录',
+ 'agent.unfreeze.confirm_title': '确认恢复代理',
+ 'agent.unfreeze.opt_unfreeze_direct_players': '同时解冻直属玩家',
'agent.msg.cascade_freeze_done': '已停用代理并冻结其直属玩家',
+ 'agent.msg.cascade_unfreeze_done': '已恢复代理并解冻其直属玩家',
'agent.msg.freeze_done': '已{action}',
'match.create_btn': '+ 新增联赛',
@@ -931,13 +969,21 @@ export const adminPagesEn: Record = {
'user.field.account_type': 'Account type',
'user.type.player': 'Player',
'user.type.tier1_agent': 'Tier-1 agent',
- 'user.type.sub_agent': 'Sub-agent',
+ 'user.type.sub_agent': 'Tier-2 agent',
'user.hint.account_type': 'Agents use credit limits; players can belong to an agent and receive top-ups',
'agent.create_btn': '+ New tier-1 agent',
'agent.create_sub_btn': '+ New tier-2 agent',
- 'agent.create_sub': 'Create sub-agent',
- 'agent.hint.sub_agent_parent': 'Tier-2 agents must belong to a tier-1 agent',
+ 'agent.create_sub': 'Create tier-2 agent',
+ 'agent.create_child_btn': '+ New sub-agent',
+ 'agent.dialog.create_child_agent': 'New sub-agent',
+ 'agent.create_level_agent': 'Create L{level} agent',
+ 'agent.create_level_agent_btn': '+ New L{level} agent',
+ 'agent.level_name': 'Tier-{level} agent',
+ 'agent.level_tab': 'L{level} agents',
+ 'agent.dialog.create_level_agent': 'New L{level} agent',
+ 'agent.hint.select_parent_for_level': 'Select a level-{level} agent as parent',
+ 'agent.err.parent_level_mismatch': 'Invalid parent level for creating a level-{level} agent',
'agent.hint.creating_under_agent': 'Create account under this agent',
'agent.filter.username_ph': 'Username',
'agent_mgr.tab.players': 'Players',
@@ -984,6 +1030,38 @@ export const adminPagesEn: Record = {
'agent.credit_tx.view_all': 'View all credit ledger',
'finance.tab.credit': 'Credit ledger',
'finance.tab.transfer': 'Transfer ledger',
+ 'finance.tab.wallet': 'Wallet ledger',
+ 'finance.filter.type_category': 'Transaction type',
+ 'finance.filter.type_category_all': 'All',
+ 'finance.filter.type_category_deposit': 'Transfers',
+ 'finance.filter.type_category_bet': 'Bets',
+ 'finance.filter.type_category_cashback': 'Cashback',
+ 'finance.col.frozen_before': 'Frozen before',
+ 'finance.col.frozen_after': 'Frozen after',
+ 'finance.col.reference': 'Related bet',
+ 'finance.tx.adjust': 'Balance adjustment',
+ 'finance.tx.bet_freeze': 'Bet freeze',
+ 'finance.tx.bet_deduct': 'Bet deduct',
+ 'finance.tx.bet_win': 'Bet payout',
+ 'finance.tx.bet_lose': 'Bet settlement',
+ 'finance.tx.bet_push': 'Push refund',
+ 'finance.tx.bet_refund': 'Bet refund',
+ 'finance.tx.bet_void': 'Bet void',
+ 'finance.tx.cashback': 'Cashback',
+ 'finance.tx.resettle': 'Resettlement',
+ 'user.action.view_wallet_ledger': 'View wallet ledger',
+ 'user.wallet_ledger_dialog_title': 'Wallet ledger — {name}',
+ 'agent.hierarchy.settings_title': 'Agent hierarchy',
+ 'agent.hierarchy.settings_hint': '0 means unlimited levels. Agents at the cap cannot create sub-agents. The default credit ratio pre-fills sub-agent credit limits.',
+ 'agent.hierarchy.max_level': 'Max agent level',
+ 'agent.hierarchy.default_sub_credit_ratio': 'Default sub-agent credit ratio',
+ 'agent.hierarchy.default_sub_credit_ratio_hint': 'When creating a sub-agent, pre-fill credit as parent available × this ratio',
+ 'agent.hierarchy.create_credit_default_hint': 'Default {ratio}% ({amount}), capped by parent available credit; adjustable',
+ 'agent.hierarchy.create_credit_quick_hint': 'Parent available {amount} — click a ratio to fill',
+ 'agent.hierarchy.create_level_hint': 'Will be created as level {n} agent',
+ 'agent.field.parent_agent': 'Parent agent',
+ 'agent.col.parent_chain': 'Parent chain',
+ 'role.agent_level': 'Level {n} agent',
'finance.filter.date_range': 'Date range',
'finance.filter.player_ph': 'Player username',
'finance.filter.parent_agent_ph': 'Parent agent username or ID',
@@ -1007,17 +1085,15 @@ export const adminPagesEn: Record = {
'agent.field.select_user': 'Select user',
'agent.ph.select_user': 'Search player username',
'agent.hint.select_user': 'Pick an existing player account to promote to tier-1 agent (no new login)',
- 'agent.suspend.settings_title': 'Agent suspension policy',
- 'agent.suspend.settings_hint': 'MVP default: suspend agent operations only; do not auto-freeze or block direct players.',
- 'agent.suspend.freeze_direct_players': 'Allow cascade freeze of direct players on suspend',
- 'agent.suspend.block_player_login': 'Block direct player login when parent agent is suspended',
- 'agent.suspend.cascade_disabled_hint': 'Cascade freeze is off; only the agent is suspended, direct players are unaffected.',
'agent.freeze.confirm_freeze_title': 'Confirm suspend agent',
'agent.freeze.confirm_freeze_body': 'Suspend agent "{name}"? They will not be able to sign in to the agent portal.',
'agent.freeze.confirm_unfreeze_body': 'Restore agent "{name}" to active status?',
- 'agent.freeze.cascade_hint': 'Also freeze all direct player accounts under this agent?',
- 'agent.freeze.cascade_label': 'Cascade freeze direct players',
+ 'agent.freeze.opt_freeze_direct_players': 'Also freeze direct players',
+ 'agent.freeze.opt_block_player_login': 'Block direct player login',
+ 'agent.unfreeze.confirm_title': 'Confirm restore agent',
+ 'agent.unfreeze.opt_unfreeze_direct_players': 'Also unfreeze direct players',
'agent.msg.cascade_freeze_done': 'Agent suspended and direct players frozen',
+ 'agent.msg.cascade_unfreeze_done': 'Agent restored and direct players unfrozen',
'agent.msg.freeze_done': '{action} completed',
'match.create_btn': '+ New league',
diff --git a/apps/admin/src/layouts/ManageLayout.vue b/apps/admin/src/layouts/ManageLayout.vue
index a58957c..bfda375 100644
--- a/apps/admin/src/layouts/ManageLayout.vue
+++ b/apps/admin/src/layouts/ManageLayout.vue
@@ -64,8 +64,10 @@ const topbarCrumbs = computed(() => resolveAdminBreadcrumb(route.path, t));
const roleLabel = computed(() => {
if (auth.isAdmin.value) return t('role.admin');
- if (auth.isTier1Agent.value) return t('role.tier1_agent');
- if (auth.isTier2Agent.value) return t('role.tier2_agent');
+ const level = auth.user.value?.agentLevel;
+ if (auth.isAgent.value && level != null && level > 0) {
+ return t('role.agent_level', { n: level });
+ }
return t('role.agent');
});
diff --git a/apps/admin/src/stores/auth.ts b/apps/admin/src/stores/auth.ts
index 26f9e5f..0c4ce30 100644
--- a/apps/admin/src/stores/auth.ts
+++ b/apps/admin/src/stores/auth.ts
@@ -9,6 +9,8 @@ export interface StaffUser {
locale?: string;
role?: string;
agentLevel?: number | null;
+ maxAgentLevel?: number | null;
+ canManageSubAgents?: boolean;
}
const TOKEN_KEY = 'manage_token';
@@ -127,6 +129,15 @@ export function useAuthStore() {
const isAgent = computed(() => resolveUserType() === 'AGENT');
const isTier1Agent = computed(() => isAgent.value && user.value?.agentLevel === 1);
const isTier2Agent = computed(() => isAgent.value && user.value?.agentLevel === 2);
+ const canManageSubAgents = computed(() => {
+ if (!isAgent.value) return false;
+ if (user.value?.canManageSubAgents != null) return user.value.canManageSubAgents;
+ const level = user.value?.agentLevel;
+ const max = user.value?.maxAgentLevel;
+ if (level == null || level < 1) return false;
+ if (max == null || max === 0) return true;
+ return level < max;
+ });
const portalLabel = computed(() => (isAdmin.value ? '平台后台' : '代理后台'));
function setSession(newToken: string, newUser: StaffUser) {
@@ -149,6 +160,7 @@ export function useAuthStore() {
isAgent,
isTier1Agent,
isTier2Agent,
+ canManageSubAgents,
portalLabel,
setSession,
logout,
diff --git a/apps/admin/src/utils/agent-level-label.ts b/apps/admin/src/utils/agent-level-label.ts
new file mode 100644
index 0000000..3607228
--- /dev/null
+++ b/apps/admin/src/utils/agent-level-label.ts
@@ -0,0 +1,20 @@
+const ZH_LEVEL_NUMERALS: Record = {
+ 1: '一',
+ 2: '二',
+ 3: '三',
+ 4: '四',
+ 5: '五',
+ 6: '六',
+ 7: '七',
+ 8: '八',
+ 9: '九',
+ 10: '十',
+};
+
+/** 中文界面用「一、二、三…」,其他语言用阿拉伯数字 */
+export function formatAgentLevelNumeral(level: number, locale: string): string {
+ if (locale.startsWith('zh') && ZH_LEVEL_NUMERALS[level]) {
+ return ZH_LEVEL_NUMERALS[level];
+ }
+ return String(level);
+}
diff --git a/apps/admin/src/utils/session-hydrate.ts b/apps/admin/src/utils/session-hydrate.ts
index c3894ab..913f23c 100644
--- a/apps/admin/src/utils/session-hydrate.ts
+++ b/apps/admin/src/utils/session-hydrate.ts
@@ -47,6 +47,8 @@ export async function hydrateStaffSession(): Promise {
locale: raw.locale,
role: raw.role,
agentLevel: typeof raw.agentLevel === 'number' ? raw.agentLevel : null,
+ maxAgentLevel: typeof raw.maxAgentLevel === 'number' ? raw.maxAgentLevel : null,
+ canManageSubAgents: raw.canManageSubAgents === true,
});
return true;
} catch (e: unknown) {
diff --git a/apps/admin/src/utils/walletTx.ts b/apps/admin/src/utils/walletTx.ts
new file mode 100644
index 0000000..4a7d46e
--- /dev/null
+++ b/apps/admin/src/utils/walletTx.ts
@@ -0,0 +1,23 @@
+export const TX_KEY_MAP: Record = {
+ MANUAL_DEPOSIT: 'finance.tx.deposit',
+ MANUAL_WITHDRAW: 'finance.tx.withdraw',
+ MANUAL_ADJUST: 'finance.tx.adjust',
+ BET_FREEZE: 'finance.tx.bet_freeze',
+ BET_DEDUCT: 'finance.tx.bet_deduct',
+ BET_SETTLE_WIN: 'finance.tx.bet_win',
+ BET_SETTLE_LOSE: 'finance.tx.bet_lose',
+ BET_SETTLE_PUSH: 'finance.tx.bet_push',
+ BET_WIN: 'finance.tx.bet_win',
+ BET_REFUND: 'finance.tx.bet_refund',
+ BET_VOID: 'finance.tx.bet_void',
+ BET_VOID_REFUND: 'finance.tx.bet_void',
+ CASHBACK: 'finance.tx.cashback',
+ CASHBACK_DEPOSIT: 'finance.tx.cashback',
+ RESETTLE_REVERSE: 'finance.tx.resettle',
+ DEPOSIT: 'finance.tx.deposit',
+ WITHDRAW: 'finance.tx.withdraw',
+};
+
+export function walletTxTypeKey(type: string): string {
+ return TX_KEY_MAP[type.toUpperCase()] ?? '';
+}
diff --git a/apps/admin/src/views/AgentManager.vue b/apps/admin/src/views/AgentManager.vue
index be60ecd..799b0b6 100644
--- a/apps/admin/src/views/AgentManager.vue
+++ b/apps/admin/src/views/AgentManager.vue
@@ -1,5 +1,5 @@
@@ -36,7 +44,14 @@ const emit = defineEmits<{ toggle: []; bet: [id: string] }>();
{{ expanded ? '−' : '+' }}
- *{{ leagueName }}
+
+ *{{ leagueName }}
+
+ {{ totalCount }}场
+ {{ liveCount }}进行中
+ {{ openCount }}待开赛
+
+
();
diff --git a/packages/shared/src/api-errors.ts b/packages/shared/src/api-errors.ts
index 31500dc..add2674 100644
--- a/packages/shared/src/api-errors.ts
+++ b/packages/shared/src/api-errors.ts
@@ -390,9 +390,24 @@ export const API_ERROR_MESSAGES = {
'ms-MY': 'Pengguna sudah menjadi ejen',
},
AGENT_LEVEL_INVALID: {
- 'zh-CN': '代理级别须为 1 或 2',
- 'en-US': 'Agent level must be 1 or 2',
- 'ms-MY': 'Tahap ejen mesti 1 atau 2',
+ 'zh-CN': '代理级别无效',
+ 'en-US': 'Invalid agent level',
+ 'ms-MY': 'Tahap ejen tidak sah',
+ },
+ AGENT_MAX_LEVEL_REACHED: {
+ 'zh-CN': '已达到最大代理层级,无法继续创建下级',
+ 'en-US': 'Maximum agent level reached; cannot create sub-agents',
+ 'ms-MY': 'Tahap ejen maksimum dicapai; tidak boleh cipta sub-ejen',
+ },
+ AGENT_PARENT_LEVEL_MISMATCH: {
+ 'zh-CN': '上级代理层级与目标层级不匹配',
+ 'en-US': 'Parent agent level does not match target level',
+ 'ms-MY': 'Tahap ejen induk tidak sepadan dengan tahap sasaran',
+ },
+ AGENT_LEVEL_ROOT_INVALID: {
+ 'zh-CN': '一级代理不可指定上级',
+ 'en-US': 'Root-level agents cannot have a parent',
+ 'ms-MY': 'Ejen peringkat akar tidak boleh ada induk',
},
LEVEL2_REQUIRES_PARENT: {
'zh-CN': '二级代理必须指定上级代理',
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index ecec252..5e22ff5 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -11,6 +11,12 @@ export enum UserStatus {
LOCKED = 'LOCKED',
}
+/** Minimum agent tier (root agents). Actual levels are unbounded integers when `agent.max_level` is 0. */
+export const MIN_AGENT_LEVEL = 1;
+
+/**
+ * Legacy enum for the original two-tier demo. Production code should use numeric `agentLevel` / `AgentProfile.level`.
+ */
export enum AgentLevel {
LEVEL_1 = 1,
LEVEL_2 = 2,