App.vue 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <script setup lang="ts">
  2. import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
  3. import { API_CONFIG } from './utils/config';
  4. import { handleUnauthorized, clearAuth } from './utils/auth';
  5. import { logger } from './utils/logger';
  6. /**
  7. * 应用启动
  8. * 登录状态检查采用非阻塞模式,避免网络延迟拖慢启动速度
  9. */
  10. onLaunch((options: any) => {
  11. logger.log("App Launch, options:", JSON.stringify(options));
  12. checkBindingCode(options);
  13. checkLoginStatus(options);
  14. });
  15. /**
  16. * 检测启动参数中的补货员绑定码
  17. * 场景:通过管理后台生成的二维码进入小程序时,会携带 scene/bindingCode 参数
  18. */
  19. const checkBindingCode = (options: any) => {
  20. if (!options) return;
  21. // 检查 scene 场景值(小程序码场景)
  22. if (options.scene) {
  23. const scene = decodeURIComponent(options.scene);
  24. logger.log('[App] 检测到场景值:', scene);
  25. // 24位大写字母数字组合为绑定码格式
  26. if (/^[A-Z0-9]{24}$/.test(scene)) {
  27. logger.log('[App] 场景值为补货员绑定码');
  28. uni.navigateTo({
  29. url: '/pages/replenish/bind?code=' + scene
  30. });
  31. return;
  32. }
  33. }
  34. // 检查 query 参数中的绑定码
  35. if (options.query && options.query.bindingCode) {
  36. logger.log('[App] 检测到补货员绑定码:', options.query.bindingCode);
  37. uni.navigateTo({
  38. url: '/pages/replenish/bind?code=' + encodeURIComponent(options.query.bindingCode)
  39. });
  40. }
  41. };
  42. /**
  43. * 应用显示
  44. */
  45. onShow(() => {
  46. logger.log("App Show");
  47. });
  48. /**
  49. * 应用隐藏
  50. */
  51. onHide(() => {
  52. logger.log("App Hide");
  53. // 当应用隐藏时,清理购物页面的轮询状态
  54. const pollingActive = uni.getStorageSync('shoppingPollingActive');
  55. if (pollingActive === 'true') {
  56. logger.log('应用隐藏,清理购物页面轮询状态');
  57. uni.removeStorageSync('shoppingPollingActive');
  58. }
  59. });
  60. /**
  61. * 免登录白名单页面
  62. * 这些页面允许未登录用户直接访问
  63. */
  64. const LOGIN_WHITE_LIST = [
  65. 'pages/login/login',
  66. 'pages/products/products',
  67. 'pages/replenish/bind',
  68. ];
  69. /**
  70. * 验证token是否有效
  71. * 调用后端用户信息接口校验token,最多等待3秒,
  72. * 超时则假定token有效(由后续API请求的401拦截兜底)
  73. */
  74. const verifyToken = (): Promise<boolean> => {
  75. return new Promise((resolve) => {
  76. const token = uni.getStorageSync('accessToken');
  77. if (!token) {
  78. resolve(false);
  79. return;
  80. }
  81. let settled = false;
  82. const done = (result: boolean) => {
  83. if (!settled) {
  84. settled = true;
  85. resolve(result);
  86. }
  87. };
  88. const url = `${API_CONFIG.baseUrl}/login/user-info`;
  89. uni.request({
  90. url,
  91. method: 'POST',
  92. header: {
  93. 'Content-Type': 'application/json',
  94. 'accessToken': token
  95. },
  96. timeout: 5000,
  97. success: (res: any) => {
  98. if (res.statusCode === 200) {
  99. const data = res.data;
  100. if (data && (data.code === 200 || data.code === 0)) {
  101. done(true);
  102. } else if (data && data.code === 401) {
  103. logger.log('[App] token无效:', data.message);
  104. done(false);
  105. } else {
  106. logger.warn('[App] 验证token返回异常状态:', data?.code, data?.message);
  107. done(true);
  108. }
  109. } else {
  110. logger.warn('[App] 验证token网络异常, statusCode:', res.statusCode);
  111. done(true);
  112. }
  113. },
  114. fail: (err: any) => {
  115. logger.warn('[App] 验证token请求失败:', err.errMsg);
  116. done(true);
  117. }
  118. });
  119. // 竞速兜底:最多等3秒,超时假定token有效
  120. setTimeout(() => done(true), 3000);
  121. });
  122. };
  123. /**
  124. * 检查登录状态
  125. * 如果未登录或token无效且当前不在白名单页面,则跳转到登录页
  126. */
  127. const checkLoginStatus = async (launchOptions?: any) => {
  128. const token = uni.getStorageSync('accessToken');
  129. // 优先从启动参数获取目标页面路径(onLaunch时getCurrentPages为空)
  130. let currentPath = launchOptions?.path || '';
  131. if (!currentPath) {
  132. const pages = getCurrentPages();
  133. const currentPage = pages[pages.length - 1];
  134. currentPath = currentPage ? currentPage.route : '';
  135. }
  136. logger.log('[App] 检查登录状态 - token:', token ? '已存在' : '不存在', ', 目标页面:', currentPath);
  137. const isWhiteListed = LOGIN_WHITE_LIST.some(path => currentPath.indexOf(path) !== -1);
  138. // 没有token
  139. if (!token) {
  140. if (!isWhiteListed) {
  141. logger.log('[App] 未登录,跳转到登录页');
  142. uni.reLaunch({ url: '/pages/login/login' });
  143. }
  144. return;
  145. }
  146. // 有token,向后端验证是否有效
  147. logger.log('[App] token存在,验证token有效性...');
  148. const isValid = await verifyToken();
  149. if (!isValid && !isWhiteListed) {
  150. // token无效且不在白名单 -> 触发全局未授权处理
  151. logger.log('[App] token无效,触发全局未授权处理');
  152. handleUnauthorized();
  153. } else if (!isValid && isWhiteListed) {
  154. // 白名单页面只清除数据不跳转(不触发全局未授权处理的重定向)
  155. logger.log('[App] token无效,清除登录数据(当前在白名单页面)');
  156. clearAuth();
  157. } else {
  158. logger.log('[App] token有效');
  159. }
  160. };
  161. </script>
  162. <style>
  163. /* 全局动画样式 */
  164. @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
  165. @keyframes slideUp { from { opacity: 0; transform: translateY(40rpx); } to { opacity: 1; transform: translateY(0); } }
  166. @keyframes slideDown { from { opacity: 0; transform: translateY(-40rpx); } to { opacity: 1; transform: translateY(0); } }
  167. @keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
  168. @keyframes pulse { 0%, 100% { transform: scale(1); opacity: 0.3; } 50% { transform: scale(1.05); opacity: 0.5; } }
  169. @keyframes bounce { 0% { transform: scale(0); opacity: 0; } 50% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }
  170. @keyframes skeleton { 0%, 100% { background-color: #F5F5F5; } 50% { background-color: #EEEEEE; } }
  171. </style>