Files
thebet365/apps/player/src/views/desktop/DesktopProfileView.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>