hookehuyr

feat(结账): 重构结账页面,添加个人信息录入组件

将原有的表单输入替换为iframe嵌入的外部表单组件
添加个人信息录入状态管理,支持本地缓存已填写数据
清理页面加载时的历史个人信息录入标记
<template>
<van-popup
v-model:show="visible"
position="right"
:style="{ width: '100%', height: '100%' }"
:close-on-click-overlay="false"
:lock-scroll="true"
@close="handleClose"
>
<div class="info-entry-container">
<!-- 头部导航栏 -->
<!-- <div class="header">
<h2 class="title">个人信息录入</h2>
</div> -->
<!-- iframe容器 -->
<div class="iframe-container" ref="iframeContainer">
<iframe
ref="iframeRef"
:src="iframeSrc"
frameborder="0"
class="form-iframe"
@load="handleIframeLoad"
></iframe>
</div>
</div>
</van-popup>
</template>
<script setup>
import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
/**
* 组件属性定义
*/
const props = defineProps({
// 控制弹窗显示状态
show: {
type: Boolean,
default: false
},
// iframe的src地址
iframeSrc: {
type: String,
required: true
}
})
/**
* 组件事件定义
*/
const emit = defineEmits(['update:show', 'data-received', 'close'])
// 响应式数据
const visible = ref(false)
const iframeRef = ref(null)
const iframeContainer = ref(null)
/**
* 监听show属性变化,同步更新visible状态
*/
watch(() => props.show, (newVal) => {
visible.value = newVal
}, { immediate: true })
/**
* 监听visible变化,同步更新父组件的show状态
*/
watch(visible, (newVal) => {
emit('update:show', newVal)
})
/**
* 处理iframe加载完成事件
*/
const handleIframeLoad = () => {
nextTick(() => {
setupMessageListener()
})
}
/**
* 调整iframe高度以适应内容
*/
const adjustIframeHeight = () => {
// 移除高度限制,让iframe自然滚动
console.log('iframe高度调整已禁用,允许自然滚动')
}
/**
* 设置消息监听器,用于接收iframe内表单的数据
*/
const setupMessageListener = () => {
window.addEventListener('message', handleMessage)
}
/**
* 处理来自iframe的消息
* @param {MessageEvent} event - 消息事件
*/
const handleMessage = (event) => {
try {
// // 验证消息来源(可根据实际情况调整)
// const allowedOrigins = [
// 'https://oa-dev.onwall.cn',
// 'https://oa.onwall.cn',
// 'http://localhost',
// 'http://127.0.0.1'
// ]
// const isAllowedOrigin = allowedOrigins.some(origin =>
// event.origin.startsWith(origin)
// )
// if (!isAllowedOrigin) {
// console.warn('收到来自未授权源的消息:', event.origin)
// return
// }
// 解析消息数据
let messageData = event.data
if (typeof messageData === 'string') {
try {
messageData = JSON.parse(messageData)
} catch (e) {
// 如果不是JSON格式,直接使用原始数据
}
}
console.log('收到iframe消息:', messageData)
// 检查是否是表单提交的数据
if (messageData && (messageData.type === 'formSubmit')) {
// 发送数据给父组件
emit('data-received', messageData)
// 关闭弹窗
handleClose()
}
} catch (error) {
console.error('处理iframe消息时出错:', error)
}
}
/**
* 处理弹窗关闭
*/
const handleClose = () => {
visible.value = false
emit('close')
}
/**
* 组件挂载时的初始化
*/
onMounted(() => {
// 如果需要在挂载时就设置监听器
setupMessageListener()
})
/**
* 组件卸载时清理
*/
onUnmounted(() => {
window.removeEventListener('message', handleMessage)
})
/**
* 暴露给父组件的方法
*/
defineExpose({
close: handleClose,
adjustHeight: adjustIframeHeight
})
</script>
<style lang="less" scoped>
.info-entry-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.header {
display: flex;
align-items: center;
padding: 12px 16px;
background-color: #fff;
border-bottom: 1px solid #eee;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 10;
}
.close-btn {
background: none;
border: none;
padding: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
&:hover {
background-color: #f0f0f0;
}
&:active {
background-color: #e0e0e0;
}
}
.title {
flex: 1;
text-align: center;
font-size: 16px;
font-weight: 500;
color: #333;
margin: 0;
}
.iframe-container {
flex: 1;
overflow: auto;
background-color: #fff;
}
.form-iframe {
width: 100%;
height: 100%;
border: none;
display: block;
}
// 响应式设计
@media (max-width: 768px) {
.header {
padding: 10px 12px;
}
.title {
font-size: 14px;
}
.close-btn {
padding: 6px;
}
}
</style>
......@@ -80,61 +80,15 @@
<div class="px-4 pt-4">
<form @submit.prevent="handleSubmit">
<FrostedGlass class="rounded-xl p-4 mb-4">
<h3 class="font-medium mb-3">个人信息</h3>
<div class="space-y-3">
<div>
<label class="block text-sm text-gray-600 mb-1">姓名 <span class="text-red-500">*</span></label>
<input
v-model="formData.receive_name"
type="text"
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
placeholder="请输入您的姓名"
required
/>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">手机号码 <span class="text-red-500">*</span></label>
<input
v-model="formData.receive_phone"
type="tel"
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
placeholder="请输入您的手机号码"
required
/>
</div>
<!-- <div>
<label class="block text-sm text-gray-600 mb-1">电子邮箱</label>
<input
v-model="formData.receive_email"
type="email"
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
placeholder="请输入您的邮箱(选填)"
/>
</div> -->
<div>
<!-- <label class="block text-sm text-gray-600 mb-1">联系地址 <span class="text-red-500">*</span></label> -->
<label class="block text-sm text-gray-600 mb-1">联系地址</label>
<input
v-model="formData.receive_address"
type="text"
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
placeholder="请输入您的详细地址"
/>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">备注</label>
<textarea
v-model="formData.notes"
class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm resize-none h-20"
placeholder="有什么需要我们注意的事项?(选填)"
/>
</div>
</div>
<!-- 预览个人信息iframe -->
<iframe
v-if="!showInfoEntry"
:src="iframeInfoSrc"
class="w-full border-0"
ref="infoEntryIframe"
style="width: 100%; min-height: 600px; height: auto; border: none;"
frameborder="0"
></iframe>
</FrostedGlass>
<FrostedGlass class="rounded-xl p-4 mb-6">
......@@ -276,43 +230,89 @@
</WechatPayment>
</div>
</van-popup>
<!-- 个人信息录入弹窗 -->
<FormPage
v-model:show="showInfoEntry"
:iframe-src="iframeSrc"
@data-received="handleInfoEntryComplete"
@close="handleInfoEntryClose"
/>
</AppLayout>
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import AppLayout from '@/components/layout/AppLayout.vue'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
import WechatPayment from '@/components/payment/WechatPayment.vue'
import FormPage from '@/components/infoEntry/formPage.vue'
import { useCart } from '@/contexts/cart'
import { useTitle } from '@vueuse/core'
import { getUserInfoAPI } from "@/api/users";
const $route = useRoute()
const $router = useRouter()
useTitle($route.meta.title)
// useTitle($route.meta.title)
const router = useRouter()
const { items: cartItems, mode, getTotalPrice, handleCheckout, clearCart, removeFromCart } = useCart()
// Form state
const formData = ref({
receive_name: '',
receive_phone: '',
receive_email: '',
receive_address: '',
notes: '',
pay_type: 'WeChat'
})
// 存储当前课程的localStorage键名,用于页面离开时清理
let currentInfoEntryKey = null
onMounted(async () => {
const { code, data } = await getUserInfoAPI();
if (code) {
// 获取默认用户信息
formData.value.receive_name = data.user.name ? data.user.name : '';
formData.value.receive_phone = data.user.mobile ? data.user.mobile : '';
// 页面加载时立即检查cartItems中是否有form_url
if (cartItems.value && cartItems.value.length > 0) {
const itemWithForm = cartItems.value.find(item => item.form_url)
if (itemWithForm && itemWithForm.form_url) {
// 检查用户是否已经录入过个人信息
const infoEntryKey = `info_entry_completed_${itemWithForm.id}`
currentInfoEntryKey = infoEntryKey // 保存键名用于清理
const savedInfoData = localStorage.getItem(infoEntryKey)
console.log('检查个人信息录入状态:', {
courseId: itemWithForm.id,
hasSavedData: !!savedInfoData
})
if (!savedInfoData) {
// 只有未录入过信息时才弹出弹窗
showInfoEntry.value = true
iframeSrc.value = itemWithForm.form_url
console.log('首次录入,显示个人信息录入弹窗')
} else {
// 已录入过信息,从缓存中恢复数据并显示预览
try {
const infoData = JSON.parse(savedInfoData)
formData.value.customize_id = infoData.customize_id
iframeInfoSrc.value = infoData.form_url + '&page_type=info' + '&data_id=' + infoData.customize_id
console.log('从缓存恢复个人信息数据:', infoData)
} catch (error) {
console.error('解析缓存数据失败:', error)
// 如果解析失败,重新录入
showInfoEntry.value = true
iframeSrc.value = itemWithForm.form_url
}
}
}
}
})
// 页面离开时清理localStorage中的录入状态标记
onUnmounted(() => {
if (currentInfoEntryKey) {
localStorage.removeItem(currentInfoEntryKey)
console.log('页面离开,清理个人信息录入状态:', currentInfoEntryKey)
}
})
......@@ -323,6 +323,27 @@ const orderStatus = ref('')
const isProcessing = ref(false)
const orderComplete = ref(false)
// 个人信息录入相关状态
const showInfoEntry = ref(false)
// 个人信息录入弹窗的iframe地址
const iframeSrc = ref(null)
// 个人信息录入弹窗的iframe地址
const iframeInfoSrc = ref(null)
/**
* 监听个人信息录入弹窗状态,动态修改页面标题
*/
watch(showInfoEntry, (newVal) => {
if (newVal) {
// 弹窗打开时,修改标题为"个人信息录入"
// document.title = '个人信息录入'
useTitle('个人信息录入')
} else {
// 弹窗关闭时,修改标题为"结账"
useTitle($route.meta.title)
}
})
// 确认对话框状态
const showConfirmDialog = ref(false)
const itemToDelete = ref(null)
......@@ -342,12 +363,6 @@ const handleImageError = (e) => {
const handleSubmit = async (e) => {
try {
// 表单验证
if (!formData.value.receive_name?.trim()) {
throw new Error('请输入姓名')
}
if (!formData.value.receive_phone?.trim()) {
throw new Error('请输入手机号码')
}
if (!formData.value.pay_type) {
throw new Error('请选择支付方式')
}
......@@ -378,6 +393,53 @@ const handleSubmit = async (e) => {
}
}
// 处理个人信息录入完成
const handleInfoEntryComplete = async (data) => {
try {
console.log('收到个人信息录入数据:', data)
// cartItems是数组,需要找到包含form的项目
const itemWithForm = cartItems.value.find(item => item.form)
const formValue = itemWithForm ? itemWithForm.form : null
// 把个人信息录入数据赋值给formData
formData.value = { ...formData.value, form: formValue, customize_id: data.id }
// 标记该课程的个人信息已录入完成,并保存相关数据
if (itemWithForm && itemWithForm.id) {
const infoEntryKey = `info_entry_completed_${itemWithForm.id}`
const infoData = {
completed: true,
customize_id: data.id,
form_url: itemWithForm.form_url,
timestamp: Date.now()
}
localStorage.setItem(infoEntryKey, JSON.stringify(infoData))
console.log('个人信息录入完成,已保存状态和数据:', infoEntryKey, infoData)
}
isProcessing.value = true
} catch (error) {
} finally {
isProcessing.value = false
}
}
// 处理个人信息录入关闭
const handleInfoEntryClose = () => {
showInfoEntry.value = false
// cartItems是数组,需要找到包含form_url的项目
const itemWithForm = cartItems.value.find(item => item.form_url)
if (itemWithForm && itemWithForm.form_url) {
// 写入地址查询详情
iframeInfoSrc.value = itemWithForm.form_url + '&page_type=info' + '&data_id=' + formData.value.customize_id
console.log('个人信息录入弹窗关闭,iframe地址:', iframeInfoSrc.value)
} else {
console.log('未找到包含form_url的购物车项目')
}
}
// 处理支付成功
const handlePaymentSuccess = () => {
orderComplete.value = true
......
......@@ -449,13 +449,25 @@ const handlePurchase = () => {
}
if (course.value) {
addToCart({
// 调试日志:检查course.value.form_url的值
console.log('CourseDetailPage - course.value.form_url:', course.value.form_url)
console.log('CourseDetailPage - 完整course数据:', course.value)
const cartItem = {
id: course.value.id,
type: 'course',
title: course.value.title,
price: course.value.price,
imageUrl: course.value.imageUrl
})
imageUrl: course.value.imageUrl,
form: course.value.form, // 报名关联的表单
// 需要把当前页面域名拼写进去
form_url: window.location.origin + course.value.form_url, // 课程关联的表单的 URL
}
// 调试日志:检查传递给addToCart的数据
console.log('CourseDetailPage - 传递给addToCart的数据:', cartItem)
addToCart(cartItem)
proceedToCheckout()
}
}
......@@ -520,6 +532,20 @@ onMounted(async () => {
router.push('/courses')
}
}
// 进入页面时清理所有info_entry_completed_开头的localStorage标记
const keysToRemove = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key && key.startsWith('info_entry_completed_')) {
keysToRemove.push(key)
}
}
keysToRemove.forEach(key => {
localStorage.removeItem(key)
console.log('清理历史个人信息录入标记:', key)
})
})
......