- 阶段 A: 增加 KeepAlive 缓存高频列表页,改用 onActivated 进行后台静默刷新,优化 tab 切换与 HomeEntry 闪屏 - 阶段 B: beforeEach 守卫在 TTL 内走同步快速路径,api 请求拦截器缓存 token 避免重复解析 JWT - 阶段 C: 引入 unplugin 插件启用 Element Plus 组件按需加载,清理 App.vue 中 960+ 行冗余暗色主题 CSS - i18n 拆包: 将三语文案包提取为独立懒加载 chunks,主包体积从 209KB 减少至 99KB (降低 53%)
493 lines
18 KiB
Vue
493 lines
18 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onActivated, watch } from 'vue';
|
|
|
|
defineOptions({ name: 'AdminFinanceLogs' });
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useAuthStore } from '../stores/auth';
|
|
import { useAdminLocale } from '../composables/useAdminLocale';
|
|
import api from '../api';
|
|
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
|
|
import { formatAmount, formatAmountFull } from '../utils/format-amount';
|
|
import { walletTxTypeKey } from '../utils/walletTx';
|
|
const { t, locale, localeTag } = useAdminLocale();
|
|
const auth = useAuthStore();
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
const activeTab = ref<'credit' | 'transfer'>('credit');
|
|
|
|
interface CreditTxRow {
|
|
id: string;
|
|
agentId: string;
|
|
agentUsername: string | null;
|
|
transactionType: string;
|
|
amount: string;
|
|
creditBefore: string;
|
|
creditAfter: string;
|
|
operatorUsername: string | null;
|
|
requestId: string | null;
|
|
remark: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
interface TransferTxRow {
|
|
id: string;
|
|
transactionId: string;
|
|
playerId: string;
|
|
playerUsername: string | null;
|
|
parentAgentId: string | null;
|
|
parentAgentUsername: string | null;
|
|
transactionType: string;
|
|
amount: string;
|
|
balanceBefore: string;
|
|
balanceAfter: string;
|
|
operatorUsername: string | null;
|
|
remark: string | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
const creditItems = ref<CreditTxRow[]>([]);
|
|
const creditTotal = ref(0);
|
|
const creditPage = ref(1);
|
|
const creditPageSize = ref(10);
|
|
|
|
const transferItems = ref<TransferTxRow[]>([]);
|
|
const transferTotal = ref(0);
|
|
const transferPage = ref(1);
|
|
const transferPageSize = ref(10);
|
|
|
|
const keyword = ref('');
|
|
const agentId = ref('');
|
|
const playerKeyword = ref('');
|
|
const parentAgentKeyword = ref('');
|
|
const operatorKeyword = ref('');
|
|
const transactionType = ref('');
|
|
const dateRange = ref<[Date, Date] | null>(null);
|
|
|
|
const creditApiPath = computed(() =>
|
|
auth.isAdmin.value ? '/admin/agents/credit-transactions' : '/agent/credit-transactions',
|
|
);
|
|
const transferApiPath = computed(() =>
|
|
auth.isAdmin.value ? '/admin/wallet/transfer-transactions' : '/agent/wallet-transactions',
|
|
);
|
|
|
|
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 transferTypeLabel(type: string) {
|
|
const key = walletTxTypeKey(type);
|
|
return key ? t(key) : type;
|
|
}
|
|
|
|
const TRANSFER_REMARK_KEYS: Record<string, string> = {
|
|
'Agent deposit': 'finance.remark.agent_deposit',
|
|
'Agent withdraw': 'finance.remark.agent_withdraw',
|
|
'代理上分': 'finance.remark.agent_deposit',
|
|
'代理下分': 'finance.remark.agent_withdraw',
|
|
'管理员上分': 'finance.remark.admin_deposit',
|
|
'管理员下分': 'finance.remark.admin_withdraw',
|
|
'开户初始余额': 'finance.remark.initial_balance',
|
|
};
|
|
|
|
function transferRemarkLabel(remark: string | null | undefined, transactionType: string) {
|
|
const raw = remark?.trim();
|
|
if (!raw) {
|
|
if (transactionType === 'MANUAL_DEPOSIT') return t('finance.remark.agent_deposit');
|
|
if (transactionType === 'MANUAL_WITHDRAW') return t('finance.remark.agent_withdraw');
|
|
return '—';
|
|
}
|
|
const key = TRANSFER_REMARK_KEYS[raw];
|
|
return key ? t(key) : raw;
|
|
}
|
|
|
|
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 loadCredit() {
|
|
const { data } = await api.get(creditApiPath.value, {
|
|
params: {
|
|
page: creditPage.value,
|
|
pageSize: creditPageSize.value,
|
|
keyword: keyword.value.trim() || undefined,
|
|
agentId: agentId.value.trim() || undefined,
|
|
operatorKeyword: operatorKeyword.value.trim() || undefined,
|
|
transactionType: transactionType.value || undefined,
|
|
...dateParams(),
|
|
},
|
|
});
|
|
creditItems.value = (data.data?.items ?? []) as CreditTxRow[];
|
|
creditTotal.value = data.data?.total ?? 0;
|
|
}
|
|
|
|
async function loadTransfer() {
|
|
const parentRaw = parentAgentKeyword.value.trim();
|
|
const parentIsId = parentRaw && /^\d+$/.test(parentRaw);
|
|
const { data } = await api.get(transferApiPath.value, {
|
|
params: {
|
|
page: transferPage.value,
|
|
pageSize: transferPageSize.value,
|
|
keyword: playerKeyword.value.trim() || undefined,
|
|
...(parentIsId ? { parentAgentId: parentRaw } : parentRaw ? { parentAgentKeyword: parentRaw } : {}),
|
|
operatorKeyword: operatorKeyword.value.trim() || undefined,
|
|
transactionType: transactionType.value || undefined,
|
|
...dateParams(),
|
|
},
|
|
});
|
|
transferItems.value = (data.data?.items ?? []) as TransferTxRow[];
|
|
transferTotal.value = data.data?.total ?? 0;
|
|
}
|
|
|
|
function onSearch() {
|
|
if (activeTab.value === 'credit') {
|
|
creditPage.value = 1;
|
|
void loadCredit();
|
|
} else {
|
|
transferPage.value = 1;
|
|
void loadTransfer();
|
|
}
|
|
}
|
|
|
|
function onTabChange(tab: string | number | boolean) {
|
|
const next = tab === 'transfer' ? 'transfer' : 'credit';
|
|
activeTab.value = next;
|
|
transactionType.value = '';
|
|
void router.replace({ query: { ...route.query, tab: next } });
|
|
if (next === 'credit' && !creditItems.value.length) void loadCredit();
|
|
if (next === 'transfer' && !transferItems.value.length) void loadTransfer();
|
|
}
|
|
|
|
function openAgentFilter(id: string) {
|
|
activeTab.value = 'credit';
|
|
agentId.value = id;
|
|
keyword.value = '';
|
|
creditPage.value = 1;
|
|
void router.replace({ query: { tab: 'credit', agentId: id } });
|
|
void loadCredit();
|
|
}
|
|
|
|
function openParentAgentFilter(id: string) {
|
|
activeTab.value = 'transfer';
|
|
parentAgentKeyword.value = id;
|
|
playerKeyword.value = '';
|
|
transferPage.value = 1;
|
|
void router.replace({ query: { tab: 'transfer', parentAgentId: id } });
|
|
void loadTransfer();
|
|
}
|
|
|
|
onMounted(() => {
|
|
const tab = route.query.tab;
|
|
if (tab === 'transfer') activeTab.value = 'transfer';
|
|
const qAgent = route.query.agentId;
|
|
if (typeof qAgent === 'string' && qAgent.trim()) agentId.value = qAgent.trim();
|
|
const qParent = route.query.parentAgentId;
|
|
if (typeof qParent === 'string' && qParent.trim()) parentAgentKeyword.value = qParent.trim();
|
|
if (activeTab.value === 'credit') void loadCredit();
|
|
else void loadTransfer();
|
|
});
|
|
// KeepAlive 激活时静默刷新
|
|
onActivated(() => {
|
|
if (activeTab.value === 'credit' && creditItems.value.length > 0) void loadCredit();
|
|
else if (activeTab.value === 'transfer' && transferItems.value.length > 0) void loadTransfer();
|
|
});
|
|
|
|
watch(
|
|
() => route.query.agentId,
|
|
(q) => {
|
|
const next = typeof q === 'string' ? q.trim() : '';
|
|
if (next !== agentId.value) {
|
|
agentId.value = next;
|
|
if (activeTab.value === 'credit') {
|
|
creditPage.value = 1;
|
|
void loadCredit();
|
|
}
|
|
}
|
|
},
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="admin-list-page finance-logs">
|
|
<el-tabs v-model="activeTab" class="finance-tabs" @tab-change="onTabChange">
|
|
<el-tab-pane :label="t('finance.tab.credit')" name="credit" />
|
|
<el-tab-pane :label="t('finance.tab.transfer')" name="transfer" />
|
|
</el-tabs>
|
|
|
|
<el-card class="filter-card" shadow="never">
|
|
<el-form inline>
|
|
<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>
|
|
|
|
<template v-if="activeTab === 'credit'">
|
|
<el-form-item :label="t('common.keyword')">
|
|
<el-input
|
|
v-model="keyword"
|
|
:placeholder="t('agent.credit_tx.filter_agent_ph')"
|
|
clearable
|
|
style="width: 160px"
|
|
@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: 120px"
|
|
@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: 110px">
|
|
<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>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<el-form-item :label="t('finance.col.player')">
|
|
<el-input
|
|
v-model="playerKeyword"
|
|
:placeholder="t('finance.filter.player_ph')"
|
|
clearable
|
|
style="width: 160px"
|
|
@keyup.enter="onSearch"
|
|
/>
|
|
</el-form-item>
|
|
<el-form-item :label="t('finance.col.parent_agent')">
|
|
<el-input
|
|
v-model="parentAgentKeyword"
|
|
:placeholder="t('finance.filter.parent_agent_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: 110px">
|
|
<el-option :label="t('finance.tx.deposit')" value="MANUAL_DEPOSIT" />
|
|
<el-option :label="t('finance.tx.admin_deposit')" value="ADMIN_DEPOSIT" />
|
|
<el-option :label="t('finance.tx.agent_deposit')" value="AGENT_DEPOSIT" />
|
|
<el-option :label="t('finance.tx.player_deposit')" value="PLAYER_DEPOSIT" />
|
|
<el-option :label="t('finance.tx.withdraw')" value="MANUAL_WITHDRAW" />
|
|
<el-option :label="t('finance.tx.admin_withdraw')" value="ADMIN_WITHDRAW" />
|
|
<el-option :label="t('finance.tx.agent_withdraw')" value="AGENT_WITHDRAW" />
|
|
</el-select>
|
|
</el-form-item>
|
|
</template>
|
|
|
|
<el-form-item :label="t('agent.credit_tx.col.operator')">
|
|
<el-input
|
|
v-model="operatorKeyword"
|
|
:placeholder="t('finance.filter.operator_ph')"
|
|
clearable
|
|
style="width: 140px"
|
|
@keyup.enter="onSearch"
|
|
/>
|
|
</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 v-show="activeTab === 'credit'" class="data-card" shadow="never">
|
|
<div class="table-wrap">
|
|
<el-table :key="`${locale}-credit`" :data="creditItems" stripe>
|
|
<template #empty>
|
|
<AdminTableEmpty />
|
|
</template>
|
|
<el-table-column type="index" :index="(i: number) => (creditPage - 1) * creditPageSize + i + 1" :label="t('common.seq')" width="70" align="center" />
|
|
<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="110">
|
|
<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 :label="t('finance.tx.request_id')" min-width="120" show-overflow-tooltip>
|
|
<template #default="{ row }">{{ row.requestId ?? '—' }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="remark" :label="t('user.field.remark')" min-width="120" show-overflow-tooltip />
|
|
</el-table>
|
|
</div>
|
|
<div class="pager">
|
|
<el-pagination
|
|
v-model:current-page="creditPage"
|
|
v-model:page-size="creditPageSize"
|
|
:total="creditTotal"
|
|
:page-sizes="[10, 20, 50]"
|
|
layout="total, sizes, prev, pager, next"
|
|
@current-change="() => loadCredit()"
|
|
@size-change="() => { creditPage = 1; loadCredit(); }"
|
|
/>
|
|
</div>
|
|
</el-card>
|
|
|
|
<el-card v-show="activeTab === 'transfer'" class="data-card" shadow="never">
|
|
<div class="table-wrap">
|
|
<el-table :key="`${locale}-transfer`" :data="transferItems" stripe>
|
|
<template #empty>
|
|
<AdminTableEmpty />
|
|
</template>
|
|
<el-table-column type="index" :index="(i: number) => (transferPage - 1) * transferPageSize + i + 1" :label="t('common.seq')" width="70" align="center" />
|
|
<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('finance.col.tx_id')" min-width="130" show-overflow-tooltip>
|
|
<template #default="{ row }">{{ row.transactionId }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('finance.col.player')" min-width="110">
|
|
<template #default="{ row }">{{ row.playerUsername ?? row.playerId }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('finance.col.parent_agent')" min-width="110">
|
|
<template #default="{ row }">
|
|
<el-button
|
|
v-if="row.parentAgentUsername && row.parentAgentId"
|
|
link
|
|
type="primary"
|
|
@click="openParentAgentFilter(row.parentAgentId!)"
|
|
>
|
|
{{ row.parentAgentUsername }}
|
|
</el-button>
|
|
<span v-else-if="row.parentAgentId">{{ row.parentAgentId }}</span>
|
|
<span v-else>—</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('agent.col.credit_type')" width="72">
|
|
<template #default="{ row }">{{ transferTypeLabel(row.transactionType) }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('finance.col.balance_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('finance.col.balance_before')" width="108" 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')" width="108" 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('agent.credit_tx.col.operator')" min-width="100">
|
|
<template #default="{ row }">{{ row.operatorUsername ?? '—' }}</template>
|
|
</el-table-column>
|
|
<el-table-column :label="t('user.field.remark')" min-width="120" show-overflow-tooltip>
|
|
<template #default="{ row }">{{ transferRemarkLabel(row.remark, row.transactionType) }}</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
<div class="pager">
|
|
<el-pagination
|
|
v-model:current-page="transferPage"
|
|
v-model:page-size="transferPageSize"
|
|
:total="transferTotal"
|
|
:page-sizes="[10, 20, 50]"
|
|
layout="total, sizes, prev, pager, next"
|
|
@current-change="() => loadTransfer()"
|
|
@size-change="() => { transferPage = 1; loadTransfer(); }"
|
|
/>
|
|
</div>
|
|
</el-card>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.finance-tabs {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.finance-tabs :deep(.el-tabs__header) {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.amt-pos {
|
|
color: var(--el-color-success);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.amt-neg {
|
|
color: var(--el-color-danger);
|
|
font-weight: 600;
|
|
}
|
|
</style>
|