Files
YGChatCS/pages/goods/components/GoodConfirm/index.vue
2025-09-06 16:24:44 +08:00

357 lines
9.3 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.

<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>