feat: 商品详情支付交互调试

This commit is contained in:
duanshuwen
2025-08-06 21:43:08 +08:00
parent 5292ede3c5
commit e482ea5393
10 changed files with 673 additions and 137 deletions

View File

@@ -1,83 +1,89 @@
<template>
<view class="more-tips">
<view class="more-tips-scroll">
<view class="more-tips-item" v-for="(item, index) in itemList" :key="index">
<text class="more-tips-item-title" @click="sendReply(item)">{{ item }}</text>
</view>
</view>
</view>
<view class="more-tips">
<view class="more-tips-scroll">
<view
class="more-tips-item"
v-for="(item, index) in itemList"
:key="index"
>
<text class="more-tips-item-title" @click="sendReply(item)">{{
item
}}</text>
</view>
</view>
</view>
</template>
<script setup>
import { defineProps } from "vue";
const emits = defineEmits(['replySent']);
defineProps({
itemList: {
type: Array,
default: [
'定温泉票',
'定酒店',
'优惠套餐',
'亲子玩法',
'了解交通',
'看看酒店',
'看看美食'
]
}
})
import { defineProps } from "vue";
const emits = defineEmits(["replySent"]);
const sendReply = (text) => {
emits('replySent', text); // 向父组件传递数据
}
defineProps({
itemList: {
type: Array,
default: [
"定温泉票",
"定酒店",
"优惠套餐",
"亲子玩法",
"了解交通",
"看看酒店",
"看看美食",
],
},
});
const sendReply = (text) => {
emits("replySent", text); // 向父组件传递数据
};
</script>
<style lang="scss" scoped>
.more-tips {
width: 100%;
margin-bottom: 12px;
&-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
/* 隐藏滚动条 */
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.more-tips-item {
border-radius: 8px;
margin: 4px;
box-shadow: 0 2px 5px 0px rgba(0,0,0,0.1);
background-color: #FFFFFF;
padding: 2px 12px;
display: flex;
flex-direction: column;
min-width: 46px;
// &:first-child {
// margin-left: 12px;
// }
// &:last-child {
// margin-right: 12px;
// }
.more-tips-item-title {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 12px;
color: #00A6FF;
line-height: 24px;
text-align: center;
}
}
}
.more-tips {
width: 100%;
&-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
padding-bottom: 12px;
box-sizing: border-box;
/* 隐藏滚动条 */
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.more-tips-item {
border-radius: 8px;
margin: 4px;
box-shadow: 0 2px 5px 0px rgba(0, 0, 0, 0.1);
background-color: #ffffff;
padding: 2px 12px;
display: flex;
flex-direction: column;
min-width: 46px;
// &:first-child {
// margin-left: 12px;
// }
// &:last-child {
// margin-right: 12px;
// }
.more-tips-item-title {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 12px;
color: #00a6ff;
line-height: 24px;
text-align: center;
}
}
}
</style>

View File

