Files
thebet365/apps/admin/src/views/AgentCreditTransactions.vue
Mars f9343b00af 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>
2026-06-17 17:51:10 +08:00

224 lines
6.8 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAdminLocale } from '../composables/useAdminLocale';
import api from '../api';
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
import { formatAmount, formatAmountFull } from '../utils/format-amount';
const { t, locale, localeTag } = useAdminLocale();
const route = useRoute();
const router = useRouter();
interface CreditTxRow {
id: string;
agentId: string;
agentUsername: string | null;
transactionType: string;
amount: string;
creditBefore: string;
creditAfter: string;
operatorUsername: string | null;
remark: string | null;
createdAt: string;
}
const items = ref<CreditTxRow[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(10);
const keyword = ref('');
const agentId = ref('');
const transactionType = ref('');
function creditTypeLabel(type: string) {
if (type === 'CREDIT_INCREASE') return t('agent.credit.increase');
if (type === 'CREDIT_DECREASE') return t('agent.credit.decrease');
return type;
}
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',
});
}
async function load() {
const { data } = await api.get('/admin/agents/credit-transactions', {
params: {
page: page.value,
pageSize: pageSize.value,
keyword: keyword.value.trim() || undefined,
agentId: agentId.value.trim() || undefined,
transactionType: transactionType.value || undefined,
},
});
items.value = (data.data?.items ?? []) as CreditTxRow[];
total.value = data.data?.total ?? 0;
}
function onSearch() {
page.value = 1;
void load();
}
function onPageChange(p: number) {
page.value = p;
void load();
}
function onSizeChange(size: number) {
pageSize.value = size;
page.value = 1;
void load();
}
function openAgentFilter(id: string) {
agentId.value = id;
keyword.value = '';
page.value = 1;
void router.replace({ query: { agentId: id } });
void load();
}
onMounted(() => {
const q = route.query.agentId;
if (typeof q === 'string' && q.trim()) {
agentId.value = q.trim();
}
void load();
});
watch(
() => route.query.agentId,
(q) => {
const next = typeof q === 'string' ? q.trim() : '';
if (next !== agentId.value) {
agentId.value = next;
page.value = 1;
void load();
}
},
);
</script>
<template>
<div class="admin-list-page">
<el-card class="filter-card" shadow="never">
<el-form inline>
<el-form-item :label="t('common.keyword')">
<el-input
v-model="keyword"
:placeholder="t('agent.credit_tx.filter_agent_ph')"
clearable
style="width: 180px"
@keyup.enter="onSearch"
/>
</el-form-item>
<el-form-item :label="t('agent.credit_tx.filter_agent_id')">
<el-input
v-model="agentId"
:placeholder="t('agent.credit_tx.filter_agent_id_ph')"
clearable
style="width: 140px"
@keyup.enter="onSearch"
/>
</el-form-item>
<el-form-item :label="t('agent.col.credit_type')">
<el-select v-model="transactionType" clearable :placeholder="t('common.all')" style="width: 120px">
<el-option :label="t('agent.credit.increase')" value="CREDIT_INCREASE" />
<el-option :label="t('agent.credit.decrease')" value="CREDIT_DECREASE" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">{{ 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 :data="items" stripe>
<template #empty>
<AdminTableEmpty />
</template>
<el-table-column :label="t('audit.col.time')" min-width="158">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column :label="t('user.col.username')" min-width="120">
<template #default="{ row }">
<el-button
v-if="row.agentUsername"
link
type="primary"
@click="openAgentFilter(row.agentId)"
>
{{ row.agentUsername }}
</el-button>
<span v-else>{{ row.agentId }}</span>
</template>
</el-table-column>
<el-table-column :label="t('agent.col.credit_type')" width="88">
<template #default="{ row }">{{ creditTypeLabel(row.transactionType) }}</template>
</el-table-column>
<el-table-column :label="t('agent.col.credit_change')" width="108" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.amount)" placement="top">
<span :class="parseFloat(row.amount) >= 0 ? 'amt-pos' : 'amt-neg'">
{{ formatAmount(row.amount) }}
</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('agent.col.credit_before')" width="108" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.creditBefore)" placement="top">
<span>{{ formatAmount(row.creditBefore) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('agent.col.credit_after')" width="108" align="right">
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.creditAfter)" placement="top">
<span>{{ formatAmount(row.creditAfter) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('agent.credit_tx.col.operator')" min-width="100">
<template #default="{ row }">{{ row.operatorUsername ?? '—' }}</template>
</el-table-column>
<el-table-column prop="remark" :label="t('user.field.remark')" min-width="140" show-overflow-tooltip />
</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]"
layout="total, sizes, prev, pager, next"
@current-change="onPageChange"
@size-change="onSizeChange"
/>
</div>
</el-card>
</div>
</template>
<style scoped>
.amt-pos {
color: var(--el-color-success);
font-weight: 600;
}
.amt-neg {
color: var(--el-color-danger);
font-weight: 600;
}
</style>