我的页面:头像姓名并入会员卡,去掉学校信息
ci/woodpecker/tag/build Pipeline was successful

原先顶部渐变区单独一块「大头像 + 姓名 + 学校 + 学院专业标签」,
对用户实际没什么用,还占掉一屏顶部空间。

改动:
- 删除顶部个人信息块(含学校名与学院/专业/班级标签)
- 头像缩为 44px 圆形,移到会员卡内 Crown 图标的位置(该图标去掉),
  姓名与会员身份(VIP/普通用户)合并为同一行
- VIP 角标改为贴在头像右下角
- 顶部渐变条高度压缩:padding-top 28px→14px、pb-20→pb-16,
  会员卡仍以 -mt-14 上浮叠压,视觉层次不变

顺带清理不再使用的导入与计算属性:Crown 图标、GraduationCap 图标、
profileTags(学院/专业/班级标签)。
修掉 img 的 :src 缺可选链导致的 null 类型报错。

npm run build 与 vue-tsc 均通过。
This commit is contained in:
becomer
2026-10-21 16:58:24 +08:00
parent 3b4a8df8fd
commit 0d639ea485
+13 -34
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { LogOut, RefreshCw, Gift, X, Dices, TriangleAlert, SunMoon, Sun, Moon, Crown, Copy, ChevronRight, Smartphone, Coins, UserPlus, GraduationCap, Headset, ShoppingCart, Flame } from 'lucide-vue-next'
import { LogOut, RefreshCw, Gift, X, Dices, TriangleAlert, SunMoon, Sun, Moon, Copy, ChevronRight, Smartphone, Coins, UserPlus, Headset, ShoppingCart, Flame } from 'lucide-vue-next'
import { themeMode, setThemeMode, type ThemeMode } from '@/composables/useTheme'
import { confirmDialog } from '@/composables/useConfirm'
import { Button } from '@/components/ui/button'
@@ -163,13 +163,6 @@ function maskDeviceCode(code: string): string {
// 是否已绑定设备码(决定「立即绑定 / 重新绑定」按钮文案)
const hasDeviceCode = computed(() => !!userInfo.value?.mobileCode)
// 头部信息标签:学院 / 专业 / 班级(有值才显示,最多 2 个,避免挤满)
const profileTags = computed(() => {
const u = userInfo.value
if (!u) return []
return [u.academy, u.className || u.major].filter((t): t is string => !!t && !!t.trim()).slice(0, 2)
})
// VIP 剩余天数(到期时间无效 / 已过期返回 null)
const vipDaysLeft = computed(() => {
if (!isVip.value || !vipExpireAt.value) return null
@@ -464,32 +457,12 @@ async function handleRedeemInvite() {
<template>
<div class="flex flex-col min-h-full">
<!-- ======== 顶部:品牌渐变背景 + 个人信息(左对齐)+ 会员卡 ======== -->
<div class="relative overflow-hidden hero-clip bg-brand-gradient px-5 pb-20 text-white flex-shrink-0" style="padding-top: calc(var(--safe-top) + 28px)">
<div class="relative overflow-hidden hero-clip bg-brand-gradient px-5 pb-16 text-white flex-shrink-0" style="padding-top: calc(var(--safe-top) + 14px)">
<!-- 装饰:柔光圆 + 细网格纹理 -->
<div class="pointer-events-none absolute -right-16 -top-20 w-64 h-64 rounded-full bg-white/10" />
<div class="pointer-events-none absolute right-10 top-24 w-28 h-28 rounded-full bg-white/10" />
<div class="pointer-events-none absolute inset-0 profile-hero-grid" />
<!-- 头像 + 姓名 + 学校 -->
<div class="relative flex items-center gap-4 rise-in">
<div class="relative flex-shrink-0">
<div class="w-[68px] h-[68px] rounded-full overflow-hidden ring-[3px] ring-white/60 shadow-lg shadow-black/15 bg-white/20 flex items-center justify-center">
<img v-show="userInfo?.avatar && !avatarFailed" :src="userInfo?.avatar" class="w-full h-full object-cover" alt="头像" @error="avatarFailed = true" />
<span v-if="!userInfo?.avatar || avatarFailed" class="text-2xl font-bold">{{ userInfo?.realName?.[0] || userInfo?.username?.[0] || '?' }}</span>
</div>
<div v-if="isVip" class="vip-badge absolute -right-2 -bottom-1 z-10 whitespace-nowrap">VIP</div>
</div>
<div class="min-w-0 flex-1">
<p class="text-[22px] font-bold tracking-tight leading-tight truncate">{{ userInfo?.realName || userInfo?.username || '--' }}</p>
<p class="mt-1 flex items-center gap-1 text-sm text-white/90 truncate">
<GraduationCap class="w-3.5 h-3.5 flex-shrink-0" />
<span class="truncate">{{ userInfo?.schoolName || '--' }}</span>
</p>
<div v-if="profileTags.length" class="mt-2 flex flex-wrap gap-1.5">
<span v-for="t in profileTags" :key="t" class="max-w-[10rem] truncate text-xs rounded-md bg-white/15 ring-1 ring-white/20 px-1.5 py-0.5 text-white/95">{{ t }}</span>
</div>
</div>
</div>
</div>
<!-- ======== 会员卡(压在头图下沿) ======== -->
@@ -497,14 +470,20 @@ async function handleRedeemInvite() {
<div class="member-card relative overflow-hidden hero-clip rounded-3xl p-4 shadow-xl" :class="isVip ? 'is-vip' : 'is-free'">
<div class="pointer-events-none absolute -right-8 -top-10 w-36 h-36 rounded-full member-card-glow" />
<div class="relative flex items-center gap-3">
<div class="member-icon w-11 h-11 rounded-2xl flex items-center justify-center flex-shrink-0">
<Crown class="w-5 h-5" :stroke-width="2.4" />
<!-- 头像(原顶部头图内容整合至此处) -->
<div class="relative flex-shrink-0">
<div class="w-11 h-11 rounded-full overflow-hidden ring-2 ring-white/70 shadow-sm bg-white/30 flex items-center justify-center">
<img v-show="userInfo?.avatar && !avatarFailed" :src="userInfo?.avatar" class="w-full h-full object-cover" alt="头像" @error="avatarFailed = true" />
<span v-if="!userInfo?.avatar || avatarFailed" class="text-base font-bold" :class="isVip ? 'text-amber-800' : 'text-brand-700'">{{ userInfo?.realName?.[0] || userInfo?.username?.[0] || '?' }}</span>
</div>
<div v-if="isVip" class="vip-badge absolute -right-1.5 -bottom-1 z-10 whitespace-nowrap">VIP</div>
</div>
<div class="min-w-0 flex-1">
<p class="member-title text-base font-bold tracking-tight">{{ isVip ? 'VIP 会员' : '普通用户' }}</p>
<!-- 姓名 + 会员身份 -->
<p class="member-title text-base font-bold tracking-tight truncate">{{ userInfo?.realName || userInfo?.username || '--' }}</p>
<p class="member-sub text-xs mt-0.5 truncate">
<template v-if="isVip">{{ vipExpireShort ? `有效期至 ${vipExpireShort}` : '会员生效中' }}<template v-if="vipDaysLeft !== null"> · 剩 {{ vipDaysLeft }} 天</template></template>
<template v-else>开通后执行任务不限次数</template>
<template v-if="isVip">{{ vipExpireShort ? `VIP · 有效期至 ${vipExpireShort}` : 'VIP 会员生效中' }}<template v-if="vipDaysLeft !== null"> · 剩 {{ vipDaysLeft }} 天</template></template>
<template v-else>普通用户 · 开通后执行任务不限次数</template>
</p>
<!-- 可执行次数:普通用户与 VIP 都显示(VIP 为不限次数时显示「不限」) -->
<p class="member-sub text-xs mt-1 flex items-center gap-1">