feat(player/admin/api): 站内邮箱、在线状态、员工菜单权限与内容管理增强
API: - 新增 player-messages 域:充值审核通过/拒绝、Banner/公告推广消息,支持多语言模板 - 新增 presence 域:Redis 心跳在线状态,管理端可查询在线玩家数 - User 表增加 visible_menus 字段;新增 player_messages 表及迁移 - 充值审核通过/拒绝时按系统配置自动写入玩家站内消息 - 管理端新增 GET /deposit-orders/pending-count、GET /presence/online-count - 玩家端新增消息 CRUD、presence/ping、home 返回 inbox 开关配置 - 员工管理支持 visibleMenus 配置与删除保护(不能删自己/最后超管) - SystemConfig 增加 inbox 功能开关及各类通知开关 Admin: - 员工管理:按角色默认菜单 + 可勾选可见菜单项 - ManageLayout:按 visibleMenus 过滤侧栏;充值待审数量角标轮询 - Contents:富文本编辑器、图片字段组件重构 - DashboardPlayers:展示在线玩家数;AdminPlayerStatusCell 在线状态列 - 多页面 i18n 与权限细节调整 Player: - 站内邮箱中心(InboxHub):消息列表/详情、未读角标、一键已读/删除 - 公告列表与详情页;走马灯可跳转详情 - 客服 Modal 改为 Panel,与邮箱 Hub 整合 - 充值状态轮询通知;presence 心跳;BetSlip 清空二次确认 - HomeView 今日赛事板块;FootballView 等体验优化 Shared: 新增 CANNOT_DELETE_SELF、STAFF_NOT_FOUND、MESSAGE_NOT_FOUND 等错误码 Docs: 玩家端缺失功能分析文档 Chore: 移除 .agents/skills 设计类 skill 文件 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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, () => {
|
||||
<aside class="sidebar" :class="{ open: sidebarOpen }">
|
||||
<div class="brand">
|
||||
<img src="/logo.png" alt="TheBet365" class="brand-logo" />
|
||||
<span class="brand-title">{{ isAdminPortal ? t('portal.admin') : t('portal.agent') }}</span>
|
||||
</div>
|
||||
|
||||
<nav class="nav">
|
||||
@@ -197,7 +215,16 @@ watch(() => route.path, () => {
|
||||
@click="onNavClick"
|
||||
>
|
||||
<AdminNavIcon :name="m.icon" />
|
||||
<span class="nav-label">{{ m.label }}</span>
|
||||
<span class="nav-label">
|
||||
{{ m.label }}
|
||||
<span
|
||||
v-if="'path' in m && m.path === '/deposit' && depositPendingCount > 0"
|
||||
class="nav-pending-badge"
|
||||
:title="t('deposit.pending_badge', { n: depositPendingCount })"
|
||||
>
|
||||
{{ depositPendingCount > 99 ? '99+' : depositPendingCount }}
|
||||
</span>
|
||||
</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
@@ -245,7 +272,6 @@ watch(() => route.path, () => {
|
||||
</div>
|
||||
</div>
|
||||
<AdminLocaleSwitcher />
|
||||
<div class="portal-tag">{{ isAdminPortal ? t('portal.admin') : t('portal.agent') }}</div>
|
||||
<button class="btn-logout" @click="logout">{{ t('logout') }}</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -285,17 +311,26 @@ watch(() => route.path, () => {
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.brand-logo {
|
||||
max-width: 118px;
|
||||
max-height: 34px;
|
||||
max-width: 52px;
|
||||
max-height: 32px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.brand-title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
color: #f0f0f0;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.nav {
|
||||
@@ -324,8 +359,26 @@ watch(() => route.path, () => {
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.nav-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
line-height: 1.25;
|
||||
flex: 1;
|
||||
}
|
||||
.nav-pending-badge {
|
||||
flex-shrink: 0;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: #f56c6c;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.25);
|
||||
}
|
||||
.nav-item:hover {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
@@ -584,14 +637,23 @@ watch(() => route.path, () => {
|
||||
.brand {
|
||||
height: 64px;
|
||||
min-height: 64px;
|
||||
padding: 0 16px;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
max-width: 132px;
|
||||
max-height: 38px;
|
||||
max-width: 56px;
|
||||
max-height: 34px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
color: #2a2824;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.nav {
|
||||
|
||||
Reference in New Issue
Block a user