Files
smt/pages/onboarding/index.vue
T
你çšnepiedg 31e504a997 feat: 添加模式选择功能与页面更新
- 在 onboarding 页面中新增使用模式选择功能,用户可选择“戒烟打卡”或“记录抽烟”模式
- 更新个人资料页面以显示当前模式并允许用户切换模式
- 在 pages.json 中注册新的模式选择页面
- 优化首页和其他相关页面以适应新模式功能
2026-03-18 00:06:01 +08:00

579 lines
13 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' }">
<view class="nav-row">
<text class="step-indicator">{{ step }} / {{ totalSteps }}</text>
</view>
<view class="progress-bar">
<view class="progress-fill" :style="{ width: progressWidth }"></view>
</view>
</view>
<view class="content">
<view class="mode-section">
<text class="mode-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': currentMode === 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="step === 1" class="step">
<text class="step-title">你每天抽多少支烟</text>
<text class="step-desc">{{ baselineDesc }}</text>
<view class="input-group">
<view class="input-row">
<view class="input-btn" @tap="decreaseCigs">-</view>
<text class="input-value">{{ formData.baseline_cigs_per_day }}</text>
<view class="input-btn" @tap="increaseCigs">+</view>
</view>
<text class="input-unit">/</text>
</view>
</view>
<view v-if="step === 2" class="step">
<text class="step-title">你的烟龄是多久</text>
<text class="step-desc">了解你的吸烟历史有助于更好地帮助你</text>
<view class="options">
<view
v-for="option in smokingYearsOptions"
:key="option.value"
class="option"
:class="{ 'option-active': formData.smoking_years === option.value }"
@tap="formData.smoking_years = option.value"
>
{{ option.label }}
</view>
</view>
</view>
<view v-if="step === 3" class="step">
<text class="step-title">{{ motivationTitle }}</text>
<text class="step-desc">{{ motivationDesc }}</text>
<view class="options options-wrap">
<view
v-for="option in quitMotivationOptions"
:key="option"
class="option option-tag"
:class="{ 'option-active': formData.quit_motivations.includes(option) }"
@tap="toggleMotivation(option)"
>
{{ option }}
</view>
</view>
</view>
<view v-if="step === 4" class="step">
<text class="step-title">你通常什么时候起床和睡觉</text>
<text class="step-desc">我们会在你的休息时间避免打扰你</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 v-if="step === 5" class="step">
<text class="step-title">每包烟多少钱</text>
<text class="step-desc">我们会帮你计算省下的钱</text>
<view class="input-group">
<view class="price-input">
<text class="price-prefix">¥</text>
<input
type="digit"
v-model="priceYuan"
class="price-field"
placeholder="0"
placeholder-style="color: #6B7280"
/>
</view>
<text class="input-unit">/</text>
</view>
</view>
</view>
<view class="footer">
<view v-if="step > 1" class="btn-secondary" @tap="prevStep">上一步</view>
<view class="btn-primary" :class="{ 'btn-full': step === 1 }" @tap="nextStep">
{{ step === 5 ? finishButtonText + ' 🚀' : '下一步' }}
</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'
const profileStore = useProfileStore()
const userStore = useUserStore()
const { waitForLogin } = useLogin()
const navBarHeight = ref(0)
const step = ref(1)
const totalSteps = 5
const modeSaving = ref(false)
const modeOptions = [
{ value: 'quit', label: '戒烟打卡', desc: '按天记录今天没抽' },
{ value: 'record', label: '记录抽烟', desc: '按支数记录变化' }
]
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
})
const priceYuan = ref('25')
const progressWidth = computed(() => `${(step.value / totalSteps) * 100}%`)
const currentMode = computed(() => formData.value.mode || userStore.mode || 'record')
const isRecordMode = computed(() => currentMode.value === 'record')
const baselineDesc = computed(() => isRecordMode.value ? '这会成为你后续记录和统计的基线' : '这将帮助我们为你制定更合适的戒烟节奏')
const motivationTitle = computed(() => isRecordMode.value ? '你为什么想先开始记录抽烟?' : '你为什么想戒烟?')
const motivationDesc = computed(() => isRecordMode.value ? '选择最符合你当前状态的原因(可多选)' : '选择对你最重要的原因(可多选)')
const finishButtonText = computed(() => isRecordMode.value ? '开始记录之旅' : '开始戒烟之旅')
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 onWakeTimeChange(e) {
formData.value.wake_up_time = e.detail.value
}
function onSleepTimeChange(e) {
formData.value.sleep_time = e.detail.value
}
function prevStep() {
if (step.value > 1) {
step.value--
}
}
async function nextStep() {
if (step.value < totalSteps) {
step.value++
return
}
formData.value.pack_price_cent = Math.round(parseFloat(priceYuan.value || '0') * 100)
try {
uni.showLoading({ title: '保存中...' })
await profileStore.saveProfile(formData.value)
uni.hideLoading()
if (!formData.value.mode) {
uni.redirectTo({ url: '/pages/mode-select/index' })
return
}
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 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 : 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
}
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: linear-gradient(to bottom, #D1FAE5 0%, #F0FDF4 45%, #FFFFFF 100%);
display: flex;
flex-direction: column;
}
.nav-area {
padding-left: 32rpx;
padding-right: 32rpx;
background: linear-gradient(to bottom, #D1FAE5, #E9FDF2);
}
.nav-row {
display: flex;
justify-content: center;
align-items: center;
padding: 12rpx 0;
}
.step-indicator {
font-size: 24rpx;
font-weight: 600;
color: #059669;
background-color: rgba(255, 255, 255, 0.7);
padding: 6rpx 24rpx;
border-radius: 999rpx;
}
.progress-bar {
height: 6rpx;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 999rpx;
margin-top: 8rpx;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #10B981, #34D399);
border-radius: 999rpx;
transition: width 0.3s ease;
}
.content {
flex: 1;
padding: 0 48rpx;
display: flex;
flex-direction: column;
justify-content: center;
}
.mode-section {
margin-bottom: 40rpx;
}
.mode-section-label {
display: block;
margin-bottom: 16rpx;
font-size: 24rpx;
font-weight: 600;
color: #047857;
}
.mode-switch {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
}
.mode-switch-item {
padding: 24rpx;
border-radius: 20rpx;
background: rgba(255, 255, 255, 0.82);
border: 2rpx solid #d1fae5;
box-shadow: 0 8rpx 20rpx rgba(16, 185, 129, 0.08);
}
.mode-switch-item-active {
background: #ecfdf5;
border-color: #10b981;
}
.mode-switch-title {
display: block;
font-size: 28rpx;
font-weight: 700;
color: #111827;
}
.mode-switch-desc {
display: block;
margin-top: 10rpx;
font-size: 22rpx;
line-height: 1.5;
color: #6b7280;
}
.step {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.step-title {
font-size: 44rpx;
font-weight: 700;
color: #111827;
display: block;
margin-bottom: 16rpx;
line-height: 1.3;
}
.step-desc {
font-size: 28rpx;
color: #6B7280;
display: block;
margin-bottom: 56rpx;
}
.input-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.input-row {
display: flex;
align-items: center;
gap: 48rpx;
}
.input-btn {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
font-size: 48rpx;
color: #10B981;
border: 2rpx solid #ECFDF3;
box-shadow: 0 8rpx 20rpx rgba(16, 185, 129, 0.12);
}
.input-value {
font-size: 96rpx;
font-weight: 700;
color: #111827;
min-width: 160rpx;
text-align: center;
}
.input-unit {
font-size: 28rpx;
color: #6B7280;
}
.options {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.options-wrap {
flex-direction: row;
flex-wrap: wrap;
}
.option {
padding: 28rpx 36rpx;
background-color: #FFFFFF;
border-radius: 16rpx;
font-size: 30rpx;
color: #111827;
border: 2rpx solid #ECFDF3;
box-shadow: 0 4rpx 12rpx rgba(16, 185, 129, 0.06);
}
.option-tag {
padding: 20rpx 28rpx;
border-radius: 32rpx;
}
.option-active {
background-color: #ECFDF5;
border-color: #10B981;
color: #059669;
}
.time-row {
display: flex;
gap: 32rpx;
}
.time-item { flex: 1; }
.time-label {
font-size: 26rpx;
color: #6B7280;
display: block;
margin-bottom: 12rpx;
}
.time-picker {
background-color: #FFFFFF;
padding: 32rpx;
border-radius: 16rpx;
font-size: 40rpx;
color: #111827;
text-align: center;
border: 2rpx solid #ECFDF3;
box-shadow: 0 4rpx 12rpx rgba(16, 185, 129, 0.06);
}
.price-input {
display: flex;
align-items: center;
background-color: #FFFFFF;
padding: 24rpx 32rpx;
border-radius: 16rpx;
gap: 8rpx;
border: 2rpx solid #ECFDF3;
box-shadow: 0 4rpx 12rpx rgba(16, 185, 129, 0.06);
}
.price-prefix {
font-size: 48rpx;
color: #9CA3AF;
}
.price-field {
font-size: 64rpx;
font-weight: 700;
color: #111827;
width: 200rpx;
text-align: center;
}
.footer {
display: flex;
gap: 24rpx;
padding: 32rpx 48rpx;
padding-bottom: 64rpx;
}
.btn-primary {
flex: 1;
height: 96rpx;
background-color: #10B981;
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
color: #FFFFFF;
box-shadow: 0 12rpx 28rpx rgba(16, 185, 129, 0.25);
}
.btn-full { flex: 1; }
.btn-secondary {
height: 96rpx;
padding: 0 48rpx;
background-color: #FFFFFF;
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #111827;
border: 2rpx solid #E5E7EB;
}
</style>