mi-assessment/uniapp/pages/assessment/info/index.vue
2026-02-20 19:12:08 +08:00

851 lines
19 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.

<script setup>
/**
* 测评信息填写页面
*
* 功能:
* - 顶部测评介绍区域
* - 表单填写:姓名、手机号、性别、年龄、学业阶段、省市区
* - 支付测评和邀请码免费测评两个入口
* - 邀请码验证弹窗
*/
import { ref, computed, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useUserStore } from '@/store/user.js'
import { useAuth } from '@/composables/useAuth.js'
import { usePayment } from '@/composables/usePayment.js'
import { getIntro, verifyInviteCode } from '@/api/assessment.js'
import { isPhone } from '@/utils/validate.js'
import Navbar from '@/components/Navbar/index.vue'
const userStore = useUserStore()
const { checkLogin } = useAuth()
const { processPayment } = usePayment()
// 页面参数
const typeId = ref(0)
const typeName = ref('')
// 测评介绍数据
const introData = ref({
title: '',
description: '',
imageUrl: '',
price: 0
})
// 表单数据
const formData = ref({
name: '',
phone: '',
gender: '',
age: '',
educationStage: '',
province: '',
city: '',
district: ''
})
// 选择器数据
const genderOptions = ['男', '女']
const ageOptions = Array.from({ length: 41 }, (_, i) => `${i + 10}`)
const educationOptions = ['小学及以下', '初中', '高中', '大专', '本科', '研究生及以上']
// 选择器索引
const genderIndex = ref(-1)
const ageIndex = ref(-1)
const educationIndex = ref(-1)
// 省市区数据
const regionValue = ref([])
// 邀请码弹窗
const showInvitePopup = ref(false)
const inviteCode = ref('')
const inviteLoading = ref(false)
// 页面加载状态
const pageLoading = ref(true)
/**
* 表单是否填写完整
*/
const isFormComplete = computed(() => {
return (
formData.value.name.trim() !== '' &&
formData.value.phone.trim() !== '' &&
formData.value.gender !== '' &&
formData.value.age !== '' &&
formData.value.educationStage !== '' &&
formData.value.province !== '' &&
formData.value.city !== '' &&
formData.value.district !== ''
)
})
/**
* 加载测评介绍
*/
async function loadIntro() {
pageLoading.value = true
try {
const res = await getIntro(typeId.value)
if (res && res.code === 0 && res.data) {
introData.value = {
title: res.data.title || typeName.value || '多元智能测评',
description: res.data.description || '',
imageUrl: res.data.imageUrl || '',
price: res.data.price || 0
}
}
} catch (error) {
console.error('加载测评介绍失败:', error)
} finally {
pageLoading.value = false
}
}
/**
* 性别选择
*/
function onGenderChange(e) {
genderIndex.value = e.detail.value
formData.value.gender = genderOptions[e.detail.value]
}
/**
* 年龄选择
*/
function onAgeChange(e) {
ageIndex.value = e.detail.value
formData.value.age = ageOptions[e.detail.value]
}
/**
* 学业阶段选择
*/
function onEducationChange(e) {
educationIndex.value = e.detail.value
formData.value.educationStage = educationOptions[e.detail.value]
}
/**
* 省市区选择
*/
function onRegionChange(e) {
const value = e.detail.value
regionValue.value = value
formData.value.province = value[0] || ''
formData.value.city = value[1] || ''
formData.value.district = value[2] || ''
}
/**
* 验证表单
*/
function validateForm() {
// 验证手机号格式
if (!isPhone(formData.value.phone)) {
uni.showModal({
title: '提示',
content: '手机号格式有误',
showCancel: false
})
return false
}
// 验证区县是否选择
if (!formData.value.district) {
uni.showModal({
title: '提示',
content: '请选择所在城市区县',
showCancel: false
})
return false
}
return true
}
/**
* 支付测评
*/
async function handlePayAssessment() {
if (!isFormComplete.value) return
// 检查登录
if (!checkLogin()) return
// 验证表单
if (!validateForm()) return
try {
uni.showLoading({ title: '创建订单中...' })
// 发起支付
const result = await processPayment({
productType: 1, // 测评类型
productId: typeId.value,
userInfo: {
name: formData.value.name,
phone: formData.value.phone,
gender: formData.value.gender,
age: formData.value.age,
educationStage: formData.value.educationStage,
province: formData.value.province,
city: formData.value.city,
district: formData.value.district
}
})
uni.hideLoading()
if (result.success) {
// 支付成功,跳转到答题页
uni.redirectTo({
url: `/pages/assessment/questions/index?typeId=${typeId.value}&orderId=${result.orderId}`
})
} else if (result.error) {
uni.showToast({
title: result.error,
icon: 'none'
})
}
} catch (error) {
uni.hideLoading()
console.error('支付失败:', error)
uni.showToast({
title: '支付失败,请重试',
icon: 'none'
})
}
}
/**
* 打开邀请码弹窗
*/
function openInvitePopup() {
if (!isFormComplete.value) return
// 检查登录
if (!checkLogin()) return
// 验证表单
if (!validateForm()) return
inviteCode.value = ''
showInvitePopup.value = true
}
/**
* 关闭邀请码弹窗
*/
function closeInvitePopup() {
showInvitePopup.value = false
inviteCode.value = ''
}
/**
* 邀请码输入处理(自动转大写)
*/
function onInviteCodeInput(e) {
inviteCode.value = e.detail.value.toUpperCase()
}
/**
* 提交邀请码
*/
async function submitInviteCode() {
if (!inviteCode.value.trim()) {
uni.showToast({
title: '请输入邀请码',
icon: 'none'
})
return
}
inviteLoading.value = true
try {
const res = await verifyInviteCode(inviteCode.value.trim())
if (res && res.code === 0) {
// 验证成功,关闭弹窗并跳转到答题页
closeInvitePopup()
uni.redirectTo({
url: `/pages/assessment/questions/index?typeId=${typeId.value}&inviteCode=${inviteCode.value}`
})
} else {
// 验证失败
const errorMsg = res?.message || '邀请码有误,请重新输入'
uni.showToast({
title: errorMsg,
icon: 'none'
})
}
} catch (error) {
console.error('验证邀请码失败:', error)
uni.showToast({
title: '验证失败,请重试',
icon: 'none'
})
} finally {
inviteLoading.value = false
}
}
/**
* 页面加载
*/
onLoad((options) => {
typeId.value = Number(options.typeId) || 1
typeName.value = decodeURIComponent(options.typeName || '')
// 恢复用户登录状态
userStore.restoreFromStorage()
// 如果已登录,预填手机号
if (userStore.isLoggedIn && userStore.phone) {
formData.value.phone = userStore.phone
}
loadIntro()
})
</script>
<template>
<view class="assessment-info-page">
<!-- 导航栏 -->
<Navbar :title="introData.title || '多元智能测评'" :showBack="true" />
<!-- 页面内容 -->
<view class="page-content">
<!-- 测评介绍区域 -->
<view class="intro-section">
<image
v-if="introData.imageUrl"
:src="introData.imageUrl"
mode="widthFix"
class="intro-image"
/>
<view v-else class="intro-text">
<view class="intro-title">{{ introData.title }}</view>
<view class="intro-desc" v-if="introData.description">{{ introData.description }}</view>
</view>
</view>
<!-- 表单区域 -->
<view class="form-section">
<!-- 表单提示 -->
<view class="form-header">
<text class="form-header-text">正式测评前,请先填写您的基本信息</text>
</view>
<!-- 姓名 -->
<view class="form-item">
<view class="form-label">
<text class="required">*</text>
<text>姓名</text>
</view>
<view class="form-input-wrap">
<input
class="form-input"
type="text"
placeholder="请输入"
v-model="formData.name"
maxlength="20"
/>
</view>
</view>
<!-- 手机号 -->
<view class="form-item">
<view class="form-label">
<text class="required">*</text>
<text>手机号</text>
</view>
<view class="form-input-wrap">
<input
class="form-input"
type="number"
placeholder="请输入"
v-model="formData.phone"
maxlength="11"
/>
</view>
</view>
<!-- 性别 -->
<view class="form-item">
<view class="form-label">
<text class="required">*</text>
<text>性别</text>
</view>
<picker
mode="selector"
:range="genderOptions"
@change="onGenderChange"
>
<view class="form-picker">
<text :class="{ 'placeholder': !formData.gender }">
{{ formData.gender || '请选择' }}
</text>
<view class="picker-arrow"></view>
</view>
</picker>
</view>
<!-- 年龄 -->
<view class="form-item">
<view class="form-label">
<text class="required">*</text>
<text>年龄</text>
</view>
<picker
mode="selector"
:range="ageOptions"
@change="onAgeChange"
>
<view class="form-picker">
<text :class="{ 'placeholder': !formData.age }">
{{ formData.age || '请选择' }}
</text>
<view class="picker-arrow"></view>
</view>
</picker>
</view>
<!-- 学业阶段 -->
<view class="form-item">
<view class="form-label">
<text class="required">*</text>
<text>学业阶段</text>
</view>
<picker
mode="selector"
:range="educationOptions"
@change="onEducationChange"
>
<view class="form-picker">
<text :class="{ 'placeholder': !formData.educationStage }">
{{ formData.educationStage || '请选择' }}
</text>
<view class="picker-arrow"></view>
</view>
</picker>
</view>
<!-- 所在城市 -->
<view class="form-item form-item-last">
<view class="form-label">
<text class="required">*</text>
<text>所在城市</text>
</view>
<picker
mode="region"
@change="onRegionChange"
>
<view class="form-region-picker">
<view class="region-item">
<text :class="{ 'placeholder': !formData.province }">
{{ formData.province || '省' }}
</text>
<view class="picker-arrow"></view>
</view>
<view class="region-item">
<text :class="{ 'placeholder': !formData.city }">
{{ formData.city || '市' }}
</text>
<view class="picker-arrow"></view>
</view>
<view class="region-item">
<text :class="{ 'placeholder': !formData.district }">
{{ formData.district || '区/县' }}
</text>
<view class="picker-arrow"></view>
</view>
</view>
</picker>
</view>
</view>
<!-- 底部按钮区域 -->
<view class="bottom-section">
<view class="bottom-btn-group">
<!-- 邀请码测评按钮 -->
<view
class="btn-invite"
:class="{ 'btn-disabled': !isFormComplete }"
@click="openInvitePopup"
>
<text>邀请码免费测评</text>
</view>
<!-- 支付测评按钮 -->
<view
class="btn-pay"
:class="{ 'btn-disabled': !isFormComplete }"
@click="handlePayAssessment"
>
<text>支付¥{{ introData.price || 0 }}元开始测评</text>
</view>
</view>
</view>
</view>
<!-- 邀请码弹窗 -->
<view v-if="showInvitePopup" class="popup-mask" @click="closeInvitePopup">
<view class="popup-container" @click.stop>
<view class="popup-header">
<text class="popup-title">填写测评邀请码</text>
<view class="popup-close" @click="closeInvitePopup">
<text>×</text>
</view>
</view>
<view class="popup-body">
<input
class="invite-input"
type="text"
placeholder="请输入5位邀请码"
:value="inviteCode"
@input="onInviteCodeInput"
maxlength="5"
/>
</view>
<view class="popup-footer">
<view
class="popup-btn"
:class="{ 'btn-loading': inviteLoading }"
@click="submitInviteCode"
>
<text>{{ inviteLoading ? '验证中...' : '提交' }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
.assessment-info-page {
min-height: 100vh;
background-color: $bg-color;
}
.page-content {
padding-bottom: 200rpx;
}
// 测评介绍区域
.intro-section {
overflow: hidden;
margin-bottom: $spacing-lg;
.intro-image {
width: 100%;
display: block;
}
.intro-text {
padding: $spacing-lg;
background-color: $bg-white;
.intro-title {
font-size: $font-size-xl;
font-weight: $font-weight-bold;
color: $text-color;
margin-bottom: $spacing-sm;
}
.intro-desc {
font-size: $font-size-md;
color: $text-secondary;
line-height: 1.6;
}
}
}
// 表单区域
.form-section {
background-color: $bg-white;
border-radius: $border-radius-lg;
margin: 0 $spacing-lg;
padding: 0 $spacing-lg;
}
// 表单提示
.form-header {
padding: $spacing-xl 0 $spacing-md;
text-align: center;
.form-header-text {
font-size: $font-size-sm;
color: $text-secondary;
}
}
.form-item {
padding: $spacing-lg 0;
border-bottom: 1rpx solid $border-light;
&.form-item-last {
border-bottom: none;
}
}
.form-label {
font-size: $font-size-md;
color: $text-color;
font-weight: $font-weight-medium;
margin-bottom: $spacing-sm;
.required {
color: $error-color;
margin-right: 4rpx;
}
}
.form-input-wrap {
margin-top: $spacing-xs;
}
.form-input {
height: 56rpx;
font-size: $font-size-md;
color: $text-color;
&::placeholder {
color: $text-placeholder;
}
}
.form-picker {
display: flex;
align-items: center;
justify-content: space-between;
height: 56rpx;
margin-top: $spacing-xs;
font-size: $font-size-md;
color: $text-color;
.placeholder {
color: $text-placeholder;
}
.picker-arrow {
width: 16rpx;
height: 16rpx;
border-right: 3rpx solid $text-placeholder;
border-bottom: 3rpx solid $text-placeholder;
transform: rotate(-45deg);
margin-left: $spacing-sm;
}
}
// 省市区选择器
.form-region-picker {
display: flex;
align-items: center;
gap: $spacing-md;
margin-top: $spacing-xs;
.region-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 64rpx;
background-color: $bg-gray;
border-radius: $border-radius-sm;
font-size: $font-size-md;
color: $text-color;
.placeholder {
color: $text-placeholder;
}
.picker-arrow {
width: 12rpx;
height: 12rpx;
border-right: 3rpx solid $text-placeholder;
border-bottom: 3rpx solid $text-placeholder;
transform: rotate(45deg);
margin-left: $spacing-xs;
}
}
}
// 底部按钮区域
.bottom-section {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: $bg-white;
padding: $spacing-lg;
padding-bottom: calc(#{$spacing-lg} + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
}
.bottom-btn-group {
display: flex;
gap: $spacing-md;
}
.btn-invite {
flex: 1;
height: 88rpx;
background-color: $bg-white;
border: 2rpx solid #FF5252;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: $font-size-md;
font-weight: $font-weight-medium;
color: #FF5252;
}
&:active {
opacity: 0.8;
}
}
.btn-pay {
flex: 1;
height: 88rpx;
background: linear-gradient(135deg, #FF6B6B 0%, #FF5252 100%);
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: $font-size-md;
font-weight: $font-weight-medium;
color: $text-white;
}
&:active {
opacity: 0.8;
}
}
.btn-disabled {
background: #CCCCCC !important;
border-color: #CCCCCC !important;
pointer-events: none;
text {
color: $text-white !important;
}
}
// 邀请码弹窗
.popup-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: $bg-mask;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.popup-container {
width: 600rpx;
background-color: $bg-white;
border-radius: $border-radius-xl;
overflow: hidden;
}
.popup-header {
position: relative;
padding: $spacing-lg;
text-align: center;
border-bottom: 1rpx solid $border-light;
.popup-title {
font-size: $font-size-lg;
font-weight: $font-weight-medium;
color: $text-color;
}
.popup-close {
position: absolute;
top: $spacing-md;
right: $spacing-md;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: 48rpx;
color: $text-placeholder;
line-height: 1;
}
}
}
.popup-body {
padding: $spacing-xl $spacing-lg;
}
.invite-input {
width: 100%;
height: 88rpx;
background-color: $bg-gray;
border-radius: $border-radius-md;
padding: 0 $spacing-lg;
font-size: $font-size-lg;
color: $text-color;
text-align: center;
letter-spacing: 8rpx;
&::placeholder {
color: $text-placeholder;
letter-spacing: 0;
}
}
.popup-footer {
padding: 0 $spacing-lg $spacing-xl;
}
.popup-btn {
height: 88rpx;
background: linear-gradient(135deg, #FF6B6B 0%, #FF5252 100%);
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: $font-size-lg;
font-weight: $font-weight-medium;
color: $text-white;
}
&:active {
opacity: 0.8;
}
&.btn-loading {
opacity: 0.7;
pointer-events: none;
}
}
</style>