mi-assessment/uniapp/pages/assessment/info/index.vue
zpc e33f4ed8f0 fix(assessment): 移除本地缓存,恢复进行中测评检测
- 移除 localStorage 表单缓存(STORAGE_KEY、saveFormToStorage、restoreFormFromStorage、clearFormStorage、watch、onUnload)
- 恢复 getPendingRecord API 调用和弹窗逻辑
- 恢复继续测评模式(表单自动填充、禁用编辑、按钮文案切换)
- 恢复进行中测评弹窗(重新开始/继续测评)
2026-02-23 01:21:19 +08:00

953 lines
23 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>
/**
* 测评信息填写页面
*
* 功能:
* - 顶部 banner 插画区域
* - 表单填写:姓名、手机号、性别、年龄、学业阶段、省市区
* - 支付测评和邀请码免费测评两个入口
* - 邀请码验证弹窗
* - 进行中测评记录恢复(断点续答)
*/
import { ref, computed } 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, getPendingRecord } from '@/api/assessment.js'
import { createOrder } from '@/api/order.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 = ['小学及以下', '初中', '高中', '大专', '本科', '研究生及以上']
/**
* 学业阶段文本转数值
*/
function getEducationStageValue(label) {
const map = {
'小学及以下': 1, '初中': 2, '高中': 3,
'大专': 4, '本科': 4, '研究生及以上': 4
}
return map[label] || 1
}
// 选择器索引
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 pendingRecord = ref(null)
const showPendingPopup = ref(false)
/**
* 是否为继续测评模式
*/
const isContinueMode = computed(() => !!pendingRecord.value)
/**
* 性别数值转文本
*/
function getGenderLabel(val) {
return val === 1 ? '男' : val === 2 ? '女' : ''
}
/**
* 学业阶段数值转文本
*/
function getEducationStageLabel(val) {
const map = { 1: '小学及以下', 2: '初中', 3: '高中', 4: '大专' }
return map[val] || ''
}
/**
* 检查是否有进行中的测评记录
*/
async function checkPendingRecord() {
try {
const res = await getPendingRecord(typeId.value)
if (res && res.code === 0 && res.data) {
pendingRecord.value = res.data
showPendingPopup.value = true
}
} catch (e) {
console.error('检查进行中测评失败:', e)
}
}
/**
* 继续进行中的测评
*/
function handleContinuePending() {
if (!pendingRecord.value) return
const r = pendingRecord.value
// 填充表单
formData.value.name = r.name || ''
formData.value.phone = r.phone || ''
formData.value.gender = getGenderLabel(r.gender)
formData.value.age = r.age ? `${r.age}` : ''
formData.value.educationStage = getEducationStageLabel(r.educationStage)
formData.value.province = r.province || ''
formData.value.city = r.city || ''
formData.value.district = r.district || ''
// 同步选择器索引
genderIndex.value = r.gender === 1 ? 0 : r.gender === 2 ? 1 : -1
ageIndex.value = r.age ? r.age - 10 : -1
educationIndex.value = educationOptions.indexOf(getEducationStageLabel(r.educationStage))
regionValue.value = [r.province || '', r.city || '', r.district || '']
showPendingPopup.value = false
}
/**
* 放弃进行中的测评,重新开始
*/
function handleDismissPending() {
pendingRecord.value = null
showPendingPopup.value = false
}
/**
* 表单是否填写完整
*/
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 !== ''
)
})
/**
* 底部按钮文案
*/
const payBtnText = computed(() => {
if (isContinueMode.value) return '继续测评'
return `支付¥${introData.value.price || 20}元开始测评`
})
/**
* 加载测评介绍
*/
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 (!formData.value.name.trim()) {
uni.showToast({ title: '请输入姓名', icon: 'none' })
return false
}
if (!isPhone(formData.value.phone)) {
uni.showToast({ title: '手机号格式有误', icon: 'none' })
return false
}
if (!formData.value.gender) {
uni.showToast({ title: '请选择性别', icon: 'none' })
return false
}
if (!formData.value.age) {
uni.showToast({ title: '请选择年龄', icon: 'none' })
return false
}
if (!formData.value.educationStage) {
uni.showToast({ title: '请选择学业阶段', icon: 'none' })
return false
}
if (!formData.value.district) {
uni.showToast({ title: '请选择所在城市', icon: 'none' })
return false
}
return true
}
/**
* 构建测评信息对象
*/
function buildAssessmentInfo() {
return {
name: formData.value.name,
phone: formData.value.phone,
gender: formData.value.gender === '男' ? 1 : 2,
age: parseInt(formData.value.age) || 0,
educationStage: getEducationStageValue(formData.value.educationStage),
province: formData.value.province,
city: formData.value.city,
district: formData.value.district
}
}
/** 支付测评 */
async function handlePayAssessment() {
if (!checkLogin()) return
// 继续测评模式:直接跳转到答题页
if (isContinueMode.value && pendingRecord.value) {
uni.redirectTo({
url: `/pages/assessment/questions/index?typeId=${typeId.value}&recordId=${pendingRecord.value.recordId}`
})
return
}
if (!validateForm()) return
try {
uni.showLoading({ title: '创建订单中...' })
const result = await processPayment({
orderType: 1,
productId: typeId.value,
assessmentInfo: buildAssessmentInfo()
})
uni.hideLoading()
if (result.success) {
// 从 createOrder 返回中获取 assessmentRecordId
const recordId = result.assessmentRecordId || ''
uni.redirectTo({
url: `/pages/assessment/questions/index?typeId=${typeId.value}&recordId=${recordId}`
})
} 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 (!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 {
// 1. 验证邀请码
const res = await verifyInviteCode(inviteCode.value.trim())
if (!res || res.code !== 0 || !res.data || !res.data.isValid) {
uni.showToast({
title: res?.data?.errorMessage || res?.message || '邀请码有误,请重新输入',
icon: 'none'
})
return
}
// 2. 邀请码有效,创建免费订单(带 inviteCodeId
const inviteCodeId = res.data.inviteCodeId
uni.showLoading({ title: '创建订单中...' })
const orderRes = await createOrder({
orderType: 1,
productId: typeId.value,
assessmentInfo: buildAssessmentInfo(),
inviteCodeId: inviteCodeId
})
uni.hideLoading()
if (orderRes && orderRes.code === 0 && orderRes.data) {
closeInvitePopup()
const recordId = orderRes.data.assessmentRecordId || ''
uni.redirectTo({
url: `/pages/assessment/questions/index?typeId=${typeId.value}&recordId=${recordId}`
})
} else {
uni.showToast({ title: orderRes?.message || '创建订单失败', icon: 'none' })
}
} catch (error) {
uni.hideLoading()
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) {
formData.value.phone = userStore.phone
}
loadIntro()
// 检查是否有进行中的测评记录
if (userStore.isLoggedIn) {
checkPendingRecord()
}
})
</script>
<template>
<view class="assessment-info-page">
<!-- 导航栏 -->
<Navbar title="多元智能测评" :showBack="true" />
<!-- 蓝色 banner 区域 -->
<view class="banner-section">
<image
v-if="introData.imageUrl"
:src="introData.imageUrl"
mode="widthFix"
class="banner-image"
/>
<view v-else class="banner-placeholder">
<image src="/static/logo.png" mode="aspectFit" class="banner-logo" />
</view>
</view>
<!-- 表单卡片 -->
<view class="form-card">
<view class="form-tip">
{{ isContinueMode ? '您有一份进行中的测评,信息已自动填充' : '正式测评前,请先填写您的基本信息' }}
</view>
<!-- 姓名 -->
<view class="form-item">
<view class="form-label"><text class="required">*</text>姓名</view>
<view class="form-input-box">
<input
type="text"
placeholder="请输入"
placeholder-class="input-placeholder"
v-model="formData.name"
maxlength="20"
:disabled="isContinueMode"
/>
</view>
</view>
<!-- 手机号 -->
<view class="form-item">
<view class="form-label"><text class="required">*</text>手机号</view>
<view class="form-input-box">
<input
type="number"
placeholder="请输入"
placeholder-class="input-placeholder"
v-model="formData.phone"
maxlength="11"
:disabled="isContinueMode"
/>
</view>
</view>
<!-- 性别 -->
<view class="form-item">
<view class="form-label"><text class="required">*</text>性别</view>
<picker mode="selector" :range="genderOptions" @change="onGenderChange" :disabled="isContinueMode">
<view class="form-select-box">
<text :class="formData.gender ? 'select-value' : 'select-placeholder'">
{{ formData.gender || '请选择' }}
</text>
<view class="arrow-down"></view>
</view>
</picker>
</view>
<!-- 年龄 -->
<view class="form-item">
<view class="form-label"><text class="required">*</text>年龄</view>
<picker mode="selector" :range="ageOptions" @change="onAgeChange" :disabled="isContinueMode">
<view class="form-select-box">
<text :class="formData.age ? 'select-value' : 'select-placeholder'">
{{ formData.age || '请选择' }}
</text>
<view class="arrow-down"></view>
</view>
</picker>
</view>
<!-- 学业阶段 -->
<view class="form-item">
<view class="form-label"><text class="required">*</text>学业阶段</view>
<picker mode="selector" :range="educationOptions" @change="onEducationChange" :disabled="isContinueMode">
<view class="form-select-box">
<text :class="formData.educationStage ? 'select-value' : 'select-placeholder'">
{{ formData.educationStage || '请选择' }}
</text>
<view class="arrow-down"></view>
</view>
</picker>
</view>
<!-- 所在城市 -->
<view class="form-item form-item-last">
<view class="form-label"><text class="required">*</text>所在城市</view>
<picker mode="region" @change="onRegionChange" :disabled="isContinueMode">
<view class="form-region-row">
<view class="region-box">
<text :class="formData.province ? 'select-value' : 'select-placeholder'">
{{ formData.province || '省' }}
</text>
<view class="arrow-down-sm"></view>
</view>
<view class="region-box">
<text :class="formData.city ? 'select-value' : 'select-placeholder'">
{{ formData.city || '市' }}
</text>
<view class="arrow-down-sm"></view>
</view>
<view class="region-box">
<text :class="formData.district ? 'select-value' : 'select-placeholder'">
{{ formData.district || '区/县' }}
</text>
<view class="arrow-down-sm"></view>
</view>
</view>
</picker>
</view>
</view>
<!-- 底部按钮 -->
<view class="btn-group">
<view v-if="!isContinueMode" class="btn-invite" @click="openInvitePopup">
<text>邀请码免费测评</text>
</view>
<view class="btn-pay" :class="{ 'btn-full': isContinueMode }" @click="handlePayAssessment">
<text>{{ payBtnText }}</text>
</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 v-if="showPendingPopup" class="popup-mask" @click="handleDismissPending">
<view class="popup-container pending-popup" @click.stop>
<view class="popup-header">
<text class="popup-title">发现进行中的测评</text>
</view>
<view class="popup-body">
<view class="pending-msg">
您有一份未完成的测评记录,是否继续?
</view>
<view class="pending-info">
<text>姓名:{{ pendingRecord?.name }}</text>
<text>手机号:{{ pendingRecord?.phone }}</text>
</view>
</view>
<view class="pending-footer">
<view class="popup-btn-outline" @click="handleDismissPending">
<text>重新开始</text>
</view>
<view class="popup-btn" @click="handleContinuePending">
<text>继续测评</text>
</view>
</view>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
@import '@/styles/variables.scss';
.assessment-info-page {
min-height: 100vh;
background-color: rgba(255, 234, 231, 1);
padding-bottom: calc(#{$spacing-xl} + env(safe-area-inset-bottom));
}
// ========== Banner 区域 ==========
.banner-section {
width: 100%;
height: 400rpx;
background: linear-gradient(180deg, #4A90E2 0%, #6BB5FF 100%);
overflow: hidden;
.banner-image {
width: 100%;
display: block;
}
.banner-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.banner-logo {
width: 400rpx;
height: 280rpx;
}
}
}
// ========== 表单卡片 ==========
.form-card {
background-color: $bg-white;
border-radius: $border-radius-xl;
margin: $spacing-lg $spacing-lg 0;
padding: $spacing-xl $spacing-lg $spacing-lg;
position: relative;
z-index: 1;
}
.form-tip {
text-align: center;
font-size: $font-size-md;
color: $text-secondary;
margin-bottom: $spacing-xl;
}
// ========== 表单项 ==========
.form-item {
margin-bottom: $spacing-lg;
}
.form-item-last {
margin-bottom: 0;
padding-bottom: $spacing-lg;
}
.form-label {
font-size: $font-size-lg;
color: $text-color;
font-weight: $font-weight-bold;
margin-bottom: $spacing-sm;
.required {
color: $error-color;
margin-right: 4rpx;
}
}
// 文本输入框 - 灰色背景圆角
.form-input-box {
background-color: $bg-gray;
border-radius: $border-radius-md;
padding: 0 $spacing-lg;
height: 80rpx;
display: flex;
align-items: center;
input {
width: 100%;
height: 100%;
font-size: $font-size-md;
color: $text-color;
}
}
.input-placeholder {
color: $text-placeholder;
font-size: $font-size-md;
}
// 下拉选择框 - 灰色背景圆角 + 下拉箭头
.form-select-box {
background-color: $bg-gray;
border-radius: $border-radius-md;
padding: 0 $spacing-lg;
height: 80rpx;
display: flex;
align-items: center;
justify-content: space-between;
.select-value {
font-size: $font-size-md;
color: $text-color;
}
.select-placeholder {
font-size: $font-size-md;
color: $text-placeholder;
}
}
// CSS 下拉箭头
.arrow-down {
width: 16rpx;
height: 16rpx;
border-right: 3rpx solid $text-placeholder;
border-bottom: 3rpx solid $text-placeholder;
transform: rotate(45deg);
flex-shrink: 0;
margin-left: $spacing-sm;
}
.arrow-down-sm {
width: 12rpx;
height: 12rpx;
border-right: 3rpx solid $text-placeholder;
border-bottom: 3rpx solid $text-placeholder;
transform: rotate(45deg);
flex-shrink: 0;
}
// 省市区三列选择
.form-region-row {
display: flex;
gap: $spacing-md;
}
.region-box {
flex: 1;
background-color: $bg-gray;
border-radius: $border-radius-md;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
gap: $spacing-xs;
.select-value {
font-size: $font-size-md;
color: $text-color;
}
.select-placeholder {
font-size: $font-size-md;
color: $text-placeholder;
}
}
// ========== 底部按钮 ==========
.btn-group {
display: flex;
gap: $spacing-md;
padding: $spacing-xl $spacing-lg 0;
}
.btn-invite {
flex: 0.8;
height: 88rpx;
background-color: $bg-white;
border: 2rpx solid #FF6B60;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: $font-size-md;
font-weight: $font-weight-medium;
color: #FF6B60;
}
&:active {
opacity: 0.8;
}
}
.btn-pay {
flex: 1;
height: 88rpx;
background-color: #FF6B60;
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-full {
flex: 1;
}
}
// ========== 弹窗通用 ==========
.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;
}
.popup-footer {
padding: 0 $spacing-lg $spacing-xl;
}
.popup-btn {
height: 88rpx;
background-color: #FF6B60;
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;
}
}
.popup-btn-outline {
height: 88rpx;
background-color: $bg-white;
border: 2rpx solid $border-color;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-size: $font-size-lg;
font-weight: $font-weight-medium;
color: $text-secondary;
}
&:active {
opacity: 0.8;
}
}
// ========== 进行中测评弹窗 ==========
.pending-popup {
.pending-msg {
font-size: $font-size-md;
color: $text-color;
text-align: center;
margin-bottom: $spacing-lg;
}
.pending-info {
display: flex;
flex-direction: column;
gap: $spacing-sm;
background-color: $bg-gray;
border-radius: $border-radius-md;
padding: $spacing-lg;
text {
font-size: $font-size-sm;
color: $text-secondary;
}
}
}
.pending-footer {
display: flex;
gap: $spacing-md;
padding: 0 $spacing-lg $spacing-xl;
.popup-btn-outline,
.popup-btn {
flex: 1;
}
}
// ========== 邀请码输入 ==========
.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;
}
</style>