- 将应用名称从"喝了么"更改为"干杯日记" - 更新所有相关文件中的品牌标识和描述信息 - 升级HaveADrink依赖包从1.0.3到1.0.4版本 - 在pages.json中新增分享个人资料页面配置 - 重构DrinkCard组件UI,增加装饰背景、酒类图标支持、饮酒感受徽章等功能 - 优化API错误处理逻辑,增加静默模式支持 - 修改白酒类别图标为 urn emoji,黄酒图标为 tea emoji - 添加分享功能按钮并优化页面返回交互体验 - 引入新的工具函数用于获取酒类图标和路径判断 - 更新主题混入、常量定义等基础配置文件 - 调整全局样式和设计令牌以匹配新品牌形象
966 lines
24 KiB
Vue
966 lines
24 KiB
Vue
<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>
|
||
</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>
|
||
<text class="hero-name">{{ user.nickname || '酒友' }}</text>
|
||
<view class="hero-level-tag">
|
||
<text class="hero-level-text">{{ drinkLevel }}</text>
|
||
</view>
|
||
<text class="hero-joined">{{ joinedDate }} 加入</text>
|
||
</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="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">
|
||
<text 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="actions-section">
|
||
<!-- 酒友圈 -->
|
||
<view class="action-card" @click="goCircle">
|
||
<view class="action-icon-wrap action-icon-circle">
|
||
<text class="action-icon">🍻</text>
|
||
</view>
|
||
<view class="action-text">
|
||
<view class="action-title-row">
|
||
<text class="action-title">酒友圈</text>
|
||
<view class="action-new-tag">NEW</view>
|
||
</view>
|
||
<text class="action-desc">看看酒友们今晚都在喝什么</text>
|
||
</view>
|
||
<text class="action-arrow">›</text>
|
||
</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'">
|
||
<text 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 } 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
|
||
const map = { baijiu: '⚱️', beer: '🍺', wine: '🍷', whisky: '🥂', huangjiu: '🫖', sake: '🍶', fruit: '🍹', cocktail: '🍸' }
|
||
return map[fav] || '🍻'
|
||
},
|
||
personaTitle() {
|
||
const fav = this.stats.favCategory
|
||
const map = { baijiu: '白酒之魂', beer: '啤酒达人', wine: '红酒雅士', whisky: '威士忌行家', huangjiu: '黄酒知音', sake: '清酒爱好者', fruit: '果酒小清新', cocktail: '调酒师' }
|
||
return map[fav] || '多元品鉴师'
|
||
},
|
||
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: {
|
||
// 构建分享名片的 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.showToast({ title: '酒友圈即将上线,敬请期待 🍻', icon: 'none', duration: 2000 })
|
||
},
|
||
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 80rpx;
|
||
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-content {
|
||
position: relative;
|
||
z-index: 2;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding-top: $sp-lg;
|
||
}
|
||
|
||
.avatar-ring {
|
||
width: 168rpx; height: 168rpx;
|
||
border-radius: 50%;
|
||
padding: 4rpx;
|
||
background: linear-gradient(160deg, rgba(232,168,56,0.7), rgba(232,168,56,0.2), rgba(139,133,184,0.3));
|
||
margin-bottom: $sp-xl;
|
||
box-shadow: 0 0 48rpx rgba(232,168,56,0.15);
|
||
}
|
||
|
||
.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: 72rpx;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.hero-name {
|
||
font-size: $fs-2xl;
|
||
font-weight: $fw-black;
|
||
color: $text-primary;
|
||
margin-bottom: $sp-md;
|
||
letter-spacing: -1rpx;
|
||
}
|
||
|
||
.hero-level-tag {
|
||
padding: 8rpx 24rpx;
|
||
background: rgba(232,168,56,0.08);
|
||
border: 1rpx solid rgba(232,168,56,0.18);
|
||
border-radius: $radius-full;
|
||
margin-bottom: $sp-md;
|
||
}
|
||
|
||
.hero-level-text {
|
||
font-size: $fs-xs;
|
||
color: $amber;
|
||
font-weight: $fw-medium;
|
||
}
|
||
|
||
.hero-joined {
|
||
font-size: $fs-xs;
|
||
color: $text-tertiary;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* === 悬浮数据卡片 === */
|
||
.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-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-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-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-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>
|