Files
thebet365/apps/player/src/views/WalletView.vue
Mars 713739475b feat(player): theme-2 钱包页与流水展示优化
- 钱包首页、明细、交易详情适配 Pinnacle 浅色主题

- WalletStatsPanel 统计面板布局与样式更新

- 流水金额展示统一使用 txAmountClass / txDisplayType
2026-06-23 15:56:53 +08:00

589 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>