@@ -66,11 +66,7 @@
<!-- 数量选择区域 -->
<view class="quantity-section">
<ModuleTitle
:title="
goodsData.commodityTypeCode === '0' ? '订房信息' : '游客信息'
"
/>
<ModuleTitle :title="sectionTitle" />
<Stepper v-model="quantity" />
</view>
@@ -83,11 +79,7 @@
>
<FormCard
v-for="(item, index) in userFormList"
:title="
goodsData.commodityTypeCode === '0'
? `房间${index + 1}`
: `游客${index + 1}`
"
:title="userCardTitle(index)"
:form="item"
:showDeleteIcon="userFormList.length > 1"
:key="index"
@@ -119,59 +111,119 @@
</template>
<script setup>
import { ref, computed, watch, defineProps, defineEmits } from "vue";
import { ref, computed, watch, onMounted, nextTick } from "vue";
import ImageSwiper from "@/components/ImageSwiper/index.vue";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
import Stepper from "@/components/Stepper/index.vue";
import FormCard from "@/components/FormCard/index.vue";
import SumCard from "@/components/SumCard/index.vue";
// 工具函数
const showToast = (title, icon = "none", duration = 2000) => {
uni.showToast({ title, icon, duration });
};
const isValidUserForm = (user) => {
return (
user &&
typeof user.name === "string" &&
user.name.trim() !== "" &&
typeof user.phone === "string" &&
user.phone.trim() !== ""
);
};
// 常量定义
const COMMODITY_TYPES = {
HOTEL: "0",
TICKET: "1",
DINING: "2",
};
const DEFAULT_PRICE = 399;
const MIN_USER_COUNT = 1;
// Props定义
const props = defineProps({
goodsData: {
type: Object,
default: () => ({
commodityTypeCode: "0", // 商品类型 0-酒店 1-门票 2-餐饮
commodityTypeCode: "0",
specificationPrice: "",
commodityName: "",
commodityPhotoList: [],
commodityServiceList: [],
}),
validator: (value) => {
return value && typeof value === "object";
},
},
});
// Emits定义
const emits = defineEmits(["confirm", "close"]);
// 工具函数
const createEmptyUserForm = () => {
return { name: "", phone: "" };
};
// 响应式数据
const popup = ref(null);
const quantity = ref(1);
const userFormList = ref([{ name: "", phone: "" }]); // 初始化一个表单项
const quantity = ref(MIN_USER_COUNT);
const userFormList = ref([createEmptyUserForm()]); // 初始化一个表单项
const isDeleting = ref(false); // 标志位防止删除时watch冲突
// 计算属性
const totalPrice = computed(() => {
const price = props.goodsData.specificationPrice || 399;
const price = props.goodsData?.specificationPrice || DEFAULT_PRICE;
return (price * quantity.value).toFixed(0);
});
const isHotelType = computed(() => {
return props.goodsData?.commodityTypeCode === COMMODITY_TYPES.HOTEL;
});
const sectionTitle = computed(() => {
return isHotelType.value ? "订房信息" : "游客信息";
});
const userCardTitle = computed(() => {
return (index) =>
isHotelType.value ? `房间${index + 1}` : `游客${index + 1}`;
});
// 监听 quantity 变化,动态调整 userFormList
watch(
quantity,
(newQuantity, oldQuantity) => {
async (newQuantity) => {
// 如果正在执行删除操作跳过watch逻辑
if (isDeleting.value) {
isDeleting.value = false;
return;
}
// 确保数量不小于最小值
if (newQuantity < MIN_USER_COUNT) {
quantity.value = MIN_USER_COUNT;
return;
}
const currentLength = userFormList.value.length;
if (newQuantity > currentLength) {
// 数量增加,添加新的表单项
for (let i = currentLength; i < newQuantity; i++) {
userFormList.value.push({ name: "", phone: "" });
}
const newForms = Array.from({ length: newQuantity - currentLength }, () =>
createEmptyUserForm()
);
userFormList.value.push(...newForms);
} else if (newQuantity < currentLength) {
// 数量减少,删除多余的表单项
userFormList.value.splice(newQuantity);
}
// 等待DOM更新完成
await nextTick();
},
{ immediate: false }
);
@@ -187,19 +239,33 @@ const closePopup = () => {
};
const updateUserForm = (index, field, value) => {
if (userFormList.value[index]) {
userFormList.value[index][field] = value;
if (!userFormList.value[index]) {
return;
}
if (!["name", "phone"].includes(field)) {
return;
}
userFormList.value[index][field] = value?.toString().trim() || "";
};
const deleteUserForm = (index) => {
// 参数验证
if (
typeof index !== "number" ||
index < 0 ||
index >= userFormList.value.length
) {
return;
}
// 确保至少保留一个表单项
if (userFormList.value.length <= 1) {
uni.showToast({
title: "至少需要一位游客信息",
icon: "none",
duration: 2000,
});
if (userFormList.value.length <= MIN_USER_COUNT) {
const message = isHotelType.value
? "至少需要一个房间信息"
: "至少需要一位游客信息";
showToast(message);
return;
}
@@ -213,21 +279,71 @@ const deleteUserForm = (index) => {
quantity.value = userFormList.value.length;
};
const confirmOrder = () => {
const orderData = {
goodsData: props.goodsData,
quantity: quantity.value,
totalPrice: totalPrice.value,
userFormList: userFormList.value,
};
emits("confirm", orderData);
closePopup();
const validateUserForms = () => {
const invalidUsers = userFormList.value.filter((user, index) => {
return !isValidUserForm(user);
});
if (invalidUsers.length > 0) {
const message = isHotelType.value
? "请填写完整的房客信息"
: "请填写完整的游客信息";
showToast(message);
return false;
}
return true;
};
// 暴露方法给父组件
const confirmOrder = () => {
try {
// 校验用户信息是否填写完整
if (!validateUserForms()) {
return;
}
// 构建订单数据
const orderData = {
goodsData: props.goodsData,
quantity: quantity.value,
totalPrice: parseFloat(totalPrice.value),
userFormList: userFormList.value.map((user) => ({
name: user.name.trim(),
phone: user.phone.trim(),
})),
commodityType: props.goodsData?.commodityTypeCode,
timestamp: Date.now(),
};
// 触发确认事件
emits("confirm", orderData);
// 关闭弹窗
closePopup();
} catch (error) {
showToast("订单处理失败,请重试");
}
};
// 生命周期钩子
onMounted(() => {
// 初始化用户表单列表
if (userFormList.value.length === 0) {
userFormList.value.push(createEmptyUserForm());
}
});
// 暴露给父组件的方法
defineExpose({
showPopup,
closePopup,
resetForm: () => {
userFormList.value = [createEmptyUserForm()];
quantity.value = MIN_USER_COUNT;
},
validateForms: validateUserForms,
getUserFormList: () => userFormList.value,
getTotalPrice: () => totalPrice.value,
});
</script>

View File

@@ -66,7 +66,7 @@ import Calender from "@/components/Calender/index.vue";
const calendarVisible = ref(false);
const goodsData = ref({});
const goodConfirmRef = ref(null);
const selectedDate = ref("");
const selectedDate = ref();
const priceData = ref([]);
// 获取商品详情数据
@@ -117,25 +117,40 @@ const showConfirmPopup = () => {
// 处理确认订单
const handleConfirmOrder = async (orderData) => {
console.log("确认订单:", orderData);
// const commodityId = orderData.commodityId;
// const purchaseAmount = orderData.purchaseAmount;
// const checkInData = orderData.checkInData;
// const checkOutData = orderData.checkOutData;
// const consumerInfoEntityList = orderData.consumerInfoEntityList;
// const payWay = "0";
// const paySource = "1";
console.log("确认订单---1:", orderData);
const { goodsData } = orderData;
// 购买的商品id
const commodityId = goodsData.commodityId;
// 消费者信息
const consumerInfoEntityList = orderData.userFormList;
// 购买数量
const purchaseAmount = orderData.userFormList.length;
// 支付方式 0-微信 1-支付宝 2-云闪付
const payWay = "0";
// 支付渠道 0-app 1-小程序 2-h5
const paySource = "1";
// const params = {
// commodityId,
// purchaseAmount,
// payWay,
// paySource,
// consumerInfoEntityList,
// checkInData,
// checkOutData,
// };
// const res = await orderPay(params);
const params = {
commodityId,
purchaseAmount,
payWay,
paySource,
consumerInfoEntityList,
};
//酒店类型添加入住时间、离店时间
if (goodsData.commodityTypeCode === "0" && selectedDate.value) {
const { startDate, endDate } = selectedDate.value;
// 入住时间
params.checkInData = startDate;
// 离店时间
params.checkOutData = endDate;
}
// 购买数量
const res = await orderPay(params);
console.log("确认订单---2:", res);
// 仅作为示例,非真实参数信息。
// uni.requestPayment({

View File

@@ -0,0 +1,71 @@
# AgreePopup 用户协议同意弹窗组件
## 组件概述
AgreePopup 是一个用于登录流程中的用户协议同意弹窗组件,用于向用户展示隐私政策和用户协议,并获取用户的同意确认。
## 功能需求
### 界面设计
- **弹窗标题**:显示"温馨提示"标题,居中显示
- **关闭按钮**:右上角显示"×"关闭按钮,点击可关闭弹窗
- **内容区域**
- 主要说明文字:"您在使用朵花温泉服务前,请仔细阅读用户隐私条款及用户注册须知,当您点击同意,即表示您已经理解并同意该条款,该条款将构成对您具有法律约束力的文件。"
- 注意事项:"请您注意:如果您不同意上述用户注册须知、隐私政策或其中任何约定,请您停止注册。如您阅读并点击同意即表示您已充分阅读理解并接受其全部内容,并表明您也同意朵花温泉可以依据以上隐私政策来处理您的个人信息。"
### 交互功能
- **复选框**
- 显示蓝色勾选框
- 文字说明:"本人已仔细阅读《用户协议》和《隐私协议》,知悉并诺遵守该内容。"
- 支持点击切换选中/未选中状态
- **确认按钮**
- 显示"我知道了"按钮
- 蓝色背景,白色文字
- 圆角设计
- 点击后触发同意事件并关闭弹窗
### 技术要求
- 使用 Vue 3 Composition API
- 支持弹窗显示/隐藏控制
- 提供事件回调:同意、关闭
- 响应式设计,适配移动端
- 使用 uni-app 框架
### 样式规范
- 弹窗背景:白色
- 圆角设计
- 文字颜色:深灰色
- 按钮:蓝色主题色
- 复选框:蓝色选中状态
- 适当的内边距和间距
### 使用场景
- 用户首次登录时显示
- 隐私政策更新后重新确认
- 注册流程中的协议确认
## 组件接口
### Props
- `visible`: Boolean - 控制弹窗显示/隐藏
- `title`: String - 弹窗标题,默认"温馨提示"
### Events
- `@agree`: 用户点击同意时触发
- `@close`: 用户关闭弹窗时触发
- `@cancel`: 用户取消操作时触发
### Methods
- `show()`: 显示弹窗
- `hide()`: 隐藏弹窗
## 文件结构
```
AgreePopup/
├── README.md # 组件说明文档
├── index.vue # 组件主文件
├── styles/
│ └── index.scss # 组件样式文件
└── images/
└── 登录授权1.png # 设计稿参考图
```

View File

@@ -0,0 +1,133 @@
<template>
<view class="demo-container">
<view class="demo-header">
<text class="demo-title">AgreePopup 组件演示</text>
</view>
<view class="demo-content">
<button class="demo-btn" @click="showPopup">显示用户协议弹窗</button>
<view class="demo-info">
<text class="info-title">组件状态</text>
<text class="info-text">弹窗可见{{ popupVisible }}</text>
<text class="info-text">用户操作{{ userAction }}</text>
</view>
</view>
<!-- AgreePopup 组件 -->
<AgreePopup
:visible="popupVisible"
title="温馨提示"
@agree="handleAgree"
@close="handleClose"
@cancel="handleCancel"
/>
</view>
</template>
<script setup>
import { ref } from 'vue'
import AgreePopup from './index.vue'
// 响应式数据
const popupVisible = ref(false)
const userAction = ref('无')
// 方法定义
const showPopup = () => {
popupVisible.value = true
userAction.value = '显示弹窗'
}
const handleAgree = () => {
popupVisible.value = false
userAction.value = '用户同意协议'
console.log('用户同意了协议')
}
const handleClose = () => {
popupVisible.value = false
userAction.value = '用户关闭弹窗'
console.log('用户关闭了弹窗')
}
const handleCancel = () => {
popupVisible.value = false
userAction.value = '用户取消操作'
console.log('用户取消了操作')
}
</script>
<style scoped lang="scss">
.demo-container {
padding: 20px;
min-height: 100vh;
background: #f5f5f5;
.demo-header {
text-align: center;
margin-bottom: 40px;
.demo-title {
font-size: 24px;
font-weight: 600;
color: #333333;
}
}
.demo-content {
display: flex;
flex-direction: column;
align-items: center;
.demo-btn {
width: 200px;
height: 44px;
background: #007AFF;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
margin-bottom: 40px;
cursor: pointer;
&:hover {
background: #0056CC;
}
&:active {
background: #004499;
transform: scale(0.98);
}
}
.demo-info {
background: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-width: 300px;
.info-title {
font-size: 16px;
font-weight: 600;
color: #333333;
display: block;
margin-bottom: 12px;
}
.info-text {
font-size: 14px;
color: #666666;
display: block;
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
}
}
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -0,0 +1,91 @@
<template>
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="agree-popup">
<!-- 弹窗头部 -->
<view class="popup-header">
<view class="popup-title">{{ title }}</view>
<view class="close-btn" @click="handleClose">
<uni-icons type="closeempty" size="24" color="#999" />
</view>
</view>
<!-- 弹窗内容 -->
<view class="popup-content">
<view class="content-text">
<text class="main-text">
您在使用朵花温泉服务前请仔细阅读用户隐私条款及用户注册须知当您点击同意即表示您已经理解并同意该条款该条款将构成对您具有法律约束力的文件
</text>
</view>
<view class="notice-text">
<text>
请您注意如果您不同意上述用户注册须知隐私政策或其中任何约定请您停止注册如您阅读并点击同意即表示您已充分阅读理解并接受其全部内容并表明您也同意朵花温泉可以依据以上隐私政策来处理您的个人信息
</text>
</view>
</view>
<!-- 确认按钮 -->
<view class="button-area">
<view class="confirm-btn" @click="handleClose"> 我知道了 </view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, watch, defineProps, defineEmits, defineExpose } from "vue";
// Props定义
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "温馨提示",
},
});
// Events定义
const emits = defineEmits(["agree", "close", "cancel"]);
// 响应式数据
const popup = ref(null);
// 监听visible变化
watch(
() => props.visible,
(newVal) => {
if (newVal) {
show();
} else {
hide();
}
}
);
// 方法定义
const show = () => {
popup.value?.open();
};
const hide = () => {
popup.value?.close();
};
const handleClose = () => {
hide();
emits("close");
};
// 暴露方法给父组件
defineExpose({
show,
hide,
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,95 @@
// AgreePopup 组件样式
.agree-popup {
width: 327px;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
// 弹窗头部
.popup-header {
position: relative;
padding: 20px 20px 0 20px;
.popup-title {
font-size: 18px;
font-weight: 600;
color: #333333;
text-align: center;
line-height: 24px;
}
.close-btn {
position: absolute;
top: 16px;
right: 16px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background: #f5f5f5;
border-radius: 50%;
}
}
}
// 弹窗内容
.popup-content {
padding: 20px;
.content-text {
margin-bottom: 16px;
.main-text {
font-size: 14px;
color: #333;
line-height: 22px;
display: block;
}
}
.notice-text {
text {
font-size: 13px;
color: #333;
line-height: 20px;
display: block;
}
}
}
// 按钮区域
.button-area {
padding: 0 20px 20px 20px;
display: flex;
justify-content: center;
align-items: center;
.confirm-btn {
width: 148px;
height: 44px;
background: linear-gradient(90deg, #22a7ff 0%, #2567ff 100%);
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
border-radius: 50px;
font-size: 16px;
font-weight: 500;
transition: all 0.3s ease;
&:hover {
background: #0056cc;
}
&:active {
background: #004499;
transform: scale(0.98);
}
}
}
}

View File

@@ -42,29 +42,33 @@
<view class="login-agreement">
<CheckBox v-model="isAgree">
<text class="login-agreement-text">阅读并同意</text>
<navigator
url="/pages/service-agreement/service-agreement"
<text
class="login-agreement-link"
>服务协议</navigator
@click.stop="handleAgreeClick('service')"
>服务协议</text
>
<text class="login-agreement-text"></text>
<navigator
url="/pages/privacy-policy/privacy-policy"
<text
class="login-agreement-link"
>隐私协议</navigator
@click.stop="handleAgreeClick('privacy')"
>隐私协议</text
>
</CheckBox>
</view>
<AgreePopup ref="agreePopup" :visible="visible" @close="visible = false" />
</view>
</template>
<script setup>
import { ref } from "vue";
import CheckBox from "@/components/CheckBox/index.vue";
import AgreePopup from "./components/AgreePopup/index.vue";
import { loginAuth, bindPhone, checkPhone } from "@/manager/LoginManager";
import { goHome } from "@/hooks/useGoHome";
const isAgree = ref(false);
const visible = ref(false);
// 同意隐私协议并获取手机号
const handleAgreeAndGetPhone = () => {
@@ -103,6 +107,11 @@ const onLogin = (e) => {
console.error("登录失败", err);
});
};
// 处理同意协议点击事件
const handleAgreeClick = (type) => {
visible.value = true;
};
</script>
<style lang="scss" scoped>