Files
smt/src/pages/onboarding/index.vue
T
2026-04-25 00:32:12 +08:00

759 lines
17 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="nav-area" :style="{ paddingTop: navBarHeight + 'px' }">
<text class="nav-title">完善你的信息</text>
<text class="nav-subtitle">帮助我们更好地为你服务</text>
</view>
<scroll-view class="content" scroll-y>
<!-- 使用模式 -->
<view class="form-section">
<text class="section-label">使用模式</text>
<view class="mode-switch">
<view
v-for="item in modeOptions"
:key="item.value"
class="mode-switch-item"
:class="{ 'mode-switch-item-active': formData.mode === item.value }"
@tap="selectMode(item.value)"
>
<text class="mode-switch-title">{{ item.label }}</text>
<text class="mode-switch-desc">{{ item.desc }}</text>
</view>
</view>
</view>
<!-- 戒烟日期戒烟模式可见 -->
<view v-if="formData.mode === 'quit'" class="form-section">
<text class="section-label">戒烟开始日期</text>
<picker mode="date" :value="formData.quit_date" @change="onQuitDateChange">
<view class="time-picker">{{ formData.quit_date || '请选择日期' }}</view>
</picker>
</view>
<!-- 每天抽烟数量 -->
<view class="form-section">
<view class="section-row">
<text class="section-label">每天抽烟</text>
<view class="inline-input">
<view class="inline-btn" @tap="decreaseCigs">-</view>
<text class="inline-value">{{ formData.baseline_cigs_per_day }}</text>
<view class="inline-btn" @tap="increaseCigs">+</view>
<text class="inline-unit">/</text>
</view>
</view>
</view>
<!-- 烟龄 -->
<view class="form-section">
<text class="section-label">烟龄</text>
<view class="options-row">
<view
v-for="option in smokingYearsOptions"
:key="option.value"
class="option-tag"
:class="{ 'option-tag-active': formData.smoking_years === option.value }"
@tap="formData.smoking_years = option.value"
>
{{ option.label }}
</view>
</view>
</view>
<!-- 戒烟动机 -->
<view class="form-section">
<text class="section-label">{{ formData.mode === 'quit' ? '戒烟原因' : '记录原因' }}可多选</text>
<view class="options-row">
<view
v-for="option in quitMotivationOptions"
:key="option"
class="option-tag"
:class="{ 'option-tag-active': formData.quit_motivations.includes(option) }"
@tap="toggleMotivation(option)"
>
{{ option }}
</view>
</view>
</view>
<!-- 作息时间 -->
<view class="form-section">
<text class="section-label">作息时间</text>
<view class="time-row">
<view class="time-item">
<text class="time-label">起床</text>
<picker mode="time" :value="formData.wake_up_time" @change="onWakeTimeChange">
<view class="time-picker">{{ formData.wake_up_time }}</view>
</picker>
</view>
<view class="time-item">
<text class="time-label">睡觉</text>
<picker mode="time" :value="formData.sleep_time" @change="onSleepTimeChange">
<view class="time-picker">{{ formData.sleep_time }}</view>
</picker>
</view>
</view>
</view>
<!-- 烟价 -->
<view class="form-section">
<view class="section-row">
<text class="section-label">每包烟价格</text>
<view class="inline-input">
<text class="inline-unit">¥</text>
<input
type="digit"
v-model="priceYuan"
class="inline-field"
placeholder="25"
placeholder-style="color: #CCCCCC"
/>
<text class="inline-unit">/</text>
</view>
</view>
</view>
<!-- 成就风格 -->
<view v-if="achievementThemes.length > 0" class="form-section">
<view class="theme-section-head">
<view>
<text class="section-label">成就称号风格</text>
<text class="section-hint">选择一套更能激励你的成长称号</text>
</view>
<text class="theme-section-badge">可更换</text>
</view>
<view class="theme-list">
<view
v-for="theme in achievementThemes"
:key="theme.id"
class="theme-card"
:class="{ 'theme-card-active': formData.achievement_theme_id === theme.id }"
@tap="formData.achievement_theme_id = theme.id"
>
<view class="theme-glow"></view>
<view class="theme-header">
<view class="theme-icon-wrap">
<text class="theme-icon">{{ theme.icon }}</text>
</view>
<view class="theme-title-wrap">
<text class="theme-name">{{ theme.name }}</text>
<text class="theme-desc">打卡进度会逐步解锁称号</text>
</view>
<view class="theme-check">
<text v-if="formData.achievement_theme_id === theme.id"></text>
</view>
</view>
<view class="theme-levels">
<text
v-for="(level, idx) in theme.levels"
:key="level.id"
class="theme-level"
>{{ level.name }}</text>
</view>
</view>
</view>
</view>
<view class="bottom-space"></view>
</scroll-view>
<view class="footer">
<view class="btn-primary" @tap="handleSubmit">
<text class="btn-text">{{ submitButtonText }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShareAppMessage } from '@dcloudio/uni-app'
import { useProfileStore } from '@/stores/profile'
import { useUserStore } from '@/stores/user'
import { useLogin } from '@/hooks/useLogin'
import * as api from '@/api'
const profileStore = useProfileStore()
const userStore = useUserStore()
const { waitForLogin } = useLogin()
const navBarHeight = ref(0)
const modeSaving = ref(false)
const achievementThemes = ref([])
const modeOptions = [
{ value: 'quit', label: '戒烟打卡', desc: '按天打卡,坚持戒烟' },
{ value: 'record', label: '记录抽烟', desc: '按支数记录变化趋势' }
]
function todayStr() {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
const formData = ref({
mode: 'record',
baseline_cigs_per_day: 10,
smoking_years: 5,
quit_motivations: ['身体健康'],
smoke_motivations: [],
wake_up_time: '07:30',
sleep_time: '23:00',
pack_price_cent: 2500,
quit_date: todayStr(),
achievement_theme_id: null
})
const priceYuan = ref('25')
const submitButtonText = computed(() => formData.value.mode === 'quit' ? '开始戒烟之旅' : '开始记录之旅')
const smokingYearsOptions = [
{ label: '<1年', value: 1 },
{ label: '1-3年', value: 2 },
{ label: '3-5年', value: 4 },
{ label: '5-10年', value: 7 },
{ label: '>10年', value: 15 }
]
const quitMotivationOptions = [
'身体健康',
'家人孩子',
'省钱',
'形象气质',
'工作需要',
'伴侣要求'
]
function increaseCigs() {
formData.value.baseline_cigs_per_day++
}
function decreaseCigs() {
if (formData.value.baseline_cigs_per_day > 1) {
formData.value.baseline_cigs_per_day--
}
}
function toggleMotivation(option) {
const index = formData.value.quit_motivations.indexOf(option)
if (index > -1) {
formData.value.quit_motivations.splice(index, 1)
} else {
formData.value.quit_motivations.push(option)
}
}
async function selectMode(mode) {
formData.value.mode = mode
userStore.setMode(mode)
if (!profileStore.exists || modeSaving.value) return
modeSaving.value = true
try {
await profileStore.saveProfile({ mode })
} catch (e) {
console.error('saveModeInOnboarding error:', e)
} finally {
modeSaving.value = false
}
}
function onQuitDateChange(e) {
formData.value.quit_date = e.detail.value
}
function onWakeTimeChange(e) {
formData.value.wake_up_time = e.detail.value
}
function onSleepTimeChange(e) {
formData.value.sleep_time = e.detail.value
}
async function handleSubmit() {
formData.value.pack_price_cent = Math.round(parseFloat(priceYuan.value || '0') * 100)
try {
uni.showLoading({ title: '保存中...' })
await profileStore.saveProfile(formData.value)
uni.hideLoading()
uni.switchTab({ url: '/pages/index/index' })
} catch (e) {
uni.hideLoading()
uni.showToast({ title: '保存失败', icon: 'none' })
}
}
onMounted(async () => {
const sys = uni.getSystemInfoSync()
const statusBarH = sys.statusBarHeight || 0
try {
const menuBtn = uni.getMenuButtonBoundingClientRect()
navBarHeight.value = menuBtn.bottom + (menuBtn.top - statusBarH)
} catch (e) {
navBarHeight.value = statusBarH + 44
}
await waitForLogin()
try {
const res = await api.getAchievementThemes()
achievementThemes.value = res.data?.themes || []
if (achievementThemes.value.length > 0 && !formData.value.achievement_theme_id) {
formData.value.achievement_theme_id = achievementThemes.value[0].id
}
} catch (e) {
console.error('loadAchievementThemes error:', e)
}
try {
const profileData = await profileStore.fetchProfile()
if (profileData?.profile) {
const profile = profileData.profile
formData.value = {
...formData.value,
mode: profile.mode || userStore.mode || formData.value.mode,
baseline_cigs_per_day: profile.baseline_cigs_per_day || formData.value.baseline_cigs_per_day,
smoking_years: profile.smoking_years || formData.value.smoking_years,
quit_motivations: Array.isArray(profile.quit_motivations) && profile.quit_motivations.length > 0
? profile.quit_motivations
: formData.value.quit_motivations,
smoke_motivations: Array.isArray(profile.smoke_motivations) ? profile.smoke_motivations : formData.value.smoke_motivations,
wake_up_time: profile.wake_up_time || formData.value.wake_up_time,
sleep_time: profile.sleep_time || formData.value.sleep_time,
pack_price_cent: profile.pack_price_cent || formData.value.pack_price_cent,
quit_date: profile.quit_date ? profile.quit_date.split('T')[0] : formData.value.quit_date,
achievement_theme_id: profile.achievement_theme_id || formData.value.achievement_theme_id
}
if (profile.pack_price_cent) {
priceYuan.value = String((profile.pack_price_cent / 100).toFixed(2)).replace(/\.00$/, '')
}
} else if (userStore.mode) {
formData.value.mode = userStore.mode
}
} catch (e) {
console.error('loadProfileForOnboarding error:', e)
}
})
onShareAppMessage(() => {
return {
title: '戒烟助手 - 帮你定制戒烟计划',
path: 'pages/index/index'
}
})
</script>
<style scoped>
.page {
min-height: 100vh;
background-color: #F5F7F6;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow-x: hidden;
width: 100%;
}
.nav-area {
padding: 20rpx 32rpx 16rpx;
background: linear-gradient(180deg, #DDF3EB 0%, #F5F7F6 100%);
box-sizing: border-box;
}
.nav-title {
display: block;
font-size: 38rpx;
font-weight: 700;
color: #1A1A1A;
}
.nav-subtitle {
display: block;
margin-top: 6rpx;
font-size: 24rpx;
color: #999999;
}
.content {
flex: 1;
padding: 0 32rpx;
box-sizing: border-box;
width: 100%;
}
.form-section {
margin-bottom: 20rpx;
background: #FFFFFF;
border-radius: 32rpx;
padding: 28rpx 24rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
box-sizing: border-box;
}
.section-label {
display: block;
margin-bottom: 18rpx;
font-size: 28rpx;
font-weight: 600;
color: #666666;
}
.section-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-row .section-label {
margin-bottom: 0;
}
.mode-switch {
display: flex;
gap: 16rpx;
}
.mode-switch-item {
flex: 1;
padding: 24rpx 20rpx;
border-radius: 20rpx;
background: #F9FBFA;
border: 2rpx solid #F0F0F0;
transition: all 0.2s;
text-align: center;
box-sizing: border-box;
}
.mode-switch-item-active {
background: #E8F5F0;
border-color: #10B981;
}
.mode-switch-title {
display: block;
font-size: 28rpx;
font-weight: 700;
color: #1A1A1A;
}
.mode-switch-item-active .mode-switch-title {
color: #10B981;
}
.mode-switch-desc {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
line-height: 1.5;
color: #999999;
}
.mode-switch-item-active .mode-switch-desc {
color: #10B981;
}
.inline-input {
display: flex;
align-items: center;
gap: 16rpx;
}
.inline-btn {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: #E8F5F0;
display: flex;
align-items: center;
justify-content: center;
font-size: 38rpx;
color: #10B981;
}
.inline-value {
font-size: 38rpx;
font-weight: 700;
color: #1A1A1A;
min-width: 52rpx;
text-align: center;
}
.inline-unit {
font-size: 24rpx;
color: #999999;
}
.inline-field {
width: 100rpx;
font-size: 32rpx;
font-weight: 600;
color: #1A1A1A;
text-align: center;
background: #F5F7F6;
padding: 10rpx 14rpx;
border-radius: 12rpx;
}
.options-row {
display: flex;
flex-wrap: wrap;
gap: 14rpx;
}
.option-tag {
padding: 14rpx 26rpx;
border-radius: 999rpx;
background: #F5F5F5;
font-size: 26rpx;
color: #666666;
}
.option-tag-active {
background: #E8F5F0;
color: #10B981;
font-weight: 600;
}
.time-row {
display: flex;
gap: 20rpx;
}
.time-item {
flex: 1;
}
.time-label {
font-size: 22rpx;
color: #999999;
display: block;
margin-bottom: 8rpx;
}
.time-picker {
background: #F5F7F6;
padding: 18rpx 20rpx;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
color: #1A1A1A;
text-align: center;
}
.section-hint {
display: block;
font-size: 22rpx;
color: #999999;
margin-top: -10rpx;
margin-bottom: 16rpx;
}
.theme-section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 18rpx;
}
.theme-section-head .section-hint {
margin-bottom: 0;
}
.theme-section-badge {
flex-shrink: 0;
padding: 8rpx 16rpx;
border-radius: 999rpx;
background: rgba(110, 231, 183, 0.16);
border: 1rpx solid rgba(16, 185, 129, 0.16);
font-size: 20rpx;
font-weight: 700;
color: #0F766E;
}
.theme-list {
display: flex;
flex-direction: column;
gap: 18rpx;
}
.theme-card {
position: relative;
overflow: hidden;
padding: 24rpx;
border-radius: 28rpx;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.92));
border: 2rpx solid rgba(226, 232, 240, 0.9);
box-sizing: border-box;
box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.04);
}
.theme-card-active {
background:
radial-gradient(circle at top right, rgba(103, 232, 249, 0.28), transparent 34%),
linear-gradient(135deg, rgba(236, 253, 245, 0.98), rgba(240, 249, 255, 0.94));
border-color: rgba(16, 185, 129, 0.72);
box-shadow: 0 18rpx 42rpx rgba(16, 185, 129, 0.13);
}
.theme-glow {
position: absolute;
top: -70rpx;
right: -70rpx;
width: 190rpx;
height: 190rpx;
border-radius: 50%;
background: rgba(110, 231, 183, 0.12);
}
.theme-card-active .theme-glow {
background: rgba(103, 232, 249, 0.28);
}
.theme-header {
position: relative;
display: flex;
align-items: center;
gap: 18rpx;
margin-bottom: 20rpx;
}
.theme-icon-wrap {
width: 72rpx;
height: 72rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
background: rgba(241, 245, 249, 0.92);
border: 1rpx solid rgba(226, 232, 240, 0.9);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.7);
flex-shrink: 0;
}
.theme-card-active .theme-icon-wrap {
background: linear-gradient(135deg, #6EE7B7, #67E8F9);
border-color: rgba(255, 255, 255, 0.9);
box-shadow: 0 12rpx 26rpx rgba(20, 184, 166, 0.18);
}
.theme-icon {
font-size: 36rpx;
line-height: 1;
}
.theme-title-wrap {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.theme-name {
font-size: 29rpx;
line-height: 1.25;
font-weight: 800;
color: #1E293B;
}
.theme-desc {
font-size: 21rpx;
line-height: 1.4;
color: #64748B;
}
.theme-card-active .theme-name {
color: #0F766E;
}
.theme-check {
width: 42rpx;
height: 42rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid rgba(148, 163, 184, 0.34);
background: rgba(255, 255, 255, 0.78);
color: #FFFFFF;
font-size: 24rpx;
font-weight: 900;
flex-shrink: 0;
}
.theme-card-active .theme-check {
border-color: transparent;
background: linear-gradient(135deg, #10B981, #06B6D4);
box-shadow: 0 8rpx 18rpx rgba(16, 185, 129, 0.2);
}
.theme-levels {
position: relative;
display: flex;
flex-wrap: wrap;
gap: 10rpx;
}
.theme-level {
max-width: 100%;
padding: 8rpx 14rpx;
border-radius: 999rpx;
background: rgba(241, 245, 249, 0.95);
border: 1rpx solid rgba(226, 232, 240, 0.9);
font-size: 21rpx;
line-height: 1.35;
font-weight: 700;
color: #64748B;
}
.theme-card-active .theme-level {
background: rgba(255, 255, 255, 0.72);
border-color: rgba(16, 185, 129, 0.15);
color: #0F766E;
}
.theme-card-active .theme-level:first-child {
background: rgba(251, 191, 36, 0.18);
border-color: rgba(251, 191, 36, 0.22);
color: #B45309;
}
.theme-arrow {
display: none;
}
.bottom-space {
height: 160rpx;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: linear-gradient(180deg, transparent 0%, rgba(245, 247, 246, 0.97) 35%);
}
.btn-primary {
height: 96rpx;
background: #10B981;
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(16, 185, 129, 0.25);
}
.btn-text {
font-size: 32rpx;
font-weight: 600;
color: #FFFFFF;
letter-spacing: 1rpx;
}
</style>