- 钱包首页、明细、交易详情适配 Pinnacle 浅色主题 - WalletStatsPanel 统计面板布局与样式更新 - 流水金额展示统一使用 txAmountClass / txDisplayType
589 lines
6.3 KiB
Vue
589 lines
6.3 KiB
Vue
<script setup lang="ts">
|
||
|
||
import { ref, onActivated, onMounted } from 'vue';
|
||
|
||
import { useRouter } from 'vue-router';
|
||
|
||
import { useI18n } from 'vue-i18n';
|
||
|
||
import api from '../api';
|
||
|
||
import { formatMoney } from '../utils/localeDisplay';
|
||
|
||
import { txTypeKey, txDisplayType, txAmountClass, txSummaryLabel } from '../utils/walletTx';
|
||
|
||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||
|
||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||
|
||
|
||
|
||
const router = useRouter();
|
||
|
||
const { t, locale } = useI18n();
|
||
|
||
|
||
|
||
type Transaction = {
|
||
|
||
transactionType: string;
|
||
|
||
displayType?: string;
|
||
|
||
summary?: string | null;
|
||
|
||
summaryKind?: 'opening_bonus' | null;
|
||
|
||
referenceType?: string | null;
|
||
|
||
amount: string;
|
||
|
||
frozenBefore?: string;
|
||
|
||
frozenAfter?: string;
|
||
|
||
createdAt: string;
|
||
|
||
transactionId: string;
|
||
|
||
};
|
||
|
||
|
||
|
||
const items = ref<Transaction[]>([]);
|
||
|
||
const loading = ref(true);
|
||
|
||
const PREVIEW_COUNT = 15;
|
||
|
||
|
||
|
||
function txLabel(tx: Transaction): string {
|
||
|
||
const key = txTypeKey(txDisplayType(tx));
|
||
|
||
if (key) {
|
||
|
||
const translated = t(key);
|
||
|
||
if (translated !== key) return translated;
|
||
|
||
}
|
||
|
||
return tx.transactionType;
|
||
|
||
}
|
||
|
||
|
||
|
||
function txSubtitle(tx: Transaction): string {
|
||
|
||
return txSummaryLabel(tx, t);
|
||
|
||
}
|
||
|
||
|
||
|
||
function goDetail(tx: Transaction) {
|
||
|
||
if (!tx.transactionId) return;
|
||
|
||
router.push(`/wallet/transactions/${tx.transactionId}`);
|
||
|
||
}
|
||
|
||
|
||
|
||
function goCashbacks() {
|
||
|
||
router.push('/wallet/cashbacks');
|
||
|
||
}
|
||
|
||
|
||
|
||
async function fetchData() {
|
||
|
||
loading.value = true;
|
||
|
||
try {
|
||
|
||
const { data } = await api.get('/player/wallet/transactions', { params: { page: 1 } });
|
||
|
||
const result = data.data ?? { items: [] };
|
||
|
||
items.value = (result.items ?? []).slice(0, PREVIEW_COUNT);
|
||
|
||
} catch {
|
||
|
||
/* ignore */
|
||
|
||
} finally {
|
||
|
||
loading.value = false;
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
const { pullDistance, spinning, progress } = usePullToRefresh({
|
||
|
||
onRefresh: fetchData,
|
||
|
||
});
|
||
|
||
|
||
|
||
onMounted(fetchData);
|
||
|
||
onActivated(fetchData);
|
||
|
||
|
||
|
||
const pullIndicatorStyle = () => ({
|
||
|
||
height: `${pullDistance.value}px`,
|
||
|
||
opacity: Math.min(pullDistance.value / 48, 1),
|
||
|
||
});
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<template>
|
||
|
||
<div class="wallet-page">
|
||
|
||
<div
|
||
|
||
class="pull-indicator"
|
||
|
||
:style="pullIndicatorStyle()"
|
||
|
||
>
|
||
|
||
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div v-if="loading" class="state">
|
||
|
||
<GoldSpinner :size="36" />
|
||
|
||
<span class="loading-text">{{ t('bet.loading') }}</span>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<template v-else>
|
||
|
||
<div class="wallet-links">
|
||
|
||
<button type="button" class="wallet-link" @click="goCashbacks">
|
||
|
||
{{ t('wallet.view_cashbacks') }}
|
||
|
||
</button>
|
||
|
||
<button
|
||
|
||
v-if="items.length"
|
||
|
||
type="button"
|
||
|
||
class="wallet-link wallet-link--primary"
|
||
|
||
@click="router.push('/wallet/detail')"
|
||
|
||
>
|
||
|
||
{{ t('wallet.view_all') }}
|
||
|
||
</button>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div v-if="items.length" class="tx-list">
|
||
|
||
<button
|
||
|
||
v-for="tx in items"
|
||
|
||
:key="tx.transactionId"
|
||
|
||
type="button"
|
||
|
||
class="tx-row"
|
||
|
||
@click="goDetail(tx)"
|
||
|
||
>
|
||
|
||
<div class="tx-col-left">
|
||
|
||
<span class="tx-type">{{ txLabel(tx) }}</span>
|
||
|
||
<span v-if="txSubtitle(tx)" class="tx-summary">{{ txSubtitle(tx) }}</span>
|
||
|
||
<span class="tx-time">{{ new Date(tx.createdAt).toLocaleString() }}</span>
|
||
|
||
</div>
|
||
|
||
<div class="tx-col-right">
|
||
|
||
<span :class="txAmountClass(tx.amount)">
|
||
|
||
{{ formatMoney(tx.amount, locale) }}
|
||
|
||
</span>
|
||
|
||
<span class="tx-arrow" aria-hidden="true">›</span>
|
||
|
||
</div>
|
||
|
||
</button>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div v-if="!items.length" class="empty">
|
||
|
||
{{ t('wallet.no_records') }}
|
||
|
||
</div>
|
||
|
||
</template>
|
||
|
||
</div>
|
||
|
||
</template>
|
||
|
||
|
||
|
||
<style scoped>
|
||
|
||
.wallet-page {
|
||
|
||
padding-bottom: 24px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.pull-indicator {
|
||
|
||
display: flex;
|
||
|
||
align-items: center;
|
||
|
||
justify-content: center;
|
||
|
||
overflow: hidden;
|
||
|
||
transition: height 0.15s ease;
|
||
|
||
}
|
||
|
||
|
||
|
||
.state {
|
||
|
||
display: flex;
|
||
|
||
flex-direction: column;
|
||
|
||
align-items: center;
|
||
|
||
gap: 12px;
|
||
|
||
text-align: center;
|
||
|
||
color: var(--text-muted);
|
||
|
||
padding: 56px 20px;
|
||
|
||
font-weight: 500;
|
||
|
||
font-size: 14px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.loading-text {
|
||
|
||
font-size: 13px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.wallet-links {
|
||
|
||
display: flex;
|
||
|
||
align-items: center;
|
||
|
||
justify-content: space-between;
|
||
|
||
gap: 12px;
|
||
|
||
margin: 0 0 12px;
|
||
|
||
}
|
||
|
||
|
||
|
||
.wallet-link {
|
||
|
||
background: none;
|
||
|
||
border: none;
|
||
|
||
padding: 4px 0;
|
||
|
||
font-size: 13px;
|
||
|
||
font-weight: 500;
|
||
|
||
color: var(--text-muted);
|
||
|
||
cursor: pointer;
|
||
|
||
}
|
||
|
||
|
||
|
||
.wallet-link--primary {
|
||
|
||
color: var(--text);
|
||
|
||
font-weight: 600;
|
||
|
||
}
|
||
|
||
|
||
|
||
.wallet-link:active {
|
||
|
||
opacity: 0.72;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tx-list {
|
||
|
||
margin-bottom: 0;
|
||
|
||
background: var(--gradient-card);
|
||
|
||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||
|
||
border-radius: var(--radius);
|
||
|
||
overflow: hidden;
|
||
|
||
box-shadow: var(--shadow-card-sm);
|
||
|
||
}
|
||
|
||
|
||
|
||
.tx-row {
|
||
|
||
display: flex;
|
||
|
||
align-items: center;
|
||
|
||
justify-content: space-between;
|
||
|
||
gap: 12px;
|
||
|
||
width: 100%;
|
||
|
||
text-align: left;
|
||
|
||
font-size: 14px;
|
||
|
||
padding: 14px 16px;
|
||
|
||
border: none;
|
||
|
||
border-bottom: 1px solid var(--border);
|
||
|
||
background: transparent;
|
||
|
||
cursor: pointer;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tx-row:last-child {
|
||
|
||
border-bottom: none;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tx-row:active {
|
||
|
||
background: var(--bg-hover);
|
||
|
||
}
|
||
|
||
|
||
|
||
.tx-col-left {
|
||
|
||
display: flex;
|
||
|
||
flex-direction: column;
|
||
|
||
gap: 3px;
|
||
|
||
min-width: 0;
|
||
|
||
flex: 1;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tx-col-right {
|
||
|
||
display: flex;
|
||
|
||
align-items: center;
|
||
|
||
gap: 6px;
|
||
|
||
flex-shrink: 0;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tx-summary {
|
||
|
||
font-size: 12px;
|
||
|
||
color: var(--text-dim);
|
||
|
||
font-weight: 500;
|
||
|
||
white-space: nowrap;
|
||
|
||
overflow: hidden;
|
||
|
||
text-overflow: ellipsis;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tx-type {
|
||
|
||
font-weight: 600;
|
||
|
||
color: var(--text);
|
||
|
||
letter-spacing: -0.01em;
|
||
|
||
}
|
||
|
||
|
||
|
||
.pos {
|
||
|
||
color: var(--success);
|
||
|
||
font-weight: 600;
|
||
|
||
font-size: 14px;
|
||
|
||
font-variant-numeric: tabular-nums;
|
||
|
||
}
|
||
|
||
|
||
|
||
.neg {
|
||
|
||
color: var(--text);
|
||
|
||
font-weight: 600;
|
||
|
||
font-size: 14px;
|
||
|
||
font-variant-numeric: tabular-nums;
|
||
|
||
}
|
||
|
||
|
||
|
||
.zero {
|
||
|
||
color: var(--text-muted);
|
||
|
||
font-weight: 600;
|
||
|
||
font-size: 14px;
|
||
|
||
font-variant-numeric: tabular-nums;
|
||
|
||
}
|
||
|
||
|
||
|
||
.tx-time {
|
||
|
||
font-size: 11px;
|
||
|
||
color: var(--text-dim);
|
||
|
||
}
|
||
|
||
|
||
|
||
.tx-arrow {
|
||
|
||
font-size: 15px;
|
||
|
||
color: var(--text-dim);
|
||
|
||
font-weight: 400;
|
||
|
||
line-height: 1;
|
||
|
||
}
|
||
|
||
|
||
|
||
.empty {
|
||
|
||
text-align: center;
|
||
|
||
color: var(--text-muted);
|
||
|
||
padding: 40px 16px;
|
||
|
||
font-weight: 500;
|
||
|
||
}
|
||
|
||
</style>
|
||
|