App.vue 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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. checkLoginStatus(options);
  13. });
  14. /**
  15. * 应用显示
  16. */
  17. onShow(() => {
  18. logger.log("App Show");
  19. });
  20. /**
  21. * 应用隐藏
  22. */
  23. onHide(() => {
  24. logger.log("App Hide");
  25. // 当应用隐藏时,清理购物页面的轮询状态
  26. const pollingActive = uni.getStorageSync('shoppingPollingActive');
  27. if (pollingActive === 'true') {
  28. logger.log('应用隐藏,清理购物页面轮询状态');
  29. uni.removeStorageSync('shoppingPollingActive');
  30. }
  31. });
  32. /**
  33. * 免登录白名单页面
  34. * 这些页面允许未登录用户直接访问
  35. */
  36. const LOGIN_WHITE_LIST = [
  37. 'pages/login/login',
  38. 'pages/products/products',
  39. ];
  40. /**
  41. * 验证token是否有效
  42. * @returns 'valid' | 'invalid' | 'unknown'
  43. * - valid: token确认有效
  44. * - invalid: 后端明确返回401,token已失效
  45. * - unknown: 网络异常/超时,无法确认(由后续API的401拦截兜底)
  46. */
  47. const verifyToken = (): Promise<'valid' | 'invalid' | 'unknown'> => {
  48. return new Promise((resolve) => {
  49. const token = uni.getStorageSync('accessToken');
  50. if (!token) {
  51. resolve('invalid');
  52. return;
  53. }
  54. let settled = false;
  55. const done = (result: 'valid' | 'invalid' | 'unknown') => {
  56. if (!settled) {
  57. settled = true;
  58. resolve(result);
  59. }
  60. };
  61. const url = `${API_CONFIG.baseUrl}/login/user-info`;
  62. uni.request({
  63. url,
  64. method: 'POST',
  65. header: {
  66. 'Content-Type': 'application/json',
  67. 'accessToken': token
  68. },
  69. timeout: 5000,
  70. success: (res: any) => {
  71. if (res.statusCode === 200) {
  72. const data = res.data;
  73. if (data && (data.code === 200 || data.code === 0)) {
  74. done('valid');
  75. } else if (data && data.code === 401) {
  76. logger.log('[App] token无效:', data.message);
  77. done('invalid');
  78. } else {
  79. logger.warn('[App] 验证token返回异常状态:', data?.code, data?.message);
  80. done('unknown');
  81. }
  82. } else if (res.statusCode === 401) {
  83. logger.log('[App] HTTP 401, token无效');
  84. done('invalid');
  85. } else {
  86. logger.warn('[App] 验证token HTTP状态异常:', res.statusCode);
  87. done('unknown');
  88. }
  89. },
  90. fail: (err: any) => {
  91. logger.warn('[App] 验证token请求失败:', err.errMsg);
  92. done('unknown');
  93. }
  94. });
  95. setTimeout(() => {
  96. logger.warn('[App] 验证token超时');
  97. done('unknown');
  98. }, 3000);
  99. });
  100. };
  101. /**
  102. * 检查登录状态
  103. * 如果未登录或token无效且当前不在白名单页面,则跳转到登录页
  104. */
  105. const checkLoginStatus = async (launchOptions?: any) => {
  106. const token = uni.getStorageSync('accessToken');
  107. // 优先从启动参数获取目标页面路径(onLaunch时getCurrentPages为空)
  108. let currentPath = launchOptions?.path || '';
  109. if (!currentPath) {
  110. const pages = getCurrentPages();
  111. const currentPage = pages[pages.length - 1];
  112. currentPath = currentPage ? currentPage.route : '';
  113. }
  114. logger.log('[App] 检查登录状态 - token:', token ? '已存在' : '不存在', ', 目标页面:', currentPath);
  115. const isWhiteListed = LOGIN_WHITE_LIST.some(path => currentPath.indexOf(path) !== -1);
  116. // 没有token
  117. if (!token) {
  118. if (!isWhiteListed) {
  119. logger.log('[App] 未登录,跳转到登录页');
  120. uni.reLaunch({ url: '/pages/login/login' });
  121. }
  122. return;
  123. }
  124. // 有token,向后端验证是否有效
  125. logger.log('[App] token存在,验证token有效性...');
  126. const verifyResult = await verifyToken();
  127. if (verifyResult === 'valid') {
  128. logger.log('[App] token有效');
  129. } else if (verifyResult === 'invalid') {
  130. // 后端明确返回401:token已失效,必须跳转登录
  131. if (!isWhiteListed) {
  132. logger.log('[App] token已失效,触发全局未授权处理');
  133. handleUnauthorized();
  134. } else {
  135. logger.log('[App] token已失效,清除登录数据(当前在白名单页面)');
  136. clearAuth();
  137. }
  138. } else {
  139. // unknown:网络异常或超时,不踢用户,交给后续API的401拦截处理
  140. logger.log('[App] token验证无法确认,保留token继续运行(由API请求401兜底)');
  141. }
  142. };
  143. </script>
  144. <style>
  145. /* 全局动画样式 */
  146. @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
  147. @keyframes slideUp { from { opacity: 0; transform: translateY(40rpx); } to { opacity: 1; transform: translateY(0); } }
  148. @keyframes slideDown { from { opacity: 0; transform: translateY(-40rpx); } to { opacity: 1; transform: translateY(0); } }
  149. @keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
  150. @keyframes pulse { 0%, 100% { transform: scale(1); opacity: 0.3; } 50% { transform: scale(1.05); opacity: 0.5; } }
  151. @keyframes bounce { 0% { transform: scale(0); opacity: 0; } 50% { transform: scale(1.1); } 100% { transform: scale(1); opacity: 1; } }
  152. @keyframes skeleton { 0%, 100% { background-color: #F5F5F5; } 50% { background-color: #EEEEEE; } }
  153. </style>