feat(nsti): add nicotine personality test flow (#36)

* fix: polish logs filter and stats money display

* fix: align floating tabs on logs and stats

* feat: enhance user profile and achievement features

- Add functionality for users to update their profile picture and nickname
- Implement achievement theme selection in onboarding
- Update API integration for profile updates and achievement themes
- Refine UI elements for better user interaction and experience

* feat: 梦想清单页与戒烟相关 API

- 梦想清单:系统导航栏、浮动添加、图标来自后台预设
- dream-presets API、pages.json 导航样式

Made-with: Cursor

* feat(nsti): add nicotine personality test flow
This commit is contained in:
hello-dd-code
2026-04-11 01:49:19 +08:00
committed by GitHub
parent ef36ca072b
commit ec87a9fc55
22 changed files with 4157 additions and 508 deletions
+138 -4
View File
@@ -6,10 +6,28 @@
<view class="section">
<text class="section-label">当前账号</text>
<view class="user-section card">
<image class="avatar" :src="userAvatar" mode="aspectFill"></image>
<!-- #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>
<text class="user-desc">已连接戒烟记录与统计数据</text>
<!-- #endif -->
<text class="user-desc">点击头像或昵称可修改</text>
<view class="user-meta">
<text class="user-pill">{{ modeText }}</text>
<text class="user-pill user-pill-muted">{{ shareToken ? '分享已启用' : '分享未生成' }}</text>
@@ -41,6 +59,19 @@
<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>
@@ -91,10 +122,12 @@
<script setup>
import { computed, ref, onMounted } from 'vue'
import { onShareAppMessage, onShow } from '@dcloudio/uni-app'
import * as api from '@/api'
import { useProfileStore } from '@/stores/profile'
import { useUserStore } from '@/stores/user'
import { useLogin } from '@/hooks/useLogin'
import { createShare } from '@/api/smoke'
import { updateUserProfile, uploadFile } from '@/api/auth'
import { getLatestNSTIResult } from '@/utils/nsti'
const profileStore = useProfileStore()
const userStore = useUserStore()
@@ -104,6 +137,7 @@ const shareToken = ref('')
const shareExpireAt = ref('')
const shareLoading = ref(false)
const navBarHeight = ref(0)
const latestNSTIResult = 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')
@@ -112,6 +146,9 @@ const modeText = computed(() => {
if (userStore.mode === 'record') return '记录抽烟'
return '未选择'
})
const nstiDesc = computed(() => latestNSTIResult.value
? `你上次测出:${latestNSTIResult.value.name}`
: '10 道题,测测你是哪一种抽象戒烟人格')
const shareDesc = computed(() => {
if (!shareToken.value) {
@@ -154,7 +191,7 @@ async function prepareShareToken(showToast = false) {
if (shareLoading.value) return
shareLoading.value = true
try {
const res = await api.createShare({ days: 7 })
const res = await createShare({ days: 7 })
shareToken.value = res.data?.share_token || ''
shareExpireAt.value = res.data?.expire_at || ''
if (showToast) {
@@ -184,10 +221,71 @@ function previewSharePage() {
})
}
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 clearCache() {
uni.showModal({
title: '清除缓存',
@@ -229,6 +327,7 @@ onShow(async () => {
await waitForLogin()
await profileStore.fetchProfile()
await prepareShareToken(false)
latestNSTIResult.value = getLatestNSTIResult()
})
</script>
@@ -283,6 +382,21 @@ onShow(async () => {
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;
@@ -298,6 +412,18 @@ onShow(async () => {
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;
@@ -368,6 +494,10 @@ onShow(async () => {
background: #F5F5F5;
}
.menu-icon-nsti {
background: linear-gradient(135deg, #FFE0F1 0%, #E6FFDE 100%);
}
.menu-glyph {
font-size: 30rpx;
font-weight: 700;
@@ -377,6 +507,10 @@ onShow(async () => {
color: #10B981;
}
.menu-icon-nsti .menu-glyph {
color: #C2187A;
}
.menu-icon-muted .menu-glyph {
color: #999999;
}