小程序初始化

This commit is contained in:
2026-02-27 11:32:50 +08:00
parent 93574dbb45
commit 97e1ea2706
252 changed files with 32427 additions and 12363 deletions
+657
View File
@@ -0,0 +1,657 @@
<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 () => {
const systemInfo = uni.getSystemInfoSync()
safeAreaTop.value = systemInfo.safeAreaInsets?.top || systemInfo.statusBarHeight || 20
safeAreaBottom.value = systemInfo.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>