From 03e72ca9b2b5a0d8b38811d3283d2e6818d284a7 Mon Sep 17 00:00:00 2001 From: Mars <3361409208a@gmail.com> Date: Thu, 11 Jun 2026 17:23:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=80=E6=88=B7=E5=A4=87=E6=B3=A8?= =?UTF-8?q?=E3=80=81=E8=B4=A6=E5=8D=95=E5=B1=95=E7=A4=BA=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=B8=8E=E5=90=8E=E5=8F=B0=E4=BB=A3=E7=90=86=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增初始上分备注(日常上分/开户赠金/自定义)及前后台校验与展示 - 优化钱包流水类型与备注显示,区分管理员/代理/玩家上下分 - 修复登录后语言被后端覆盖的问题,登录时同步当前语言到服务端 - 后台代理/玩家表格操作栏重构,充值订单增加备注列 - 前台个人中心、充值、账单与验证码组件体验优化 Co-authored-by: Cursor --- apps/admin/src/App.vue | 125 +++ .../src/components/AdminAgentRowActions.vue | 76 ++ apps/admin/src/components/AdminNavIcon.vue | 97 +++ .../components/AdminResponsiveRowActions.vue | 89 +- apps/admin/src/components/AdminTableWrap.vue | 17 + .../components/InitialDepositRemarkField.vue | 47 ++ .../src/components/InviteManageDialog.vue | 16 +- .../useAdminTableRowActionsLayout.ts | 53 ++ apps/admin/src/i18n/admin-pages-ms.ts | 22 + apps/admin/src/i18n/admin-pages.ts | 66 +- apps/admin/src/layouts/ManageLayout.vue | 49 +- apps/admin/src/utils/initial-depositRemark.ts | 21 + apps/admin/src/utils/walletTx.ts | 15 +- apps/admin/src/views/AgentManager.vue | 341 ++++---- apps/admin/src/views/DepositOrders.vue | 7 +- apps/admin/src/views/Users.vue | 176 ++-- apps/admin/src/views/agent/Players.vue | 759 +++++++++++++----- .../src/views/agent/agent-player-form.ts | 16 +- .../src/views/agent/agent-sub-agent-form.ts | 27 + .../src/views/matches/LeagueMatchesPanel.vue | 85 +- apps/admin/src/views/user-form.ts | 15 +- apps/admin/vite.config.ts | 4 + .../applications/admin/admin.controller.ts | 14 + .../agent/agent-portal.controller.ts | 94 ++- apps/api/src/domains/agent/agents.service.ts | 598 +++++++++++++- .../src/domains/catalog/matches.service.ts | 23 + .../src/domains/deposit/deposit.service.ts | 1 + apps/api/src/domains/ledger/wallet.service.ts | 211 ++++- apps/player/src/components/RobotVerify.vue | 350 +++----- .../src/components/WalletStatsPanel.vue | 45 +- apps/player/src/composables/useAppLocale.ts | 16 +- .../src/composables/usePullToRefresh.ts | 28 +- apps/player/src/layouts/MainLayout.vue | 4 +- apps/player/src/main.ts | 45 +- apps/player/src/utils/walletTx.ts | 30 +- apps/player/src/views/CashbackRecordsView.vue | 91 ++- apps/player/src/views/LoginView.vue | 4 +- apps/player/src/views/ProfileView.vue | 578 ++++++++----- apps/player/src/views/RechargeHistoryView.vue | 77 +- apps/player/src/views/RechargeView.vue | 253 +++++- apps/player/src/views/WalletDetailView.vue | 37 +- .../src/views/WalletTransactionDetailView.vue | 31 +- apps/player/src/views/WalletView.vue | 61 +- packages/shared/src/api-errors.ts | 20 + packages/shared/src/index.ts | 1 + packages/shared/src/initial-depositRemark.ts | 45 ++ 46 files changed, 3721 insertions(+), 1059 deletions(-) create mode 100644 apps/admin/src/components/AdminAgentRowActions.vue create mode 100644 apps/admin/src/components/AdminNavIcon.vue create mode 100644 apps/admin/src/components/AdminTableWrap.vue create mode 100644 apps/admin/src/components/InitialDepositRemarkField.vue create mode 100644 apps/admin/src/composables/useAdminTableRowActionsLayout.ts create mode 100644 apps/admin/src/utils/initial-depositRemark.ts create mode 100644 packages/shared/src/initial-depositRemark.ts diff --git a/apps/admin/src/App.vue b/apps/admin/src/App.vue index 120af2c..2ed3a46 100644 --- a/apps/admin/src/App.vue +++ b/apps/admin/src/App.vue @@ -360,6 +360,15 @@ body::-webkit-scrollbar { .admin-list-page .table-wrap .el-table th.el-table__cell .cell { white-space: nowrap; } +.admin-list-page .table-wrap.is-compact-row-actions .admin-responsive-actions__inline { + display: none !important; +} +.admin-list-page .table-wrap.is-compact-row-actions .admin-responsive-actions__menu { + display: inline-flex !important; +} +.admin-list-page .table-wrap:not(.is-compact-row-actions) .admin-responsive-actions__menu { + display: none !important; +} .admin-list-page .list-hint { flex-shrink: 0; margin: 0; @@ -737,6 +746,122 @@ body { overflow-y: auto; } +.user-edit-dialog .edit-meta, +.agent-edit-dialog .edit-meta { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + font-size: 12px; + color: #888; +} +.user-edit-dialog .edit-form-section, +.agent-edit-dialog .edit-form-section { + margin-bottom: 12px; +} +.user-edit-dialog .edit-form-section:last-child, +.agent-edit-dialog .edit-form-section:last-child { + margin-bottom: 0; +} +.user-edit-dialog .section-title, +.agent-edit-dialog .section-title { + font-size: 11px; + font-weight: 700; + color: #888; + letter-spacing: 0.04em; + margin-bottom: 8px; +} +.user-edit-dialog .compact-edit-form .el-form-item, +.agent-edit-dialog .compact-edit-form .el-form-item { + margin-bottom: 10px; +} +.user-edit-dialog .field-hint, +.agent-edit-dialog .field-hint { + font-size: 12px; + color: #888; + margin-top: 4px; + line-height: 1.4; +} +.user-edit-dialog .inline-hint, +.agent-edit-dialog .inline-hint { + margin-top: 0; +} +.user-edit-dialog .password-mgmt-block, +.agent-edit-dialog .password-mgmt-block { + margin: 0; + padding: 10px 12px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + background: rgba(0, 0, 0, 0.15); +} +.user-edit-dialog .block-title, +.agent-edit-dialog .block-title { + font-size: 12px; + font-weight: 700; + color: #e8a84a; + margin-bottom: 8px; + letter-spacing: 0.04em; +} +.user-edit-dialog .password-field-row, +.agent-edit-dialog .password-field-row { + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + gap: 8px 10px; + align-items: center; + margin-bottom: 8px; +} +.user-edit-dialog .password-field-row:last-child, +.agent-edit-dialog .password-field-row:last-child { + margin-bottom: 0; +} +.user-edit-dialog .password-field-label, +.agent-edit-dialog .password-field-label { + font-size: 12px; + color: #888; + line-height: 1.35; +} +.user-edit-dialog .password-plain, +.agent-edit-dialog .password-plain { + font-family: ui-monospace, monospace; + font-size: 14px; + font-weight: 600; + color: #f0d090; + letter-spacing: 0.06em; +} +.user-edit-dialog .password-empty, +.agent-edit-dialog .password-empty { + color: #666; +} +.user-edit-dialog .block-hint, +.agent-edit-dialog .block-hint { + margin: -2px 0 8px; + padding-left: 82px; +} +.user-edit-dialog .cashback-edit-block, +.agent-edit-dialog .cashback-edit-block { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px 10px; +} +.user-edit-dialog .contact-row, +.agent-edit-dialog .contact-row { + margin-bottom: 0; +} +.user-edit-dialog .contact-row .el-form-item, +.agent-edit-dialog .contact-row .el-form-item { + margin-bottom: 0; +} +.user-edit-dialog .edit-stats-panel, +.agent-edit-dialog .edit-stats-panel { + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} +.agent-edit-dialog .edit-stats { + margin-top: 4px; +} + .entity-detail-dialog .el-dialog__body { padding: 12px 20px 16px !important; max-height: none !important; diff --git a/apps/admin/src/components/AdminAgentRowActions.vue b/apps/admin/src/components/AdminAgentRowActions.vue new file mode 100644 index 0000000..a00eda3 --- /dev/null +++ b/apps/admin/src/components/AdminAgentRowActions.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/apps/admin/src/components/AdminNavIcon.vue b/apps/admin/src/components/AdminNavIcon.vue new file mode 100644 index 0000000..dd69bfd --- /dev/null +++ b/apps/admin/src/components/AdminNavIcon.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/apps/admin/src/components/AdminResponsiveRowActions.vue b/apps/admin/src/components/AdminResponsiveRowActions.vue index 8a60f39..ede465b 100644 --- a/apps/admin/src/components/AdminResponsiveRowActions.vue +++ b/apps/admin/src/components/AdminResponsiveRowActions.vue @@ -1,10 +1,71 @@ diff --git a/apps/admin/src/composables/useAdminTableRowActionsLayout.ts b/apps/admin/src/composables/useAdminTableRowActionsLayout.ts new file mode 100644 index 0000000..8a4c7ac --- /dev/null +++ b/apps/admin/src/composables/useAdminTableRowActionsLayout.ts @@ -0,0 +1,53 @@ +import { ref, provide, inject, onMounted, onBeforeUnmount, nextTick, type Ref, type InjectionKey } from 'vue'; + +export const AdminTableRowActionsMenuKey: InjectionKey> = Symbol('adminTableRowActionsMenu'); + +const MOBILE_MAX = 768; + +export function useAdminTableRowActionsProvider(tableWrapRef: Ref) { + const useMenu = ref(false); + + function update() { + const wrap = tableWrapRef.value; + if (!wrap) return; + + if (window.innerWidth <= MOBILE_MAX) { + useMenu.value = true; + return; + } + + const table = wrap.querySelector('.el-table') as HTMLElement | null; + if (!table) { + useMenu.value = false; + return; + } + + useMenu.value = table.scrollWidth > wrap.clientWidth + 2; + } + + let observer: ResizeObserver | null = null; + + onMounted(async () => { + await nextTick(); + update(); + if (tableWrapRef.value) { + observer = new ResizeObserver(() => update()); + observer.observe(tableWrapRef.value); + const table = tableWrapRef.value.querySelector('.el-table'); + if (table) observer.observe(table); + } + window.addEventListener('resize', update); + }); + + onBeforeUnmount(() => { + observer?.disconnect(); + window.removeEventListener('resize', update); + }); + + provide(AdminTableRowActionsMenuKey, useMenu); + return useMenu; +} + +export function useAdminTableRowActionsMenu() { + return inject(AdminTableRowActionsMenuKey, null); +} diff --git a/apps/admin/src/i18n/admin-pages-ms.ts b/apps/admin/src/i18n/admin-pages-ms.ts index 6adeadc..991d59c 100644 --- a/apps/admin/src/i18n/admin-pages-ms.ts +++ b/apps/admin/src/i18n/admin-pages-ms.ts @@ -12,7 +12,9 @@ export const adminPagesMs: Record = { 'common.freeze': 'Bekukan', 'common.unfreeze': 'Nyahbeku', 'common.settle': 'Selesaikan', + 'common.resettle': 'Selesaikan semula', 'common.close_betting': 'Tutup pertaruhan', + 'common.reopen_betting': 'Buka semula pertaruhan', 'common.never_login': 'Belum pernah log masuk', 'common.optional': 'Pilihan', 'common.to': 'Hingga', @@ -73,6 +75,10 @@ export const adminPagesMs: Record = { 'user.reset_database_disabled_prod': 'Dilumpuhkan dalam produksi melainkan ALLOW_DB_RESET=true', 'user.reset_database_success': 'Pangkalan data diset semula. Sila log masuk semula.', 'user.reset_database_accounts': 'Akaun demo', + 'user.section.basic_info': 'Maklumat asas', + 'user.section.affiliation': 'Affiliasi', + 'user.section.contact': 'Hubungan', + 'user.section.account_overview': 'Gambaran akaun', 'user.section.password_mgmt': 'Pengurusan kata laluan', 'user.field.current_password': 'Kata laluan semasa', 'user.msg.created_with_password': 'Pemain dicipta. Kata laluan: {password}', @@ -207,7 +213,13 @@ export const adminPagesMs: Record = { 'finance.deposit_method.manual_agent': 'Deposit manual ejen', 'finance.deposit_method.manual': 'Deposit manual', 'finance.tx.deposit': 'Deposit', + 'finance.tx.admin_deposit': 'Tambah baki admin', + 'finance.tx.agent_deposit': 'Tambah baki ejen', + 'finance.tx.initial_deposit': 'Bonus pembukaan', + 'finance.tx.player_deposit': 'Deposit sendiri', 'finance.tx.withdraw': 'Pengeluaran', + 'finance.tx.admin_withdraw': 'Pengeluaran admin', + 'finance.tx.agent_withdraw': 'Pengeluaran ejen', 'finance.tx.request_id': 'ID permintaan', 'finance.remark.agent_deposit': 'Deposit ejen', 'finance.remark.agent_withdraw': 'Pengeluaran ejen', @@ -613,6 +625,12 @@ export const adminPagesMs: Record = { 'agent_portal.no_sub_agents': 'Tiada ejen peringkat 2. Klik butang di atas untuk cipta.', 'agent_portal.no_sub_agents_level': 'Tiada ejen peringkat {level}. Klik butang di atas untuk cipta.', 'agent_portal.sub_agent_players_readonly': 'Pemain langsung di bawah sub-ejen ini hanya boleh dilihat. Pembukaan akaun dan tambah baki diurus oleh sub-ejen.', + 'agent_portal.sub_agent_downline_readonly': 'Semua ejen dan pemain bawahan di bawah sub-ejen ini hanya boleh dilihat. Anda hanya boleh mengendalikan sub-ejen langsung; pembukaan akaun dan tambah baki diurus oleh setiap peringkat.', + 'agent_portal.sub_agent_downline_readonly_level': 'Semua ejen dan pemain di bawah ejen peringkat {level} ini hanya boleh dilihat. Anda hanya boleh mengendalikan sub-ejen langsung; pembukaan akaun dan tambah baki diurus oleh setiap peringkat.', + 'agent_portal.downline_agents_title': 'Ejen bawahan', + 'agent_portal.downline_players_title': 'Pemain bawahan', + 'agent_portal.no_downline_agents': 'Tiada ejen bawahan', + 'agent_portal.no_downline_players': 'Tiada pemain bawahan', 'agent_portal.sub_agent_players_readonly_level': 'Pemain langsung di bawah ejen peringkat {level} ini hanya boleh dilihat. Pembukaan akaun dan tambah baki diurus oleh ejen tersebut.', 'agent_portal.create_sub_agent_dialog': 'Ejen peringkat 2 baharu', 'agent_portal.sub_agent_credit_hint': 'Kredit awal diperuntukkan dari had tersedia anda', @@ -634,6 +652,10 @@ export const adminPagesMs: Record = { 'msg.match_created_draft': 'Perlawanan tunggal dicipta (draf)', 'msg.published': 'Diterbitkan dengan pasaran', 'msg.closed': 'Pertaruhan ditutup', + 'msg.reopened': 'Pertaruhan dibuka semula', + 'match.reopen_kickoff_title': 'Tetapkan masa mula baharu', + 'match.reopen_kickoff_hint': 'Masa mula telah berlalu. Pilih masa mula baharu pada masa hadapan sebelum membuka semula.', + 'match.reopen_kickoff_invalid': 'Sila pilih masa mula pada masa hadapan', 'msg.invalid_json': 'JSON tidak sah', 'msg.import_failed': 'Import gagal', 'msg.import_done': 'Import: {imported} ok, {skipped} dilangkau, {failed} gagal / {total} jumlah', diff --git a/apps/admin/src/i18n/admin-pages.ts b/apps/admin/src/i18n/admin-pages.ts index 3cedb5c..10a3379 100644 --- a/apps/admin/src/i18n/admin-pages.ts +++ b/apps/admin/src/i18n/admin-pages.ts @@ -12,7 +12,9 @@ export const adminPagesZh: Record = { 'common.freeze': '冻结', 'common.unfreeze': '解冻', 'common.settle': '结算', + 'common.resettle': '重新结算', 'common.close_betting': '封盘', + 'common.reopen_betting': '解除封盘', 'common.never_login': '从未登录', 'common.optional': '选填', 'common.to': '止', @@ -27,6 +29,8 @@ export const adminPagesZh: Record = { 'user.filter.agent_ph': '全部', 'user.col.username': '用户名', 'user.col.agent': '所属代理', + 'user.col.agent_cashback': '代理返水率', + 'user.col.player_cashback': '玩家返水率', 'user.col.invite_code': '邀请码', 'user.col.balance': '可用 / 冻结', 'user.col.bets': '注单', @@ -43,6 +47,11 @@ export const adminPagesZh: Record = { 'user.field.confirm_password': '确认密码', 'user.field.initial_balance': '初始余额', 'user.field.deposit_remark': '上分备注', + 'user.field.initial_deposit_kind': '流水说明', + 'user.initial_deposit_kind.daily': '日常充值', + 'user.initial_deposit_kind.opening_bonus': '开户赠金', + 'user.initial_deposit_kind.custom': '自定义', + 'user.ph.initial_deposit_custom': '请输入流水说明(至少 2 个字符)', 'user.field.amount': '金额', 'user.field.remark': '备注', 'user.field.account_status': '账号状态', @@ -73,6 +82,10 @@ export const adminPagesZh: Record = { 'user.reset_database_disabled_prod': '生产环境已禁用;需服务端设置 ALLOW_DB_RESET=true', 'user.reset_database_success': '数据库已重置,请使用初始账号重新登录', 'user.reset_database_accounts': '演示账号', + 'user.section.basic_info': '基本信息', + 'user.section.affiliation': '归属设置', + 'user.section.contact': '联系方式', + 'user.section.account_overview': '账户概览', 'user.section.password_mgmt': '密码管理', 'user.field.current_password': '当前密码', 'user.msg.created_with_password': '玩家已创建,登录密码:{password}', @@ -84,7 +97,7 @@ export const adminPagesZh: Record = { 'user.ph.no_agent': '不设置(平台直属玩家)', 'user.hint.no_agent': '留空表示不挂靠代理,由平台直接管理', 'user.hint.platform_direct_player': '该玩家隶属于平台(管理员直属)', - 'user.hint.initial_balance': '创建后自动上分,0 表示不开户赠金', + 'user.hint.initial_balance': '创建后自动上分,0 表示不开户上分', 'user.hint.deposit_remark': '有初始余额时写入流水备注', 'user.hint.freeze_in_list': '冻结/解冻请在列表操作列进行', 'user.hint.agent_change': '留空表示平台直属;变更后会重算相关代理已用授信', @@ -211,7 +224,12 @@ export const adminPagesZh: Record = { 'finance.deposit_method.manual_agent': '代理人工充值', 'finance.deposit_method.manual': '人工充值', 'finance.tx.deposit': '充值', + 'finance.tx.admin_deposit': '管理员上分', + 'finance.tx.agent_deposit': '代理上分', + 'finance.tx.player_deposit': '自助充值', 'finance.tx.withdraw': '下分', + 'finance.tx.admin_withdraw': '管理员下分', + 'finance.tx.agent_withdraw': '代理下分', 'finance.tx.request_id': '请求 ID', 'finance.remark.agent_deposit': '代理上分', 'finance.remark.agent_withdraw': '代理下分', @@ -502,6 +520,8 @@ export const adminPagesZh: Record = { 'err.user_required': '请选择用户', 'err.agent_no_parent': '一级代理不可设置上级玩家', 'err.agent_no_initial_deposit': '设为代理时请勿填写玩家初始余额', + 'err.initial_deposit_kind_required': '有初始余额时请选择上分流水说明', + 'err.initial_deposit_custom_required': '自定义流水说明至少 2 个字符', 'settlement.back': '返回赛事列表', 'settlement.kickoff': '开赛时间', @@ -618,8 +638,15 @@ export const adminPagesZh: Record = { 'credit.context.acting_agent': '当前代理', 'agent_portal.create_player_dialog': '新建直属玩家', 'agent_portal.edit_player_dialog': '编辑直属玩家', + 'agent_portal.my_cashback_rate': '反水比例', 'agent_portal.credit_available_hint': '当前可用授信:{amount}(上分将从授信中扣除)', 'agent_portal.sub_agent_players_readonly': '以下为该二级代理直属玩家,仅可查看;开户、上分等操作由二级代理自行处理。', + 'agent_portal.sub_agent_downline_readonly': '以下为该二级代理下级所有代理与玩家,仅可查看;您只能操作直属二级代理,开户、上分等由各级代理自行处理。', + 'agent_portal.sub_agent_downline_readonly_level': '以下为该{level}级代理下级所有代理与玩家,仅可查看;您只能操作直属下级代理,开户、上分等由各级代理自行处理。', + 'agent_portal.downline_agents_title': '下级代理', + 'agent_portal.downline_players_title': '下级玩家', + 'agent_portal.no_downline_agents': '暂无下级代理', + 'agent_portal.no_downline_players': '暂无下级玩家', 'agent_portal.initial_deposit_hint': '可选。开户时从您的授信中给玩家上分,不能超过可用授信', 'agent_portal.search_player_ph': '用户名或 ID', 'agent_portal.no_players': '暂无直属玩家,点击右上角创建', @@ -698,6 +725,10 @@ export const adminPagesZh: Record = { 'msg.match_created_draft': '单场已创建(草稿)', 'msg.published': '已发布并生成盘口', 'msg.closed': '已封盘', + 'msg.reopened': '已解除封盘', + 'match.reopen_kickoff_title': '设置新的开赛时间', + 'match.reopen_kickoff_hint': '开赛时间已过,请选择新的未来开赛时间后再解除封盘。', + 'match.reopen_kickoff_invalid': '请选择未来的开赛时间', 'msg.invalid_json': 'JSON 格式无效', 'msg.import_failed': '导入失败', 'msg.import_done': '导入完成:成功 {imported},跳过 {skipped},失败 {failed} / 共 {total}', @@ -951,7 +982,9 @@ export const adminPagesEn: Record = { 'common.freeze': 'Freeze', 'common.unfreeze': 'Unfreeze', 'common.settle': 'Settle', + 'common.resettle': 'Resettle', 'common.close_betting': 'Close', + 'common.reopen_betting': 'Reopen betting', 'common.never_login': 'Never signed in', 'common.optional': 'Optional', 'common.to': 'To', @@ -966,6 +999,8 @@ export const adminPagesEn: Record = { 'user.filter.agent_ph': 'All', 'user.col.username': 'Username', 'user.col.agent': 'Agent', + 'user.col.agent_cashback': 'Agent cashback', + 'user.col.player_cashback': 'Player cashback', 'user.col.invite_code': 'Invite code', 'user.col.balance': 'Available / Frozen', 'user.col.bets': 'Bets', @@ -982,6 +1017,11 @@ export const adminPagesEn: Record = { 'user.field.confirm_password': 'Confirm password', 'user.field.initial_balance': 'Initial balance', 'user.field.deposit_remark': 'Top-up note', + 'user.field.initial_deposit_kind': 'Ledger note', + 'user.initial_deposit_kind.daily': 'Regular top-up', + 'user.initial_deposit_kind.opening_bonus': 'Opening bonus', + 'user.initial_deposit_kind.custom': 'Custom', + 'user.ph.initial_deposit_custom': 'Enter ledger note (min. 2 characters)', 'user.field.amount': 'Amount', 'user.field.remark': 'Note', 'user.field.account_status': 'Account status', @@ -1012,6 +1052,10 @@ export const adminPagesEn: Record = { 'user.reset_database_disabled_prod': 'Disabled in production unless ALLOW_DB_RESET=true is set on the server', 'user.reset_database_success': 'Database reset complete. Sign in again with demo accounts.', 'user.reset_database_accounts': 'Demo accounts', + 'user.section.basic_info': 'Basic info', + 'user.section.affiliation': 'Affiliation', + 'user.section.contact': 'Contact', + 'user.section.account_overview': 'Account overview', 'user.section.password_mgmt': 'Password management', 'user.field.current_password': 'Current password', 'user.msg.created_with_password': 'Player created. Login password: {password}', @@ -1023,7 +1067,7 @@ export const adminPagesEn: Record = { 'user.ph.no_agent': 'None (platform direct)', 'user.hint.no_agent': 'Leave empty for platform-managed player', 'user.hint.platform_direct_player': 'This player belongs to the platform (admin direct).', - 'user.hint.initial_balance': 'Auto top-up on create; 0 = no bonus', + 'user.hint.initial_balance': 'Auto top-up on create; 0 = no initial top-up', 'user.hint.deposit_remark': 'Written to ledger when initial balance > 0', 'user.hint.freeze_in_list': 'Freeze/unfreeze from the list actions', 'user.hint.agent_change': 'Empty = platform direct; changes recalc agent credit', @@ -1150,7 +1194,12 @@ export const adminPagesEn: Record = { 'finance.deposit_method.manual_agent': 'Agent manual deposit', 'finance.deposit_method.manual': 'Manual deposit', 'finance.tx.deposit': 'Deposit', + 'finance.tx.admin_deposit': 'Admin top-up', + 'finance.tx.agent_deposit': 'Agent top-up', + 'finance.tx.player_deposit': 'Self deposit', 'finance.tx.withdraw': 'Withdraw', + 'finance.tx.admin_withdraw': 'Admin withdraw', + 'finance.tx.agent_withdraw': 'Agent withdraw', 'finance.tx.request_id': 'Request ID', 'finance.remark.agent_deposit': 'Agent deposit', 'finance.remark.agent_withdraw': 'Agent withdraw', @@ -1441,6 +1490,8 @@ export const adminPagesEn: Record = { 'err.user_required': 'Please select a user', 'err.agent_no_parent': 'Tier-1 agents cannot have a parent player', 'err.agent_no_initial_deposit': 'Do not set initial player balance when creating an agent', + 'err.initial_deposit_kind_required': 'Select a ledger note when initial balance > 0', + 'err.initial_deposit_custom_required': 'Custom ledger note must be at least 2 characters', 'settlement.back': 'Back to matches', 'settlement.kickoff': 'Kick-off', @@ -1558,8 +1609,15 @@ export const adminPagesEn: Record = { 'credit.context.acting_agent': 'Current agent', 'agent_portal.create_player_dialog': 'New direct player', 'agent_portal.edit_player_dialog': 'Edit direct player', + 'agent_portal.my_cashback_rate': 'Cashback rate', 'agent_portal.credit_available_hint': 'Available credit: {amount} (top-ups deduct from your limit)', 'agent_portal.sub_agent_players_readonly': 'Players under this sub-agent are read-only here. Account opening and top-ups are handled by the sub-agent.', + 'agent_portal.sub_agent_downline_readonly': 'All subordinate agents and players below this sub-agent are read-only. You may only operate direct sub-agents; account opening and top-ups are handled by each level.', + 'agent_portal.sub_agent_downline_readonly_level': 'All agents and players below this L{level} agent are read-only. You may only operate direct sub-agents; account opening and top-ups are handled by each level.', + 'agent_portal.downline_agents_title': 'Subordinate agents', + 'agent_portal.downline_players_title': 'Subordinate players', + 'agent_portal.no_downline_agents': 'No subordinate agents', + 'agent_portal.no_downline_players': 'No subordinate players', 'agent_portal.initial_deposit_hint': 'Optional. Initial top-up from your credit at account creation', 'agent_portal.search_player_ph': 'Username or ID', 'agent_portal.no_players': 'No direct players yet. Use the button above to create one.', @@ -1638,6 +1696,10 @@ export const adminPagesEn: Record = { 'msg.match_created_draft': 'Fixture created (draft)', 'msg.published': 'Published with markets', 'msg.closed': 'Betting closed', + 'msg.reopened': 'Betting reopened', + 'match.reopen_kickoff_title': 'Set new kickoff time', + 'match.reopen_kickoff_hint': 'Kickoff has passed. Choose a new future start time before reopening.', + 'match.reopen_kickoff_invalid': 'Please choose a future kickoff time', 'msg.invalid_json': 'Invalid JSON', 'msg.import_failed': 'Import failed', 'msg.import_done': 'Import: {imported} ok, {skipped} skipped, {failed} failed / {total} total', diff --git a/apps/admin/src/layouts/ManageLayout.vue b/apps/admin/src/layouts/ManageLayout.vue index afa9526..774610f 100644 --- a/apps/admin/src/layouts/ManageLayout.vue +++ b/apps/admin/src/layouts/ManageLayout.vue @@ -4,6 +4,7 @@ import { RouterView, RouterLink, useRoute, useRouter } from 'vue-router'; import { useAuthStore } from '../stores/auth'; import { useAdminLocale } from '../composables/useAdminLocale'; import AdminLocaleSwitcher from '../components/AdminLocaleSwitcher.vue'; +import AdminNavIcon from '../components/AdminNavIcon.vue'; import { resolveAdminBreadcrumb } from '../utils/admin-breadcrumb'; const route = useRoute(); @@ -15,24 +16,24 @@ const sidebarOpen = ref(false); const isMobileNav = ref(false); const adminMenus = computed(() => [ - { path: '/', label: t('nav.dashboard'), matchPrefix: true }, - { path: '/matches', label: t('nav.matches'), matchPrefix: true }, - { path: '/users', label: t('nav.agents_players') }, - { path: '/finance-logs', label: t('nav.finance_logs') }, - { path: '/deposit', label: t('nav.deposit_manage'), matchPrefix: true }, - { path: '/cashback', label: t('nav.cashback') }, - { path: '/bets', label: t('nav.bets') }, - { path: '/contents', label: t('nav.contents') }, - { path: '/media', label: t('nav.media') }, - { path: '/audit', label: t('nav.audit') }, - { path: '/smoke-tests', label: t('nav.smoke_tests') }, + { path: '/', label: t('nav.dashboard'), icon: 'dashboard', matchPrefix: true }, + { path: '/matches', label: t('nav.matches'), icon: 'matches', matchPrefix: true }, + { path: '/users', label: t('nav.agents_players'), icon: 'users' }, + { path: '/finance-logs', label: t('nav.finance_logs'), icon: 'finance' }, + { path: '/deposit', label: t('nav.deposit_manage'), icon: 'deposit', matchPrefix: true }, + { path: '/cashback', label: t('nav.cashback'), icon: 'cashback' }, + { path: '/bets', label: t('nav.bets'), icon: 'bets' }, + { path: '/contents', label: t('nav.contents'), icon: 'contents' }, + { path: '/media', label: t('nav.media'), icon: 'media' }, + { path: '/audit', label: t('nav.audit'), icon: 'audit' }, + { path: '/smoke-tests', label: t('nav.smoke_tests'), icon: 'smoke-tests' }, ]); const agentMenus = computed(() => [ - { path: '/', label: t('nav.dashboard') }, - { path: '/my-players', label: t('nav.agents_players') }, - { path: '/finance-logs', label: t('nav.finance_logs') }, - { path: '/my-bets', label: t('nav.myBets') }, + { path: '/', label: t('nav.dashboard'), icon: 'dashboard' }, + { path: '/my-players', label: t('nav.agents_players'), icon: 'users' }, + { path: '/finance-logs', label: t('nav.finance_logs'), icon: 'finance' }, + { path: '/my-bets', label: t('nav.myBets'), icon: 'bets' }, ]); const menus = computed(() => (auth.isAdmin.value ? adminMenus.value : agentMenus.value)); @@ -159,7 +160,8 @@ watch(() => route.path, () => { }" @click="onNavClick" > - {{ m.label }} + + {{ m.label }} @@ -272,6 +274,7 @@ watch(() => route.path, () => { .nav-item { display: flex; align-items: center; + gap: 8px; padding: 8px 10px; border-radius: 6px; color: #737373; @@ -280,15 +283,29 @@ watch(() => route.path, () => { transition: color 0.15s, background 0.15s; letter-spacing: 0; } +.nav-item :deep(.admin-nav-icon) { + opacity: 0.72; + transition: opacity 0.15s; +} +.nav-label { + min-width: 0; + line-height: 1.25; +} .nav-item:hover { background: rgba(255, 255, 255, 0.04); color: #d4d4d4; } +.nav-item:hover :deep(.admin-nav-icon) { + opacity: 0.92; +} .nav-item.active { background: rgba(255, 255, 255, 0.06); color: #f5f5f5; font-weight: 500; } +.nav-item.active :deep(.admin-nav-icon) { + opacity: 1; +} .sidebar-foot { padding: 10px 10px; diff --git a/apps/admin/src/utils/initial-depositRemark.ts b/apps/admin/src/utils/initial-depositRemark.ts new file mode 100644 index 0000000..931ebf3 --- /dev/null +++ b/apps/admin/src/utils/initial-depositRemark.ts @@ -0,0 +1,21 @@ +import { + type InitialDepositRemarkKind, + type InitialDepositOperator, + resolveInitialDepositRemark, +} from '@thebet365/shared'; +import { FormValidationError } from '../i18n/form-validation'; + +export function buildInitialDepositRemark( + initialDeposit: number, + kind: InitialDepositRemarkKind | '', + custom: string, + operator: InitialDepositOperator, +): string | undefined { + if (initialDeposit <= 0) return undefined; + if (!kind) throw new FormValidationError('err.initial_deposit_kind_required'); + try { + return resolveInitialDepositRemark(kind, custom, operator); + } catch { + throw new FormValidationError('err.initial_deposit_custom_required'); + } +} diff --git a/apps/admin/src/utils/walletTx.ts b/apps/admin/src/utils/walletTx.ts index 1d8ee89..f98c9f9 100644 --- a/apps/admin/src/utils/walletTx.ts +++ b/apps/admin/src/utils/walletTx.ts @@ -1,6 +1,11 @@ export const TX_KEY_MAP: Record = { MANUAL_DEPOSIT: 'finance.tx.deposit', + ADMIN_DEPOSIT: 'finance.tx.admin_deposit', + AGENT_DEPOSIT: 'finance.tx.agent_deposit', + INITIAL_DEPOSIT: 'finance.tx.admin_deposit', MANUAL_WITHDRAW: 'finance.tx.withdraw', + ADMIN_WITHDRAW: 'finance.tx.admin_withdraw', + AGENT_WITHDRAW: 'finance.tx.agent_withdraw', MANUAL_ADJUST: 'finance.tx.adjust', BET_FREEZE: 'finance.tx.bet_freeze', BET_DEDUCT: 'finance.tx.bet_deduct', @@ -16,7 +21,7 @@ export const TX_KEY_MAP: Record = { RESETTLE_REVERSE: 'finance.tx.resettle', DEPOSIT: 'finance.tx.deposit', WITHDRAW: 'finance.tx.withdraw', - PLAYER_DEPOSIT: 'finance.tx.deposit', + PLAYER_DEPOSIT: 'finance.tx.player_deposit', }; export function walletTxTypeKey(type: string): string { @@ -25,6 +30,9 @@ export function walletTxTypeKey(type: string): string { export const DEPOSIT_RECHARGE_TYPES = new Set([ 'MANUAL_DEPOSIT', + 'ADMIN_DEPOSIT', + 'AGENT_DEPOSIT', + 'INITIAL_DEPOSIT', 'DEPOSIT', 'PLAYER_DEPOSIT', ]); @@ -49,5 +57,10 @@ export function walletDepositMethodLabel( const key = DEPOSIT_METHOD_KEY_MAP[row.depositMethodKey]; return key ? t(key) : row.depositMethodKey; } + const type = row.transactionType.toUpperCase(); + if (type === 'ADMIN_DEPOSIT') return t('finance.tx.admin_deposit'); + if (type === 'AGENT_DEPOSIT') return t('finance.tx.agent_deposit'); + if (type === 'INITIAL_DEPOSIT') return t('finance.tx.admin_deposit'); + if (type === 'PLAYER_DEPOSIT') return t('finance.tx.player_deposit'); return '—'; } diff --git a/apps/admin/src/views/AgentManager.vue b/apps/admin/src/views/AgentManager.vue index 0542dfc..1b95d08 100644 --- a/apps/admin/src/views/AgentManager.vue +++ b/apps/admin/src/views/AgentManager.vue @@ -45,12 +45,14 @@ import { import AdminTableEmpty from '../components/AdminTableEmpty.vue'; import PlayerWalletLedgerDialog from '../components/PlayerWalletLedgerDialog.vue'; import WalletTransferContext from '../components/WalletTransferContext.vue'; +import InitialDepositRemarkField from '../components/InitialDepositRemarkField.vue'; import AgentCreditContext from '../components/AgentCreditContext.vue'; import RatePercentInput from '../components/RatePercentInput.vue'; import { formatRatePercent, percentToDecimalRate, decimalRateToPercent } from '../utils/rate-percent'; import InviteCodePanel from '../components/InviteCodePanel.vue'; import InviteManageDialog from '../components/InviteManageDialog.vue'; -import AdminRowActionsDropdown from '../components/AdminRowActionsDropdown.vue'; +import AdminTableWrap from '../components/AdminTableWrap.vue'; +import AdminAgentRowActions from '../components/AdminAgentRowActions.vue'; import AdminPlayerRowActions from '../components/AdminPlayerRowActions.vue'; import AdminDetailGrid from '../components/AdminDetailGrid.vue'; import AdminDetailItem from '../components/AdminDetailItem.vue'; @@ -83,11 +85,6 @@ type SubAgentLevelState = { const subAgentLevelState = reactive>({}); const agentLevelCounts = ref>({}); -const subAgentTableRefs = ref void } | null>>({}); - -function setSubAgentTableRef(level: number, el: unknown) { - subAgentTableRefs.value[level] = el as { toggleRowExpansion: (row: AgentRow) => void } | null; -} function ensureSubAgentState(level: number): SubAgentLevelState { if (!subAgentLevelState[level]) { @@ -155,7 +152,8 @@ const agentOptions = ref<{ id: string; username: string; level: number; parentUs const expandedSet = ref(new Set()); const agentPlayersMap = ref>({}); const expandLoading = ref>({}); -const tier1AgentTableRef = ref(); + +const expandedRowKeys = computed(() => Array.from(expandedSet.value)); const createToolbarChildLevel = ref(null); @@ -402,17 +400,23 @@ onMounted(() => { /* ─── 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; + try { + 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; + } catch (e) { + tier1Agents.value = []; + tier1Total.value = 0; + ElMessage.error(resolveApiError(e, t, 'msg.load_failed')); + } } function onTier1PageChange(p: number) { @@ -447,17 +451,23 @@ async function loadAgentLevelCounts() { async function loadSubAgentsAtLevel(level: number) { const st = ensureSubAgentState(level); - const { data } = await api.get('/admin/agents', { - params: { - page: st.page, - pageSize: st.pageSize, - keyword: st.keyword.trim() || undefined, - status: st.filterStatus || undefined, - level, - }, - }); - st.agents = data.data.items as AgentRow[]; - st.total = data.data.total; + try { + const { data } = await api.get('/admin/agents', { + params: { + page: st.page, + pageSize: st.pageSize, + keyword: st.keyword.trim() || undefined, + status: st.filterStatus || undefined, + level, + }, + }); + st.agents = data.data.items as AgentRow[]; + st.total = data.data.total; + } catch (e) { + st.agents = []; + st.total = 0; + ElMessage.error(resolveApiError(e, t, 'msg.load_failed')); + } } function onSubAgentPageChange(level: number, p: number) { @@ -560,18 +570,11 @@ function directPlayersTabLabel(ownerName: string, count: number) { } function onTier1AgentRowClick(row: AgentRow, _column: unknown, event: MouseEvent) { - onAgentRowClick(row, tier1AgentTableRef, _column, event); + onAgentRowClick(row, event); } -function onSubAgentRowClick(level: number, row: AgentRow, _column: unknown, event: MouseEvent) { - if (!shouldToggleExpandOnRowClick(event)) return; - subAgentTableRefs.value[level]?.toggleRowExpansion(row); -} - -function bindSubAgentRowClick(level: number) { - return (row: AgentRow, column: unknown, event: MouseEvent) => { - onSubAgentRowClick(level, row, column, event); - }; +function onSubAgentRowClick(row: AgentRow, _column: unknown, event: MouseEvent) { + onAgentRowClick(row, event); } watch(activeViewTab, (tab) => { @@ -595,9 +598,17 @@ async function onExpandChange(row: DisplayAgentRow, expandedRows: DisplayAgentRo } } -function onAgentRowClick(row: AgentRow, tableRef: typeof tier1AgentTableRef, _column: unknown, event: MouseEvent) { +function onAgentRowClick(row: AgentRow, event: MouseEvent) { if (!shouldToggleExpandOnRowClick(event)) return; - tableRef.value?.toggleRowExpansion(row); + const userId = row.userId; + const next = new Set(expandedSet.value); + if (next.has(userId)) { + next.delete(userId); + } else { + next.add(userId); + if (!agentPlayersMap.value[userId]) void loadExpansionData(userId); + } + expandedSet.value = next; } async function loadExpansionData(agentId: string) { @@ -1360,7 +1371,7 @@ function creditTypeLabel(type: string) { {{ t('user.create_btn') }} -
+ - + @@ -1931,61 +1945,90 @@ function creditTypeLabel(type: string) { - +
ID {{ editPlayerForm.id }} {{ statusLabel(editPlayerForm.status) }}
- - -
{{ t('user.hint.username_player') }}
-
-
-
{{ t('user.section.password_mgmt') }}
- - {{ editPlayerForm.managedPassword }} - - -

{{ t('user.hint.password_reset_to_view') }}

- - + +
+
{{ t('user.section.basic_info') }}
+ + +
{{ t('user.hint.username_player') }}
- - {{ affiliationLabel(editPlayerForm) }} -
{{ t('user.hint.agent_readonly') }}
-
- -
- - {{ t('cashback.use_custom_rate') }} - - -

- {{ t('cashback.use_default_rate', { rate: `${editPlayerForm.defaultCashbackRate.toFixed(2)}%` }) }} -

+ +
+
+
{{ t('user.section.password_mgmt') }}
+
+ {{ t('user.field.current_password') }} + {{ editPlayerForm.managedPassword }} + +
+

{{ t('user.hint.password_reset_to_view') }}

+
+ {{ t('user.field.reset_password') }} + +
- - - - - - - - - {{ formatAmount(editPlayerForm.availableBalance) }} - {{ formatAmount(editPlayerForm.frozenBalance) }} - {{ t('user.bets_edit_value', { n: editPlayerForm.betCount, stake: formatAmount(editPlayerForm.totalStake) }) }} - {{ formatAmount(editPlayerForm.totalReturn) }} - - {{ editPlayerForm.lastLoginAt ? formatTime(editPlayerForm.lastLoginAt) : t('common.never_login') }} - · {{ t('user.login_fail_value', { n: editPlayerForm.loginFailCount }) }} - - +
+ +
+
{{ t('user.section.affiliation') }}
+ + {{ affiliationLabel(editPlayerForm) }} +
{{ t('user.hint.agent_readonly') }}
+
+ +
+ + {{ t('cashback.use_custom_rate') }} + + + + {{ `${editPlayerForm.defaultCashbackRate.toFixed(2)}%` }} + +
+
+
+ +
+
{{ t('user.section.contact') }}
+ + + + + + + + + + + + +
+ +
+
{{ t('user.section.account_overview') }}
+ + {{ formatAmount(editPlayerForm.availableBalance) }} + {{ formatAmount(editPlayerForm.frozenBalance) }} + + {{ t('user.bets_edit_value', { n: editPlayerForm.betCount, stake: formatAmount(editPlayerForm.totalStake) }) }} + + {{ formatAmount(editPlayerForm.totalReturn) }} + + {{ editPlayerForm.lastLoginAt ? formatTime(editPlayerForm.lastLoginAt) : t('common.never_login') }} + · {{ t('user.login_fail_value', { n: editPlayerForm.loginFailCount }) }} + + +
@@ -2571,41 +2605,6 @@ function creditTypeLabel(type: string) { } .amount-full-hint { font-size: 11px; color: #666; margin-left: 4px; } .text-muted { color: #666; font-size: 12px; } -.password-mgmt-block { - margin: 4px 0 10px; - padding: 10px 12px; - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 8px; - background: rgba(0, 0, 0, 0.15); -} -.block-title { - font-size: 12px; - font-weight: 700; - color: #e8a84a; - margin-bottom: 8px; - letter-spacing: 0.04em; -} -.password-plain { - font-family: ui-monospace, monospace; - font-size: 14px; - font-weight: 600; - color: #f0d090; - letter-spacing: 0.06em; -} -.password-empty { color: #666; } -.block-hint { margin: -4px 0 8px; } -.edit-meta { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 10px; - font-size: 12px; - color: #888; -} -.compact-edit-form :deep(.el-form-item) { - margin-bottom: 10px; -} -.edit-stats { margin-top: 4px; } .list-settings-block--danger { margin-top: 12px; padding-top: 12px; diff --git a/apps/admin/src/views/DepositOrders.vue b/apps/admin/src/views/DepositOrders.vue index 9be8f9d..593a8cc 100644 --- a/apps/admin/src/views/DepositOrders.vue +++ b/apps/admin/src/views/DepositOrders.vue @@ -174,6 +174,7 @@ onMounted(fetchList); {{ t('common.status') }} {{ t('deposit.approved_amount') }} {{ t('deposit.reviewer') }} + {{ t('user.field.remark') }} {{ t('deposit.time') }} {{ t('common.actions') }} @@ -195,15 +196,13 @@ onMounted(fetchList); {{ statusLabel(row.status) }} {{ row.approvedAmount ? formatAmount(row.approvedAmount) : '-' }} {{ row.reviewerUsername || '-' }} + {{ row.remark || row.rejectReason || '-' }} {{ new Date(row.createdAt).toLocaleString() }} - @@ -293,6 +292,7 @@ onMounted(fetchList); .mono { font-family: monospace; font-size: 11px; } .amount { font-weight: 700; } .time-cell { font-size: 11px; color: #888; } +.remark-cell { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #aaa; } .screenshot-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: 4px; cursor: pointer; border: 1px solid #444; } .badge { padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 700; } .badge-blue { background: #1e3a5f; color: #66b1ff; } @@ -305,7 +305,6 @@ onMounted(fetchList); .btn-approve:hover { background: rgba(61, 115, 88, 0.24); } .btn-reject { background: #3a1a1a; color: #f56c6c; } .btn-reject:hover { background: #4a2525; } -.reject-reason { font-size: 11px; color: #f56c6c; max-width: 120px; display: inline-block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .pagination { display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 16px; } .pagination button { background: #333; color: #ddd; border: none; border-radius: 4px; padding: 6px 14px; cursor: pointer; } .pagination button:disabled { opacity: 0.4; cursor: default; } diff --git a/apps/admin/src/views/Users.vue b/apps/admin/src/views/Users.vue index 94fa3a8..fb1a3dc 100644 --- a/apps/admin/src/views/Users.vue +++ b/apps/admin/src/views/Users.vue @@ -30,6 +30,7 @@ import AdminTableEmpty from '../components/AdminTableEmpty.vue'; import AdminDetailGrid from '../components/AdminDetailGrid.vue'; import AdminDetailItem from '../components/AdminDetailItem.vue'; import WalletTransferContext from '../components/WalletTransferContext.vue'; +import InitialDepositRemarkField from '../components/InitialDepositRemarkField.vue'; import { useAdminPlayerTransfer } from '../composables/useAdminPlayerTransfer'; const users = ref([]); @@ -667,8 +668,15 @@ function statusLabel(s: string) {
{{ t('user.hint.initial_balance') }}
- - + + @@ -681,7 +689,7 @@ function statusLabel(s: string) { @@ -691,59 +699,82 @@ function statusLabel(s: string) { {{ statusLabel(editForm.status) }}
- - -
{{ t('user.hint.username_player') }}
-
- -
-
{{ t('user.section.password_mgmt') }}
- - {{ editForm.managedPassword }} - - -

- {{ t('user.hint.password_reset_to_view') }} -

- - +
+
{{ t('user.section.basic_info') }}
+ + +
{{ t('user.hint.username_player') }}
- - {{ playerAffiliationLabel(editForm) }} -
{{ t('user.hint.agent_readonly') }}
-
- - - - - - +
+
+
{{ t('user.section.password_mgmt') }}
+
+ {{ t('user.field.current_password') }} + {{ editForm.managedPassword }} + +
+

+ {{ t('user.hint.password_reset_to_view') }} +

+
+ {{ t('user.field.reset_password') }} + +
+
+
- - - {{ formatAmount(editForm.availableBalance) }} - - - {{ formatAmount(editForm.frozenBalance) }} - - - {{ t('user.bets_edit_value', { n: editForm.betCount, stake: formatAmount(editForm.totalStake) }) }} - - - {{ formatAmount(editForm.totalReturn) }} - - - {{ editForm.lastLoginAt ? formatTime(editForm.lastLoginAt) : t('common.never_login') }} - · {{ t('user.login_fail_value', { n: editForm.loginFailCount }) }} - - +
+
{{ t('user.section.affiliation') }}
+ + {{ playerAffiliationLabel(editForm) }} +
{{ t('user.hint.agent_readonly') }}
+
+
+ +
+
{{ t('user.section.contact') }}
+ + + + + + + + + + + + +
+ +
+
{{ t('user.section.account_overview') }}
+ + + {{ formatAmount(editForm.availableBalance) }} + + + {{ formatAmount(editForm.frozenBalance) }} + + + {{ t('user.bets_edit_value', { n: editForm.betCount, stake: formatAmount(editForm.totalStake) }) }} + + + {{ formatAmount(editForm.totalReturn) }} + + + {{ editForm.lastLoginAt ? formatTime(editForm.lastLoginAt) : t('common.never_login') }} + · {{ t('user.login_fail_value', { n: editForm.loginFailCount }) }} + + +