Files
happy-life-star/mini-program/src/pages/onboarding/index.vue
T
peanut b9473d5059 fix: 替换已弃用的 getSystemInfoSync API 并优化移动端布局
- 将所有 uni.getSystemInfoSync() 替换为新的推荐 API
  - uni.getDeviceInfo() 获取设备信息
  - uni.getWindowInfo() 获取窗口信息
  - uni.getAppBaseInfo() 获取应用基础信息
- 优化页面布局适配移动端小程序
  - page 添加 height: 100% 和 overflow: hidden
  - 主页面使用 height: 100vh 替代 min-height
  - 移除滚动条显示(::-webkit-scrollbar)
  - 添加 flex-shrink: 0 防止卡片收缩
- 修复隐藏滚动条,提升移动端体验

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:27:04 +08:00

659 lines
18 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="onboarding-page">
<view class="bg-decoration">
<view class="aurora-top"></view>
<view class="aurora-bottom"></view>
</view>
<view class="content" :style="{ paddingTop: safeAreaTop + 20 + 'px', paddingBottom: safeAreaBottom + 20 + 'px' }">
<scroll-view class="step-content" scroll-y>
<view class="step-inner">
<view v-if="currentStep === 1" class="step animate-fade-in">
<view class="step-header">
<text class="step-title font-serif">系统初始化</text>
<text class="step-subtitle">定义你在人生OS中的唯一代码</text>
</view>
<view class="form-grid">
<view class="input-group">
<text class="label">你的昵称</text>
<input
class="glass-input"
placeholder="输入昵称"
v-model="formData.nickname"
/>
</view>
<view class="input-group">
<text class="label">性别</text>
<picker class="glass-picker" mode="selector" :range="genderOptions" :value="genderIndex" @change="onGenderChange">
<view class="picker-value">{{ formData.gender || '请选择' }}</view>
</picker>
</view>
<view class="input-group">
<text class="label">星座</text>
<picker class="glass-picker" mode="selector" :range="zodiacOptions" :value="zodiacIndex" @change="onZodiacChange">
<view class="picker-value">{{ formData.zodiac || '请选择' }}</view>
</picker>
</view>
<view class="input-group">
<text class="label">MBTI人格</text>
<picker class="glass-picker" mode="selector" :range="mbtiOptions" :value="mbtiIndex" @change="onMbtiChange">
<view class="picker-value">{{ formData.mbti || '请选择' }}</view>
</picker>
</view>
<view class="input-group full-width">
<text class="label">兴趣爱好</text>
<input
class="glass-input"
placeholder="用逗号分隔,如:阅读,旅行,摄影"
v-model="hobbiesText"
/>
</view>
</view>
</view>
<view v-if="currentStep === 2" class="step animate-fade-in">
<view class="step-header">
<text class="step-title font-serif">回溯起源</text>
<text class="step-subtitle">一段让你感到温暖的早期时光</text>
</view>
<view class="memory-form">
<view class="input-group">
<text class="label">日期</text>
<picker class="glass-picker" mode="date" :value="formData.childhood.date" @change="onChildhoodDateChange">
<view class="picker-value">{{ formData.childhood.date || '请选择日期' }}</view>
</picker>
</view>
<view class="input-group">
<text class="label">回忆一段具体且温暖的午后...</text>
<textarea
class="glass-textarea"
rows="4"
placeholder="描述那段时光..."
v-model="formData.childhood.text"
/>
</view>
<view class="hint-section">
<view class="hint-title">
<text class="icon"></text>
<text>灵感气泡</text>
</view>
<view class="hint-tags">
<text
v-for="(hint, index) in childhoodHints"
:key="index"
class="hint-tag"
@click="addChildhoodHint(hint)"
>
{{ hint }}
</text>
</view>
</view>
</view>
</view>
<view v-if="currentStep === 3" class="step animate-fade-in">
<view class="step-header">
<text class="step-title font-serif">悦然时刻</text>
<text class="step-subtitle">一段感受到生命跃动的经历</text>
</view>
<view class="memory-form">
<view class="input-group">
<text class="label">日期</text>
<picker class="glass-picker" mode="date" :value="formData.joy.date" @change="onJoyDateChange">
<view class="picker-value">{{ formData.joy.date || '请选择日期' }}</view>
</picker>
</view>
<view class="input-group">
<text class="label">想一个令你会心一笑的瞬间...</text>
<textarea
class="glass-textarea"
rows="4"
placeholder="描述那个瞬间..."
v-model="formData.joy.text"
/>
</view>
<view class="hint-section">
<view class="hint-title">
<text class="icon"></text>
<text>灵感气泡</text>
</view>
<view class="hint-tags">
<text
v-for="(hint, index) in joyHints"
:key="index"
class="hint-tag"
@click="addJoyHint(hint)"
>
{{ hint }}
</text>
</view>
</view>
</view>
</view>
<view v-if="currentStep === 4" class="step animate-fade-in">
<view class="step-header">
<text class="step-title font-serif">破茧成蝶</text>
<text class="step-subtitle">在宁静中默默积蓄力量的日子</text>
</view>
<view class="memory-form">
<view class="input-group">
<text class="label">日期</text>
<picker class="glass-picker" mode="date" :value="formData.low.date" @change="onLowDateChange">
<view class="picker-value">{{ formData.low.date || '请选择日期' }}</view>
</picker>
</view>
<view class="input-group">
<text class="label">描述那段有些艰难但让你成长的日子...</text>
<textarea
class="glass-textarea"
rows="4"
placeholder="描述那段经历..."
v-model="formData.low.text"
/>
</view>
<view class="hint-section">
<view class="hint-title">
<text class="icon"></text>
<text>灵感气泡</text>
</view>
<view class="hint-tags">
<text
v-for="(hint, index) in lowHints"
:key="index"
class="hint-tag"
@click="addLowHint(hint)"
>
{{ hint }}
</text>
</view>
</view>
</view>
</view>
<view v-if="currentStep === 5" class="step animate-fade-in">
<view class="step-header">
<text class="step-title font-serif">未来憧憬</text>
<text class="step-subtitle">对未来理想生活状态的预见</text>
</view>
<view class="memory-form">
<view class="input-group">
<text class="label">你想成为怎样的人</text>
<textarea
class="glass-textarea"
rows="3"
placeholder="描述你对未来的愿景..."
v-model="formData.future.vision"
/>
</view>
<view class="input-group">
<text class="label">你的理想生活状态</text>
<textarea
class="glass-textarea"
rows="3"
placeholder="描述理想的一天..."
v-model="formData.future.ideal"
/>
</view>
<view class="quote-box">
<text class="quote-icon"></text>
<text class="quote-text">"照见未来,便是创造的开始。"</text>
</view>
</view>
</view>
</view>
</scroll-view>
<view class="bottom-bar">
<view class="step-dots">
<view
v-for="step in 5"
:key="step"
class="step-dot"
:class="{ active: currentStep === step }"
/>
</view>
<view class="actions">
<button
v-if="currentStep > 1"
class="btn-secondary"
@click="prevStep"
>
返回
</button>
<button
class="btn-primary next-btn"
:loading="isSaving"
@click="nextStep"
>
<text v-if="currentStep === 5">{{ isSaving ? '保存中...' : '开启人生' }}</text>
<text v-else>继续</text>
</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, reactive, onMounted, watch } from 'vue'
import { useAppStore } from '../../stores/app.js'
import * as lifeEventService from '../../services/lifeEvent.js'
const store = useAppStore()
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
onMounted(async () => {
// 使用新的推荐 API 替代已弃用的 getSystemInfoSync
const windowInfo = uni.getWindowInfo()
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
await store.fetchUserProfile()
Object.assign(formData, store.registrationData)
currentStep.value = store.currentStep || 1
})
const currentStep = ref(store.currentStep || 1)
const isSaving = ref(false)
const formData = reactive({
nickname: '',
gender: '',
mbti: '',
zodiac: '',
profession: '',
hobbies: [],
childhood: { date: '', text: '' },
joy: { date: '', text: '' },
low: { date: '', text: '' },
future: { vision: '', ideal: '' }
})
const genderOptions = ['男', '女']
const zodiacOptions = ['白羊座', '金牛座', '双子座', '巨蟹座', '狮子座', '处女座', '天秤座', '天蝎座', '射手座', '摩羯座', '水瓶座', '双鱼座']
const mbtiOptions = ['INTJ', 'INTP', 'ENTJ', 'ENTP', 'INFJ', 'INFP', 'ENFJ', 'ENFP', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
const childhoodHints = ['秘密花园', '老旧弄堂', '秋千', '夏蝉', '被保护的', '好奇心']
const joyHints = ['突破', '共鸣', '清晨阳光', '认可', '旅行终点', '深呼吸']
const lowHints = ['迷茫', '无力感', '雨后街道', '重新出发', '裂痕中的光', '蜕变']
const hobbiesText = computed({
get: () => formData.hobbies.join(''),
set: (val) => {
formData.hobbies = val.split(/[,]/).map(s => s.trim()).filter(s => s)
}
})
const genderIndex = computed(() => genderOptions.indexOf(formData.gender))
const zodiacIndex = computed(() => zodiacOptions.indexOf(formData.zodiac))
const mbtiIndex = computed(() => mbtiOptions.indexOf(formData.mbti))
const onGenderChange = (e) => {
formData.gender = genderOptions[e.detail.value]
}
const onZodiacChange = (e) => {
formData.zodiac = zodiacOptions[e.detail.value]
}
const onMbtiChange = (e) => {
formData.mbti = mbtiOptions[e.detail.value]
}
const onChildhoodDateChange = (e) => {
formData.childhood.date = e.detail.value
}
const onJoyDateChange = (e) => {
formData.joy.date = e.detail.value
}
const onLowDateChange = (e) => {
formData.low.date = e.detail.value
}
const addChildhoodHint = (hint) => {
formData.childhood.text += hint + ' '
}
const addJoyHint = (hint) => {
formData.joy.text += hint + ' '
}
const addLowHint = (hint) => {
formData.low.text += hint + ' '
}
const prevStep = () => {
if (currentStep.value > 1) {
saveStepData()
currentStep.value--
}
}
const nextStep = async () => {
saveStepData()
if (currentStep.value < 5) {
currentStep.value++
} else {
isSaving.value = true
try {
const result = await store.saveUserProfile()
if (result.success) {
const eventsToSave = [
{ data: formData.childhood, type: 'childhood', title: '童年记忆' },
{ data: formData.joy, type: 'joy', title: '光芒闪耀的时刻' },
{ data: formData.low, type: 'low', title: '在暗夜中潜行' }
]
await Promise.all(
eventsToSave.map(({ data, type, title }) => {
if (!data?.date || !data?.text) return Promise.resolve()
return lifeEventService.createEvent({
title,
time: data.date,
content: data.text,
eventType: 'milestone',
tags: [type]
})
})
)
await store.fetchEvents()
uni.showToast({ title: '欢迎开启人生OS', icon: 'success' })
setTimeout(() => {
uni.redirectTo({ url: '/pages/main/index' })
}, 1500)
} else {
uni.showToast({ title: result.error || '保存失败', icon: 'none' })
}
} catch (error) {
uni.showToast({ title: '保存失败', icon: 'none' })
} finally {
isSaving.value = false
}
}
}
const saveStepData = () => {
store.updateRegistration({ ...formData })
}
watch(currentStep, (val) => {
store.setCurrentStep(val)
})
</script>
<style scoped>
.onboarding-page {
min-height: 100vh;
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
position: relative;
}
.bg-decoration {
position: absolute;
inset: 0;
pointer-events: none;
}
.aurora-top {
position: absolute;
top: -10%;
left: -10%;
width: 120%;
height: 60%;
background: rgba(168, 85, 247, 0.08);
filter: blur(120rpx);
border-radius: 50%;
}
.aurora-bottom {
position: absolute;
bottom: -10%;
right: -10%;
width: 100%;
height: 50%;
background: rgba(139, 92, 246, 0.05);
filter: blur(100rpx);
border-radius: 50%;
}
.content {
position: relative;
z-index: 1;
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 40rpx;
padding-top: calc(40rpx + constant(safe-area-inset-top));
padding-top: calc(40rpx + env(safe-area-inset-top));
}
.step-content {
flex: 1;
overflow-y: auto;
}
.step-inner {
padding-bottom: 40rpx;
}
.step {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20rpx); }
to { opacity: 1; transform: translateY(0); }
}
.step-header {
margin-bottom: 48rpx;
}
.step-title {
display: block;
font-size: 46rpx;
font-weight: 400;
color: rgba(255, 255, 255, 0.95);
margin-bottom: 12rpx;
letter-spacing: 4rpx;
}
.step-subtitle {
display: block;
font-size: 24rpx;
color: rgba(192, 132, 252, 0.6);
font-style: italic;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24rpx;
}
.full-width {
grid-column: 1 / -1;
}
.input-group {
margin-bottom: 8rpx;
}
.label {
display: block;
font-size: 18rpx;
color: rgba(255, 255, 255, 0.35);
margin-bottom: 12rpx;
letter-spacing: 4rpx;
text-transform: uppercase;
}
.glass-input, .glass-picker {
width: 100%;
height: 88rpx;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20rpx;
padding: 0 24rpx;
color: #F3E8FF;
font-size: 28rpx;
}
.glass-input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
.picker-value {
line-height: 88rpx;
color: rgba(255, 255, 255, 0.8);
}
.glass-textarea {
width: 100%;
height: 200rpx;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 20rpx;
padding: 24rpx;
color: #F3E8FF;
font-size: 28rpx;
}
.glass-textarea::placeholder {
color: rgba(255, 255, 255, 0.3);
}
.memory-form {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.hint-section {
background: rgba(168, 85, 247, 0.08);
border: 1px solid rgba(168, 85, 247, 0.15);
border-radius: 32rpx;
padding: 32rpx;
margin-top: 16rpx;
box-shadow: inset 0 0 30rpx rgba(168, 85, 247, 0.08);
}
.hint-title {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 18rpx;
color: rgba(192, 132, 252, 0.6);
text-transform: uppercase;
letter-spacing: 4rpx;
margin-bottom: 20rpx;
}
.icon {
font-size: 24rpx;
}
.hint-tags {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.hint-tag {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 32rpx;
padding: 10rpx 22rpx;
font-size: 22rpx;
color: rgba(243, 232, 255, 0.8);
}
.hint-tag:active {
background: rgba(168, 85, 247, 0.2);
border-color: rgba(168, 85, 247, 0.4);
}
.quote-box {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.15), rgba(232, 121, 249, 0.1));
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 20rpx;
padding: 32rpx;
display: flex;
align-items: center;
gap: 16rpx;
margin-top: 16rpx;
}
.quote-icon {
font-size: 32rpx;
}
.quote-text {
font-size: 26rpx;
color: rgba(243, 232, 255, 0.7);
font-style: italic;
}
.bottom-bar {
padding-top: 32rpx;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.step-dots {
display: flex;
gap: 8rpx;
margin-bottom: 32rpx;
}
.step-dot {
flex: 1;
height: 8rpx;
background: rgba(255, 255, 255, 0.1);
border-radius: 4rpx;
transition: all 0.4s ease;
}
.step-dot.active {
flex: 1;
height: 8rpx;
background: #A855F7;
box-shadow: 0 0 24rpx rgba(168, 85, 247, 0.6);
}
.actions {
display: flex;
justify-content: flex-end;
gap: 24rpx;
align-items: center;
}
.next-btn {
min-width: 200rpx;
}
</style>