Files
smt/pages/onboarding/index.vue
T
2026-03-10 23:00:37 +08:00

461 lines
9.6 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 v-if="step === 1" class="step">
<text class="step-title">你每天抽多少支烟</text>
<text class="step-desc">这将帮助我们为你制定个性化的戒烟计划</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">你为什么想戒烟</text>
<text class="step-desc">选择对你最重要的原因可多选</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 ? '开始戒烟之旅 🚀' : '下一步' }}
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShareAppMessage } from '@dcloudio/uni-app'
import { useProfileStore } from '@/stores/profile'
import { useLogin } from '@/hooks/useLogin'
const profileStore = useProfileStore()
const { waitForLogin } = useLogin()
const navBarHeight = ref(0)
const step = ref(1)
const totalSteps = 5
const formData = ref({
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 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)
}
}
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()
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()
})
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;
}
.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>