| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542 |
- <template>
- <view class="container">
- <!-- 顶部品牌区 -->
- <view class="brand-section">
- <text class="brand-title">AI零售柜</text>
- <text class="brand-slogan">智能视觉 · 即拿即走</text>
- </view>
- <!-- 核心操作区 -->
- <view class="action-section">
- <!-- 状态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>
- <!-- 快捷功能区 -->
- <view class="quick-actions">
- <view class="quick-action-item" @click="goToMy">
- <view class="action-icon">
- <image src="/static/icons/my.svg" mode="aspectFit"></image>
- </view>
- <text class="action-label">我的</text>
- </view>
- <view class="quick-action-item" @click="goToOrders">
- <view class="action-icon">
- <image src="/static/icons/orders.svg" mode="aspectFit"></image>
- </view>
- <text class="action-label">订单</text>
- </view>
- <view class="quick-action-item" @click="goToCouponCenter">
- <view class="action-icon">
- <image src="/static/icons/coupon.svg" mode="aspectFit"></image>
- </view>
- <text class="action-label">优惠券</text>
- </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">
- <image class="info-icon payscore-icon" src="/static/icons/pay-socre-logo.png" mode="aspectFit"></image>
- <text class="info-text">微信支付分 | 550分及以上优享</text>
- </view>
- <view class="info-item">
- <CustomerServiceButton
- mode="inline"
- title="首页-联系客服"
- :path="'/pages/index/index'"
- @contact="onContactSuccess"
- >
- <text class="info-icon">💬</text>
- <text class="info-text">在线客服</text>
- </CustomerServiceButton>
- </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 { isLoggedIn, getToken } from '../../utils/auth'
- import { logger } from '../../utils/logger'
- const authReady = ref(false)
- const authBusy = ref(false)
- const authDeviceId = ref('')
- const authPackage = ref('')
- onShow(() => {
- const token = getToken()
- 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' });
- }
- });
- return;
- }
- uni.scanCode({
- success: async function (res) {
- logger.log('扫码结果:', res.result);
- let deviceId = '';
- try {
- const urlPattern = /\/([A-Z0-9]+)(\?|$)/;
- const match = res.result.match(urlPattern);
- if (match && match[1]) {
- deviceId = match[1];
- } else {
- try {
- const qrData = JSON.parse(res.result);
- if (qrData.deviceId) deviceId = qrData.deviceId;
- } catch (e) {
- deviceId = res.result;
- }
- }
- if (!deviceId) throw new Error('无法解析设备ID');
- logger.log('提取到设备ID:', deviceId);
- } catch (error) {
- logger.error('解析设备ID失败:', error);
- uni.showToast({ title: '二维码格式错误', icon: 'none' });
- return;
- }
- await prepareAuth(deviceId);
- },
- fail: function (err) {
- logger.log('扫码取消:', err);
- }
- });
- };
- const prepareAuth = async (deviceId: string) => {
- uni.showLoading({ title: '准备中...', mask: true });
- try {
- const result = await preCreatePayscoreOrder({ deviceId });
- uni.hideLoading();
- if (!result.success || !result.package) {
- uni.showToast({
- title: result.errorMsg || '创建支付分订单失败',
- icon: 'none',
- });
- return;
- }
- logger.log('[支付分] 预创建成功, package 已就绪');
- authDeviceId.value = deviceId;
- authPackage.value = result.package;
- authReady.value = true;
- } catch (error: any) {
- uni.hideLoading();
- logger.error('[支付分] 预创建失败', error);
- uni.showToast({
- title: error?.message || '创建支付分订单失败',
- icon: 'none',
- });
- }
- };
- /**
- * 用户点击按钮 → 直接触发 wx.openBusinessView
- * 此函数由 button @click 同步调用,满足微信的用户手势要求
- */
- const invokePayScore = () => {
- if (authBusy.value) return;
- authBusy.value = true;
- const pkg = authPackage.value;
- logger.log('[支付分] invokePayScore, package 长度:', pkg?.length);
- if (!pkg) {
- authBusy.value = false;
- uni.showModal({
- title: '参数异常',
- content: 'package 为空,请重新扫码',
- showCancel: false,
- });
- return;
- }
- wx.openBusinessView({
- businessType: 'wxpayScoreUse',
- extraData: {
- package: pkg,
- },
- success: () => {
- authBusy.value = false;
- authReady.value = false;
- logger.log('[支付分] 确认成功,开门');
- doOpenDoor(authDeviceId.value);
- },
- fail: (err: any) => {
- 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 = () => {
- 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' });
- 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' });
- }, 1000);
- } catch (error: any) {
- uni.hideLoading();
- logger.error('开门失败:', error);
- }
- };
- 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">
- .container {
- min-height: 100vh;
- background: $color-bg-secondary;
- display: flex;
- flex-direction: column;
- padding: $spacing-xxl $spacing-lg;
- padding-bottom: calc(100rpx + constant(safe-area-inset-bottom));
- padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
- box-sizing: border-box;
- }
- .brand-section {
- text-align: center;
- padding: $spacing-xxl 0 $spacing-xl;
- .brand-title {
- font-size: 56rpx;
- font-weight: 300;
- color: $color-text-primary;
- letter-spacing: 8rpx;
- display: block;
- margin-bottom: $spacing-sm;
- }
- .brand-slogan {
- font-size: 24rpx;
- color: $color-text-secondary;
- letter-spacing: 4rpx;
- }
- }
- /* ========== 操作区 ========== */
- .action-section {
- display: flex;
- justify-content: center;
- align-items: center;
- padding: $spacing-xl 0;
- }
- /* 扫码按钮 */
- .scan-button {
- position: relative;
- width: 400rpx;
- height: 400rpx;
- min-width: 400rpx;
- max-width: 400rpx;
- min-height: 400rpx;
- max-height: 400rpx;
- border-radius: 50%;
- background: transparent;
- border: none;
- padding: 0;
- margin: 0;
- overflow: hidden;
- box-sizing: border-box;
- flex-shrink: 0;
- line-height: 1;
- &::after { border: none; }
- &-inner {
- width: 100%;
- height: 100%;
- border-radius: 50%;
- background: linear-gradient(135deg, $color-primary-light 0%, $color-primary 100%);
- box-shadow: $shadow-primary;
- display: flex;
- flex-direction: column;
- 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%;
- left: 50%;
- width: 100%;
- height: 100%;
- border-radius: 50%;
- background: $color-primary;
- transform: translate(-50%, -50%);
- animation: pulse 3s cubic-bezier(0.42, 0, 0.58, 1) infinite;
- opacity: 0.2;
- z-index: -1;
- }
- }
- .scan-icon {
- width: 160rpx;
- height: 160rpx;
- margin-bottom: $spacing-md;
- flex-shrink: 0;
- display: block;
- }
- .scan-text {
- font-size: 40rpx;
- font-weight: 600;
- color: $color-text-primary;
- letter-spacing: 4rpx;
- }
- /* 授权按钮 */
- .auth-button {
- width: 400rpx;
- height: 400rpx;
- min-width: 400rpx;
- max-width: 400rpx;
- min-height: 400rpx;
- max-height: 400rpx;
- border-radius: 50%;
- background: transparent;
- border: none;
- padding: 0;
- margin: 0;
- overflow: hidden;
- box-sizing: border-box;
- flex-shrink: 0;
- line-height: 1;
- &::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;
- justify-content: center;
- align-items: center;
- gap: $spacing-xl;
- margin: $spacing-xxl 0;
- &-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: $spacing-md;
- position: relative;
- flex: 0 0 auto;
- &:active { opacity: 0.8; }
- }
- .action-icon {
- width: 120rpx;
- height: 120rpx;
- background: $color-bg-primary;
- border-radius: $radius-lg;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
- margin-bottom: $spacing-sm;
- border: 2rpx solid $color-border;
- flex-shrink: 0;
- image {
- width: 64rpx;
- height: 64rpx;
- display: block;
- }
- &:active {
- box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
- border-color: $color-primary-light;
- }
- }
- .action-label {
- font-size: 28rpx;
- color: $color-text-primary;
- font-weight: 500;
- text-align: center;
- display: block;
- width: auto;
- }
- }
- /* ========== 底部信息卡片 ========== */
- .info-card {
- background: $color-bg-primary;
- border-radius: $radius-lg;
- padding: $spacing-lg;
- box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
- margin-top: auto;
- width: 100%;
- box-sizing: border-box;
- .info-item {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: $spacing-sm 0;
- min-height: 60rpx;
- &:not(:last-child) {
- border-bottom: 1rpx solid $color-border;
- padding-bottom: $spacing-md;
- margin-bottom: $spacing-md;
- }
- }
- .info-icon {
- font-size: 28rpx;
- margin-right: $spacing-sm;
- flex-shrink: 0;
- line-height: 1;
- &.payscore-icon {
- width: 36rpx;
- height: 36rpx;
- }
- }
- .info-text {
- font-size: 24rpx;
- color: $color-text-secondary;
- line-height: 1.5;
- text-align: center;
- }
- }
- </style>
|