Files
h5-meike/pages/ticket/ordersure.vue
T
xuchaoandClaude Opus 4.8 f767aa93de fix: 设备适配 —— 宽屏居中锁定手机尺寸 + 全面屏底部安全区
宽屏(桌面/平板):
- pages.json 配置 rpxCalcMaxDeviceWidth=750/rpxCalcBaseDeviceWidth=375,
  窗口超过 750px 时 rpx 换算基准锁定为 375px(uni-app 官方机制),不再随屏幕拉伸
- 页面列宽 750rpx 居中展示,两侧露出底色+阴影形成手机框视觉
- 固定底栏统一 left/right:0+margin:auto 视口居中,与页面列对齐
- 悬浮球及菜单项宽屏时锚定到页面列左缘

全面屏(iPhone 刘海屏):
- 订单底栏/订单详情按钮栏/弹窗确认栏用同色边框向下延伸覆盖 home 指示条区域
- 悬浮按钮(完成/选择)整体上移避开安全区
- 确认订单页底栏改为 bottom 锚定(原来用 JS 计算 top)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 08:57:40 +08:00

735 lines
20 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>
<view v-if="skuObj">
<!-- <view style="position: fixed;top: 0px;width: 750rpx;z-index: 1000;" :style="{height:windowHeight+'px'}">
<view style="width: inherit;" :style="{height:windowHeight-62+'px'}"></view> -->
<view class="db_rq" :style="{top:windowHeight-62+'px'}">
<view>
<text style="color: rgba(0, 0, 0, 0.5);font-size: 20rpx;">总价</text>
<view style="display: flex;align-items: flex-end;gap: 24rpx;" @tap='show1=true'>
<text style="color: #FF0000;font-size: 48rpx;font-weight: bold;">¥{{salesPriceSum}}</text>
<view style="margin-bottom: 10rpx;display: flex;align-items: center;gap: 10rpx;color: #457AF7;">
明细<u-icon name="arrow-down-fill" size='20'></u-icon></view>
</view>
</view>
<view class="bt" @tap="buy">立即购买</view>
</view>
<!-- </view> -->
<view style="padding: 24rpx;background-color: #fff;">
<view style="color: #000;font-size: 32rpx;font-weight: bold;line-height: normal;">
{{skuObj.sku.skuName}}
</view>
<text style="color: rgba(0, 0, 0, 0.5);font-size: 20rpx;">适用年龄:{{skuObj.sku.typeInfo}}</text>
<view style="margin-top: 24rpx;border-top: 1px dashed rgba(0, 0, 0, 0.1);"></view>
<view style="margin-top: 24rpx;color: rgba(0, 0, 0, 0.5);font-size: 500;position: relative;">使用日期
</view>
<view style="width: 702rpx;display: flex;gap: 8rpx;margin-top: 12rpx;">
<view v-for="(item,index) in cList" :key="index" v-if="index<3"
:class="item.isSelect?'mt_rq':item.isEnabled?'jt_rq':'ht_rq'" @click="selectDate(item)">
{{getDayName(item.enableDate)}}<br />
{{formatToMonthDay(item.enableDate)}}
<view class="jt_tag">{{item.isSelect?'√':item.isEnabled?'可订':'不可订'}}</view>
</view>
<view class="gd_rq" @tap="show=true">更多<br />
日期
<u-icon name="arrow-right" style="margin-left: 5rpx;"></u-icon>
</view>
</view>
<view style="margin-top: 24rpx;color: rgba(0, 0, 0, 0.5);font-size: 500;" v-if="!skuObj.sku.stockDateType">
适用时段</view>
<view style="width: 702rpx;display: flex;gap: 12rpx;margin-top: 12rpx;flex-wrap: wrap;"
v-if="!skuObj.sku.stockDateType">
<view :class="item.isSelect?'xz_item':'nx_item' " v-for="(item,index) in timeSlotList" :key="index"
@click="selectSlot(index)">{{item.name}}
<view class="xz_tag">余{{item.stock}}张</view>
</view>
</view>
<view style="margin-top: 24rpx;border-top: 1px dashed rgba(0, 0, 0, 0.1);"></view>
<view style="display: flex;align-items: center;margin-top: 24rpx;height: 96rpx;">
<text style="color: #000;font-size: 32rpx;font-weight: bold;">购买数量</text>
<u-number-box v-model="value" :max='getMaxStock()' :min='1' @change="valChange"
style="position: absolute;right: 24rpx;"></u-number-box>
</view>
</view>
<view style="padding: 24rpx;background-color: #fff;margin-top: 20rpx;" v-if="skuObj.sku.enterDemand=='2'">
<view style="display: flex;margin-top: 24rpx;align-items: flex-end;">
<text style="color: #000;font-size: 32rpx;font-weight: bold;">游客信息</text>
<view style="margin-left: 24rpx;color: rgba(0, 0, 0, 0.5);font-size: 20rpx;">
需要 <text style="color: #000;font-size: 24rpx;font-weight: bold;">{{value}}位</text> 出行游客信息,用于入园身份验证
</view>
</view>
<view style="margin-top: 24rpx;border-top: 1px dashed rgba(0, 0, 0, 0.1);"></view>
<view style="display: flex;gap: 24rpx;margin-top: 24rpx;">
<view class="xz_name" v-for="(item,index) in personList" :key="index">{{item.name}}</view>
<view class="nx_name" @tap='addPerson'>新增/更换 ></view>
</view>
<view v-for="(item,index) in personList" :key="index"
style="display: flex;width: 702rpx;color: #000;padding: 24rpx 0rpx;position:relative;border-bottom: 1px dashed rgba(0, 0, 0, 0.1);align-items: center;">
<u-icon name="close" style="color: rgba(0, 0, 0, 0.5);" @click="deletePerson(index)"></u-icon>
<text style="margin-left: 24rpx;">游客{{index+1}}</text>
<input style="margin-left: 104rpx;font-size: 28rpx;" v-model="item.name" :disabled="true" />
<input style="position: absolute;right: 0rpx;font-size: 28rpx;" v-model="item.identityNo"
:disabled="true" />
<!-- <text style="color: #0066F6;position: absolute;right: 0rpx;">编辑</text> -->
</view>
<view style="margin-top: 24rpx;color: rgba(0, 0, 0, 0.8);">
联系手机
<text style="color: #000;margin-left: 95rpx;">{{createInfo.mobile}}</text>
</view>
</view>
<view style="padding: 24rpx;background-color: #fff;margin-top: 20rpx;" v-else>
<view style="display: flex;margin-top: 24rpx;align-items: flex-end;">
<text style="color: #000;font-size: 32rpx;font-weight: bold;">联系人信息</text>
</view>
<view style="margin-top: 24rpx;border-top: 1px dashed rgba(0, 0, 0, 0.1);"></view>
<view style="margin-top: 24rpx;color: rgba(0, 0, 0, 0.8);display: flex;align-items: center;">
联系人
<input :adjust-position="false" type="text" v-model="createInfo.contactName"
style="color: #000;margin-left: 95rpx;" placeholder="请输入联系人姓名" />
</view>
<view style="margin-top: 24rpx;color: rgba(0, 0, 0, 0.8);">
手机号
<text style="color: #000;margin-left: 95rpx;">{{createInfo.mobile}}</text>
</view>
</view>
<u-calendar v-model="show" :mode="mode" activeBgColor='#F4612C' :calendarList='calendarList'
@change="change"></u-calendar>
<u-popup v-model="show1" border-radius='20' mode='bottom'>
<view style="padding:48rpx;display: flex;flex-direction: column;">
<text style="color: #000;font-size: 36rpx;font-weight: bold;">费用明细</text>
<text style="color: #000;font-size: 32rpx;font-weight: bold;margin-top: 18rpx;">景区/场馆</text>
<view
style="height: 88rpx;display: flex;align-items: center;position: relative;border-bottom: 1px dashed #E1E1E1;">
<text style="color: rgba(0, 0, 0, 0.5);">门票</text>
<text style="color: #000;position: absolute;right: 0rpx;">¥{{salesPrice}}
&nbsp;&nbsp;x{{value}}</text>
</view>
<view style="position: relative;margin-top: 24rpx;">
<view style="color: rgba(0, 0, 0, 0.5);font-size: 20rpx;position: absolute;right: 0rpx;">总价<text
style="color: #FF0000;">¥</text><text
style="color: #FF0000;font-size: 32rpx;">{{salesPriceSum}}</text>
</view>
</view>
<view style="width: 654rpx;height: 120rpx;"></view>
<view
style="width: 654rpx;position: fixed;height: 88rpx;line-height: 88rpx;background: #fff;bottom: 0rpx;left: 0rpx;right: 0rpx;margin: 0 auto;border-bottom: constant(safe-area-inset-bottom) solid #fff;border-bottom: env(safe-area-inset-bottom) solid #fff;">
<u-button shape="circle" size="default"
style="background: linear-gradient(180deg, #FF9E49 0%, #ED3B1A 100%);color: #fff;"
@click="show1=false">确定</u-button>
</view>
</view>
</u-popup>
</view>
</template>
<script>
// xuchao: 确认订单页 —— 支付统一走 common/pay.js 支付层
import pay from '@/common/pay.js'
var that = ''
export default {
data() {
return {
show1: false,
show: false,
mode: 'date',
sf: `**** **** **** *1918`,
mz: `李富贵`,
value: 1,
hday: '',
whday: '',
skuObj: '',
calendarList: [],
windowHeight: '',
mobile: '',
personList: [],
objInfo: '',
createInfo: {
"ticketsId": 0,
"skuId": 0,
"buyNum": 0,
"calendarId": 0,
"timeSlot": 0,
"touristIdList": [],
"contactMobile": "",
"contactName": "",
"price": "",
"orderSource": "Channel_XGH",
"mobile": ""
}
}
},
computed: {
salesPrice() {
let price = 0
if (this.skuObj) {
if (this.skuObj.sku.stockDateType) {
this.calendarList.forEach(item => {
if (item.isSelect) {
price = (item.salePrice / 100).toFixed(2)
}
})
} else {
this.timeSlotList.forEach(item => {
if (item.isSelect) {
price = (item.salePrice / 100).toFixed(2)
}
})
}
}
return price
},
salesPriceSum() {
let priceSum = 0
if (this.skuObj) {
if (this.skuObj.sku.stockDateType) {
this.calendarList.forEach(item => {
if (item.isSelect) {
priceSum = (item.salePrice * this.value / 100).toFixed(2)
}
})
} else {
this.timeSlotList.forEach(item => {
if (item.isSelect) {
priceSum = (item.salePrice * this.value / 100).toFixed(2)
}
})
}
}
return priceSum
},
cList() {
let arr = []
console.log(this.calendarList,12312);
if (this.calendarList.length != 0) {
this.calendarList.forEach((item, index) => {
if (item.isSelect && index < 3) {
if(this.calendarList.length==1){
arr.push(this.calendarList[0])
}else if(this.calendarList.length==2){
arr.push(this.calendarList[0])
arr.push(this.calendarList[1])
}else if(this.calendarList.length>=3){
arr.push(this.calendarList[0])
arr.push(this.calendarList[1])
arr.push(this.calendarList[2])
}
} else if (item.isSelect && index >= 3) {
arr.push(this.calendarList[0])
arr.push(this.calendarList[1])
arr.push(item)
}
})
}
console.log(arr);
return arr
},
timeSlotList() {
let arr = []
if (this.calendarList.length != 0) {
this.calendarList.forEach((item, index) => {
if (item.isSelect) {
arr = item.timeSlotList
}
})
if (arr) {
arr.forEach((item1, index1) => {
if (index1 == 0) {
this.$set(item1, 'isSelect', true)
} else {
this.$set(item1, 'isSelect', false)
}
})
}
}
return arr
}
},
onLoad() {
that = this
uni.getSystemInfo({
success: function(res) {
that.windowHeight = res.windowHeight
}
});
this.createInfo.mobile = uni.getStorageSync('phone')
this.createInfo.contactMobile = uni.getStorageSync('phone')
this.skuObj = uni.getStorageSync('scenicDetail')
this.createInfo.ticketsId = this.skuObj.tickets.id
this.createInfo.skuId = this.skuObj.sku.id
if (this.skuObj.sku.stockDateType) {
for (var i = 0; i < this.skuObj.calendarList.length; i++) {
this.skuObj.calendarList[i].enableDate = this.formatDateTime(this.skuObj.calendarList[i].enableDate)
if (this.skuObj.sku.bookType == 1) {
if (i == 0) {
this.$set(this.skuObj.calendarList[i], 'isSelect', true)
} else {
this.$set(this.skuObj.calendarList[i], 'isSelect', false)
}
} else {
if (i == 1) {
this.$set(this.skuObj.calendarList[i], 'isSelect', true)
this.skuObj.calendarList[0].isEnabled = false
} else {
this.$set(this.skuObj.calendarList[i], 'isSelect', false)
}
}
this.calendarList.push(this.skuObj.calendarList[i])
}
} else {
for (var i = 0; i < this.skuObj.timeSlotData.length; i++) {
this.$set(this.skuObj.timeSlotData[i], 'isEnabled', true)
this.$set(this.skuObj.timeSlotData[i], 'enableDate', this.formatDateTime(this.skuObj.timeSlotData[i]
.date))
if (this.skuObj.sku.bookType == 1) {
if (i == 0) {
this.$set(this.skuObj.timeSlotData[i], 'isSelect', true)
} else {
this.$set(this.skuObj.timeSlotData[i], 'isSelect', false)
}
} else {
if (i == 1) {
this.$set(this.skuObj.timeSlotData[i], 'isSelect', true)
this.skuObj.timeSlotData[0].isEnabled = false
} else {
this.$set(this.skuObj.timeSlotData[i], 'isSelect', false)
}
}
this.calendarList.push(this.skuObj.timeSlotData[i])
}
}
console.log(this.calendarList);
},
onShow() {
var pages = getCurrentPages();
var prevPage = pages[pages.length - 1];
if (prevPage.objitem != undefined) {
that.isshow = true
that.personList = JSON.parse(prevPage.objitem);
}
},
methods: {
formatDateTime(isoString) {
// const date = new Date(isoString);
// const year = date.getFullYear();
// const month = String(date.getMonth() + 1).padStart(2, '0');
// const day = String(date.getDate()).padStart(2, '0');
// const hours = String(date.getHours()).padStart(2, '0');
// const minutes = String(date.getMinutes()).padStart(2, '0');
// const seconds = String(date.getSeconds()).padStart(2, '0');
return isoString.replace(/-/g, "/");;
},
getMaxStock() {
let stock = 0
if (this.skuObj.sku.stockNumType) {
stock = 99999
} else {
if (this.skuObj.sku.stockDateType) {
this.calendarList.forEach(item => {
if (item.isSelect) {
stock = item.stock
}
})
} else {
this.timeSlotList.forEach(item => {
if (item.isSelect) {
stock = item.stock
}
})
}
}
return stock
},
deletePerson(e) {
this.personList.splice(e, 1)
},
selectDate(e) {
if (e.isEnabled) {
this.cList.forEach(item => {
if (item.enableDate == e.enableDate) {
item.isSelect = true
} else {
item.isSelect = false
}
})
}
this.value = 1
},
selectSlot(e) {
this.timeSlotList.forEach((item, index) => {
if (e == index) {
this.timeSlotList[index].isSelect = true
} else {
this.timeSlotList[index].isSelect = false
}
})
this.value = 1
},
formatToMonthDay(dateStr) {
const date = new Date(dateStr)
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${month}月${day}日`;
},
valChange(e) {
console.log('当前值为: ' + e.value)
},
change(e) {
this.calendarList.forEach((item, index) => {
if (new Date(item.enableDate).getTime() == new Date(e.result + ' 00:00:00').getTime()) {
item.isSelect = true
} else {
item.isSelect = false
}
})
this.value = 1
},
formatDateToMMDD(dateStr) {
const [year, month, day] = dateStr.split('-');
return `${month.padStart(2, '0')}月${day.padStart(2, '0')}日`;
},
buy() {
// uni.showModal({
// title: '提示',
// content: '预订人同一证件号1天内,最多可购买1份,当前超限制的证件号尾号为0514',
// showCancel: false
// })
if (this.skuObj.sku.enterDemand == '2') {
if (this.value > this.personList.length) {
uni.showToast({
icon: 'none',
title: '还需填写' + (this.value - this.personList.length) + '位游客信息',
duration: 2000
});
return
}
} else {
if (this.createInfo.contactName == '') {
uni.showToast({
icon: 'none',
title: '请填写联系人姓名',
duration: 2000
});
return
}
}
this.createInfo.buyNum = this.value
if (this.skuObj.sku.stockDateType) {
this.createInfo.calendarId = this.cList.filter(item => {
return item.isSelect
})[0].id
} else {
this.createInfo.timeSlot = this.timeSlotList.filter(item => {
return item.isSelect
})[0].id
}
this.createInfo.price = this.salesPriceSum
this.createInfo.touristIdList = this.personList.map(item => {
return item.id
})
console.log(this.createInfo);
this.$u.api.createTicketsOrder(this.createInfo).then(res => {
if (res.errcode == 0) {
// xuchao: 支付层 —— 开发模式模拟成功,正式模式唤起微信支付
pay.pay(this, {
orderId: res.data,
onSuccess: () => {
uni.redirectTo({
url: 'paysuccess?id=' + res.data
})
},
onFail: () => {
uni.showToast({
icon: 'none',
title: '支付未完成,可在订单中继续支付',
duration: 2000
})
setTimeout(() => {
uni.redirectTo({
url: 'orderlist'
})
}, 1500)
}
})
}
})
},
addPerson() {
uni.navigateTo({
url: 'select?value=' + this.value
})
},
getFormattedDate(offset = 0) {
const date = new Date();
date.setDate(date.getDate() + offset);
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${month}月${day}日`;
},
getDayName(date) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const inputDate = new Date(date);
inputDate.setHours(0, 0, 0, 0);
const diffDays = Math.round((inputDate - today) / (1000 * 60 * 60 * 24));
// 处理今天/明天/后天
if (diffDays === 0) return "今天";
if (diffDays === 1) return "明天";
if (diffDays === 2) return "后天";
// 其他日期返回星期几
const weekDays = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
return weekDays[inputDate.getDay()];
}
}
}
</script>
<style>
page {
background: #F8F9FC;
}
</style>
<style scoped lang="less">
.bt {
width: 288rpx;
height: 100rpx;
background: linear-gradient(180deg, #FF9E49 0%, #ED3B1A 100%);
border-radius: 100rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
color: #fff;
position: absolute;
right: 24rpx;
bottom: 14rpx;
font-weight: bold;
}
.db_rq {
width: 750rpx;
background-color: #fff;
height: 62px;
position: fixed;
// xuchao: 改为底部锚定(原来用 JS 算 top,宽屏缩放后会错位),并给全面屏留出底部安全区
top: auto !important;
bottom: 0;
// xuchao: 视口水平居中 —— 宽屏下与居中的页面列对齐
left: 0rpx;
right: 0rpx;
margin-left: auto;
margin-right: auto;
border-bottom: constant(safe-area-inset-bottom) solid #fff;
border-bottom: env(safe-area-inset-bottom) solid #fff;
padding: 14rpx 24rpx;
}
.xz_name {
height: 56rpx;
padding: 0rpx 24rpx;
line-height: 56rpx;
color: #fff;
border-radius: 4rpx;
background: linear-gradient(180deg, #FF9E49 0%, #ED3B1A 100%);
}
.nx_name {
height: 56rpx;
padding: 0rpx 24rpx;
line-height: 56rpx;
color: rgba(0, 0, 0, 0.5);
border-radius: 4rpx;
background: #F8F9FC;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.nx_item {
width: 226rpx;
height: 110rpx;
border-radius: 8rpx;
border: 1px solid #E1E1E1;
color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
position: relative;
.xz_tag {
height: 30rpx;
padding: 0rpx 10rpx;
display: flex;
align-items: center;
color: rgba(0, 0, 0, 0.5);
font-size: 16rpx;
background: #E1E1E1;
border-radius: 0px 8rpx 0px 8rpx;
position: absolute;
top: 0rpx;
right: 0rpx;
}
}
.xz_item {
width: 226rpx;
height: 110rpx;
border-radius: 8rpx;
border: 1px solid #F66C31;
color: #F66C31;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.xz_tag {
height: 30rpx;
padding: 0rpx 10rpx;
display: flex;
align-items: center;
color: #fff;
font-size: 16rpx;
background: linear-gradient(180deg, #FF9E49 0%, #ED3B1A 100%);
border-radius: 0px 8rpx 0px 8rpx;
position: absolute;
top: 0rpx;
right: 0rpx;
}
}
.gd_rq {
width: 120rpx;
height: 170rpx;
border: 1px solid #E1E1E1;
border-radius: 8rpx;
color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.ht_rq {
width: 186rpx;
height: 170rpx;
border: 1px solid #E1E1E1;
border-radius: 8rpx;
color: rgba(0, 0, 0, 0.5);
position: relative;
line-height: 24px;
padding-top: 48rpx;
padding-left: 24rpx;
background: #F8F9FC;
.jt_tag {
font-size: 16rpx;
height: 30rpx;
display: flex;
align-items: center;
padding: 0rpx 10rpx;
background: #E1E1E1;
border-radius: 0px 8rpx 0px 8rpx;
position: absolute;
top: 0rpx;
right: 0rpx;
}
}
.jt_rq {
width: 186rpx;
height: 170rpx;
border: 1px solid #E1E1E1;
border-radius: 8rpx;
color: rgba(0, 0, 0, 0.5);
position: relative;
line-height: 24px;
padding-top: 48rpx;
padding-left: 24rpx;
.jt_tag {
font-size: 16rpx;
height: 30rpx;
display: flex;
align-items: center;
padding: 0rpx 10rpx;
background: #E1E1E1;
border-radius: 0px 8rpx 0px 8rpx;
position: absolute;
top: 0rpx;
right: 0rpx;
}
}
.mt_rq {
width: 186rpx;
height: 170rpx;
border: 1px solid #F66C31;
border-radius: 8rpx;
color: #F66C31;
position: relative;
line-height: 24px;
padding-top: 48rpx;
padding-left: 24rpx;
.jt_tag {
font-size: 16rpx;
height: 30rpx;
display: flex;
align-items: center;
padding: 0rpx 10rpx;
background: linear-gradient(180deg, #FF9E49 0%, #ED3B1A 100%);
border-radius: 0px 8rpx 0px 8rpx;
position: absolute;
color: #fff;
top: 0rpx;
right: 0rpx;
}
}
</style>