Merge branch 'order-729'
合并order-729分支代码
This commit is contained in:
@@ -8,6 +8,10 @@
|
||||
- 📱 **响应式布局** - 完美适配各种屏幕尺寸
|
||||
- 🛒 **商品信息展示** - 支持商品图片、标题、价格、标签展示
|
||||
- 🔢 **数量选择** - 提供加减按钮和手动输入两种方式
|
||||
- 👥 **动态表单管理** - 根据数量自动添加/删除游客信息表单
|
||||
- 📝 **表单数据绑定** - 支持姓名和手机号的双向绑定
|
||||
- 📱 **横向滚动支持** - 游客信息区域支持横向滚动浏览多个表单
|
||||
- 🗑️ **删除表单支持** - 支持删除游客信息表单,自动同步数量和列表长度
|
||||
- 💰 **实时计算** - 自动计算并显示总价
|
||||
- ⚡ **性能优化** - 基于 Vue 3 Composition API,性能卓越
|
||||
- 🎭 **动画效果** - 流畅的弹出和交互动画
|
||||
@@ -53,7 +57,13 @@ const showConfirm = () => {
|
||||
|
||||
const handleConfirm = (orderData) => {
|
||||
console.log('确认订单:', orderData)
|
||||
// 处理订单确认逻辑
|
||||
// orderData 包含:
|
||||
// {
|
||||
// goodsData: 商品数据,
|
||||
// quantity: 购买数量,
|
||||
// totalPrice: 总价,
|
||||
// userFormList: 游客信息列表 [{ name: '', phone: '' }, ...]
|
||||
// }
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -114,6 +124,10 @@ interface OrderData {
|
||||
goodsData: GoodsData; // 商品数据
|
||||
quantity: number; // 购买数量
|
||||
totalPrice: string; // 总价(字符串格式)
|
||||
userFormList: Array<{ // 游客信息列表
|
||||
name: string; // 游客姓名
|
||||
phone: string; // 游客手机号
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -202,9 +216,50 @@ const handleConfirm = async (orderData) => {
|
||||
3. **数量限制**:组件默认最小购买数量为 1,可根据业务需求调整
|
||||
4. **价格格式**:价格支持数字类型,组件内部会自动处理格式化
|
||||
5. **事件处理**:建议在 `confirm` 事件中添加适当的错误处理和用户反馈
|
||||
6. **表单管理**:游客信息表单会根据购买数量自动调整,无需手动管理
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.4.2 (2024-12-19)
|
||||
**修复Stepper组件响应性问题**
|
||||
- 🐛 修复Stepper组件不响应外部modelValue变化的问题
|
||||
- 🔄 添加watch监听器,确保Stepper组件能实时同步外部值变化
|
||||
- ✨ 完善删除表单卡片时Stepper显示值的自动更新
|
||||
- 🎯 提升组件间数据同步的准确性和实时性
|
||||
|
||||
### v1.4.1 (2024-12-19)
|
||||
**修复quantity更新问题**
|
||||
- 🐛 修复删除表单时quantity值未正确更新的问题
|
||||
- 🔧 优化watch监听器逻辑,添加删除标志位防止冲突
|
||||
- 🎯 改进deleteUserForm方法,确保数据同步准确
|
||||
- 🔍 增强demo页面调试信息,便于测试验证
|
||||
|
||||
### v1.4.0
|
||||
- 🗑️ **新增删除功能** - 支持点击删除图标删除游客信息表单
|
||||
- 🔄 **智能数量同步** - 删除表单时自动同步quantity数量和userFormList长度
|
||||
- 🛡️ **最小限制保护** - 确保至少保留一位游客信息,防止全部删除
|
||||
- 🎯 **动态图标控制** - 只有多于一个表单时才显示删除图标
|
||||
- 📱 **用户体验优化** - 删除操作提供友好的提示信息
|
||||
|
||||
### v1.3.0
|
||||
- 🚫 **防换行优化** - 确保FormCard组件在任何情况下都不会换行显示
|
||||
- 📐 **布局增强** - 添加flex-shrink: 0确保表单卡片保持固定宽度
|
||||
- 🎯 **滚动优化** - 改进横向滚动体验,支持触摸滚动
|
||||
- 🧪 **测试改进** - demo页面新增多人数测试按钮,便于测试横向滚动效果
|
||||
|
||||
### v1.2.0
|
||||
- 📱 **新增横向滚动** - 游客信息区域支持横向滚动浏览多个表单
|
||||
- 🎨 **优化布局设计** - 表单卡片采用固定宽度,提升视觉体验
|
||||
- 🔧 **改进滚动体验** - 隐藏滚动条,提供更清爽的界面
|
||||
- 📐 **响应式优化** - 确保在不同屏幕尺寸下的良好表现
|
||||
|
||||
### v1.1.0
|
||||
- 🎉 **新增动态表单管理** - 根据购买数量自动添加/删除游客信息表单
|
||||
- 📝 **新增表单数据绑定** - 支持游客姓名和手机号的双向绑定
|
||||
- 🔄 **优化数据结构** - 确认订单时返回完整的用户信息列表
|
||||
- 🎯 **改进用户体验** - 表单项数量与购买数量实时同步
|
||||
- 🐛 **修复已知问题** - 优化组件初始化和数据更新逻辑
|
||||
|
||||
### v1.0.0 (2024-01-XX)
|
||||
- ✨ 初始版本发布
|
||||
- 🎨 基于 uni-popup 的底部弹出设计
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
<template>
|
||||
<view class="demo-container">
|
||||
<TopNavBar title="商品确认组件演示" :fixed="true" />
|
||||
<view class="demo-header">
|
||||
<text class="demo-title">GoodConfirm 组件演示</text>
|
||||
</view>
|
||||
|
||||
<view class="content-wrapper">
|
||||
<view class="demo-section">
|
||||
<view class="section-title">基础用法</view>
|
||||
<view class="demo-item">
|
||||
<button class="demo-btn" @click="showBasicDemo">显示基础确认弹窗</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="demo-section">
|
||||
<view class="section-title">自定义商品数据</view>
|
||||
<view class="demo-item">
|
||||
<button class="demo-btn" @click="showCustomDemo">显示自定义商品弹窗</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="demo-section">
|
||||
<view class="section-title">功能特性</view>
|
||||
<view class="feature-list">
|
||||
<view class="feature-item">✓ 基于 uni-popup 弹出层组件</view>
|
||||
<view class="feature-item">✓ 商品信息展示(图片、标题、价格、标签)</view>
|
||||
<view class="feature-item">✓ 数量选择控制(加减按钮、手动输入)</view>
|
||||
<view class="feature-item">✓ 实时总价计算</view>
|
||||
<view class="feature-item">✓ 确认购买和关闭事件</view>
|
||||
<view class="feature-item">✓ 响应式设计,适配移动端</view>
|
||||
<view class="feature-item">✓ 优雅的动画效果</view>
|
||||
<view class="demo-section">
|
||||
<view class="section-title">基础用法</view>
|
||||
<button class="demo-btn" @click="showConfirm">显示商品确认弹窗</button>
|
||||
<button class="demo-btn" @click="setQuantity(5)" style="margin-left: 12px;">设置5人测试横向滚动</button>
|
||||
</view>
|
||||
|
||||
<view class="demo-section">
|
||||
<view class="section-title">当前状态</view>
|
||||
<view class="status-info">
|
||||
<text>Stepper数量: {{ quantity }}</text>
|
||||
<text>表单项数量: {{ userFormCount }}</text>
|
||||
<text>总价: ¥{{ totalPrice }}</text>
|
||||
<text class="debug-info">实时quantity值: {{ quantity }}</text>
|
||||
<text class="feature-highlight">✨ 支持横向滚动浏览多个游客信息</text>
|
||||
<text class="feature-highlight">🗑️ 支持删除游客信息(至少保留一位)</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="demo-section" v-if="lastOrderData">
|
||||
<view class="section-title">最后提交的数据</view>
|
||||
<view class="order-data">
|
||||
<text>商品: {{ lastOrderData.goodsData.commodityName }}</text>
|
||||
<text>数量: {{ lastOrderData.quantity }}</text>
|
||||
<text>总价: ¥{{ lastOrderData.totalPrice }}</text>
|
||||
<text>用户信息:</text>
|
||||
<view class="user-list">
|
||||
<view
|
||||
v-for="(user, index) in lastOrderData.userFormList"
|
||||
:key="index"
|
||||
class="user-item"
|
||||
>
|
||||
<text>游客{{ index + 1 }}: {{ user.name || '未填写' }} - {{ user.phone || '未填写' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基础演示弹窗 -->
|
||||
|
||||
<GoodConfirm
|
||||
ref="basicDemoRef"
|
||||
:goodsData="basicGoodsData"
|
||||
@confirm="handleBasicConfirm"
|
||||
@close="handleBasicClose"
|
||||
/>
|
||||
|
||||
<!-- 自定义演示弹窗 -->
|
||||
<GoodConfirm
|
||||
ref="customDemoRef"
|
||||
:goodsData="customGoodsData"
|
||||
@confirm="handleCustomConfirm"
|
||||
@close="handleCustomClose"
|
||||
ref="confirmRef"
|
||||
:goodsData="goodsData"
|
||||
@confirm="handleConfirm"
|
||||
@close="handleClose"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import TopNavBar from '@/components/TopNavBar/index.vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import GoodConfirm from './index.vue'
|
||||
|
||||
// 引用
|
||||
const basicDemoRef = ref(null)
|
||||
const customDemoRef = ref(null)
|
||||
const confirmRef = ref(null)
|
||||
const quantity = ref(1)
|
||||
const userFormCount = ref(1)
|
||||
const lastOrderData = ref(null)
|
||||
|
||||
// 基础商品数据
|
||||
const basicGoodsData = ref({
|
||||
commodityName: '【成人票】云从朵花温泉门票',
|
||||
price: 399,
|
||||
timeTag: '随时可退',
|
||||
const goodsData = ref({
|
||||
commodityName: '云从朵花温泉票(成人票)',
|
||||
price: 70,
|
||||
timeTag: '条件退款',
|
||||
commodityPhotoList: [
|
||||
{
|
||||
photoUrl: '/static/test/mk_img_1.png'
|
||||
@@ -70,62 +70,61 @@ const basicGoodsData = ref({
|
||||
]
|
||||
})
|
||||
|
||||
// 自定义商品数据
|
||||
const customGoodsData = ref({
|
||||
commodityName: '【亲子套票】海洋世界一日游',
|
||||
price: 268,
|
||||
timeTag: '限时优惠',
|
||||
commodityPhotoList: [
|
||||
{
|
||||
photoUrl: '/static/test/mk_img_1.png'
|
||||
}
|
||||
]
|
||||
const totalPrice = computed(() => {
|
||||
return (goodsData.value.price * quantity.value).toFixed(0)
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
const showBasicDemo = () => {
|
||||
basicDemoRef.value?.showPopup()
|
||||
const showConfirm = () => {
|
||||
confirmRef.value?.showPopup()
|
||||
}
|
||||
|
||||
const showCustomDemo = () => {
|
||||
customDemoRef.value?.showPopup()
|
||||
}
|
||||
|
||||
const handleBasicConfirm = (orderData) => {
|
||||
console.log('基础演示确认订单:', orderData)
|
||||
uni.showModal({
|
||||
title: '订单确认',
|
||||
content: `商品:${orderData.goodsData.commodityName}\n数量:${orderData.quantity}\n总价:¥${orderData.totalPrice}`,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
|
||||
const handleBasicClose = () => {
|
||||
console.log('基础演示关闭弹窗')
|
||||
}
|
||||
|
||||
const handleCustomConfirm = (orderData) => {
|
||||
console.log('自定义演示确认订单:', orderData)
|
||||
uni.showModal({
|
||||
const handleConfirm = (orderData) => {
|
||||
console.log('确认订单:', orderData)
|
||||
lastOrderData.value = orderData
|
||||
quantity.value = orderData.quantity
|
||||
userFormCount.value = orderData.userFormList.length
|
||||
|
||||
uni.showToast({
|
||||
title: '订单确认成功',
|
||||
content: `商品:${orderData.goodsData.commodityName}\n数量:${orderData.quantity}\n总价:¥${orderData.totalPrice}`,
|
||||
showCancel: false
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const handleCustomClose = () => {
|
||||
console.log('自定义演示关闭弹窗')
|
||||
const handleClose = () => {
|
||||
console.log('弹窗关闭')
|
||||
}
|
||||
|
||||
const setQuantity = (num) => {
|
||||
quantity.value = num
|
||||
userFormCount.value = num
|
||||
// 如果弹窗已打开,需要通过组件内部的quantity来更新
|
||||
if (confirmRef.value) {
|
||||
// 这里可以通过ref访问组件内部状态,但由于组件封装,我们通过重新打开来演示
|
||||
uni.showToast({
|
||||
title: `已设置${num}人,请重新打开弹窗查看效果`,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.demo-container {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 100px 20px 20px;
|
||||
.demo-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.demo-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
@@ -134,57 +133,89 @@ const handleCustomClose = () => {
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #ff6b35;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8f65 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
.demo-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8f65 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 1px 4px rgba(255, 107, 53, 0.3);
|
||||
.status-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
padding: 8px 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
|
||||
&.feature-highlight {
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8f65 100%);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
|
||||
&.debug-info {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
font-weight: bold;
|
||||
border: 1px solid #91d5ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
.feature-item {
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
.order-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
text {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.user-list {
|
||||
margin-top: 8px;
|
||||
padding-left: 12px;
|
||||
|
||||
.user-item {
|
||||
margin-bottom: 4px;
|
||||
|
||||
text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +1,107 @@
|
||||
<template>
|
||||
<uni-popup ref="popup" type="bottom">
|
||||
<view class="good-confirm-container">
|
||||
<!-- 头部标题栏 -->
|
||||
<uni-popup
|
||||
ref="popup"
|
||||
type="bottom"
|
||||
background-color="#E9F3F7"
|
||||
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="header-title">填写信息</view>
|
||||
<view class="close-btn" @click="closePopup">
|
||||
<uni-icons type="closeempty" size="20" color="#666"></uni-icons>
|
||||
<uni-icons type="closeempty" size="24" color="#333" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品信息区域 -->
|
||||
<view class="goods-info">
|
||||
<view class="goods-image">
|
||||
<image
|
||||
:src="
|
||||
goodsData.commodityPhotoList?.[0]?.photoUrl ||
|
||||
'/static/test/mk_img_1.png'
|
||||
"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
<view class="goods-details">
|
||||
<view class="goods-title">{{
|
||||
goodsData.commodityName || "商品名称"
|
||||
}}</view>
|
||||
<view class="goods-price">
|
||||
<text class="currency">¥</text>
|
||||
<text class="price">{{ goodsData.price || 399 }}</text>
|
||||
</view>
|
||||
<view class="goods-tag" v-if="goodsData.timeTag">
|
||||
{{ goodsData.timeTag }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数量选择区域 -->
|
||||
<view class="quantity-section">
|
||||
<view class="quantity-label">购买数量</view>
|
||||
<view class="quantity-control">
|
||||
<view
|
||||
class="quantity-btn"
|
||||
:class="{ disabled: quantity <= 1 }"
|
||||
@click="decreaseQuantity"
|
||||
>
|
||||
<uni-icons type="minus" size="16" color="#666"></uni-icons>
|
||||
</view>
|
||||
<view class="quantity-input">
|
||||
<input
|
||||
type="number"
|
||||
v-model="quantity"
|
||||
@input="handleQuantityInput"
|
||||
:disabled="false"
|
||||
<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>
|
||||
<view class="quantity-btn" @click="increaseQuantity">
|
||||
<uni-icons type="plus" size="16" color="#666"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 总价区域 -->
|
||||
<view class="total-section">
|
||||
<view class="total-label">合计</view>
|
||||
<view class="total-price">
|
||||
<text class="currency">¥</text>
|
||||
<text class="price">{{ totalPrice }}</text>
|
||||
<!-- 商品信息区域 -->
|
||||
<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.price || 399 }}</text>
|
||||
</view>
|
||||
<view class="goods-service-list">
|
||||
<view class="service-title">包含服务</view>
|
||||
<view class="goods-service-item">
|
||||
<text class="service-label">随时可退</text>
|
||||
<text class="service-value">1份</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数量选择区域 -->
|
||||
<view class="quantity-section">
|
||||
<ModuleTitle title="游客信息" />
|
||||
|
||||
<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="`游客${index + 1}`"
|
||||
:form="item"
|
||||
:showDeleteIcon="userFormList.length > 1"
|
||||
:key="index"
|
||||
@update:name="(value) => updateUserForm(index, 'name', value)"
|
||||
@update:phone="(value) => updateUserForm(index, 'phone', value)"
|
||||
@delete="() => deleteUserForm(index)"
|
||||
/>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 总价区域 -->
|
||||
<SumCard />
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部按钮区域 -->
|
||||
<view class="footer">
|
||||
<button class="confirm-btn" @click="confirmOrder">确认购买</button>
|
||||
<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, defineProps, defineEmits } from "vue";
|
||||
import { ref, computed, watch, defineProps, defineEmits } 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";
|
||||
|
||||
// Props定义
|
||||
const props = defineProps({
|
||||
@@ -93,6 +117,8 @@ const emits = defineEmits(["confirm", "close"]);
|
||||
// 响应式数据
|
||||
const popup = ref(null);
|
||||
const quantity = ref(1);
|
||||
const userFormList = ref([{ name: "", phone: "" }]); // 初始化一个表单项
|
||||
const isDeleting = ref(false); // 标志位,防止删除时watch冲突
|
||||
|
||||
// 计算属性
|
||||
const totalPrice = computed(() => {
|
||||
@@ -100,6 +126,31 @@ const totalPrice = computed(() => {
|
||||
return (price * quantity.value).toFixed(0);
|
||||
});
|
||||
|
||||
// 监听 quantity 变化,动态调整 userFormList
|
||||
watch(
|
||||
quantity,
|
||||
(newQuantity, oldQuantity) => {
|
||||
// 如果正在执行删除操作,跳过watch逻辑
|
||||
if (isDeleting.value) {
|
||||
isDeleting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLength = userFormList.value.length;
|
||||
|
||||
if (newQuantity > currentLength) {
|
||||
// 数量增加,添加新的表单项
|
||||
for (let i = currentLength; i < newQuantity; i++) {
|
||||
userFormList.value.push({ name: "", phone: "" });
|
||||
}
|
||||
} else if (newQuantity < currentLength) {
|
||||
// 数量减少,删除多余的表单项
|
||||
userFormList.value.splice(newQuantity);
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
// 方法定义
|
||||
const showPopup = () => {
|
||||
popup.value?.open();
|
||||
@@ -110,23 +161,31 @@ const closePopup = () => {
|
||||
emits("close");
|
||||
};
|
||||
|
||||
const increaseQuantity = () => {
|
||||
quantity.value++;
|
||||
};
|
||||
|
||||
const decreaseQuantity = () => {
|
||||
if (quantity.value > 1) {
|
||||
quantity.value--;
|
||||
const updateUserForm = (index, field, value) => {
|
||||
if (userFormList.value[index]) {
|
||||
userFormList.value[index][field] = value;
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuantityInput = (e) => {
|
||||
const value = parseInt(e.detail.value);
|
||||
if (value && value > 0) {
|
||||
quantity.value = value;
|
||||
} else {
|
||||
quantity.value = 1;
|
||||
const deleteUserForm = (index) => {
|
||||
// 确保至少保留一个表单项
|
||||
if (userFormList.value.length <= 1) {
|
||||
uni.showToast({
|
||||
title: "至少需要一位游客信息",
|
||||
icon: "none",
|
||||
duration: 2000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置删除标志位,防止watch监听器干扰
|
||||
isDeleting.value = true;
|
||||
|
||||
// 删除指定索引的表单项
|
||||
userFormList.value.splice(index, 1);
|
||||
|
||||
// 同步更新quantity
|
||||
quantity.value = userFormList.value.length;
|
||||
};
|
||||
|
||||
const confirmOrder = () => {
|
||||
@@ -134,6 +193,7 @@ const confirmOrder = () => {
|
||||
goodsData: props.goodsData,
|
||||
quantity: quantity.value,
|
||||
totalPrice: totalPrice.value,
|
||||
userFormList: userFormList.value,
|
||||
};
|
||||
emits("confirm", orderData);
|
||||
closePopup();
|
||||
|
||||
@@ -1,65 +1,61 @@
|
||||
.good-confirm-container {
|
||||
background: #fff;
|
||||
border-radius: 20px 20px 0 0;
|
||||
padding: 0;
|
||||
max-height: 80vh;
|
||||
.good-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
position: relative;
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.good-content {
|
||||
background: #e9f3f7;
|
||||
box-sizing: border-box;
|
||||
max-height: 60vh;
|
||||
overflow: hidden;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
position: relative;
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 16px;
|
||||
background: #f8f8f8;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:active {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
}
|
||||
.good-info-wrapper {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
.goods-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
padding: 12px;
|
||||
|
||||
.goods-details {
|
||||
flex: 1;
|
||||
@@ -82,7 +78,7 @@
|
||||
.goods-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.currency {
|
||||
font-size: 14px;
|
||||
@@ -98,15 +94,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
.goods-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #fff2e8;
|
||||
color: #ff6b35;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ffdbcc;
|
||||
align-self: flex-start;
|
||||
.goods-service-list {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.service-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.goods-service-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.service-label,
|
||||
.service-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.service-label {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.service-value {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,125 +134,70 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
.quantity-label {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quantity-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
.quantity-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8f8f8;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:active:not(.disabled) {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
width: 60px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
border-left: 1px solid #e8e8e8;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
box-sizing: border-box;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.total-section {
|
||||
.user-form-list {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.total-count {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.total-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-size: 24px;
|
||||
color: #f55726;
|
||||
|
||||
&::before {
|
||||
content: "¥";
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
width: 160px;
|
||||
height: 48px;
|
||||
background: linear-gradient(179deg, #00a6ff 0%, #0256ff 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
margin-left: auto;
|
||||
|
||||
.total-label {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.total-price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.currency {
|
||||
font-size: 16px;
|
||||
color: #ff6b35;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 24px;
|
||||
color: #ff6b35;
|
||||
font-weight: 700;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
|
||||
.confirm-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #ff6b35 0%, #ff8f65 100%);
|
||||
color: #fff;
|
||||
&::after {
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
<template>
|
||||
<view class="good-info">
|
||||
<!-- 价格区域 -->
|
||||
<view class="price-section">
|
||||
<view class="price-main">
|
||||
<text class="currency">¥</text>
|
||||
<text class="price">{{ goodsData.price || 399 }}</text>
|
||||
</view>
|
||||
<view class="price-tag" v-if="goodsData.tag">
|
||||
{{ goodsData.tag }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 标题区域 -->
|
||||
<view class="title-section">
|
||||
<text class="title">
|
||||
|
||||
@@ -2,33 +2,6 @@
|
||||
background: #fff;
|
||||
margin-bottom: 12px;
|
||||
|
||||
// 价格区域
|
||||
.price-section {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.price-main {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
.currency {
|
||||
font-size: 12px;
|
||||
color: #ff6a00;
|
||||
font-weight: 600;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.price {
|
||||
font-size: 18px;
|
||||
color: #ff6a00;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标题区域
|
||||
.title-section {
|
||||
margin-bottom: 12px;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<view class="goods-container">
|
||||
<TopNavBar title="商品详情" :fixed="true" />
|
||||
<TopNavBar title="商品详情" />
|
||||
|
||||
<view class="content-wrapper">
|
||||
<ImageSwiper :border-radius="0" :images="goodsData.commodityPhotoList" />
|
||||
<!-- 滚动区域 -->
|
||||
<scroll-view class="content-wrapper" scroll-y>
|
||||
<ImageSwiper
|
||||
:border-radius="0"
|
||||
:height="300"
|
||||
:images="goodsData.commodityPhotoList"
|
||||
/>
|
||||
|
||||
<view class="goods-content">
|
||||
<!-- 商品信息组件 -->
|
||||
@@ -12,12 +17,16 @@
|
||||
<ModuleTitle title="购买须知" />
|
||||
|
||||
<zero-markdown-view :markdown="goodsData.commodityTip" :fontSize="14" />
|
||||
|
||||
<!-- 立即抢购 -->
|
||||
<view class="footer">
|
||||
<button class="buy-button" @click="showConfirmPopup">立即抢购</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 立即抢购 -->
|
||||
<view class="footer">
|
||||
<view class="left">
|
||||
<text class="label">价格:</text>
|
||||
<text class="price">{{ goodsData.commodityPrice || 399 }}</text>
|
||||
</view>
|
||||
<view class="buy-button" @click="showConfirmPopup">立即抢购</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品确认弹窗 -->
|
||||
|
||||
@@ -2,14 +2,24 @@ $button-color: #00a6ff;
|
||||
$button-hover-color: darken($button-color, 8%);
|
||||
|
||||
.goods-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
|
||||
// 顶部导航栏固定样式
|
||||
:deep(.top-nav-bar) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
// 为固定导航栏预留空间
|
||||
padding-top: calc(var(--status-bar-height, 44px) + 68px);
|
||||
// 为安全区预留空间
|
||||
padding-bottom: calc(var(--safe-area-inset-bottom, 0px) + 100px);
|
||||
flex: 1;
|
||||
height: 0; // 关键:让flex子项能够正确计算高度
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch; // iOS平滑滚动
|
||||
}
|
||||
|
||||
.goods-content {
|
||||
@@ -20,77 +30,101 @@ $button-hover-color: darken($button-color, 8%);
|
||||
margin-top: -30px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #fff;
|
||||
padding-top: 12px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08);
|
||||
// 为安全区预留空间
|
||||
padding-bottom: var(--safe-area-inset-bottom, 0);
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
padding: 12px;
|
||||
// 为安全区预留空间
|
||||
padding-bottom: var(--safe-area-inset-bottom, 0);
|
||||
// 阴影
|
||||
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
flex-shrink: 0; // 防止被压缩
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.buy-button {
|
||||
width: 100%;
|
||||
background: linear-gradient(179deg, #00a6ff 0%, #0256ff 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-top: 12px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
letter-spacing: 0.5px;
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.price {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-size: 24px;
|
||||
color: #f55726;
|
||||
|
||||
&::before {
|
||||
content: "¥";
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.buy-button {
|
||||
width: 160px;
|
||||
background: linear-gradient(179deg, #00a6ff 0%, #0256ff 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
letter-spacing: 0.5px;
|
||||
margin-left: auto;
|
||||
|
||||
// 按钮波纹效果
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$button-hover-color 0%,
|
||||
darken($button-hover-color, 5%) 100%
|
||||
);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba($button-color, 0.4);
|
||||
|
||||
// 按钮波纹效果
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$button-hover-color 0%,
|
||||
darken($button-hover-color, 5%) 100%
|
||||
);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba($button-color, 0.4);
|
||||
&:active {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba($button-color, 0.3);
|
||||
}
|
||||
|
||||
&::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba($button-color, 0.3);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba($button-color, 0.3);
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba($button-color, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user