Преглед изворни кода

fix: 优化支付分授权流程UI,弹窗改为按钮内联切换

扫码后圆形按钮从"扫码开门"切换为绿色"确认支付分授权"按钮,
点击直接触发 wx.openBusinessView,满足微信用户手势要求。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline пре 2 недеља
родитељ
комит
e0c0d5cb50
2 измењених фајлова са 148 додато и 181 уклоњено
  1. 7 1
      haha-mp/src/manifest.json
  2. 141 180
      haha-mp/src/pages/index/index.vue

+ 7 - 1
haha-mp/src/manifest.json

@@ -54,7 +54,13 @@
         "setting" : {
             "urlCheck" : false
         },
-        "usingComponents" : true
+        "usingComponents" : true,
+        "requiredPrivateInfos" : [],
+        "permission" : {
+            "scope.userLocation" : {
+                "desc" : "获取位置信息"
+            }
+        }
     },
     "mp-alipay" : {
         "usingComponents" : true

+ 141 - 180
haha-mp/src/pages/index/index.vue

@@ -8,13 +8,23 @@
 
     <!-- 核心操作区 -->
     <view class="action-section">
-      <button class="scan-button" @click="scanCode">
+      <!-- 状态1:扫码开门 -->
+      <button v-if="!authReady" class="scan-button" @click="scanCode">
         <view class="scan-button-inner">
           <image class="scan-icon" src="/static/icons/scan.svg" mode="aspectFill"></image>
           <text class="scan-text">扫码开门</text>
         </view>
         <view class="scan-button-ripple"></view>
       </button>
+
+      <!-- 状态2:API 返回后,展示授权按钮 -->
+      <button v-else class="auth-button" @click="invokePayScore" :disabled="authBusy">
+        <view class="auth-button-inner">
+          <image class="auth-btn-icon" src="/static/icons/pay-socre-logo.png" mode="aspectFit"></image>
+          <text class="auth-btn-text">{{ authBusy ? '跳转中...' : '确认支付分授权' }}</text>
+          <text class="auth-btn-sub">点击后调起微信支付分</text>
+        </view>
+      </button>
     </view>
 
     <!-- 快捷功能区 -->
@@ -39,6 +49,11 @@
       </view>
     </view>
 
+    <!-- 授权就绪时,取消按钮 -->
+    <view v-if="authReady" class="cancel-auth-wrap">
+      <text class="cancel-auth-link" @click="cancelAuth">取消,返回扫码</text>
+    </view>
+
     <!-- 底部信息卡片 -->
     <view class="info-card">
       <view class="info-item">
@@ -57,21 +72,6 @@
         </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>
 
@@ -83,36 +83,30 @@ import { preCreatePayscoreOrder } from '../../api/payscore'
 import { isLoggedIn, getToken } from '../../utils/auth'
 import { logger } from '../../utils/logger'
 
-// 授权弹窗状态
-const showAuthPopup = ref(false)
+const authReady = ref(false)
+const authBusy = ref(false)
 const authDeviceId = ref('')
 const authPackage = ref('')
 const authMchId = ref('')
-const authLoading = ref(false)
 
-// 页面显示时检查 token
 onShow(() => {
   const token = getToken()
-  logger.log('[首页 onShow] token 状态:', token ? '存在' : '不存在')
+  logger.log('[首页 onShow] token:', token ? '存在' : '不存在')
 })
 
 const scanCode = async () => {
-  // 检查登录状态
   if (!isLoggedIn()) {
     uni.showModal({
       title: '提示',
       content: '请先登录后再扫码开门',
       showCancel: false,
       success: () => {
-        uni.reLaunch({
-          url: '/pages/login/login?redirect=/pages/index/index'
-        });
+        uni.reLaunch({ url: '/pages/login/login?redirect=/pages/index/index' });
       }
     });
     return;
   }
 
-  // 调用摄像头扫码
   uni.scanCode({
     success: async function (res) {
       logger.log('扫码结果:', res.result);
@@ -132,19 +126,15 @@ const scanCode = async () => {
           }
         }
 
-        if (!deviceId) {
-          throw new Error('无法解析设备ID');
-        }
+        if (!deviceId) throw new Error('无法解析设备ID');
+        logger.log('提取到设备ID:', deviceId);
       } catch (error) {
         logger.error('解析设备ID失败:', error);
         uni.showToast({ title: '二维码格式错误', icon: 'none' });
         return;
       }
 
-      logger.log('提取到设备ID:', deviceId);
-
-      // 预创建支付分订单,获取 package
-      await preparePayScoreAuth(deviceId);
+      await prepareAuth(deviceId);
     },
     fail: function (err) {
       logger.log('扫码取消:', err);
@@ -152,15 +142,11 @@ const scanCode = async () => {
   });
 };
 
-/**
- * 预创建支付分订单,成功后在页面内展示授权按钮
- */
-const preparePayScoreAuth = async (deviceId: string) => {
+const prepareAuth = async (deviceId: string) => {
   uni.showLoading({ title: '准备中...', mask: true });
 
   try {
     const result = await preCreatePayscoreOrder({ deviceId });
-
     uni.hideLoading();
 
     if (!result.success || !result.package) {
@@ -171,13 +157,12 @@ const preparePayScoreAuth = async (deviceId: string) => {
       return;
     }
 
-    logger.log('[支付分] 预创建成功, package:', result.package);
+    logger.log('[支付分] 预创建成功, package 已就绪');
 
-    // 存到状态中,等用户点击"确认授权"按钮
     authDeviceId.value = deviceId;
     authPackage.value = result.package;
     authMchId.value = result.mchId || '';
-    showAuthPopup.value = true;
+    authReady.value = true;
   } catch (error: any) {
     uni.hideLoading();
     logger.error('[支付分] 预创建失败', error);
@@ -189,59 +174,74 @@ const preparePayScoreAuth = async (deviceId: string) => {
 };
 
 /**
- * 用户点击"确认授权"按钮,直接触发 wx.openBusinessView
- * 关键:此调用在用户点击事件的同步调用链中,不会被微信拦截
+ * 用户点击按钮 → 直接触发 wx.openBusinessView
+ * 此函数由 button @click 同步调用,满足微信的用户手势要求
  */
 const invokePayScore = () => {
-  if (authLoading.value) return;
-  authLoading.value = true;
+  if (authBusy.value) return;
+  authBusy.value = true;
 
-  logger.log('[支付分] 用户点击确认授权,调起 openBusinessView');
+  const mchId = authMchId.value;
+  const pkg = authPackage.value;
+
+  logger.log('[支付分] invokePayScore - mchId:', mchId, 'package 长度:', pkg?.length);
+
+  // 确保所有参数有效
+  if (!pkg || !mchId) {
+    authBusy.value = false;
+    uni.showModal({
+      title: '参数异常',
+      content: 'package 或 mchId 为空,请重新扫码',
+      showCancel: false,
+    });
+    return;
+  }
 
   wx.openBusinessView({
     businessType: 'payscore',
     extraData: {
-      mch_id: authMchId.value,
-      package: authPackage.value,
+      mch_id: mchId,
+      package: pkg,
     },
-    success: async () => {
-      authLoading.value = false;
-      showAuthPopup.value = false;
-      logger.log('[支付分] 用户确认成功,开始开门');
-      await doOpenDoor(authDeviceId.value);
+    success: () => {
+      authBusy.value = false;
+      authReady.value = false;
+      logger.log('[支付分] 确认成功,开门');
+      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),
-          showCancel: false,
-        });
+      authBusy.value = false;
+      const errMsg = err?.errMsg || JSON.stringify(err);
+      logger.error('[支付分] openBusinessView fail:', errMsg);
+
+      // 用户取消不弹窗
+      if (errMsg.includes('cancel')) {
+        return;
       }
+
+      uni.showModal({
+        title: '调起支付分失败',
+        content: errMsg,
+        showCancel: false,
+      });
+    },
+    complete: () => {
+      logger.log('[支付分] openBusinessView complete');
     },
   });
 };
 
-/**
- * 用户取消授权
- */
 const cancelAuth = () => {
-  showAuthPopup.value = false;
-  logger.log('[支付分] 用户取消授权');
+  authReady.value = false;
+  authBusy.value = false;
+  logger.log('[支付分] 用户取消,返回扫码');
 };
 
-/**
- * 执行开门操作
- */
 const doOpenDoor = async (deviceId: string) => {
   uni.showLoading({ title: '正在开门...', mask: true });
 
   try {
     const response = await scanDoor(deviceId);
-
     uni.hideLoading();
     uni.showToast({ title: '开门成功', icon: 'success' });
 
@@ -259,21 +259,10 @@ const doOpenDoor = async (deviceId: string) => {
   }
 };
 
-const goToMy = () => {
-  uni.navigateTo({ url: '/pages/my/my' })
-}
-
-const goToOrders = () => {
-  uni.navigateTo({ url: '/pages/orders/orders' })
-}
-
-const goToCouponCenter = () => {
-  uni.navigateTo({ url: '/pages/couponCenter/couponCenter' })
-}
-
-const onContactSuccess = () => {
-  logger.log('[首页] 客服会话已打开')
-}
+const goToMy = () => uni.navigateTo({ url: '/pages/my/my' })
+const goToOrders = () => uni.navigateTo({ url: '/pages/orders/orders' })
+const goToCouponCenter = () => uni.navigateTo({ url: '/pages/couponCenter/couponCenter' })
+const onContactSuccess = () => logger.log('[首页] 客服会话已打开')
 </script>
 
 <style lang="scss">
@@ -288,11 +277,9 @@ const onContactSuccess = () => {
   box-sizing: border-box;
 }
 
-/* ========== 品牌区 ========== */
 .brand-section {
   text-align: center;
   padding: $spacing-xxl 0 $spacing-xl;
-  animation: slideUp 0.3s ease;
 
   .brand-title {
     font-size: 56rpx;
@@ -310,7 +297,7 @@ const onContactSuccess = () => {
   }
 }
 
-/* ========== 核心操作区 ========== */
+/* ========== 操作区 ========== */
 .action-section {
   display: flex;
   justify-content: center;
@@ -318,12 +305,11 @@ const onContactSuccess = () => {
   padding: $spacing-xl 0;
 }
 
+/* 扫码按钮 */
 .scan-button {
   position: relative;
   width: 400rpx;
   height: 400rpx;
-  min-width: 400rpx;
-  min-height: 400rpx;
   border-radius: 50%;
   background: transparent;
   border: none;
@@ -331,9 +317,7 @@ const onContactSuccess = () => {
   margin: 0;
   overflow: hidden;
 
-  &::after {
-    border: none;
-  }
+  &::after { border: none; }
 
   &-inner {
     width: 100%;
@@ -383,6 +367,69 @@ const onContactSuccess = () => {
   letter-spacing: 4rpx;
 }
 
+/* 授权按钮 */
+.auth-button {
+  width: 400rpx;
+  height: 400rpx;
+  border-radius: 50%;
+  background: transparent;
+  border: none;
+  padding: 0;
+
+  &::after { border: none; }
+
+  &-inner {
+    width: 100%;
+    height: 100%;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #07C160 0%, #06AD56 100%);
+    box-shadow: 0 8rpx 32rpx rgba(7, 193, 96, 0.35);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.3s ease;
+
+    &:active {
+      transform: scale(0.95);
+    }
+  }
+
+  &[disabled] .auth-button-inner {
+    opacity: 0.7;
+  }
+}
+
+.auth-btn-icon {
+  width: 100rpx;
+  height: 100rpx;
+  margin-bottom: $spacing-sm;
+}
+
+.auth-btn-text {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #fff;
+}
+
+.auth-btn-sub {
+  font-size: 22rpx;
+  color: rgba(255, 255, 255, 0.8);
+  margin-top: 8rpx;
+}
+
+/* 取消授权 */
+.cancel-auth-wrap {
+  text-align: center;
+  margin-top: -$spacing-lg;
+}
+
+.cancel-auth-link {
+  font-size: 26rpx;
+  color: $color-text-secondary;
+  padding: $spacing-sm $spacing-lg;
+}
+
 /* ========== 快捷功能区 ========== */
 .quick-actions {
   display: flex;
@@ -481,90 +528,4 @@ 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>