| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477 |
- <template>
- <view class="container">
- <!-- 顶部品牌区 -->
- <view class="brand-section">
- <text class="brand-title">AI零售柜</text>
- <text class="brand-slogan">智能视觉 · 即拿即走</text>
- </view>
- <!-- 核心操作区 -->
- <view class="action-section">
- <button 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>
- </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 class="info-card">
- <view class="info-item">
- <image class="info-icon payscore-icon" src="/static/icons/payscore.svg" 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, onMounted } from 'vue'
- import { onShow } from '@dcloudio/uni-app'
- import { scanDoor } from '../../api/device'
- import { checkPayscoreEnabled } from '../../api/payscore'
- import { getCouponCount } from '../../api/coupon'
- import { isLoggedIn, getToken } from '../../utils/auth'
- import { logger } from '../../utils/logger'
- // 页面显示时检查 token
- onShow(() => {
- const token = getToken()
- logger.log('[首页 onShow] token 状态:', token ? '存在' : '不存在')
- })
- // 处理支付分开通成功
- const onPayscoreEnabled = () => {
- logger.log('[首页] 用户已开通支付分')
- // 可以自动触发扫码
- setTimeout(() => {
- scanCode()
- }, 500)
- }
- // 定义暴露给其他页面的方法
- defineExpose({
- onPayscoreEnabled
- })
- const scanCode = async () => {
- // 检查登录状态
- if (!isLoggedIn()) {
- uni.showModal({
- title: '提示',
- content: '请先登录后再扫码开门',
- showCancel: false,
- success: () => {
- uni.reLaunch({
- url: '/pages/login/login?redirect=/pages/index/index'
- });
- }
- });
- return;
- }
- // 检查微信支付分开通状态
- try {
- const payscoreResult = await checkPayscoreEnabled()
- if (!payscoreResult.enabled) {
- // 未开通,跳转到开通页面
- uni.navigateTo({
- url: '/pages/payscore/enable'
- })
- return
- }
- } catch (error: any) {
- logger.error('检查支付分状态失败:', error)
- // 如果检查失败,也跳转到开通页面
- uni.navigateTo({
- url: '/pages/payscore/enable'
- })
- return
- }
- // 调用摄像头扫码
- uni.scanCode({
- 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;
- }
- } catch (e) {
- // 不是JSON格式,直接使用扫码结果作为deviceId
- deviceId = res.result;
- }
- }
- if (!deviceId) {
- throw new Error('无法解析设备ID');
- }
- } catch (error) {
- logger.error('解析设备ID失败:', error);
- uni.showToast({
- title: '二维码格式错误',
- icon: 'none'
- });
- return;
- }
- // 弹出选择弹窗:查看柜内商品 or 直接开门
- uni.showActionSheet({
- itemList: ['查看柜内商品', '直接开门'],
- success: async (actionRes) => {
- if (actionRes.tapIndex === 0) {
- // 查看柜内商品 -> 跳转到商品陈列页
- uni.navigateTo({
- url: '/pages/products/products?deviceId=' + deviceId
- });
- } else if (actionRes.tapIndex === 1) {
- // 直接开门 -> 执行现有开门逻辑
- await doOpenDoor(deviceId);
- }
- },
- fail: () => {
- logger.log('用户取消选择');
- }
- });
- },
- fail: function (err) {
- logger.log('扫码取消:', err);
- uni.showToast({
- title: '扫码取消',
- icon: 'none'
- });
- }
- });
- };
- /**
- * 执行开门操作
- */
- 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;
- animation: slideUp 0.3s ease;
- will-change: transform, opacity;
- transform: translateZ(0);
- .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;
- animation: scaleIn 0.3s ease 0.1s both;
- will-change: transform, opacity;
- transform: translateZ(0);
- }
- .scan-button {
- position: relative;
- width: 400rpx;
- 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%;
- 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;
- }
- /* ========== 快捷功能区 ========== */
- .quick-actions {
- display: flex;
- justify-content: center;
- 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;
- 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);
- 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;
- 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>
|