feat: add smoke tests, agent credit ledger, and player cashback page
Introduce admin smoke-test suite with API probes, agent credit transaction history, and player cashback records; fix SmokeTestModule DI and polish admin/player UI assets. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
223
apps/admin/src/views/AgentCreditTransactions.vue
Normal file
223
apps/admin/src/views/AgentCreditTransactions.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<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(20);
|
||||
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 :key="locale" :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>
|
||||
Reference in New Issue
Block a user