Files
hejiu/pages/profile/profile.vue
T
cg d335ec290a refactor(app): 重构应用主题和页面结构
- 移除动态主题切换功能,改为全局统一深色主题
- 实现自定义TabBar组件替换原生tabBar,解决iOS闪白问题
- 创建main容器页面统一管理三个tab页面的生命周期和状态
- 将所有页面的onShow/onHide等生命周期方法迁移到子组件内部
- 更新页面路径从index改为main,并调整路由跳转逻辑
- 移除theme-mixin和相关主题工具函数
- 统一页面背景色设置,优化用户体验一致性
2026-09-23 20:56:14 +08:00

1126 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page-container profile-page">
<!-- 沉浸式头部 -->
<view class="hero-header" :style="{ paddingTop: headerPaddingTop }">
<view class="hero-bg">
<view class="hero-orb hero-orb-1"></view>
<view class="hero-orb hero-orb-2"></view>
<text class="hero-deco">🥃</text>
</view>
<!-- 右上角分享按钮(避让胶囊) -->
<button class="hero-share-btn" open-type="share" :style="shareBtnStyle">
<text class="hero-share-icon">分享</text>
</button>
<view class="hero-content">
<view class="avatar-ring">
<image v-if="isValidAvatar(user.avatar)" class="hero-avatar" :src="user.avatar" mode="aspectFill"></image>
<view v-else class="hero-avatar hero-avatar-placeholder">
<text class="hero-avatar-icon">👤</text>
</view>
</view>
<view class="hero-info">
<text class="hero-greeting">{{ greeting }}</text>
<view class="hero-name-row">
<text class="hero-name">{{ user.nickname || '酒友' }}</text>
<view class="hero-level-tag">
<text class="hero-level-text">{{ drinkLevel }}</text>
</view>
</view>
<text class="hero-joined">{{ joinedDate }} 加入 · 碰盏日记</text>
</view>
</view>
</view>
<!-- 悬浮数据卡片 -->
<view class="stats-float-card">
<view class="stats-item" @click="noop">
<text class="stats-num text-num">{{ stats.totalDays }}</text>
<text class="stats-label">打卡天数</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item" @click="noop">
<text class="stats-num text-num">{{ stats.totalRecords }}</text>
<text class="stats-label">饮酒次数</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item" @click="noop">
<text class="stats-num text-num text-amber">{{ stats.totalCups }}</text>
<text class="stats-label">标准杯</text>
</view>
<view class="stats-divider"></view>
<view class="stats-item" @click="noop">
<text class="stats-num stats-num-text">{{ favCategoryName }}</text>
<text class="stats-label">最爱酒类</text>
</view>
</view>
<!-- 酒桌小游戏入口 -->
<view class="actions-section">
<view class="action-card" @click="goGames">
<view class="action-icon-wrap action-icon-games">
<text class="action-icon">🎲</text>
</view>
<view class="action-text">
<view class="action-title-row">
<text class="action-title">酒桌小游戏</text>
<view class="action-hot-tag">HOT</view>
</view>
<text class="action-desc">数字炸弹·猜拳·骰子·转盘,今晚谁喝?</text>
</view>
<text class="action-arrow">›</text>
</view>
</view>
<view class="streak-section">
<view class="streak-row">
<view class="streak-card streak-drank">
<view class="streak-card-top">
<text class="streak-badge-emoji">🔥</text>
<text class="streak-title">连续饮酒</text>
</view>
<text class="streak-sub">保持记录,理性饮酒</text>
<view class="streak-num-row">
<text class="streak-days text-num">{{ stats.drankStreak || 0 }}</text>
<text class="streak-days-unit">天</text>
</view>
</view>
<view class="streak-card streak-abstain">
<view class="streak-card-top">
<text class="streak-badge-emoji">💪</text>
<text class="streak-title">连续戒酒</text>
</view>
<text class="streak-sub">自律即自由,继续坚持</text>
<view class="streak-num-row">
<text class="streak-days text-num">{{ stats.abstainStreak || 0 }}</text>
<text class="streak-days-unit">天</text>
</view>
</view>
</view>
</view>
<!-- 饮酒人格卡片 -->
<view class="persona-card">
<view class="persona-glow"></view>
<view class="persona-content">
<view class="persona-header">
<image v-if="isIconPath(personaEmoji)" :src="personaEmoji" class="persona-icon-img" mode="aspectFit"></image>
<text v-else class="persona-emoji">{{ personaEmoji }}</text>
<view class="persona-text">
<text class="persona-title">{{ personaTitle }}</text>
<text class="persona-desc">{{ personaDesc }}</text>
</view>
</view>
<view class="persona-tags">
<view class="persona-tag" v-if="favCategoryName !== '-'">
<text>偏爱{{ favCategoryName }}</text>
</view>
<view class="persona-tag" v-if="stats.categoryVariety > 0">
<text>尝过{{ stats.categoryVariety }}种酒</text>
</view>
<view class="persona-tag" v-if="stats.totalCups > 0">
<text>累计{{ stats.totalCups }}标准杯</text>
</view>
</view>
</view>
</view>
<!-- 成就墙 - 横向滚动 -->
<view class="achievements-section">
<view class="section-header">
<text class="section-title">成就徽章</text>
<text class="section-count text-num">{{ unlockedCount }}/{{ achievementList.length }}</text>
</view>
<scroll-view class="achievements-scroll" scroll-x :show-scrollbar="false">
<view class="achievements-track">
<view class="achievement-item" v-for="a in achievementList" :key="a.id">
<AchievementBadge
:achievement="a"
@click="showAchievementDetail(a)"
/>
</view>
</view>
</scroll-view>
</view>
<!-- 登出/登录 -->
<view class="logout-area">
<button v-if="isGuest" class="btn-ghost logout-btn" @click="goLogin">
登录账号,同步数据
</button>
<button v-else class="logout-text-btn" @click="handleLogout">
退出登录
</button>
</view>
<!-- 成就详情弹窗 -->
<view v-if="showDetail" class="detail-overlay" @click="showDetail = false">
<view class="detail-card" @click.stop>
<view class="detail-icon-ring" :class="detailAchievement.unlocked ? 'ring-unlocked' : 'ring-locked'">
<image v-if="isIconPath(detailAchievement.icon)" :src="detailAchievement.icon" class="detail-icon-img" mode="aspectFit"></image>
<text v-else class="detail-emoji">{{ detailAchievement.icon }}</text>
</view>
<text class="detail-name">{{ detailAchievement.name }}</text>
<text class="detail-desc">{{ detailAchievement.desc }}</text>
<view class="detail-status" :class="detailAchievement.unlocked ? 'status-unlocked' : 'status-locked'">
<text>{{ detailAchievement.unlocked ? '✓ 已解锁' : '🔒 未解锁' }}</text>
</view>
<button class="detail-close-btn" @click="showDetail = false">关闭</button>
</view>
</view>
</view>
</template>
<script>
import AchievementBadge from '../../components/AchievementBadge.vue'
import client, { clearAuth, GetAchievements, isLoggedIn, requireLogin } from '../../common/api'
import { checkAchievements, getCatIcon, isIconPath, calcLocalStreak, isValidAvatar } from '../../common/utils'
import { ACHIEVEMENTS, DRINK_CATEGORIES } from '../../common/constants'
export default {
components: { AchievementBadge },
// Vue3 组件事件必须显式声明
emits: ['switch-tab'],
data() {
// 获取胶囊按钮位置,动态计算分享按钮偏移和头部内边距
const sysInfo = uni.getSystemInfoSync()
const menuBtn = uni.getMenuButtonBoundingClientRect()
return {
user: {},
stats: {},
achievementList: [],
showDetail: false,
detailAchievement: {},
isGuest: false,
// 分享按钮定位:与胶囊垂直居中对齐,右侧留出胶囊宽度
shareBtnStyle: `top:${menuBtn.top}px;right:${sysInfo.windowWidth - menuBtn.left + 8}px;`,
// 头部内容从胶囊底部开始,避免遮挡头像
headerPaddingTop: (menuBtn.bottom + 20) + 'px'
}
},
computed: {
favCategoryName() {
if (!this.stats.favCategory) return '-'
const cat = DRINK_CATEGORIES.find(c => c.id === this.stats.favCategory)
return cat ? cat.name : '-'
},
joinedDate() {
const raw = this.user.joinedAt
if (!raw) return '2026'
// 只取年月日,去掉时间戳
const str = String(raw)
const m = str.match(/(\d{4})[-/](\d{2})[-/](\d{2})/)
if (m) return `${m[1]}.${m[2]}.${m[3]}`
return str.slice(0, 10)
},
unlockedCount() {
return this.achievementList.filter(a => a.unlocked).length
},
drinkLevel() {
const cups = this.stats.totalCups || 0
if (cups === 0) return '🌱 新手上路'
if (cups < 20) return '🍶 小酌新手'
if (cups < 50) return '🍺 进阶酒友'
if (cups < 100) return '🥃 资深酒客'
if (cups < 200) return '🏆 品酒达人'
return '👑 传奇酒神'
},
personaEmoji() {
const fav = this.stats.favCategory
if (fav) return getCatIcon(fav)
return '🍻'
},
personaTitle() {
const fav = this.stats.favCategory
const map = { baijiu: '白酒之魂', beer: '啤酒达人', wine: '红酒雅士', whisky: '威士忌行家', huangjiu: '黄酒知音', sake: '清酒爱好者', fruit: '果酒小清新', cocktail: '调酒师' }
return map[fav] || '多元品鉴师'
},
greeting() {
const h = new Date().getHours()
if (h < 6) return '🌙 夜深了,微醺正好'
if (h < 11) return '☀️ 早上好,新的一天'
if (h < 14) return '🌤 中午好,小酌怡情'
if (h < 18) return '🌇 下午好,来一杯?'
return '🌙 晚上好,今晚喝点?'
},
personaDesc() {
const days = this.stats.totalDays || 0
if (days === 0) return '开始记录你的第一杯吧'
if (days < 7) return '初来乍到,探索中'
if (days < 30) return '已养成记录习惯'
if (days < 100) return '资深记录者,每一杯都有故事'
return '百天老友,酒中见真章'
}
},
methods: {
// === 子组件生命周期(由 main.vue 容器通过 ref 调用)===
pageShow() {
this.checkLoginState()
this.loadData()
},
getShareAppMessage() {
const name = this.user.nickname || '我'
return {
title: `${name}的饮酒名片 - 碰盏日记`,
path: `/pages/share-profile/share-profile?${this.buildShareQuery()}`
}
},
getShareTimeline() {
const name = this.user.nickname || '我'
return {
title: `${name}的饮酒名片 - 碰盏日记`,
query: this.buildShareQuery()
}
},
isValidAvatar,
isIconPath,
// 构建分享名片的 query 参数(携带公开档案数据)
buildShareQuery() {
const parts = [`nick=${encodeURIComponent(this.user.nickname || '酒友')}`]
if (isValidAvatar(this.user.avatar)) parts.push(`avatar=${encodeURIComponent(this.user.avatar)}`)
parts.push(`days=${this.stats.totalDays || 0}`)
parts.push(`records=${this.stats.totalRecords || 0}`)
parts.push(`cups=${this.stats.totalCups || 0}`)
parts.push(`streak=${this.stats.streak || 0}`)
parts.push(`st=${this.stats.streakType || 'drank'}`)
if (this.stats.favCategory) parts.push(`fav=${this.stats.favCategory}`)
return parts.join('&')
},
async loadData() {
try {
const [userResp, statsResp, achieveResp] = await Promise.allSettled([
client.GetUserProfile({}),
client.GetStatsOverview({}),
GetAchievements()
])
// 用户信息
if (userResp.status === 'fulfilled' && userResp.value) {
this.user = userResp.value.data || userResp.value
} else {
try {
this.user = JSON.parse(uni.getStorageSync('user_info') || '{}')
} catch (e) { this.user = {} }
}
// 统计概览
if (statsResp.status === 'fulfilled' && statsResp.value) {
const s = statsResp.value.data || statsResp.value
this.stats = {
weekDrinkCount: s.weekDrinkCount || 0,
weekCups: s.weekCups || 0,
monthDrinkCount: s.monthDrinkCount || 0,
monthCups: s.monthCups || 0,
streak: s.streak || 0,
streakType: String(s.streakType || 'drank'),
totalDays: s.totalDays || 0,
totalRecords: s.totalRecords || 0,
totalCups: s.totalCups || 0,
favCategory: s.favCategory ? String(s.favCategory) : null,
categoryVariety: s.categoryVariety || 0
}
// 后端 streak 不区分饮酒/戒酒模式,拉取记录本地重算修正
this.refreshLocalStreak()
}
// 成就列表
if (achieveResp.status === 'fulfilled' && achieveResp.value) {
// SDK 已解包 data.data,成就数组在 achievements 键下
const payload = achieveResp.value.data || achieveResp.value
const list = payload.achievements || payload.list || (Array.isArray(payload) ? payload : [])
if (Array.isArray(list) && list.length > 0) {
this.achievementList = list.map((a, i) => ({
id: a.achievement_id || a.id,
name: a.name,
desc: a.desc,
// 后端返回的 icon 是不存在的图片路径,优先按成就 id 匹配本地 emoji 图标
icon: this.resolveAchievementIcon(a.achievement_id || a.id, i),
condition: a.condition || '',
unlocked: !!a.unlocked,
progress: a.progress || 0
}))
} else {
// API 返回空,降级本地计算
this.achievementList = checkAchievements(this.stats, ACHIEVEMENTS)
}
} else {
this.achievementList = checkAchievements(this.stats, ACHIEVEMENTS)
}
} catch (e) {
console.warn('加载个人数据失败:', e)
try {
this.user = JSON.parse(uni.getStorageSync('user_info') || '{}')
} catch (err) { this.user = {} }
this.achievementList = checkAchievements(this.stats, ACHIEVEMENTS)
}
},
showAchievementDetail(a) {
this.detailAchievement = a
this.showDetail = true
},
/** 后端 streak 不区分饮酒/戒酒模式,拉取全部记录后本地重算连续天数 */
async refreshLocalStreak() {
try {
const resp = await client.GetRecords({ page: 1, pageSize: 100 })
const payload = resp.data || resp
const list = payload.list || []
if (!list.length) return
const { streak, streakType, drankStreak, abstainStreak } = calcLocalStreak(list)
this.stats.streak = streak
this.stats.streakType = streakType
this.stats.drankStreak = drankStreak
this.stats.abstainStreak = abstainStreak
} catch (e) {
console.warn('本地计算连续天数失败:', e)
}
},
// 后端返回的 icon 是无效图片路径,优先按成就 id 匹配本地 ACHIEVEMENTS 的 emoji 图标,
// 匹配不到(后端新增成就)时再按索引循环兜底
resolveAchievementIcon(id, index) {
const local = ACHIEVEMENTS.find(a => a.id === id)
if (local) return local.icon
if (!ACHIEVEMENTS.length) return '🏅'
return ACHIEVEMENTS[index % ACHIEVEMENTS.length].icon
},
goCircle() {
this.$emit('switch-tab', 1)
},
goGames() {
uni.navigateTo({ url: '/pages/games/games' })
},
goHome() {
this.$emit('switch-tab', 0)
},
goRecord() {
if (!requireLogin()) return
uni.navigateTo({ url: '/pages/record/record' })
},
checkLoginState() {
// 微信审核要求:游客可停留在"我的"页浏览,不强制跳转登录,仅展示登录入口
this.isGuest = !isLoggedIn()
},
handleLogout() {
uni.showModal({
title: '退出登录',
content: '退出后本地数据将保留,重新登录后可同步',
confirmText: '退出',
confirmColor: '#FF6B6B',
success: (res) => {
if (res.confirm) {
clearAuth()
// 退出后以游客身份留在当前页继续浏览,不强制跳登录
this.isGuest = true
this.loadData()
uni.showToast({ title: '已退出登录', icon: 'none' })
}
}
})
},
shareTimeline() {
uni.showModal({
title: '分享到朋友圈',
content: '请点击右上角「···」菜单,选择「分享到朋友圈」',
showCancel: false,
confirmText: '知道了'
})
},
goLogin() {
uni.navigateTo({ url: '/pages/login/login' })
},
noop() {}
}
}
</script>
<style lang="scss" scoped>
.profile-page {
padding-bottom: calc(env(safe-area-inset-bottom) + 40rpx);
}
/* 游客登录引导卡片 */
.guest-login-card {
display: flex;
align-items: center;
gap: $sp-md;
margin: 0 $sp-lg $sp-md;
padding: $sp-md $sp-lg;
border-radius: $radius-lg;
background: rgba(232, 168, 56, 0.08);
border: 2rpx solid rgba(232, 168, 56, 0.25);
&:active {
background: rgba(232, 168, 56, 0.14);
}
}
.guest-login-icon {
width: 72rpx;
height: 72rpx;
border-radius: $radius-full;
background: $bg-card;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
flex-shrink: 0;
}
.guest-login-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
min-width: 0;
}
.guest-login-title {
font-size: $fs-sm;
font-weight: $fw-bold;
color: $text-primary;
}
.guest-login-desc {
font-size: $fs-xs;
color: $text-tertiary;
}
.guest-login-btn {
font-size: $fs-sm;
font-weight: $fw-bold;
color: $text-on-amber;
background: linear-gradient(135deg, $amber-light, $amber-deep);
padding: $sp-xs $sp-lg;
border-radius: $radius-full;
flex-shrink: 0;
}
/* === 沉浸式头部 === */
.hero-header {
position: relative;
padding: 0 $sp-lg 64rpx;
overflow: hidden;
}
.hero-share-btn {
position: absolute;
z-index: 10;
height: 64rpx;
padding: 0 32rpx;
display: flex;
align-items: center;
justify-content: center;
/* 玻璃模态效果:半透明白 + 强模糊 + 高光描边 */
background: rgba(255,255,255,0.14);
border: 1rpx solid rgba(255,255,255,0.30);
border-radius: $radius-full;
backdrop-filter: blur(20px) saturate(160%);
box-shadow: inset 0 1rpx 0 rgba(255,255,255,0.35), 0 8rpx 24rpx rgba(0,0,0,0.18);
&::after {
border: none;
}
&:active {
background: rgba(255,255,255,0.22);
transform: scale(0.94);
}
}
/* 分享胶囊按钮文字:与微信胶囊同构 */
.hero-share-icon {
font-size: $fs-sm;
color: $amber;
font-weight: $fw-bold;
line-height: 1;
letter-spacing: 4rpx;
text-shadow: 0 2rpx 8rpx rgba(0,0,0,0.15);
}
.hero-bg {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(ellipse 80% 60% at 50% 20%, rgba(232,168,56,0.08) 0%, transparent 70%);
}
.hero-orb {
position: absolute;
border-radius: 50%;
filter: blur(80rpx);
}
.hero-orb-1 {
width: 400rpx; height: 400rpx;
top: -120rpx; right: -100rpx;
background: rgba(232,168,56,0.07);
}
.hero-orb-2 {
width: 280rpx; height: 280rpx;
top: 60rpx; left: -80rpx;
background: rgba(139,133,184,0.06);
}
.hero-deco {
position: absolute;
right: -20rpx;
bottom: -30rpx;
font-size: 200rpx;
opacity: 0.06;
transform: rotate(-15deg);
z-index: 1;
}
.hero-content {
position: relative;
z-index: 2;
display: flex;
flex-direction: row;
align-items: center;
}
.avatar-ring {
width: 128rpx; height: 128rpx;
border-radius: 50%;
padding: 4rpx;
flex-shrink: 0;
background: linear-gradient(160deg, rgba(232,168,56,0.7), rgba(232,168,56,0.2), rgba(139,133,184,0.3));
box-shadow: 0 0 40rpx rgba(232,168,56,0.12);
}
.hero-avatar {
width: 100%; height: 100%;
border-radius: 50%;
}
.hero-avatar-placeholder {
background: $bg-card;
display: flex;
align-items: center;
justify-content: center;
}
.hero-avatar-icon {
font-size: 56rpx;
opacity: 0.5;
}
.hero-info {
flex: 1;
margin-left: $sp-xl;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.hero-greeting {
font-size: $fs-xs;
color: $text-tertiary;
margin-bottom: 14rpx;
letter-spacing: 1rpx;
}
.hero-name-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 14rpx;
margin-bottom: 16rpx;
}
.hero-name {
font-size: $fs-2xl;
font-weight: $fw-black;
color: $text-primary;
letter-spacing: -1rpx;
}
.hero-level-tag {
padding: 6rpx 18rpx;
background: rgba(232,168,56,0.10);
border: 1rpx solid rgba(232,168,56,0.20);
border-radius: $radius-full;
margin-left: 20rpx;
}
.hero-level-text {
font-size: 20rpx;
color: $amber;
font-weight: $fw-medium;
}
.hero-joined {
font-size: $fs-xs;
color: $text-tertiary;
opacity: 0.7;
line-height: $lh-normal;
}
/* === 悬浮数据卡片 === */
.stats-float-card {
display: flex;
align-items: center;
margin: -40rpx $sp-lg 0;
padding: $sp-xl $sp-md;
background: $bg-card;
border-radius: $radius-xl;
box-shadow: $shadow-md;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
position: relative;
z-index: 10;
}
.stats-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.stats-num {
font-size: $fs-xl;
font-weight: $fw-black;
color: $text-primary;
}
.stats-num-text {
font-size: $fs-lg;
}
.stats-label {
font-size: $fs-xs;
color: $text-tertiary;
}
.stats-divider {
width: 2rpx;
height: 48rpx;
background: var(--divider-color, rgba(255,255,255,0.08));
}
/* === 连续打卡 === */
.streak-section {
padding: $sp-lg $sp-lg 0;
}
.streak-row {
display: flex;
gap: $sp-md;
}
.streak-card {
flex: 1;
display: flex;
flex-direction: column;
padding: $sp-lg;
border-radius: $radius-lg;
position: relative;
overflow: hidden;
&.streak-drank {
background: linear-gradient(135deg, rgba(232,168,56,0.12) 0%, rgba(232,168,56,0.04) 100%);
border: 1rpx solid rgba(232,168,56,0.2);
}
&.streak-abstain {
background: linear-gradient(135deg, rgba(94,198,160,0.12) 0%, rgba(94,198,160,0.04) 100%);
border: 1rpx solid rgba(94,198,160,0.2);
}
}
.streak-card-top {
display: flex;
align-items: center;
gap: $sp-xs;
margin-bottom: 6rpx;
}
.streak-badge-emoji {
font-size: 32rpx;
}
.streak-title {
font-size: $fs-sm;
font-weight: $fw-bold;
color: $text-primary;
}
.streak-sub {
font-size: $fs-xs;
color: $text-tertiary;
margin-bottom: $sp-md;
}
.streak-num-row {
display: flex;
align-items: baseline;
gap: 4rpx;
}
.streak-days {
font-size: $fs-3xl;
font-weight: $fw-black;
color: $amber;
line-height: 1;
.streak-abstain & {
color: $mint;
}
}
.streak-days-unit {
font-size: $fs-sm;
color: $text-secondary;
font-weight: $fw-medium;
}
/* === 饮酒人格卡片 === */
.persona-card {
margin: $sp-lg $sp-lg 0;
border-radius: $radius-xl;
background: $bg-card;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
padding: $sp-xl;
position: relative;
overflow: hidden;
}
.persona-glow {
position: absolute;
width: 240rpx; height: 240rpx;
top: -80rpx; right: -60rpx;
background: radial-gradient(circle, rgba(232,168,56,0.10) 0%, transparent 70%);
border-radius: 50%;
pointer-events: none;
}
.persona-content {
position: relative;
z-index: 2;
}
.persona-header {
display: flex;
align-items: center;
gap: $sp-md;
margin-bottom: $sp-lg;
}
.persona-emoji {
font-size: 56rpx;
}
.persona-icon-img {
width: 64rpx;
height: 64rpx;
flex-shrink: 0;
}
.persona-text {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.persona-title {
font-size: $fs-lg;
font-weight: $fw-bold;
color: $text-primary;
}
.persona-desc {
font-size: $fs-xs;
color: $text-tertiary;
}
.persona-tags {
display: flex;
flex-wrap: wrap;
gap: $sp-sm;
}
.persona-tag {
padding: 8rpx 20rpx;
background: $bg-elevated;
border-radius: $radius-full;
border: 1rpx solid var(--border-micro, rgba(255,255,255,0.05));
text {
font-size: $fs-xs;
color: $text-secondary;
}
}
/* === 成就墙 === */
.achievements-section {
margin-top: $sp-xl;
padding: 0 $sp-lg;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $sp-lg;
}
.section-title {
font-size: $fs-lg;
font-weight: $fw-bold;
color: $text-primary;
}
.section-count {
font-size: $fs-sm;
color: $text-tertiary;
}
.achievements-scroll {
white-space: nowrap;
width: 100%;
}
.achievements-track {
display: inline-flex;
gap: $sp-md;
padding-bottom: $sp-sm;
}
.achievement-item {
display: inline-flex;
min-width: 180rpx;
}
/* === 操作入口 === */
.actions-section {
margin: $sp-xl $sp-lg 0;
display: flex;
flex-direction: column;
gap: $sp-sm;
}
.action-card {
display: flex;
align-items: center;
gap: $sp-md;
padding: $sp-lg;
background: $bg-card;
border-radius: $radius-lg;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
transition: all $duration-fast $ease-out;
line-height: 1.4;
text-align: left;
&::after {
border: none;
}
&:active {
transform: scale(0.98);
background: $bg-card-alt;
}
}
.action-icon-wrap {
width: 80rpx; height: 80rpx;
border-radius: $radius-md;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.action-icon-circle {
background: rgba(139,133,184,0.12);
}
.action-icon-games {
background: rgba(232,168,56,0.12);
}
.action-icon-timeline {
background: rgba(94,198,160,0.12);
}
.action-icon {
font-size: 36rpx;
}
.action-text {
flex: 1;
min-width: 0;
}
.action-title-row {
display: flex;
align-items: center;
gap: $sp-sm;
}
.action-title {
font-size: $fs-base;
font-weight: $fw-medium;
color: $text-primary;
}
.action-new-tag {
font-size: 18rpx;
font-weight: $fw-bold;
color: #fff;
background: linear-gradient(135deg, $amber, $amber-deep);
padding: 2rpx 10rpx;
border-radius: $radius-full;
}
.action-hot-tag {
font-size: 18rpx;
font-weight: $fw-bold;
color: #fff;
background: linear-gradient(135deg, $coral, #e04848);
padding: 2rpx 10rpx;
border-radius: $radius-full;
}
.action-desc {
display: block;
font-size: $fs-xs;
color: $text-tertiary;
margin-top: 4rpx;
}
.action-arrow {
font-size: $fs-xl;
color: $text-tertiary;
opacity: 0.5;
}
/* === 登出 === */
.logout-area {
padding: $sp-2xl $sp-lg $sp-xl;
display: flex;
justify-content: center;
}
.logout-btn {
min-width: 300rpx;
}
.logout-text-btn {
background: none;
border: none;
color: $text-tertiary;
font-size: $fs-sm;
padding: $sp-sm $sp-lg;
&:active {
color: $coral;
}
}
/* === 成就详情弹窗 === */
.detail-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--overlay-modal, rgba(0,0,0,0.7));
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: $sp-xl;
}
.detail-card {
width: 100%;
max-width: 580rpx;
text-align: center;
padding: $sp-2xl $sp-xl;
background: $bg-card;
border-radius: $radius-xl;
box-shadow: $shadow-lg;
border: 1rpx solid var(--border-faint, rgba(255,255,255,0.08));
}
.detail-icon-ring {
width: 140rpx; height: 140rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto $sp-lg;
&.ring-unlocked {
background: rgba(232,168,56,0.12);
border: 3rpx solid rgba(232,168,56,0.3);
box-shadow: 0 0 32rpx rgba(232,168,56,0.2);
}
&.ring-locked {
background: $bg-elevated;
border: 3rpx solid var(--border-faint, rgba(255,255,255,0.08));
}
}
.detail-emoji {
font-size: 64rpx;
}
.detail-icon-img {
width: 72rpx;
height: 72rpx;
}
.detail-name {
display: block;
font-size: $fs-xl;
font-weight: $fw-bold;
color: $text-primary;
margin-bottom: $sp-sm;
}
.detail-desc {
display: block;
font-size: $fs-sm;
color: $text-secondary;
margin-bottom: $sp-lg;
line-height: $lh-loose;
}
.detail-status {
display: inline-flex;
padding: $sp-sm $sp-lg;
border-radius: $radius-full;
font-size: $fs-sm;
font-weight: $fw-medium;
&.status-unlocked {
background: rgba(94,198,160,0.15);
color: $mint;
}
&.status-locked {
background: $bg-elevated;
color: $text-tertiary;
}
}
.detail-close-btn {
width: 100%;
margin-top: $sp-xl;
background: $bg-elevated;
color: $text-primary;
font-size: $fs-base;
font-weight: $fw-medium;
padding: $sp-md 0;
border-radius: $radius-full;
border: 1rpx solid var(--border-subtle, rgba(255,255,255,0.10));
&::after {
border: none;
}
&:active {
background: $bg-card-alt;
}
}
</style>