357 lines
9.3 KiB
Vue
357 lines
9.3 KiB
Vue
<template>
|
||
<uni-popup
|
||
ref="popup"
|
||
type="bottom"
|
||
background-color="#fff"
|
||
border-radius="12px 12px 0 0"
|
||
mask-background-color="rgba(0,0,0,0.5)"
|
||
:safe-area="false"
|
||
>
|
||
<view class="good-container">
|
||
<!-- 头部区域 -->
|
||
<view class="header">
|
||
<view class="header-title">填写信息</view>
|
||
<view class="close-btn" @click="closePopup">
|
||
<uni-icons type="closeempty" size="24" color="#333" />
|
||
</view>
|
||
</view>
|
||
<!-- 商品信息区域 -->
|
||
<scroll-view
|
||
class="good-content"
|
||
:scroll-y="true"
|
||
:show-scrollbar="false"
|
||
>
|
||
<view class="wrapper">
|
||
<view class="good-info-wrapper">
|
||
<!-- 轮播图区域 -->
|
||
<ImageSwiper
|
||
:images="goodsData.commodityPhotoList"
|
||
:height="130"
|
||
:border-radius="0"
|
||
:showThumbnails="false"
|
||
/>
|
||
|
||
<!-- 商品信息区域 -->
|
||
<view class="goods-info">
|
||
<view class="goods-details">
|
||
<view class="goods-title">{{
|
||
goodsData.commodityName || "商品名称"
|
||
}}</view>
|
||
<view class="goods-price">
|
||
<text class="currency">¥</text>
|
||
<text class="price">
|
||
{{ goodsData.specificationPrice || 399 }}
|
||
</text>
|
||
</view>
|
||
<view
|
||
class="goods-service-list"
|
||
v-if="
|
||
goodsData.commodityServiceList &&
|
||
goodsData.commodityServiceList.length
|
||
"
|
||
>
|
||
<view class="service-title">包含服务</view>
|
||
<view
|
||
class="goods-service-item"
|
||
v-for="item in goodsData.commodityServiceList"
|
||
:key="item.serviceTitle"
|
||
>
|
||
<text class="service-label">{{ item.serviceTitle }}</text>
|
||
<text class="service-value">{{ item.serviceAmount }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 数量选择区域 -->
|
||
<view class="quantity-section">
|
||
<ModuleTitle :title="sectionTitle" />
|
||
|
||
<Stepper v-model="quantity" />
|
||
</view>
|
||
|
||
<!-- 游客信息区域 -->
|
||
<scroll-view
|
||
class="user-form-list"
|
||
:scroll-x="true"
|
||
:show-scrollbar="false"
|
||
>
|
||
<FormCard
|
||
v-for="(item, index) in userFormList"
|
||
:title="userCardTitle(index)"
|
||
:form="item"
|
||
:showDeleteIcon="userFormList.length > 1"
|
||
:key="index"
|
||
@update:visitorName="
|
||
(value) => updateUserForm(index, 'visitorName', value)
|
||
"
|
||
@update:contactPhone="
|
||
(value) => updateUserForm(index, 'contactPhone', value)
|
||
"
|
||
@delete="() => deleteUserForm(index)"
|
||
/>
|
||
</scroll-view>
|
||
|
||
<!-- 总价区域 -->
|
||
<SumCard
|
||
:referencePrice="goodsData.specificationPrice"
|
||
:discount="totalPrice"
|
||
/>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部按钮区域 -->
|
||
<view class="footer">
|
||
<view class="left">
|
||
<text class="total-count">共{{ quantity }}间,合计:</text>
|
||
<text class="total-price">{{ totalPrice }}</text>
|
||
</view>
|
||
|
||
<view class="confirm-btn" @click="confirmOrder">立即支付</view>
|
||
</view>
|
||
</view>
|
||
</uni-popup>
|
||
</template>
|
||
|
||
<script setup>
|
||
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.visitorName === "string" &&
|
||
user.visitorName.trim() !== "" &&
|
||
typeof user.contactPhone === "string" &&
|
||
user.contactPhone.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",
|
||
specificationPrice: "",
|
||
commodityName: "",
|
||
commodityPhotoList: [],
|
||
commodityServiceList: [],
|
||
}),
|
||
validator: (value) => {
|
||
return value && typeof value === "object";
|
||
},
|
||
},
|
||
});
|
||
|
||
// Emits定义
|
||
const emits = defineEmits(["confirm", "close"]);
|
||
|
||
// 工具函数
|
||
const createEmptyUserForm = () => {
|
||
return { visitorName: "", contactPhone: "" };
|
||
};
|
||
|
||
// 响应式数据
|
||
const popup = ref(null);
|
||
const quantity = ref(MIN_USER_COUNT);
|
||
const userFormList = ref([createEmptyUserForm()]); // 初始化一个表单项
|
||
const isDeleting = ref(false); // 标志位,防止删除时watch冲突
|
||
|
||
// 计算属性
|
||
const totalPrice = computed(() => {
|
||
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,
|
||
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) {
|
||
// 数量增加,添加新的表单项
|
||
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 }
|
||
);
|
||
|
||
// 方法定义
|
||
const showPopup = () => {
|
||
popup.value?.open();
|
||
};
|
||
|
||
const closePopup = () => {
|
||
popup.value?.close();
|
||
emits("close");
|
||
};
|
||
|
||
const updateUserForm = (index, field, value) => {
|
||
if (!userFormList.value[index]) {
|
||
return;
|
||
}
|
||
|
||
if (!["visitorName", "contactPhone"].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 <= MIN_USER_COUNT) {
|
||
const message = isHotelType.value
|
||
? "至少需要一个房间信息"
|
||
: "至少需要一位游客信息";
|
||
showToast(message);
|
||
return;
|
||
}
|
||
|
||
// 设置删除标志位,防止watch监听器干扰
|
||
isDeleting.value = true;
|
||
|
||
// 删除指定索引的表单项
|
||
userFormList.value.splice(index, 1);
|
||
|
||
// 同步更新quantity
|
||
quantity.value = userFormList.value.length;
|
||
};
|
||
|
||
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) => ({
|
||
visitorName: user.visitorName.trim(),
|
||
contactPhone: user.contactPhone.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>
|
||
|
||
<style scoped lang="scss">
|
||
@import "./styles/index.scss";
|
||
</style>
|