Files
cg 57eef74eb1 feat(sdk): 升级HaveADrink SDK并集成邀请系统
- 将HaveADrink依赖从1.0.8升级至1.0.12版本
- 集成邀请码功能,新增common/invite.js处理邀请链路
- 实现发送好友请求返回邀请码的新流程
- 添加删除动态功能,新增DeleteFeed接口
- 集成UniAppWebSocket适配器,重构WebSocket连接管理
- 优化好友请求支持关键词搜索功能
- 在FeedCard组件中添加删除按钮和分享按钮
- 更新SDK类型定义文件以匹配新接口规范
2026-08-04 23:32:45 +08:00

1042 lines
26 KiB
Vue
Raw Permalink 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="themeClass" 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="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-card" :class="stats.streakType === 'drank' ? 'streak-drank' : 'streak-abstain'">
<view class="streak-left">
<view class="streak-badge">
<text class="streak-badge-emoji">{{ stats.streakType === 'drank' ? '🔥' : '💪' }}</text>
</view>
<view class="streak-info">
<text class="streak-title">{{ stats.streakType === 'drank' ? '连续饮酒' : '连续戒酒' }}</text>
<text class="streak-sub">{{ stats.streakType === 'drank' ? '保持记录,理性饮酒' : '自律即自由,继续坚持' }}</text>
</view>
</view>
<view class="streak-right">
<text class="streak-days text-num">{{ stats.streak }}</text>
<text class="streak-days-unit"></text>
</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 } from '../../common/api'
import { checkAchievements, getCatIcon, isIconPath } from '../../common/utils'
import { ACHIEVEMENTS, DRINK_CATEGORIES } from '../../common/constants'
import themeMixin from '../../common/theme-mixin'
export default {
mixins: [themeMixin],
components: { AchievementBadge },
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 '百天老友,酒中见真章'
}
},
onShow() {
this.checkLoginState()
this.loadData()
},
onShareAppMessage() {
const name = this.user.nickname || '我'
return {
title: `${name}的饮酒名片 - 碰盏日记`,
path: `/pages/share-profile/share-profile?${this.buildShareQuery()}`
}
},
onShareTimeline() {
const name = this.user.nickname || '我'
return {
title: `${name}的饮酒名片 - 碰盏日记`,
query: this.buildShareQuery()
}
},
methods: {
isIconPath,
// 构建分享名片的 query 参数(携带公开档案数据)
buildShareQuery() {
const parts = [`nick=${encodeURIComponent(this.user.nickname || '酒友')}`]
if (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({}),
client.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
}
}
// 成就列表
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 是不存在的图片路径,按顺序复用本地 emoji 图标
icon: this.resolveAchievementIcon(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
},
// 后端返回的 icon 是无效图片路径,按索引复用本地成就的 emoji 图标(超出后循环取)
resolveAchievementIcon(index) {
if (!ACHIEVEMENTS.length) return '🏅'
return ACHIEVEMENTS[index % ACHIEVEMENTS.length].icon
},
goCircle() {
uni.switchTab({ url: '/pages/circle/circle' })
},
goGames() {
uni.navigateTo({ url: '/pages/games/games' })
},
goHome() {
uni.switchTab({ url: '/pages/index/index' })
},
goRecord() {
uni.navigateTo({ url: '/pages/record/record' })
},
checkLoginState() {
const isLoggedIn = uni.getStorageSync('is_logged_in')
const isGuest = uni.getStorageSync('is_guest')
if (!isLoggedIn || isLoggedIn !== 'true') {
uni.reLaunch({ url: '/pages/login/login' })
return
}
this.isGuest = isGuest === 'true'
},
handleLogout() {
uni.showModal({
title: '退出登录',
content: '退出后本地数据将保留,重新登录后可同步',
confirmText: '退出',
confirmColor: '#FF6B6B',
success: (res) => {
if (res.confirm) {
clearAuth()
uni.reLaunch({ url: '/pages/login/login' })
}
}
})
},
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);
}
/* === 沉浸式头部 === */
.hero-header {
position: relative;
padding: 0 $sp-lg 64rpx;
overflow: hidden;
}
.hero-share-btn {
position: absolute;
z-index: 10;
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.06);
border: 1rpx solid rgba(255,255,255,0.10);
border-radius: 50%;
backdrop-filter: blur(10px);
&::after {
border: none;
}
&:active {
background: rgba(255,255,255,0.12);
transform: scale(0.9);
}
}
.hero-share-icon {
font-size: $fs-base;
color: $text-secondary;
font-weight: $fw-bold;
}
.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-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: $sp-xl;
border-radius: $radius-xl;
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-left {
display: flex;
align-items: center;
gap: $sp-md;
}
.streak-badge {
width: 88rpx; height: 88rpx;
border-radius: 50%;
background: $bg-card;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-sm;
}
.streak-badge-emoji {
font-size: 44rpx;
}
.streak-info {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.streak-title {
font-size: $fs-base;
font-weight: $fw-bold;
color: $text-primary;
}
.streak-sub {
font-size: $fs-xs;
color: $text-tertiary;
}
.streak-right {
display: flex;
align-items: baseline;
gap: 4rpx;
}
.streak-days {
font-size: $fs-hero;
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>