Files
thebet365/apps/admin/src/views/agent/AgentDirectPlayersView.vue
Mars cffad00652 sync(theme-4): 同步 main 最新 API/Admin 与玩家端逻辑
从 main 同步站内信手动发送、媒体库选择、列表子路由重构等 API/Admin 改动;玩家端仅合并 ADMIN_CUSTOM 富文本、消息预览与充值截图压缩逻辑,保留 theme-4 样式。
2026-06-22 14:12:40 +08:00

234 lines
7.6 KiB
Vue

<script setup lang="ts">
import { ref, computed, inject, onMounted, onUnmounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useAdminLocale } from '../../composables/useAdminLocale';
import {
agentDirectPlayersReloadKey,
agentPlayerActionsKey,
} from '../../composables/agent-direct-players-context';
import api from '../../api';
import AdminSubNav from '../../components/AdminSubNav.vue';
import AdminTableWrap from '../../components/AdminTableWrap.vue';
import AdminTableEmpty from '../../components/AdminTableEmpty.vue';
import AdminPlayerStatusCell from '../../components/AdminPlayerStatusCell.vue';
import AdminPlayerRowActions from '../../components/AdminPlayerRowActions.vue';
import { formatAmount, formatAmountFull } from '../../utils/format-amount';
import type { PlayerRow } from '../user-form';
defineOptions({ name: 'AdminAgentDirectPlayersView' });
const route = useRoute();
const { t } = useAdminLocale();
const actions = inject(agentPlayerActionsKey);
const reloadRef = inject(agentDirectPlayersReloadKey);
const agentId = computed(() => String(route.params.agentId ?? ''));
const agentUsername = computed(() => {
const name = String(route.query.username ?? '').trim();
return name || `#${agentId.value}`;
});
const players = ref<PlayerRow[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = ref(20);
const keyword = ref('');
const filterStatus = ref('');
const loading = ref(false);
const pageTitle = computed(() =>
t('agent.direct_players_title', { name: agentUsername.value }),
);
async function loadPlayers() {
if (!agentId.value) return;
loading.value = true;
try {
const { data } = await api.get('/admin/users', {
params: {
parentId: agentId.value,
page: page.value,
pageSize: pageSize.value,
keyword: keyword.value.trim() || undefined,
status: filterStatus.value || undefined,
},
});
players.value = (data.data.items ?? []) as PlayerRow[];
total.value = data.data.total ?? 0;
} finally {
loading.value = false;
}
}
function onSearch() {
page.value = 1;
void loadPlayers();
}
function onPageChange(p: number) {
page.value = p;
void loadPlayers();
}
function onSizeChange(size: number) {
pageSize.value = size;
page.value = 1;
void loadPlayers();
}
watch(agentId, () => {
page.value = 1;
void loadPlayers();
});
onMounted(() => {
if (reloadRef) reloadRef.value = () => void loadPlayers();
void loadPlayers();
});
onUnmounted(() => {
if (reloadRef) reloadRef.value = null;
});
</script>
<template>
<div class="admin-list-page agent-direct-players-page">
<AdminSubNav :title="pageTitle" :subtitle="t('agent.direct_players')" />
<section class="list-panel player-list-panel">
<div class="list-panel-toolbar">
<el-form inline class="list-chrome__grow">
<el-form-item :label="t('common.keyword')">
<el-input
v-model="keyword"
:placeholder="t('user.filter.username_ph')"
clearable
style="width: 180px"
@keyup.enter="onSearch"
/>
</el-form-item>
<el-form-item :label="t('common.status')">
<el-select v-model="filterStatus" :placeholder="t('common.all')" clearable style="width: 120px">
<el-option :label="t('user.status.ACTIVE')" value="ACTIVE" />
<el-option :label="t('user.status.SUSPENDED')" value="SUSPENDED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSearch">{{ t('common.search') }}</el-button>
</el-form-item>
</el-form>
<div v-if="actions?.canCreatePlayer" class="list-chrome__actions">
<el-button type="primary" @click="actions?.openCreatePlayer(agentId)">
{{ t('user.create_btn') }}
</el-button>
</div>
</div>
<AdminTableWrap>
<el-table v-loading="loading" :data="players" stripe class="inner-table">
<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"
/>
<el-table-column prop="username" :label="t('user.col.username')" min-width="120" />
<el-table-column :label="t('common.status')" min-width="128">
<template #default="{ row }">
<AdminPlayerStatusCell :status="row.status" :is-online="row.isOnline" />
</template>
</el-table-column>
<el-table-column :label="t('user.col.invite_code')" min-width="96" show-overflow-tooltip>
<template #default="{ row }">
<code v-if="row.inviteCode" class="invite-code-cell">{{ row.inviteCode }}</code>
<span v-else></span>
</template>
</el-table-column>
<el-table-column :label="t('user.col.balance')" min-width="120" align="right">
<template #default="{ row }">
<el-tooltip
:content="`${formatAmountFull(row.availableBalance)} / ${formatAmountFull(row.frozenBalance)}`"
placement="top"
>
<span class="amount-compact">
{{ formatAmount(row.availableBalance) }} / {{ formatAmount(row.frozenBalance) }}
</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="betCount" :label="t('user.col.bets')" width="56" align="center" />
<el-table-column :label="t('user.col.stake_payout')" min-width="100" align="right">
<template #default="{ row }">
<span class="amount-compact">
{{ formatAmount(row.totalStake) }} / {{ formatAmount(row.totalReturn) }}
</span>
</template>
</el-table-column>
<el-table-column v-if="actions" :label="t('common.actions')" min-width="320" align="center">
<template #default="{ row }">
<AdminPlayerRowActions
v-bind="actions.playerActionFlags"
:row="row"
@detail="actions.openDetailPlayer(row.id)"
@ledger="actions.openPlayerWalletLedger(row.id, row.username)"
@edit="actions.openEditPlayer(row.id)"
@deposit="actions.openTransfer('deposit', row)"
@withdraw="actions.openTransfer('withdraw', row)"
@freeze="actions.toggleFreezePlayer(row)"
@delete="actions.deletePlayer(row)"
/>
</template>
</el-table-column>
</el-table>
</AdminTableWrap>
<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>
</section>
</div>
</template>
<style scoped>
.agent-direct-players-page {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.agent-direct-players-page :deep(.admin-subnav) {
margin-bottom: 10px;
}
.agent-direct-players-page .list-panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.agent-direct-players-page .player-list-panel :deep(.admin-table-wrap) {
flex: 1;
min-height: 0;
}
.agent-direct-players-page .inner-table {
height: 100%;
}
</style>