alipay-emulator/pages/other/train-tickets/edit/edit.vue

825 lines
21 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 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 v-if="app == '12306'" 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 v-if="app == 'ctrip'" class="form-item">
<text class="label">订单总价</text>
<input class="input" type="number" v-model="ticketsInfo.orderInfo.price" />
</view>
</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" />
</view>
<view v-if="app == 'ctrip'" class="form-item">
<text class="label">火车名称</text>
<input class="input" v-model="ticketsInfo.ticketInfo.trainName" />
</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" type="number" v-model="passenger.price" @input="onPriceInput" />
</view>
<view class="form-item">
<text class="label">积分</text>
<input class="input" type="number" v-model="passenger.points" />
</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 v-if="app == '12306' || app == 'ctrip'" class="section-container">
<view class="section-header" @click="toggleSection('hotelInfo')">
<text class="section-title">{{ app == '12306' ? '酒店广告' : '返现任务' }}</text>
<uni-icons :type="collapsed.hotelInfo ? 'bottom' : 'top'" size="16" color="#666"></uni-icons>
</view>
<view class="card" v-show="!collapsed.hotelInfo">
<view v-if="app == '12306'" class="form-item">
<text class="label">城市</text>
<input class="input" v-model="ticketsInfo.hotelInfo.city" />
</view>
<view v-if="app == 'ctrip'" class="form-item">
<text class="label">返现金额</text>
<input class="input" type="number" v-model="ticketsInfo.hotelInfo.cashback" />
</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';
import { onLoad } from '@dcloudio/uni-app';
const defaultData = {
"orderInfo": {
"orderNo": "EJ66223536",
"orderTime": "2026.01.01",
"price": "4440"
},
"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",
"trainName": "复兴号"
},
"passengerList": [{
"name": "张元英",
"type": "成人票",
"seatType": "商务座",
"carriage": "01",
"seatNo": "03C",
"idType": "外国护照KR",
"price": "2110",
"status": "已支付",
"isMe": true,
"points": 9632
}],
"hotelInfo": {
"city": "上海",
"cashback": "25",
"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
},
app: '12306',
STORAGE_KEY: 'ticketInfo'
})
let { app } = toRefs(data)
const {
ticketsInfo,
collapsed
} = toRefs(data)
onLoad((options) => {
console.log("options", options)
if (options.app) {
data.app = options.app
}
if (options.storageKey) {
data.STORAGE_KEY = options.storageKey
}
})
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(data.STORAGE_KEY)
if (stored) {
Object.assign(data.ticketsInfo, stored)
}
updateDuration();
})
/**
* 更新总价
*/
const onPriceInput = () => {
setTimeout(() => {
let total = 0;
data.ticketsInfo.passengerList.forEach(item => {
const price = Number(item.price) || 0;
total += price;
});
data.ticketsInfo.orderInfo.price = total.toString();
}, 50);
}
/**
* 确认
*/
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(data.STORAGE_KEY, 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);
onPriceInput();
}
}
})
}
/**
* 添加乘客
*/
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,
points: oldPassenger.points || '2898',
}
data.ticketsInfo.passengerList.push(newPassenger);
onPriceInput();
}
/**
* 获取选择器日期
* @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>