- MatchDetailView: replace StatusWatermark with top-right solid status tags, remove match-hero background image - ParlayPanel: replace StatusWatermark with corner tags, improve toggle button style and layout spacing, fix overflow clipping - Admin: wallet ledger dialog, agent manager, and player page refinements 🤖 Generated with [Qoder][https://qoder.com]
305 lines
8.8 KiB
Vue
305 lines
8.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue';
|
|
import { useAuthStore } from '../stores/auth';
|
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
|
import api from '../api';
|
|
import AdminTableEmpty from './AdminTableEmpty.vue';
|
|
import { formatAmount, formatAmountFull } from '../utils/format-amount';
|
|
import { walletTxTypeKey } from '../utils/walletTx';
|
|
|
|
interface WalletTxRow {
|
|
id: string;
|
|
transactionId: string;
|
|
transactionType: string;
|
|
amount: string;
|
|
balanceBefore: string;
|
|
balanceAfter: string;
|
|
frozenBefore: string;
|
|
frozenAfter: string;
|
|
betNo: string | null;
|
|
operatorUsername: string | null;
|
|
remark: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
const props = defineProps<{
|
|
modelValue: boolean;
|
|
playerId: string;
|
|
playerUsername?: string | null;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: boolean];
|
|
}>();
|
|
|
|
const { t, locale, localeTag } = useAdminLocale();
|
|
const auth = useAuthStore();
|
|
|
|
const visible = computed({
|
|
get: () => props.modelValue,
|
|
set: (v: boolean) => emit('update:modelValue', v),
|
|
});
|
|
|
|
const dialogTitle = computed(() => {
|
|
const name = props.playerUsername?.trim() || props.playerId;
|
|
return t('user.wallet_ledger_dialog_title', { name });
|
|
});
|
|
|
|
const walletApiPath = computed(() =>
|
|
auth.isAdmin.value ? '/admin/wallet/transactions' : '/agent/wallet/ledger-transactions',
|
|
);
|
|
|
|
const items = ref<WalletTxRow[]>([]);
|
|
const total = ref(0);
|
|
const page = ref(1);
|
|
const pageSize = ref(20);
|
|
const loading = ref(false);
|
|
const typeCategory = ref('');
|
|
const dateRange = ref<[Date, Date] | null>(null);
|
|
|
|
function walletTypeLabel(type: string) {
|
|
const key = walletTxTypeKey(type);
|
|
return key ? t(key) : 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',
|
|
});
|
|
}
|
|
|
|
function dateParams() {
|
|
if (!dateRange.value?.length) return {};
|
|
const [from, to] = dateRange.value;
|
|
const end = new Date(to);
|
|
end.setHours(23, 59, 59, 999);
|
|
return {
|
|
dateFrom: from.toISOString(),
|
|
dateTo: end.toISOString(),
|
|
};
|
|
}
|
|
|
|
async function load() {
|
|
if (!props.playerId) return;
|
|
loading.value = true;
|
|
try {
|
|
const { data } = await api.get(walletApiPath.value, {
|
|
params: {
|
|
page: page.value,
|
|
pageSize: pageSize.value,
|
|
playerId: props.playerId,
|
|
typeCategory: typeCategory.value || undefined,
|
|
...dateParams(),
|
|
},
|
|
});
|
|
items.value = (data.data?.items ?? []) as WalletTxRow[];
|
|
total.value = data.data?.total ?? 0;
|
|
} catch {
|
|
items.value = [];
|
|
total.value = 0;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function onSearch() {
|
|
page.value = 1;
|
|
void load();
|
|
}
|
|
|
|
function resetFilters() {
|
|
typeCategory.value = '';
|
|
dateRange.value = null;
|
|
page.value = 1;
|
|
}
|
|
|
|
watch(
|
|
() => [props.modelValue, props.playerId] as const,
|
|
([open, id]) => {
|
|
if (open && id) {
|
|
resetFilters();
|
|
void load();
|
|
}
|
|
},
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<el-dialog
|
|
v-model="visible"
|
|
:title="dialogTitle"
|
|
width="980px"
|
|
destroy-on-close
|
|
class="player-wallet-ledger-dialog"
|
|
append-to-body
|
|
>
|
|
<el-form inline class="ledger-filter">
|
|
<el-form-item :label="t('finance.filter.date_range')">
|
|
<el-date-picker
|
|
v-model="dateRange"
|
|
type="daterange"
|
|
:start-placeholder="t('common.to')"
|
|
:end-placeholder="t('common.to')"
|
|
value-format="YYYY-MM-DD"
|
|
style="width: 240px"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item :label="t('finance.filter.type_category')">
|
|
<el-select v-model="typeCategory" clearable :placeholder="t('finance.filter.type_category_all')" style="width: 120px">
|
|
<el-option :label="t('finance.filter.type_category_deposit')" value="deposit" />
|
|
<el-option :label="t('finance.filter.type_category_bet')" value="bet" />
|
|
<el-option :label="t('finance.filter.type_category_cashback')" value="cashback" />
|
|
</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 class="table-wrap">
|
|
<el-table v-loading="loading" :key="`${locale}-${playerId}`" :data="items" stripe max-height="420">
|
|
<template #empty>
|
|
<AdminTableEmpty />
|
|
</template>
|
|
<el-table-column :label="t('audit.col.time')" min-width="150">
|
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('finance.col.tx_id')" min-width="120" show-overflow-tooltip>
|
|
<template #default="{ row }">{{ row.transactionId }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('agent.col.credit_type')" min-width="88">
|
|
<template #default="{ row }">{{ walletTypeLabel(row.transactionType) }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('finance.col.balance_change')" min-width="96" 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('finance.col.balance_before')" min-width="92" align="right">
|
|
<template #default="{ row }">
|
|
<el-tooltip :content="formatAmountFull(row.balanceBefore)" placement="top">
|
|
<span>{{ formatAmount(row.balanceBefore) }}</span>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('finance.col.balance_after')" min-width="92" align="right">
|
|
<template #default="{ row }">
|
|
<el-tooltip :content="formatAmountFull(row.balanceAfter)" placement="top">
|
|
<span>{{ formatAmount(row.balanceAfter) }}</span>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('finance.col.frozen_before')" min-width="92" align="right">
|
|
<template #default="{ row }">
|
|
<el-tooltip :content="formatAmountFull(row.frozenBefore)" placement="top">
|
|
<span>{{ formatAmount(row.frozenBefore) }}</span>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('finance.col.frozen_after')" min-width="92" align="right">
|
|
<template #default="{ row }">
|
|
<el-tooltip :content="formatAmountFull(row.frozenAfter)" placement="top">
|
|
<span>{{ formatAmount(row.frozenAfter) }}</span>
|
|
</el-tooltip>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('finance.col.reference')" min-width="110" show-overflow-tooltip>
|
|
<template #default="{ row }">
|
|
<router-link
|
|
v-if="row.betNo"
|
|
:to="{ path: '/bets', query: { keyword: row.betNo } }"
|
|
class="bet-link"
|
|
@click="visible = false"
|
|
>
|
|
{{ row.betNo }}
|
|
</router-link>
|
|
<span v-else>—</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('agent.credit_tx.col.operator')" min-width="88">
|
|
<template #default="{ row }">{{ row.operatorUsername ?? '—' }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('user.field.remark')" min-width="100" show-overflow-tooltip>
|
|
<template #default="{ row }">{{ row.remark ?? '—' }}</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]"
|
|
layout="total, sizes, prev, pager, next"
|
|
@current-change="() => load()"
|
|
@size-change="() => { page = 1; load(); }"
|
|
/>
|
|
</div>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.ledger-filter {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.ledger-filter :deep(.el-form-item) {
|
|
margin-bottom: 0;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.table-wrap {
|
|
margin: 0 -2px;
|
|
}
|
|
|
|
.pager {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.amt-pos {
|
|
color: var(--el-color-success);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.amt-neg {
|
|
color: var(--el-color-danger);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.bet-link {
|
|
color: var(--el-color-primary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.bet-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|
|
|
|
<style>
|
|
.player-wallet-ledger-dialog .el-dialog__header {
|
|
padding: 12px 16px 6px;
|
|
margin-right: 0;
|
|
}
|
|
|
|
.player-wallet-ledger-dialog .el-dialog__body {
|
|
padding: 10px 12px;
|
|
}
|
|
|
|
.player-wallet-ledger-dialog .el-dialog__headerbtn {
|
|
top: 10px;
|
|
}
|
|
</style>
|