Files
hejiu/pages/profile/profile.vue
T
cg b95243c8f8 docs(api): 更新API接口文档并调整应用配置
- 添加完整的后端接口文档,包含用户、打卡记录、统计等模块
- 修复应用名称从"喝酒了么"统一更正为"喝了么"
- 重构App.vue中的启动逻辑,增加数据迁移和路由守卫功能
- 调整DrinkCalendar组件的UI间距和尺寸参数
- 优化DrinkCard组件的品牌展示文案
- 修改manifest.json中的应用描述和微信小程序配置
- 调整pages.json中的页面路径顺序和tabBar配置
- 更新全局样式文件中的应用名称标识
- 修改mock数据中的用户头像默认值
- 优化卡片导出功能的绘制逻辑和视觉效果
2026-07-14 23:27:23 +08:00

421 lines
10 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="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 { getRecords, getUserInfo } from '../../common/mock-data'
import { calcStats, checkAchievements } from '../../common/utils'
import { ACHIEVEMENTS, DRINK_CATEGORIES } from '../../common/constants'
export default {
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: {
loadData() {
this.user = getUserInfo()
const records = getRecords()
this.stats = calcStats(records)
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) {
uni.removeStorageSync('is_logged_in')
uni.removeStorageSync('is_guest')
uni.removeStorageSync('user_info')
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;
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;
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;
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;
}
.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;
&: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: 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>