feat(vr-ticket): 新增票务订单确认页 + 观影人选择功能
parent
8ce0e5b7d2
commit
c52adc7a4a
|
|
@ -439,4 +439,109 @@
|
|||
|
||||
.cp {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* =========== 观影人信息卡片 =========== */
|
||||
.viewer-card {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.viewer-card .section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.viewer-card .required-hint {
|
||||
font-size: 24rpx;
|
||||
color: #ff4d4f;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 表单项 */
|
||||
.viewer-card .form-item {
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.viewer-card .form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.viewer-card .label-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.viewer-card .required-star {
|
||||
color: #ff4d4f;
|
||||
margin-left: 4rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.viewer-card .form-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.viewer-card .form-input {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.viewer-card .form-input:focus {
|
||||
background: #fff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
/* 快速授权按钮 */
|
||||
.viewer-card .quick-fill-btn {
|
||||
padding: 0 24rpx;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
border-radius: 8rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.viewer-card .quick-fill-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.viewer-card .quick-fill-btn:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 提示信息 */
|
||||
.viewer-card .viewer-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-top: 24rpx;
|
||||
padding: 16rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.viewer-card .tip-text {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
|
@ -67,6 +67,82 @@
|
|||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 观影人信息卡片 -->
|
||||
<view class="section-card viewer-card" v-if="viewerConfig.require_viewer">
|
||||
<view class="section-header">
|
||||
<text class="section-title">观影人信息</text>
|
||||
<text class="required-hint">*必填</text>
|
||||
</view>
|
||||
|
||||
<!-- 手机号字段 -->
|
||||
<view class="form-item" v-if="viewerConfig.require_viewer_mobile">
|
||||
<view class="form-label">
|
||||
<text class="label-text">手机号</text>
|
||||
<text class="required-star">*</text>
|
||||
</view>
|
||||
<view class="form-content">
|
||||
<input
|
||||
class="form-input"
|
||||
type="number"
|
||||
v-model="viewerMobile"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
:disabled="isGettingPhone"
|
||||
/>
|
||||
<!-- 微信小程序快速获取手机号按钮 -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button
|
||||
class="quick-fill-btn"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="onGetPhoneNumber"
|
||||
:loading="isGettingPhone"
|
||||
>
|
||||
{{ isGettingPhone ? '获取中...' : '一键授权' }}
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 姓名字段 -->
|
||||
<view class="form-item" v-if="viewerConfig.require_viewer_name">
|
||||
<view class="form-label">
|
||||
<text class="label-text">姓名</text>
|
||||
<text class="required-star">*</text>
|
||||
</view>
|
||||
<view class="form-content">
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="viewerName"
|
||||
placeholder="请输入观影人姓名"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 身份证号字段(预留,仅开发环境显示) -->
|
||||
<!-- #ifdef DEVELOPMENT -->
|
||||
<view class="form-item" v-if="viewerConfig.require_viewer_idcard">
|
||||
<view class="form-label">
|
||||
<text class="label-text">身份证号</text>
|
||||
<text class="required-star">*</text>
|
||||
</view>
|
||||
<view class="form-content">
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="viewerIdcard"
|
||||
placeholder="请输入身份证号"
|
||||
maxlength="18"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<view class="viewer-tip">
|
||||
<iconfont name="icon-info-circle" size="28rpx" color="#999"></iconfont>
|
||||
<text class="tip-text">请确保观影人信息准确,订单支付后无法修改</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 购票须知卡片 -->
|
||||
<view class="section-card">
|
||||
<view class="section-header" @click="noticeExpanded = !noticeExpanded">
|
||||
|
|
@ -243,6 +319,22 @@ export default {
|
|||
// 场馆地址
|
||||
venueAddress: '',
|
||||
|
||||
// ===== 观影人信息 =====
|
||||
// 观影人配置(从 URL 参数获取)
|
||||
viewerConfig: {
|
||||
require_viewer: false,
|
||||
require_viewer_per_seat: 0,
|
||||
require_viewer_mobile: 0,
|
||||
require_viewer_name: 0,
|
||||
require_viewer_idcard: 0
|
||||
},
|
||||
// 观影人表单数据
|
||||
viewerName: '',
|
||||
viewerMobile: '',
|
||||
viewerIdcard: '',
|
||||
// 微信手机号授权中
|
||||
isGettingPhone: false,
|
||||
|
||||
// ===== Buy 页面相关数据 =====
|
||||
// 提交按钮禁用状态
|
||||
buy_submit_disabled_status: false,
|
||||
|
|
@ -396,6 +488,26 @@ export default {
|
|||
this.venueAddress = decodeURIComponent(params.venueAddress);
|
||||
}
|
||||
|
||||
// 解析观影人配置
|
||||
if (params.viewerConfig) {
|
||||
try {
|
||||
this.viewerConfig = JSON.parse(decodeURIComponent(params.viewerConfig));
|
||||
console.log('[Order] viewerConfig loaded:', this.viewerConfig);
|
||||
} catch (e) {
|
||||
console.error('[Order] Failed to parse viewerConfig:', e);
|
||||
this.viewerConfig = { require_viewer: false };
|
||||
}
|
||||
}
|
||||
|
||||
// 自动填充观影人信息
|
||||
if (this.viewerConfig.require_viewer) {
|
||||
// 尝试从用户信息自动填充手机号
|
||||
if (this.userInfo && this.userInfo.mobile) {
|
||||
this.viewerMobile = this.userInfo.mobile;
|
||||
console.log('[Order] Auto-filled mobile from user:', this.viewerMobile);
|
||||
}
|
||||
}
|
||||
|
||||
// 记录来源页面
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
|
|
@ -431,6 +543,102 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
// ===== 观影人信息相关方法 =====
|
||||
|
||||
// 微信小程序一键获取手机号
|
||||
onGetPhoneNumber(e) {
|
||||
const { encryptedData, iv, code, errMsg } = e.detail;
|
||||
|
||||
// 用户拒绝授权
|
||||
if (!encryptedData && !code) {
|
||||
this.app.globalData.showToast('请手动填写手机号');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isGettingPhone = true;
|
||||
|
||||
const user = this.app.globalData.get_login_cache_info() || {};
|
||||
const clientType = this.app.globalData.application_client_type();
|
||||
|
||||
uni.request({
|
||||
url: this.app.globalData.get_request_url('onekeyusermobilebind', 'user'),
|
||||
method: 'POST',
|
||||
data: {
|
||||
encrypted_data: encryptedData || '',
|
||||
iv: iv || '',
|
||||
code: code || '',
|
||||
openid: user[clientType + '_openid'] || '',
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data.code == 0) {
|
||||
this.viewerMobile = res.data.data.mobile || '';
|
||||
this.app.globalData.showToast('授权成功', 'success');
|
||||
} else {
|
||||
this.app.globalData.showToast(res.data.msg || '授权失败,请手动填写');
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
this.app.globalData.showToast('网络错误,请手动填写');
|
||||
},
|
||||
complete: () => {
|
||||
this.isGettingPhone = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 前端表单校验
|
||||
validateViewerForm() {
|
||||
const config = this.viewerConfig;
|
||||
|
||||
// 手机号校验
|
||||
if (config.require_viewer_mobile) {
|
||||
if (!this.viewerMobile) {
|
||||
this.app.globalData.showToast('请填写手机号');
|
||||
return false;
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(this.viewerMobile)) {
|
||||
this.app.globalData.showToast('手机号格式不正确');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 姓名校验
|
||||
if (config.require_viewer_name) {
|
||||
if (!this.viewerName || !this.viewerName.trim()) {
|
||||
this.app.globalData.showToast('请填写观影人姓名');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 身份证号校验(预留)
|
||||
if (config.require_viewer_idcard) {
|
||||
if (!this.viewerIdcard) {
|
||||
this.app.globalData.showToast('请填写身份证号');
|
||||
return false;
|
||||
}
|
||||
if (!/^\d{17}[\dXx]$/.test(this.viewerIdcard)) {
|
||||
this.app.globalData.showToast('身份证号格式不正确');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// 组装观影人数据
|
||||
getViewerData() {
|
||||
// 如果不需要观影人,返回 null
|
||||
if (!this.viewerConfig.require_viewer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [{
|
||||
name: this.viewerName || '',
|
||||
mobile: this.viewerMobile || '',
|
||||
idcard: this.viewerIdcard || ''
|
||||
}];
|
||||
},
|
||||
|
||||
// 构建购买参数
|
||||
buildParams() {
|
||||
// 组装规格数据
|
||||
|
|
@ -686,6 +894,13 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
// 观影人信息校验
|
||||
if (this.viewerConfig.require_viewer) {
|
||||
if (!this.validateViewerForm()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setData({
|
||||
buy_submit_disabled_status: true,
|
||||
});
|
||||
|
|
@ -703,6 +918,12 @@ export default {
|
|||
site_model: 3, // 虚拟商品模式
|
||||
};
|
||||
|
||||
// 添加观影人数据
|
||||
const viewerData = this.getViewerData();
|
||||
if (viewerData) {
|
||||
data.viewer_data = viewerData;
|
||||
}
|
||||
|
||||
console.log('[Order Confirm] Submit order with data:', JSON.stringify(data));
|
||||
|
||||
var self = this;
|
||||
|
|
|
|||
|
|
@ -300,16 +300,9 @@ export default {
|
|||
this.goodsId = params.id;
|
||||
}
|
||||
|
||||
// 尝试从缓存获取商品数据
|
||||
var goods = app.globalData.goods_data_cache_handle(this.goodsId);
|
||||
if (goods != null) {
|
||||
console.log('[VR Ticket] goods from cache:', goods);
|
||||
this.handleGoodsData(goods);
|
||||
this.loadTreeData();
|
||||
} else {
|
||||
// 请求 API 获取商品详情
|
||||
this.loadGoodsDetail();
|
||||
}
|
||||
// 始终从 API 获取最新数据,避免缓存导致收藏状态不一致
|
||||
// 请求 API 获取商品详情
|
||||
this.loadGoodsDetail();
|
||||
},
|
||||
|
||||
// 加载商品详情
|
||||
|
|
@ -325,8 +318,6 @@ export default {
|
|||
var goods = res.data.data.goods;
|
||||
console.log('[VR Ticket] goods from API:', goods);
|
||||
self.handleGoodsData(goods);
|
||||
// 更新商品缓存,确保 user_is_favor 等字段同步
|
||||
self.app.globalData.goods_data_cache_handle(goods.id, goods);
|
||||
self.loadTreeData();
|
||||
}
|
||||
}
|
||||
|
|
@ -391,6 +382,20 @@ export default {
|
|||
console.log('[VR Ticket] detailContent loaded:', this.detailContent);
|
||||
}
|
||||
|
||||
// 提取并存储完整的 vr_goods_config 数组
|
||||
if (apiData.goods && apiData.goods.vr_goods_config) {
|
||||
try {
|
||||
this.vrConfig = typeof apiData.goods.vr_goods_config === 'string'
|
||||
? JSON.parse(apiData.goods.vr_goods_config)
|
||||
: apiData.goods.vr_goods_config;
|
||||
|
||||
console.log('[VR Ticket] vrConfig loaded:', this.vrConfig);
|
||||
} catch (e) {
|
||||
console.error('[VR Ticket] Failed to parse vr_goods_config:', e);
|
||||
this.vrConfig = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 提取场馆数据
|
||||
const venuesMap = {};
|
||||
Object.values(apiData.seat_templates || {}).forEach(template => {
|
||||
|
|
@ -406,9 +411,22 @@ export default {
|
|||
|
||||
// 找出距离最近的场馆
|
||||
findClosestVenue() {
|
||||
// 模拟当前用户坐标 (例如: 北京市朝阳区)
|
||||
// 在实际项目中,可以使用 uni.getLocation 获取真实经纬度
|
||||
const currentLoc = { lat: 39.9042, lng: 116.4074 };
|
||||
// 使用 ShopXO 全局位置选择器获取真实用户位置
|
||||
const userLocation = this.app.globalData.choice_user_location_init();
|
||||
|
||||
// 构建当前位置坐标(带兜底策略:默认北京天安门)
|
||||
const currentLoc = {
|
||||
lat: parseFloat(userLocation.lat) || 39.9042,
|
||||
lng: parseFloat(userLocation.lng) || 116.4074
|
||||
};
|
||||
|
||||
// 日志记录(便于调试)
|
||||
console.log('[VR Ticket] User location:', {
|
||||
status: userLocation.status,
|
||||
name: userLocation.name,
|
||||
lat: currentLoc.lat,
|
||||
lng: currentLoc.lng
|
||||
});
|
||||
|
||||
let closest = this.venues[0];
|
||||
let minDistance = Infinity;
|
||||
|
|
@ -429,6 +447,9 @@ export default {
|
|||
});
|
||||
|
||||
this.currentVenue = closest || this.venues[0];
|
||||
|
||||
// 日志记录最终选择的场馆
|
||||
console.log('[VR Ticket] Selected venue:', this.currentVenue.name, 'Distance:', minDistance.toFixed(0), 'm');
|
||||
},
|
||||
|
||||
// Haversine 距离计算 (单位:米)
|
||||
|
|
@ -495,12 +516,6 @@ export default {
|
|||
if (res.data.code == 0) {
|
||||
// 更新收藏状态
|
||||
self.collected = res.data.data.status == 1;
|
||||
// 更新商品缓存,确保数据一致性
|
||||
var goods = self.app.globalData.goods_data_cache_handle(self.goodsId);
|
||||
if (goods) {
|
||||
goods.user_is_favor = res.data.data.status;
|
||||
self.app.globalData.goods_data_cache_handle(self.goodsId, goods);
|
||||
}
|
||||
self.app.globalData.showToast(res.data.msg, 'success');
|
||||
} else {
|
||||
if (self.app.globalData.is_login_check(res.data, self, 'toggleCollect')) {
|
||||
|
|
@ -579,6 +594,20 @@ export default {
|
|||
posterImg = this.goodsData.images[0] || '';
|
||||
}
|
||||
|
||||
// 根据当前场馆匹配观影人配置
|
||||
let viewerConfig = null;
|
||||
if (this.vrConfig && Array.isArray(this.vrConfig) && this.currentVenue?.name) {
|
||||
const matchedConfig = this.vrConfig.find(config =>
|
||||
config.template_snapshot?.venue?.name === this.currentVenue.name
|
||||
);
|
||||
if (matchedConfig && matchedConfig.viewer_config) {
|
||||
viewerConfig = matchedConfig.viewer_config;
|
||||
console.log('[VR Ticket] Matched viewer config for venue:', this.currentVenue.name, viewerConfig);
|
||||
} else {
|
||||
console.log('[VR Ticket] No viewer config found for venue:', this.currentVenue.name);
|
||||
}
|
||||
}
|
||||
|
||||
// 组装跳转参数
|
||||
const params = {
|
||||
goodsId: this.goodsId,
|
||||
|
|
@ -589,7 +618,8 @@ export default {
|
|||
batchExpire: this.goodsData.batch_number_expire || '',
|
||||
sessionDatetime: sessionDatetime,
|
||||
posterUrl: posterImg,
|
||||
venueAddress: this.currentVenue?.address || ''
|
||||
venueAddress: this.currentVenue?.address || '',
|
||||
viewerConfig: viewerConfig ? encodeURIComponent(JSON.stringify(viewerConfig)) : ''
|
||||
};
|
||||
|
||||
// 拼接 URL 参数
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export default {
|
|||
getQrcodeOptions: function(ticket) {
|
||||
return {
|
||||
code: ticket.qr_payload || '',
|
||||
size: 450,
|
||||
size: 300,
|
||||
bgColor: '#ffffff',
|
||||
};
|
||||
},
|
||||
|
|
@ -279,11 +279,13 @@ export default {
|
|||
/* ========== 二维码 ========== */
|
||||
.qrcode-wrapper {
|
||||
position: relative;
|
||||
width: 320rpx;
|
||||
height: 320rpx;
|
||||
width: 340rpx;
|
||||
height: 340rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 过期遮罩 */
|
||||
|
|
|
|||
|
|
@ -52,17 +52,44 @@
|
|||
v-for="(item, idx) in group.list"
|
||||
:key="'record-' + item.id"
|
||||
>
|
||||
<view class="record-time">
|
||||
<text>{{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
<view class="record-info">
|
||||
<!-- 卡片头部:商品标题 + 核销时间 -->
|
||||
<view class="card-header-row">
|
||||
<text class="record-title">{{ item.goods_title }}</text>
|
||||
<text class="record-detail">
|
||||
{{ item.real_name }} · {{ formatSeatShort(item.seat_info) }}
|
||||
</text>
|
||||
<view class="record-time-badge">
|
||||
<text class="time-text">{{ formatTime(item.created_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="record-status">
|
||||
<text class="status-icon success">✓</text>
|
||||
|
||||
<!-- 卡片内容:展示详细字段 -->
|
||||
<view class="card-details-grid">
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">券码</text>
|
||||
<text class="detail-value highlight-code">{{ item.short_code || '-' }}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">订单号</text>
|
||||
<text class="detail-value">{{ item.order_no || '-' }}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">观演人</text>
|
||||
<text class="detail-value">
|
||||
{{ item.real_name || '-' }}
|
||||
<text class="phone-sub" v-if="item.phone">({{ item.phone }})</text>
|
||||
</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<text class="detail-label">座位</text>
|
||||
<text class="detail-value">{{ formatSeatShort(item.seat_info) || '-' }}</text>
|
||||
</view>
|
||||
<view class="detail-item" v-if="item.buyer_name && item.buyer_name !== item.real_name">
|
||||
<text class="detail-label">购买人</text>
|
||||
<text class="detail-value">{{ item.buyer_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已核销印章 -->
|
||||
<view class="success-stamp">
|
||||
<text class="stamp-text">已核销</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
|
|||
|
|
@ -136,70 +136,109 @@
|
|||
|
||||
/* ========== 记录卡片 ========== */
|
||||
.record-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.record-time {
|
||||
width: 100rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.record-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 2rpx dashed #f5f5f5;
|
||||
padding-bottom: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.record-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: #1a1a1a;
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.record-detail {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.record-status {
|
||||
width: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.record-time-badge {
|
||||
background: #fdf2f4;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20rpx;
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
.time-text {
|
||||
font-size: 22rpx;
|
||||
color: #e94560;
|
||||
font-weight: 700;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
|
||||
.status-icon.success {
|
||||
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
||||
.card-details-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
padding-right: 120rpx; /* Leave space for the verified stamp */
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
width: 110rpx;
|
||||
color: #999999;
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #333333;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.detail-value.highlight-code {
|
||||
color: #e94560;
|
||||
font-weight: 700;
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.phone-sub {
|
||||
color: #8c8c8c;
|
||||
font-size: 22rpx;
|
||||
margin-left: 8rpx;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.success-stamp {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
bottom: 24rpx;
|
||||
border: 4rpx solid rgba(82, 196, 26, 0.35);
|
||||
color: #52c41a;
|
||||
border-radius: 8rpx;
|
||||
padding: 4rpx 10rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 900;
|
||||
transform: rotate(-12deg);
|
||||
background-color: rgba(82, 196, 26, 0.03);
|
||||
pointer-events: none;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ========== 加载更多 ========== */
|
||||
|
|
|
|||
|
|
@ -629,3 +629,192 @@
|
|||
justify-content: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* ========== H5 手动输入核销 ========== */
|
||||
.h5-manual-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40rpx;
|
||||
margin-bottom: 20rpx;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* H5 警告提示卡片 */
|
||||
.h5-warning-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
background: rgba(233, 69, 96, 0.08);
|
||||
border: 1px dashed rgba(233, 69, 96, 0.4);
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(233, 69, 96, 0.05);
|
||||
}
|
||||
|
||||
.warning-icon-wrap {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(233, 69, 96, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
font-size: 28rpx;
|
||||
color: #ff6b6b;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.warning-text-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.warning-desc {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 手动核销输入卡片 */
|
||||
.manual-card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 32rpx;
|
||||
padding: 48rpx 40rpx;
|
||||
box-shadow: 0 20rpx 80rpx rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(40rpx);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 36rpx;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 900;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
/* 输入框包装器 (带渐变边框和发光效果) */
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 110rpx;
|
||||
padding: 0 32rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 24rpx;
|
||||
position: relative;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.input-wrapper.focused {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(233, 69, 96, 0.8);
|
||||
box-shadow: 0 0 25rpx rgba(233, 69, 96, 0.3);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
font-size: 36rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-right: 20rpx;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.input-wrapper.focused .input-icon {
|
||||
color: #e94560;
|
||||
}
|
||||
|
||||
.premium-input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
font-size: 32rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.clear-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 确认核销按钮 */
|
||||
.verify-submit-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
background: linear-gradient(135deg, #e94560 0%, #ff6b6b 100%);
|
||||
border-radius: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 30rpx rgba(233, 69, 96, 0.35);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.verify-submit-btn:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4rpx 15rpx rgba(233, 69, 96, 0.25);
|
||||
}
|
||||
|
||||
.verify-submit-btn[disabled] {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.verify-submit-btn[disabled] .btn-text {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,54 @@
|
|||
</view>
|
||||
</view>
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
<!-- H5手动输入区域 -->
|
||||
<view class="h5-manual-container">
|
||||
<!-- 醒目提示 -->
|
||||
<view class="h5-warning-card">
|
||||
<view class="warning-icon-wrap">
|
||||
<text class="warning-icon">⚠</text>
|
||||
</view>
|
||||
<view class="warning-text-wrap">
|
||||
<text class="warning-title">当前为 H5 预览环境</text>
|
||||
<text class="warning-desc">浏览器环境限制无法直接调用摄像头。请使用下方手动核销,或在微信小程序/APP中进行扫码。</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手动输入卡片 -->
|
||||
<view class="manual-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">手动核销券码</text>
|
||||
<text class="card-subtitle">支持输入 12位短码 或 36位UUID票码</text>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<view class="input-wrapper" :class="{ 'focused': isInputFocused }">
|
||||
<text class="input-icon">✎</text>
|
||||
<input
|
||||
type="text"
|
||||
v-model="manualCode"
|
||||
placeholder="请输入或粘贴核销码"
|
||||
placeholder-style="color: rgba(255, 255, 255, 0.35)"
|
||||
class="premium-input"
|
||||
@focus="isInputFocused = true"
|
||||
@blur="isInputFocused = false"
|
||||
confirm-type="done"
|
||||
@confirm="submitManualCode"
|
||||
/>
|
||||
<view class="clear-btn" v-if="manualCode" @click="manualCode = ''">
|
||||
<text class="clear-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="verify-submit-btn" @click="submitManualCode" :disabled="!manualCode.trim() || isScanning">
|
||||
<text class="btn-text">确认核销</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 今日统计 -->
|
||||
<view class="today-stats">
|
||||
<view class="stat-item">
|
||||
|
|
@ -174,10 +222,18 @@ export default {
|
|||
resultData: {},
|
||||
// 无权限提示
|
||||
showNoPermission: false,
|
||||
// H5 手动输入核销相关
|
||||
manualCode: '',
|
||||
isInputFocused: false,
|
||||
isH5: false,
|
||||
};
|
||||
},
|
||||
|
||||
onLoad(params) {
|
||||
// #ifdef H5
|
||||
this.isH5 = true;
|
||||
// #endif
|
||||
|
||||
// 记录来源
|
||||
if (params && params.from) {
|
||||
this.sourceFrom = params.from;
|
||||
|
|
@ -301,7 +357,7 @@ export default {
|
|||
|
||||
// #ifdef H5
|
||||
// H5 环境提示使用手机扫码
|
||||
app.globalData.showToast('请使用手机端扫码功能');
|
||||
app.globalData.showToast('当前非小程序或APP环境,请使用手动核销');
|
||||
return;
|
||||
// #endif
|
||||
|
||||
|
|
@ -324,6 +380,15 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
// 手动输入提交核销
|
||||
submitManualCode() {
|
||||
if (!this.manualCode || this.manualCode.trim() === '') {
|
||||
app.globalData.showToast('请输入有效的票码');
|
||||
return;
|
||||
}
|
||||
this.processScanResult(this.manualCode.trim());
|
||||
},
|
||||
|
||||
// 处理扫码结果
|
||||
processScanResult(code) {
|
||||
// 提取票码(可能包含URL等前缀)
|
||||
|
|
@ -350,6 +415,9 @@ export default {
|
|||
this.resultData = res.data.data || {};
|
||||
this.showResult = true;
|
||||
|
||||
// 清空 H5 手动输入框
|
||||
this.manualCode = '';
|
||||
|
||||
// 震动反馈
|
||||
uni.vibrateShort();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue