From 5c5aa7e55a798689c30ff53dd402b3dbded227a7 Mon Sep 17 00:00:00 2001 From: Mars <3361409208a@gmail.com> Date: Thu, 18 Jun 2026 10:08:03 +0800 Subject: [PATCH] feat(theme-4): sync inbox/announcements/presence/deposit from main --- apps/admin/src/App.vue | 40 + .../src/components/AdminPlayerStatusCell.vue | 52 + apps/admin/src/components/AuditLogTable.vue | 3 +- .../src/components/ContentImageField.vue | 316 ++++ .../src/components/ContentRichEditor.vue | 531 +++++++ .../src/composables/useDepositPendingCount.ts | 40 + apps/admin/src/i18n/admin-messages.ts | 46 +- apps/admin/src/i18n/admin-pages-ms.ts | 45 +- apps/admin/src/i18n/admin-pages.ts | 66 +- apps/admin/src/i18n/locale-loader.ts | 6 +- apps/admin/src/i18n/pages/en.ts | 47 +- apps/admin/src/i18n/pages/zh.ts | 49 +- apps/admin/src/layouts/ManageLayout.vue | 104 +- apps/admin/src/stores/auth.ts | 2 + apps/admin/src/utils/html.ts | 12 + .../src/views/AgentCreditTransactions.vue | 2 +- apps/admin/src/views/AgentManager.vue | 42 +- apps/admin/src/views/Bets.vue | 1 + apps/admin/src/views/Cashback.vue | 1 + apps/admin/src/views/Contents.vue | 1337 ++++++++++------- apps/admin/src/views/DepositOrders.vue | 33 +- apps/admin/src/views/FinanceLogs.vue | 6 +- apps/admin/src/views/Matches.vue | 2 +- apps/admin/src/views/MatchesOutrights.vue | 2 +- apps/admin/src/views/PaymentMethods.vue | 4 +- apps/admin/src/views/Settlement.vue | 2 + apps/admin/src/views/SmokeTests.vue | 1 + apps/admin/src/views/StaffManage.vue | 190 ++- apps/admin/src/views/Users.vue | 7 +- apps/admin/src/views/dashboard-types.ts | 1 + .../src/views/dashboard/DashboardPlayers.vue | 6 + .../src/views/matches/LeagueMatchesPanel.vue | 4 +- apps/admin/src/views/user-form.ts | 1 + .../migration.sql | 2 + .../migration.sql | 22 + apps/api/prisma/schema.prisma | 19 + .../admin/admin-dashboard.service.ts | 9 +- .../applications/admin/admin.controller.ts | 101 +- .../src/applications/admin/admin.module.ts | 4 + .../applications/player/player.controller.ts | 83 +- .../src/applications/player/player.module.ts | 4 +- .../domains/catalog/matches.service.spec.ts | 185 ++- .../src/domains/catalog/matches.service.ts | 54 + .../api/src/domains/deposit/deposit.module.ts | 4 +- .../domains/deposit/deposit.service.spec.ts | 24 +- .../src/domains/deposit/deposit.service.ts | 44 + .../domains/identity/admin-staff.service.ts | 44 +- .../src/domains/identity/auth.controller.ts | 13 + apps/api/src/domains/identity/auth.service.ts | 1 + apps/api/src/domains/identity/users.module.ts | 3 +- .../api/src/domains/identity/users.service.ts | 15 +- .../operations/content/content.service.ts | 4 + .../player-messages/player-messages.module.ts | 8 + .../player-messages.service.spec.ts | 165 ++ .../player-messages.service.ts | 437 ++++++ .../src/domains/presence/presence.module.ts | 10 + .../domains/presence/presence.service.spec.ts | 70 + .../src/domains/presence/presence.service.ts | 57 + .../shared/config/system-config.service.ts | 41 + apps/player/src/assets/images/vs.png | Bin 0 -> 114184 bytes .../src/components/AnnouncementMarquee.vue | 33 +- apps/player/src/components/BetSlipDrawer.vue | 326 +++- apps/player/src/components/ConfirmDialog.vue | 186 +++ .../src/components/CustomerServiceModal.vue | 165 -- .../src/components/CustomerServicePanel.vue | 60 + .../src/components/MessageListPanel.vue | 345 +++++ .../composables/useDepositNotifications.ts | 174 +++ .../player/src/composables/useInboxFeature.ts | 23 + apps/player/src/composables/usePlayerHome.ts | 91 +- .../src/composables/usePlayerMessages.ts | 130 ++ .../player/src/composables/usePresencePing.ts | 37 + apps/player/src/i18n/en-US.ts | 75 +- apps/player/src/i18n/ms-MY.ts | 75 +- apps/player/src/i18n/zh-CN.ts | 77 +- apps/player/src/layouts/MainLayout.vue | 99 +- apps/player/src/router/index.ts | 4 + apps/player/src/stores/betSlip.ts | 11 + apps/player/src/utils/html.ts | 65 + .../src/views/AnnouncementDetailView.vue | 320 ++++ .../player/src/views/AnnouncementListView.vue | 178 +++ apps/player/src/views/HomeView.vue | 307 +++- apps/player/src/views/InboxHubView.vue | 315 ++++ apps/player/src/views/MessageDetailView.vue | 360 +++++ apps/player/src/views/ProfileView.vue | 13 + packages/shared/public/site.webmanifest | 4 +- packages/shared/src/api-errors.js | 20 + packages/shared/src/api-errors.ts | 20 + 87 files changed, 6911 insertions(+), 1026 deletions(-) create mode 100644 apps/admin/src/components/AdminPlayerStatusCell.vue create mode 100644 apps/admin/src/components/ContentImageField.vue create mode 100644 apps/admin/src/components/ContentRichEditor.vue create mode 100644 apps/admin/src/composables/useDepositPendingCount.ts create mode 100644 apps/admin/src/utils/html.ts create mode 100644 apps/api/prisma/migrations/20260617083520_add_visible_menus/migration.sql create mode 100644 apps/api/prisma/migrations/20260617120000_player_messages/migration.sql create mode 100644 apps/api/src/domains/player-messages/player-messages.module.ts create mode 100644 apps/api/src/domains/player-messages/player-messages.service.spec.ts create mode 100644 apps/api/src/domains/player-messages/player-messages.service.ts create mode 100644 apps/api/src/domains/presence/presence.module.ts create mode 100644 apps/api/src/domains/presence/presence.service.spec.ts create mode 100644 apps/api/src/domains/presence/presence.service.ts create mode 100644 apps/player/src/assets/images/vs.png create mode 100644 apps/player/src/components/ConfirmDialog.vue delete mode 100644 apps/player/src/components/CustomerServiceModal.vue create mode 100644 apps/player/src/components/CustomerServicePanel.vue create mode 100644 apps/player/src/components/MessageListPanel.vue create mode 100644 apps/player/src/composables/useDepositNotifications.ts create mode 100644 apps/player/src/composables/useInboxFeature.ts create mode 100644 apps/player/src/composables/usePlayerMessages.ts create mode 100644 apps/player/src/composables/usePresencePing.ts create mode 100644 apps/player/src/utils/html.ts create mode 100644 apps/player/src/views/AnnouncementDetailView.vue create mode 100644 apps/player/src/views/AnnouncementListView.vue create mode 100644 apps/player/src/views/InboxHubView.vue create mode 100644 apps/player/src/views/MessageDetailView.vue diff --git a/apps/admin/src/App.vue b/apps/admin/src/App.vue index 137036e..0ff3dec 100644 --- a/apps/admin/src/App.vue +++ b/apps/admin/src/App.vue @@ -1153,6 +1153,12 @@ a { color: var(--text) !important; } +.el-input, +.el-select, +.el-date-editor { + height: auto !important; +} + .el-input__wrapper:hover, .el-select__wrapper:hover, .el-textarea__inner:hover { @@ -1426,6 +1432,40 @@ input:-webkit-autofill:focus { color: var(--warning-text) !important; } +/* Link buttons hover in light theme - override dark legacy */ +.el-button.is-link:hover, +.el-button.is-link:focus, +.el-button.is-link.el-button--default:hover, +.el-button.is-link.el-button--default:focus, +.el-button.is-link.el-button--primary:hover, +.el-button.is-link.el-button--primary:focus { + color: #155b86 !important; /* Darker blue */ + background: transparent !important; + text-decoration: underline !important; +} + +.el-button.is-link.el-button--success:hover, +.el-button.is-link.el-button--success:focus { + color: #224225 !important; /* Darker green */ + background: transparent !important; + text-decoration: underline !important; +} + +.el-button.is-link.el-button--warning:hover, +.el-button.is-link.el-button--warning:focus { + color: #704b00 !important; /* Darker warning/gold */ + background: transparent !important; + text-decoration: underline !important; +} + +.el-button.is-link.el-button--danger:hover, +.el-button.is-link.el-button--danger:focus { + color: #7d2321 !important; /* Darker danger/red */ + background: transparent !important; + text-decoration: underline !important; +} + + .el-button.is-disabled, .el-button.is-disabled:hover { background: #f4f0e8 !important; diff --git a/apps/admin/src/components/AdminPlayerStatusCell.vue b/apps/admin/src/components/AdminPlayerStatusCell.vue new file mode 100644 index 0000000..0cd8302 --- /dev/null +++ b/apps/admin/src/components/AdminPlayerStatusCell.vue @@ -0,0 +1,52 @@ + + + + + {{ statusLabel(status) }} + + ({{ t(isOnline ? 'user.presence_online' : 'user.presence_offline') }}) + + + + + diff --git a/apps/admin/src/components/AuditLogTable.vue b/apps/admin/src/components/AuditLogTable.vue index 17b9803..6ce4f79 100644 --- a/apps/admin/src/components/AuditLogTable.vue +++ b/apps/admin/src/components/AuditLogTable.vue @@ -37,7 +37,7 @@ interface AuditRow { const logs = ref([]); const total = ref(0); const page = ref(1); -const pageSize = ref(20); +const pageSize = ref(10); const filterModule = ref(''); onMounted(load); @@ -129,6 +129,7 @@ function operatorDisplay(row: AuditRow): string { + {{ formatTime(row.createdAt) }} diff --git a/apps/admin/src/components/ContentImageField.vue b/apps/admin/src/components/ContentImageField.vue new file mode 100644 index 0000000..1459588 --- /dev/null +++ b/apps/admin/src/components/ContentImageField.vue @@ -0,0 +1,316 @@ + + + + + + + + + × + + + + {{ t(sizeHintKey) }} + + + + {{ uploading ? t('content.upload.uploading') : t('content.upload.upload_btn') }} + + + {{ t('content.upload.pick_media') }} + + + + + + + + + {{ t('common.loading') }} + {{ t('content.upload.no_media') }} + + + + + SVG + + {{ file.filename }} + + + + + + diff --git a/apps/admin/src/components/ContentRichEditor.vue b/apps/admin/src/components/ContentRichEditor.vue new file mode 100644 index 0000000..4590f00 --- /dev/null +++ b/apps/admin/src/components/ContentRichEditor.vue @@ -0,0 +1,531 @@ + + + + + + + B + + + I + + + • + + + 1. + + + + {{ t('content.editor.insert_image') }} + + + {{ t('content.upload.pick_media') }} + + + + + + + {{ t('common.loading') }} + {{ t('content.upload.no_media') }} + + + + + SVG + + {{ file.filename }} + + + + + + diff --git a/apps/admin/src/composables/useDepositPendingCount.ts b/apps/admin/src/composables/useDepositPendingCount.ts new file mode 100644 index 0000000..2d204aa --- /dev/null +++ b/apps/admin/src/composables/useDepositPendingCount.ts @@ -0,0 +1,40 @@ +import { ref } from 'vue'; +import api from '../api'; + +const pendingCount = ref(0); +let pollTimer: ReturnType | null = null; +let pollConsumers = 0; + +export async function refreshDepositPendingCount() { + try { + const { data } = await api.get('/admin/deposit-orders/pending-count'); + pendingCount.value = Number(data.data?.count ?? 0); + } catch { + pendingCount.value = 0; + } +} + +export function useDepositPendingCount() { + function startDepositPendingPolling() { + pollConsumers += 1; + if (pollConsumers > 1) return; + void refreshDepositPendingCount(); + pollTimer = setInterval(() => void refreshDepositPendingCount(), 30_000); + } + + function stopDepositPendingPolling() { + pollConsumers = Math.max(0, pollConsumers - 1); + if (pollConsumers > 0) return; + if (pollTimer) { + clearInterval(pollTimer); + pollTimer = null; + } + } + + return { + pendingCount, + refreshDepositPendingCount, + startDepositPendingPolling, + stopDepositPendingPolling, + }; +} diff --git a/apps/admin/src/i18n/admin-messages.ts b/apps/admin/src/i18n/admin-messages.ts index 6a64988..2a618cc 100644 --- a/apps/admin/src/i18n/admin-messages.ts +++ b/apps/admin/src/i18n/admin-messages.ts @@ -35,6 +35,7 @@ const zh: Record = { 'staff.col.last_login': '最近登录', 'staff.dialog.create': '创建后台账号', 'staff.dialog.edit': '编辑后台账号', + 'staff.field.visible_menus': '菜单权限', 'login.captcha_ph': '验证码', 'login.captcha_refresh': '点击刷新', @@ -69,6 +70,7 @@ const zh: Record = { 'deposit.status_approved': '已通过', 'deposit.status_rejected': '已拒绝', 'deposit.search_player_ph': '搜索玩家...', + 'deposit.pending_badge': '待审核 {n} 笔', 'deposit.order_no': '订单号', 'deposit.player': '玩家', 'deposit.amount': '金额', @@ -114,7 +116,7 @@ const zh: Record = { 'deposit.add_method': '+ 添加', 'deposit.display_name': '展示名称', 'deposit.details': '详情', - 'deposit.sort': '排序', + 'deposit.sort': '排序值', 'deposit.active': '启用', 'deposit.show_player': '前台展示', 'deposit.edit_method': '编辑收款方式', @@ -124,7 +126,7 @@ const zh: Record = { 'deposit.account_number': '银行账号', 'deposit.usdt_address': 'USDT 地址', 'deposit.qr_code': '二维码', - 'deposit.sort_order': '排序序号', + 'deposit.sort_order': '排序值', 'deposit.show_on_player': '展示给玩家', 'deposit.save': '保存', 'deposit.confirm_deactivate': '确认停用此收款方式?', @@ -150,11 +152,12 @@ const zh: Record = { 'role.tier2_agent': '二级代理', 'logout': '退出', 'lang': '语言', - 'portal.admin': '平台后台', - 'portal.agent': '代理后台', + 'portal.admin': '平台后台管理', + 'portal.agent': '代理后台管理', 'common.all': '全部', 'common.search': '查询', + 'common.refresh': '刷新', 'common.reset': '重置', 'common.edit': '编辑', 'common.yes': '是', @@ -164,9 +167,13 @@ const zh: Record = { 'common.confirm': '确定', 'common.retry': '重试', 'common.status': '状态', + 'user.status.ACTIVE': '正常', + 'user.status.SUSPENDED': '已冻结', + 'user.status.DISABLED': '已停用', 'common.type': '类型', 'common.keyword': '关键词', 'common.actions': '操作', + 'common.seq': '序号', 'common.more': '更多', 'common.loading': '加载中…', 'common.no_data': '暂无数据', @@ -218,6 +225,9 @@ const zh: Record = { 'dash.match_pending_settle': '待结算', 'dash.match_settled': '已结算', 'dash.user_active': '正常玩家', + 'dash.user_online': '当前在线', + 'dash.players_online': '当前在线', + 'dash.players_online_hint': '约 2 分钟内活跃', 'dash.user_suspended': '停用', 'dash.user_direct': '直属', 'dash.user_agents': '代理', @@ -334,6 +344,7 @@ const en: Record = { 'staff.col.last_login': 'Last login', 'staff.dialog.create': 'Create staff account', 'staff.dialog.edit': 'Edit staff account', + 'staff.field.visible_menus': 'Menu Permissions', 'login.captcha_ph': 'Captcha', 'login.captcha_refresh': 'Click to refresh', @@ -370,6 +381,7 @@ const en: Record = { 'deposit.status_approved': 'Approved', 'deposit.status_rejected': 'Rejected', 'deposit.search_player_ph': 'Search player...', + 'deposit.pending_badge': '{n} pending', 'deposit.order_no': 'Order No', 'deposit.player': 'Player', 'deposit.amount': 'Amount', @@ -449,11 +461,12 @@ const en: Record = { 'role.tier2_agent': 'Tier-2 Agent', 'logout': 'Logout', 'lang': 'Language', - 'portal.admin': 'Platform Admin', - 'portal.agent': 'Agent Portal', + 'portal.admin': 'Admin Console', + 'portal.agent': 'Agent Console', 'common.all': 'All', 'common.search': 'Search', + 'common.refresh': 'Refresh', 'common.reset': 'Reset', 'common.edit': 'Edit', 'common.yes': 'Yes', @@ -463,9 +476,13 @@ const en: Record = { 'common.confirm': 'OK', 'common.retry': 'Retry', 'common.status': 'Status', + 'user.status.ACTIVE': 'Active', + 'user.status.SUSPENDED': 'Suspended', + 'user.status.DISABLED': 'Disabled', 'common.type': 'Type', 'common.keyword': 'Keyword', 'common.actions': 'Actions', + 'common.seq': 'No.', 'common.more': 'More', 'common.loading': 'Loading…', 'common.no_data': 'No data', @@ -517,6 +534,9 @@ const en: Record = { 'dash.match_pending_settle': 'Pending settlement', 'dash.match_settled': 'Settled', 'dash.user_active': 'Active players', + 'dash.user_online': 'Online now', + 'dash.players_online': 'Online now', + 'dash.players_online_hint': 'Active within ~2 min', 'dash.user_suspended': 'Suspended', 'dash.user_direct': 'Direct', 'dash.user_agents': 'Agents', @@ -633,6 +653,7 @@ const ms: Record = { 'staff.col.last_login': 'Log masuk terakhir', 'staff.dialog.create': 'Cipta akaun kakitangan', 'staff.dialog.edit': 'Edit akaun kakitangan', + 'staff.field.visible_menus': 'Kebenaran Menu', 'login.captcha_ph': 'Captcha', 'login.captcha_refresh': 'Klik untuk muat semula', @@ -669,6 +690,7 @@ const ms: Record = { 'deposit.status_approved': 'Diluluskan', 'deposit.status_rejected': 'Ditolak', 'deposit.search_player_ph': 'Cari pemain...', + 'deposit.pending_badge': '{n} menunggu', 'deposit.order_no': 'No. Pesanan', 'deposit.player': 'Pemain', 'deposit.amount': 'Jumlah', @@ -748,11 +770,12 @@ const ms: Record = { 'role.tier2_agent': 'Ejen Peringkat 2', 'logout': 'Log keluar', 'lang': 'Bahasa', - 'portal.admin': 'Admin Platform', - 'portal.agent': 'Portal Ejen', + 'portal.admin': 'Pengurusan Admin', + 'portal.agent': 'Pengurusan Ejen', 'common.all': 'Semua', 'common.search': 'Cari', + 'common.refresh': 'Muat semula', 'common.reset': 'Set semula', 'common.edit': 'Edit', 'common.yes': 'Ya', @@ -762,9 +785,13 @@ const ms: Record = { 'common.confirm': 'OK', 'common.retry': 'Cuba lagi', 'common.status': 'Status', + 'user.status.ACTIVE': 'Aktif', + 'user.status.SUSPENDED': 'Digantung', + 'user.status.DISABLED': 'Dinyahaktifkan', 'common.type': 'Jenis', 'common.keyword': 'Kata kunci', 'common.actions': 'Tindakan', + 'common.seq': 'No.', 'common.more': 'Lagi', 'common.loading': 'Memuatkan…', 'common.no_data': 'Tiada data', @@ -816,6 +843,9 @@ const ms: Record = { 'dash.match_pending_settle': 'Menunggu penyelesaian', 'dash.match_settled': 'Diselesaikan', 'dash.user_active': 'Pemain aktif', + 'dash.user_online': 'Dalam talian', + 'dash.players_online': 'Dalam talian', + 'dash.players_online_hint': 'Aktif dalam ~2 min', 'dash.user_suspended': 'Digantung', 'dash.user_direct': 'Terus', 'dash.user_agents': 'Ejen', diff --git a/apps/admin/src/i18n/admin-pages-ms.ts b/apps/admin/src/i18n/admin-pages-ms.ts index 49621f3..288ae94 100644 --- a/apps/admin/src/i18n/admin-pages-ms.ts +++ b/apps/admin/src/i18n/admin-pages-ms.ts @@ -28,6 +28,10 @@ export const adminPagesMs: Record = { 'user.filter.agent': 'Ejen', 'user.filter.agent_ph': 'Semua', 'user.col.username': 'Nama pengguna', + 'user.col.online': 'Dalam talian', + 'user.online_yes': 'Sedang dalam talian', + 'user.presence_online': 'Dalam talian', + 'user.presence_offline': 'Luar talian', 'user.col.agent': 'Ejen', 'user.col.invite_code': 'Kod jemputan', 'user.col.balance': 'Tersedia / Dibekukan', @@ -765,11 +769,39 @@ export const adminPagesMs: Record = { 'content.btn.enable': 'Aktifkan', 'content.btn.disable': 'Nyahaktif', 'content.dialog.create': 'Kandungan awam baharu', - 'content.dialog.edit': 'Edit kandungan awam', + 'content.dialog.create_banner': 'Promosi laman utama baharu', + 'content.dialog.create_notice': 'Terbitkan notifikasi', + 'content.dialog.edit': 'Edit kandungan', + 'content.dialog.edit_banner': 'Edit promosi laman utama', + 'content.dialog.edit_notice': 'Edit notifikasi', 'content.confirm_delete': 'Padam "{title}"?', - 'content.type.BANNER': 'Banner laman utama', - 'content.type.ANNOUNCEMENT': 'Pengumuman', - 'content.hint.announcement': 'Dipaparkan di ticker atas pemain; isi tajuk atau kandungan', + 'content.type.BANNER': 'Promosi laman utama', + 'content.type.ANNOUNCEMENT': 'Notifikasi', + 'content.type.INBOX_NOTIFY': 'Notifikasi peti mesej', + 'content.inbox_notify.inbox_enabled': 'Peti mesej pemain', + 'content.inbox_notify.inbox_enabled_hint': 'Jika dimatikan, pemain hanya dibawa ke sokongan (tiada tab peti mesej)', + 'content.inbox_notify.deposit': 'Keputusan deposit', + 'content.inbox_notify.deposit_hint': 'Hantar mesej apabila diluluskan atau ditolak', + 'content.inbox_notify.manual_title': 'Tanda "Notifikasi peti mesej" semasa mencipta:', + 'content.inbox_notify.banner_note': 'Promosi laman utama', + 'content.inbox_notify.announcement_note': 'Notifikasi / ticker', + 'content.hint.banner': 'Dipaparkan dalam karusel laman utama; ketik untuk halaman butiran. Muat naik imej muka depan dan kandungan kaya seperti pengumuman laman rasmi.', + 'content.hint.announcement': 'Dipaparkan sebagai teks bergulir di bahagian atas aplikasi pemain. Isi tajuk dan teks bergulir setiap bahasa — teks biasa sahaja.', + 'content.section.publish': 'Tetapan terbitan', + 'content.section.content': 'Kandungan notifikasi', + 'content.field.publish_kind': 'Jenis terbitan', + 'content.publish_kind.notice': 'Notifikasi penuh (halaman butiran)', + 'content.publish_kind.ticker': 'Teks ticker sahaja', + 'content.publish_kind.hint': 'Notifikasi penuh muncul dalam senarai dan butiran; ticker hanya papar satu baris bergulir.', + 'content.field.cover_image': 'Imej muka depan', + 'content.field.ticker_title_ph': 'Tajuk ticker (pilihan)', + 'content.field.ticker_body_ph': 'Teks dipaparkan dalam ticker', + 'content.field.ticker_hint': 'Ticker hanya papar teks biasa — tiada imej atau format kaya.', + 'content.editor.placeholder': 'Tulis kandungan notifikasi; boleh sisip imej dan senarai…', + 'content.editor.insert_image': 'Sisip imej', + 'content.upload.cover_size_hint': 'Lebar disyorkan 860px+. Imej muka depan dan dalam kandungan akan menyesuaikan lebar pada pemain.', + 'content.upload.pick_media_title': 'Pilih imej', + 'content.upload.no_media': 'Tiada imej dalam pustaka — muat naik dahulu', 'content.status.DRAFT': 'Draf', 'content.status.ACTIVE': 'Aktif', 'content.status.INACTIVE': 'Tidak aktif', @@ -783,6 +815,9 @@ export const adminPagesMs: Record = { 'content.field.link_target': 'Sasaran pautan', 'content.field.start_time': 'Masa mula', 'content.field.end_time': 'Masa tamat', + 'content.field.notify_inbox': 'Notifikasi peti mesej', + 'content.field.notify_inbox_hint': 'Hantar mesej peti kepada semua pemain tentang promosi ini selepas simpan', + 'content.msg.notify_sent': 'Notifikasi dihantar kepada {count} pemain', 'content.field.title': 'Tajuk', 'content.field.title_ph': 'Pilihan', 'content.field.body': 'Kandungan', @@ -795,8 +830,6 @@ export const adminPagesMs: Record = { 'content.upload.size_error': 'Imej mestilah di bawah 5 MB', 'content.upload.remove': 'Buang imej', 'content.upload.pick_media': 'Pilih dari pustaka', - 'content.upload.pick_media_title': 'Pilih Imej Banner', - 'content.upload.no_media': 'Tiada imej banner dalam pustaka — muat naik dahulu', 'content.upload.url_placeholder': 'Atau tampal URL imej', 'content.upload.recommended_size': 'Saiz disyorkan: 860 x 360 px, atau imej nisbah 43:18. Karusel pemain memaparkan imej penuh dan mengisi ruang tambahan.', 'content.link.none': 'Tiada pautan', diff --git a/apps/admin/src/i18n/admin-pages.ts b/apps/admin/src/i18n/admin-pages.ts index 21a776b..412a84c 100644 --- a/apps/admin/src/i18n/admin-pages.ts +++ b/apps/admin/src/i18n/admin-pages.ts @@ -838,15 +838,33 @@ export const adminPagesZh: Record = { 'content.btn.enable': '启用', 'content.btn.disable': '停用', 'content.dialog.create': '新建公共内容', - 'content.dialog.edit': '编辑公共内容', + 'content.dialog.create_banner': '新建首页推广', + 'content.dialog.create_notice': '发布通知公告', + 'content.dialog.edit': '编辑内容', + 'content.dialog.edit_banner': '编辑首页推广', + 'content.dialog.edit_notice': '编辑通知公告', 'content.confirm_delete': '确定删除「{title}」?', - 'content.type.BANNER': '首页轮播', - 'content.type.ANNOUNCEMENT': '公告滚动', - 'content.hint.announcement': '显示在玩家端顶部跑马灯;标题与正文填一项即可,建议正文为主', + 'content.type.BANNER': '首页推广', + 'content.type.ANNOUNCEMENT': '通知公告', + 'content.hint.banner': '用于首页轮播展示,点击后进入通知详情页;请填写封面图与正文,像官网发布活动通知一样编辑。', + 'content.hint.announcement': '在玩家端顶部显示滚动跑马灯文字;填写各语言标题与滚动文案即可,纯文本无富文本。', + 'content.section.publish': '发布设置', + 'content.section.content': '通知内容', + 'content.field.publish_kind': '发布类型', + 'content.publish_kind.notice': '完整通知(详情页)', + 'content.publish_kind.ticker': '仅跑马灯文字', + 'content.publish_kind.hint': '完整通知会出现在公告列表与详情页;跑马灯仅显示顶部滚动一行文字。', + 'content.field.cover_image': '封面图', + 'content.field.ticker_title_ph': '跑马灯标题(选填)', + 'content.field.ticker_body_ph': '跑马灯显示的文字', + 'content.field.ticker_hint': '跑马灯只显示纯文字,不支持图片与富文本格式。', + 'content.editor.placeholder': '输入通知正文,可插入图片、列表等…', + 'content.editor.insert_image': '插入图片', + 'content.upload.cover_size_hint': '建议宽度 860px 以上;玩家端详情页会完整显示封面,正文内图片也会自适应宽度。', 'content.status.DRAFT': '草稿', 'content.status.ACTIVE': '已启用', 'content.status.INACTIVE': '已停用', - 'content.col.sort': '排序', + 'content.col.sort': '排序值', 'content.col.preview': '预览', 'content.col.title': '标题/摘要', 'content.col.player_visible': '玩家可见', @@ -857,7 +875,7 @@ export const adminPagesZh: Record = { 'content.field.start_time': '开始时间', 'content.field.end_time': '结束时间', 'content.field.title': '标题', - 'content.field.title_ph': '选填,可与正文相同', + 'content.field.title_ph': '通知标题,玩家端详情页展示', 'content.field.body': '正文', 'content.field.announce_text': '滚动文案', 'content.field.image_url': '图片地址', @@ -868,8 +886,8 @@ export const adminPagesZh: Record = { 'content.upload.size_error': '图片大小不能超过 5MB', 'content.upload.remove': '移除图片', 'content.upload.pick_media': '从媒体库选择', - 'content.upload.pick_media_title': '选择 Banner 图片', - 'content.upload.no_media': '媒体库中暂无 Banner 图片,请先上传', + 'content.upload.pick_media_title': '选择图片', + 'content.upload.no_media': '媒体库中暂无图片,请先上传', 'content.upload.url_placeholder': '或手动粘贴图片 URL', 'content.upload.recommended_size': '建议尺寸:860 x 360 px,或 43:18 同比例图片;前台会完整显示并自动填充不合比例区域。', 'content.link.none': '无跳转', @@ -1911,11 +1929,29 @@ export const adminPagesEn: Record = { 'content.btn.enable': 'Enable', 'content.btn.disable': 'Disable', 'content.dialog.create': 'New public content', - 'content.dialog.edit': 'Edit public content', + 'content.dialog.create_banner': 'New home promotion', + 'content.dialog.create_notice': 'Publish notification', + 'content.dialog.edit': 'Edit content', + 'content.dialog.edit_banner': 'Edit home promotion', + 'content.dialog.edit_notice': 'Edit notification', 'content.confirm_delete': 'Delete "{title}"?', - 'content.type.BANNER': 'Home banners', - 'content.type.ANNOUNCEMENT': 'Announcements', - 'content.hint.announcement': 'Shown in the player top marquee; fill title or body (body recommended)', + 'content.type.BANNER': 'Home promotions', + 'content.type.ANNOUNCEMENT': 'Notifications', + 'content.hint.banner': 'Shown in the home carousel; tapping opens the detail page. Add a cover image and rich body like an official site announcement.', + 'content.hint.announcement': 'Shows as a scrolling marquee at the top of the player app. Enter title and marquee text per language — plain text only.', + 'content.section.publish': 'Publish settings', + 'content.section.content': 'Notification content', + 'content.field.publish_kind': 'Publish type', + 'content.publish_kind.notice': 'Full notification (detail page)', + 'content.publish_kind.ticker': 'Ticker text only', + 'content.publish_kind.hint': 'Full notifications appear in the list and detail page; ticker-only shows one scrolling line at the top.', + 'content.field.cover_image': 'Cover image', + 'content.field.ticker_title_ph': 'Ticker title (optional)', + 'content.field.ticker_body_ph': 'Text shown in the marquee', + 'content.field.ticker_hint': 'Ticker shows plain text only — no images or rich formatting.', + 'content.editor.placeholder': 'Write the notification body; you can insert images and lists…', + 'content.editor.insert_image': 'Insert image', + 'content.upload.cover_size_hint': 'Recommended width 860px+. Cover and inline images scale to fit on the player detail page.', 'content.status.DRAFT': 'Draft', 'content.status.ACTIVE': 'Active', 'content.status.INACTIVE': 'Inactive', @@ -1930,7 +1966,7 @@ export const adminPagesEn: Record = { 'content.field.start_time': 'Start time', 'content.field.end_time': 'End time', 'content.field.title': 'Title', - 'content.field.title_ph': 'Optional; can match body', + 'content.field.title_ph': 'Title shown on the player detail page', 'content.field.body': 'Body', 'content.field.announce_text': 'Marquee text', 'content.field.image_url': 'Image URL', @@ -1941,8 +1977,8 @@ export const adminPagesEn: Record = { 'content.upload.size_error': 'Image must be under 5 MB', 'content.upload.remove': 'Remove image', 'content.upload.pick_media': 'Pick from library', - 'content.upload.pick_media_title': 'Select Banner Image', - 'content.upload.no_media': 'No banner images in library — upload one first', + 'content.upload.pick_media_title': 'Select image', + 'content.upload.no_media': 'No images in library — upload one first', 'content.upload.url_placeholder': 'Or paste image URL', 'content.upload.recommended_size': 'Recommended size: 860 x 360 px, or any 43:18 image. The player carousel keeps the full image visible and fills extra space.', 'content.link.none': 'No link', diff --git a/apps/admin/src/i18n/locale-loader.ts b/apps/admin/src/i18n/locale-loader.ts index b2de9c7..b801914 100644 --- a/apps/admin/src/i18n/locale-loader.ts +++ b/apps/admin/src/i18n/locale-loader.ts @@ -8,15 +8,17 @@ const loaders: Record Promise<{ default: Record>(); +const loaded = new Set(); /** 按语言动态加载文案包(仅当前 locale 进入主路径,其余为独立 chunk)。 */ export async function ensureAdminLocaleLoaded(locale: AdminLocale): Promise { - if (Object.keys(adminMessages[locale]).length > 0) return; + if (loaded.has(locale)) return; const pending = inflight.get(locale); if (pending) return pending; const task = loaders[locale]().then((mod) => { - adminMessages[locale] = mod.default; + Object.assign(adminMessages[locale], mod.default); + loaded.add(locale); }); inflight.set(locale, task); try { diff --git a/apps/admin/src/i18n/pages/en.ts b/apps/admin/src/i18n/pages/en.ts index c0f541e..a069a0a 100644 --- a/apps/admin/src/i18n/pages/en.ts +++ b/apps/admin/src/i18n/pages/en.ts @@ -27,6 +27,10 @@ const adminPages: Record = { 'user.filter.agent': 'Agent', 'user.filter.agent_ph': 'All', 'user.col.username': 'Username', + 'user.col.online': 'Online', + 'user.online_yes': 'Online now', + 'user.presence_online': 'Online', + 'user.presence_offline': 'Offline', 'user.col.agent': 'Agent', 'user.col.agent_cashback': 'Agent cashback', 'user.col.player_cashback': 'Player cashback', @@ -786,15 +790,43 @@ const adminPages: Record = { 'msg.outright_odds_saved': 'Outright odds saved', 'msg.load_failed': 'Load failed', + 'content.upload.pick_media_title': 'Select image', + 'content.upload.no_media': 'No images in library — upload one first', 'content.btn.create': 'New content', 'content.btn.enable': 'Enable', 'content.btn.disable': 'Disable', 'content.dialog.create': 'New public content', - 'content.dialog.edit': 'Edit public content', + 'content.dialog.create_banner': 'New home promotion', + 'content.dialog.create_notice': 'Publish notification', + 'content.dialog.edit': 'Edit content', + 'content.dialog.edit_banner': 'Edit home promotion', + 'content.dialog.edit_notice': 'Edit notification', 'content.confirm_delete': 'Delete "{title}"?', - 'content.type.BANNER': 'Home banners', - 'content.type.ANNOUNCEMENT': 'Announcements', - 'content.hint.announcement': 'Shown in the player top marquee; fill title or body (body recommended)', + 'content.type.BANNER': 'Home promotions', + 'content.type.ANNOUNCEMENT': 'Notifications', + 'content.type.INBOX_NOTIFY': 'Inbox notify', + 'content.inbox_notify.inbox_enabled': 'Player inbox', + 'content.inbox_notify.inbox_enabled_hint': 'When off, the player hub opens support only (no mailbox tab)', + 'content.inbox_notify.deposit': 'Deposit results', + 'content.inbox_notify.deposit_hint': 'Auto-send inbox messages on approve or reject', + 'content.inbox_notify.manual_title': 'Check "Inbox notify" when creating:', + 'content.inbox_notify.banner_note': 'Homepage promo', + 'content.inbox_notify.announcement_note': 'Announcements / ticker', + 'content.hint.banner': 'Shown in the home carousel; tapping opens the detail page. Add a cover image and rich body like an official site announcement.', + 'content.hint.announcement': 'Shows as a scrolling marquee at the top of the player app. Enter title and marquee text per language — plain text only.', + 'content.section.publish': 'Publish settings', + 'content.section.content': 'Notification content', + 'content.field.publish_kind': 'Publish type', + 'content.publish_kind.notice': 'Full notification (detail page)', + 'content.publish_kind.ticker': 'Ticker text only', + 'content.publish_kind.hint': 'Full notifications appear in the list and detail page; ticker-only shows one scrolling line at the top.', + 'content.field.cover_image': 'Cover image', + 'content.field.ticker_title_ph': 'Ticker title (optional)', + 'content.field.ticker_body_ph': 'Text shown in the marquee', + 'content.field.ticker_hint': 'Ticker shows plain text only — no images or rich formatting.', + 'content.editor.placeholder': 'Write the notification body; you can insert images and lists…', + 'content.editor.insert_image': 'Insert image', + 'content.upload.cover_size_hint': 'Recommended width 860px+. Cover and inline images scale to fit on the player detail page.', 'content.status.DRAFT': 'Draft', 'content.status.ACTIVE': 'Active', 'content.status.INACTIVE': 'Inactive', @@ -808,8 +840,11 @@ const adminPages: Record = { 'content.field.link_target': 'Link target', 'content.field.start_time': 'Start time', 'content.field.end_time': 'End time', + 'content.field.notify_inbox': 'Inbox notification', + 'content.field.notify_inbox_hint': 'Send an inbox message to all players about this promotion after saving', + 'content.msg.notify_sent': 'Inbox notification sent to {count} player(s)', 'content.field.title': 'Title', - 'content.field.title_ph': 'Optional; can match body', + 'content.field.title_ph': 'Title shown on the player detail page', 'content.field.body': 'Body', 'content.field.announce_text': 'Marquee text', 'content.field.image_url': 'Image URL', @@ -820,8 +855,6 @@ const adminPages: Record = { 'content.upload.size_error': 'Image must be under 5 MB', 'content.upload.remove': 'Remove image', 'content.upload.pick_media': 'Pick from library', - 'content.upload.pick_media_title': 'Select Banner Image', - 'content.upload.no_media': 'No banner images in library — upload one first', 'content.upload.url_placeholder': 'Or paste image URL', 'content.upload.recommended_size': 'Recommended size: 860 x 360 px, or any 43:18 image. The player carousel keeps the full image visible and fills extra space.', 'content.link.none': 'No link', diff --git a/apps/admin/src/i18n/pages/zh.ts b/apps/admin/src/i18n/pages/zh.ts index ac87909..dfd5ee7 100644 --- a/apps/admin/src/i18n/pages/zh.ts +++ b/apps/admin/src/i18n/pages/zh.ts @@ -28,6 +28,10 @@ const adminPages: Record = { 'user.filter.agent': '所属代理', 'user.filter.agent_ph': '全部', 'user.col.username': '用户名', + 'user.col.online': '在线', + 'user.online_yes': '当前在线', + 'user.presence_online': '在线', + 'user.presence_offline': '离线', 'user.col.agent': '所属代理', 'user.col.agent_cashback': '代理返水率', 'user.col.player_cashback': '玩家返水率', @@ -790,15 +794,43 @@ const adminPages: Record = { 'content.btn.enable': '启用', 'content.btn.disable': '停用', 'content.dialog.create': '新建公共内容', - 'content.dialog.edit': '编辑公共内容', + 'content.dialog.create_banner': '新建首页推广', + 'content.dialog.create_notice': '发布通知公告', + 'content.dialog.edit': '编辑内容', + 'content.dialog.edit_banner': '编辑首页推广', + 'content.dialog.edit_notice': '编辑通知公告', 'content.confirm_delete': '确定删除「{title}」?', - 'content.type.BANNER': '首页轮播', - 'content.type.ANNOUNCEMENT': '公告滚动', - 'content.hint.announcement': '显示在玩家端顶部跑马灯;标题与正文填一项即可,建议正文为主', + 'content.type.BANNER': '首页推广', + 'content.type.ANNOUNCEMENT': '通知公告', + 'content.type.INBOX_NOTIFY': '邮箱通知', + 'content.inbox_notify.inbox_enabled': '站内邮箱', + 'content.inbox_notify.inbox_enabled_hint': '关闭后玩家端入口直达客服,不展示邮箱标签页', + 'content.inbox_notify.deposit': '充值结果', + 'content.inbox_notify.deposit_hint': '审核通过或拒绝时自动发送站内信', + 'content.inbox_notify.manual_title': '以下类型在新建时勾选「邮箱通知」', + 'content.inbox_notify.banner_note': '首页推广', + 'content.inbox_notify.announcement_note': '通知公告 / 跑马灯', + 'content.hint.banner': '用于首页轮播展示,点击后进入通知详情页;请填写封面图与正文,像官网发布活动通知一样编辑。', + 'content.hint.announcement': '在玩家端顶部显示滚动跑马灯文字;填写各语言标题与滚动文案即可,纯文本无富文本。', + 'content.section.publish': '发布设置', + 'content.section.content': '通知内容', + 'content.field.publish_kind': '发布类型', + 'content.publish_kind.notice': '完整通知(详情页)', + 'content.publish_kind.ticker': '仅跑马灯文字', + 'content.publish_kind.hint': '完整通知会出现在公告列表与详情页;跑马灯仅显示顶部滚动一行文字。', + 'content.field.cover_image': '封面图', + 'content.field.ticker_title_ph': '跑马灯标题(选填)', + 'content.field.ticker_body_ph': '跑马灯显示的文字', + 'content.field.ticker_hint': '跑马灯只显示纯文字,不支持图片与富文本格式。', + 'content.editor.placeholder': '输入通知正文,可插入图片、列表等…', + 'content.editor.insert_image': '插入图片', + 'content.upload.cover_size_hint': '建议宽度 860px 以上;玩家端详情页会完整显示封面,正文内图片也会自适应宽度。', + 'content.upload.pick_media_title': '选择图片', + 'content.upload.no_media': '媒体库中暂无图片,请先上传', 'content.status.DRAFT': '草稿', 'content.status.ACTIVE': '已启用', 'content.status.INACTIVE': '已停用', - 'content.col.sort': '排序', + 'content.col.sort': '排序值', 'content.col.preview': '预览', 'content.col.title': '标题/摘要', 'content.col.player_visible': '玩家可见', @@ -808,8 +840,11 @@ const adminPages: Record = { 'content.field.link_target': '链接目标', 'content.field.start_time': '开始时间', 'content.field.end_time': '结束时间', + 'content.field.notify_inbox': '邮箱通知', + 'content.field.notify_inbox_hint': '保存后向全部玩家发送站内信,通知查看此推广', + 'content.msg.notify_sent': '已向 {count} 位玩家发送邮箱通知', 'content.field.title': '标题', - 'content.field.title_ph': '选填,可与正文相同', + 'content.field.title_ph': '通知标题,玩家端详情页展示', 'content.field.body': '正文', 'content.field.announce_text': '滚动文案', 'content.field.image_url': '图片地址', @@ -820,8 +855,6 @@ const adminPages: Record = { 'content.upload.size_error': '图片大小不能超过 5MB', 'content.upload.remove': '移除图片', 'content.upload.pick_media': '从媒体库选择', - 'content.upload.pick_media_title': '选择 Banner 图片', - 'content.upload.no_media': '媒体库中暂无 Banner 图片,请先上传', 'content.upload.url_placeholder': '或手动粘贴图片 URL', 'content.upload.recommended_size': '建议尺寸:860 x 360 px,或 43:18 同比例图片;前台会完整显示并自动填充不合比例区域。', 'content.link.none': '无跳转', diff --git a/apps/admin/src/layouts/ManageLayout.vue b/apps/admin/src/layouts/ManageLayout.vue index 387e766..5b6787e 100644 --- a/apps/admin/src/layouts/ManageLayout.vue +++ b/apps/admin/src/layouts/ManageLayout.vue @@ -9,6 +9,7 @@ import { AdminPerm } from '../constants/permissions'; import AdminLocaleSwitcher from '../components/AdminLocaleSwitcher.vue'; import AdminNavIcon from '../components/AdminNavIcon.vue'; import { resolveAdminBreadcrumb } from '../utils/admin-breadcrumb'; +import { useDepositPendingCount } from '../composables/useDepositPendingCount'; const route = useRoute(); const router = useRouter(); @@ -16,11 +17,18 @@ const auth = useAuthStore(); const { t } = useAdminLocale(); const { allowed: smokeTestsAllowed, ensureLoaded: ensureSmokeTestsAllowed } = useSmokeTestsAllowed(); const { hasPermission, role: adminRole } = usePermissions(); +const { pendingCount: depositPendingCount, startDepositPendingPolling, stopDepositPendingPolling } = + useDepositPendingCount(); + +const canSeeDepositPending = computed( + () => auth.isAdmin.value && hasPermission(AdminPerm.depositReview, AdminPerm.depositManage), +); const sidebarOpen = ref(false); const isMobileNav = ref(false); type AdminMenuItem = { + key: string; path: string; label: string; icon: string; @@ -34,23 +42,30 @@ function menuVisible(item: AdminMenuItem): boolean { const code = adminRole.value; if (item.path === '/smoke-tests' && smokeTestsAllowed.value === false) return false; if (code && code !== 'SUPER_ADMIN' && item.excludeRoles?.includes(code)) return false; - return hasPermission(...item.permissions); + if (!hasPermission(...item.permissions)) return false; + + const visible = auth.user.value?.visibleMenus; + if (visible) { + const list = visible.split(','); + return list.includes(item.key); + } + return true; } const adminMenus = computed(() => { const items: AdminMenuItem[] = [ - { path: '/', label: t('nav.dashboard'), icon: 'dashboard', matchPrefix: true, permissions: [AdminPerm.reports], excludeRoles: ['SUPPORT'] }, - { path: '/matches', label: t('nav.matches'), icon: 'matches', matchPrefix: true, permissions: [AdminPerm.matches] }, - { path: '/users', label: t('nav.agents_players'), icon: 'users', permissions: [AdminPerm.usersView, AdminPerm.agentsView] }, - { path: '/finance-logs', label: t('nav.finance_logs'), icon: 'finance', permissions: [AdminPerm.reports], excludeRoles: ['MATCH_ADMIN'] }, - { path: '/deposit', label: t('nav.deposit_manage'), icon: 'deposit', matchPrefix: true, permissions: [AdminPerm.depositManage, AdminPerm.depositReview] }, - { path: '/cashback', label: t('nav.cashback'), icon: 'cashback', permissions: [AdminPerm.cashback], excludeRoles: ['MATCH_ADMIN', 'SUPPORT'] }, - { path: '/bets', label: t('nav.bets'), icon: 'bets', permissions: [AdminPerm.bets] }, - { path: '/contents', label: t('nav.contents'), icon: 'contents', permissions: [AdminPerm.content] }, - { path: '/media', label: t('nav.media'), icon: 'media', permissions: [AdminPerm.content, AdminPerm.matches] }, - { path: '/audit', label: t('nav.audit'), icon: 'audit', permissions: [AdminPerm.audit], excludeRoles: ['MATCH_ADMIN'] }, - { path: '/staff', label: t('nav.staff'), icon: 'users', permissions: [AdminPerm.settings] }, - { path: '/smoke-tests', label: t('nav.smoke_tests'), icon: 'smoke-tests', permissions: [AdminPerm.settings] }, + { key: 'dashboard', path: '/', label: t('nav.dashboard'), icon: 'dashboard', matchPrefix: true, permissions: [AdminPerm.reports], excludeRoles: ['SUPPORT'] }, + { key: 'matches', path: '/matches', label: t('nav.matches'), icon: 'matches', matchPrefix: true, permissions: [AdminPerm.matches] }, + { key: 'users', path: '/users', label: t('nav.agents_players'), icon: 'users', permissions: [AdminPerm.usersView, AdminPerm.agentsView] }, + { key: 'finance-logs', path: '/finance-logs', label: t('nav.finance_logs'), icon: 'finance', permissions: [AdminPerm.reports], excludeRoles: ['MATCH_ADMIN'] }, + { key: 'deposit', path: '/deposit', label: t('nav.deposit_manage'), icon: 'deposit', matchPrefix: true, permissions: [AdminPerm.depositManage, AdminPerm.depositReview] }, + { key: 'cashback', path: '/cashback', label: t('nav.cashback'), icon: 'cashback', permissions: [AdminPerm.cashback], excludeRoles: ['MATCH_ADMIN', 'SUPPORT'] }, + { key: 'bets', path: '/bets', label: t('nav.bets'), icon: 'bets', permissions: [AdminPerm.bets] }, + { key: 'contents', path: '/contents', label: t('nav.contents'), icon: 'contents', permissions: [AdminPerm.content] }, + { key: 'media', path: '/media', label: t('nav.media'), icon: 'media', permissions: [AdminPerm.content, AdminPerm.matches] }, + { key: 'audit', path: '/audit', label: t('nav.audit'), icon: 'audit', permissions: [AdminPerm.audit], excludeRoles: ['MATCH_ADMIN'] }, + { key: 'staff', path: '/staff', label: t('nav.staff'), icon: 'users', permissions: [AdminPerm.settings] }, + { key: 'smoke-tests', path: '/smoke-tests', label: t('nav.smoke_tests'), icon: 'smoke-tests', permissions: [AdminPerm.settings] }, ]; return items.filter(menuVisible); }); @@ -149,11 +164,13 @@ onMounted(() => { window.addEventListener('resize', syncMobileNav); if (auth.isAdmin.value) { void ensureSmokeTestsAllowed(); + if (canSeeDepositPending.value) startDepositPendingPolling(); } }); onUnmounted(() => { window.removeEventListener('resize', syncMobileNav); + stopDepositPendingPolling(); }); watch(() => route.path, () => { @@ -177,6 +194,7 @@ watch(() => route.path, () => {
{{ t(sizeHintKey) }}