web-retail-h5/pages/ticket/buy.vue

852 lines
20 KiB
Vue
Raw Normal View History

2025-05-28 09:16:29 +08:00
<!--
* @Descripttion: 自助购票页面
* @version: 1.0.0
* @Author: Assistant
* @Date: 2025-01-22
-->
<template>
<view class="buy-ticket-container">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<view class="back-btn" @click="goBack">
<u-icon name="arrow-left" size="20" color="#333"></u-icon>
</view>
<view class="navbar-title">自助购票</view>
<view class="placeholder"></view>
</view>
</view>
<!-- 内容区域 -->
<view class="content-area">
<!-- 提示信息 -->
<view class="tip-banner">
<view class="tip-icon">
<u-icon name="info-circle" size="16" color="#f56c6c"></u-icon>
</view>
<text class="tip-text">自助购票请填写以下信息</text>
</view>
<!-- 购票人信息列表 -->
<view class="buyer-list">
<view
class="buyer-item"
v-for="(buyer, index) in buyerList"
:key="index"
>
<view class="buyer-header">
<text class="buyer-title">购票人信息{{ index + 1 }}</text>
<view
class="delete-btn"
v-if="buyerList.length > 1"
@click="removeBuyer(index)"
>
<u-icon name="trash" size="16" color="#999"></u-icon>
</view>
</view>
<view class="form-section">
<!-- 姓名 -->
<view class="form-item">
<view class="label required">
<text>姓名</text>
</view>
<view class="input-wrapper">
<input
class="form-input"
v-model="buyer.name"
placeholder="请输入"
maxlength="20"
/>
</view>
</view>
<!-- 联系方式 -->
<view class="form-item">
<view class="label required">
<text>联系方式</text>
</view>
<view class="input-wrapper">
<input
class="form-input"
v-model="buyer.phone"
placeholder="请输入"
type="number"
maxlength="11"
/>
</view>
</view>
<!-- 证件号码 -->
<view class="form-item">
<view class="label required">
<text>证件号码</text>
</view>
<view class="input-wrapper">
<input
class="form-input"
v-model="buyer.idCard"
placeholder="请输入"
maxlength="18"
/>
</view>
</view>
<!-- 性别 -->
<view class="form-item">
<view class="label required">
<text>性别</text>
</view>
<view class="input-wrapper">
<picker
:value="buyer.sexIndex"
:range="sexOptions"
@change="onSexChange($event, index)"
>
<view class="picker-input">
<text
class="picker-text"
:class="{ placeholder: buyer.sexIndex === -1 }"
>
{{
buyer.sexIndex !== -1
? sexOptions[buyer.sexIndex]
: '请选择'
}}
</text>
<u-icon name="arrow-down" size="12" color="#999"></u-icon>
</view>
</picker>
</view>
</view>
<!-- 服装尺寸 -->
<view class="form-item">
<view class="label required">
<text>服装尺寸</text>
</view>
<view class="input-wrapper">
<input
class="form-input"
v-model="buyer.clothSize"
placeholder="请输入"
maxlength="10"
/>
</view>
</view>
<!-- 同住人 -->
<view class="form-item">
<view class="label">
<text>同住人</text>
</view>
<view class="input-wrapper">
<input
class="form-input"
v-model="buyer.cohabitant"
placeholder="请输入"
maxlength="20"
/>
</view>
</view>
<!-- 紧急联系方式 -->
<view class="form-item">
<view class="label required">
<text>紧急联系方式</text>
</view>
<view class="input-wrapper">
<input
class="form-input"
v-model="buyer.emergencyPhone"
placeholder="请输入"
type="number"
maxlength="11"
/>
</view>
</view>
</view>
</view>
</view>
<!-- 添加购票人按钮 -->
<view class="add-buyer-btn" @click="addBuyer">
<u-icon name="plus" size="16" color="#666"></u-icon>
<text class="add-text">继续添加购票人</text>
</view>
<!-- 提交按钮 -->
<view class="submit-wrapper">
<button
class="submit-btn"
:class="{ disabled: submitting }"
:disabled="submitting"
@click="handleSubmit"
>
{{ submitting ? '提交中...' : '提交' }}
</button>
</view>
</view>
<!-- 支付密码弹窗 -->
<u-popup
:show="showPayPwdModal"
mode="center"
border-radius="16"
width="320px"
height="auto"
>
<view class="pay-pwd-modal">
<view class="modal-header">
<view class="header-icon">
<u-icon name="lock" size="24" color="#005bac"></u-icon>
</view>
<text class="modal-title">输入支付密码</text>
<text class="modal-subtitle">请输入您的支付密码以完成购票</text>
</view>
<view class="modal-body">
<view class="pwd-input-wrapper">
<input
class="pwd-input"
v-model="payPassword"
placeholder="请输入支付密码"
type="password"
maxlength="20"
@input="onPayPwdInput"
@focus="onPwdFocus"
@blur="onPwdBlur"
/>
<view
class="input-border"
:class="{ focused: pwdInputFocused }"
></view>
</view>
</view>
<view class="modal-footer">
<view class="btn-group">
<button class="modal-btn cancel-btn" @click="closePayPwdModal">
取消
</button>
<button
class="modal-btn confirm-btn"
:class="{ disabled: !isPayPwdValid }"
:disabled="!isPayPwdValid"
@click="confirmPayPwd"
>
确认支付
</button>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import { buyTicket } from '@/config/ticket.js'
export default {
data() {
return {
activityId: '',
activityName: '',
price: '',
buyerList: [
{
2025-05-28 11:03:00 +08:00
name: '',
phone: '',
idCard: '',
2025-05-28 09:16:29 +08:00
sexIndex: 1,
sex: 1,
2025-05-28 11:03:00 +08:00
clothSize: '',
cohabitant: '',
emergencyPhone: '',
2025-05-28 09:16:29 +08:00
},
],
sexOptions: ['男', '女'],
submitting: false,
showPayPwdModal: false,
payPassword: '',
pwdInputFocused: false,
}
},
computed: {
// 支付密码是否有效(非空且为纯数字)
isPayPwdValid() {
return this.payPassword.length > 0 && /^\d+$/.test(this.payPassword)
},
},
onLoad(options) {
if (options.activityId) {
this.activityId = options.activityId
}
if (options.activityName) {
this.activityName = decodeURIComponent(options.activityName)
}
if (options.price) {
this.price = options.price
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
},
// 性别选择变化
onSexChange(e, index) {
const sexIndex = e.detail.value
this.buyerList[index].sexIndex = sexIndex
this.buyerList[index].sex = sexIndex == 0 ? 1 : 2 // 1-男2-女
},
// 添加购票人
addBuyer() {
this.buyerList.push({
name: '',
phone: '',
idCard: '',
sexIndex: -1,
sex: '',
clothSize: '',
cohabitant: '',
emergencyPhone: '',
})
},
// 删除购票人
removeBuyer(index) {
uni.showModal({
title: '提示',
content: '确定删除该购票人信息吗?',
success: res => {
if (res.confirm) {
this.buyerList.splice(index, 1)
}
},
})
},
// 验证表单
validateForm() {
for (let i = 0; i < this.buyerList.length; i++) {
const buyer = this.buyerList[i]
if (!buyer.name.trim()) {
uni.$u.toast(`购票人${i + 1}的姓名不能为空`)
return false
}
if (!buyer.phone.trim()) {
uni.$u.toast(`购票人${i + 1}的联系方式不能为空`)
return false
}
if (!/^1[3-9]\d{9}$/.test(buyer.phone)) {
uni.$u.toast(`购票人${i + 1}的联系方式格式不正确`)
return false
}
if (!buyer.idCard.trim()) {
uni.$u.toast(`购票人${i + 1}的证件号码不能为空`)
return false
}
if (buyer.sexIndex === -1 || !buyer.sex) {
uni.$u.toast(`请选择购票人${i + 1}的性别`)
return false
}
if (!buyer.clothSize.trim()) {
uni.$u.toast(`购票人${i + 1}的服装尺寸不能为空`)
return false
}
if (!buyer.emergencyPhone.trim()) {
uni.$u.toast(`购票人${i + 1}的紧急联系方式不能为空`)
return false
}
if (!/^1[3-9]\d{9}$/.test(buyer.emergencyPhone)) {
uni.$u.toast(`购票人${i + 1}的紧急联系方式格式不正确`)
return false
}
}
return true
},
// 处理提交按钮点击
handleSubmit() {
if (!this.validateForm()) {
return
}
console.log('支付弹窗?')
// 显示支付密码弹窗
this.showPayPwdModal = true
this.payPassword = ''
},
// 关闭支付密码弹窗
closePayPwdModal() {
this.showPayPwdModal = false
this.payPassword = ''
},
// 支付密码输入处理
onPayPwdInput(e) {
// 只允许输入数字
this.payPassword = e.detail.value.replace(/[^\d]/g, '')
},
// 密码输入框获得焦点
onPwdFocus() {
this.pwdInputFocused = true
},
// 密码输入框失去焦点
onPwdBlur() {
this.pwdInputFocused = false
},
// 确认支付密码
confirmPayPwd() {
if (!this.isPayPwdValid) {
uni.$u.toast('请输入正确的支付密码')
return
}
this.showPayPwdModal = false
this.submitOrder()
},
// 提交订单
async submitOrder() {
if (!this.validateForm()) {
return
}
this.submitting = true
try {
// 构造提交数据
const orderData = {
payPwd: this.payPassword,
pkTicket: this.activityId,
ticketParamList: this.buyerList.map(buyer => ({
buyName: buyer.name,
phone: buyer.phone,
idCard: buyer.idCard,
sex: buyer.sex,
clothSize: buyer.clothSize,
cohabitant: buyer.cohabitant || '',
emergencyPhone: buyer.emergencyPhone,
})),
}
const res = await buyTicket(orderData)
if (res.code === 200) {
uni.showToast({
title: '购票成功',
icon: 'success',
duration: 2000,
})
setTimeout(() => {
// 返回门票列表页面并切换到我的门票tab
uni.navigateBack({
delta: 1,
})
}, 2000)
} else {
uni.$u.toast(res.msg || '购票失败,请重试')
}
} catch (error) {
console.error('购票失败:', error)
uni.$u.toast('购票失败,请重试')
} finally {
this.submitting = false
}
},
},
}
</script>
<style lang="scss" scoped>
.buy-ticket-container {
min-height: 100vh;
background: #f8f8f8;
}
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background: #fff;
padding-top: var(--status-bar-height);
border-bottom: 1px solid #eee;
.navbar-content {
display: flex;
align-items: center;
height: 44px;
padding: 0 16px;
.back-btn {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.navbar-title {
flex: 1;
text-align: center;
font-size: 18px;
font-weight: 600;
color: #333;
}
.placeholder {
width: 40px;
}
}
}
.content-area {
margin-top: calc(var(--status-bar-height) + 44px);
padding: 16px;
padding-bottom: 100px;
}
.tip-banner {
background: #fff5f5;
border: 1px solid #ffebee;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 16px;
display: flex;
align-items: center;
.tip-icon {
margin-right: 8px;
}
.tip-text {
font-size: 14px;
color: #f56c6c;
}
}
.buyer-list {
.buyer-item {
background: #fff;
border-radius: 12px;
margin-bottom: 16px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.buyer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
border-bottom: 1px solid #f5f5f5;
.buyer-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.delete-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f8f8;
border-radius: 16px;
}
}
.form-section {
padding: 16px;
.form-item {
display: flex;
align-items: center;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.label {
width: 100px;
font-size: 14px;
color: #333;
position: relative;
&.required::before {
content: '*';
color: #f56c6c;
position: absolute;
left: -10px;
}
}
.input-wrapper {
flex: 1;
.form-input {
width: 100%;
height: 44px;
background: #f8f8f8;
border-radius: 8px;
border: none;
padding: 0 16px;
font-size: 14px;
color: #333;
box-sizing: border-box;
&::placeholder {
color: #c0c0c0;
}
}
.picker-input {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 44px;
background: #f8f8f8;
box-sizing: border-box;
border-radius: 8px;
padding: 0 16px;
.picker-text {
font-size: 14px;
color: #333;
&.placeholder {
color: #c0c0c0;
}
}
}
}
}
}
}
}
.add-buyer-btn {
display: flex;
align-items: center;
justify-content: center;
background: #fff;
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
border: 2px dashed #e0e0e0;
.add-text {
font-size: 14px;
color: #666;
margin-left: 8px;
}
}
.submit-wrapper {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 16px;
border-top: 1px solid #eee;
.submit-btn {
width: 100%;
height: 48px;
background: #005bac;
color: #fff;
border: none;
border-radius: 24px;
font-size: 16px;
font-weight: 600;
&.disabled {
background: #ccc;
}
}
}
/* 支付密码弹窗样式 */
.pay-pwd-modal {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
.modal-header {
padding: 40rpx 40rpx 20rpx;
text-align: center;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
.header-icon {
width: 48px;
height: 48px;
background: rgba(0, 91, 172, 0.1);
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
}
.modal-title {
font-size: 18px;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8px;
}
.modal-subtitle {
font-size: 14px;
color: #666;
display: block;
line-height: 1.4;
}
}
.modal-body {
padding: 24rpx;
.pwd-input-wrapper {
position: relative;
margin-bottom: 16rpx;
.pwd-input {
width: 100%;
height: 80rpx;
background: #f8f9fa;
border-radius: 24rpx;
border: none;
padding: 0 20rpx;
font-size: 24rpx;
color: #333;
text-align: center;
letter-spacing: 4rpx;
box-sizing: border-box;
transition: all 0.3s ease;
&::placeholder {
color: #c0c0c0;
letter-spacing: normal;
}
&:focus {
background: #fff;
outline: none;
}
}
.input-border {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 2px solid transparent;
border-radius: 12px;
pointer-events: none;
transition: all 0.3s ease;
&.focused {
border-color: #005bac;
box-shadow: 0 0 0 4px rgba(0, 91, 172, 0.1);
}
}
}
.pwd-strength {
.strength-dots {
display: flex;
justify-content: center;
gap: 8px;
.dot {
width: 8px;
height: 8px;
border-radius: 4px;
background: #e9ecef;
transition: all 0.3s ease;
&.active {
background: #005bac;
transform: scale(1.2);
}
}
}
}
}
.modal-footer {
padding: 0 24px 24px;
.btn-group {
display: flex;
gap: 12px;
.modal-btn {
flex: 1;
height: 48rpx;
border: none;
border-radius: 24rpx;
font-size: 24rpx;
font-weight: 500;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
&.cancel-btn {
background: #f8f9fa;
color: #666;
&:active {
background: #e9ecef;
}
}
&.confirm-btn {
background: #005bac;
color: #fff;
font-weight: 600;
&:active {
background: #004494;
}
&.disabled {
background: #e9ecef;
color: #adb5bd;
cursor: not-allowed;
&:active {
background: #e9ecef;
}
}
}
}
}
}
}
</style>