Files
happy-life-star/mini-program/src/pages/onboarding/index.vue
T

896 lines
22 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="profile-page">
<view class="space-bg"></view>
<view class="status-space" :style="{ height: capsuleTopReservePx + 'px' }"></view>
<view class="topbar" :style="topbarStyle">
<text class="back" @click="goBack"></text>
<text class="title">编辑资料 <text class="gold"></text></text>
<text class="save" @click="saveProfile">保存</text>
</view>
<scroll-view class="scroll" scroll-y :show-scrollbar="false">
<view class="hero-card glass-card">
<view class="avatar-wrap">
<image class="avatar-img" :src="avatarUrl" mode="aspectFill" />
<view class="avatar-edit" @click="chooseAvatar">
<view class="pen-icon"></view>
</view>
</view>
<view class="hero-info">
<view class="hero-name-row">
<text class="hero-name">{{ form.nickname || 'Zoey' }}</text>
<text class="gold hero-star"></text>
</view>
<text class="hero-sub">正在成为更清晰的自己</text>
<view class="hero-line"></view>
<text class="hero-quote">生活是自己的选择也是</text>
</view>
<view class="hero-planet">
<view class="planet-core"></view>
<view class="planet-ring"></view>
</view>
</view>
<view class="panel glass-card">
<view class="section-head">
<view class="section-icon basic-icon"></view>
<text class="section-title">基础信息</text>
</view>
<view class="profile-row">
<text class="row-label">昵称</text>
<input class="row-input" v-model="form.nickname" placeholder="输入昵称" placeholder-class="placeholder" />
<text class="chevron"></text>
</view>
<view class="profile-row">
<text class="row-label">性别</text>
<view class="gender-row">
<text v-for="item in genderOptions" :key="item" class="choice" :class="{ active: form.gender === item }" @click="form.gender = item">{{ item }}</text>
</view>
</view>
<view class="profile-row">
<text class="row-label">生日</text>
<picker class="row-picker" mode="date" :value="birthday" @change="onBirthday">
<view class="row-value">{{ birthdayDisplay }}</view>
</picker>
<text class="chevron"></text>
</view>
<view class="profile-row">
<text class="row-label">所在城市</text>
<input class="row-input" v-model="form.city" placeholder="填写城市" placeholder-class="placeholder" />
<text class="chevron"></text>
</view>
<view class="astro-panel">
<view class="astro-col">
<view class="astro-title-row">
<view class="zodiac-head-icon"></view>
<text class="astro-title">星座</text>
<text class="astro-current">{{ form.zodiac || '巨蟹座' }}</text>
<text class="chevron"></text>
</view>
<text class="select-title">选择星座</text>
<view class="zodiac-grid">
<view
v-for="item in zodiacOptions"
:key="item.name"
class="zodiac-item"
:class="{ active: form.zodiac === item.name }"
@click="form.zodiac = item.name"
>
<view class="zodiac-bubble">{{ item.symbol }}</view>
<text>{{ item.name }}</text>
</view>
</view>
</view>
<view class="astro-col mbti-col">
<view class="astro-title-row">
<view class="mbti-head-icon"></view>
<text class="astro-title">MBTI</text>
<text class="astro-current">{{ form.mbti || 'ENTJ' }}</text>
<text class="chevron"></text>
</view>
<text class="select-title">选择MBTI</text>
<view class="mbti-grid">
<text
v-for="item in mbtiOptions"
:key="item"
class="mbti-chip"
:class="{ active: form.mbti === item }"
@click="form.mbti = item"
>{{ item }}</text>
</view>
</view>
</view>
</view>
<view class="panel glass-card">
<view class="section-head">
<view class="section-icon job-title-icon"></view>
<text class="section-title">职业信息</text>
</view>
<view class="profile-row">
<text class="row-label">职业</text>
<input class="row-input" v-model="form.profession" placeholder="产品经理" placeholder-class="placeholder" />
<text class="chevron"></text>
</view>
<view class="profile-row">
<text class="row-label">行业</text>
<input class="row-input" v-model="form.industry" placeholder="互联网" placeholder-class="placeholder" />
<text class="chevron"></text>
</view>
<view class="profile-row">
<text class="row-label">公司可选</text>
<input class="row-input muted" v-model="form.company" placeholder="填写公司名称" placeholder-class="placeholder" />
<text class="chevron"></text>
</view>
</view>
<view class="panel glass-card">
<view class="section-head">
<view class="section-icon smile-title-icon"></view>
<text class="section-title">性格标签</text>
<text class="section-hint">最多选择5个</text>
</view>
<view class="tag-grid">
<text
v-for="tag in personalityTags"
:key="tag"
class="tag-choice"
:class="{ active: form.personalityTags.includes(tag) }"
@click="toggleList(form.personalityTags, tag, 5)"
>{{ tag }}</text>
<text class="tag-choice dashed"> 添加标签</text>
</view>
</view>
<view class="panel glass-card">
<view class="section-head with-action">
<view class="section-title-wrap">
<view class="section-icon heart-title-icon"></view>
<text class="section-title">兴趣爱好</text>
<text class="section-hint">最多选择5个</text>
</view>
<text class="custom" @click="addCustomHobby"> 自定义兴趣</text>
</view>
<view class="tag-grid">
<text
v-for="tag in hobbyOptions"
:key="tag"
class="tag-choice"
:class="{ active: form.hobbies.includes(tag) }"
@click="toggleList(form.hobbies, tag, 5)"
>{{ tag }}</text>
</view>
</view>
<view class="panel glass-card bio-panel">
<view class="section-head">
<view class="section-icon bio-title-icon"></view>
<text class="section-title">个人简介</text>
</view>
<textarea
class="bio"
v-model="form.future.ideal"
maxlength="200"
:placeholder="bioPlaceholder"
placeholder-class="placeholder"
/>
<text class="bio-count">{{ (form.future.ideal || '').length }}/200</text>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
const store = useAppStore()
const { capsuleTopReservePx, topbarStyle } = useMenuButtonSafeArea({ extraTopPx: 2 })
const isEdit = ref(false)
const saving = ref(false)
const birthday = ref('')
const avatarLocal = ref('')
const bioPlaceholder = '热爱阅读和旅行,喜欢用文字和镜头记录生活。\n相信真诚和努力能让世界变得更美好。'
const genderOptions = ['女', '男', '不透露']
const zodiacOptions = [
{ name: '白羊座', symbol: '♈' },
{ name: '金牛座', symbol: '♉' },
{ name: '双子座', symbol: '♊' },
{ name: '巨蟹座', symbol: '♋' },
{ name: '狮子座', symbol: '♌' },
{ name: '处女座', symbol: '♍' },
{ name: '天秤座', symbol: '♎' },
{ name: '天蝎座', symbol: '♏' },
{ name: '射手座', symbol: '♐' },
{ name: '摩羯座', symbol: '♑' },
{ name: '水瓶座', symbol: '♒' },
{ name: '双鱼座', symbol: '♓' }
]
const mbtiOptions = ['INTJ', 'INTP', 'ENTJ', 'ENTP', 'INFJ', 'INFP', 'ENFJ', 'ENFP', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
const personalityTags = ['理性', '感性', '乐观', '独立', '有创造力', '坚韧', '细腻', '好奇', '内敛', '冒险', '自由']
const hobbyOptions = ['阅读', '旅行', '音乐', '写作', '摄影', '电影', '运动', '绘画', '咖啡', '游戏']
const form = reactive({
nickname: '',
gender: '',
zodiac: '',
mbti: '',
profession: '',
city: '',
industry: '',
company: '',
personalityTags: [],
hobbies: [],
childhood: { date: '', text: '' },
joy: { date: '', text: '' },
low: { date: '', text: '' },
future: { vision: '', ideal: '' }
})
const avatarUrl = computed(() => {
if (avatarLocal.value) return avatarLocal.value
const nickname = form.nickname || 'User'
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff`
})
const birthdayDisplay = computed(() => {
if (!birthday.value) return '请选择生日'
const [year, month, day] = birthday.value.split('-')
return `${year}${month}${day}`
})
const syncFromStore = () => {
const source = store.userProfile || store.registrationData || {}
Object.assign(form, {
nickname: source.nickname || '',
gender: source.gender || '',
zodiac: source.zodiac || '',
mbti: source.mbti || '',
profession: source.profession || '',
city: source.city || '',
industry: source.industry || '',
company: source.company || '',
personalityTags: Array.isArray(source.personalityTags) ? [...source.personalityTags] : [],
hobbies: Array.isArray(source.hobbies) ? [...source.hobbies] : [],
childhood: { date: source.childhood?.date || '', text: source.childhood?.text || '' },
joy: { date: source.joy?.date || '', text: source.joy?.text || '' },
low: { date: source.low?.date || '', text: source.low?.text || '' },
future: { vision: source.future?.vision || '', ideal: source.future?.ideal || '' }
})
birthday.value = source.birthday || ''
}
const onBirthday = (event) => {
birthday.value = event.detail.value
}
const toggleList = (list, tag, max) => {
const index = list.indexOf(tag)
if (index >= 0) {
list.splice(index, 1)
return
}
if (list.length >= max) {
uni.showToast({ title: `最多选择${max}`, icon: 'none' })
return
}
list.push(tag)
}
const chooseAvatar = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
avatarLocal.value = res.tempFilePaths?.[0] || ''
uni.showToast({ title: '头像已更新到本地预览', icon: 'none' })
}
})
}
const addCustomHobby = () => {
uni.showModal({
title: '自定义兴趣',
editable: true,
placeholderText: '输入兴趣名称',
success: (res) => {
const value = String(res.content || '').trim()
if (!res.confirm || !value) return
if (!hobbyOptions.includes(value)) hobbyOptions.push(value)
toggleList(form.hobbies, value, 5)
}
})
}
const saveProfile = async () => {
if (!form.nickname.trim() || saving.value) return
saving.value = true
store.updateRegistration(JSON.parse(JSON.stringify({ ...form, birthday: birthday.value })))
const res = await store.saveUserProfile()
saving.value = false
if (!res.success) {
uni.showToast({ title: res.error || '保存失败', icon: 'none' })
return
}
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => {
if (isEdit.value) uni.navigateBack()
else uni.reLaunch({ url: '/pages/main/index?tab=script' })
}, 350)
}
const goBack = () => {
if (isEdit.value) uni.navigateBack()
else uni.reLaunch({ url: '/pages/main/index?tab=script' })
}
onMounted(() => {
const pages = getCurrentPages()
isEdit.value = pages[pages.length - 1]?.options?.edit === '1'
syncFromStore()
})
</script>
<style scoped>
.profile-page {
position: relative;
height: 100vh;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
color: #fff;
background: #050615;
}
.space-bg {
position: absolute;
inset: 0;
background:
radial-gradient(circle at 84% 3%, rgba(87, 122, 255, 0.12), transparent 28%),
radial-gradient(circle at 14% 26%, rgba(130, 71, 255, 0.16), transparent 30%),
linear-gradient(180deg, #05081b 0%, #07031a 52%, #03020d 100%);
}
.status-space,
.topbar,
.scroll {
position: relative;
z-index: 1;
}
.status-space,
.topbar {
flex-shrink: 0;
}
.topbar {
height: 72rpx;
display: grid;
grid-template-columns: 76rpx 1fr 76rpx;
align-items: center;
padding: 0 28rpx;
}
.back {
color: #fff;
font-size: 56rpx;
line-height: 1;
}
.title {
text-align: center;
color: #fff;
font-size: 32rpx;
font-weight: 900;
}
.gold {
color: #ffd58c;
text-shadow: 0 0 20rpx rgba(255, 202, 125, 0.45);
}
.save {
color: #b94cff;
font-size: 26rpx;
text-align: right;
}
.scroll {
flex: 1;
height: 0;
min-height: 0;
box-sizing: border-box;
padding: 0 24rpx 28rpx;
margin-top: -6rpx;
}
.glass-card {
position: relative;
overflow: hidden;
border: 1rpx solid rgba(155, 110, 255, 0.14);
background:
radial-gradient(circle at 92% 12%, rgba(104, 66, 255, 0.1), transparent 34%),
rgba(10, 13, 43, 0.54);
box-shadow: inset 0 0 30rpx rgba(123, 60, 255, 0.05), 0 10rpx 36rpx rgba(0, 0, 0, 0.16);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
}
.hero-card {
min-height: 156rpx;
border-radius: 22rpx;
margin-bottom: 18rpx;
padding: 18rpx 24rpx;
display: flex;
align-items: center;
gap: 22rpx;
}
.avatar-wrap {
position: relative;
width: 118rpx;
height: 118rpx;
flex-shrink: 0;
padding: 5rpx;
border-radius: 50%;
background: linear-gradient(135deg, #fff, #9b54ff 46%, #4a67ff);
box-shadow: 0 0 34rpx rgba(162, 91, 255, 0.52);
}
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
background: #27124a;
}
.avatar-edit {
position: absolute;
right: -4rpx;
bottom: -2rpx;
width: 38rpx;
height: 38rpx;
border-radius: 50%;
background: linear-gradient(135deg, #8f4dff, #582cff);
box-shadow: 0 0 18rpx rgba(158, 91, 255, 0.62);
}
.pen-icon {
width: 17rpx;
height: 6rpx;
margin: 16rpx auto;
border-radius: 999rpx;
background: #fff;
transform: rotate(-45deg);
}
.hero-info {
position: relative;
z-index: 2;
flex: 1;
min-width: 0;
}
.hero-name-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.hero-name {
color: #fff;
font-size: 34rpx;
font-weight: 900;
line-height: 1.1;
}
.hero-star {
font-size: 22rpx;
}
.hero-sub {
display: block;
margin-top: 8rpx;
color: rgba(239, 232, 255, 0.84);
font-size: 23rpx;
}
.hero-line {
width: 280rpx;
height: 1rpx;
margin-top: 14rpx;
background: rgba(180, 139, 255, 0.22);
}
.hero-quote {
display: block;
margin-top: 12rpx;
color: #b94cff;
font-size: 23rpx;
font-weight: 700;
}
.hero-planet {
position: absolute;
right: 34rpx;
top: 28rpx;
width: 210rpx;
height: 150rpx;
opacity: 0.42;
}
.planet-core {
position: absolute;
right: 30rpx;
top: 24rpx;
width: 86rpx;
height: 86rpx;
border-radius: 50%;
background: radial-gradient(circle at 34% 26%, #7d63ff, #4b1da8 62%, #19083b);
box-shadow: 0 0 48rpx rgba(141, 78, 255, 0.58);
}
.planet-ring {
position: absolute;
right: 4rpx;
top: 58rpx;
width: 158rpx;
height: 34rpx;
border: 5rpx solid rgba(161, 92, 255, 0.55);
border-top-color: transparent;
border-radius: 50%;
transform: rotate(-18deg);
}
.panel {
border-radius: 22rpx;
margin-bottom: 18rpx;
padding: 22rpx 24rpx;
}
.section-head,
.section-title-wrap {
display: flex;
align-items: center;
}
.section-head {
min-height: 44rpx;
gap: 10rpx;
}
.section-head.with-action {
justify-content: space-between;
}
.section-title {
color: rgba(239, 232, 255, 0.9);
font-size: 24rpx;
font-weight: 800;
}
.section-hint {
color: rgba(222, 211, 240, 0.54);
font-size: 22rpx;
}
.section-icon {
position: relative;
width: 28rpx;
height: 28rpx;
color: #a855ff;
flex-shrink: 0;
}
.basic-icon::before {
content: '♜';
color: currentColor;
font-size: 28rpx;
line-height: 1;
}
.job-title-icon {
border: 4rpx solid currentColor;
border-radius: 6rpx;
box-sizing: border-box;
}
.job-title-icon::before {
content: '';
position: absolute;
left: 7rpx;
top: -8rpx;
width: 10rpx;
height: 7rpx;
border: 3rpx solid currentColor;
border-bottom: 0;
border-radius: 6rpx 6rpx 0 0;
}
.smile-title-icon {
border-radius: 50%;
border: 3rpx solid currentColor;
}
.smile-title-icon::before,
.smile-title-icon::after {
content: '';
position: absolute;
top: 8rpx;
width: 4rpx;
height: 4rpx;
border-radius: 50%;
background: currentColor;
}
.smile-title-icon::before { left: 7rpx; }
.smile-title-icon::after { right: 7rpx; }
.heart-title-icon::before {
content: '♡';
font-size: 30rpx;
line-height: 1;
}
.bio-title-icon {
border: 3rpx solid currentColor;
border-radius: 5rpx;
box-sizing: border-box;
}
.bio-title-icon::before,
.bio-title-icon::after {
content: '';
position: absolute;
left: 6rpx;
right: 6rpx;
height: 3rpx;
background: currentColor;
}
.bio-title-icon::before { top: 8rpx; }
.bio-title-icon::after { top: 15rpx; }
.profile-row {
min-height: 60rpx;
display: grid;
grid-template-columns: 142rpx 1fr 24rpx;
align-items: center;
border-top: 1rpx solid rgba(180, 139, 255, 0.16);
}
.section-head + .profile-row {
margin-top: 8rpx;
}
.row-label {
color: rgba(205, 191, 238, 0.82);
font-size: 23rpx;
}
.row-input,
.row-value {
min-width: 0;
color: rgba(255, 255, 255, 0.9);
font-size: 23rpx;
text-align: left;
}
.row-picker {
min-width: 0;
}
.muted {
color: rgba(217, 205, 238, 0.42);
}
.chevron {
color: rgba(218, 204, 243, 0.7);
font-size: 38rpx;
line-height: 1;
text-align: right;
}
.gender-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
}
.choice,
.mbti-chip,
.tag-choice {
height: 40rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(224, 214, 243, 0.72);
border: 1rpx solid rgba(151, 111, 255, 0.42);
background: rgba(255, 255, 255, 0.02);
font-size: 22rpx;
box-sizing: border-box;
}
.choice.active,
.mbti-chip.active,
.tag-choice.active {
color: #fff;
border-color: rgba(206, 82, 255, 0.95);
background: linear-gradient(180deg, rgba(169, 61, 255, 0.62), rgba(107, 41, 206, 0.5));
box-shadow: 0 0 18rpx rgba(168, 67, 255, 0.52), inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
}
.astro-panel {
display: flex;
flex-direction: column;
gap: 22rpx;
margin-top: 10rpx;
padding-top: 16rpx;
border-top: 1rpx solid rgba(180, 139, 255, 0.16);
}
.astro-col {
min-width: 0;
padding: 0;
}
.mbti-col {
border-left: 0;
padding-top: 18rpx;
border-top: 1rpx solid rgba(180, 139, 255, 0.12);
}
.astro-title-row {
display: grid;
grid-template-columns: 34rpx 76rpx 1fr 18rpx;
align-items: center;
gap: 8rpx;
}
.zodiac-head-icon,
.mbti-head-icon {
width: 28rpx;
height: 28rpx;
color: #a855ff;
}
.zodiac-head-icon {
font-size: 28rpx;
line-height: 1;
}
.mbti-head-icon {
border: 4rpx solid currentColor;
border-radius: 4rpx;
box-sizing: border-box;
}
.astro-title {
color: rgba(222, 211, 240, 0.76);
font-size: 23rpx;
}
.astro-current {
color: rgba(255, 255, 255, 0.9);
font-size: 22rpx;
text-align: right;
}
.select-title {
display: block;
margin-top: 16rpx;
color: rgba(222, 211, 240, 0.72);
font-size: 21rpx;
}
.zodiac-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14rpx 10rpx;
margin-top: 14rpx;
}
.zodiac-item {
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 7rpx;
color: rgba(226, 217, 246, 0.84);
font-size: 20rpx;
}
.zodiac-bubble {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #a855ff;
font-size: 26rpx;
background: rgba(124, 58, 237, 0.28);
border: 1rpx solid rgba(173, 84, 255, 0.36);
}
.zodiac-item.active .zodiac-bubble {
color: #fff;
border-color: rgba(215, 128, 255, 0.95);
background: linear-gradient(135deg, rgba(168, 85, 247, 0.8), rgba(98, 47, 190, 0.66));
box-shadow: 0 0 22rpx rgba(190, 92, 255, 0.72);
}
.mbti-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12rpx 14rpx;
margin-top: 14rpx;
}
.mbti-chip {
height: 40rpx;
font-size: 20rpx;
}
.tag-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 14rpx 24rpx;
margin-top: 20rpx;
}
.tag-choice {
height: 40rpx;
font-size: 21rpx;
}
.tag-choice.dashed {
border-style: dashed;
color: #c06dff;
}
.custom {
color: #c06dff;
font-size: 23rpx;
}
.bio-panel {
margin-bottom: 0;
}
.bio {
width: 100%;
height: 116rpx;
box-sizing: border-box;
margin-top: 16rpx;
padding: 18rpx 20rpx;
border-radius: 18rpx;
border: 1rpx solid rgba(151, 111, 255, 0.22);
background: rgba(10, 12, 40, 0.66);
color: #fff;
font-size: 23rpx;
line-height: 1.5;
}
.bio-count {
display: block;
margin-top: 8rpx;
text-align: right;
color: rgba(224, 214, 243, 0.56);
font-size: 20rpx;
}
.placeholder {
color: rgba(214, 204, 235, 0.42);
}
</style>