index.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. <template>
  2. <view class="container">
  3. <!-- 顶部品牌区 -->
  4. <view class="brand-section">
  5. <text class="brand-title">AI零售柜</text>
  6. <text class="brand-slogan">智能视觉 · 即拿即走</text>
  7. </view>
  8. <!-- 核心操作区 -->
  9. <view class="action-section">
  10. <button class="scan-button" @click="scanCode">
  11. <view class="scan-button-inner">
  12. <image class="scan-icon" src="/static/icons/scan.svg" mode="aspectFill"></image>
  13. <text class="scan-text">扫码开门</text>
  14. </view>
  15. <view class="scan-button-ripple"></view>
  16. </button>
  17. </view>
  18. <!-- 快捷功能区 -->
  19. <view class="quick-actions">
  20. <view class="quick-action-item" @click="goToMy">
  21. <view class="action-icon">
  22. <image src="/static/icons/my.svg" mode="aspectFit"></image>
  23. </view>
  24. <text class="action-label">我的</text>
  25. </view>
  26. <view class="quick-action-item" @click="goToOrders">
  27. <view class="action-icon">
  28. <image src="/static/icons/orders.svg" mode="aspectFit"></image>
  29. </view>
  30. <text class="action-label">订单</text>
  31. </view>
  32. <view class="quick-action-item" @click="goToCouponCenter">
  33. <view class="action-icon">
  34. <image src="/static/icons/coupon.svg" mode="aspectFit"></image>
  35. </view>
  36. <text class="action-label">优惠券</text>
  37. </view>
  38. </view>
  39. <!-- 底部信息卡片 -->
  40. <view class="info-card">
  41. <view class="info-item">
  42. <text class="info-icon">💚</text>
  43. <text class="info-text">微信支付分 550分及以上优享</text>
  44. </view>
  45. <view class="info-item">
  46. <text class="info-icon">📞</text>
  47. <text class="info-text">客服: 400-0759-515</text>
  48. </view>
  49. </view>
  50. </view>
  51. </template>
  52. <script setup lang="ts">
  53. import { ref, onMounted } from 'vue'
  54. import { onShow } from '@dcloudio/uni-app'
  55. import { scanDoor } from '../../api/device'
  56. import { checkPayscoreEnabled } from '../../api/payscore'
  57. import { getCouponCount } from '../../api/coupon'
  58. import { isLoggedIn, getToken } from '../../utils/auth'
  59. // 页面显示时检查 token
  60. onShow(() => {
  61. const token = getToken()
  62. console.log('[首页 onShow] token 状态:', token ? '存在' : '不存在')
  63. })
  64. // 处理支付分开通成功
  65. const onPayscoreEnabled = () => {
  66. console.log('[首页] 用户已开通支付分')
  67. // 可以自动触发扫码
  68. setTimeout(() => {
  69. scanCode()
  70. }, 500)
  71. }
  72. // 定义暴露给其他页面的方法
  73. defineExpose({
  74. onPayscoreEnabled
  75. })
  76. const scanCode = async () => {
  77. // 检查登录状态
  78. if (!isLoggedIn()) {
  79. uni.showModal({
  80. title: '提示',
  81. content: '请先登录后再扫码开门',
  82. showCancel: false,
  83. success: () => {
  84. uni.reLaunch({
  85. url: '/pages/login/login?redirect=/pages/index/index'
  86. });
  87. }
  88. });
  89. return;
  90. }
  91. // 检查微信支付分开通状态
  92. try {
  93. const payscoreResult = await checkPayscoreEnabled()
  94. if (!payscoreResult.enabled) {
  95. // 未开通,跳转到开通页面
  96. uni.navigateTo({
  97. url: '/pages/payscore/enable'
  98. })
  99. return
  100. }
  101. } catch (error: any) {
  102. console.error('检查支付分状态失败:', error)
  103. // 如果检查失败,也跳转到开通页面
  104. uni.navigateTo({
  105. url: '/pages/payscore/enable'
  106. })
  107. return
  108. }
  109. // 调用摄像头扫码
  110. uni.scanCode({
  111. success: async function (res) {
  112. console.log('扫码结果:', res.result);
  113. // 从扫码结果中解析设备ID
  114. // 二维码格式: https://hh.hahabianli.com/B142977?_wxpmm0=6009000C0000
  115. // 需要提取路径中的设备ID: B142977
  116. let deviceId = '';
  117. try {
  118. // 尝试从URL中提取deviceId
  119. const urlPattern = /\/([A-Z0-9]+)(\?|$)/;
  120. const match = res.result.match(urlPattern);
  121. if (match && match[1]) {
  122. deviceId = match[1];
  123. console.log('提取到设备ID:', deviceId);
  124. } else {
  125. // 如果不是URL格式,尝试解析JSON格式
  126. try {
  127. const qrData = JSON.parse(res.result);
  128. if (qrData.deviceId) {
  129. deviceId = qrData.deviceId;
  130. }
  131. } catch (e) {
  132. // 不是JSON格式,直接使用扫码结果作为deviceId
  133. deviceId = res.result;
  134. }
  135. }
  136. if (!deviceId) {
  137. throw new Error('无法解析设备ID');
  138. }
  139. } catch (error) {
  140. console.error('解析设备ID失败:', error);
  141. uni.showToast({
  142. title: '二维码格式错误',
  143. icon: 'none'
  144. });
  145. return;
  146. }
  147. // 显示加载提示
  148. uni.showLoading({
  149. title: '正在开门...',
  150. mask: true
  151. });
  152. try {
  153. // 调用真实接口扫码开门
  154. const response = await scanDoor(deviceId);
  155. // 隐藏加载提示
  156. uni.hideLoading();
  157. // 开门成功
  158. uni.showToast({
  159. title: '开门成功',
  160. icon: 'success'
  161. });
  162. // 将设备信息存储到本地,供购物页面使用
  163. uni.setStorageSync('currentDeviceId', response.deviceId);
  164. uni.setStorageSync('currentOutTradeNo', response.outTradeNo);
  165. uni.setStorageSync('currentOrderNo', response.orderNo);
  166. // 清理可能存在的旧轮询状态标记
  167. uni.removeStorageSync('shoppingPollingActive');
  168. // 跳转到购物进行中页面
  169. setTimeout(() => {
  170. uni.navigateTo({
  171. url: '/pages/shopping/shopping'
  172. });
  173. }, 1000);
  174. } catch (error: any) {
  175. // 隐藏加载提示
  176. uni.hideLoading();
  177. console.error('开门失败:', error);
  178. // 错误信息已经在request工具中显示,这里不需要重复显示
  179. }
  180. },
  181. fail: function (err) {
  182. console.log('扫码取消:', err);
  183. uni.showToast({
  184. title: '扫码取消',
  185. icon: 'none'
  186. });
  187. }
  188. });
  189. };
  190. const goToMy = () => {
  191. uni.vibrateShort({ type: 'light' })
  192. uni.navigateTo({
  193. url: '/pages/my/my'
  194. })
  195. }
  196. const goToOrders = () => {
  197. uni.vibrateShort({ type: 'light' })
  198. uni.navigateTo({
  199. url: '/pages/orders/orders'
  200. })
  201. }
  202. const goToCouponCenter = () => {
  203. uni.vibrateShort({ type: 'light' })
  204. uni.navigateTo({
  205. url: '/pages/couponCenter/couponCenter'
  206. })
  207. }
  208. </script>
  209. <style lang="scss">
  210. .container {
  211. min-height: 100vh;
  212. background: $color-bg-secondary;
  213. display: flex;
  214. flex-direction: column;
  215. padding: $spacing-xxl $spacing-lg;
  216. padding-bottom: calc(100rpx + constant(safe-area-inset-bottom));
  217. padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
  218. box-sizing: border-box;
  219. }
  220. /* ========== 品牌区 ========== */
  221. .brand-section {
  222. text-align: center;
  223. padding: $spacing-xxl 0 $spacing-xl;
  224. animation: slideUp 0.6s cubic-bezier(0.25, 0.1, 0.25, 1);
  225. .brand-title {
  226. font-size: 56rpx;
  227. font-weight: 300;
  228. color: $color-text-primary;
  229. letter-spacing: 8rpx;
  230. display: block;
  231. margin-bottom: $spacing-sm;
  232. }
  233. .brand-slogan {
  234. font-size: 24rpx;
  235. color: $color-text-secondary;
  236. letter-spacing: 4rpx;
  237. }
  238. }
  239. /* ========== 核心操作区 ========== */
  240. .action-section {
  241. display: flex;
  242. justify-content: center;
  243. align-items: center;
  244. padding: $spacing-xl 0;
  245. animation: scaleIn 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55);
  246. }
  247. .scan-button {
  248. position: relative;
  249. width: 400rpx;
  250. height: 400rpx;
  251. min-width: 400rpx;
  252. min-height: 400rpx;
  253. aspect-ratio: 1 / 1;
  254. border-radius: 50%;
  255. background: transparent;
  256. border: none;
  257. padding: 0;
  258. margin: 0;
  259. overflow: hidden;
  260. &::after {
  261. border: none;
  262. }
  263. &-inner {
  264. width: 100%;
  265. height: 100%;
  266. border-radius: 50%;
  267. background: linear-gradient(135deg, $color-primary-light 0%, $color-primary 100%);
  268. box-shadow: $shadow-primary;
  269. display: flex;
  270. flex-direction: column;
  271. align-items: center;
  272. justify-content: center;
  273. transition: all $duration-normal $ease-out;
  274. &:active {
  275. transform: scale(0.95);
  276. box-shadow: 0 4rpx 16rpx rgba(255, 193, 7, 0.3);
  277. }
  278. }
  279. &-ripple {
  280. position: absolute;
  281. top: 50%;
  282. left: 50%;
  283. width: 100%;
  284. height: 100%;
  285. border-radius: 50%;
  286. background: $color-primary;
  287. transform: translate(-50%, -50%);
  288. animation: pulse 2s cubic-bezier(0.42, 0, 0.58, 1) infinite;
  289. opacity: 0.3;
  290. z-index: -1;
  291. }
  292. }
  293. .scan-icon {
  294. width: 160rpx;
  295. height: 160rpx;
  296. margin-bottom: $spacing-md;
  297. flex-shrink: 0;
  298. display: block;
  299. }
  300. .scan-text {
  301. font-size: 40rpx;
  302. font-weight: 600;
  303. color: #1A1A1A;
  304. letter-spacing: 4rpx;
  305. }
  306. /* ========== 快捷功能区 ========== */
  307. .quick-actions {
  308. display: flex;
  309. justify-content: center;
  310. align-items: center;
  311. gap: $spacing-xl;
  312. margin: $spacing-xxl 0;
  313. animation: slideUp 1s cubic-bezier(0.25, 0.1, 0.25, 1);
  314. &-item {
  315. display: flex;
  316. flex-direction: column;
  317. align-items: center;
  318. justify-content: center;
  319. padding: $spacing-md;
  320. transition: all $duration-fast $ease-out;
  321. position: relative;
  322. flex: 0 0 auto;
  323. &:active {
  324. transform: scale(0.9);
  325. }
  326. }
  327. .action-icon {
  328. width: 120rpx;
  329. height: 120rpx;
  330. background: #ffffff;
  331. border-radius: $radius-lg;
  332. display: flex;
  333. align-items: center;
  334. justify-content: center;
  335. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
  336. margin-bottom: $spacing-sm;
  337. transition: all $duration-fast $ease-out;
  338. border: 2rpx solid #F0F0F0;
  339. flex-shrink: 0;
  340. image {
  341. width: 64rpx;
  342. height: 64rpx;
  343. display: block;
  344. }
  345. &:active {
  346. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
  347. transform: translateY(-4rpx);
  348. border-color: $color-primary-light;
  349. }
  350. }
  351. .action-label {
  352. font-size: 28rpx;
  353. color: $color-text-primary;
  354. font-weight: 500;
  355. text-align: center;
  356. display: block;
  357. width: auto;
  358. }
  359. }
  360. /* ========== 底部信息卡片 ========== */
  361. .info-card {
  362. background: $color-bg-primary;
  363. border-radius: $radius-lg;
  364. padding: $spacing-lg;
  365. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
  366. animation: slideUp 1.2s cubic-bezier(0.25, 0.1, 0.25, 1);
  367. margin-top: auto;
  368. width: 100%;
  369. box-sizing: border-box;
  370. .info-item {
  371. display: flex;
  372. align-items: center;
  373. padding: $spacing-sm 0;
  374. min-height: 60rpx;
  375. &:not(:last-child) {
  376. border-bottom: 1rpx solid $color-border;
  377. padding-bottom: $spacing-md;
  378. margin-bottom: $spacing-md;
  379. }
  380. }
  381. .info-icon {
  382. font-size: 28rpx;
  383. margin-right: $spacing-sm;
  384. flex-shrink: 0;
  385. line-height: 1;
  386. }
  387. .info-text {
  388. font-size: 24rpx;
  389. color: $color-text-secondary;
  390. line-height: 1.5;
  391. flex: 1;
  392. word-break: break-all;
  393. }
  394. }
  395. </style>