vr-shopxo-uniapp/pages/goods-vr-ticket-order/goods-vr-ticket-order.vue

870 lines
26 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="order-confirm-page">
<!-- 顶部返回按钮 -->
<view class="top-nav-left-icon pf" style="top: 44px;">
<view class="icon back-icon round cp" @click="goBack"></view>
</view>
<!-- Header Banner - ticket-header -->
<view class="order-header-banner">
<view class="poster">
<image
class="poster-img"
:src="posterUrl || (goodsData.images && goodsData.images[0]) || '/static/images/common/no-image.png'"
mode="aspectFill"
/>
</view>
<view class="header-info">
<text class="title">{{ goodsData.title || '加载中...' }}</text>
<text class="tag" v-if="goodsData.simple_desc">#{{ goodsData.simple_desc }}</text>
<!-- 日期时间(含过期时间) -->
<view class="info-row">
<iconfont name="icon-calendar" size="28rpx" color="#d0cdd8"></iconfont>
<text class="info-text">{{ formattedSessionTime }}</text>
</view>
<!-- 场馆信息布局 -->
<view class="venue-info-block">
<!-- 第一行:图标 + 场馆名称 -->
<view class="venue-row">
<iconfont name="icon-location" size="28rpx" color="#d0cdd8"></iconfont>
<text class="venue-name">{{ currentSelection.venue || '未知场馆' }}</text>
</view>
<!-- 第二行:场馆地址 -->
<text class="venue-address">{{ venueAddress || '暂无位置信息' }}</text>
</view>
</view>
</view>
<!-- Main Content - 白色背景卡片 -->
<view class="main-content">
<!-- 座位信息卡片 -->
<view class="section-card">
<view class="section-header">
<text class="section-title">座位信息</text>
<text class="card-subtitle">共 {{ confirmedSeats.length }} 张票</text>
</view>
<view class="seat-list">
<!-- 场次信息摘要 -->
<view class="session-summary" v-if="currentSelection.session">
<text class="session-text">{{ currentSelection.session }}</text>
<text class="separator">|</text>
<text class="session-text">{{ currentSelection.venue }}</text>
<text class="separator">|</text>
<text class="session-text">{{ currentSelection.room }}</text>
</view>
<view class="seat-item" v-for="(seat, index) in confirmedSeats" :key="index">
<view class="seat-badge">票档 {{ seat.sectionChar || 'A' }}</view>
<text class="seat-name">{{ seat.rowLabel }}排{{ seat.colNum }}座</text>
<text class="seat-price">¥{{ seat.price }}</text>
</view>
</view>
<view class="seat-total">
<text class="total-label">合计</text>
<text class="total-price">¥{{ totalPrice }}</text>
</view>
</view>
<!-- 购票须知卡片 -->
<view class="section-card">
<view class="section-header" @click="noticeExpanded = !noticeExpanded">
<text class="section-title">购票须知</text>
<view class="expand-btn">
<text>{{ noticeExpanded ? '收起' : '展开' }}</text>
<iconfont :name="noticeExpanded ? 'icon-angle-double-up' : 'icon-angle-double-down'" size="28rpx" color="#999"></iconfont>
</view>
</view>
<view class="notice-list" v-show="noticeExpanded">
<view class="notice-item" v-for="(item, index) in buyNotices" :key="index">
<view class="notice-dot"></view>
<view class="notice-content">
<text class="notice-title">{{ item.title }}</text>
<text class="notice-desc">{{ item.desc }}</text>
</view>
</view>
</view>
<view class="notice-list collapsed" v-show="!noticeExpanded">
<view class="notice-item" v-for="(item, index) in buyNotices.slice(0, 2)" :key="index">
<view class="notice-dot"></view>
<view class="notice-content">
<text class="notice-title">{{ item.title }}</text>
</view>
</view>
</view>
</view>
<!-- 服务说明卡片 -->
<view class="section-card">
<view class="section-header">
<text class="section-title">服务说明</text>
</view>
<view class="service-list">
<view class="service-item" v-for="(item, index) in services" :key="index">
<text :class="item.status === 'success' ? 'icon-success' : 'icon-warning'">
{{ item.status === 'success' ? '✔' : '!' }}
</text>
<view class="service-text">
<text class="service-title">{{ item.title }}</text>
<text class="service-desc">{{ item.desc }}</text>
</view>
</view>
</view>
</view>
<!-- 支付方式选择卡片 -->
<view class="section-card payment-card" v-if="total_price > 0 && payment_list.length > 0">
<view class="section-header">
<text class="section-title">支付方式</text>
</view>
<view class="payment-list">
<view
class="payment-item"
v-for="(item, index) in payment_list"
:key="index"
:class="{ 'payment-item-selected': payment_id == item.id }"
@click="payment_event(item)"
>
<image v-if="item.logo" class="payment-icon" :src="item.logo" mode="widthFix"></image>
<text class="payment-name">{{ item.name }}</text>
<view class="payment-check">
<iconfont
:name="payment_id == item.id ? 'icon-selected-solid cr-main' : 'icon-not-selected'"
size="40rpx"
:color="payment_id == item.id ? theme_color : '#999'"
></iconfont>
</view>
</view>
</view>
</view>
<!-- 留言卡片 -->
<view class="section-card" v-if="user_note_status || common_order_is_booking != 1">
<view class="section-header">
<text class="section-title">订单留言</text>
</view>
<view class="textarea-container">
<textarea
v-if="user_note_status"
class="textarea"
@blur="bind_user_note_blur_event"
@input="bind_user_note_event"
:focus="true"
:disable-default-padding="false"
:value="user_note_value"
maxlength="230"
placeholder="选填,可备注特殊需求"
></textarea>
<view v-else @click="bind_user_note_tap_event" :class="'textarea-view ' + ((user_note_value || null) == null ? 'cr-grey' : '')">
{{ user_note_value || '选填,可备注特殊需求' }}
</view>
</view>
</view>
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
</view>
<!-- 底部固定操作栏 -->
<view class="bottom-bar">
<view class="price-section">
<text class="price-label">订单金额</text>
<view class="price-wrap">
<text class="currency">¥</text>
<text class="price-amount">{{ total_price > 0 ? total_price : totalPrice }}</text>
</view>
</view>
<button
class="submit-btn"
:disabled="buy_submit_disabled_status"
@click="buy_submit_event"
>
{{ buy_submit_disabled_status ? '提交中...' : '确认支付' }}
</button>
</view>
<!-- 支付组件 -->
<component-payment
ref="payment"
:propIsRedirectTo="true"
:propPayUrl="pay_url"
:propQrcodeUrl="qrcode_url"
:propToAppointPage="to_appoint_page"
propPayDataKey="ids"
:propPaymentList="payment_list"
:propToPageBack="to_page_back"
:propToFailPage="to_fail_page"
></component-payment>
</view>
</template>
<script>
import iconfont from '@/components/iconfont/iconfont.vue';
import componentPayment from '@/components/payment/payment.vue';
export default {
name: 'GoodsVrTicketOrder',
components: {
iconfont,
componentPayment
},
data() {
return {
// App 实例
app: null,
// 主题
theme_view: '',
theme_color: '',
// 商品ID
goodsId: null,
// 商品数据
goodsData: {},
// 当前选中的规格
currentSelection: {},
// 已确认的座位列表
confirmedSeats: [],
// 购票须知
buyNotices: [],
// 服务说明
services: [],
// 须知是否展开
noticeExpanded: false,
// 页面来源参数
sourcePage: null,
// 登录用户信息
userInfo: null,
// 批号过期时间戳(来自 URL 参数)
batchExpire: null,
// 场次日期时间(来自 session_meta
sessionDatetime: '',
// 海报URL
posterUrl: '',
// 场馆地址
venueAddress: '',
// ===== Buy 页面相关数据 =====
// 提交按钮禁用状态
buy_submit_disabled_status: false,
// 订单是否已进入支付
is_order_submit_payment: 0,
// 订单参数
params: null,
// 支付方式列表
payment_list: [],
payment_id: 0,
// 总价(来自 API 计算)
total_price: 0,
// 用户留言
user_note_value: '',
user_note_status: false,
// 是否首次加载
is_first: 1,
// 货币符号
currency_symbol: '',
// 是否预约模式
common_order_is_booking: 0,
// 支付 URL
pay_url: '',
qrcode_url: '',
// 跳转页面配置
to_page_back: {
title: '订单详情',
page: '/pages/user-order/user-order',
},
to_fail_page: '/pages/user-order/user-order',
to_appoint_page: '/pages/user-order/user-order',
// 默认购票须知
defaultBuyNotices: [
{ title: '实名购票', desc: '本场演出实行实名制购票观演政策,一证一票。' },
{ title: '演出时长以现场为准', desc: '受现场不可控因素影响,实际演出时长以现场为准。' },
{ title: '定时购说明', desc: '使用定时购服务后,将在开票时为您自动购票。' },
{ title: '候补说明', desc: '使用候补服务后,将按照订单出票时间先后顺序候补。' }
],
// 默认服务说明
defaultServices: [
{ title: '电子票', desc: '现场验票时请观演人出示APP票夹中的电子票二维码验票入场。', status: 'success' },
{ title: '不支持转赠', desc: '购票完成后,观演人信息不可更改,需携带购票时填写的证件验证入场。', status: 'warning' },
{ title: '不支持退换票', desc: '票品为有价票券,不支持退换。', status: 'warning' }
]
};
},
computed: {
totalPrice() {
return this.confirmedSeats.reduce((sum, seat) => sum + Number(seat.price || 0), 0).toFixed(2);
},
batchExpireTime() {
const timestamp = this.batchExpire || this.goodsData.batch_number_expire;
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
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');
return `${month}-${day} ${hours}:${minutes}`;
},
formattedSessionTime() {
if (this.sessionDatetime) {
return this.sessionDatetime;
}
const session = this.currentSelection.session || '';
const expire = this.batchExpireTime;
if (expire) {
return `${session} (${expire} 过期)`;
}
return session;
}
},
onLoad(params) {
const app = getApp();
this.app = app;
// 主题
this.theme_view = app.globalData.get_theme_value_view();
this.theme_color = app.globalData.get_theme_color();
// 货币符号
this.currency_symbol = app.globalData.currency_symbol();
// 获取登录用户信息
this.userInfo = app.globalData.get_user_info();
// 解析页面参数
if (params.goodsId) {
this.goodsId = parseInt(params.goodsId, 10) || null;
}
// 解析座位数据
if (params.seats) {
try {
this.confirmedSeats = JSON.parse(decodeURIComponent(params.seats));
} catch (e) {
console.error('[Order Confirm] Parse seats failed:', e);
this.confirmedSeats = [];
}
}
// 解析选中的规格
if (params.selection) {
try {
this.currentSelection = JSON.parse(decodeURIComponent(params.selection));
} catch (e) {
console.error('[Order Confirm] Parse selection failed:', e);
this.currentSelection = {};
}
}
// 解析须知数据
if (params.buyNotices) {
try {
this.buyNotices = JSON.parse(decodeURIComponent(params.buyNotices));
} catch (e) {
this.buyNotices = this.defaultBuyNotices;
}
} else {
this.buyNotices = this.defaultBuyNotices;
}
if (params.services) {
try {
this.services = JSON.parse(decodeURIComponent(params.services));
} catch (e) {
this.services = this.defaultServices;
}
} else {
this.services = this.defaultServices;
}
// 解析批号过期时间
if (params.batchExpire) {
this.batchExpire = parseInt(params.batchExpire, 10) || null;
}
// 解析场次日期时间
if (params.sessionDatetime) {
this.sessionDatetime = decodeURIComponent(params.sessionDatetime);
}
// 解析海报URL
if (params.posterUrl) {
this.posterUrl = decodeURIComponent(params.posterUrl);
}
// 解析场馆地址
if (params.venueAddress) {
this.venueAddress = decodeURIComponent(params.venueAddress);
}
// 记录来源页面
const pages = getCurrentPages();
if (pages.length > 1) {
const prevPage = pages[pages.length - 2];
this.sourcePage = prevPage.route;
}
// 构建 params用于 API 调用)
this.buildParams();
// 加载商品数据
this.loadGoodsData();
// 初始化配置
this.init_config();
// 初始化支付 URL
this.pay_url = this.app.globalData.get_request_url('pay', 'order');
this.qrcode_url = this.app.globalData.get_request_url('paycheck', 'order');
},
onShow() {
// 数据加载
this.init();
},
methods: {
// 初始化配置
init_config(status) {
if ((status || false) == true) {
this.currency_symbol = this.app.globalData.get_config('currency_symbol');
this.common_order_is_booking = this.app.globalData.get_config('config.common_order_is_booking');
} else {
this.app.globalData.is_config(this, 'init_config');
}
},
// 构建购买参数
buildParams() {
// 组装规格数据
var goods_data = this.confirmedSeats.map(s => {
var specs = [];
if (s.specKey) {
var parts = s.specKey.split('|');
parts.forEach(part => {
var idx = part.indexOf('=');
if (idx > 0) {
var type = part.substring(0, idx);
var value = part.substring(idx + 1);
specs.push({ type: type, value: value });
}
});
}
if (specs.length === 0) {
specs = [
{ type: '$vr-场次', value: this.currentSelection.session || '' },
{ type: '$vr-场馆', value: this.currentSelection.venue || '' },
{ type: '$vr-演播室', value: this.currentSelection.room || '' },
{ type: '$vr-分区', value: this.currentSelection.section || '' },
{ type: '$vr-座位号', value: (s.rowLabel || '') + '排' + (s.colNum || '') + '座' }
];
}
return {
goods_id: this.goodsId,
stock: 1,
spec: specs
};
});
this.params = {
buy_type: 'goods',
goods_data: goods_data
};
},
// 加载商品数据
loadGoodsData() {
var goods = this.app.globalData.goods_data_cache_handle(this.goodsId);
if (goods != null) {
console.log('[Order Confirm] Goods from cache:', goods);
this.goodsData = goods;
this.parseVrConfig(goods);
} else {
var self = this;
uni.request({
url: this.app.globalData.get_request_url('detail', 'goods'),
method: 'POST',
data: { id: this.goodsId },
dataType: 'json',
success: function(res) {
if (res.data.code == 0) {
self.goodsData = res.data.data.goods;
self.parseVrConfig(res.data.data.goods);
}
}
});
}
},
// 解析 VR 配置获取须知
parseVrConfig(goods) {
if (this.buyNotices.length > 0 && this.services.length > 0) {
return;
}
var vrConfig = goods.vr_goods_config;
if (typeof vrConfig === 'string') {
try {
vrConfig = JSON.parse(vrConfig);
} catch (e) {
vrConfig = [];
}
}
},
// 获取数据(类似 buy.vue 的 init
init() {
// 订单是否已经提交进入支付
if (this.is_order_submit_payment == 1) {
return false;
}
// 订单参数信息是否正确
if (this.params == null) {
console.log('[Order Confirm] params is null');
return false;
}
// 加载 loading
if (this.is_first == 0) {
uni.showLoading({
title: '加载中...',
});
}
var data = {
buy_type: this.params.buy_type,
goods_data: this.encodeGoodsData(this.params.goods_data),
payment_id: this.payment_id,
site_model: 3 // 虚拟商品模式
};
var self = this;
uni.request({
url: this.app.globalData.get_request_url('index', 'buy'),
method: 'POST',
data: data,
dataType: 'json',
success: function(res) {
uni.stopPullDownRefresh();
if (self.is_first == 0) {
uni.hideLoading();
}
if (res.data.code == 0) {
var data = res.data.data;
// 设置支付方式列表
self.payment_list = data.payment_list || [];
// 首次加载
if (self.is_first == 1) {
// 赋值默认支付方式
var default_payment_id = parseInt(data.default_payment_id || 0);
if (self.payment_list.length > 0 && default_payment_id > 0) {
var temp_payment_ids = self.payment_list.map(function(item) { return item.id; });
if (temp_payment_ids.indexOf(default_payment_id) != -1) {
self.payment_id = default_payment_id;
}
}
}
// 订单是否已提交
if ((data.is_order_submit || 0) == 1) {
self.buy_submit_response_handle(res.data);
return false;
}
// 设置订单数据
if ((data.goods_list || []).length > 0) {
self.setData({
total_price: data.base.actual_price,
common_order_is_booking: data.common_order_is_booking || 0,
is_first: 0,
});
} else {
// 没有商品数据时使用本地计算的价格
self.setData({
total_price: parseFloat(self.totalPrice),
is_first: 0,
});
}
} else {
// API 失败时使用本地价格
self.setData({
total_price: parseFloat(self.totalPrice),
});
// 如果没有支付方式列表,设置默认空数组
if (!self.payment_list || self.payment_list.length === 0) {
self.setData({
payment_list: [],
});
}
// 登录检查
if (self.app.globalData.is_login_check(res.data, self, 'init')) {
self.app.globalData.showToast(res.data.msg);
}
}
},
fail: function() {
uni.stopPullDownRefresh();
if (self.is_first == 0) {
uni.hideLoading();
}
// 网络失败时使用本地价格
self.setData({
total_price: parseFloat(self.totalPrice),
});
}
});
},
// 编码 goods_data与 buy.vue 一致)
encodeGoodsData(goods_data) {
var base64 = require('@/common/js/lib/base64.js');
return encodeURIComponent(base64.encode(JSON.stringify(goods_data)));
},
// 支付方式选择
payment_event(item) {
this.setData({
payment_id: (this.payment_id == item.id) ? 0 : item.id,
});
},
// 用户留言输入
bind_user_note_event(e) {
this.setData({
user_note_value: e.detail.value,
});
},
// 用户留言点击
bind_user_note_tap_event(e) {
this.setData({
user_note_status: true,
});
},
// 用户留言失去焦点
bind_user_note_blur_event(e) {
this.setData({
user_note_status: false,
});
},
// 提交订单
buy_submit_event(e) {
// 登录检查
var user = this.app.globalData.get_user_info(this, 'buy_submit_event');
if (user == false) {
return;
}
if (this.buy_submit_disabled_status) {
return;
}
// 数据验证
if (!this.goodsId) {
this.app.globalData.showToast('商品信息错误');
return;
}
if (!this.confirmedSeats || this.confirmedSeats.length === 0) {
this.app.globalData.showToast('请选择座位');
return;
}
// 需要选择支付方式
if (this.total_price > 0 && this.common_order_is_booking != 1) {
if (!this.payment_id) {
this.app.globalData.showToast('请选择支付方式');
return;
}
}
this.setData({
buy_submit_disabled_status: true,
});
uni.showLoading({
title: '提交中...',
});
// 构建请求数据
var data = {
buy_type: this.params.buy_type,
goods_data: this.encodeGoodsData(this.params.goods_data),
payment_id: this.payment_id,
user_note: this.user_note_value || '',
site_model: 3, // 虚拟商品模式
};
console.log('[Order Confirm] Submit order with data:', JSON.stringify(data));
var self = this;
uni.request({
url: this.app.globalData.get_request_url('add', 'buy'),
method: 'POST',
data: data,
dataType: 'json',
success: function(res) {
uni.hideLoading();
if (res.data.code == 0) {
self.buy_submit_response_handle(res.data);
} else {
self.app.globalData.showToast(res.data.msg);
self.setData({
buy_submit_disabled_status: false,
});
}
},
fail: function() {
uni.hideLoading();
self.setData({
buy_submit_disabled_status: false,
});
self.app.globalData.showToast('网络错误,请重试');
}
});
},
// 订单提交响应处理
buy_submit_response_handle(res) {
// 是否预约模式
if (res.data.order_status == 0) {
var self = this;
uni.showModal({
title: '提示',
content: res.msg,
confirmText: '确定',
showCancel: false,
success: function(res) {
self.app.globalData.url_open(self.to_appoint_page, true);
},
fail: function(res) {
self.app.globalData.url_open(self.to_appoint_page, true);
}
});
} else if (res.data.order_status == 2) {
// 线下支付
this.app.globalData.url_open(this.to_appoint_page, true);
} else {
// 调起支付
this.$refs.payment.pay_handle(res.data.order_ids.join(','), res.data.payment_id, this.payment_list);
}
this.setData({
is_order_submit_payment: 1
});
},
// 返回上一页
goBack() {
uni.navigateBack();
},
// 设置数据的兼容方法
setData(obj) {
Object.keys(obj).forEach(key => {
this[key] = obj[key];
});
}
}
};
</script>
<style>
@import "./goods-vr-ticket-order.css";
/* =========== 支付方式卡片 =========== */
.payment-card {
margin-top: 0;
}
.payment-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.payment-item {
display: flex;
align-items: center;
padding: 14px 16px;
background-color: #fafafa;
border-radius: 10px;
border: 2rpx solid transparent;
transition: all 0.2s;
}
.payment-item-selected {
background-color: #fff5f5;
border-color: #ff404f;
}
.payment-icon {
width: 32px;
height: 32px;
border-radius: 6px;
margin-right: 12px;
object-fit: contain;
}
.payment-name {
flex: 1;
font-size: 15px;
color: #333;
}
.payment-check {
margin-left: 12px;
}
/* =========== 留言卡片 =========== */
.textarea-container {
min-height: 80px;
}
.textarea {
width: 100%;
min-height: 80px;
padding: 12px;
font-size: 14px;
color: #333;
background-color: #f5f6f8;
border-radius: 8px;
box-sizing: border-box;
line-height: 1.6;
}
.textarea-view {
min-height: 80px;
padding: 12px;
font-size: 14px;
background-color: #f5f6f8;
border-radius: 8px;
line-height: 1.6;
color: #333;
}
/* =========== 加载状态 =========== */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
}
.loading-text {
font-size: 14px;
color: #999;
margin-top: 12px;
}
</style>