feige996 8 месяцев назад
Родитель
Сommit
b90f039e1a

+ 1 - 0
.vscode/settings.json

@@ -71,6 +71,7 @@
   "cSpell.words": [
     "alova",
     "Aplipay",
+    "chooseavatar",
     "climblee",
     "commitlint",
     "dcloudio",

+ 3 - 0
codes/README.md

@@ -0,0 +1,3 @@
+# 参考代码
+
+部分代码片段,供参考。

+ 222 - 0
codes/router.txt

@@ -0,0 +1,222 @@
+import { getCurrentInstance, type App } from 'vue'
+import { useUserLoginStore } from '@/store/login'
+import { Pages } from './pages'
+import { LoginPopupViewer } from './loginPopupServices'
+import Loading from './Loading'
+
+/** 实时判断用户是否已登录(避免 computed 缓存) */
+function isUserLoggedIn(): boolean {
+  return useUserLoginStore().isLoggedIn
+}
+
+// 路由相关配置
+// 这里可以根据实际情况调整
+// 例如:需要登录验证的页面等
+// 以及登录页面、会员中心页面等
+
+// 需要登录验证的页面
+const authPages = [
+  Pages.USER_INFO_EDIT,
+  Pages.VIP_CENTER,
+  //Pages.PRODUCT_LIST,
+  //Pages.PRODUCT_DETAILS,
+  Pages.USER_ACCOUNT_SECURITY,
+  Pages.USER_EDIT_NICKNAME,
+  Pages.USER_ORDER_LIST,
+  Pages.USER_ORDER_DETAILS,
+  Pages.USER_MOBILE,
+  Pages.DISTRIBUTION_CENTER,
+  Pages.DISTRIBUTION_CENTER_DETAILS,
+  Pages.USER_MOBILE_CHANGE,
+  Pages.USER_PERSONAL_INFO,
+  Pages.USER_REMARK,
+  Pages.PRODUCT_ORDER_CONFIRM,
+  Pages.PRODUCT_PAY_MODE,
+  Pages.COUPON_CENTER,
+  Pages.COUPON_LIST,
+  Pages.CUSTOMER_SERVICE,
+  Pages.SHIPPING_ADDRESS_ADDED_OR_EDIT,
+  Pages.SHIPPING_ADDRESS_LIST,
+  Pages.USER_PASSWORD_CONFIG,
+  Pages.WITHDRAWAL,
+  Pages.WITHDRAWAL_RECORD_LIST,
+]
+
+/** 判断是否需要登录 */
+function getBasePath(url: string): string {
+  const index = url.indexOf('?')
+  return index !== -1 ? url.substring(0, index) : url
+}
+
+function isAuthRequired(url: string): boolean {
+  const cleanUrl = getBasePath(url)
+  console.log(`URL数据源:${authPages}`)
+  console.log(`URL原始值: ${url}`)
+  console.log(`URL过滤值: ${cleanUrl}`)
+  return authPages.some((item) => item === cleanUrl)
+}
+
+/** 缓存跳转路径 */
+function cacheRedirect(url: string) {
+  uni.setStorageSync('pending_redirect', url)
+}
+
+/** 读取并清除缓存跳转路径 */
+function consumeRedirect(): string | null {
+  const url = uni.getStorageSync('pending_redirect')
+  uni.removeStorageSync('pending_redirect')
+  return url || null
+}
+
+/** 路由核心跳转方法 */
+async function internalNavigate(
+  type: 'navigateTo' | 'redirectTo' | 'switchTab' | 'reLaunch',
+  url: string,
+  options: Record<string, any> = {},
+) {
+  const originUrl: string = url.startsWith('/') ? url : `/${url}`
+  const isAuthPage = isAuthRequired(originUrl)
+  console.log(`[Router][${type}] 跳转到:`, originUrl, '需要登录:', isAuthPage)
+  console.log(`[Router][${type}] 是否登录:`, isUserLoggedIn)
+
+  // 如果需要登录但未登录,则弹出登录框
+  if (isAuthPage && !isUserLoggedIn()) {
+    cacheRedirect(originUrl)
+    const loginResult = await LoginPopupViewer.open()
+    console.log(`[Router][${type}] 登录弹窗结果:`, loginResult)
+
+    // 如果登录失败(或用户取消),中断跳转
+    if (!loginResult) {
+      console.log(`[Router][${type}] 已终止跳转,原因:用户未登录或取消登录`)
+      Loading.showError({ msg: '已取消登录' })
+      return
+    }
+  }
+
+  // 登录状态已满足,可以安全跳转
+  try {
+    switch (type) {
+      case 'navigateTo':
+        return await uniNavigateTo(originUrl, options)
+      case 'redirectTo':
+        return await uniRedirectTo(originUrl, options)
+      case 'switchTab':
+        return await uniSwitchTab(originUrl)
+      case 'reLaunch':
+        return await uniReLaunch(originUrl)
+    }
+  } catch (error) {
+    console.error(`[Router][${type}] 跳转失败:`, error)
+  }
+}
+
+/** ✅ Promise 封装 uni API **/
+function uniNavigateTo(url: string, options: any) {
+  return new Promise((resolve, reject) => {
+    uni.navigateTo({
+      url,
+      ...options,
+      success: resolve,
+      fail: reject,
+    })
+  })
+}
+function uniRedirectTo(url: string, options: any) {
+  return new Promise((resolve, reject) => {
+    uni.redirectTo({
+      url,
+      ...options,
+      success: resolve,
+      fail: reject,
+    })
+  })
+}
+function uniSwitchTab(url: string) {
+  return new Promise((resolve, reject) => {
+    uni.switchTab({
+      url,
+      success: resolve,
+      fail: reject,
+    })
+  })
+}
+function uniReLaunch(url: string) {
+  return new Promise((resolve, reject) => {
+    uni.reLaunch({
+      url,
+      success: resolve,
+      fail: reject,
+    })
+  })
+}
+
+// ✅ Router API 对象
+// ✅ Router API 对象
+export const Router = {
+  // 页面跳转,支持登录鉴权
+  async navigateTo(opt: { url: string; requiresAuth?: boolean } & Record<string, any>) {
+    return await internalNavigate('navigateTo', opt.url, opt)
+  },
+
+  // 页面重定向,支持登录鉴权
+  async redirectTo(opt: { url: string; requiresAuth?: boolean } & Record<string, any>) {
+    return await internalNavigate('redirectTo', opt.url, opt)
+  },
+
+  // tab 页面切换
+  async switchTab(opt: { url: string }) {
+    return await internalNavigate('switchTab', opt.url, opt)
+  },
+
+  // 重新启动应用跳转
+  async reLaunch(opt: { url: string }) {
+    return await internalNavigate('reLaunch', opt.url, opt)
+  },
+
+  // 重定向别名
+  async replace(opt: { url: string; requiresAuth?: boolean } & Record<string, any>) {
+    return await internalNavigate('redirectTo', opt.url, opt)
+  },
+
+  // 返回上一级
+  async back(delta = 1) {
+    return await new Promise((resolve, reject) => {
+      uni.navigateBack({
+        delta,
+        success: resolve,
+        fail: reject,
+      })
+    })
+  },
+
+  consumeRedirect,
+}
+
+let cachedRouter: typeof Router | null = null
+
+/**
+ * ✅ 全局安全获取 $Router 实例(推荐使用)
+ */
+export function useRouter(): typeof Router {
+  if (cachedRouter) return cachedRouter
+
+  const instance = getCurrentInstance()
+  if (!instance) {
+    throw new Error('useRouter() 必须在 setup() 或生命周期中调用')
+  }
+
+  const router = instance.appContext.config.globalProperties.$Router
+  if (!router) {
+    throw new Error('$Router 尚未注入,请在 main.ts 中使用 app.use(RouterPlugin)')
+  }
+
+  cachedRouter = router
+  return router
+}
+
+/** ✅ 注册为全局插件 */
+export default {
+  install(app: App) {
+    app.config.globalProperties.$Router = Router
+  },
+}

+ 0 - 3
env/.env

@@ -8,9 +8,6 @@ VITE_WX_APPID = 'wxa2abb91f64032a2b'
 # https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
 VITE_APP_PUBLIC_BASE=/
 
-# 登录页面
-VITE_LOGIN_URL = '/pages/login/index'
-
 # 后台请求地址
 VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
 # 后台上传地址

+ 1 - 0
eslint.config.mjs

@@ -18,6 +18,7 @@ export default uniHelper({
     'src/service/app/**',
   ],
   rules: {
+    'no-useless-return': 'off',
     'no-console': 'off',
     'no-unused-vars': 'off',
     'vue/no-unused-refs': 'off',

+ 0 - 3
src/App.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
 import { navigateToInterceptor } from '@/router/interceptor'
-import { tabbarStore } from './tabbar/store'
 import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
 
 onLaunch((options) => {
@@ -17,8 +16,6 @@ onShow((options) => {
   else {
     navigateToInterceptor.invoke({ url: '/' })
   }
-  // 处理直接进入路由非首页时,tabbarIndex 不正确的问题
-  tabbarStore.setAutoCurIdx(options.path)
 })
 onHide(() => {
   console.log('App Hide')

+ 0 - 50
src/hooks/usePageAuth.ts

@@ -1,50 +0,0 @@
-import { onLoad } from '@dcloudio/uni-app'
-import { useUserStore } from '@/store'
-import { needLoginPages as _needLoginPages, getNeedLoginPages } from '@/utils'
-
-const loginRoute = import.meta.env.VITE_LOGIN_URL
-const isDev = import.meta.env.DEV
-function isLogined() {
-  const userStore = useUserStore()
-  return !!userStore.userInfo.username
-}
-// 检查当前页面是否需要登录
-export function usePageAuth() {
-  onLoad((options) => {
-    // 获取当前页面路径
-    const pages = getCurrentPages()
-    const currentPage = pages[pages.length - 1]
-    const currentPath = `/${currentPage.route}`
-
-    // 获取需要登录的页面列表
-    let needLoginPages: string[] = []
-    if (isDev) {
-      needLoginPages = getNeedLoginPages()
-    }
-    else {
-      needLoginPages = _needLoginPages
-    }
-
-    // 检查当前页面是否需要登录
-    const isNeedLogin = needLoginPages.includes(currentPath)
-    if (!isNeedLogin) {
-      return
-    }
-
-    const hasLogin = isLogined()
-    if (hasLogin) {
-      return true
-    }
-
-    // 构建重定向URL
-    const queryString = Object.entries(options || {})
-      .map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`)
-      .join('&')
-
-    const currentFullPath = queryString ? `${currentPath}?${queryString}` : currentPath
-    const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(currentFullPath)}`
-
-    // 重定向到登录页
-    uni.redirectTo({ url: redirectRoute })
-  })
-}

+ 1 - 1
src/http/alova.ts

@@ -30,7 +30,7 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
       }
       catch (error) {
         // 切换到登录页
-        await uni.reLaunch({ url: '/pages/common/login/index' })
+        await uni.reLaunch({ url: '/pages/login/login' })
         throw error
       }
     },

+ 23 - 0
src/login/README.md

@@ -0,0 +1,23 @@
+# 登录 说明
+
+## 登录 2种策略
+- 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
+- 默认需要登录策略: DEFAULT_NEED_LOGIN
+
+### 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
+进入任何页面都不需要登录,只有进入到黑名单中的页面/或者页面中某些动作需要登录,才需要登录。
+
+比如大部分2C的应用,美团、今日头条、抖音等,都可以直接浏览,只有点赞、评论、分享等操作或者去特殊页面(比如个人中心),才需要登录。
+
+### 默认需要登录策略: DEFAULT_NEED_LOGIN
+
+进入任何页面都需要登录,只有进入到白名单中的页面,才不需要登录。默认进入应用需要先去登录页。
+
+比如大部分2B和后台管理类的应用,比如企业微信、钉钉、飞书、内部报表系统、CMS系统等,都需要登录,只有登录后,才能使用。
+
+### EXCLUDE_PAGE_LIST
+`EXCLUDE_PAGE_LIST` 表示排除的路由列表。
+
+在 `默认无需登录策略: DEFAULT_NO_NEED_LOGIN` 中,只有路由在 `EXCLUDE_PAGE_LIST` 中,才需要登录,相当于黑名单。
+
+在 `默认需要登录策略: DEFAULT_NEED_LOGIN` 中,只有路由在 `EXCLUDE_PAGE_LIST` 中,才不需要登录,相当于白名单。

+ 15 - 0
src/login/config.ts

@@ -0,0 +1,15 @@
+export const LOGIN_STRATEGY_MAP = {
+  DEFAULT_NO_NEED_LOGIN: 0, // 黑名单策略,默认可以进入APP
+  DEFAULT_NEED_LOGIN: 1, // 白名单策略,默认不可以进入APP,需要强制登录
+}
+// 登录策略,默认使用`无需登录策略`,即默认不需要登录就可以访问
+export const LOGIN_STRATEGY = LOGIN_STRATEGY_MAP.DEFAULT_NO_NEED_LOGIN
+export const isNeedLogin = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN
+
+export const LOGIN_PAGE = '/pages/login/login'
+export const LOGIN_PAGE_LIST = [LOGIN_PAGE, '/pages/login/register']
+
+// 排除在外的列表,白名单策略指白名单列表,黑名单策略指黑名单列表
+export const EXCLUDE_PAGE_LIST = [
+  '/pages/xxx/index',
+]

+ 29 - 0
src/pages.json

@@ -36,6 +36,12 @@
         "selectedIconPath": "static/tabbar/exampleHL.png",
         "pagePath": "pages/about/about",
         "text": "关于"
+      },
+      {
+        "iconPath": "static/tabbar/personal.png",
+        "selectedIconPath": "static/tabbar/personalHL.png",
+        "pagePath": "pages/me/me",
+        "text": "个人"
       }
     ]
   },
@@ -72,6 +78,29 @@
       "style": {
         "navigationBarTitleText": "Vue Query 请求演示"
       }
+    },
+    {
+      "path": "pages/login/login",
+      "type": "page",
+      "layout": "default",
+      "style": {
+        "navigationBarTitleText": "登录"
+      }
+    },
+    {
+      "path": "pages/login/register",
+      "type": "page",
+      "layout": "default",
+      "style": {
+        "navigationBarTitleText": "注册"
+      }
+    },
+    {
+      "path": "pages/me/me",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "我的"
+      }
     }
   ],
   "subPackages": [

+ 20 - 0
src/pages/about/about.vue

@@ -17,6 +17,12 @@ import RequestComp from './components/request.vue'
 // testOxlint('oxlint')
 console.log('about')
 
+function toLogin() {
+  uni.navigateTo({
+    url: `/pages/login/login?redirect=${encodeURIComponent('/pages/about/about')}`,
+  })
+}
+
 function gotoAlova() {
   uni.navigateTo({
     url: '/pages/about/alova',
@@ -41,6 +47,12 @@ onReady(() => {
   console.log('onReady:', uniLayout.value) // onReady: Proxy(Object)
   console.log('onReady:', uniLayout.value.testUniLayoutExposedData) // onReady: testUniLayoutExposedData
 })
+
+function gotoTabbar() {
+  uni.switchTab({
+    url: '/pages/index/index',
+  })
+}
 </script>
 
 <template>
@@ -51,6 +63,9 @@ onReady(() => {
     <view class="my-2 text-center">
       <image src="/static/images/avatar.jpg" class="h-100px w-100px" />
     </view>
+    <button class="mt-4 w-40 text-center" @click="toLogin">
+      点击去登录页
+    </button>
     <RequestComp />
     <view class="mb-6 h-1px bg-#eee" />
     <view class="text-center">
@@ -58,6 +73,11 @@ onReady(() => {
         前往 alova 示例页面
       </button>
     </view>
+    <view class="text-center">
+      <button type="primary" size="mini" class="w-160px" @click="gotoTabbar">
+        切换tabbar
+      </button>
+    </view>
     <view class="text-center">
       <button type="primary" size="mini" class="w-160px" @click="gotoVueQuery">
         vue-query 示例页面

+ 9 - 0
src/pages/index/index.vue

@@ -50,6 +50,12 @@ onLoad(() => {
   console.log('项目作者:', author.value)
 })
 
+function toLogin() {
+  uni.navigateTo({
+    url: '/pages/login/login',
+  })
+}
+
 console.log('index')
 </script>
 
@@ -122,6 +128,9 @@ console.log('index')
         https://wot-design-uni.cn
       </text>
     </view>
+    <button class="mt-4 w-40 text-center" @click="toLogin">
+      点击去登录页
+    </button>
     <view class="h-6" />
   </view>
 </template>

+ 10 - 0
src/pages/login/README.md

@@ -0,0 +1,10 @@
+# 登录注册
+
+登录页 `login.vue` 对应路由是 `/pages/login/login`.
+注册页 `register.vue` 对应路由是 `/pages/login/register`.
+
+## 适用性
+
+登录注册页主要适用于 `h5` 和 `App`,因为小程序通常会使用平台提供的快捷登录。
+
+特殊情况也是可以用在 `小程序` 上的,如业务需要跨平台复用登录注册页时,所以主要还是看业务形态。

+ 66 - 0
src/pages/login/login.vue

@@ -0,0 +1,66 @@
+<route lang="jsonc" type="page">
+{
+  "layout": "default",
+  "style": {
+    "navigationBarTitleText": "登录"
+  }
+}
+</route>
+
+<script lang="ts" setup>
+import { useUserStore } from '@/store/user'
+import { tabbarList } from '@/tabbar/config'
+import { isPageTabbar } from '@/tabbar/store'
+import { ensureDecodeURIComponent } from '@/utils'
+
+const redirectUrl = ref('')
+onLoad((options) => {
+  console.log('login options', options)
+  if (options.redirect) {
+    redirectUrl.value = ensureDecodeURIComponent(options.redirect)
+  }
+  else {
+    redirectUrl.value = tabbarList[0].pagePath
+  }
+})
+const userStore = useUserStore()
+function doLogin() {
+  userStore.setUserInfo({
+    id: 1,
+    username: '菲鸽',
+    avatar: 'https://unibest.oss-cn-beijing.aliyuncs.com/avatar.png',
+    token: 'fake-token',
+  })
+  console.log(redirectUrl.value)
+  let path = redirectUrl.value
+  if (!path.startsWith('/')) {
+    path = `/${path}`
+  }
+  console.log('path:', path)
+  if (isPageTabbar(path)) {
+    uni.switchTab({
+      url: path,
+    })
+  }
+  else {
+    uni.redirectTo({
+      url: path,
+    })
+  }
+}
+</script>
+
+<template>
+  <view class="login">
+    <view class="text-center">
+      登录页
+    </view>
+    <button class="mt-4 w-40 text-center" @click="doLogin">
+      点击模拟登录
+    </button>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+//
+</style>

+ 35 - 0
src/pages/login/register.vue

@@ -0,0 +1,35 @@
+<route lang="jsonc" type="page">
+{
+  "layout": "default",
+  "style": {
+    "navigationBarTitleText": "注册"
+  }
+}
+</route>
+
+<script lang="ts" setup>
+function doRegister() {
+  uni.showToast({
+    title: '注册成功',
+  })
+  // 注册成功后跳转到登录页
+  uni.navigateTo({
+    url: '/pages/login/login',
+  })
+}
+</script>
+
+<template>
+  <view class="login">
+    <view class="text-center">
+      注册页
+    </view>
+    <button class="mt-4 w-40 text-center" @click="doRegister">
+      点击模拟注册
+    </button>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+//
+</style>

+ 219 - 0
src/pages/me/me.vue

@@ -0,0 +1,219 @@
+<route lang="json5">
+{
+  style: {
+    navigationBarTitleText: '我的',
+  },
+}
+</route>
+
+<script lang="ts" setup>
+import type { IUploadSuccessInfo } from '@/api/types/login'
+import { storeToRefs } from 'pinia'
+import { useUserStore } from '@/store'
+import { useUpload } from '@/utils/uploadFile'
+
+const userStore = useUserStore()
+// 使用storeToRefs解构userInfo
+const { userInfo } = storeToRefs(userStore)
+const hasLogin = ref(false)
+
+onShow((options) => {
+  hasLogin.value = !!uni.getStorageSync('token')
+  console.log('个人中心onShow', hasLogin.value, options)
+
+  hasLogin.value && useUserStore().getUserInfo()
+})
+// #ifndef MP-WEIXIN
+// 上传头像
+const { run: uploadAvatar } = useUpload<IUploadSuccessInfo>(
+  import.meta.env.VITE_UPLOAD_BASEURL,
+  {},
+  {
+    onSuccess: (res) => {
+      console.log('h5头像上传成功', res)
+      useUserStore().setUserAvatar(res.url)
+    },
+  },
+)
+// #endif
+
+// 微信小程序下登录
+async function handleLogin() {
+  // #ifdef MP-WEIXIN
+
+  // 微信登录
+  await userStore.wxLogin()
+  hasLogin.value = true
+  // #endif
+  // #ifndef MP-WEIXIN
+  uni.navigateTo({ url: '/pages/login/login' })
+  // #endif
+}
+
+// #ifdef MP-WEIXIN
+
+// 微信小程序下选择头像事件
+function onChooseAvatar(e: any) {
+  console.log('选择头像', e.detail)
+  const { avatarUrl } = e.detail
+  const { run } = useUpload<IUploadSuccessInfo>(
+    import.meta.env.VITE_UPLOAD_BASEURL,
+    {},
+    {
+      onSuccess: (res) => {
+        console.log('wx头像上传成功', res)
+        useUserStore().setUserAvatar(res.url)
+      },
+    },
+    avatarUrl,
+  )
+  run()
+}
+// #endif
+// #ifdef MP-WEIXIN
+// 微信小程序下设置用户名
+function getUserInfo(e: any) {
+  console.log(e.detail)
+}
+// #endif
+
+// 退出登录
+function handleLogout() {
+  uni.showModal({
+    title: '提示',
+    content: '确定要退出登录吗?',
+    success: (res) => {
+      if (res.confirm) {
+        // 清空用户信息
+        useUserStore().logout()
+        hasLogin.value = false
+        // 执行退出登录逻辑
+        uni.showToast({
+          title: '退出登录成功',
+          icon: 'success',
+        })
+        // #ifdef MP-WEIXIN
+        // 微信小程序,去首页
+        // uni.reLaunch({ url: '/pages/index/index' })
+        // #endif
+        // #ifndef MP-WEIXIN
+        // 非微信小程序,去登录页
+        // uni.navigateTo({ url: '/pages/login/login' })
+        // #endif
+      }
+    },
+  })
+}
+</script>
+
+<template>
+  <view class="profile-container">
+    <!-- 用户信息区域 -->
+    <view class="user-info-section">
+      <!-- #ifdef MP-WEIXIN -->
+      <button class="avatar-button" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
+        <image :src="userInfo.avatar" mode="scaleToFill" class="h-full w-full" />
+      </button>
+      <!-- #endif -->
+      <!-- #ifndef MP-WEIXIN -->
+      <view class="avatar-wrapper" @click="uploadAvatar">
+        <image :src="userInfo.avatar" mode="scaleToFill" class="h-full w-full" />
+      </view>
+      <!-- #endif -->
+      <view class="user-details">
+        <!-- #ifdef MP-WEIXIN -->
+        <input
+          v-model="userInfo.username"
+          type="nickname"
+          class="weui-input"
+          placeholder="请输入昵称"
+        >
+        <!-- #endif -->
+        <!-- #ifndef MP-WEIXIN -->
+        <view class="username">
+          {{ userInfo.username }}
+        </view>
+        <!-- #endif -->
+        <view class="user-id">
+          ID: {{ userInfo.id }}
+        </view>
+      </view>
+    </view>
+
+    <view class="mt-3 break-all px-3">
+      {{ JSON.stringify(userInfo, null, 2) }}
+    </view>
+
+    <view class="mt-20 px-3">
+      <view class="m-auto w-160px text-center">
+        <button v-if="hasLogin" type="warn" class="w-full" @click="handleLogout">
+          退出登录
+        </button>
+        <button v-else type="primary" class="w-full" @click="handleLogin">
+          登录
+        </button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+/* 基础样式 */
+.profile-container {
+  overflow: hidden;
+  font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
+  // background-color: #f7f8fa;
+}
+/* 用户信息区域 */
+.user-info-section {
+  display: flex;
+  align-items: center;
+  padding: 40rpx;
+  margin: 30rpx 30rpx 20rpx;
+  background-color: #fff;
+  border-radius: 24rpx;
+  box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
+  transition: all 0.3s ease;
+}
+
+.avatar-wrapper {
+  width: 160rpx;
+  height: 160rpx;
+  margin-right: 40rpx;
+  overflow: hidden;
+  border: 4rpx solid #f5f5f5;
+  border-radius: 50%;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+}
+.avatar-button {
+  height: 160rpx;
+  padding: 0;
+  margin-right: 40rpx;
+  overflow: hidden;
+  border: 4rpx solid #f5f5f5;
+  border-radius: 50%;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+}
+.user-details {
+  flex: 1;
+}
+
+.username {
+  margin-bottom: 12rpx;
+  font-size: 38rpx;
+  font-weight: 600;
+  color: #333;
+  letter-spacing: 0.5rpx;
+}
+
+.user-id {
+  font-size: 28rpx;
+  color: #666;
+}
+
+.user-created {
+  margin-top: 8rpx;
+  font-size: 24rpx;
+  color: #999;
+}
+</style>

+ 47 - 30
src/router/interceptor.ts

@@ -1,22 +1,13 @@
 /**
- * by 菲鸽 on 2024-03-06
+ * by 菲鸽 on 2025-08-19
  * 路由拦截,通常也是登录拦截
  * 可以设置路由白名单,或者黑名单,看业务需要选哪一个
  * 我这里应为大部分都可以随便进入,所以使用黑名单
  */
 import { useUserStore } from '@/store'
 import { tabbarStore } from '@/tabbar/store'
-import { needLoginPages as _needLoginPages, getLastPage, getNeedLoginPages } from '@/utils'
-
-// TODO Check
-const loginRoute = import.meta.env.VITE_LOGIN_URL
-
-function isLogined() {
-  const userStore = useUserStore()
-  return !!userStore.userInfo.username
-}
-
-const isDev = import.meta.env.DEV
+import { getLastPage } from '@/utils'
+import { EXCLUDE_PAGE_LIST, isNeedLogin, LOGIN_PAGE, LOGIN_PAGE_LIST } from '../login/config'
 
 // 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
 export const navigateToInterceptor = {
@@ -24,37 +15,63 @@ export const navigateToInterceptor = {
   // 增加对相对路径的处理,BY 网友 @ideal
   invoke({ url, query }: { url: string, query?: Record<string, string> }) {
     console.log(url) // /pages/route-interceptor/index?name=feige&age=30
-    console.log(query) // /pages/route-interceptor/index?name=feige&age=30
+    if (url === undefined) {
+      return
+    }
+    console.log(getCurrentPages())
+    if (getCurrentPages().length === 0) {
+      return
+    }
     let path = url.split('?')[0]
 
     // 处理相对路径
     if (!path.startsWith('/')) {
-      const currentPath = getLastPage().route
+      const currentPath = getLastPage()?.route || ''
       const normalizedCurrentPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}`
       const baseDir = normalizedCurrentPath.substring(0, normalizedCurrentPath.lastIndexOf('/'))
       path = `${baseDir}/${path}`
     }
 
-    let needLoginPages: string[] = []
-    // 为了防止开发时出现BUG,这里每次都获取一下。生产环境可以移到函数外,性能更好
-    if (isDev) {
-      needLoginPages = getNeedLoginPages()
+    // 处理直接进入路由非首页时,tabbarIndex 不正确的问题
+    tabbarStore.setAutoCurIdx(path)
+
+    if (LOGIN_PAGE_LIST.includes(path)) {
+      console.log('000')
+      return
     }
-    else {
-      needLoginPages = _needLoginPages
+
+    console.log('拦截器中得到的 path:', path)
+    console.log('拦截器中得到的 query:', query)
+    if (query) {
+      path += `?${Object.keys(query).map(key => `${key}=${query[key]}`).join('&')}`
     }
-    const isNeedLogin = needLoginPages.includes(path)
-    if (!isNeedLogin) {
-      return true
+    const redirectUrl = `${LOGIN_PAGE}?redirect=${encodeURIComponent(path)}`
+
+    const userStore = useUserStore()
+
+    // #region 1/2 需要登录的情况 ---------------------------
+    if (isNeedLogin) {
+      if (userStore.hasLogin) {
+        return
+      }
+      else {
+        if (EXCLUDE_PAGE_LIST.includes(path)) {
+          return
+        }
+        else {
+          uni.navigateTo({ url: redirectUrl })
+        }
+      }
     }
-    const hasLogin = isLogined()
-    if (hasLogin) {
-      return true
+    // #endregion 1/2 需要登录的情况 ---------------------------
+
+    // #region 2/2 不需要登录的情况 ---------------------------
+    else {
+      if (EXCLUDE_PAGE_LIST.includes(path)) {
+        uni.navigateTo({ url: redirectUrl })
+      }
     }
-    tabbarStore.restorePrevIdx()
-    const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}`
-    uni.navigateTo({ url: redirectRoute })
-    return false
+    // #endregion 2/2 不需要登录的情况 ---------------------------
   },
 }
 

+ 2 - 0
src/store/user.ts

@@ -133,9 +133,11 @@ export const useUserStore = defineStore(
       userToken,
       login,
       wxLogin,
+      setUserInfo,
       getUserInfo,
       setUserAvatar,
       logout,
+      hasLogin: computed(() => !!userToken.value.token),
       refreshToken,
     }
   },

+ 15 - 8
src/tabbar/config.ts

@@ -38,6 +38,12 @@ export const nativeTabbarList: NativeTabBarItem[] = [
     pagePath: 'pages/about/about',
     text: '关于',
   },
+  {
+    iconPath: 'static/tabbar/personal.png',
+    selectedIconPath: 'static/tabbar/personalHL.png',
+    pagePath: 'pages/me/me',
+    text: '个人',
+  },
 ]
 
 export interface CustomTabBarItem {
@@ -71,17 +77,18 @@ export const customTabbarList: CustomTabBarItem[] = [
     icon: 'i-carbon-code',
     // badge: 10,
   },
-
-  // {
-  //   pagePath: 'pages/mine/index',
-  //   text: '我的',
-  //   // 注意 iconfont 图标需要额外加上 'iconfont',如下
-  //   iconType: 'iconfont',
-  //   icon: 'iconfont icon-my',
-  // },
+  {
+    pagePath: 'pages/me/me',
+    text: '我的',
+    iconType: 'uniUi',
+    icon: 'contact',
+  },
   // {
   //   pagePath: 'pages/index/index',
   //   text: '首页',
+  // 注意 iconfont 图标需要额外加上 'iconfont',如下
+  // iconType: 'iconfont',
+  // icon: 'iconfont icon-my',
   //   // 使用 ‘image’时,需要配置 icon + iconActive 2张图片(不推荐)
   //   // 既然已经用了自定义tabbar了,就不建议用图片了,所以不推荐
   //   iconType: 'image',

+ 13 - 6
src/tabbar/store.ts

@@ -1,21 +1,28 @@
 import type { CustomTabBarItem } from './config'
-import { tabbarList as _tabbarList } from './config'
+import { tabbarList as _tabbarList, customTabbarEnable } from './config'
 
 // TODO 1/2: 中间的鼓包tabbarItem的开关
 const BULGE_ENABLE = true
 
 /** tabbarList 里面的 path 从 pages.config.ts 得到 */
-const tabbarList: CustomTabBarItem[] = _tabbarList.map(item => ({ ...item, pagePath: item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}` }))
+const tabbarList: CustomTabBarItem[] = _tabbarList.map(item => ({
+  ...item,
+  pagePath: item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}`,
+}))
 
-if (BULGE_ENABLE) {
-  if (tabbarList.length % 2 === 1) {
-    console.error('tabbar 数量必须是偶数,否则样式很奇怪!!')
+if (customTabbarEnable && BULGE_ENABLE) {
+  if (tabbarList.length % 2) {
+    console.error('有鼓包时 tabbar 数量必须是偶数,否则样式很奇怪!!')
   }
   tabbarList.splice(tabbarList.length / 2, 0, {
     isBulge: true,
   } as CustomTabBarItem)
 }
 
+export function isPageTabbar(path: string) {
+  return tabbarList.some(item => item.pagePath === path)
+}
+
 /**
  * 自定义 tabbar 的状态管理,原生 tabbar 无需关注本文件
  * tabbar 状态,增加 storageSync 保证刷新浏览器时在正确的 tabbar 页面
@@ -30,7 +37,7 @@ const tabbarStore = reactive({
   },
   setAutoCurIdx(path: string) {
     const index = tabbarList.findIndex(item => item.pagePath === path)
-    // console.log('index:', index, path)
+    console.log('index:', index, path)
     // console.log('tabbarList:', tabbarList)
     if (index === -1) {
       this.setCurIdx(0)

+ 5 - 17
src/utils/index.ts

@@ -12,7 +12,7 @@ export function getLastPage() {
 
 /**
  * 获取当前页面路由的 path 路径和 redirectPath 路径
- * path 如 '/pages/login/index'
+ * path 如 '/pages/login/login'
  * redirectPath 如 '/pages/demo/base/route-interceptor'
  */
 export function currRoute() {
@@ -25,8 +25,8 @@ export function currRoute() {
   // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱
   const { fullPath } = currRoute as { fullPath: string }
   // console.log(fullPath)
-  // eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
-  // eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
+  // eg: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
+  // eg: /pages/login/login?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
   return getUrlObj(fullPath)
 }
 
@@ -38,8 +38,8 @@ export function ensureDecodeURIComponent(url: string) {
 }
 /**
  * 解析 url 得到 path 和 query
- * 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
- * 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}}
+ * 比如输入url: /pages/login/login?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
+ * 输出: {path: /pages/login/login, query: {redirect: /pages/demo/base/route-interceptor}}
  */
 export function getUrlObj(url: string) {
   const [path, queryStr] = url.split('?')
@@ -110,18 +110,6 @@ export function getCurrentPageI18nKey() {
   return currPage.style.navigationBarTitleText
 }
 
-/**
- * 得到所有的需要登录的 pages,包括主包和分包的
- * 只得到 path 数组
- */
-export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map(page => page.path)
-
-/**
- * 得到所有的需要登录的 pages,包括主包和分包的
- * 只得到 path 数组
- */
-export const needLoginPages: string[] = getAllPages('needLogin').map(page => page.path)
-
 /**
  * 根据微信小程序当前环境,判断应该获取的 baseUrl
  */

+ 1 - 0
tsconfig.json

@@ -29,6 +29,7 @@
     "plugins": ["@uni-helper/uni-types/volar-plugin"]
   },
   "include": [
+    "package.json",
     "src/**/*.ts",
     "src/**/*.js",
     "src/**/*.d.ts",