Files
smt/src/pages/onboarding/index.vue
T
2026-03-18 19:24:51 +08:00

584 lines
14 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:
radial-gradient(circle at top left, rgba(52, 200, 160, 0.16), transparent 30%),
radial-gradient(circle at top right, rgba(255, 255, 255, 0.92), transparent 22%),
linear-gradient(180deg, #edf2f8 0%, #f5f7fb 38%, #fbfdff 100%);
display: flex;
flex-direction: column;
}
.nav-area {
padding-left: 32rpx;
padding-right: 32rpx;
background: transparent;
}
.nav-row {
display: flex;
justify-content: center;
align-items: center;
padding: 12rpx 0;
}
.step-indicator {
font-size: 24rpx;
font-weight: 600;
color: #667085;
background-color: rgba(255, 255, 255, 0.76);
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, #32c59d, #1aa37a);
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: #667085;
}
.mode-switch {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
}
.mode-switch-item {
padding: 24rpx;
border-radius: 24rpx;
background: rgba(255, 255, 255, 0.8);
border: 2rpx solid rgba(255, 255, 255, 0.66);
box-shadow: 0 12rpx 28rpx rgba(15, 23, 42, 0.06);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
}
.mode-switch-item-active {
background: rgba(255, 255, 255, 0.92);
border-color: rgba(255, 255, 255, 0.78);
}
.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: rgba(255, 255, 255, 0.86);
display: flex;
align-items: center;
justify-content: center;
font-size: 48rpx;
color: #1aa37a;
border: 2rpx solid rgba(255, 255, 255, 0.72);
box-shadow: 0 10rpx 24rpx rgba(15, 23, 42, 0.06);
}
.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: rgba(255, 255, 255, 0.84);
border-radius: 16rpx;
font-size: 30rpx;
color: #111827;
border: 2rpx solid rgba(255, 255, 255, 0.72);
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.05);
}
.option-tag {
padding: 20rpx 28rpx;
border-radius: 32rpx;
}
.option-active {
background-color: rgba(255, 255, 255, 0.96);
border-color: rgba(255, 255, 255, 0.82);
color: #1a7f61;
}
.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: rgba(255, 255, 255, 0.86);
padding: 32rpx;
border-radius: 16rpx;
font-size: 40rpx;
color: #111827;
text-align: center;
border: 2rpx solid rgba(255, 255, 255, 0.72);
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.05);
}
.price-input {
display: flex;
align-items: center;
background-color: rgba(255, 255, 255, 0.86);
padding: 24rpx 32rpx;
border-radius: 16rpx;
gap: 8rpx;
border: 2rpx solid rgba(255, 255, 255, 0.72);
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.05);
}
.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: linear-gradient(180deg, #32c59d 0%, #1aa37a 100%);
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
color: #FFFFFF;
box-shadow: 0 12rpx 28rpx rgba(26, 163, 122, 0.22);
}
.btn-full { flex: 1; }
.btn-secondary {
height: 96rpx;
padding: 0 48rpx;
background-color: rgba(255, 255, 255, 0.86);
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #111827;
border: 2rpx solid rgba(15, 23, 42, 0.08);
}
</style>