从 main 同步站内信手动发送、媒体库选择、列表子路由重构等 API/Admin 改动;玩家端仅合并 ADMIN_CUSTOM 富文本、消息预览与充值截图压缩逻辑,保留 theme-4 样式。
234 lines
7.6 KiB
Vue
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>
|