404 lines
9.2 KiB
Vue
404 lines
9.2 KiB
Vue
<template>
|
|
<div class="member-center-page">
|
|
<div class="member-screen">
|
|
<header class="mc-header">
|
|
<h1 class="mc-title">{{ t('memberCenter.title') }}</h1>
|
|
</header>
|
|
|
|
<section class="hero-card">
|
|
<div class="hero-row">
|
|
<div class="hero-left">
|
|
<span class="hero-lbl">{{ t('memberCenter.currentLevel') }}</span>
|
|
<span class="hero-lvl">{{ t('memberCenter.vipLabel', { n: displayLevel }) }}</span>
|
|
</div>
|
|
<button type="button" class="btn-recharge" @click="goWallet">
|
|
{{ t('memberCenter.goRecharge') }}
|
|
</button>
|
|
</div>
|
|
<p v-if="nextHint" class="hero-hint">{{ nextHint }}</p>
|
|
</section>
|
|
|
|
<section v-if="vipLoading" class="mc-status">{{ t('common.loading') }}</section>
|
|
<section v-else-if="vipLoadError" class="mc-status mc-status--error">
|
|
{{ vipLoadError }}
|
|
</section>
|
|
|
|
<section v-else class="explain-block">
|
|
<h2 class="explain-title">{{ t('memberCenter.explainTitle') }}</h2>
|
|
<ul class="explain-list">
|
|
<li
|
|
v-for="row in tierRows"
|
|
:key="row.levelKey"
|
|
class="explain-row"
|
|
:class="{ 'explain-row--current': row.levelNum === displayLevel }"
|
|
>
|
|
<div class="explain-main">
|
|
<p class="explain-desc">{{ row.condition }}</p>
|
|
<p v-if="row.fees" class="explain-fees">{{ row.fees }}</p>
|
|
</div>
|
|
<span class="explain-vip">{{ row.label }}</span>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<p class="mc-foot">{{ t('memberCenter.footnote') }}</p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useUserStore } from '@/stores/user'
|
|
import { getPmVipLevelPublic, type PmVipLevelItem } from '@/api/vipLevel'
|
|
|
|
const router = useRouter()
|
|
const { t } = useI18n()
|
|
const userStore = useUserStore()
|
|
|
|
const vipLevels = ref<PmVipLevelItem[]>([])
|
|
const vipLoading = ref(false)
|
|
const vipLoadError = ref<string | null>(null)
|
|
|
|
onMounted(() => {
|
|
loadVipLevels()
|
|
if (userStore.isLoggedIn) userStore.fetchUserInfo()
|
|
})
|
|
|
|
async function loadVipLevels() {
|
|
vipLoading.value = true
|
|
vipLoadError.value = null
|
|
try {
|
|
const res = await getPmVipLevelPublic()
|
|
if (res.code !== 0 && res.code !== 200) {
|
|
vipLoadError.value = res.msg || t('memberCenter.loadError')
|
|
return
|
|
}
|
|
vipLevels.value = res.data?.list ?? []
|
|
} catch {
|
|
vipLoadError.value = t('memberCenter.loadError')
|
|
} finally {
|
|
vipLoading.value = false
|
|
}
|
|
}
|
|
|
|
const rawUser = computed(() => (userStore.user ?? {}) as Record<string, unknown>)
|
|
|
|
function resolveVipLevel(user: Record<string, unknown>): number {
|
|
const keys = ['vipLevel', 'memberLevel', 'vip', 'level']
|
|
for (const k of keys) {
|
|
const v = user[k]
|
|
if (typeof v === 'number' && Number.isFinite(v)) {
|
|
return Math.max(0, Math.floor(v))
|
|
}
|
|
if (typeof v === 'string' && v.trim()) {
|
|
const n = parseInt(v.replace(/\D/g, ''), 10)
|
|
if (!Number.isNaN(n)) return Math.max(0, n)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
* 与订单价格 bps 一致:费率整数 / 10000 为比例,展示为百分比 → 数值 / 100 + '%'
|
|
*/
|
|
function formatTradingFee(rate: number): string {
|
|
if (!Number.isFinite(rate)) return '--'
|
|
return `${(rate / 10000).toFixed(2)}%`
|
|
}
|
|
|
|
/** 与接口 needDeposit / accumulated 同一套口径:大额按 USDC 6 位小数,否则按美元数值 */
|
|
function depositRawToUsd(raw: number): number {
|
|
if (!Number.isFinite(raw) || raw <= 0) return 0
|
|
return raw / 1_000_000
|
|
}
|
|
function parseUserAccumulatedUsd(user: Record<string, unknown>): number {
|
|
const v = user.accumulated ?? user.Accumulated
|
|
if (typeof v === 'number' && Number.isFinite(v)) return depositRawToUsd(v)
|
|
if (typeof v === 'string' && v.trim()) {
|
|
const n = parseFloat(v.replace(/,/g, ''))
|
|
return Number.isFinite(n) ? depositRawToUsd(n) : 0
|
|
}
|
|
return 0
|
|
}
|
|
|
|
function formatNeedDepositUsd(n: number): string {
|
|
const dollars = depositRawToUsd(n)
|
|
if (dollars <= 0) return ''
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
maximumFractionDigits: Number.isInteger(dollars) && dollars >= 100 ? 0 : 2,
|
|
}).format(dollars)
|
|
}
|
|
|
|
function formatUsdcAmountPlain(dollars: number): string {
|
|
if (!Number.isFinite(dollars) || dollars <= 0) return ''
|
|
return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(dollars)
|
|
}
|
|
|
|
const sortedLevels = computed(() =>
|
|
[...vipLevels.value].sort(
|
|
(a, b) => parseInt(String(a.levelName), 10) - parseInt(String(b.levelName), 10),
|
|
),
|
|
)
|
|
|
|
const displayLevel = computed(() => resolveVipLevel(rawUser.value))
|
|
|
|
const nextHint = computed(() => {
|
|
const list = sortedLevels.value
|
|
if (!list.length) return ''
|
|
const lv = displayLevel.value
|
|
const idx = list.findIndex((x) => parseInt(String(x.levelName), 10) === lv)
|
|
if (idx < 0 || idx >= list.length - 1) return ''
|
|
const nextTier = list[idx + 1]
|
|
if (!nextTier) return ''
|
|
const nextNeedUsd = depositRawToUsd(nextTier.needDeposit)
|
|
if (nextNeedUsd <= 0) return ''
|
|
const accumulatedUsd = parseUserAccumulatedUsd(rawUser.value)
|
|
const remainUsd = Math.max(0, nextNeedUsd - accumulatedUsd)
|
|
const amount = formatUsdcAmountPlain(remainUsd)
|
|
if (!amount) return ''
|
|
return t('memberCenter.hintNeedMoreDeposit', { amount })
|
|
})
|
|
|
|
const FALLBACK_MAX = 4
|
|
|
|
const tierRows = computed(() => {
|
|
const list = sortedLevels.value
|
|
if (list.length) {
|
|
return list.map((item) => {
|
|
const n = parseInt(String(item.levelName), 10)
|
|
const levelNum = Number.isNaN(n) ? 0 : n
|
|
const condition =
|
|
!item.needDeposit || item.needDeposit <= 0
|
|
? t('memberCenter.tier0')
|
|
: t('memberCenter.tierNeedDeposit', { amount: formatNeedDepositUsd(item.needDeposit) })
|
|
const fees = t('memberCenter.feesLine', {
|
|
taker: formatTradingFee(item.takerFeeRate),
|
|
maker: formatTradingFee(item.makerFeeRate),
|
|
})
|
|
return {
|
|
levelKey: String(item.levelName),
|
|
levelNum,
|
|
condition,
|
|
fees,
|
|
label: t('memberCenter.vipLabel', { n: levelNum }),
|
|
}
|
|
})
|
|
}
|
|
return Array.from({ length: FALLBACK_MAX + 1 }, (_, i) => ({
|
|
levelKey: `fallback-${i}`,
|
|
levelNum: i,
|
|
condition: t(`memberCenter.tier${i}`),
|
|
fees: '',
|
|
label: t('memberCenter.vipLabel', { n: i }),
|
|
}))
|
|
})
|
|
function goWallet() {
|
|
router.push('/wallet')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.member-center-page {
|
|
min-height: 100vh;
|
|
background: #ffffff;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: flex-start;
|
|
padding: 0;
|
|
}
|
|
|
|
.member-screen {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
min-height: 0;
|
|
background: #fcfcfc;
|
|
font-family: Inter, sans-serif;
|
|
padding: 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.mc-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.mc-back {
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 0;
|
|
font-size: 22px;
|
|
line-height: 1;
|
|
color: #111827;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.mc-title {
|
|
margin: 0;
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
color: #111827;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.mc-status {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
color: #6b7280;
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.mc-status--error {
|
|
color: #b45309;
|
|
}
|
|
|
|
.hero-card {
|
|
background: #fff7e6;
|
|
border: 1.5px solid #e6a817;
|
|
border-radius: 16px;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.hero-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
}
|
|
|
|
.hero-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.hero-lbl {
|
|
color: #78350f;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.hero-lvl {
|
|
color: #c9970a;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.btn-recharge {
|
|
flex-shrink: 0;
|
|
height: 36px;
|
|
padding: 0 14px;
|
|
border: 0;
|
|
border-radius: 8px;
|
|
background: #c9970a;
|
|
color: #fffef5;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.hero-hint {
|
|
margin: 0;
|
|
color: #a16207;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.explain-block {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
.explain-title {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: #333333;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.explain-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.explain-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
}
|
|
|
|
.explain-main {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.explain-desc {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
line-height: 1.45;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.explain-fees {
|
|
margin: 0;
|
|
font-size: 11px;
|
|
line-height: 1.35;
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.explain-vip {
|
|
flex-shrink: 0;
|
|
min-width: 52px;
|
|
text-align: right;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: #111827;
|
|
}
|
|
|
|
.explain-row--current .explain-desc {
|
|
color: #78350f;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.explain-row--current .explain-fees {
|
|
color: #a16207;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.explain-row--current .explain-vip {
|
|
color: #c9970a;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.mc-foot {
|
|
margin: 0;
|
|
font-size: 11px;
|
|
line-height: 1.35;
|
|
color: #6b7280;
|
|
}
|
|
</style>
|