Files
smt/src/pages/profile/index.vue
T
2026-04-27 00:53:15 +08:00

547 lines
12 KiB
Vue
Raw 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="page">
<view class="page-bg"></view>
<view class="nav-placeholder" :style="{ height: navBarHeight + 'px' }"></view>
<view class="section">
<text class="section-label">当前账号</text>
<view class="user-section card">
<!-- #ifdef MP-WEIXIN -->
<button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<image class="avatar" :src="userAvatar" mode="aspectFill" @tap="onChooseAvatarH5"></image>
<!-- #endif -->
<view class="user-copy">
<!-- #ifdef MP-WEIXIN -->
<input
type="nickname"
class="nickname-input"
:value="userStore.user?.nickname || ''"
placeholder="点击设置昵称"
@blur="onNicknameChange"
/>
<!-- #endif -->
<!-- #ifdef H5 -->
<text class="user-name">{{ userName }}</text>
<!-- #endif -->
<text class="user-desc">点击头像或昵称可修改</text>
<view class="user-meta">
<text class="user-pill">{{ modeText }}</text>
<text class="user-pill user-pill-muted">{{ achievementTitle }}</text>
</view>
</view>
</view>
</view>
<view class="section">
<text class="section-label">常用操作</text>
<view class="menu-list card">
<view class="menu-item">
<view class="menu-icon menu-icon-accent">
<text class="menu-glyph"></text>
</view>
<view class="menu-content">
<text class="menu-label">生成成就海报</text>
<text class="menu-desc">{{ posterDesc }}</text>
</view>
<button class="share-btn" @tap.stop="goAchievementPoster">
生成
</button>
</view>
<view class="menu-divider"></view>
<view class="menu-item" @tap="goSupervisor">
<view class="menu-icon menu-icon-accent">
<text class="menu-glyph"></text>
</view>
<view class="menu-content">
<text class="menu-label">监督人</text>
<text class="menu-desc">邀请朋友监督你或查看你监督的人</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-divider"></view>
<view class="menu-item" @tap="goNSTI">
<view class="menu-icon menu-icon-nsti">
<text class="menu-glyph"></text>
</view>
<view class="menu-content">
<text class="menu-label">赛博尼古丁测试</text>
<text class="menu-desc">{{ nstiDesc }}</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-divider"></view>
<view class="menu-item" @tap="goOnboarding">
<view class="menu-icon menu-icon-accent">
<text class="menu-glyph"></text>
</view>
<view class="menu-content">
<text class="menu-label">重新填写问卷</text>
<text class="menu-desc">修改打卡模式吸烟基线等个人信息</text>
</view>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<view class="section">
<text class="section-label">更多设置</text>
<view class="menu-list card">
<view class="menu-item" @tap="clearCache">
<view class="menu-icon menu-icon-muted">
<text class="menu-glyph"></text>
</view>
<view class="menu-content">
<text class="menu-label">清除缓存</text>
<text class="menu-desc">仅清理本地缓存不影响云端数据</text>
</view>
<text class="menu-arrow"></text>
</view>
<view class="menu-divider"></view>
<view class="menu-item" @tap="copyInfo">
<view class="menu-icon menu-icon-muted">
<text class="menu-glyph"></text>
</view>
<view class="menu-content">
<text class="menu-label">意见反馈</text>
<text class="menu-desc">复制反馈邮箱发送使用建议或问题</text>
</view>
<text class="menu-arrow"></text>
</view>
</view>
</view>
<text class="version">版本 1.0.0</text>
<view class="bottom-safe"></view>
</view>
</template>
<script setup>
import { computed, ref, onMounted } from 'vue'
import { onShareAppMessage, onShow } from '@dcloudio/uni-app'
import { useProfileStore } from '@/stores/profile'
import { useUserStore } from '@/stores/user'
import { useLogin } from '@/hooks/useLogin'
import { getAchievement } from '@/api/smoke'
import { updateUserProfile, uploadFile } from '@/api/auth'
import { getLatestNSTIResult } from '@/utils/nsti'
const profileStore = useProfileStore()
const userStore = useUserStore()
const { waitForLogin } = useLogin()
const navBarHeight = ref(0)
const latestNSTIResult = ref(null)
const achievementData = ref(null)
const userName = computed(() => userStore.user?.nickname || '戒烟用户')
const userAvatar = computed(() => userStore.user?.avatar_url || 'https://linghu-wmr.oss-cn-beijing.aliyuncs.com/smt/avatar.png')
const modeText = computed(() => {
if (userStore.mode === 'quit') return '戒烟打卡'
if (userStore.mode === 'record') return '记录抽烟'
return '未选择'
})
const nstiDesc = computed(() => latestNSTIResult.value
? `你上次测出:${latestNSTIResult.value.name}`
: '10 道题,测测你是哪一种抽象戒烟人格')
const currentAchievement = computed(() => achievementData.value?.current || null)
const achievementTitle = computed(() => currentAchievement.value?.name || '戒烟新星')
const achievementTheme = computed(() => achievementData.value?.theme_name || '少抽成就')
const achievementScore = computed(() => Number(achievementData.value?.score) || 0)
const posterDesc = computed(() => {
if (!achievementData.value) return '生成你的等级称号、少抽进度和节省成果'
return `${achievementTheme.value} · ${achievementTitle.value} · 积分 ${achievementScore.value}`
})
function setupNavBar() {
const systemInfo = uni.getSystemInfoSync()
const statusBarH = systemInfo.statusBarHeight || 0
try {
const menuBtn = uni.getMenuButtonBoundingClientRect()
navBarHeight.value = menuBtn.bottom + (menuBtn.top - statusBarH)
} catch (e) {
navBarHeight.value = statusBarH + 44
}
}
async function fetchPosterData() {
try {
const achievementRes = await getAchievement()
achievementData.value = achievementRes.data?.achievement || null
} catch (e) {
console.error('fetchPosterData error:', e)
}
}
async function onChooseAvatar(e) {
const avatarUrl = e.detail.avatarUrl
if (!avatarUrl) return
try {
uni.showLoading({ title: '上传头像...' })
const uploadRes = await uploadFile(avatarUrl)
await doUpdateProfile({ avatar_url: uploadRes.url })
} catch (err) {
console.error('头像上传失败:', err)
uni.showToast({ title: '头像更新失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
function onChooseAvatarH5() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album'],
success: async (res) => {
const tempPath = res.tempFilePaths[0]
try {
uni.showLoading({ title: '上传头像...' })
const uploadRes = await uploadFile(tempPath)
await doUpdateProfile({ avatar_url: uploadRes.url })
} catch (err) {
console.error('头像上传失败:', err)
uni.showToast({ title: '头像更新失败', icon: 'none' })
} finally {
uni.hideLoading()
}
}
})
}
async function onNicknameChange(e) {
const nickname = (e.detail.value || '').trim()
if (!nickname || nickname === userStore.user?.nickname) return
try {
await doUpdateProfile({ nickname })
} catch (err) {
uni.showToast({ title: '昵称更新失败', icon: 'none' })
}
}
async function doUpdateProfile(data) {
try {
const res = await updateUserProfile(data)
userStore.updateUser(res.data)
uni.showToast({ title: '修改成功', icon: 'success' })
} catch (e) {
console.error('doUpdateProfile error:', e)
uni.showToast({ title: '修改失败', icon: 'none' })
}
}
function goOnboarding() {
uni.navigateTo({ url: '/pages/onboarding/index' })
}
function goNSTI() {
uni.navigateTo({ url: '/pages/nsti/index' })
}
function goSupervisor() {
uni.navigateTo({ url: '/pages/supervisor/index' })
}
function goAchievementPoster() {
uni.navigateTo({ url: '/pages/share/index' })
}
function clearCache() {
uni.showModal({
title: '清除缓存',
content: '将清除本地缓存数据,不会影响云端记录',
success: (res) => {
if (res.confirm) {
try {
uni.clearStorageSync()
uni.showToast({ title: '缓存已清除', icon: 'success' })
} catch (e) {
uni.showToast({ title: '清除失败', icon: 'none' })
}
}
}
})
}
function copyInfo() {
uni.setClipboardData({
data: '806669289@qq.com',
success: () => {
uni.showToast({ title: '反馈邮箱已复制', icon: 'success' })
}
})
}
onShareAppMessage(() => {
return {
title: `${userName.value}正在解锁「${achievementTitle.value}`,
path: 'pages/index/index'
}
})
onMounted(() => {
setupNavBar()
})
onShow(async () => {
await waitForLogin()
await profileStore.fetchProfile()
await fetchPosterData()
latestNSTIResult.value = getLatestNSTIResult()
})
</script>
<style scoped>
.page {
min-height: 100vh;
position: relative;
background-color: #F5F7F6;
padding: 0 32rpx 0;
box-sizing: border-box;
}
.page-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 500rpx;
background: linear-gradient(180deg, #DDF3EB 0%, #F5F7F6 100%);
z-index: 0;
}
.nav-placeholder,
.section,
.version {
position: relative;
z-index: 1;
}
.section {
margin-bottom: 36rpx;
}
.section-label {
display: block;
margin: 0 0 16rpx 16rpx;
font-size: 28rpx;
font-weight: 600;
color: #666666;
}
.card {
background: #FFFFFF;
border-radius: 32rpx;
padding: 32rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
}
.user-section {
display: flex;
align-items: center;
gap: 32rpx;
}
.avatar-btn {
padding: 0;
margin: 0;
background: transparent;
border: none;
line-height: 1;
width: 128rpx;
height: 128rpx;
flex-shrink: 0;
}
.avatar-btn::after {
border: none;
}
.avatar {
width: 128rpx;
height: 128rpx;
border-radius: 50%;
background-color: #F0F4F2;
flex-shrink: 0;
}
.user-copy {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.nickname-input {
font-size: 38rpx;
font-weight: 700;
color: #1A1A1A;
background: transparent;
border: none;
padding: 0;
height: 56rpx;
line-height: 56rpx;
width: 100%;
}
.user-name {
font-size: 38rpx;
font-weight: 700;
color: #1A1A1A;
}
.user-desc {
display: block;
margin-top: 8rpx;
font-size: 26rpx;
color: #999999;
}
.user-meta {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 16rpx;
}
.user-pill {
padding: 8rpx 20rpx;
border-radius: 999rpx;
background: #E8F5F0;
font-size: 22rpx;
font-weight: 600;
color: #10B981;
}
.user-pill-muted {
background: #F5F5F5;
color: #999999;
}
.menu-list {
padding: 8rpx 32rpx;
}
.menu-item {
display: flex;
align-items: center;
gap: 24rpx;
padding: 24rpx 0;
background: transparent;
}
.menu-divider {
height: 1px;
background: #F0F0F0;
margin: 0;
}
.menu-icon {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.menu-icon-accent {
background: #E8F5F0;
}
.menu-icon-muted {
background: #F5F5F5;
}
.menu-icon-nsti {
background: linear-gradient(135deg, #FFE0F1 0%, #E6FFDE 100%);
}
.menu-glyph {
font-size: 30rpx;
font-weight: 700;
}
.menu-icon-accent .menu-glyph {
color: #10B981;
}
.menu-icon-nsti .menu-glyph {
color: #C2187A;
}
.menu-icon-muted .menu-glyph {
color: #999999;
}
.menu-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.menu-label {
font-size: 30rpx;
color: #1A1A1A;
font-weight: 600;
}
.menu-desc {
font-size: 24rpx;
line-height: 1.4;
color: #999999;
}
.share-btn {
margin: 0;
padding: 12rpx 32rpx;
line-height: 1.5;
font-size: 26rpx;
border: none;
border-radius: 999rpx;
color: #FFFFFF;
background: #10B981;
font-weight: 600;
}
.share-btn[disabled] {
background: #A7F3D0;
color: #FFFFFF;
}
.share-btn::after {
border: none;
}
.menu-arrow {
font-size: 36rpx;
color: #CCCCCC;
}
.version {
display: block;
text-align: center;
font-size: 24rpx;
color: #B0B0B0;
margin-top: 40rpx;
}
.bottom-safe {
height: calc(40rpx + env(safe-area-inset-bottom));
}
</style>