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>
196 lines
5.6 KiB
Vue
196 lines
5.6 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, watch } from 'vue';
|
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
|
import { useAuditLabels } from '../utils/audit-labels';
|
|
import api from '../api';
|
|
import AdminTableEmpty from './AdminTableEmpty.vue';
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
endpoint?: string;
|
|
showModuleColumn?: boolean;
|
|
showModuleFilter?: boolean;
|
|
}>(),
|
|
{
|
|
endpoint: '/admin/audit-logs',
|
|
showModuleColumn: true,
|
|
showModuleFilter: true,
|
|
},
|
|
);
|
|
|
|
const { t, locale, localeTag } = useAdminLocale();
|
|
const { auditActionLabel, auditModuleLabel } = useAuditLabels();
|
|
|
|
interface AuditRow {
|
|
action: string;
|
|
module: string;
|
|
targetId: string | null;
|
|
operatorId: string | null;
|
|
operatorUsername: string | null;
|
|
operatorRole: string | null;
|
|
operatorUserType: string | null;
|
|
operatorType: string;
|
|
ipAddress: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
const logs = ref<AuditRow[]>([]);
|
|
const total = ref(0);
|
|
const page = ref(1);
|
|
const pageSize = ref(10);
|
|
const filterModule = ref('');
|
|
|
|
onMounted(load);
|
|
|
|
watch(
|
|
() => props.endpoint,
|
|
() => {
|
|
page.value = 1;
|
|
load();
|
|
},
|
|
);
|
|
|
|
async function load() {
|
|
const { data } = await api.get(props.endpoint, {
|
|
params: {
|
|
page: page.value,
|
|
pageSize: pageSize.value,
|
|
module: props.showModuleFilter && filterModule.value.trim()
|
|
? filterModule.value.trim()
|
|
: undefined,
|
|
},
|
|
});
|
|
logs.value = (data.data.items ?? []) as AuditRow[];
|
|
total.value = data.data.total ?? 0;
|
|
}
|
|
|
|
function onPageChange(p: number) {
|
|
page.value = p;
|
|
load();
|
|
}
|
|
|
|
function onSizeChange(size: number) {
|
|
pageSize.value = size;
|
|
page.value = 1;
|
|
load();
|
|
}
|
|
|
|
function formatTime(v: string) {
|
|
return new Date(v).toLocaleString(localeTag.value, {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
});
|
|
}
|
|
|
|
function operatorRoleLabel(row: AuditRow): string | null {
|
|
const code = row.operatorRole;
|
|
if (code === 'SUPER_ADMIN') return t('role.super_admin');
|
|
if (code === 'MATCH_ADMIN') return t('role.match_admin');
|
|
if (code === 'FINANCE_ADMIN') return t('role.finance_admin');
|
|
if (code === 'SUPPORT') return t('role.support');
|
|
if (row.operatorUserType === 'ADMIN' || row.operatorType === 'ADMIN') return t('role.admin');
|
|
if (row.operatorUserType === 'AGENT' || row.operatorType === 'AGENT') return t('role.agent');
|
|
if (row.operatorUserType === 'PLAYER' || row.operatorType === 'PLAYER') return t('audit.operator_player');
|
|
return null;
|
|
}
|
|
|
|
function operatorDisplay(row: AuditRow): string {
|
|
if (row.operatorUsername) return row.operatorUsername;
|
|
if (row.operatorType === 'SYSTEM') return t('audit.operator_system');
|
|
return '—';
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<el-card v-if="showModuleFilter" class="filter-card" shadow="never">
|
|
<el-form inline>
|
|
<el-form-item :label="t('common.module')">
|
|
<el-input
|
|
v-model="filterModule"
|
|
:placeholder="t('audit.module_ph')"
|
|
clearable
|
|
style="width: 160px"
|
|
@keyup.enter="load"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" @click="load">{{ t('common.search') }}</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</el-card>
|
|
|
|
<el-card class="data-card" shadow="never">
|
|
<div class="table-wrap">
|
|
<el-table :key="locale" :data="logs" stripe>
|
|
<template #empty>
|
|
<AdminTableEmpty />
|
|
</template>
|
|
<el-table-column type="index" :index="(i: number) => (page - 1) * pageSize + i + 1" :label="t('common.seq')" width="70" align="center" fixed="left" />
|
|
<el-table-column :label="t('audit.col.time')" min-width="168" fixed="left">
|
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('audit.col.operator')" min-width="160">
|
|
<template #default="{ row }">
|
|
<div class="operator-cell">
|
|
<span class="operator-name">{{ operatorDisplay(row) }}</span>
|
|
<span v-if="operatorRoleLabel(row)" class="operator-role">{{ operatorRoleLabel(row) }}</span>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('audit.col.action')" min-width="140">
|
|
<template #default="{ row }">{{ auditActionLabel(row.action) }}</template>
|
|
</el-table-column>
|
|
<el-table-column
|
|
v-if="showModuleColumn"
|
|
:label="t('audit.col.module')"
|
|
width="120"
|
|
>
|
|
<template #default="{ row }">{{ auditModuleLabel(row.module) }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="targetId" :label="t('audit.col.target_id')" min-width="100" show-overflow-tooltip />
|
|
<el-table-column :label="t('audit.col.ip')" min-width="120" show-overflow-tooltip>
|
|
<template #default="{ row }">{{ row.ipAddress ?? '—' }}</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
<div class="pager">
|
|
<el-pagination
|
|
v-model:current-page="page"
|
|
v-model:page-size="pageSize"
|
|
:total="total"
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
layout="total, sizes, prev, pager, next"
|
|
background
|
|
@current-change="onPageChange"
|
|
@size-change="onSizeChange"
|
|
/>
|
|
</div>
|
|
</el-card>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.filter-card { border-radius: 12px; margin-bottom: 16px; }
|
|
.data-card { border-radius: 12px; }
|
|
|
|
.operator-cell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.operator-name {
|
|
font-weight: 500;
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
|
|
.operator-role {
|
|
font-size: 12px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
</style>
|