feat: 质惠游 H5 一期 —— 门票模块全链路(微信公众号版)

从 xghmall(云闪付版湘工游)迁移民票模块,改造为微信公众号 H5:

- 技术栈沿用 uni-app + Vue2 + uView(含定制版日历组件)
- 登录/支付独立成层(common/auth.js、common/pay.js):
  一期假登录(测试手机号)+ 模拟支付,后端微信接口就绪后只换这两处
- 登录页重写:短信验证码 + 公众号网页授权(code 换用户信息)预留
- 门票列表页为入口页(进页必须登录),加订单悬浮按钮
- 支付成功页接通:查看订单/继续逛逛 + 订单状态轮询
- 详情页定位导航改用 uni.openLocation + 高德网页兜底
- 出行人页:身份证 18 位格式校验,删除死的删除按钮,修正证件类型回显笔误
- 清理云闪付 SDK、支付宝授权、死页面引用等残留

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
xuchao
2026-07-21 00:43:59 +08:00
co-authored by Claude Opus 4.8
commit 4190565eb7
192 changed files with 35664 additions and 0 deletions
+726
View File
@@ -0,0 +1,726 @@
<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;">
<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;
// bottom: 0rpx;
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>