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:
2026-06-17 17:51:10 +08:00
parent e9a23de935
commit f9343b00af
105 changed files with 6960 additions and 7523 deletions

View File

@@ -77,6 +77,7 @@ import { formatRatePercent, percentToDecimalRate, decimalRateToPercent } from '.
import InviteCodePanel from '../components/InviteCodePanel.vue';
import InviteManageDialog from '../components/InviteManageDialog.vue';
import AdminTableWrap from '../components/AdminTableWrap.vue';
import AdminPlayerStatusCell from '../components/AdminPlayerStatusCell.vue';
import AdminAgentRowActions from '../components/AdminAgentRowActions.vue';
import AdminPlayerRowActions from '../components/AdminPlayerRowActions.vue';
import AdminDetailGrid from '../components/AdminDetailGrid.vue';
@@ -94,7 +95,7 @@ const inviteDialogOpen = ref(false);
const tier1Agents = ref<AgentRow[]>([]);
const tier1Total = ref(0);
const tier1Page = ref(1);
const tier1PageSize = ref(20);
const tier1PageSize = ref(10);
const tier1Keyword = ref('');
const tier1FilterStatus = ref('');
@@ -117,7 +118,7 @@ function ensureSubAgentState(level: number): SubAgentLevelState {
agents: [],
total: 0,
page: 1,
pageSize: 20,
pageSize: 10,
keyword: '',
filterStatus: '',
};
@@ -166,7 +167,7 @@ const activeViewTab = ref('players');
const allPlayers = ref<PlayerRow[]>([]);
const playerTotal = ref(0);
const playerPage = ref(1);
const playerPageSize = ref(20);
const playerPageSize = ref(10);
const playerKeyword = ref('');
const playerFilterStatus = ref('');
const playerFilterAgent = ref('');
@@ -1557,11 +1558,11 @@ function creditTypeLabel(type: string) {
<template #empty>
<AdminTableEmpty />
</template>
<el-table-column prop="id" label="ID" width="72" />
<el-table-column type="index" :index="(i: number) => (playerPage - 1) * playerPageSize + i + 1" :label="t('common.seq')" width="70" align="center" />
<el-table-column prop="username" :label="t('user.col.username')" min-width="120" />
<el-table-column :label="t('common.status')" width="88">
<el-table-column :label="t('common.status')" min-width="128">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
<AdminPlayerStatusCell :status="row.status" :is-online="row.isOnline" />
</template>
</el-table-column>
<el-table-column :label="t('user.col.agent')" min-width="200">
@@ -1671,19 +1672,19 @@ function creditTypeLabel(type: string) {
</div>
<el-table :data="getPlayers(row.userId)" stripe class="inner-table">
<template #empty><AdminTableEmpty /></template>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column type="index" :label="t('common.seq')" width="55" align="center" />
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" />
<el-table-column :label="t('common.status')" min-width="120">
<template #default="{ row: player }">
<AdminPlayerStatusCell :status="player.status" :is-online="player.isOnline" />
</template>
</el-table-column>
<el-table-column :label="t('user.col.invite_code')" min-width="96" show-overflow-tooltip>
<template #default="{ row: player }">
<code v-if="player.inviteCode" class="invite-code-cell">{{ player.inviteCode }}</code>
<span v-else></span>
</template>
</el-table-column>
<el-table-column :label="t('common.status')" width="80">
<template #default="{ row: player }">
<el-tag :type="statusTagType(player.status)" size="small">{{ statusLabel(player.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('user.col.balance')" min-width="120" align="right">
<template #default="{ row: player }">
<el-tooltip :content="`${formatAmountFull(player.availableBalance)} / ${formatAmountFull(player.frozenBalance)}`" placement="top">
@@ -1717,8 +1718,7 @@ function creditTypeLabel(type: string) {
</div>
</template>
</el-table-column>
<el-table-column prop="userId" label="ID" min-width="64" />
<el-table-column type="index" :index="(i: number) => (tier1Page - 1) * tier1PageSize + i + 1" :label="t('common.seq')" width="70" align="center" />
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" show-overflow-tooltip />
<el-table-column :label="t('common.status')" min-width="72">
<template #default="{ row }">
@@ -1837,19 +1837,19 @@ function creditTypeLabel(type: string) {
</div>
<el-table :data="getPlayers(row.userId)" stripe class="inner-table">
<template #empty><AdminTableEmpty /></template>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column type="index" :label="t('common.seq')" width="55" align="center" />
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" />
<el-table-column :label="t('common.status')" min-width="120">
<template #default="{ row: player }">
<AdminPlayerStatusCell :status="player.status" :is-online="player.isOnline" />
</template>
</el-table-column>
<el-table-column :label="t('user.col.invite_code')" min-width="96" show-overflow-tooltip>
<template #default="{ row: player }">
<code v-if="player.inviteCode" class="invite-code-cell">{{ player.inviteCode }}</code>
<span v-else></span>
</template>
</el-table-column>
<el-table-column :label="t('common.status')" width="80">
<template #default="{ row: player }">
<el-tag :type="statusTagType(player.status)" size="small">{{ statusLabel(player.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('user.col.balance')" min-width="120" align="right">
<template #default="{ row: player }">
<span class="amount-compact">{{ formatAmount(player.availableBalance) }} / {{ formatAmount(player.frozenBalance) }}</span>
@@ -1875,7 +1875,7 @@ function creditTypeLabel(type: string) {
</div>
</template>
</el-table-column>
<el-table-column prop="userId" label="ID" min-width="64" />
<el-table-column type="index" :index="(i: number) => (ensureSubAgentState(agentLevel).page - 1) * ensureSubAgentState(agentLevel).pageSize + i + 1" :label="t('common.seq')" width="70" align="center" />
<el-table-column prop="username" :label="t('user.col.username')" min-width="100" show-overflow-tooltip />
<el-table-column :label="t('agent.col.parent_chain')" min-width="120" show-overflow-tooltip>
<template #default="{ row }">{{ parentChainLabel(row) }}</template>