404 lines
11 KiB
Vue
404 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted, onActivated, computed } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useRouter } from 'vue-router';
|
|
import api from '../../api';
|
|
import GoldSpinner from '../../components/GoldSpinner.vue';
|
|
import LocaleFlag from '../../components/LocaleFlag.vue';
|
|
import { useAuthStore } from '../../stores/auth';
|
|
import { useAppLocale } from '../../composables/useAppLocale';
|
|
import { usePlayerProfile } from '../../composables/usePlayerProfile';
|
|
import { useInboxFeature } from '../../composables/useInboxFeature';
|
|
import { formatMoney } from '../../utils/localeDisplay';
|
|
import { parseCashbackApiData, sumCashbackAmount } from '../../utils/cashback';
|
|
|
|
const { t, locale } = useI18n();
|
|
const router = useRouter();
|
|
const auth = useAuthStore();
|
|
const { locales, setLocale, initFromUser } = useAppLocale();
|
|
const { profileRaw, refreshProfile } = usePlayerProfile();
|
|
const { hubRoute } = useInboxFeature();
|
|
|
|
const loading = ref(true);
|
|
const error = ref(false);
|
|
const cashbackTotal = ref('0');
|
|
|
|
async function fetchProfile() {
|
|
loading.value = true;
|
|
error.value = false;
|
|
try {
|
|
await refreshProfile();
|
|
initFromUser(profileRaw.value?.locale);
|
|
void fetchCashbackTotal();
|
|
} catch {
|
|
error.value = true;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function fetchCashbackTotal() {
|
|
try {
|
|
const { data } = await api.get('/player/cashbacks');
|
|
cashbackTotal.value = sumCashbackAmount(parseCashbackApiData(data.data)).toString();
|
|
} catch {
|
|
try {
|
|
const { data } = await api.get('/player/wallet/transactions/stats');
|
|
const byType = data.data?.byType ?? [];
|
|
let sum = 0;
|
|
for (const g of byType) {
|
|
if (['CASHBACK', 'CASHBACK_DEPOSIT'].includes(g.transactionType?.toUpperCase())) {
|
|
sum += Math.abs(parseFloat(g.totalAmount ?? '0'));
|
|
}
|
|
}
|
|
cashbackTotal.value = sum.toString();
|
|
} catch { /* ignore */ }
|
|
}
|
|
}
|
|
|
|
onMounted(fetchProfile);
|
|
onActivated(fetchProfile);
|
|
|
|
function logout() {
|
|
auth.logout();
|
|
router.push('/');
|
|
}
|
|
|
|
const balanceDisplay = computed(() =>
|
|
formatMoney(profileRaw.value?.wallet?.availableBalance, locale.value),
|
|
);
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div class="desktop-account-page">
|
|
<main class="account-main">
|
|
<div class="profile-layout">
|
|
<div class="page-header">
|
|
<h1 class="page-title">{{ t('nav.profile') }}</h1>
|
|
</div>
|
|
|
|
<div v-if="loading" class="loading-state">
|
|
<GoldSpinner :size="36" />
|
|
</div>
|
|
|
|
<div v-else-if="error" class="error-state">
|
|
<p>{{ t('common.load_failed') }}</p>
|
|
<button type="button" class="retry-btn" @click="fetchProfile">{{ t('common.retry') }}</button>
|
|
</div>
|
|
|
|
<template v-else>
|
|
<!-- Wallet Banner -->
|
|
<div class="wallet-banner">
|
|
<div class="card-overlay">
|
|
<div class="bank-card-top">
|
|
<div class="bank-card-top-left">
|
|
<img src="/logo.png" alt="TheBet365" class="brand-logo" />
|
|
<div class="bank-card-brand-text">
|
|
<span class="brand-name">THEBET365</span>
|
|
<span class="bank-card-type">MEMBER</span>
|
|
</div>
|
|
</div>
|
|
<router-link to="/wallet/recharge" class="recharge-btn">
|
|
<span class="recharge-icon">+</span>
|
|
<span>{{ t('recharge.title') }}</span>
|
|
</router-link>
|
|
</div>
|
|
|
|
<div class="bank-card-balance">
|
|
<span class="bank-card-label">{{ t('wallet.available') || '可用余额' }}</span>
|
|
<p class="bank-card-number">{{ balanceDisplay }}</p>
|
|
</div>
|
|
|
|
<div class="bank-card-footer">
|
|
<div class="bank-card-field">
|
|
<span class="bank-card-label">{{ t('profile.username') || '持卡人' }}</span>
|
|
<span class="bank-card-holder">{{ profileRaw?.username }}</span>
|
|
</div>
|
|
<div class="bank-card-field bank-card-field--center">
|
|
<span class="bank-card-label">{{ t('wallet.cashback_total') || '累计返水' }}</span>
|
|
<span class="bank-card-stat">{{ formatMoney(cashbackTotal, locale) }}</span>
|
|
</div>
|
|
<div class="bank-card-field bank-card-field--right">
|
|
<span class="bank-card-label">{{ t('wallet.frozen') || '冻结' }}</span>
|
|
<span class="bank-card-stat">{{ formatMoney(profileRaw?.wallet?.frozenBalance, locale) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Account info -->
|
|
<div class="info-card">
|
|
<div class="info-header">{{ t('profile.account') || '账户信息' }}</div>
|
|
<div class="info-row">
|
|
<span class="info-label">{{ t('profile.username') || '用户名' }}</span>
|
|
<span class="info-val">{{ profileRaw?.username }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="info-label">{{ t('profile.phone') || '手机号' }}</span>
|
|
<span class="info-val">{{ profileRaw?.preferences?.phone || '-' }}</span>
|
|
</div>
|
|
<div class="info-actions">
|
|
<router-link to="/profile/edit" class="edit-btn">{{ t('profile.edit') }}</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Language -->
|
|
<div class="lang-card">
|
|
<div class="info-header">{{ t('profile.language') }}</div>
|
|
<div class="lang-group">
|
|
<button
|
|
v-for="item in locales"
|
|
:key="item.code"
|
|
type="button"
|
|
class="lang-opt"
|
|
:class="{ active: locale === item.code }"
|
|
@click="setLocale(item.code)"
|
|
>
|
|
<LocaleFlag :locale="item.code" :size="14" />
|
|
<span>{{ item.label }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logout -->
|
|
<button type="button" class="logout-btn" @click="logout">
|
|
{{ t('auth.logout') }}
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.desktop-account-page { display: flex; flex-direction: column; min-height: 0; flex: 1; width: 100%; }
|
|
|
|
.account-main { flex: 1; min-width: 0; padding: 24px 28px; overflow-y: auto; }
|
|
.page-header { margin-bottom: 24px; }
|
|
.page-title { font-size: 18px; font-weight: 800; color: var(--primary-light); margin: 0; }
|
|
.loading-state { display: flex; justify-content: center; align-items: center; padding: 80px 0; }
|
|
.error-state { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 80px 20px; color: var(--text-muted); }
|
|
.retry-btn { padding: 8px 24px; border-radius: 6px; border: 1px solid var(--primary); background: transparent; color: var(--primary-light); font-size: 13px; font-weight: 700; cursor: pointer; }
|
|
|
|
.profile-layout {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.wallet-banner {
|
|
position: relative;
|
|
width: 100%;
|
|
aspect-ratio: 21 / 9;
|
|
border-radius: 16px;
|
|
overflow: hidden;
|
|
box-shadow: 0 4px 16px rgba(0, 61, 107, 0.18);
|
|
background: linear-gradient(135deg, #003D6B 0%, #005B9F 52%, #002847 100%);
|
|
}
|
|
|
|
.card-overlay {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
padding: 24px;
|
|
}
|
|
|
|
.bank-card-top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.bank-card-top-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.brand-logo {
|
|
width: 24px;
|
|
height: 24px;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.bank-card-brand-text {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.brand-name {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
letter-spacing: 0.14em;
|
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.bank-card-type {
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
color: rgba(255, 255, 255, 0.72);
|
|
font-style: italic;
|
|
}
|
|
|
|
.recharge-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 8px 16px;
|
|
border-radius: 14px;
|
|
background: rgba(255, 255, 255, 0.12);
|
|
border: 1px solid rgba(255, 255, 255, 0.28);
|
|
color: #fff;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
transition: background 0.15s ease;
|
|
}
|
|
|
|
.recharge-btn:hover { background: rgba(255, 255, 255, 0.2); }
|
|
|
|
.bank-card-balance {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.bank-card-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
color: rgba(255, 255, 255, 0.72);
|
|
}
|
|
|
|
.bank-card-number {
|
|
margin: 0;
|
|
font-family: var(--font-mono);
|
|
font-weight: 700;
|
|
color: #fff;
|
|
font-size: 38px;
|
|
}
|
|
|
|
.bank-card-footer {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 12px;
|
|
padding-top: 16px;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.bank-card-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.bank-card-field--center { text-align: center; }
|
|
.bank-card-field--right { text-align: right; }
|
|
|
|
.bank-card-holder {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.bank-card-stat {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
}
|
|
|
|
.info-card, .lang-card {
|
|
background: var(--bg-card);
|
|
backdrop-filter: blur(12px);
|
|
border: 1px solid rgba(0, 0, 0, 0.04);
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.info-header {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
margin-bottom: 14px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid var(--desktop-border);
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.info-label { width: 80px; flex-shrink: 0; font-size: 12px; color: var(--text-muted); font-weight: 600; }
|
|
.info-val { flex: 1; font-size: 14px; font-weight: 700; color: var(--text); }
|
|
.info-actions { padding-top: 14px; }
|
|
|
|
.edit-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 7px 18px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border-active);
|
|
color: var(--primary-light);
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
text-decoration: none;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.edit-btn:hover { background: rgba(0, 102, 204,0.08); }
|
|
|
|
.lang-group { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
|
|
.lang-opt {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 7px 14px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--desktop-border);
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.lang-opt.active {
|
|
border-color: var(--border-active);
|
|
color: var(--primary-light);
|
|
background: rgba(0, 102, 204,0.08);
|
|
}
|
|
|
|
.logout-btn {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 12px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--desktop-border);
|
|
background: transparent;
|
|
color: var(--danger);
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.logout-btn:hover { background: rgba(255,69,58,0.06); }
|
|
</style>
|