- 添加日夜主题CSS变量定义,支持琥珀夜光和暖白琥珀两种风格 - 实现主题切换逻辑,根据时间自动切换白天(light)和夜间(dark)主题 - 在App.vue中集成主题初始化和token恢复功能 - 更新全局样式类应用主题颜色变量,包括卡片、文本、边框等 - 创建主题混入(mixin)供各页面使用,确保主题同步更新 - 实现导航栏和TabBar主题动态切换,提升用户体验 - 添加API服务层封装HaveADrink SDK,统一处理认证和请求 - 优化DrinkCard组件显示饮酒感受信息,丰富打卡记录展示 - 更新项目依赖配置,集成新的API客户端库
482 lines
12 KiB
Vue
482 lines
12 KiB
Vue
<template>
|
||
<view :class="themeClass" class="page-container profile-page safe-bottom">
|
||
<!-- 用户头部 -->
|
||
<view class="profile-header">
|
||
<view class="profile-glow"></view>
|
||
<image v-if="user.avatar" class="profile-avatar" :src="user.avatar" mode="aspectFill"></image>
|
||
<view v-else class="profile-avatar profile-avatar-placeholder">
|
||
<text class="profile-avatar-icon">👤</text>
|
||
</view>
|
||
<text class="profile-name">{{ user.nickname }}</text>
|
||
<text class="profile-joined">加入于 {{ user.joinedAt || '2026' }}</text>
|
||
</view>
|
||
|
||
<!-- 饮酒档案统计 -->
|
||
<view class="stats-section">
|
||
<text class="section-title">饮酒档案</text>
|
||
<view class="stats-grid">
|
||
<view class="profile-stat card">
|
||
<text class="profile-stat-value text-num">{{ stats.totalDays }}</text>
|
||
<text class="profile-stat-label">打卡天数</text>
|
||
</view>
|
||
<view class="profile-stat card">
|
||
<text class="profile-stat-value text-num">{{ stats.totalRecords }}</text>
|
||
<text class="profile-stat-label">饮酒次数</text>
|
||
</view>
|
||
<view class="profile-stat card">
|
||
<text class="profile-stat-value text-num text-amber">{{ stats.totalCups }}</text>
|
||
<text class="profile-stat-label">标准杯</text>
|
||
</view>
|
||
<view class="profile-stat card">
|
||
<text class="profile-stat-value text-num">{{ favCategoryName }}</text>
|
||
<text class="profile-stat-label">最爱酒类</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 连续打卡 -->
|
||
<view class="streak-card card">
|
||
<view class="flex-between">
|
||
<view>
|
||
<text class="streak-label">{{ stats.streakType === 'drank' ? '连续饮酒' : '连续戒酒' }}</text>
|
||
<text class="streak-value text-num" :class="stats.streakType === 'drank' ? 'text-amber' : 'text-mint'">
|
||
{{ stats.streak }} 天
|
||
</text>
|
||
</view>
|
||
<text class="streak-emoji">{{ stats.streakType === 'drank' ? '🔥' : '💪' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 成就墙 -->
|
||
<view class="achievements-section">
|
||
<text class="section-title">成就徽章</text>
|
||
<view class="achievements-grid">
|
||
<AchievementBadge
|
||
v-for="a in achievementList"
|
||
:key="a.id"
|
||
:achievement="a"
|
||
@click="showAchievementDetail(a)"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 设置入口 -->
|
||
<view class="settings-section card">
|
||
<view class="settings-item" @click="showComingSoon('偏好设置')">
|
||
<text class="settings-icon">⚙️</text>
|
||
<text class="settings-name">饮酒偏好设置</text>
|
||
<text class="settings-arrow">›</text>
|
||
</view>
|
||
<view class="divider"></view>
|
||
<view class="settings-item" @click="showComingSoon('隐私设置')">
|
||
<text class="settings-icon">🔒</text>
|
||
<text class="settings-name">隐私设置</text>
|
||
<text class="settings-arrow">›</text>
|
||
</view>
|
||
<view class="divider"></view>
|
||
<view class="settings-item" @click="showComingSoon('关于')">
|
||
<text class="settings-icon">ℹ️</text>
|
||
<text class="settings-name">关于喝了么</text>
|
||
<text class="settings-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 登出/登录按钮 -->
|
||
<view class="logout-area">
|
||
<button v-if="isGuest" class="btn-ghost logout-btn" @click="goLogin">
|
||
登录账号,同步数据
|
||
</button>
|
||
<button v-else class="btn-text logout-btn" @click="handleLogout">
|
||
退出登录
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 成就详情弹窗 -->
|
||
<view v-if="showDetail" class="detail-overlay" @click="showDetail = false">
|
||
<view class="detail-card card-elevated" @click.stop>
|
||
<text class="detail-emoji">{{ detailAchievement.icon }}</text>
|
||
<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="btn-secondary" style="width: 100%; margin-top: 24rpx;" @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() {
|
||
return {
|
||
user: {},
|
||
stats: {},
|
||
achievementList: [],
|
||
showDetail: false,
|
||
detailAchievement: {},
|
||
isGuest: false
|
||
}
|
||
},
|
||
computed: {
|
||
favCategoryName() {
|
||
if (!this.stats.favCategory) return '-'
|
||
const cat = DRINK_CATEGORIES.find(c => c.id === this.stats.favCategory)
|
||
return cat ? cat.name : '-'
|
||
}
|
||
},
|
||
onShow() {
|
||
this.checkLoginState()
|
||
this.loadData()
|
||
},
|
||
methods: {
|
||
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) {
|
||
const list = achieveResp.value.list || achieveResp.value.data || achieveResp.value
|
||
if (Array.isArray(list) && list.length > 0) {
|
||
this.achievementList = list.map(a => ({
|
||
id: a.achievement_id || a.id,
|
||
name: a.name,
|
||
desc: a.desc,
|
||
icon: a.icon,
|
||
condition: a.condition || '',
|
||
unlocked: !!a.unlocked
|
||
}))
|
||
} 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
|
||
},
|
||
showComingSoon(name) {
|
||
uni.showToast({ title: `${name}即将上线`, icon: 'none' })
|
||
},
|
||
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' })
|
||
}
|
||
}
|
||
})
|
||
},
|
||
goLogin() {
|
||
uni.navigateTo({ url: '/pages/login/login' })
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.profile-page {
|
||
padding: $sp-lg;
|
||
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
/* 头部 */
|
||
.profile-header {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: $sp-2xl 0 $sp-xl;
|
||
position: relative;
|
||
}
|
||
|
||
.profile-glow {
|
||
position: absolute;
|
||
width: 300rpx;
|
||
height: 300rpx;
|
||
background: radial-gradient(circle, rgba(232,168,56,0.12) 0%, transparent 70%);
|
||
border-radius: 50%;
|
||
top: 0;
|
||
filter: blur(30rpx);
|
||
}
|
||
|
||
.profile-avatar {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
border-radius: $radius-full;
|
||
border: 4rpx solid $amber;
|
||
margin-bottom: $sp-lg;
|
||
position: relative;
|
||
z-index: 2;
|
||
}
|
||
|
||
.profile-avatar-placeholder {
|
||
background: $bg-elevated;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-style: dashed;
|
||
border-color: rgba(232,168,56,0.4);
|
||
}
|
||
|
||
.profile-avatar-icon {
|
||
font-size: 80rpx;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.profile-name {
|
||
font-size: $fs-xl;
|
||
font-weight: $fw-bold;
|
||
color: $text-primary;
|
||
margin-bottom: $sp-xs;
|
||
}
|
||
|
||
.profile-joined {
|
||
font-size: $fs-sm;
|
||
color: $text-secondary;
|
||
}
|
||
|
||
/* 统计 */
|
||
.stats-section {
|
||
margin-bottom: $sp-lg;
|
||
}
|
||
|
||
.section-title {
|
||
display: block;
|
||
font-size: $fs-lg;
|
||
font-weight: $fw-bold;
|
||
color: $text-primary;
|
||
margin-bottom: $sp-lg;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: $sp-md;
|
||
}
|
||
|
||
.profile-stat {
|
||
text-align: center;
|
||
padding: $sp-lg;
|
||
}
|
||
|
||
.profile-stat-value {
|
||
display: block;
|
||
font-size: $fs-2xl;
|
||
font-weight: $fw-bold;
|
||
color: $text-primary;
|
||
margin-bottom: $sp-xs;
|
||
}
|
||
|
||
.profile-stat-label {
|
||
font-size: $fs-xs;
|
||
color: $text-secondary;
|
||
}
|
||
|
||
/* 连续打卡 */
|
||
.streak-card {
|
||
margin-bottom: $sp-xl;
|
||
padding: $sp-xl;
|
||
}
|
||
|
||
.streak-label {
|
||
display: block;
|
||
font-size: $fs-sm;
|
||
color: $text-secondary;
|
||
margin-bottom: $sp-sm;
|
||
}
|
||
|
||
.streak-value {
|
||
display: block;
|
||
font-size: $fs-3xl;
|
||
font-weight: $fw-black;
|
||
color: $text-primary;
|
||
}
|
||
|
||
.streak-emoji {
|
||
font-size: $fs-hero;
|
||
}
|
||
|
||
/* 成就 */
|
||
.achievements-section {
|
||
margin-bottom: $sp-xl;
|
||
}
|
||
|
||
.achievements-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: $sp-md;
|
||
}
|
||
|
||
/* 设置 */
|
||
.settings-section {
|
||
margin-bottom: $sp-xl;
|
||
}
|
||
|
||
.settings-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: $sp-md;
|
||
padding: $sp-lg 0;
|
||
color: $text-primary;
|
||
|
||
&:active {
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
.settings-icon {
|
||
font-size: $fs-xl;
|
||
}
|
||
|
||
.settings-name {
|
||
flex: 1;
|
||
font-size: $fs-base;
|
||
}
|
||
|
||
.settings-arrow {
|
||
font-size: $fs-xl;
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
/* 登出区域 */
|
||
.logout-area {
|
||
padding: $sp-xl 0 $sp-3xl;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.logout-btn {
|
||
min-width: 300rpx;
|
||
}
|
||
|
||
/* 成就详情弹窗 */
|
||
.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: 600rpx;
|
||
text-align: center;
|
||
padding: $sp-2xl $sp-xl;
|
||
}
|
||
|
||
.detail-emoji {
|
||
font-size: 120rpx;
|
||
display: block;
|
||
margin-bottom: $sp-lg;
|
||
}
|
||
|
||
.detail-name {
|
||
display: block;
|
||
font-size: $fs-xl;
|
||
font-weight: $fw-bold;
|
||
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;
|
||
}
|
||
}
|
||
</style>
|