Files
hejiu/pages/profile/profile.vue
T
cg 860136bceb feat(theme): 实现日夜主题自动切换功能
- 添加日夜主题CSS变量定义,支持琥珀夜光和暖白琥珀两种风格
- 实现主题切换逻辑,根据时间自动切换白天(light)和夜间(dark)主题
- 在App.vue中集成主题初始化和token恢复功能
- 更新全局样式类应用主题颜色变量,包括卡片、文本、边框等
- 创建主题混入(mixin)供各页面使用,确保主题同步更新
- 实现导航栏和TabBar主题动态切换,提升用户体验
- 添加API服务层封装HaveADrink SDK,统一处理认证和请求
- 优化DrinkCard组件显示饮酒感受信息,丰富打卡记录展示
- 更新项目依赖配置,集成新的API客户端库
2026-07-16 22:47:38 +08:00

482 lines
12 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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>