|
|
@@ -57,17 +57,39 @@
|
|
|
</CustomerServiceButton>
|
|
|
</view>
|
|
|
</view>
|
|
|
+
|
|
|
+ <!-- 支付分授权确认弹窗 -->
|
|
|
+ <view v-if="showAuthPopup" class="auth-overlay" @click.stop>
|
|
|
+ <view class="auth-card">
|
|
|
+ <view class="auth-icon-wrap">
|
|
|
+ <image class="auth-icon" src="/static/icons/pay-socre-logo.png" mode="aspectFit"></image>
|
|
|
+ </view>
|
|
|
+ <text class="auth-title">微信支付分授权</text>
|
|
|
+ <text class="auth-desc">需授权微信支付分后开门购物,费用离店后自动结算</text>
|
|
|
+ <view class="auth-actions">
|
|
|
+ <button class="auth-btn cancel" @click="cancelAuth">取消</button>
|
|
|
+ <button class="auth-btn confirm" @click="invokePayScore">确认授权</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
+import { ref } from 'vue'
|
|
|
import { onShow } from '@dcloudio/uni-app'
|
|
|
import { scanDoor } from '../../api/device'
|
|
|
import { preCreatePayscoreOrder } from '../../api/payscore'
|
|
|
-import { getCouponCount } from '../../api/coupon'
|
|
|
import { isLoggedIn, getToken } from '../../utils/auth'
|
|
|
import { logger } from '../../utils/logger'
|
|
|
|
|
|
+// 授权弹窗状态
|
|
|
+const showAuthPopup = ref(false)
|
|
|
+const authDeviceId = ref('')
|
|
|
+const authPackage = ref('')
|
|
|
+const authMchId = ref('')
|
|
|
+const authLoading = ref(false)
|
|
|
+
|
|
|
// 页面显示时检查 token
|
|
|
onShow(() => {
|
|
|
const token = getToken()
|
|
|
@@ -95,28 +117,17 @@ const scanCode = async () => {
|
|
|
success: async function (res) {
|
|
|
logger.log('扫码结果:', res.result);
|
|
|
|
|
|
- // 从扫码结果中解析设备ID
|
|
|
- // 二维码格式: https://hh.hahabianli.com/B142977?_wxpmm0=6009000C0000
|
|
|
- // 需要提取路径中的设备ID: B142977
|
|
|
let deviceId = '';
|
|
|
-
|
|
|
try {
|
|
|
- // 尝试从URL中提取deviceId
|
|
|
const urlPattern = /\/([A-Z0-9]+)(\?|$)/;
|
|
|
const match = res.result.match(urlPattern);
|
|
|
-
|
|
|
if (match && match[1]) {
|
|
|
deviceId = match[1];
|
|
|
- logger.log('提取到设备ID:', deviceId);
|
|
|
} else {
|
|
|
- // 如果不是URL格式,尝试解析JSON格式
|
|
|
try {
|
|
|
const qrData = JSON.parse(res.result);
|
|
|
- if (qrData.deviceId) {
|
|
|
- deviceId = qrData.deviceId;
|
|
|
- }
|
|
|
+ if (qrData.deviceId) deviceId = qrData.deviceId;
|
|
|
} catch (e) {
|
|
|
- // 不是JSON格式,直接使用扫码结果作为deviceId
|
|
|
deviceId = res.result;
|
|
|
}
|
|
|
}
|
|
|
@@ -126,39 +137,33 @@ const scanCode = async () => {
|
|
|
}
|
|
|
} catch (error) {
|
|
|
logger.error('解析设备ID失败:', error);
|
|
|
- uni.showToast({
|
|
|
- title: '二维码格式错误',
|
|
|
- icon: 'none'
|
|
|
- });
|
|
|
+ uni.showToast({ title: '二维码格式错误', icon: 'none' });
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 弹出选择弹窗:查看柜内商品 or 直接开门
|
|
|
- // 需确认模式:创建支付分订单并拉起用户确认
|
|
|
- await confirmPayScoreAndOpenDoor(deviceId);
|
|
|
+ logger.log('提取到设备ID:', deviceId);
|
|
|
+
|
|
|
+ // 预创建支付分订单,获取 package
|
|
|
+ await preparePayScoreAuth(deviceId);
|
|
|
},
|
|
|
fail: function (err) {
|
|
|
logger.log('扫码取消:', err);
|
|
|
- uni.showToast({
|
|
|
- title: '扫码取消',
|
|
|
- icon: 'none'
|
|
|
- });
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
- * 需确认模式:预创建支付分订单 + 用户确认 + 开门
|
|
|
+ * 预创建支付分订单,成功后在页面内展示授权按钮
|
|
|
*/
|
|
|
-const confirmPayScoreAndOpenDoor = async (deviceId: string) => {
|
|
|
- uni.showLoading({ title: '创建订单...', mask: true });
|
|
|
+const preparePayScoreAuth = async (deviceId: string) => {
|
|
|
+ uni.showLoading({ title: '准备中...', mask: true });
|
|
|
|
|
|
try {
|
|
|
- // 1. 预创建支付分服务订单
|
|
|
const result = await preCreatePayscoreOrder({ deviceId });
|
|
|
|
|
|
+ uni.hideLoading();
|
|
|
+
|
|
|
if (!result.success || !result.package) {
|
|
|
- uni.hideLoading();
|
|
|
uni.showToast({
|
|
|
title: result.errorMsg || '创建支付分订单失败',
|
|
|
icon: 'none',
|
|
|
@@ -166,80 +171,87 @@ const confirmPayScoreAndOpenDoor = async (deviceId: string) => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ logger.log('[支付分] 预创建成功, package:', result.package);
|
|
|
+
|
|
|
+ // 存到状态中,等用户点击"确认授权"按钮
|
|
|
+ authDeviceId.value = deviceId;
|
|
|
+ authPackage.value = result.package;
|
|
|
+ authMchId.value = result.mchId || '';
|
|
|
+ showAuthPopup.value = true;
|
|
|
+ } catch (error: any) {
|
|
|
uni.hideLoading();
|
|
|
+ logger.error('[支付分] 预创建失败', error);
|
|
|
+ uni.showToast({
|
|
|
+ title: error?.message || '创建支付分订单失败',
|
|
|
+ icon: 'none',
|
|
|
+ });
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
- // 2. 拉起微信支付分确认页面
|
|
|
- logger.log('[支付分] 准备调起 openBusinessView, result:', JSON.stringify(result));
|
|
|
- logger.log('[支付分] mchId:', result.mchId, 'package:', result.package);
|
|
|
+/**
|
|
|
+ * 用户点击"确认授权"按钮,直接触发 wx.openBusinessView
|
|
|
+ * 关键:此调用在用户点击事件的同步调用链中,不会被微信拦截
|
|
|
+ */
|
|
|
+const invokePayScore = () => {
|
|
|
+ if (authLoading.value) return;
|
|
|
+ authLoading.value = true;
|
|
|
|
|
|
- // 检查 wx 对象是否可用
|
|
|
- if (typeof wx === 'undefined' || !wx.openBusinessView) {
|
|
|
- logger.error('[支付分] wx.openBusinessView 不可用');
|
|
|
- uni.showToast({ title: '当前环境不支持支付分,请在手机上尝试', icon: 'none' });
|
|
|
- return;
|
|
|
- }
|
|
|
+ logger.log('[支付分] 用户点击确认授权,调起 openBusinessView');
|
|
|
|
|
|
- wx.openBusinessView({
|
|
|
- businessType: 'payscore',
|
|
|
- extraData: {
|
|
|
- mch_id: result.mchId || '',
|
|
|
- package: result.package,
|
|
|
- },
|
|
|
- success: async () => {
|
|
|
- logger.log('[支付分] 用户确认成功,开始开门');
|
|
|
- await doOpenDoor(deviceId);
|
|
|
- },
|
|
|
- fail: (err: any) => {
|
|
|
- logger.error('[支付分] openBusinessView 失败或用户取消', JSON.stringify(err));
|
|
|
+ wx.openBusinessView({
|
|
|
+ businessType: 'payscore',
|
|
|
+ extraData: {
|
|
|
+ mch_id: authMchId.value,
|
|
|
+ package: authPackage.value,
|
|
|
+ },
|
|
|
+ success: async () => {
|
|
|
+ authLoading.value = false;
|
|
|
+ showAuthPopup.value = false;
|
|
|
+ logger.log('[支付分] 用户确认成功,开始开门');
|
|
|
+ await doOpenDoor(authDeviceId.value);
|
|
|
+ },
|
|
|
+ fail: (err: any) => {
|
|
|
+ authLoading.value = false;
|
|
|
+ logger.error('[支付分] openBusinessView 失败', JSON.stringify(err));
|
|
|
+ // 用户取消不弹窗,真正的错误才提示
|
|
|
+ if (err && err.errMsg && err.errMsg.indexOf('cancel') === -1) {
|
|
|
uni.showModal({
|
|
|
- title: '支付分调起失败',
|
|
|
- content: '错误详情: ' + JSON.stringify(err),
|
|
|
+ title: '授权失败',
|
|
|
+ content: JSON.stringify(err),
|
|
|
showCancel: false,
|
|
|
});
|
|
|
- },
|
|
|
- complete: () => {
|
|
|
- logger.log('[支付分] openBusinessView complete 回调触发');
|
|
|
- },
|
|
|
- });
|
|
|
- } catch (error: any) {
|
|
|
- uni.hideLoading();
|
|
|
- logger.error('预创建支付分订单失败', error);
|
|
|
- uni.showModal({
|
|
|
- title: '异常',
|
|
|
- content: error?.message || '创建支付分订单失败',
|
|
|
- showCancel: false,
|
|
|
- });
|
|
|
- }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 用户取消授权
|
|
|
+ */
|
|
|
+const cancelAuth = () => {
|
|
|
+ showAuthPopup.value = false;
|
|
|
+ logger.log('[支付分] 用户取消授权');
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* 执行开门操作
|
|
|
*/
|
|
|
const doOpenDoor = async (deviceId: string) => {
|
|
|
- uni.showLoading({
|
|
|
- title: '正在开门...',
|
|
|
- mask: true
|
|
|
- });
|
|
|
+ uni.showLoading({ title: '正在开门...', mask: true });
|
|
|
|
|
|
try {
|
|
|
const response = await scanDoor(deviceId);
|
|
|
|
|
|
uni.hideLoading();
|
|
|
- uni.showToast({
|
|
|
- title: '开门成功',
|
|
|
- icon: 'success'
|
|
|
- });
|
|
|
+ uni.showToast({ title: '开门成功', icon: 'success' });
|
|
|
|
|
|
- // 将设备信息存储到本地,供购物页面使用
|
|
|
uni.setStorageSync('currentDeviceId', response.deviceId);
|
|
|
uni.setStorageSync('currentOutTradeNo', response.outTradeNo);
|
|
|
uni.setStorageSync('currentOrderNo', response.orderNo);
|
|
|
uni.removeStorageSync('shoppingPollingActive');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
- uni.navigateTo({
|
|
|
- url: '/pages/shopping/shopping'
|
|
|
- });
|
|
|
+ uni.navigateTo({ url: '/pages/shopping/shopping' });
|
|
|
}, 1000);
|
|
|
} catch (error: any) {
|
|
|
uni.hideLoading();
|
|
|
@@ -248,24 +260,17 @@ const doOpenDoor = async (deviceId: string) => {
|
|
|
};
|
|
|
|
|
|
const goToMy = () => {
|
|
|
- uni.navigateTo({
|
|
|
- url: '/pages/my/my'
|
|
|
- })
|
|
|
+ uni.navigateTo({ url: '/pages/my/my' })
|
|
|
}
|
|
|
|
|
|
const goToOrders = () => {
|
|
|
- uni.navigateTo({
|
|
|
- url: '/pages/orders/orders'
|
|
|
- })
|
|
|
+ uni.navigateTo({ url: '/pages/orders/orders' })
|
|
|
}
|
|
|
|
|
|
const goToCouponCenter = () => {
|
|
|
- uni.navigateTo({
|
|
|
- url: '/pages/couponCenter/couponCenter'
|
|
|
- })
|
|
|
+ uni.navigateTo({ url: '/pages/couponCenter/couponCenter' })
|
|
|
}
|
|
|
|
|
|
-// 客服会话消息发送成功回调
|
|
|
const onContactSuccess = () => {
|
|
|
logger.log('[首页] 客服会话已打开')
|
|
|
}
|
|
|
@@ -288,8 +293,6 @@ const onContactSuccess = () => {
|
|
|
text-align: center;
|
|
|
padding: $spacing-xxl 0 $spacing-xl;
|
|
|
animation: slideUp 0.3s ease;
|
|
|
- will-change: transform, opacity;
|
|
|
- transform: translateZ(0);
|
|
|
|
|
|
.brand-title {
|
|
|
font-size: 56rpx;
|
|
|
@@ -313,9 +316,6 @@ const onContactSuccess = () => {
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
padding: $spacing-xl 0;
|
|
|
- animation: scaleIn 0.3s ease 0.1s both;
|
|
|
- will-change: transform, opacity;
|
|
|
- transform: translateZ(0);
|
|
|
}
|
|
|
|
|
|
.scan-button {
|
|
|
@@ -324,18 +324,17 @@ const onContactSuccess = () => {
|
|
|
height: 400rpx;
|
|
|
min-width: 400rpx;
|
|
|
min-height: 400rpx;
|
|
|
- aspect-ratio: 1 / 1;
|
|
|
border-radius: 50%;
|
|
|
background: transparent;
|
|
|
border: none;
|
|
|
padding: 0;
|
|
|
margin: 0;
|
|
|
overflow: hidden;
|
|
|
-
|
|
|
+
|
|
|
&::after {
|
|
|
border: none;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
&-inner {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
@@ -347,13 +346,13 @@ const onContactSuccess = () => {
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
transition: all $duration-normal $ease-out;
|
|
|
-
|
|
|
+
|
|
|
&:active {
|
|
|
transform: scale(0.95);
|
|
|
box-shadow: 0 4rpx 16rpx rgba(255, 193, 7, 0.3);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
&-ripple {
|
|
|
position: absolute;
|
|
|
top: 50%;
|
|
|
@@ -391,9 +390,6 @@ const onContactSuccess = () => {
|
|
|
align-items: center;
|
|
|
gap: $spacing-xl;
|
|
|
margin: $spacing-xxl 0;
|
|
|
- animation: slideUp 0.35s ease 0.15s both;
|
|
|
- will-change: transform, opacity;
|
|
|
- transform: translateZ(0);
|
|
|
|
|
|
&-item {
|
|
|
display: flex;
|
|
|
@@ -404,9 +400,7 @@ const onContactSuccess = () => {
|
|
|
position: relative;
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
- &:active {
|
|
|
- opacity: 0.8;
|
|
|
- }
|
|
|
+ &:active { opacity: 0.8; }
|
|
|
}
|
|
|
|
|
|
.action-icon {
|
|
|
@@ -450,13 +444,10 @@ const onContactSuccess = () => {
|
|
|
border-radius: $radius-lg;
|
|
|
padding: $spacing-lg;
|
|
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
|
|
- animation: slideUp 0.35s ease 0.2s both;
|
|
|
margin-top: auto;
|
|
|
width: 100%;
|
|
|
box-sizing: border-box;
|
|
|
- will-change: transform, opacity;
|
|
|
- transform: translateZ(0);
|
|
|
-
|
|
|
+
|
|
|
.info-item {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
@@ -490,4 +481,90 @@ const onContactSuccess = () => {
|
|
|
text-align: center;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+/* ========== 授权弹窗 ========== */
|
|
|
+.auth-overlay {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 999;
|
|
|
+ animation: fadeIn 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-card {
|
|
|
+ width: 600rpx;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 60rpx 40rpx 40rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ animation: scaleIn 0.25s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-icon-wrap {
|
|
|
+ width: 100rpx;
|
|
|
+ height: 100rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-icon {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-title {
|
|
|
+ font-size: 36rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: $color-text-primary;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-desc {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: $color-text-secondary;
|
|
|
+ text-align: center;
|
|
|
+ line-height: 1.6;
|
|
|
+ margin-bottom: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 24rpx;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.auth-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 88rpx;
|
|
|
+ line-height: 88rpx;
|
|
|
+ text-align: center;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ border: none;
|
|
|
+ padding: 0;
|
|
|
+
|
|
|
+ &::after { border: none; }
|
|
|
+
|
|
|
+ &.cancel {
|
|
|
+ background: #f5f5f5;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.confirm {
|
|
|
+ background: linear-gradient(135deg, $color-primary-light, $color-primary);
|
|
|
+ color: $color-text-primary;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ opacity: 0.85;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|