新增:VIP显示功能
This commit is contained in:
parent
99037b536e
commit
4927953dc9
@ -9,7 +9,7 @@
|
||||
"name": "Profile Screen",
|
||||
"clip": true,
|
||||
"width": 402,
|
||||
"height": 648,
|
||||
"height": 980,
|
||||
"fill": "$--bg-page",
|
||||
"layout": "vertical",
|
||||
"gap": 16,
|
||||
@ -94,8 +94,8 @@
|
||||
"type": "text",
|
||||
"id": "VLEgU",
|
||||
"name": "tagText",
|
||||
"fill": "$--primary",
|
||||
"content": "VIP Trader",
|
||||
"fill": "#a16207",
|
||||
"content": "尊享交易权益",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "600"
|
||||
@ -134,6 +134,97 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "BQsV9",
|
||||
"name": "VIP Entry",
|
||||
"width": "fill_container",
|
||||
"fill": "#fff7e6",
|
||||
"cornerRadius": "$--radius-md",
|
||||
"stroke": {
|
||||
"align": "inside",
|
||||
"thickness": 1.5,
|
||||
"fill": "#e6a817"
|
||||
},
|
||||
"gap": 10,
|
||||
"padding": [
|
||||
12,
|
||||
14
|
||||
],
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "AsI1v",
|
||||
"name": "vipLeft",
|
||||
"gap": 10,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "tklXN",
|
||||
"name": "vipTitle",
|
||||
"fill": "#78350f",
|
||||
"content": "会员中心",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 15,
|
||||
"fontWeight": "600"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "RjQ0D",
|
||||
"name": "vipRight",
|
||||
"gap": 8,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "sKfYz",
|
||||
"name": "levelCapsule",
|
||||
"width": 30,
|
||||
"height": 22,
|
||||
"fill": {
|
||||
"type": "color",
|
||||
"color": "#c9970a",
|
||||
"enabled": false
|
||||
},
|
||||
"cornerRadius": "$--radius-pill",
|
||||
"padding": [
|
||||
8,
|
||||
14
|
||||
],
|
||||
"justifyContent": "center",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "N7wG9",
|
||||
"name": "levelCapsuleText",
|
||||
"fill": "#c9970a",
|
||||
"content": "vip3",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "6K4W4",
|
||||
"name": "entryChevron",
|
||||
"fill": "#ca8a04",
|
||||
"content": "›",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 18,
|
||||
"fontWeight": "normal"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -6370,6 +6461,387 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "lvTGn",
|
||||
"x": 3820,
|
||||
"y": 0,
|
||||
"name": "会员中心 Screen",
|
||||
"clip": true,
|
||||
"width": 402,
|
||||
"height": 1180,
|
||||
"fill": "$--bg-page",
|
||||
"layout": "vertical",
|
||||
"gap": 16,
|
||||
"padding": 16,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "RjQ1F",
|
||||
"name": "mcHeader",
|
||||
"width": "fill_container",
|
||||
"gap": 12,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "EqeEY",
|
||||
"name": "mcBack",
|
||||
"fill": "$--text-primary",
|
||||
"content": "←",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 22
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "3ZEsD",
|
||||
"name": "mcTitle",
|
||||
"fill": "$--text-primary",
|
||||
"content": "会员中心",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 20,
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "bX2qC",
|
||||
"name": "currentLevelCard",
|
||||
"width": "fill_container",
|
||||
"fill": "#fff7e6",
|
||||
"cornerRadius": "$--radius-lg",
|
||||
"stroke": {
|
||||
"align": "inside",
|
||||
"thickness": 1.5,
|
||||
"fill": "#e6a817"
|
||||
},
|
||||
"layout": "vertical",
|
||||
"gap": 12,
|
||||
"padding": 20,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "8zw2G",
|
||||
"name": "heroRow",
|
||||
"width": "fill_container",
|
||||
"gap": 10,
|
||||
"justifyContent": "space_between",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "h9i8v",
|
||||
"name": "heroLeft",
|
||||
"width": "fill_container",
|
||||
"gap": 8,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "LRQkX",
|
||||
"name": "heroLbl2",
|
||||
"fill": "#78350f",
|
||||
"content": "当前等级",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 13,
|
||||
"fontWeight": "500"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "Gn048",
|
||||
"name": "heroLvl",
|
||||
"fill": "#c9970a",
|
||||
"content": "VIP 3",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 16,
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "K46YD",
|
||||
"name": "btnRecharge",
|
||||
"height": 36,
|
||||
"fill": "#c9970a",
|
||||
"cornerRadius": 8,
|
||||
"padding": [
|
||||
8,
|
||||
14
|
||||
],
|
||||
"justifyContent": "center",
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "zdalb",
|
||||
"name": "btx",
|
||||
"fill": "#fffef5",
|
||||
"content": "去充值",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 13,
|
||||
"fontWeight": "600"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "xINU0",
|
||||
"name": "heroHint",
|
||||
"fill": "#a16207",
|
||||
"content": "距离 vip4 还需累计交易量达到 $500,000",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "500"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "lEW94",
|
||||
"name": "mcLevelBlock",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 14,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "dgWjb",
|
||||
"name": "mcExplainTitle",
|
||||
"fill": "$--text-secondary",
|
||||
"content": "等级说明",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "600"
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "KPIS6",
|
||||
"name": "mcExplain",
|
||||
"width": "fill_container",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "RYpWx",
|
||||
"name": "exRow0",
|
||||
"width": "fill_container",
|
||||
"fill": "$--bg-page",
|
||||
"stroke": {
|
||||
"align": "inside",
|
||||
"thickness": 0,
|
||||
"fill": "$--border-color"
|
||||
},
|
||||
"gap": 10,
|
||||
"justifyContent": "space_between",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "HbZIh",
|
||||
"name": "ex0l",
|
||||
"fill": "$--text-secondary",
|
||||
"content": "完成注册即可。",
|
||||
"lineHeight": 1.45,
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "normal"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "2T1e2",
|
||||
"name": "ex0r",
|
||||
"fill": "$--text-primary",
|
||||
"textGrowth": "fixed-width",
|
||||
"width": 52,
|
||||
"content": "VIP 0",
|
||||
"textAlign": "right",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "brfky",
|
||||
"name": "exRow1",
|
||||
"width": "fill_container",
|
||||
"fill": "$--bg-page",
|
||||
"stroke": {
|
||||
"align": "inside",
|
||||
"thickness": 0,
|
||||
"fill": "$--border-color"
|
||||
},
|
||||
"gap": 10,
|
||||
"justifyContent": "space_between",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "5Q49A",
|
||||
"name": "r2l",
|
||||
"fill": "$--text-secondary",
|
||||
"content": "累计充值 ≥ $10,000 USDC。",
|
||||
"lineHeight": 1.45,
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "normal"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "oSw0l",
|
||||
"name": "r2r",
|
||||
"fill": "$--text-primary",
|
||||
"textGrowth": "fixed-width",
|
||||
"width": 52,
|
||||
"content": "VIP 1",
|
||||
"textAlign": "right",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "wEUKd",
|
||||
"name": "exRow2",
|
||||
"width": "fill_container",
|
||||
"fill": "$--bg-page",
|
||||
"stroke": {
|
||||
"align": "inside",
|
||||
"thickness": 0,
|
||||
"fill": "$--border-color"
|
||||
},
|
||||
"gap": 10,
|
||||
"justifyContent": "space_between",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "F8h9p",
|
||||
"name": "r3l",
|
||||
"fill": "$--text-secondary",
|
||||
"content": "累计充值 ≥ $50,000 USDC。",
|
||||
"lineHeight": 1.45,
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "normal"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "zqbDv",
|
||||
"name": "r3r",
|
||||
"fill": "$--text-primary",
|
||||
"textGrowth": "fixed-width",
|
||||
"width": 52,
|
||||
"content": "VIP 2",
|
||||
"textAlign": "right",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "YacFs",
|
||||
"name": "exRow3",
|
||||
"width": "fill_container",
|
||||
"fill": "$--bg-page",
|
||||
"stroke": {
|
||||
"align": "inside",
|
||||
"thickness": 0,
|
||||
"fill": "$--border-color"
|
||||
},
|
||||
"gap": 10,
|
||||
"justifyContent": "space_between",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "UElGL",
|
||||
"name": "r4l",
|
||||
"fill": "#78350f",
|
||||
"content": "累计充值 ≥ $200,000 USDC。",
|
||||
"lineHeight": 1.45,
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "normal"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "9HZuo",
|
||||
"name": "r4r",
|
||||
"fill": "#c9970a",
|
||||
"textGrowth": "fixed-width",
|
||||
"width": 52,
|
||||
"content": "VIP 3",
|
||||
"textAlign": "right",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "aKf3Y",
|
||||
"name": "exRow4",
|
||||
"width": "fill_container",
|
||||
"fill": "$--bg-page",
|
||||
"stroke": {
|
||||
"align": "inside",
|
||||
"thickness": 0,
|
||||
"fill": "$--border-color"
|
||||
},
|
||||
"gap": 10,
|
||||
"justifyContent": "space_between",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "tIUh6",
|
||||
"name": "r5l",
|
||||
"fill": "$--text-secondary",
|
||||
"content": "累计充值 ≥ $500,000 USDC;\n或邀请有效用户 ≥ 50 人。",
|
||||
"lineHeight": 1.45,
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "normal"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "Z1nPr",
|
||||
"name": "r5r",
|
||||
"fill": "$--text-primary",
|
||||
"textGrowth": "fixed-width",
|
||||
"width": 52,
|
||||
"content": "VIP 4",
|
||||
"textAlign": "right",
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 12,
|
||||
"fontWeight": "700"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "83n3g",
|
||||
"name": "mcFoot",
|
||||
"fill": "$--text-secondary",
|
||||
"content": "* 统计数据与门槛以后台规则为准(示意数据,非最终规则)。",
|
||||
"lineHeight": 1.35,
|
||||
"fontFamily": "Inter",
|
||||
"fontSize": 11,
|
||||
"fontWeight": "normal"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"variables": {
|
||||
|
||||
404
src/views/MemberCenter.vue
Normal file
404
src/views/MemberCenter.vue
Normal file
@ -0,0 +1,404 @@
|
||||
<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_000 ? raw / 1_000_000 : raw
|
||||
}
|
||||
|
||||
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>
|
||||
@ -4,12 +4,7 @@
|
||||
<section class="card profile-card">
|
||||
<div class="top-row">
|
||||
<label class="avatar-wrap" :aria-label="t('profile.changeAvatar')">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="avatar-input"
|
||||
@change="onAvatarFileChange"
|
||||
/>
|
||||
<input type="file" accept="image/*" class="avatar-input" @change="onAvatarFileChange" />
|
||||
<div class="avatar">
|
||||
<v-progress-circular
|
||||
v-if="avatarUploading"
|
||||
@ -32,27 +27,54 @@
|
||||
<div class="acc-text">{{ t('profile.uidLabel', { uid: userIdText }) }}</div>
|
||||
<div class="tag-text">{{ userTag }}</div>
|
||||
</div>
|
||||
<button class="edit-btn" type="button" @click="onEditProfile">{{ t('profile.edit') }}</button>
|
||||
<button class="edit-btn" type="button" @click="onEditProfile">
|
||||
{{ t('profile.edit') }}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="vip-entry" @click="goMemberCenter">
|
||||
<span class="vip-entry-title">{{ t('profile.memberCenter') }}</span>
|
||||
<span class="vip-entry-right">
|
||||
<span class="vip-pill">{{ t('memberCenter.vipLabel', { n: vipLevel }) }}</span>
|
||||
<span class="vip-chev" aria-hidden="true">›</span>
|
||||
</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="card wallet-card">
|
||||
<div class="wallet-head">
|
||||
<span class="wallet-title">{{ t('profile.walletOverview') }}</span>
|
||||
<button class="wallet-link" type="button" @click="goWallet">{{ t('profile.walletDetail') }} ></button>
|
||||
<button class="wallet-link" type="button" @click="goWallet">
|
||||
{{ t('profile.walletDetail') }} >
|
||||
</button>
|
||||
</div>
|
||||
<div class="wallet-balance">${{ totalBalance }}</div>
|
||||
<div class="wallet-sub">
|
||||
{{ t('profile.walletSub', { available: availableBalance, frozen: frozenBalance }) }}
|
||||
</div>
|
||||
<div class="wallet-actions">
|
||||
<button type="button" class="wallet-action-primary" @click="goWallet">
|
||||
{{ t('profile.depositCoin') }}
|
||||
</button>
|
||||
<button type="button" class="wallet-action-secondary" @click="goWallet">
|
||||
{{ t('profile.withdrawCoin') }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card menu-card">
|
||||
<div class="menu-title">{{ t('profile.accountSettings') }}</div>
|
||||
<button v-for="item in settingItems" :key="item.label" class="menu-item" type="button" @click="goSetting(item)">
|
||||
<button
|
||||
v-for="item in settingItems"
|
||||
:key="item.label"
|
||||
class="menu-item"
|
||||
type="button"
|
||||
@click="goSetting(item)"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<span v-if="item.action === 'locale'" class="menu-locale">{{ currentLocaleLabel }}</span>
|
||||
<span v-else-if="item.action === 'wallet'" class="menu-locale">{{ walletAddressShort }}</span>
|
||||
<span v-else-if="item.action === 'wallet'" class="menu-locale">{{
|
||||
walletAddressShort
|
||||
}}</span>
|
||||
<span v-else class="menu-arrow">></span>
|
||||
</button>
|
||||
</section>
|
||||
@ -83,7 +105,12 @@
|
||||
<div class="wallet-dialog-title">{{ t('profile.currentWalletAddress') }}</div>
|
||||
<div class="wallet-dialog-address">{{ walletAddressText }}</div>
|
||||
<div class="wallet-dialog-actions">
|
||||
<button class="wallet-copy-btn" type="button" :disabled="!walletAddress" @click="copyWalletAddress">
|
||||
<button
|
||||
class="wallet-copy-btn"
|
||||
type="button"
|
||||
:disabled="!walletAddress"
|
||||
@click="copyWalletAddress"
|
||||
>
|
||||
{{ t('profile.copyAddress') }}
|
||||
</button>
|
||||
</div>
|
||||
@ -104,7 +131,12 @@
|
||||
persistent-hint
|
||||
/>
|
||||
<div class="name-dialog-actions">
|
||||
<button class="name-dialog-cancel-btn" type="button" :disabled="isSaving" @click="closeEditNameDialog">
|
||||
<button
|
||||
class="name-dialog-cancel-btn"
|
||||
type="button"
|
||||
:disabled="isSaving"
|
||||
@click="closeEditNameDialog"
|
||||
>
|
||||
{{ t('profile.cancel') }}
|
||||
</button>
|
||||
<button class="name-dialog-save-btn" type="button" :disabled="isSaving" @click="saveName">
|
||||
@ -182,7 +214,9 @@ const walletAddress = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
const walletAddressText = computed(() => walletAddress.value || t('profile.walletAddressUnavailable'))
|
||||
const walletAddressText = computed(
|
||||
() => walletAddress.value || t('profile.walletAddressUnavailable'),
|
||||
)
|
||||
const walletAddressShort = computed(() => {
|
||||
const value = walletAddress.value
|
||||
if (!value) return t('profile.unbound')
|
||||
@ -271,6 +305,28 @@ function goWallet() {
|
||||
router.push('/wallet')
|
||||
}
|
||||
|
||||
function goMemberCenter() {
|
||||
router.push('/member-center')
|
||||
}
|
||||
|
||||
function resolveVipLevel(user: Record<string, unknown> | null): number {
|
||||
if (!user) return 0
|
||||
const keys = ['vipLevel', 'memberLevel', 'vip', 'level']
|
||||
for (const k of keys) {
|
||||
const v = user[k]
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
return Math.min(4, 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.min(4, Math.max(0, n))
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const vipLevel = computed(() => resolveVipLevel(rawUser.value as Record<string, unknown> | null))
|
||||
|
||||
function validateUserName(name: string): string | null {
|
||||
const v = name.trim()
|
||||
if (!v) return t('profile.nameRequired')
|
||||
@ -535,6 +591,49 @@ onMounted(() => {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vip-entry {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1.5px solid #e6a817;
|
||||
background: #fff7e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vip-entry-title {
|
||||
color: #78350f;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.vip-entry-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vip-pill {
|
||||
padding: 2px 14px;
|
||||
color: #c9970a;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.vip-chev {
|
||||
color: #ca8a04;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wallet-card {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
@ -584,25 +683,28 @@ onMounted(() => {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
.wallet-action-primary {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: #5b5bd6;
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
border: 0;
|
||||
background: #5b5bd6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-secondary {
|
||||
.wallet-action-secondary {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #fcfcfc;
|
||||
color: #111827;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user