b9473d5059
- 将所有 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>
659 lines
18 KiB
Vue
659 lines
18 KiB
Vue
<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>
|