完成12306火车票页面

This commit is contained in:
tangxinyue 2026-01-31 18:27:45 +08:00
parent fde1ce793d
commit 3fc6af7aad
16 changed files with 3072 additions and 258 deletions

View File

@ -10,7 +10,9 @@
<view class="nav-bar-left">
<slot name="left">
<view class="left-icon" @click.stop="onBack">
<image class="nav-icon-back" src="/static/image/nav-bar/back-black.png" mode="">
<image class="nav-icon-back"
:src="`/static/image/nav-bar/back-${textColor == '#fff' || textColor == '#fffffff' ? 'white' : 'black'}.png`"
mode="">
</image>
</view>
</slot>
@ -25,7 +27,9 @@
<view class="nav-bar-right" @click.stop="onRightClick">
<slot name="right">
<view v-if="isRightIcon" class="right-icon">
<image class="nav-icon-more" src="/static/image/nav-bar/more-black.png" mode="">
<image class="nav-icon-more"
:src="`/static/image/nav-bar/more-${textColor == '#fff' || textColor == '#fffffff' ? 'white' : 'black'}.png`"
mode="">
</image>
</view>
<view v-if="isRightButton" class="right-button">
@ -74,276 +78,276 @@
</template>
<script setup>
import popup from '../popup/popup.vue'
import {
onMounted,
reactive,
ref,
toRefs
} from 'vue'
import popup from '../popup/popup.vue'
import {
onMounted,
reactive,
ref,
toRefs
} from 'vue'
const topPopup = ref()
const topPopup = ref()
//
const props = defineProps({
bgColor: {
type: String,
default: '#fff'
},
textColor: {
type: String,
default: '#000'
},
title: {
type: String,
default: ''
},
buttonGroup: {
type: Array,
default: () => []
},
isRightIcon: {
type: Boolean,
default: false
},
isRightButton: {
type: Boolean,
default: false
},
rightButtonText: {
type: String,
default: '确定'
},
zIndex: {
type: Number,
default: 999
},
noBack: {
type: Boolean,
default: false
},
tipLayerText: {
type: String,
default: ''
},
isTipLayer: {
type: Boolean,
default: false
},
tipLayerType: {
type: String,
default: ''
}
})
//
const emit = defineEmits(['back', 'right-click', 'button-click', 'refresh'])
const data = reactive({
statusBarHeight: 0,
showTipLayer: true,
})
let {
showTipLayer
} = toRefs(data)
onMounted(() => {
//
const systemInfo = uni.getSystemInfoSync();
data.statusBarHeight = systemInfo.statusBarHeight || 0;
if (props.isTipLayer) {
if (uni.getStorageSync(props.tipLayerType) == props.tipLayerType) {
showTipLayer.value = false
}
}
})
const closeTipLayer = () => {
showTipLayer.value = false
uni.setStorageSync(props.tipLayerType, props.tipLayerType)
emit("refresh")
//
const props = defineProps({
bgColor: {
type: String,
default: '#fff'
},
textColor: {
type: String,
default: '#000'
},
title: {
type: String,
default: ''
},
buttonGroup: {
type: Array,
default: () => []
},
isRightIcon: {
type: Boolean,
default: false
},
isRightButton: {
type: Boolean,
default: false
},
rightButtonText: {
type: String,
default: '确定'
},
zIndex: {
type: Number,
default: 999
},
noBack: {
type: Boolean,
default: false
},
tipLayerText: {
type: String,
default: ''
},
isTipLayer: {
type: Boolean,
default: false
},
tipLayerType: {
type: String,
default: ''
}
})
const openPopup = () => {
if (props.buttonGroup.length > 0) {
topPopup.value.open()
//
const emit = defineEmits(['back', 'right-click', 'button-click', 'refresh'])
const data = reactive({
statusBarHeight: 0,
showTipLayer: true,
})
let {
showTipLayer
} = toRefs(data)
onMounted(() => {
//
const systemInfo = uni.getSystemInfoSync();
data.statusBarHeight = systemInfo.statusBarHeight || 0;
if (props.isTipLayer) {
if (uni.getStorageSync(props.tipLayerType) == props.tipLayerType) {
showTipLayer.value = false
}
}
//
const onBack = () => {
emit('back')
//
if (props.noBack) return
uni.navigateBack()
}
})
//
const onRightClick = () => {
emit('right-click')
const closeTipLayer = () => {
showTipLayer.value = false
uni.setStorageSync(props.tipLayerType, props.tipLayerType)
emit("refresh")
}
const openPopup = () => {
if (props.buttonGroup.length > 0) {
topPopup.value.open()
}
}
//
const onBack = () => {
emit('back')
//
if (props.noBack) return
uni.navigateBack()
}
//
const onRightClick = () => {
emit('right-click')
}
const buttonClick = (button) => {
topPopup.value.close()
emit('button-click', button)
}
const buttonClick = (button) => {
topPopup.value.close()
emit('button-click', button)
}
</script>
<style scoped>
@import "/common/main.css";
@import "/common/main.css";
.nav-bar-container {
display: flex;
flex-direction: column;
position: fixed !important;
top: 0;
left: 0;
right: 0;
z-index: 999;
}
.nav-bar-container {
display: flex;
flex-direction: column;
position: fixed !important;
top: 0;
left: 0;
right: 0;
z-index: 999;
}
.nav-bar {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 1;
}
.nav-bar {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
z-index: 1;
}
.status-placeholder {
width: 100%;
}
.status-placeholder {
width: 100%;
}
::v-deep .uni-navbar__content {
width: 100%;
}
::v-deep .uni-navbar__content {
width: 100%;
}
.nav-bar-left {
display: flex;
align-items: center;
justify-content: flex-start;
}
.nav-bar-left {
display: flex;
align-items: center;
justify-content: flex-start;
}
.nav-icon-back {
width: 24px;
height: 24px;
}
.nav-icon-back {
width: 24px;
height: 24px;
}
.nav-icon-more {
width: 26px;
height: 26px;
}
.nav-icon-more {
width: 26px;
height: 26px;
}
.nav-bar-title {
flex: 1;
margin: auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
font-weight: 500;
}
.nav-bar-title {
flex: 1;
margin: auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
font-weight: 500;
}
.nav-bar-right {
display: flex;
align-items: center;
justify-content: flex-end;
}
.nav-bar-right {
display: flex;
align-items: center;
justify-content: flex-end;
}
.right-button {
font-size: 12px;
border-radius: 16px;
color: #fff;
text-align: center;
line-height: 30px;
height: 30px;
min-width: 60px;
background: linear-gradient(90deg, #187AFF 0%, #3295FC 100%);
}
.right-button {
font-size: 12px;
border-radius: 16px;
color: #fff;
text-align: center;
line-height: 30px;
height: 30px;
min-width: 60px;
background: linear-gradient(90deg, #187AFF 0%, #3295FC 100%);
}
.button-box {
width: calc(50% - 8rpx);
text-align: center;
margin-top: 16rpx;
}
.button-box {
width: calc(50% - 8rpx);
text-align: center;
margin-top: 16rpx;
}
.button {
border: 1px solid #E4E4E4;
border-radius: 8px;
height: 42px;
line-height: 42px;
font-size: 28rpx;
}
.button {
border: 1px solid #E4E4E4;
border-radius: 8px;
height: 42px;
line-height: 42px;
font-size: 28rpx;
}
.tipLayer {
box-sizing: border-box;
min-width: 200px !important;
height: 48px;
background: #B8EDFE;
border-radius: 8px 8px 8px 8px;
position: fixed;
/* top: 115px; */
left: 50%;
transform: translateX(-50%);
z-index: 999;
.tipLayer {
box-sizing: border-box;
min-width: 200px !important;
height: 48px;
background: #B8EDFE;
border-radius: 8px 8px 8px 8px;
position: fixed;
/* top: 115px; */
left: 50%;
transform: translateX(-50%);
z-index: 999;
}
}
.tipLayer-content {
position: relative;
.tipLayer-content {
position: relative;
}
}
.title {
font-weight: 450;
.title {
font-weight: 450;
font-size: 14px;
color: #268FFF;
line-height: 48px;
text-align: center;
text {
font-size: 14px;
color: #268FFF;
line-height: 48px;
text-align: center;
text {
font-size: 14px;
font-weight: bold;
color: #006ADD;
}
::v-deep text {
font-size: 14px;
font-weight: bold;
color: #006ADD;
}
font-weight: bold;
color: #006ADD;
}
.triangleImg {
width: 111px;
height: 52px;
pointer-events: none;
position: absolute;
top: -23px;
left: calc(50% - 111px);
::v-deep text {
font-size: 14px;
font-weight: bold;
color: #006ADD;
}
}
.triangle {
position: absolute;
top: -57px;
left: calc(50% - 40px);
pointer-events: none;
}
.triangleImg {
width: 111px;
height: 52px;
pointer-events: none;
position: absolute;
top: -23px;
left: calc(50% - 111px);
}
.close {
position: absolute;
top: -5px;
right: -5px;
width: 18px;
height: 18px;
}
.triangle {
position: absolute;
top: -57px;
left: calc(50% - 40px);
pointer-events: none;
}
.close {
position: absolute;
top: -5px;
right: -5px;
width: 18px;
height: 18px;
}
</style>

View File

@ -27,7 +27,7 @@ export function createApp() {
const systemInfo = uni.getStorageSync('systemInfo') || {}
app.config.globalProperties.$system = systemInfo.platform == 'ios' ? 'iOS' : 'Android'
app.config.globalProperties.$systemInfo = systemInfo
uni.setStorageSync('version', '1.0.0.sp7')
uni.setStorageSync('version', '1.0.0.sp9')
app.config.globalProperties.$version = uni.getStorageSync('version')
app.use(globalMethods);

View File

@ -70,7 +70,8 @@
}
}
]
}, {
},
{
"root": "pages/finance-management",
"pages": [{
"path": "index",
@ -111,6 +112,29 @@
"navigationBarTitleText": "工资单",
"navigationStyle": "custom"
}
},
{
"path": "train-tickets/12306-tickets/12306-tickets",
"style": {
"navigationBarTitleText": "12306火车票",
"navigationStyle": "custom",
"navigationBarTextStyle": "white"
}
},
{
"path": "train-tickets/edit/edit",
"style": {
"navigationBarTitleText": "修改火车票信息",
"navigationStyle": "custom"
}
},
{
"path": "air-tickets/qunar-air-tickets/qunar-air-tickets",
"style": {
"navigationBarTitleText": "去哪儿",
"navigationStyle": "custom",
"navigationBarTextStyle": "white"
}
}
]
},
@ -155,7 +179,8 @@
"pages/bill",
"pages/common",
"pages/finance-management",
"pages/ant-credit-pay"
"pages/ant-credit-pay",
"pages/other"
]
}
},

View File

@ -95,6 +95,8 @@
</view>
</view>
<view class="overdue-box">
<image class="bg-image" src="/static/image/ant-credit-pay/overdue-payment/overdue-bg.png" mode="widthFix">
</image>
<view class="overdue-info">
<image class="icon" src="/static/image/ant-credit-pay/overdue-payment/warring-icon.png"></image>
<view class="err-text">抱歉您暂时无法使用该服务服务机构将不定期评估您的使用资格请保持良好信用行为并耐心等待通知</view>
@ -258,7 +260,7 @@ const buttonGroup = [{
openStyleDialog()
}
}, {
name: "逾期停用",
name: "逾期停用",
isSwitch: true,
key: 'isOverdueDeactivate',
click: () => {
@ -471,7 +473,7 @@ const goBack = () => {
display: flex;
flex-direction: column;
background-color: #F6F6F6;
height: 100vh;
// height: 100vh;
// overflow: hidden;
padding-bottom: 16rpx;
@ -646,26 +648,42 @@ const goBack = () => {
}
.overdue-box {
position: relative;
width: 100%;
height: auto;
display: flex;
align-items: center;
justify-content: center;
padding-top: 18rpx;
background-image: url('/static/image/ant-credit-pay/overdue-payment/overdue-bg.png');
background-size: contain;
background-repeat: no-repeat;
// background-image: url('/static/image/ant-credit-pay/overdue-payment/overdue-bg.png');
// background-size: contain;
// background-repeat: no-repeat;
margin-top: -1px;
// height: 700rpx;
.bg-image {
position: absolute;
width: 100%;
top: 0;
left: 0;
right: 0;
}
.overdue-info {
width: 654rpx;
// height: 628rpx;
position: relative;
width: 100%;
margin: 0 48rpx;
background: linear-gradient(180deg, #FFF5F4 0%, #FFFBF7 100%);
border-radius: 20rpx 20rpx 20rpx 20rpx;
padding: 100rpx 24rpx 60rpx;
display: flex;
align-items: center;
flex-direction: column;
text-align: center;
// display: flex;
// align-items: center;
// flex-direction: column;
// min-height: 628rpx;
// max-height: 100%;
.icon {
position: relative;
width: 72rpx;
height: 72rpx;
}
@ -710,7 +728,7 @@ const goBack = () => {
border-radius: 16rpx;
margin: 16rpx 24rpx 0;
display: flex;
align-items: center;
// align-items: center;
flex-wrap: wrap;
padding-bottom: 12rpx;

View File

@ -16,7 +16,7 @@
</view>
<view class="content-box" :style="{ height: windowHeight + 'px' }">
<scroll-view scroll-y="true"
<scroll-view scroll-y="true" class="scroll-view"
:style="{ height: (windowHeight - statusBarHeight - 44) + 'px', marginTop: (statusBarHeight + 44) + 'px' }"
@scroll="handleScroll">
<view>
@ -125,6 +125,8 @@
<view class="footer-box" :class="{ 'ios-padding-bottom': platform === 'ios' }">
<text class="vision-text">版本:{{ vision }}</text>
<text class="vision-text margin-l-6" v-if="qqgroup.enable">{{ qqgroup.text }}</text>
<text class="vision-text" @click="copyNumber(qqgroup.number)">{{ qqgroup.number }}</text>
</view>
</scroll-view>
</view>
@ -150,7 +152,9 @@ import {
import {
onLoad,
onShow
onShow,
onHide,
onUnload
} from '@dcloudio/uni-app';
// 内部埋点方法
@ -222,7 +226,7 @@ const otherList = [{
{
icon: "/static/image/index/qita/huochepiao.png",
name: "火车票",
path: ""
path: "/pages/other/train-tickets/12306-tickets/12306-tickets"
},
{
icon: "/static/image/index/qita/gongzidan.png",
@ -245,7 +249,8 @@ const data = reactive({
videoHelpList: [],
noticeInfo: {},
vision: "",
platform: '' // 添加平台信息
platform: '', // 添加平台信息,
qqgroup: {}
})
const {
@ -256,7 +261,8 @@ const {
videoHelpList,
noticeInfo,
vision,
platform
platform,
qqgroup
} = toRefs(data);
/**
@ -389,6 +395,7 @@ const setUserData = () => {
// 启动走马灯
startMarquee();
data.videoHelpList = configData.config['client.uniapp.alipay.video_help'] || []
data.qqgroup = configData.config['client.uniapp.qqgroup'] || { enable: false, number: "", text: "" }
} else {
data.noticeInfo = {
text: '加载中...',
@ -437,6 +444,30 @@ const clickNotice = () => {
util.goPage(`/pages/common/webview/webview?url=${encodeURIComponent(url)}&title=${noticeInfo.value.title}`)
}
/**
* 终极兼容版复制函数
*/
const copyNumber = (number) => {
if (!number) return;
const text = String(number);
uni.setClipboardData({
data: text,
success: function () {
uni.showToast({
title: '复制成功',
icon: 'none'
});
},
fail: function () {
uni.showToast({
title: '复制失败',
icon: 'none'
});
}
});
}
/**
* 退出模拟器
*/
@ -566,12 +597,38 @@ const runMarqueeAnimation = (containerWidth, textWidth, myId) => {
});
});
}
/**
* 停止走马灯
*/
const stopMarquee = () => {
// 清除定时器
if (marqueeTimer) {
clearTimeout(marqueeTimer);
marqueeTimer = null;
}
// 增加ID使其失效
currentMarqueeId.value++;
}
onHide(() => {
stopMarquee();
})
onUnload(() => {
stopMarquee();
})
</script>
<style>
.container {
background-color: #F0F4F9;
}
.margin-l-6 {
margin-left: 6px;
}
.index-bg-img {
position: fixed;
top: 0;
@ -600,6 +657,13 @@ const runMarqueeAnimation = (containerWidth, textWidth, myId) => {
justify-content: space-between;
}
::v-deep .uni-scroll-view-content {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.status-box {
width: 100%;
}
@ -846,6 +910,7 @@ const runMarqueeAnimation = (containerWidth, textWidth, myId) => {
.footer-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-top: 40rpx;

File diff suppressed because it is too large Load Diff

View File

@ -579,14 +579,14 @@ onReachBottom(() => {
}
.btn-save-image {
background-color: #07C160;
background: linear-gradient(90deg, #187AFF 0%, #3295FC 100%);
color: #fff;
border: none;
padding: 18rpx 60rpx;
border-radius: 40rpx;
font-size: 26rpx;
font-weight: bold;
box-shadow: 0 3rpx 10rpx rgba(7, 193, 96, 0.3);
box-shadow: 0 3rpx 10rpx rgba(7, 66, 193, 0.3);
transition: all 0.3s ease;
text-align: center;
min-width: 160rpx;
@ -594,11 +594,11 @@ onReachBottom(() => {
.btn-save-image:hover {
transform: translateY(-2rpx);
box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.4);
box-shadow: 0 4rpx 12rpx rgba(7, 72, 193, 0.4);
}
.btn-save-image:active {
transform: translateY(0);
box-shadow: 0 2rpx 6rpx rgba(7, 193, 96, 0.3);
box-shadow: 0 2rpx 6rpx rgba(7, 140, 193, 0.3);
}
</style>

View File

@ -0,0 +1,930 @@
<template>
<view class="container">
<!-- 顶部背景 -->
<view class="header-bg"></view>
<!-- 自定义导航栏 -->
<NavBar class="nav-bar" isBack bgColor="#3C99FB" textColor="#fff" isRightIcon tipLayerType="train-tickets-tip"
isTipLayer tipLayerText="修改车票信息" :buttonGroup="buttonGroup" @button-click="util.clickTitlePopupButton">
<template v-slot:center>
<text class="center-text">订单详情</text>
</template>
</NavBar>
<!-- 页面内容 -->
<scroll-view scroll-y class="content-scroll">
<!-- 订单信息 -->
<view class="order-info-box">
<view class="order-number-row">
<text class="order-label">订单号:{{ ticketsInfo.orderInfo.orderNo }}</text>
<image class="copy-btn" @click="copyOrderNo"
src="/static/image/other/train-tickets/12306-tickets/copy-button.png"></image>
</view>
<text class="order-time">下单时间:{{ ticketsInfo.orderInfo.orderTime }}</text>
</view>
<view class="main-card">
<!-- 车票卡片 -->
<view class="ticket-card">
<view class="ticket-main-info">
<view class="station-time-box">
<text class="time-text">{{ departureTimeDisplay }}</text>
<view class="station-text">{{ ticketsInfo.ticketInfo.departureStation }}
<uni-icons type="forward" size="10" color="#767676"></uni-icons>
</view>
</view>
<view class="train-info-box">
<view class="train-no">
<text style="line-height: 26rpx;">{{ ticketsInfo.ticketInfo.trainNo }}</text>
<uni-icons type="arrowright" size="10" color="#979797"></uni-icons>
</view>
<view class="stopover-box">
<image style="width: 100%;height: 100%;"
src="/static/image/other/train-tickets/12306-tickets/stopover-bg.png"></image>
</view>
<text class="duration-text">历时{{ ticketsInfo.ticketInfo.duration }}</text>
</view>
<view class="station-time-box" style="align-items: flex-start;">
<view class="time-row">
<text class="time-text">{{ arrivalTimeDisplay }}</text>
<text v-if="dayDiff > 0" class="day-badge">+{{ dayDiff }}</text>
</view>
<view class="station-text">{{ ticketsInfo.ticketInfo.arrivalStation }}
<uni-icons type="forward" size="10" color="#767676"></uni-icons>
</view>
</view>
</view>
<view class="date-row">
<text class="date-text">发车时间{{ ticketsInfo.ticketInfo.date }} {{ ticketsInfo.ticketInfo.weekDay
}}</text>
<text class="valid-text">车票当日当次有效</text>
</view>
<view class="gate-info-box">
<text>检票口{{ ticketsInfo.ticketInfo.gate }}</text>
<text class="gate-text">如有变更请以现场公告为准</text>
</view>
</view>
<!-- 操作栏 -->
<view class="action-bar">
<view class="action-item" @click="handleAction('变更到站')">
<text class="action-text">变更到站</text>
</view>
<view class="divider"></view>
<view class="action-item" @click="handleAction('改签')">
<text class="action-text">改签</text>
</view>
<view class="divider"></view>
<view class="action-item" @click="handleAction('退票')">
<text class="action-text">退票</text>
</view>
</view>
<!-- 乘客卡片 -->
<view class="passenger-card-box" v-for="(passenger, index) in ticketsInfo.passengerList" :key="index">
<view class="passenger-card">
<view class="passenger-header">
<view class="name-box">
<text class="passenger-name">{{ passenger.name }}</text>
<text class="tag-text">{{ passenger.type }}</text>
</view>
<view class="seat-info">
<text v-if="computeSeatNo(passenger.seatNo)" class="tag-text-grey">{{
computeSeatNo(passenger.seatNo) }}</text>
<text class="seat-text">{{ passenger.seatType }} {{ passenger.carriage }} {{
passenger.seatNo
}}</text>
<uni-icons style="margin-left: 4rpx;margin-top: 4rpx;" type="forward" size="10"
color="#979797"></uni-icons>
</view>
</view>
<view class="passenger-details">
<text class="id-card-text">{{ passenger.idType }}</text>
<text class="price-text">¥{{ passenger.price }}</text>
</view>
<view class="status-row">
<text class="status-text">已支付</text>
<text class="refund-rule-text">退改说明</text>
</view>
</view>
<view class="passenger-actions">
<view class="svc-btn outline">
<text class="svc-text">订餐</text>
</view>
<view class="svc-btn outline">
<text class="svc-text">购乘意险</text>
</view>
<view class="svc-btn primary" v-if="passenger.isMe">
<text class="svc-text-white">
<image style="width: 20rpx;height: 20rpx;"
src="/static/image/other/train-tickets/12306-tickets/qr-code.png"></image>
<text style="margin-left: 8rpx;">二维码验票</text>
</text>
</view>
</view>
</view>
</view>
<view style="margin: 0 16rpx;">
<!-- 分享区 -->
<view class="share-section">
<text class="share-tips">美好旅途快与朋友一起分享吧~</text>
<image style="width: 158rpx;height: 50rpx;"
src="/static/image/other/train-tickets/12306-tickets/share.png">
</image>
</view>
<!-- 提示 -->
<view class="notice-row">
<uni-icons type="notification" size="14" color="#AAAAAA"></uni-icons>
<text class="notice-text">订单信息查询有效期限为30日</text>
</view>
<!-- 广告Banner -->
<view class="ad-banner">
<image class="bg-image" src="/static/image/other/train-tickets/12306-tickets/hotel-bg.png"
mode="widthFix">
</image>
<image class="bg-image" style="opacity: 0;position: relative;"
src="/static/image/other/train-tickets/12306-tickets/hotel-bg.png" mode="widthFix">
</image>
<view class="hotel-ad">
<view class="hotel-search-row">
<view class="city-box">
<uni-icons class="search-icon" style="margin-right: 8rpx;height: 16px;" type="search"
size="16" color="#C8C8C8"></uni-icons>
<text class="city-text">{{ ticketsInfo.hotelInfo.city }}</text>
</view>
<view class="date-range">
<text class="date-val">{{ ticketsInfo.hotelInfo.startDay }}</text>
<text class="date-label">入住</text>
<view class="night-count">
<text class="night-num">{{ data.nightCount }}</text>
</view>
<text class="date-val">{{ ticketsInfo.hotelInfo.endDay }}</text>
<text class="date-label">离店</text>
</view>
</view>
</view>
</view>
<!-- 更多服务 -->
<view class="bottom-image">
<image style="width: 100%;height: 100%;"
src="/static/image/other/train-tickets/12306-tickets/bottom-bg.png" mode="widthFix">
</image>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import NavBar from '@/components/nav-bar/nav-bar.vue';
import {
ref,
reactive,
toRefs,
computed
} from 'vue';
import {
onLoad,
onShow
} from '@dcloudio/uni-app';
import { util } from '@/utils/common.js';
const buttonGroup = [{
name: "编辑车票信息",
click: () => {
util.goPage('/pages/other/train-tickets/edit/edit')
}
}]
const ticketType = [
{
label: '成人票',
value: '1'
},
{
label: '儿童票',
value: '2'
},
{
label: '学生票',
value: '3'
},
{
label: '残疾军人票',
value: '4'
}
]
const statusBarHeight = ref(20);
const data = reactive({
ticketsInfo: {
"orderInfo": {
"orderNo": "EJ66223536",
"orderTime": "2026.01.01"
},
"ticketInfo": {
"departureTime": "01-01 09:19",
"departureStation": "北京南",
"arrivalTime": "01-01 14:04",
"arrivalStation": "上海虹桥",
"trainNo": "G905",
"duration": "4时45分",
"date": "2026.01.01",
"weekDay": "星期四",
"gate": "6B"
},
"passengerList": [
{
"name": "张元英",
"type": "成人票",
"seatType": "商务座",
"carriage": "01",
"seatNo": "03C",
"idType": "外国护照KR",
"price": "2110",
"status": "已支付",
"isMe": true
}
],
"hotelInfo": {
"city": "上海",
"startDay": "01-01",
"endDay": "01-02"
}
},
nightCount: 1
})
let { ticketsInfo } = toRefs(data)
// Computed Helpers for Display
const getDisplayTime = (str) => {
if (!str) return '';
// If format is "MM-DD HH:mm", split and take time
if (str.length > 5) {
return str.split(' ')[1];
}
return str;
}
const departureTimeDisplay = computed(() => {
return getDisplayTime(data.ticketsInfo.ticketInfo.departureTime);
})
const arrivalTimeDisplay = computed(() => {
return getDisplayTime(data.ticketsInfo.ticketInfo.arrivalTime);
})
const dayDiff = computed(() => {
const dep = data.ticketsInfo.ticketInfo.departureTime;
const arr = data.ticketsInfo.ticketInfo.arrivalTime;
if (!dep || !arr) return 0;
// Format is "MM-DD HH:mm"
const depDate = dep.split(' ')[0];
const arrDate = arr.split(' ')[0];
if (depDate === arrDate) return 0;
const year = new Date().getFullYear();
// Use "/" for compatibility
const d1 = new Date(`${year}/${depDate.replace(/-/g, '/')} 00:00:00`).getTime();
const d2 = new Date(`${year}/${arrDate.replace(/-/g, '/')} 00:00:00`).getTime();
const diffTime = d2 - d1;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays > 0 ? diffDays : 0;
})
const computeSeatNo = (seatNo) => {
if (seatNo.includes('C') || seatNo.includes('F')) {
return '靠窗';
} else if (seatNo.includes('A') || seatNo.includes('D')) {
return '靠过道';
}
return '';
}
/**
* 计算两个日期之间的天数差
*/
const calculateNightCount = () => {
const currentYear = new Date().getFullYear();
// YYYY-MM-DD
const startStr = `${currentYear}-${data.ticketsInfo.hotelInfo.startDay}`;
const endStr = `${currentYear}-${data.ticketsInfo.hotelInfo.endDay}`;
// ( iOS / -)
const startTime = new Date(startStr.replace(/-/g, '/')).getTime();
const endTime = new Date(endStr.replace(/-/g, '/')).getTime();
if (!isNaN(startTime) && !isNaN(endTime)) {
const diff = endTime - startTime;
//
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
data.nightCount = days > 0 ? days : 1;
} else {
data.nightCount = 1;
}
}
onLoad(() => {
const sys = uni.getSystemInfoSync();
statusBarHeight.value = sys.statusBarHeight || 20;
});
onShow(() => {
if (uni.getStorageSync('ticketsInfo')) {
Object.assign(data.ticketsInfo, uni.getStorageSync('ticketsInfo'))
calculateNightCount()
}
// #ifdef APP-PLUS
util.setAndroidSystemBarColor('#ffffff')
setTimeout(() => {
plus.navigator.setStatusBarStyle("light");
}, 500)
// #endif
const stored = uni.getStorageSync('ticketsInfo')
if (stored) {
Object.assign(data.ticketsInfo, stored)
calculateNightCount()
}
});
const goBack = () => {
uni.navigateBack();
};
const copyOrderNo = () => {
uni.setClipboardData({
data: orderInfo.value.orderNo,
success: () => {
uni.showToast({
title: '复制成功',
icon: 'none'
});
}
});
};
const handleAction = (action) => {
uni.showToast({
title: `点击了${action}`,
icon: 'none'
})
}
</script>
<style lang="less" scoped>
.container {
width: 100%;
min-height: 100vh;
background-color: #ffffff;
position: relative;
}
/* 顶部渐变背景 */
.header-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 540rpx;
background: linear-gradient(180deg, #3C99FB 0%, #3C99FB 45.73%, rgba(60, 153, 251, 0.6007) 60.23%, rgba(122, 122, 122, 0) 98.34%);
z-index: 0;
}
/* 导航栏 */
.nav-bar {
.center-text {
font-size: 40rpx;
font-weight: 400;
color: #fff;
}
}
::v-deep.right-icon {
margin-right: 50rpx;
}
/* 内容区域 */
.content-scroll {
position: relative;
z-index: 1;
// padding: 0 16rpx;
box-sizing: border-box;
width: 100%;
}
/* 订单源信息 */
.order-info-box {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 0 16rpx;
margin-bottom: 12rpx;
margin-top: 14rpx;
}
.order-number-row {
display: flex;
flex-direction: row;
align-items: center;
}
.order-label {
font-size: 26rpx;
color: #FFFFFF;
margin-right: 5px;
line-height: 52rpx;
}
.copy-btn {
height: 28rpx;
width: 50rpx;
}
.order-time {
font-size: 24rpx;
color: #FFFFFF;
}
.main-card {
margin: 0 16rpx;
position: relative;
/* 四周均匀扩散阴影 */
box-shadow: 0rpx 0rpx 32rpx 4rpx rgba(0, 0, 0, 0.1);
background-color: #fff;
border-radius: 16rpx;
}
/* 票卡片 */
.ticket-card {
position: relative;
background-color: #FFFFFF;
border-radius: 12rpx;
padding: 30rpx 28rpx;
box-shadow: 0rpx 8rpx 30rpx 0rpx rgba(0, 0, 0, 0.05);
z-index: 9;
}
.ticket-main-info {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30rpx;
}
.station-time-box {
display: flex;
flex-direction: column;
}
.time-text {
font-size: 48rpx;
font-weight: 500;
color: #1a1a1a;
line-height: 52rpx;
margin: 0 10rpx;
}
.station-text {
font-size: 30rpx;
color: #1a1a1a;
margin: 12rpx 12rpx 0;
display: flex;
align-items: center;
}
.train-info-box {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 8rpx;
}
.train-no {
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #1A1A1A;
}
.stopover-box {
width: 90rpx;
height: 24rpx;
position: relative;
margin-bottom: 8rpx;
margin-top: 14rpx;
}
.stopover-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 18rpx;
text-align: center;
white-space: nowrap;
}
.duration-line {
width: 60px;
height: 1px;
background-color: #E0E0E0;
margin: 2px 0;
}
.duration-text {
font-size: 24rpx;
color: #999999;
}
.date-row {
display: flex;
flex-direction: row;
justify-content: space-between;
font-size: 26rpx;
color: #767676;
padding: 0 12rpx;
line-height: 30rpx;
margin-bottom: 22rpx;
}
.gate-info-box {
background-color: #FFF9F0;
border-radius: 12rpx;
padding: 12rpx 16rpx 14rpx;
font-size: 26rpx;
color: #664A37;
display: flex;
flex-direction: row;
align-items: flex-end;
line-height: 30rpx;
}
.gate-text {
font-size: 20rpx;
color: #767676;
}
.action-bar {
position: relative;
margin-top: -24rpx;
background-color: #fff;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
border-top: 1px solid #F0F0F0;
padding-top: 48rpx;
padding-bottom: 30rpx;
box-shadow: 0rpx 8rpx 30rpx 0rpx rgba(0, 0, 0, 0.05);
/* REMOVED box-shadow from child */
z-index: 2;
}
.action-item {
flex: 1;
text-align: center;
}
.action-text {
font-size: 30rpx;
color: #4C9BEC;
font-weight: 500;
}
.divider {
width: 1px;
height: 16px;
background-color: #EFEFEF;
}
/* 乘客卡片 */
.passenger-card-box {
position: relative;
border-bottom: 1px solid #F0F0F0;
z-index: 1;
}
.passenger-card-box:last-child {
border-bottom: none;
}
.passenger-card {
position: relative;
background-color: #FFFFFF;
padding: 32rpx 42rpx 38rpx;
box-shadow: 0rpx 8rpx 50rpx 0rpx rgba(0, 0, 0, 0.04);
}
.passenger-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 22rpx;
}
.name-box {
display: flex;
flex-direction: row;
align-items: center;
}
.passenger-name {
font-size: 36rpx;
font-weight: 500;
color: #3D3D3D;
line-height: 30rpx;
margin-right: 8px;
}
.tag-text {
border-radius: 4rpx;
padding: 4rpx 2rpx;
border: 1rpx solid #B2CDE8;
font-size: 20rpx;
line-height: 20rpx;
color: #4A94D2;
}
.seat-info {
display: flex;
align-items: center;
.tag-text-grey {
border-radius: 4rpx;
padding: 2rpx 4rpx;
border: 1rpx solid #EDEDED;
font-size: 18rpx;
line-height: 18rpx;
color: #979797;
margin-right: 8rpx;
}
.seat-text {
font-size: 13px;
color: #1A1A1A;
font-weight: 400;
}
}
.passenger-details {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 28rpx;
.id-card-text {
font-size: 28rpx;
color: #979797;
line-height: 30rpx;
}
.price-text {
font-size: 28rpx;
color: #F28127;
line-height: 28rpx;
}
}
.status-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.status-text {
font-size: 28rpx;
color: #767676;
line-height: 28rpx;
}
.refund-rule-text {
font-size: 28rpx;
color: #4A94D2;
text-decoration: underline;
line-height: 28rpx;
}
}
.passenger-actions {
padding: 14rpx 28rpx;
padding-right: 18rpx;
background-color: #FFFFFF;
display: flex;
flex-direction: row;
justify-content: flex-end;
// gap: 20rpx;
.svc-btn {
height: 54rpx;
padding: 0 16rpx;
border-radius: 4rpx;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 10rpx;
}
.svc-btn.outline {
border: 1px solid #EDEDED;
background-color: #FFFFFF;
}
.svc-btn.primary {
background-color: #3C99FB;
border: 1px solid #C3EFFF;
}
.svc-text {
color: #767676;
}
.svc-text-white {
color: #FFFFFF;
}
}
/* 分享区 */
.share-section {
position: relative;
margin-top: 20rpx;
background-color: #FFFFFF;
border-radius: 8rpx;
padding: 20rpx 30rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 28rpx;
box-shadow: 0rpx 0rpx 4rpx 0rpx rgba(0, 0, 0, 0.05);
.share-tips {
font-size: 24rpx;
color: #3D3D3D;
}
}
/* 提示 */
.notice-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
line-height: 24rpx;
margin-bottom: 28rpx;
}
.notice-text {
font-size: 24rpx;
color: #AAAAAA;
margin-left: 4rpx;
}
/* 广告Banner */
.ad-banner {
position: relative;
width: 100%;
.bg-image {
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
}
.hotel-ad {
position: absolute;
bottom: 0;
left: 0;
right: 0;
width: 100%;
padding: 0 20rpx 12rpx;
margin-bottom: 12rpx;
}
.hotel-search-row {
background-color: #FFFFFF;
border-radius: 20px;
padding: 14rpx 44rpx;
display: flex;
flex-direction: row;
align-items: center;
height: 60rpx;
.city-box {
display: flex;
align-items: center;
.search-icon {
margin-right: 18rpx;
height: 16px;
display: flex;
align-items: center;
}
}
.city-text {
font-size: 30rpx;
color: #4495F0;
margin-right: 44rpx;
font-weight: 500;
}
.date-range {
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
padding: 0 32rpx;
border-left: 1rpx solid #D8D8D8;
padding-right: 0;
}
.date-val {
font-size: 30rpx;
font-weight: 500;
color: #4495F0;
border-bottom: 1px solid #4495F0;
}
.date-label {
font-size: 24rpx;
color: #1A1A1A;
margin: 0 10rpx;
}
.night-count {
font-size: 24rpx;
border: 1px solid #E0E0E0;
border-radius: 16rpx;
height: 34rpx;
width: 80rpx;
line-height: 34rpx;
text-align: center;
margin: 0 10rpx;
margin-right: 18rpx;
}
.night-num {
font-size: 24rpx;
line-height: 24rpx;
color: #333333;
}
}
}
.time-row {
display: flex;
flex-direction: row;
align-items: flex-start;
}
.day-badge {
font-size: 22rpx;
color: rgb(60, 153, 251);
margin-left: 2rpx;
line-height: 30rpx;
position: relative;
top: 0rpx;
}
</style>

View File

@ -0,0 +1,757 @@
<template>
<view class="container">
<NavBar title="修改车票信息" bgColor="#F5F5F5" isRightButton @right-click="handleRightButtonClick"></NavBar>
<scroll-view scroll-y class="form-content">
<!-- 订单信息 -->
<view class="section-container">
<view class="section-header" @click="toggleSection('orderInfo')">
<text class="section-title">订单信息</text>
<uni-icons :type="collapsed.orderInfo ? 'bottom' : 'top'" size="16" color="#666"></uni-icons>
</view>
<view class="card" v-show="!collapsed.orderInfo">
<view class="form-item">
<text class="label">订单号</text>
<input class="input" v-model="ticketsInfo.orderInfo.orderNo" />
</view>
<picker mode="date" fields="day" :value="getPickerDate(ticketsInfo.orderInfo.orderTime)"
@change="onOrderTimeChange">
<view class="form-item">
<text class="label">下单时间</text>
<view class="input">{{ ticketsInfo.orderInfo.orderTime }}</view>
</view>
</picker>
</view>
</view>
<!-- 车票信息 -->
<view class="section-container">
<view class="section-header" @click="toggleSection('ticketInfo')">
<text class="section-title">车票信息</text>
<uni-icons :type="collapsed.ticketInfo ? 'bottom' : 'top'" size="16" color="#666"></uni-icons>
</view>
<view class="card" v-show="!collapsed.ticketInfo">
<view class="form-item">
<text class="label">车次</text>
<input class="input" v-model="ticketsInfo.ticketInfo.trainNo" />
</view>
<picker mode="date" fields="day" :value="getPickerDate(ticketsInfo.ticketInfo.date)"
@change="onTicketDateChange">
<view class="form-item" style="border-bottom: 1rpx solid #F5F5F5;">
<text class="label">日期</text>
<view class="input">{{ ticketsInfo.ticketInfo.date }}</view>
</view>
</picker>
<view class="form-item">
<text class="label">检票口</text>
<input class="input" v-model="ticketsInfo.ticketInfo.gate" />
</view>
<view class="form-item">
<text class="label">出发站</text>
<input class="input" v-model="ticketsInfo.ticketInfo.departureStation" />
</view>
<picker mode="multiSelector" :range="departureTimeRange" :value="departureTimeIndex"
@change="onDepartureTimeChange">
<view class="form-item" style="border-bottom: 1rpx solid #F5F5F5;">
<text class="label">出发时间</text>
<view class="input">{{ ticketsInfo.ticketInfo.departureTime }}</view>
</view>
</picker>
<view class="form-item">
<text class="label">到达站</text>
<input class="input" v-model="ticketsInfo.ticketInfo.arrivalStation" />
</view>
<!-- Arrival Time (Multi-Selector Picker) -->
<picker mode="multiSelector" :range="arrivalRange" :value="arrivalIndex" @change="onArrivalChange">
<view class="form-item">
<text class="label">到达时间</text>
<view class="input">{{ ticketsInfo.ticketInfo.arrivalTime }}</view>
</view>
</picker>
<!-- <view class="form-item">
<text class="label">历时</text>
<input class="input" v-model="ticketsInfo.ticketInfo.duration" disabled />
</view> -->
</view>
</view>
<!-- 乘客信息 -->
<view class="section-container">
<view class="section-header" @click="toggleSection('passengerList')">
<text class="section-title">乘客信息 ({{ ticketsInfo.passengerList.length }})</text>
<uni-icons :type="collapsed.passengerList ? 'bottom' : 'top'" size="16" color="#666"></uni-icons>
</view>
<view v-show="!collapsed.passengerList">
<view class="card" v-for="(passenger, index) in ticketsInfo.passengerList" :key="index">
<view class="card-header-row">
<text class="card-header">乘客 {{ index + 1 }}</text>
<text class="delete-btn" @click="removePassenger(index)">删除</text>
</view>
<view class="form-item">
<text class="label">姓名</text>
<input class="input" v-model="passenger.name" />
</view>
<picker :range="ticketType" range-key="label" @change="(e) => onTicketTypeChange(e, index)">
<view class="form-item" style="border-bottom: 1rpx solid #F5F5F5;">
<text class="label">票种</text>
<view class="input">{{ passenger.type }}</view>
</view>
</picker>
<view class="form-item">
<text class="label">席别</text>
<input class="input" v-model="passenger.seatType" />
</view>
<view class="form-item">
<text class="label">车厢</text>
<input class="input" v-model="passenger.carriage" />
</view>
<view class="form-item">
<text class="label">座位号</text>
<input class="input" v-model="passenger.seatNo" />
</view>
<view class="form-item">
<text class="label">票价</text>
<input class="input" v-model="passenger.price" />
</view>
<view class="form-item">
<text class="label">证件类型</text>
<input class="input" v-model="passenger.idType" />
</view>
<view class="form-item">
<text class="label">是否本人</text>
<switch :checked="passenger.isMe" @change="(e) => passenger.isMe = e.detail.value" />
</view>
</view>
<view class="add-btn-box" @click="addPassenger">
<uni-icons type="plusempty" size="20" color="#1677FF"></uni-icons>
<text class="add-text">添加乘客</text>
</view>
</view>
</view>
<!-- 酒店广告 -->
<view class="section-container">
<view class="section-header" @click="toggleSection('hotelInfo')">
<text class="section-title">酒店广告</text>
<uni-icons :type="collapsed.hotelInfo ? 'bottom' : 'top'" size="16" color="#666"></uni-icons>
</view>
<view class="card" v-show="!collapsed.hotelInfo">
<view class="form-item">
<text class="label">城市</text>
<input class="input" v-model="ticketsInfo.hotelInfo.city" />
</view>
<uni-datetime-picker type="daterange" v-model="hotelDateRange" :border="false">
<view class="form-item">
<text class="label">入住/离店日期</text>
<view class="input">{{ ticketsInfo.hotelInfo.startDay }} {{ ticketsInfo.hotelInfo.endDay
}}</view>
</view>
</uni-datetime-picker>
</view>
</view>
<view class="placeholder"></view>
</scroll-view>
</view>
</template>
<script setup>
import NavBar from '@/components/nav-bar/nav-bar.vue'
import { reactive, toRefs, onMounted, computed } from 'vue';
const defaultData = {
"orderInfo": {
"orderNo": "EJ66223536",
"orderTime": "2026.01.01"
},
"ticketInfo": {
"departureTime": "01-01 09:19",
"departureStation": "北京南",
"arrivalTime": "01-01 14:04",
"arrivalStation": "上海虹桥",
"trainNo": "G905",
"duration": "4时45分",
"date": "2026.01.01",
"weekDay": "星期四",
"gate": "6B"
},
"passengerList": [
{
"name": "张元英",
"type": "成人票",
"seatType": "商务座",
"carriage": "01",
"seatNo": "03C",
"idType": "外国护照KR",
"price": "2110",
"status": "已支付",
"isMe": true
}
],
"hotelInfo": {
"city": "上海",
"startDay": "01-01",
"endDay": "01-02"
}
}
//
const ticketType = [
{
label: '成人票',
value: '1'
},
{
label: '儿童票',
value: '2'
},
{
label: '学生票',
value: '3'
},
{
label: '残疾军人票',
value: '4'
}
]
const data = reactive({
ticketsInfo: JSON.parse(JSON.stringify(defaultData)),
collapsed: {
orderInfo: true,
ticketInfo: false, // Default open ticket info as it is most likely to be edited
passengerList: false,
hotelInfo: true
}
})
const { ticketsInfo, collapsed } = toRefs(data)
const ticketYear = computed(() => {
const dateStr = data.ticketsInfo.ticketInfo.date;
if (dateStr && dateStr.length >= 4) {
return dateStr.substring(0, 4);
}
return new Date().getFullYear().toString();
})
/**
* 获取酒店日期范围
*/
const hotelDateRange = computed({
get() {
const year = ticketYear.value;
const start = data.ticketsInfo.hotelInfo.startDay ? `${year}-${data.ticketsInfo.hotelInfo.startDay}` : '';
const end = data.ticketsInfo.hotelInfo.endDay ? `${year}-${data.ticketsInfo.hotelInfo.endDay}` : '';
if (start && end) {
return [start, end];
}
return [];
},
set(val) {
if (Array.isArray(val) && val.length === 2) {
data.ticketsInfo.hotelInfo.startDay = val[0].substring(5);
data.ticketsInfo.hotelInfo.endDay = val[1].substring(5);
}
}
})
onMounted(() => {
const stored = uni.getStorageSync('ticketsInfo')
if (stored) {
Object.assign(data.ticketsInfo, stored)
}
updateDuration();
})
/**
* 确认
*/
const handleRightButtonClick = () => {
console.log("handleRightButtonClick", data.ticketsInfo)
const orderTimeStr = data.ticketsInfo.orderInfo.orderTime;
const ticketDateStr = data.ticketsInfo.ticketInfo.date;
if (orderTimeStr && ticketDateStr) {
if (orderTimeStr > ticketDateStr) {
uni.showToast({
title: '下单时间不能晚于出发日期',
icon: 'none'
});
return;
}
}
if (data.ticketsInfo.passengerList.length === 0) {
uni.showToast({
title: '请至少添加一名乘客',
icon: 'none'
});
return;
} else {
const passengerList = data.ticketsInfo.passengerList.filter(item => item.isMe)
if (passengerList.length > 1) {
uni.showToast({
title: '至多添加一名乘客为本人',
icon: 'none'
});
return;
}
}
uni.setStorageSync('ticketsInfo', data.ticketsInfo)
uni.navigateBack()
}
/**
* 切换折叠状态
* @param {string} key
*/
const toggleSection = (key) => {
data.collapsed[key] = !data.collapsed[key]
}
/**
* 删除乘客
* @param {number} index
*/
const removePassenger = (index) => {
uni.showModal({
title: '提示',
content: '确定要删除该乘客吗?',
success: (res) => {
if (res.confirm) {
data.ticketsInfo.passengerList.splice(index, 1)
}
}
})
}
/**
* 添加乘客
*/
const addPassenger = () => {
const oldPassenger = data.ticketsInfo.passengerList[data.ticketsInfo.passengerList.length - 1]
const newPassenger = {
name: '新乘客',
type: oldPassenger.type || '成人票',
seatType: oldPassenger.seatType || '二等座',
carriage: oldPassenger.carriage || '01',
seatNo: '01A',
idType: '中国居民身份证',
price: oldPassenger.price || '0',
status: '已支付',
isMe: false
}
data.ticketsInfo.passengerList.push(newPassenger)
}
/**
* 获取选择器日期
* @param {string} dateStr
* @returns {string}
*/
const getPickerDate = (dateStr) => {
if (!dateStr) return '';
// Handle YYYY.MM.DD format
if (dateStr.includes('.')) {
return dateStr.replace(/\./g, '-');
}
// Handle MM-DD (prepend year) - logic from before, but mainly for Hotel.
// Ticket date is full date YYYY.MM.DD
if (dateStr.length <= 5) {
const year = new Date().getFullYear();
return `${year}-${dateStr}`;
}
return dateStr;
}
/**
* 切换下单时间
* @param {*} e
*/
const onOrderTimeChange = (e) => {
const val = e.detail.value;
if (val) {
data.ticketsInfo.orderInfo.orderTime = val.replace(/-/g, '.');
}
}
/**
* 切换乘客类型
* @param {*} e
* @param {*} index
*/
const onTicketTypeChange = (e, index) => {
const val = e.detail.value;
data.ticketsInfo.passengerList[index].type = ticketType[val].label;
}
/**
* 切换出发日期
* @param {*} e
*/
const onTicketDateChange = (e) => {
const val = e.detail.value; // YYYY-MM-DD
if (val) {
// Update Date: YYYY.MM.DD
data.ticketsInfo.ticketInfo.date = val.replace(/-/g, '.');
// Update WeekDay
const dateObj = new Date(val.replace(/-/g, '/')); // Compatible
const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
data.ticketsInfo.ticketInfo.weekDay = days[dateObj.getDay()];
}
}
/**
* 获取时间
* @param {string} fullStr
* @returns {string}
*/
const getTimeHHMM = (fullStr) => {
if (!fullStr) return '';
if (fullStr.length > 5) return fullStr.split(' ')[1];
return fullStr;
}
/**
* 获取出发时间
*/
const departureTimeHHMM = computed(() => {
return getTimeHHMM(data.ticketsInfo.ticketInfo.departureTime);
})
/**
* 获取出发时间选择器范围
*/
const departureTimeRange = computed(() => {
const hours = Array.from({ length: 24 }, (_, i) => i < 10 ? '0' + i : '' + i);
const minutes = Array.from({ length: 60 }, (_, i) => i < 10 ? '0' + i : '' + i);
return [hours, minutes];
})
/**
* 获取出发时间索引
*/
const departureTimeIndex = computed(() => {
const timeStr = getTimeHHMM(data.ticketsInfo.ticketInfo.departureTime);
if (!timeStr) return [0, 0];
const [h, m] = timeStr.split(':');
const hours = departureTimeRange.value[0];
const minutes = departureTimeRange.value[1];
let hIdx = hours.indexOf(h);
if (hIdx === -1) hIdx = 0;
let mIdx = minutes.indexOf(m);
if (mIdx === -1) mIdx = 0;
return [hIdx, mIdx];
})
/**
* 获取到达时间日期范围
*/
const arrivalRange = computed(() => {
const dateStr = data.ticketsInfo.ticketInfo.date; // YYYY.MM.DD
const dates = [];
if (dateStr) {
const baseDate = new Date(dateStr.replace(/\./g, '-').replace(/-/g, '/') + ' 00:00:00');
for (let i = 0; i < 4; i++) { // Ticket Date + 3 days
const d = new Date(baseDate);
d.setDate(d.getDate() + i);
const pad = n => n < 10 ? '0' + n : n;
dates.push(`${pad(d.getMonth() + 1)}-${pad(d.getDate())}`);
}
} else {
dates.push('MM-DD');
}
const hours = Array.from({ length: 24 }, (_, i) => i < 10 ? '0' + i : '' + i);
const minutes = Array.from({ length: 60 }, (_, i) => i < 10 ? '0' + i : '' + i);
return [dates, hours, minutes];
})
/**
* 获取到达时间索引
*/
const arrivalIndex = computed(() => {
const arrTime = data.ticketsInfo.ticketInfo.arrivalTime;
if (!arrTime || arrTime.length < 5) return [0, 0, 0];
// Might be HH:mm or MM-DD HH:mm
const parts = arrTime.split(' ');
let datePart = parts[0];
let timePart = parts[1];
// Handle case where only HH:mm (old data)
if (!timePart && datePart.includes(':')) {
timePart = datePart;
datePart = arrivalRange.value[0][0];
}
if (!timePart) return [0, 0, 0];
const [h, m] = timePart.split(':');
const dates = arrivalRange.value[0];
const hours = arrivalRange.value[1];
const minutes = arrivalRange.value[2];
let dateIdx = dates.indexOf(datePart);
if (dateIdx === -1) dateIdx = 0;
let hIdx = hours.indexOf(h);
if (hIdx === -1) hIdx = 0;
let mIdx = minutes.indexOf(m);
if (mIdx === -1) mIdx = 0;
return [dateIdx, hIdx, mIdx];
})
/**
* 切换到达时间
* @param {*} e
*/
const onArrivalChange = (e) => {
const idxs = e.detail.value;
const range = arrivalRange.value;
if (!range[0][idxs[0]]) return;
const dateStr = range[0][idxs[0]];
const hStr = range[1][idxs[1]];
const mStr = range[2][idxs[2]];
const newArrTime = `${dateStr} ${hStr}:${mStr}`;
// Validate: >= Departure
const getTs = (str) => {
if (!str || str.length <= 5) return 0;
const year = new Date().getFullYear();
// Robust format: YYYY/MM/DD HH:mm:00
return new Date(`${year}/${str.replace(/-/g, '/')}:00`).getTime();
}
const startTs = getTs(data.ticketsInfo.ticketInfo.departureTime);
const endTs = getTs(newArrTime);
if (startTs > 0 && endTs < startTs) {
uni.showToast({
title: '到达时间不能早于出发时间',
icon: 'none'
});
return;
}
data.ticketsInfo.ticketInfo.arrivalTime = newArrTime;
updateDuration();
}
/**
* 切换出发时间
* @param e
*/
/**
* 切换出发时间
* @param e
*/
const onDepartureTimeChange = (e) => {
let val = e.detail.value; // Array [hIdx, mIdx]
// Convert array to HH:mm string
if (Array.isArray(val)) {
const h = departureTimeRange.value[0][val[0]];
const m = departureTimeRange.value[1][val[1]];
val = `${h}:${m}`;
}
if (!val) return;
// Construct New Departure Timestamp
// Departure uses Ticket Date
const ticketDate = data.ticketsInfo.ticketInfo.date; // YYYY.MM.DD
if (!ticketDate) return;
// Assuming format YYYY.MM.DD
const depDateStr = ticketDate.replace(/\./g, '/'); // YYYY/MM/DD
const newDepTs = new Date(`${depDateStr} ${val}:00`).getTime();
// Get Arrival Timestamp
const arrStr = data.ticketsInfo.ticketInfo.arrivalTime; // MM-DD HH:mm
if (arrStr && arrStr.length > 5) {
// Use Ticket Year as base.
const ticketYear = ticketDate.split('.')[0];
const arrDatePart = arrStr.split(' ')[0]; // MM-DD
const arrTimePart = arrStr.split(' ')[1]; // HH:mm
// Handle Cross Year if Ticket Date is Dec and Arrival is Jan
let arrYear = parseInt(ticketYear);
const ticketMonth = parseInt(ticketDate.split('.')[1]);
const arrMonth = parseInt(arrDatePart.split('-')[0]);
if (ticketMonth === 12 && arrMonth === 1) {
arrYear++;
}
const arrTs = new Date(`${arrYear}/${arrDatePart.replace(/-/g, '/')} ${arrTimePart}:00`).getTime();
if (newDepTs > arrTs) {
uni.showToast({
title: '出发时间不能晚于到达时间',
icon: 'none'
});
return;
}
}
const dateParts = ticketDate.split('.');
let mmdd = `${dateParts[1]}-${dateParts[2]}`;
data.ticketsInfo.ticketInfo.departureTime = `${mmdd} ${val}`;
updateDuration();
}
/**
* 更新时长
*/
const updateDuration = () => {
// Helper to parse MM-DD HH:mm to timestamp (using current year)
// Safer to use "/" for cross-platform compatibility
const getTs = (str) => {
if (!str || str.length <= 5) return 0;
const year = new Date().getFullYear();
// Format: "YYYY/MM/DD HH:mm:00"
return new Date(`${year}/${str.replace(/-/g, '/')}:00`).getTime();
}
const start = getTs(data.ticketsInfo.ticketInfo.departureTime);
const end = getTs(data.ticketsInfo.ticketInfo.arrivalTime);
if (start && end && end >= start) {
const diffMs = end - start;
const diffHrs = Math.floor(diffMs / (1000 * 60 * 60));
const diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
data.ticketsInfo.ticketInfo.duration = `${diffHrs}${diffMins}`;
}
}
</script>
<style>
@import "@/common/main.css";
page {
background-color: #F8F8F8;
height: 100vh;
overflow: hidden;
}
</style>
<style lang="less" scoped>
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.form-content {
flex: 1;
height: 0;
padding: 24rpx;
box-sizing: border-box;
}
.section-container {
margin-bottom: 24rpx;
}
.section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 24rpx 12rpx 16rpx;
background-color: transparent;
}
.section-title {
font-size: 28rpx;
color: #666;
font-weight: 500;
}
.card {
background-color: #fff;
border-radius: 16rpx;
padding: 0 24rpx;
margin-bottom: 24rpx;
overflow: hidden;
.card-header-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 0 12rpx;
border-bottom: 1rpx solid #f5f5f5;
margin-bottom: 12rpx;
}
.card-header {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.delete-btn {
font-size: 26rpx;
color: #FF4D4F;
padding: 4rpx 12rpx;
}
}
.form-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #F5F5F5;
&:last-child {
border-bottom: none;
}
.label {
font-size: 30rpx;
color: #333;
width: 240rpx;
}
.input {
flex: 1;
font-size: 30rpx;
color: #333;
text-align: right;
}
}
.add-btn-box {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
justify-content: center;
align-items: center;
border: 2rpx dashed #1677FF;
margin-bottom: 24rpx;
.add-text {
color: #1677FF;
font-size: 30rpx;
margin-left: 8rpx;
}
}
.placeholder {
height: 60rpx;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB