790 lines
28 KiB
Vue
790 lines
28 KiB
Vue
<template>
|
||
<view :class="theme_view" class="ticket-wallet-page">
|
||
<!-- 顶部返回 + 标题导航 -->
|
||
<!-- <view class="top-nav-bar">
|
||
<view class="nav-back" @click="goBack">
|
||
<text class="back-arrow">‹</text>
|
||
</view>
|
||
<view class="nav-title">
|
||
<text class="title-text">我的票夹</text>
|
||
<text class="ticket-count" v-if="totalActiveCount > 0">{{ totalActiveCount }}张待使用</text>
|
||
</view>
|
||
<view class="nav-placeholder"></view>
|
||
</view> -->
|
||
|
||
<!-- 主内容区域 -->
|
||
<scroll-view
|
||
class="wallet-scroll"
|
||
scroll-y
|
||
:refresher-enabled="true"
|
||
:refresher-triggered="isRefreshing"
|
||
@refresherrefresh="onPullDownRefresh"
|
||
>
|
||
<!-- 顶部潮流 Banner -->
|
||
<view class="brand-banner">
|
||
<!-- 氛围模糊渐变光晕 -->
|
||
<view class="ambiance-light-1"></view>
|
||
<view class="ambiance-light-2"></view>
|
||
|
||
<!-- 等高线矢量线条背景 -->
|
||
<svg class="contour-bg" viewBox="0 0 400 300" fill="none" stroke="currentColor">
|
||
<path d="M-50,60 C80,30 180,120 450,40" stroke-width="1" stroke-dasharray="3 3" />
|
||
<path d="M-50,110 C90,70 190,170 450,90" stroke-width="1" />
|
||
<path d="M-50,160 C100,120 200,210 450,140" stroke-width="0.8" />
|
||
<path d="M-50,210 C110,170 210,250 450,190" stroke-width="1.2" />
|
||
<path d="M-50,260 C120,220 220,290 450,240" stroke-width="0.8" stroke-dasharray="4 2" />
|
||
</svg>
|
||
|
||
<!-- 旋转黑胶唱片左侧(背景大黑胶) -->
|
||
<view class="record-disc-large">
|
||
<view class="record-body">
|
||
<view class="record-circle-1"></view>
|
||
<view class="record-circle-2"></view>
|
||
<view class="record-center">
|
||
<view class="record-dot"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 旋转黑胶唱片右侧(背景小黑胶) -->
|
||
<view class="record-disc-small">
|
||
<view class="record-body">
|
||
<view class="record-circle-1"></view>
|
||
<view class="record-circle-2"></view>
|
||
<view class="record-center">
|
||
<view class="record-dot"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 涂鸦笑脸 -->
|
||
<view class="graffiti-face">
|
||
<svg class="graffiti-svg" viewBox="0 0 40 40" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="20" cy="20" r="16" />
|
||
<path d="M14,16 L14.01,16 M26,16 L26.01,16" stroke-width="3.5" />
|
||
<path d="M13,22 Q20,29 27,22" />
|
||
</svg>
|
||
</view>
|
||
|
||
<!-- Live Music 竖排小文字 -->
|
||
<view class="live-music-tag">
|
||
<text>l</text><text>i</text><text>v</text><text>e</text>
|
||
<text class="live-music-dot">•</text>
|
||
<text>m</text><text>u</text><text>s</text><text>i</text><text>c</text>
|
||
<text class="live-music-dot">•</text>
|
||
<text>i</text><text>n</text>
|
||
</view>
|
||
<view class="live-music-subtag">
|
||
<text>v</text><text>r</text><text>t</text><text>i</text><text>c</text><text>k</text><text>e</text><text>t</text>
|
||
</view>
|
||
|
||
<!-- 品牌旋转星标 Hub -->
|
||
<view class="brand-star-hub">
|
||
<!-- 品牌旋转星标文字环 (使用 xlink:href 兼容小程序) -->
|
||
<svg class="rotating-text-svg svg-compat" viewBox="0 0 120 120">
|
||
<path id="vrTicketPath" d="M 60 16 A 44 44 0 1 1 59.9 16" fill="none" />
|
||
<text fill="black" style="font-size: 7.2px; font-weight: 900; letter-spacing: 0.26em;">
|
||
<textPath xlink:href="#vrTicketPath" startOffset="0%">
|
||
VRTICKET • VRTICKET • VRTICKET • VRTICKET •
|
||
</textPath>
|
||
</text>
|
||
</svg>
|
||
|
||
<!-- 四角星中心 -->
|
||
<svg class="star-core-svg" viewBox="0 0 100 100" fill="none">
|
||
<path d="M 50 2 C 50 38 38 50 2 50 C 38 50 50 62 50 98 C 50 62 62 50 98 50 C 62 50 50 38 50 2 Z" fill="black" />
|
||
</svg>
|
||
|
||
<!-- 斜横幅标 -->
|
||
<view class="brand-label-plate">
|
||
VRTicket
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 票券主面板容器 -->
|
||
<view class="ticket-panel-container">
|
||
<!-- 待使用演出列表 -->
|
||
<view class="section-header-row" v-if="activeGroups.length > 0">
|
||
<text class="section-main-title">待核销票据</text>
|
||
<view class="section-count-badge">
|
||
<text>共</text>
|
||
<text class="section-count-number">{{ totalActiveCount }}</text>
|
||
<text>张票</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section-block" v-if="activeGroups.length > 0">
|
||
<view
|
||
class="ticket-group-card"
|
||
v-for="(group, gIdx) in activeGroups"
|
||
:key="'active-' + gIdx"
|
||
@click="openQrPopup(group)"
|
||
>
|
||
<!-- 左右侧切打孔 notch -->
|
||
<view class="ticket-punch-hole-left"></view>
|
||
<view class="ticket-punch-hole-right"></view>
|
||
|
||
<!-- 卡券主要内容区 -->
|
||
<view class="card-inner-box">
|
||
<!-- 影院/场馆行 + 动作按钮 -->
|
||
<view class="card-header-row">
|
||
<view class="cinema-title-wrap">
|
||
<iconfont name="icon-store" size="32rpx" color="#000000" propClass="lh-il va-m"></iconfont>
|
||
<text class="cinema-title-text">{{ group.venue_name || '测试场馆' }}</text>
|
||
</view>
|
||
|
||
<!-- 快捷联系 actions -->
|
||
<view class="action-buttons">
|
||
<!-- 电话呼叫 -->
|
||
<view class="action-circle-btn" @click.stop="callService(group.venue_name)">
|
||
<iconfont name="icon-phone" size="28rpx" color="#444444" propClass="lh-il va-m"></iconfont>
|
||
</view>
|
||
<!-- 地图导航 -->
|
||
<view class="action-circle-btn" @click.stop="openNavigation(group.venue_name)">
|
||
<iconfont name="icon-map-location" size="28rpx" color="#444444" propClass="lh-il va-m"></iconfont>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 详细内容主体 (左栏文本 + 右栏电影海报小卡) -->
|
||
<view class="card-content-body">
|
||
<!-- 左边栏:名称、时间、座位 -->
|
||
<view class="card-left-info">
|
||
<text class="movie-title-text">{{ group.goods_title }}</text>
|
||
<!-- <text class="movie-format-tag">{{ group.tickets[0] && group.tickets[0].goods_type === 1 ? '国语 3D' : '国语 2D' }}</text> -->
|
||
|
||
<!-- 时间点(Montserrat 重磅粗体) -->
|
||
<view class="session-time-text">{{ formatSessionTime(group.session_time) }}</view>
|
||
|
||
<!-- 座位信息块 -->
|
||
<view class="seat-badge-block">
|
||
<!-- 场馆厅信息 -->
|
||
<view class="seat-hall-text" v-if="group.tickets[0] && getHallFromSeatInfo(group.tickets[0].seat_info)">
|
||
<iconfont name="icon-map-position" size="24rpx" color="#e1251b" propClass="lh-il va-m mr-xs"></iconfont> {{ getHallFromSeatInfo(group.tickets[0].seat_info) }}
|
||
</view>
|
||
<!-- 座位号列表 -->
|
||
<view class="seat-detail-wrap">
|
||
<view class="seat-numbers-list">
|
||
<text v-for="(t, tIdx) in group.tickets" :key="t.id" class="seat-item">
|
||
{{ t.seat_number }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 右边栏:高颜值海报小卡 -->
|
||
<view class="card-right-poster">
|
||
<!-- 红光氛围灯效 overlay -->
|
||
<view class="poster-spotlight"></view>
|
||
<image
|
||
class="poster-image-item"
|
||
:src="resolveImageUrl(group.goods_image) || defaultPoster"
|
||
mode="aspectFill"
|
||
/>
|
||
<!-- 海报底部的水印文字 -->
|
||
<view class="poster-text-overlay" v-if="!group.goods_image">
|
||
<text class="poster-abbr-title">{{ getShortTitle(group.goods_title) }}</text>
|
||
<text class="poster-en-title">{{ getEnTitle(group.goods_title) }}</text>
|
||
</view>
|
||
<!-- <view class="poster-format-badge">EVE</view> -->
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部查看票码分割栏 -->
|
||
<view class="ticket-card-footer">
|
||
<text class="footer-tip-text">使用时请点击展示二维码/条形码</text>
|
||
<text class="view-code-text">查看票码 ›</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态:没有待使用票据 -->
|
||
<view class="empty-section" v-else-if="!isLoading && loadStatus === 3">
|
||
<image class="empty-icon" src="/static/images/common/no-data.png" mode="aspectFit" />
|
||
<text class="empty-text">暂无待使用的电子票</text>
|
||
<view class="empty-tip">
|
||
<text>购买演出票后,在这里查看</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载中骨架 -->
|
||
<view class="loading-section" v-if="isLoading">
|
||
<view class="skeleton-card" v-for="i in 2" :key="i">
|
||
<view class="skeleton-header">
|
||
<view class="skeleton-thumb"></view>
|
||
<view class="skeleton-meta">
|
||
<view class="skeleton-line short"></view>
|
||
<view class="skeleton-line"></view>
|
||
<view class="skeleton-line"></view>
|
||
</view>
|
||
</view>
|
||
<view class="skeleton-divider"></view>
|
||
<view class="skeleton-footer"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 历史票根 -->
|
||
<view v-if="historyGroups.length > 0">
|
||
<view class="history-section-title-wrap">
|
||
<text class="history-section-title">历史票根 ({{ historyGroups.length }})</text>
|
||
</view>
|
||
<view
|
||
class="ticket-group-card history-card"
|
||
v-for="(group, gIdx) in historyGroups"
|
||
:key="'history-' + gIdx"
|
||
>
|
||
<!-- 左右侧切打孔 notch -->
|
||
<!-- 左右侧切打孔 notch -->
|
||
<view class="ticket-punch-hole-left"></view>
|
||
<view class="ticket-punch-hole-right"></view>
|
||
|
||
<!-- 核销水印盖章 -->
|
||
<view class="stamp-container">
|
||
<view class="verified-stamp" :class="{ 'refunded': group.tickets[0] && group.tickets[0].verify_status !== 1 }">
|
||
{{ group.tickets[0] && group.tickets[0].verify_status === 1 ? '已核销 PASSED' : '已退款 REFUND' }}
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 卡券内容区 -->
|
||
<view class="card-inner-box">
|
||
<!-- 影院/场馆行 -->
|
||
<view class="card-header-row">
|
||
<view class="cinema-title-wrap">
|
||
<iconfont name="icon-store" size="32rpx" color="#888888" propClass="lh-il va-m"></iconfont>
|
||
<text class="cinema-title-text">{{ group.venue_name || '测试场馆' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 详细内容主体 -->
|
||
<view class="card-content-body">
|
||
<view class="card-left-info">
|
||
<text class="movie-title-text">{{ group.goods_title }}</text>
|
||
<!-- <text class="movie-format-tag">{{ group.tickets[0] && group.tickets[0].goods_type === 1 ? '国语 3D' : '国语 2D' }}</text> -->
|
||
|
||
<view class="session-time-text">{{ formatSessionTime(group.session_time) }}</view>
|
||
|
||
<!-- 座位信息块 -->
|
||
<view class="seat-badge-block">
|
||
<!-- 场馆厅信息 -->
|
||
<view class="seat-hall-text" v-if="group.tickets[0] && getHallFromSeatInfo(group.tickets[0].seat_info)">
|
||
<iconfont name="icon-map-position" size="24rpx" color="#888888" propClass="lh-il va-m mr-xs"></iconfont> {{ getHallFromSeatInfo(group.tickets[0].seat_info) }}
|
||
</view>
|
||
<!-- 座位号列表 -->
|
||
<view class="seat-detail-wrap">
|
||
<view class="seat-numbers-list">
|
||
<text v-for="(t, tIdx) in group.tickets" :key="t.id" class="seat-item">
|
||
{{ t.seat_number }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 海报小卡 -->
|
||
<view class="card-right-poster">
|
||
<image
|
||
class="poster-image-item"
|
||
:src="resolveImageUrl(group.goods_image) || defaultPoster"
|
||
mode="aspectFill"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部安全区占位 -->
|
||
<view class="safe-area-bottom"></view>
|
||
</scroll-view>
|
||
|
||
<!-- QR 弹窗 -->
|
||
<u-popup
|
||
ref="qrPopupRef"
|
||
propType="center"
|
||
:propIsMaskClick="true"
|
||
:propRound="24"
|
||
:propBackgroundColor="qrPopupBg"
|
||
@change="onPopupChange"
|
||
@close="onPopupClose"
|
||
>
|
||
<ticket-qr-popup
|
||
:tickets="currentQrTickets"
|
||
:current-index="currentQrIndex"
|
||
:show-info="currentShowInfo"
|
||
@change="onQrSlideChange"
|
||
@refresh="onQrRefresh"
|
||
/>
|
||
</u-popup>
|
||
|
||
<!-- 提示弹窗 (Mock 效果) -->
|
||
<view v-if="showInfoModal" class="info-modal-mask" @click="showInfoModal = false">
|
||
<view class="info-modal-content" @click.stop>
|
||
<view class="info-modal-icon">ℹ️</view>
|
||
<view class="info-modal-title">系统模拟提示</view>
|
||
<scroll-view scroll-y class="info-modal-scroll">
|
||
<text class="info-modal-text">{{ infoModalText }}</text>
|
||
</scroll-view>
|
||
<button class="info-modal-btn" @click="showInfoModal = false">关闭</button>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 快捷导航 -->
|
||
<component-quick-nav :propIsNav="true" :propIsBar="true" :propIsGrayscale="plugins_mourning_data_is_app"></component-quick-nav>
|
||
|
||
<!-- 公共 -->
|
||
<component-common ref="common" :propIsGrayscale="plugins_mourning_data_is_app"></component-common>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import uPopup from '@/components/u-popup/u-popup.vue';
|
||
import TicketQrPopup from './components/ticket-qr-popup/index.vue';
|
||
import componentCommon from '@/components/common/common';
|
||
import componentQuickNav from '@/components/quick-nav/quick-nav';
|
||
|
||
const app = getApp();
|
||
|
||
export default {
|
||
name: 'TicketWallet',
|
||
components: {
|
||
uPopup,
|
||
TicketQrPopup,
|
||
componentCommon,
|
||
componentQuickNav,
|
||
},
|
||
data() {
|
||
return {
|
||
// 主题样式
|
||
theme_view: '',
|
||
// 哀悼灰度插件
|
||
plugins_mourning_data_is_app: app.globalData.is_app_mourning(),
|
||
// 加载状态 1=加载中 2=加载失败 3=加载成功
|
||
loadStatus: 1,
|
||
isRefreshing: false,
|
||
// 原始票据列表(API 返回扁平数组)
|
||
rawTickets: [],
|
||
// 默认海报兜底图
|
||
defaultPoster: '/static/images/common/empty.png',
|
||
// QR 弹窗控制(使用 ref.open()/close() 模式)
|
||
qrPopupBg: '#ffffff',
|
||
// 当前弹窗内展示的票(带 qr_payload)
|
||
currentQrTickets: [],
|
||
currentQrIndex: 0,
|
||
currentShowInfo: {},
|
||
// 内部定时器
|
||
refreshTimer: null,
|
||
// 屏幕亮度
|
||
screenBrightnessValue: 0.5,
|
||
// 当前展示票的剩余秒数
|
||
currentExpiresIn: 0,
|
||
// 快捷互动遮罩
|
||
showInfoModal: false,
|
||
infoModalText: '',
|
||
};
|
||
},
|
||
computed: {
|
||
isLoading() {
|
||
return this.loadStatus === 1;
|
||
},
|
||
// 按演出 + 场次分组(verify_status == 0)
|
||
activeGroups() {
|
||
const active = this.rawTickets.filter(t => t.verify_status === 0);
|
||
return this.groupByShow(active);
|
||
},
|
||
// 历史票根分组(verify_status == 1 已核销 或 2 已退款)
|
||
historyGroups() {
|
||
const history = this.rawTickets.filter(t => t.verify_status === 1 || t.verify_status === 2);
|
||
return this.groupByShow(history);
|
||
},
|
||
// 待使用票据总张数
|
||
totalActiveCount() {
|
||
return this.activeGroups.reduce((sum, g) => sum + g.tickets.length, 0);
|
||
},
|
||
},
|
||
onLoad(params) {
|
||
// 调用公共事件方法
|
||
app.globalData.page_event_onload_handle(params);
|
||
|
||
// 记录来源页面
|
||
if (params && params.from) {
|
||
this.sourceFrom = params.from;
|
||
}
|
||
// 获取初始屏幕亮度
|
||
// #ifndef H5
|
||
uni.getScreenBrightness({
|
||
success: (res) => {
|
||
this.screenBrightnessValue = res.value;
|
||
},
|
||
});
|
||
// #endif
|
||
},
|
||
onShow() {
|
||
// 调用公共事件方法
|
||
app.globalData.page_event_onshow_handle();
|
||
|
||
|
||
// 加载票据列表
|
||
this.init();
|
||
|
||
// 初始化配置
|
||
if (app.globalData.get_config('status') == 1) {
|
||
app.globalData.init_config(0, this, 'init_config', true);
|
||
} else {
|
||
app.globalData.is_config(this, 'init_config');
|
||
}
|
||
|
||
// 公共onshow事件
|
||
if ((this.$refs.common || null) != null) {
|
||
this.$refs.common.on_show({object: this, method: 'init'});
|
||
}
|
||
},
|
||
onUnload() {
|
||
this.clearRefreshTimer();
|
||
this.restoreScreenBrightness();
|
||
},
|
||
methods: {
|
||
// 初始化配置
|
||
init_config(status) {
|
||
if ((status || false) == true) {
|
||
var theme_view = app.globalData.get_theme_value_view();
|
||
var mourning = parseInt(app.globalData.get_config('plugins_mourning_data') || 0) == 1;
|
||
this.setData({
|
||
theme_view: theme_view,
|
||
plugins_mourning_data_is_app: mourning,
|
||
});
|
||
}
|
||
},
|
||
|
||
// 返回上一页
|
||
goBack() {
|
||
const pages = getCurrentPages();
|
||
if (pages.length > 1) {
|
||
uni.navigateBack();
|
||
} else {
|
||
uni.reLaunch({ url: '/pages/index/index' });
|
||
}
|
||
},
|
||
|
||
// 格式化日期与时间
|
||
formatSessionDate(timeStr) {
|
||
if (!timeStr) return '';
|
||
const parts = timeStr.split(' ');
|
||
return parts[0] || '';
|
||
},
|
||
formatSessionTime(timeStr) {
|
||
if (!timeStr) return '';
|
||
// session_time is already in "08:00-23:59" format
|
||
return timeStr;
|
||
},
|
||
getShortTitle(title) {
|
||
if (!title) return '潮流演艺';
|
||
return title.slice(0, 4);
|
||
},
|
||
// 从 seat_info 中提取场馆厅信息
|
||
// seat_info 格式: "08:00-23:59|测试场馆|老展厅 1|A|2排1座"
|
||
getHallFromSeatInfo(seatInfo) {
|
||
if (!seatInfo) return '';
|
||
const parts = seatInfo.split('|');
|
||
// parts[2] 是厅信息,如 "老展厅 1"
|
||
return parts[2] || '';
|
||
},
|
||
getEnTitle(title) {
|
||
if (!title) return 'LIVE MUSIC SHOW';
|
||
return 'CREATIVE EXPERIENCE';
|
||
},
|
||
// 将相对路径拼接为完整静态资源 URL(兼容非 H5 环境)
|
||
resolveImageUrl(path) {
|
||
if (!path) return '';
|
||
// 已经是完整 URL 则直接返回
|
||
if (path.indexOf('http') === 0) return path;
|
||
// 相对路径补上 static_url
|
||
var base = (app.globalData.data.static_url || '').replace(/\/+$/, '');
|
||
return base + '/' + path.replace(/^\/+/, '');
|
||
},
|
||
callService(venueName) {
|
||
this.infoModalText = `正在虚拟呼叫 「${venueName}」 客服热线:\n\n📞 400-880-9911\n\n[提示]:由于在安全沙箱中运行,呼叫将在本地虚拟完成,为您解答关于演出的任何问题。`;
|
||
this.showInfoModal = true;
|
||
},
|
||
openNavigation(venueName) {
|
||
this.infoModalText = `📌 正在智能检索并规划前往「${venueName}」的最佳通行路线:\n\n- 驾车:预计 12 分钟 (3.5公里)\n- 步行:预计 25 分钟 (1.8公里)\n- 地铁:2号线直达\n\n路线规划成功,已无线映射至您的智能HUD导航!`;
|
||
this.showInfoModal = true;
|
||
},
|
||
|
||
// 初始化加载
|
||
init() {
|
||
var self = this;
|
||
var user = app.globalData.get_user_info(this, 'init');
|
||
if (user == false) {
|
||
this.loadStatus = 2;
|
||
return;
|
||
}
|
||
this.loadStatus = 1;
|
||
uni.request({
|
||
url: app.globalData.get_request_url('list', 'ticket', 'vr_ticket'),
|
||
method: 'GET',
|
||
dataType: 'json',
|
||
success: function(res) {
|
||
if (res.data.code == 0) {
|
||
var tickets = res.data.data.tickets || [];
|
||
self.rawTickets = tickets;
|
||
self.loadStatus = 3;
|
||
} else {
|
||
self.loadStatus = 2;
|
||
if (app.globalData.is_login_check(res.data, self, 'init')) {
|
||
app.globalData.showToast(res.data.msg);
|
||
}
|
||
}
|
||
},
|
||
fail: function() {
|
||
self.loadStatus = 2;
|
||
app.globalData.showToast(self.$t('common.internet_error_tips'));
|
||
},
|
||
});
|
||
},
|
||
|
||
// 按演出 + 场馆 + 下单时间(issued_at)进行分组组织
|
||
groupByShow(tickets) {
|
||
var map = {};
|
||
tickets.forEach(function(t) {
|
||
var key = t.goods_id + '|' + (t.venue_name || '') + '|' + (t.issued_at || '');
|
||
if (!map[key]) {
|
||
map[key] = {
|
||
goods_id: t.goods_id,
|
||
goods_title: t.goods_title,
|
||
goods_image: t.goods_image,
|
||
session_time: t.session_time,
|
||
venue_name: t.venue_name,
|
||
issued_at: t.issued_at,
|
||
tickets: [],
|
||
};
|
||
}
|
||
map[key].tickets.push(t);
|
||
});
|
||
// 按下单时间或场次时间倒序(最近的在前)
|
||
return Object.values(map).sort(function(a, b) {
|
||
if (a.issued_at && b.issued_at) {
|
||
return b.issued_at - a.issued_at;
|
||
}
|
||
return (b.session_time || '').localeCompare(a.session_time || '');
|
||
});
|
||
},
|
||
|
||
// 下拉刷新
|
||
onPullDownRefresh() {
|
||
this.isRefreshing = true;
|
||
var self = this;
|
||
uni.request({
|
||
url: app.globalData.get_request_url('list', 'ticket', 'vr_ticket'),
|
||
method: 'GET',
|
||
dataType: 'json',
|
||
success: function(res) {
|
||
uni.stopPullDownRefresh();
|
||
self.isRefreshing = false;
|
||
if (res.data.code == 0) {
|
||
self.rawTickets = res.data.data.tickets || [];
|
||
self.loadStatus = 3;
|
||
} else {
|
||
app.globalData.showToast(res.data.msg || '刷新失败');
|
||
}
|
||
},
|
||
fail: function() {
|
||
uni.stopPullDownRefresh();
|
||
self.isRefreshing = false;
|
||
app.globalData.showToast(self.$t('common.internet_error_tips'));
|
||
},
|
||
});
|
||
},
|
||
|
||
// 打开 QR 弹窗(点击演出卡)
|
||
openQrPopup(group) {
|
||
var self = this;
|
||
var ticketIds = group.tickets.map(function(t) { return t.id; });
|
||
|
||
uni.showLoading({ title: '加载中...' });
|
||
|
||
// 并行请求每张票的 detail(含 qr_payload)
|
||
var promises = ticketIds.map(function(id) {
|
||
return self.fetchTicketDetail(id);
|
||
});
|
||
|
||
Promise.all(promises).then(function(results) {
|
||
uni.hideLoading();
|
||
// results 中可能有 undefined(请求失败的票),过滤掉
|
||
var validTickets = results.filter(function(r) { return r != null; });
|
||
if (validTickets.length === 0) {
|
||
app.globalData.showToast('加载票据失败,请重试');
|
||
return;
|
||
}
|
||
self.currentQrTickets = validTickets;
|
||
self.currentQrIndex = 0;
|
||
self.currentShowInfo = {
|
||
title: group.goods_title,
|
||
session_time: group.session_time,
|
||
venue_name: group.venue_name,
|
||
};
|
||
// 匹配暗黑 iOS Wallet 风格的背景
|
||
self.qrPopupBg = '#111111';
|
||
self.boostScreenBrightness();
|
||
self.startQrCountdown();
|
||
|
||
// 打开弹窗(通过 ref.open() 方式)
|
||
self.$nextTick(function() {
|
||
if (self.$refs.qrPopupRef) {
|
||
self.$refs.qrPopupRef.open();
|
||
}
|
||
});
|
||
}).catch(function() {
|
||
uni.hideLoading();
|
||
app.globalData.showToast('加载票据失败,请重试');
|
||
});
|
||
},
|
||
|
||
// QR 弹窗关闭回调(来自 u-popup @change show=false 或 @close)
|
||
onPopupChange: function(e) {
|
||
if (e && e.show === false) {
|
||
this.clearRefreshTimer();
|
||
this.restoreScreenBrightness();
|
||
}
|
||
},
|
||
|
||
// QR 弹窗关闭事件
|
||
onPopupClose: function() {
|
||
this.clearRefreshTimer();
|
||
this.restoreScreenBrightness();
|
||
},
|
||
|
||
// 获取单张票详情(含 qr_payload)
|
||
fetchTicketDetail(ticketId) {
|
||
return new Promise(function(resolve) {
|
||
uni.request({
|
||
url: app.globalData.get_request_url('detail', 'ticket', 'vr_ticket', 'id=' + ticketId),
|
||
method: 'GET',
|
||
dataType: 'json',
|
||
success: function(res) {
|
||
if (res.data.code == 0 && res.data.data.ticket) {
|
||
resolve(res.data.data.ticket);
|
||
} else {
|
||
resolve(null);
|
||
}
|
||
},
|
||
fail: function() {
|
||
resolve(null);
|
||
},
|
||
});
|
||
});
|
||
},
|
||
|
||
// Swiper 滑动切换
|
||
onQrSlideChange(index) {
|
||
this.currentQrIndex = index;
|
||
// 重启倒计时(针对新票)
|
||
this.startQrCountdown();
|
||
},
|
||
|
||
// 启动内部倒计时(无 UI)
|
||
startQrCountdown() {
|
||
var self = this;
|
||
this.clearRefreshTimer();
|
||
|
||
var currentTicket = this.currentQrTickets[this.currentQrIndex];
|
||
if (!currentTicket) return;
|
||
|
||
this.currentExpiresIn = currentTicket.qr_expires_in || 0;
|
||
|
||
this.refreshTimer = setInterval(function() {
|
||
self.currentExpiresIn--;
|
||
// 剩余 ≤ 30 秒时静默刷新
|
||
if (self.currentExpiresIn <= 30 && self.currentExpiresIn > 0) {
|
||
self.silentRefreshQr(self.currentQrIndex);
|
||
}
|
||
if (self.currentExpiresIn <= 0) {
|
||
self.clearRefreshTimer();
|
||
}
|
||
}, 1000);
|
||
},
|
||
|
||
// 静默刷新某张票 of QR
|
||
silentRefreshQr(ticketIndex) {
|
||
var self = this;
|
||
var ticket = this.currentQrTickets[ticketIndex];
|
||
if (!ticket) return;
|
||
|
||
uni.request({
|
||
url: app.globalData.get_request_url('refreshQr', 'ticket', 'vr_ticket', 'id=' + ticket.id),
|
||
method: 'GET',
|
||
dataType: 'json',
|
||
success: function(res) {
|
||
if (res.data.code == 0 && res.data.data.ticket) {
|
||
self.$set(self.currentQrTickets, ticketIndex, res.data.data.ticket);
|
||
self.currentExpiresIn = res.data.data.ticket.qr_expires_in || 0;
|
||
}
|
||
},
|
||
fail: function() {
|
||
// 静默失败,不提示
|
||
},
|
||
});
|
||
},
|
||
|
||
// 用户点击过期遮罩触发刷新(来自 popup 组件事件)
|
||
onQrRefresh() {
|
||
var self = this;
|
||
var ticketIndex = this.currentQrIndex;
|
||
var ticket = this.currentQrTickets[ticketIndex];
|
||
if (!ticket) return;
|
||
|
||
uni.showLoading({ title: '刷新中...' });
|
||
uni.request({
|
||
url: app.globalData.get_request_url('refreshQr', 'ticket', 'vr_ticket', 'id=' + ticket.id),
|
||
method: 'GET',
|
||
dataType: 'json',
|
||
success: function(res) {
|
||
uni.hideLoading();
|
||
if (res.data.code == 0 && res.data.data.ticket) {
|
||
self.$set(self.currentQrTickets, ticketIndex, res.data.data.ticket);
|
||
self.currentExpiresIn = res.data.data.ticket.qr_expires_in || 0;
|
||
} else {
|
||
app.globalData.showToast(res.data.msg || '刷新失败');
|
||
}
|
||
},
|
||
fail: function() {
|
||
uni.hideLoading();
|
||
app.globalData.showToast('网络错误,刷新失败');
|
||
},
|
||
});
|
||
},
|
||
|
||
// 清理定时器
|
||
clearRefreshTimer() {
|
||
if (this.refreshTimer) {
|
||
clearInterval(this.refreshTimer);
|
||
this.refreshTimer = null;
|
||
}
|
||
},
|
||
|
||
// 提亮屏幕(检票场景)
|
||
boostScreenBrightness() {
|
||
// #ifndef H5
|
||
uni.setScreenBrightness({ value: 1.0 });
|
||
// #endif
|
||
},
|
||
|
||
// 恢复屏幕原始亮度
|
||
restoreScreenBrightness() {
|
||
// #ifndef H5
|
||
if (this.screenBrightnessValue > 0) {
|
||
uni.setScreenBrightness({ value: this.screenBrightnessValue });
|
||
}
|
||
// #endif
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style>
|
||
@import './ticket-wallet.css';
|
||
</style>
|