Browse Source

chore: update

Z.X.PING 7 months ago
parent
commit
faf18877b4

+ 16 - 16
.github/workflows/auto-merge.yml

@@ -115,23 +115,23 @@ jobs:
           git merge main --no-ff -m "Auto merge main into base-uview-plus"
           git push origin base-uview-plus
 
-  merge-to-base-tm-ui:
-    name: Merge main into base-tm-ui
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-          token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
+  # merge-to-base-tm-ui:
+  #   name: Merge main into base-tm-ui
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - name: Checkout repository
+  #       uses: actions/checkout@v4
+  #       with:
+  #         fetch-depth: 0
+  #         token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
 
-      - name: Merge main into base-tm-ui
-        run: |
-          git config user.name "GitHub Actions"
-          git config user.email "actions@github.com"
-          git checkout base-tm-ui
-          git merge main --no-ff -m "Auto merge main into base-tm-ui"
-          git push origin base-tm-ui
+  #     - name: Merge main into base-tm-ui
+  #       run: |
+  #         git config user.name "GitHub Actions"
+  #         git config user.email "actions@github.com"
+  #         git checkout base-tm-ui
+  #         git merge main --no-ff -m "Auto merge main into base-tm-ui"
+  #         git push origin base-tm-ui
 
   merge-to-base-skiyee-ui:
     name: Merge main into base-skiyee-ui

+ 4 - 2
.gitignore

@@ -28,10 +28,12 @@ docs/.vitepress/dist
 docs/.vitepress/cache
 
 src/types
+src/manifest.json
+src/pages.json
 
 # lock 文件还是不要了,我主要的版本写死就好了
-# pnpm-lock.yaml
-# package-lock.json
+pnpm-lock.yaml
+package-lock.json
 
 # TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
 # `git rm -r --cached .` 然后提交 commit 即可。

+ 14 - 1
.vscode/vue3.code-snippets

@@ -48,7 +48,20 @@
     "prefix": "sc",
     "body": [
       "<script lang=\"ts\" setup>",
-      "//$3",
+      "//$1",
+      "</script>\n"
+    ],
+  },
+  "Print unibest script with definePage": {
+    "scope": "vue",
+    "prefix": "scdp",
+    "body": [
+      "<script lang=\"ts\" setup>",
+      "definePage({",
+      "  style: {",
+      "    navigationBarTitleText: '$1',",
+      "  },",
+      "})",
       "</script>\n"
     ],
   },

+ 4 - 0
env/.env

@@ -13,9 +13,13 @@ VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
 # 后台上传地址
 VITE_UPLOAD_BASEURL = 'https://ukw0y1.laf.run/upload'
 
+# 注意,如果是微信小程序,还有一套请求地址的配置,在 `src/utils/index.ts` 中
+
 # h5是否需要配置代理
 VITE_APP_PROXY_ENABLE = true
 VITE_APP_PROXY_PREFIX = '/api'
+# 后端是否有统一前缀 /api,决定本地代码的时候是否需要去掉 /api 前缀。这里面默认是没有的,即前端会把/api 转发去掉
+VITE_SERVER_HAS_API_PREFIX = false
 
 # 第二个请求地址 (目前alova中可以使用)
 VITE_API_SECONDARY_URL = 'https://ukw0y1.laf.run'

+ 6 - 1
eslint.config.mjs

@@ -15,8 +15,9 @@ export default uniHelper({
     'src/pages.json',
     'src/manifest.json',
     // 忽略自动生成文件
-    'src/service/app/**',
+    'src/service/**',
   ],
+  // https://eslint-config.antfu.me/rules
   rules: {
     'no-useless-return': 'off',
     'no-console': 'off',
@@ -34,6 +35,10 @@ export default uniHelper({
         externalIgnores: ['text'],
       },
     ],
+    // vue SFC 调换顺序改这里
+    'vue/block-order': ['error', {
+      order: [['script', 'template'], 'style'],
+    }],
   },
   formatters: {
     /**

+ 1 - 1
index.html

@@ -14,7 +14,7 @@
           '" />',
       )
     </script>
-    <title>unibest</title>
+    <title>%VITE_APP_TITLE%</title>
     <!--preload-links-->
     <!--app-context-->
   </head>

+ 30 - 27
package.json

@@ -1,8 +1,10 @@
 {
   "name": "unibest",
   "type": "module",
-  "version": "3.12.4",
-  "unibest-version": "3.12.4",
+  "version": "3.15.1",
+  "unibest-version": "3.15.1",
+  "update-time": "2025-09-11",
+  "packageManager": "pnpm@10.10.0",
   "description": "unibest - 最好的 uniapp 开发模板",
   "generate-time": "用户创建项目时生成",
   "author": {
@@ -21,7 +23,7 @@
   },
   "engines": {
     "node": ">=22",
-    "pnpm": ">=9 <=10.12"
+    "pnpm": ">=9"
   },
   "scripts": {
     "preinstall": "npx only-allow pnpm",
@@ -86,29 +88,29 @@
     "build:quickapp-webview-union": "uni build -p quickapp-webview-union",
     "type-check": "vue-tsc --noEmit",
     "openapi-ts-request": "openapi-ts",
-    "prepare": "git init && husky",
+    "prepare": "git init && husky && node ./scripts/create-base-files.js",
     "lint": "eslint",
     "lint:fix": "eslint --fix"
   },
   "dependencies": {
     "@alova/adapter-uniapp": "^2.0.14",
     "@alova/shared": "^1.3.1",
-    "@dcloudio/uni-app": "3.0.0-4070520250711001",
-    "@dcloudio/uni-app-harmony": "3.0.0-4070520250711001",
-    "@dcloudio/uni-app-plus": "3.0.0-4070520250711001",
-    "@dcloudio/uni-components": "3.0.0-4070520250711001",
-    "@dcloudio/uni-h5": "3.0.0-4070520250711001",
-    "@dcloudio/uni-mp-alipay": "3.0.0-4070520250711001",
-    "@dcloudio/uni-mp-baidu": "3.0.0-4070520250711001",
-    "@dcloudio/uni-mp-harmony": "3.0.0-4070520250711001",
-    "@dcloudio/uni-mp-jd": "3.0.0-4070520250711001",
-    "@dcloudio/uni-mp-kuaishou": "3.0.0-4070520250711001",
-    "@dcloudio/uni-mp-lark": "3.0.0-4070520250711001",
-    "@dcloudio/uni-mp-qq": "3.0.0-4070520250711001",
-    "@dcloudio/uni-mp-toutiao": "3.0.0-4070520250711001",
-    "@dcloudio/uni-mp-weixin": "3.0.0-4070520250711001",
-    "@dcloudio/uni-mp-xhs": "3.0.0-4070520250711001",
-    "@dcloudio/uni-quickapp-webview": "3.0.0-4070520250711001",
+    "@dcloudio/uni-app": "3.0.0-4070620250821001",
+    "@dcloudio/uni-app-harmony": "3.0.0-4070620250821001",
+    "@dcloudio/uni-app-plus": "3.0.0-4070620250821001",
+    "@dcloudio/uni-components": "3.0.0-4070620250821001",
+    "@dcloudio/uni-h5": "3.0.0-4070620250821001",
+    "@dcloudio/uni-mp-alipay": "3.0.0-4070620250821001",
+    "@dcloudio/uni-mp-baidu": "3.0.0-4070620250821001",
+    "@dcloudio/uni-mp-harmony": "3.0.0-4070620250821001",
+    "@dcloudio/uni-mp-jd": "3.0.0-4070620250821001",
+    "@dcloudio/uni-mp-kuaishou": "3.0.0-4070620250821001",
+    "@dcloudio/uni-mp-lark": "3.0.0-4070620250821001",
+    "@dcloudio/uni-mp-qq": "3.0.0-4070620250821001",
+    "@dcloudio/uni-mp-toutiao": "3.0.0-4070620250821001",
+    "@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001",
+    "@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
+    "@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
     "@tanstack/vue-query": "^5.62.16",
     "abortcontroller-polyfill": "^1.7.8",
     "alova": "^3.3.3",
@@ -116,18 +118,18 @@
     "js-cookie": "^3.0.5",
     "pinia": "2.0.36",
     "pinia-plugin-persistedstate": "3.2.1",
-    "vue": "3.4.21",
+    "vue": "^3.4.21",
     "wot-design-uni": "^1.11.1",
     "z-paging": "2.8.7"
   },
   "devDependencies": {
     "@commitlint/cli": "^19.8.1",
     "@commitlint/config-conventional": "^19.8.1",
-    "@dcloudio/types": "^3.4.19",
-    "@dcloudio/uni-automator": "3.0.0-4070520250711001",
-    "@dcloudio/uni-cli-shared": "3.0.0-4070520250711001",
-    "@dcloudio/uni-stacktracey": "3.0.0-4070520250711001",
-    "@dcloudio/vite-plugin-uni": "3.0.0-4070520250711001",
+    "@dcloudio/types": "^3.4.8",
+    "@dcloudio/uni-automator": "3.0.0-4070620250821001",
+    "@dcloudio/uni-cli-shared": "3.0.0-4070620250821001",
+    "@dcloudio/uni-stacktracey": "3.0.0-4070620250821001",
+    "@dcloudio/vite-plugin-uni": "3.0.0-4070620250821001",
     "@esbuild/darwin-arm64": "0.20.2",
     "@esbuild/darwin-x64": "0.20.2",
     "@iconify-json/carbon": "^1.2.4",
@@ -146,7 +148,8 @@
     "@uni-ku/bundle-optimizer": "^1.3.3",
     "@uni-ku/root": "^1.3.4",
     "@unocss/eslint-plugin": "^66.2.3",
-    "@vue/runtime-core": "3.4.21",
+    "@unocss/preset-legacy-compat": "66.0.0",
+    "@vue/runtime-core": "^3.4.21",
     "@vue/tsconfig": "^0.1.3",
     "autoprefixer": "^10.4.20",
     "cross-env": "^10.0.0",

+ 3 - 1
pages.config.ts

@@ -1,3 +1,4 @@
+import { isH5 } from '@uni-helper/uni-env'
 import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
 import { tabBar } from './src/tabbar/config'
 
@@ -19,5 +20,6 @@ export default defineUniPages({
     },
   },
   // tabbar 的配置统一在 “./src/tabbar/config.ts” 文件中
-  tabBar: tabBar as any,
+  // 无tabbar模式下,h5 设置为 {} 为了防止浏览器报错导致白屏
+  tabBar: tabBar || (isH5 ? {} : undefined) as any,
 })

File diff suppressed because it is too large
+ 0 - 13674
pnpm-lock.yaml


+ 30 - 0
scripts/create-base-files.js

@@ -0,0 +1,30 @@
+// 生成 src/manifest.json 和 src/pages.json
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+// 获取当前文件的目录路径(替代 CommonJS 中的 __dirname)
+const __filename = fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+
+const manifest = {
+  name: 'unibest',
+  description: 'unibest - 最好的 uniapp 开发模板',
+  versionName: '1.0.0',
+  versionCode: '100',
+}
+
+const pages = {
+  pages: [
+    {
+      path: 'pages/index/index',
+      style: {
+        navigationBarTitleText: 'uni-app',
+      },
+    },
+  ],
+}
+
+// 使用修复后的 __dirname 来解析文件路径
+fs.writeFileSync(path.resolve(__dirname, '../src/manifest.json'), JSON.stringify(manifest, null, 2))
+fs.writeFileSync(path.resolve(__dirname, '../src/pages.json'), JSON.stringify(pages, null, 2))

+ 77 - 11
scripts/postupgrade.js

@@ -2,7 +2,14 @@
 // # 在升级完后,会自动添加很多无用依赖,这需要删除以减小依赖包体积
 // # 只需要执行下面的命令即可
 
-const { exec } = require('node:child_process')
+import { exec } from 'node:child_process'
+import { promisify } from 'node:util'
+
+// 日志控制开关,设置为 true 可以启用所有日志输出
+const FG_LOG_ENABLE = true
+
+// 将 exec 转换为返回 Promise 的函数
+const execPromise = promisify(exec)
 
 // 定义要执行的命令
 const dependencies = [
@@ -21,15 +28,74 @@ const dependencies = [
   'vue-i18n',
 ]
 
-// 使用exec执行命令
-exec(`pnpm un ${dependencies.join(' ')}`, (error, stdout, stderr) => {
-  if (error) {
-    // 如果有错误,打印错误信息
-    console.error(`执行出错: ${error}`)
-    return
+/**
+ * 带开关的日志输出函数
+ * @param {string} message 日志消息
+ * @param {string} type 日志类型 (log, error)
+ */
+function log(message, type = 'log') {
+  if (FG_LOG_ENABLE) {
+    if (type === 'error') {
+      console.error(message)
+    }
+    else {
+      console.log(message)
+    }
+  }
+}
+
+/**
+ * 卸载单个依赖包
+ * @param {string} dep 依赖包名
+ * @returns {Promise<boolean>} 是否成功卸载
+ */
+async function uninstallDependency(dep) {
+  try {
+    log(`开始卸载依赖: ${dep}`)
+    const { stdout, stderr } = await execPromise(`pnpm un ${dep}`)
+    if (stdout) {
+      log(`stdout [${dep}]: ${stdout}`)
+    }
+    if (stderr) {
+      log(`stderr [${dep}]: ${stderr}`, 'error')
+    }
+    log(`成功卸载依赖: ${dep}`)
+    return true
+  }
+  catch (error) {
+    // 单个依赖卸载失败不影响其他依赖
+    log(`卸载依赖 ${dep} 失败: ${error.message}`, 'error')
+    return false
+  }
+}
+
+/**
+ * 串行卸载所有依赖包
+ */
+async function uninstallAllDependencies() {
+  log(`开始串行卸载 ${dependencies.length} 个依赖包...`)
+
+  let successCount = 0
+  let failedCount = 0
+
+  // 串行执行所有卸载命令
+  for (const dep of dependencies) {
+    const success = await uninstallDependency(dep)
+    if (success) {
+      successCount++
+    }
+    else {
+      failedCount++
+    }
+
+    // 为了避免命令执行过快导致的问题,添加短暂延迟
+    await new Promise(resolve => setTimeout(resolve, 100))
   }
-  // 打印正常输出
-  console.log(`stdout: ${stdout}`)
-  // 如果有错误输出,也打印出来
-  console.error(`stderr: ${stderr}`)
+
+  log(`卸载操作完成: 成功 ${successCount} 个, 失败 ${failedCount} 个`)
+}
+
+// 执行串行卸载
+uninstallAllDependencies().catch((err) => {
+  log(`串行卸载过程中出现未捕获的错误: ${err}`, 'error')
 })

+ 2 - 2
src/api/login.ts

@@ -7,8 +7,8 @@ import { http } from '@/http/http'
 export interface ILoginForm {
   username: string
   password: string
-  code: string
-  uuid: string
+  code?: string
+  uuid?: string
 }
 
 /**

+ 2 - 0
src/env.d.ts

@@ -19,6 +19,8 @@ interface ImportMetaEnv {
   readonly VITE_APP_PROXY_ENABLE: 'true' | 'false'
   /** H5是否需要代理,需要的话有个前缀 */
   readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
+  /** 后端是否有统一前缀 /api */
+  readonly VITE_SERVER_HAS_API_PREFIX: 'true' | 'false'
   /** 认证模式,'single' | 'double' ==> 单token | 双token */
   readonly VITE_AUTH_MODE: 'single' | 'double'
   /** 上传图片地址 */

+ 2 - 2
src/hooks/useRequest.ts

@@ -23,7 +23,7 @@ interface IUseRequestReturn<T> {
  * @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
  */
 export default function useRequest<T>(
-  func: () => Promise<IResData<T>>,
+  func: () => Promise<T>,
   options: IUseRequestOptions<T> = { immediate: false },
 ): IUseRequestReturn<T> {
   const loading = ref(false)
@@ -33,7 +33,7 @@ export default function useRequest<T>(
     loading.value = true
     return func()
       .then((res) => {
-        data.value = res.data
+        data.value = res
         error.value = false
         return data.value
       })

+ 9 - 4
src/http/http.ts

@@ -1,9 +1,10 @@
 import type { IDoubleTokenRes } from '@/api/types/login'
-import type { CustomRequestOptions } from '@/http/types'
+import type { CustomRequestOptions, IResponse } from '@/http/types'
 import { nextTick } from 'vue'
 import { LOGIN_PAGE } from '@/router/config'
 import { useTokenStore } from '@/store/token'
 import { isDoubleTokenMode } from '@/utils'
+import { ResultEnum } from './tools/enum'
 
 // 刷新 token 状态管理
 let refreshing = false // 防止重复刷新 token 标识
@@ -11,7 +12,7 @@ let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
 
 export function http<T>(options: CustomRequestOptions) {
   // 1. 返回 Promise 对象
-  return new Promise<IResData<T>>((resolve, reject) => {
+  return new Promise<T>((resolve, reject) => {
     uni.request({
       ...options,
       dataType: 'json',
@@ -22,8 +23,12 @@ export function http<T>(options: CustomRequestOptions) {
       success: async (res) => {
         // 状态码 2xx,参考 axios 的设计
         if (res.statusCode >= 200 && res.statusCode < 300) {
-          // 2.1 提取核心数据 res.data
-          return resolve(res.data as IResData<T>)
+          // 2.1  处理业务逻辑错误
+          const { code, message, data } = res.data as IResponse<T>
+          if (code !== ResultEnum.Success) {
+            throw new Error(`请求错误[${code}]:${message}`)
+          }
+          return resolve(data as T)
         }
         const resData: IResData<T> = res.data as IResData<T>
         if ((res.statusCode === 401) || (resData.code === 401)) {

+ 4 - 3
src/http/interceptor.ts

@@ -1,5 +1,5 @@
 import type { CustomRequestOptions } from '@/http/types'
-import { useUserStore } from '@/store'
+import { useTokenStore } from '@/store'
 import { getEnvBaseUrl } from '@/utils'
 import { platform } from '@/utils/platform'
 import { stringifyQuery } from './tools/queryString'
@@ -47,8 +47,9 @@ const httpInterceptor = {
       ...options.header,
     }
     // 3. 添加 token 请求头标识
-    const userStore = useUserStore()
-    const { token } = userStore.userInfo as unknown as IUserToken
+    const tokenStore = useTokenStore()
+    const token = tokenStore.validToken
+
     if (token) {
       options.header.Authorization = `Bearer ${token}`
     }

+ 0 - 116
src/manifest.json

@@ -1,116 +0,0 @@
-{
-  "name": "unibest",
-  "appid": "__UNI__D1E5001",
-  "description": "",
-  "versionName": "1.0.0",
-  "versionCode": "100",
-  "transformPx": false,
-  "app-plus": {
-    "usingComponents": true,
-    "nvueStyleCompiler": "uni-app",
-    "compilerVersion": 3,
-    "splashscreen": {
-      "alwaysShowBeforeRender": true,
-      "waiting": true,
-      "autoclose": true,
-      "delay": 0
-    },
-    "modules": {},
-    "distribute": {
-      "android": {
-        "permissions": [
-          "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
-          "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
-          "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
-          "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
-          "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
-          "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
-          "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
-          "<uses-permission android:name=\"android.permission.CAMERA\"/>",
-          "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
-          "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
-          "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
-          "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
-          "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
-          "<uses-feature android:name=\"android.hardware.camera\"/>",
-          "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
-        ],
-        "minSdkVersion": 21,
-        "targetSdkVersion": 30,
-        "abiFilters": [
-          "armeabi-v7a",
-          "arm64-v8a"
-        ]
-      },
-      "ios": {},
-      "sdkConfigs": {},
-      "icons": {
-        "android": {
-          "hdpi": "static/app/icons/72x72.png",
-          "xhdpi": "static/app/icons/96x96.png",
-          "xxhdpi": "static/app/icons/144x144.png",
-          "xxxhdpi": "static/app/icons/192x192.png"
-        },
-        "ios": {
-          "appstore": "static/app/icons/1024x1024.png",
-          "ipad": {
-            "app": "static/app/icons/76x76.png",
-            "app@2x": "static/app/icons/152x152.png",
-            "notification": "static/app/icons/20x20.png",
-            "notification@2x": "static/app/icons/40x40.png",
-            "proapp@2x": "static/app/icons/167x167.png",
-            "settings": "static/app/icons/29x29.png",
-            "settings@2x": "static/app/icons/58x58.png",
-            "spotlight": "static/app/icons/40x40.png",
-            "spotlight@2x": "static/app/icons/80x80.png"
-          },
-          "iphone": {
-            "app@2x": "static/app/icons/120x120.png",
-            "app@3x": "static/app/icons/180x180.png",
-            "notification@2x": "static/app/icons/40x40.png",
-            "notification@3x": "static/app/icons/60x60.png",
-            "settings@2x": "static/app/icons/58x58.png",
-            "settings@3x": "static/app/icons/87x87.png",
-            "spotlight@2x": "static/app/icons/80x80.png",
-            "spotlight@3x": "static/app/icons/120x120.png"
-          }
-        }
-      }
-    },
-    "compatible": {
-      "ignoreVersion": true
-    }
-  },
-  "quickapp": {},
-  "mp-weixin": {
-    "appid": "wxa2abb91f64032a2b",
-    "setting": {
-      "urlCheck": false,
-      "es6": true,
-      "minified": true
-    },
-    "usingComponents": true,
-    "optimization": {
-      "subPackages": true
-    }
-  },
-  "mp-alipay": {
-    "usingComponents": true,
-    "styleIsolation": "shared"
-  },
-  "mp-baidu": {
-    "usingComponents": true
-  },
-  "mp-toutiao": {
-    "usingComponents": true
-  },
-  "uniStatistics": {
-    "enable": false
-  },
-  "vueVersion": "3",
-  "h5": {
-    "router": {
-      "base": "/"
-    }
-  }
-}

+ 0 - 108
src/pages.json

@@ -1,108 +0,0 @@
-{
-  "globalStyle": {
-    "navigationStyle": "default",
-    "navigationBarTitleText": "unibest",
-    "navigationBarBackgroundColor": "#f8f8f8",
-    "navigationBarTextStyle": "black",
-    "backgroundColor": "#FFFFFF"
-  },
-  "easycom": {
-    "autoscan": true,
-    "custom": {
-      "^fg-(.*)": "@/components/fg-$1/fg-$1.vue",
-      "^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue",
-      "^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue"
-    }
-  },
-  "tabBar": {
-    "custom": true,
-    "color": "#999999",
-    "selectedColor": "#018d71",
-    "backgroundColor": "#F8F8F8",
-    "borderStyle": "black",
-    "height": "50px",
-    "fontSize": "10px",
-    "iconWidth": "24px",
-    "spacing": "3px",
-    "list": [
-      {
-        "text": "首页",
-        "pagePath": "pages/index/index"
-      },
-      {
-        "text": "关于",
-        "pagePath": "pages/about/about"
-      },
-      {
-        "text": "我的",
-        "pagePath": "pages/me/me"
-      }
-    ]
-  },
-  "pages": [
-    {
-      "path": "pages/index/index",
-      "type": "home",
-      "style": {
-        "navigationStyle": "custom",
-        "navigationBarTitleText": "首页"
-      }
-    },
-    {
-      "path": "pages/about/about",
-      "type": "page",
-      "style": {
-        "navigationBarTitleText": "关于"
-      }
-    },
-    {
-      "path": "pages/about/alova",
-      "type": "page",
-      "style": {
-        "navigationBarTitleText": "Alova 演示"
-      }
-    },
-    {
-      "path": "pages/about/vue-query",
-      "type": "page",
-      "style": {
-        "navigationBarTitleText": "Vue Query 演示"
-      }
-    },
-    {
-      "path": "pages/login/login",
-      "type": "page",
-      "style": {
-        "navigationBarTitleText": "登录"
-      }
-    },
-    {
-      "path": "pages/login/register",
-      "type": "page",
-      "style": {
-        "navigationBarTitleText": "注册"
-      }
-    },
-    {
-      "path": "pages/me/me",
-      "type": "page",
-      "style": {
-        "navigationBarTitleText": "我的"
-      }
-    }
-  ],
-  "subPackages": [
-    {
-      "root": "pages-sub",
-      "pages": [
-        {
-          "path": "demo/index",
-          "type": "page",
-          "style": {
-            "navigationBarTitleText": "分包页面"
-          }
-        }
-      ]
-    }
-  ]
-}

+ 47 - 16
src/pages/about/about.vue

@@ -1,6 +1,7 @@
 <script lang="ts" setup>
 import { isApp, isAppAndroid, isAppHarmony, isAppIOS, isAppPlus, isH5, isMpWeixin, isWeb } from '@uni-helper/uni-env'
 import { LOGIN_PAGE } from '@/router/config'
+import { useTokenStore } from '@/store'
 import { tabbarStore } from '@/tabbar/store'
 import RequestComp from './components/request.vue'
 import VBindCss from './components/VBindCss.vue'
@@ -9,16 +10,46 @@ definePage({
   style: {
     navigationBarTitleText: '关于',
   },
+  // 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 arc/router 文件夹
+  excludeLoginPath: false,
 })
 
+const tokenStore = useTokenStore()
 // 浏览器打印 isH5为true, isWeb为false,大家尽量用 isH5
 console.log({ isApp, isAppAndroid, isAppHarmony, isAppIOS, isAppPlus, isH5, isMpWeixin, isWeb })
 
-function toLogin() {
+function gotoLogin() {
+  if (tokenStore.hasLogin) {
+    uni.showToast({
+      title: '已登录,不能去登录页',
+      icon: 'none',
+    })
+    return
+  }
   uni.navigateTo({
-    url: `${LOGIN_PAGE}?redirect=${encodeURIComponent('/pages/about/about')}`,
+    url: `${LOGIN_PAGE}?redirect=${encodeURIComponent('/pages/about/about?a=1&b=2')}`,
   })
 }
+function logout() {
+  // 清空用户信息
+  tokenStore.logout()
+  // 执行退出登录逻辑
+  uni.showToast({
+    title: '退出登录成功',
+    icon: 'success',
+  })
+}
+
+function gotoTabbar() {
+  uni.switchTab({
+    url: '/pages/index/index',
+  })
+}
+// #region setTabbarBadge
+function setTabbarBadge() {
+  tabbarStore.setTabbarItemBadge(1, 100)
+}
+// #endregion
 
 function gotoAlova() {
   uni.navigateTo({
@@ -35,6 +66,7 @@ function gotoSubPage() {
     url: '/pages-sub/demo/index',
   })
 }
+
 // uniLayout里面的变量通过 expose 暴露出来后可以在 onReady 钩子获取到(onLoad 钩子不行)
 const uniLayout = ref()
 onLoad(() => {
@@ -50,17 +82,6 @@ onShow(() => {
   console.log('onShow:', uniLayout.value?.testUniLayoutExposedData) // onReady: testUniLayoutExposedData
 })
 
-function gotoTabbar() {
-  uni.switchTab({
-    url: '/pages/index/index',
-  })
-}
-// #region setTabbarBadge
-function setTabbarBadge() {
-  tabbarStore.setTabbarItemBadge(1, 100)
-}
-// #endregion
-
 const uniKuRoot = ref()
 // 结论:(同上)第一次通过onShow获取不到,但是可以通过 onReady获取到,后面就可以通过onShow获取到了
 onReady(() => {
@@ -72,6 +93,8 @@ onShow(() => {
 </script>
 
 <template root="uniKuRoot">
+  <!-- page-meta 使用范例 -->
+  <page-meta page-style="overflow: auto" />
   <view>
     <view class="mt-8 text-center text-xl text-gray-400">
       请求调用、unocss、static图片
@@ -79,9 +102,17 @@ onShow(() => {
     <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>
+    <view class="my-2 text-center">
+      当前是否登录:{{ tokenStore.hasLogin }}
+    </view>
+    <view class="m-auto max-w-600px flex items-center">
+      <button class="mt-4 w-40 text-center" @click="gotoLogin">
+        点击去登录页
+      </button>
+      <button class="mt-4 w-40 text-center" @click="logout">
+        点击退出登录
+      </button>
+    </view>
     <button class="mt-4 w-60 text-center" @click="setTabbarBadge">
       设置tabbarBadge
     </button>

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

@@ -1,5 +1,4 @@
 <script lang="ts" setup>
-import { LOGIN_PAGE } from '@/router/config'
 import { useThemeStore } from '@/store'
 import { safeAreaInsets } from '@/utils/systemInfo'
 
@@ -18,7 +17,6 @@ definePage({
 
 const themeStore = useThemeStore()
 
-const author = ref('菲鸽')
 const description = ref(
   'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite5 + UnoCss + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
 )
@@ -27,12 +25,6 @@ console.log('index/index 首页打印了')
 onLoad(() => {
   console.log('测试 uni API 自动引入: onLoad')
 })
-
-function toLogin() {
-  uni.navigateTo({
-    url: LOGIN_PAGE,
-  })
-}
 </script>
 
 <template>
@@ -104,9 +96,6 @@ function toLogin() {
         https://wot-design-uni.cn
       </text>
     </view>
-    <button class="mt-4 w-40 text-center" @click="toLogin">
-      点击去登录页
-    </button>
     <view class="h-6" />
   </view>
 </template>

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

@@ -0,0 +1,20 @@
+# 登录页
+需要输入账号、密码/验证码的登录页。
+
+## 适用性
+
+本页面主要用于 `h5` 和 `APP`。
+
+小程序通常有平台的登录方式 `uni.login` 通常用不到登录页,所以不适用于 `小程序`。(即默认情况下,小程序环境是不会走登录拦截逻辑的。)
+
+但是如果您的小程序也需要现实的 `登录页` 那也是可以使用的。
+
+在 `src/router/config.ts` 中有一个变量 `LOGIN_PAGE_ENABLE_IN_MP` 来控制是否在小程序中使用 `H5的登录页`。
+
+更多信息请看 `src/router` 文件夹的内容。
+
+## 登录跳转
+
+目前登录的跳转逻辑主要在 `src/router/interceptor.ts` 和 `src/pages/login/login.vue` 里面,默认会在登录后自动重定向到来源/配置的页面。
+
+如果与您的业务不符,您可以自行修改。

+ 34 - 8
src/pages/login/login.vue

@@ -1,4 +1,5 @@
 <script lang="ts" setup>
+import { useTokenStore } from '@/store/token'
 import { useUserStore } from '@/store/user'
 import { tabbarList } from '@/tabbar/config'
 import { isPageTabbar } from '@/tabbar/store'
@@ -24,14 +25,39 @@ onLoad((options) => {
 })
 
 const userStore = useUserStore()
-function doLogin() {
-  userStore.setUserInfo({
-    userId: 123456,
-    username: 'abc123456',
-    nickname: '菲鸽',
-    avatar: 'https://oss.laf.run/ukw0y1-site/avatar.jpg',
-  })
-  console.log(redirectUrl.value)
+const tokenStore = useTokenStore()
+async function doLogin() {
+  if (tokenStore.hasLogin) {
+    uni.navigateBack()
+    return
+  }
+  try {
+    // 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口(各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
+    // 1/2 调用接口回来后设置token信息
+    // 这里用单token来模拟
+    tokenStore.setTokenInfo({
+      token: '123456',
+      expiresIn: 60 * 60 * 24 * 7,
+    })
+
+    // 2/2 调用接口回来后设置用户信息
+    // const res = await login({
+    //   username: '菲鸽',
+    //   password: '123456',
+    // })
+    // console.log('接口拿到的登录信息:', res)
+    userStore.setUserInfo({
+      userId: 123456,
+      username: 'abc123456',
+      nickname: '菲鸽',
+      avatar: 'https://oss.laf.run/ukw0y1-site/avatar.jpg',
+    })
+
+    console.log(redirectUrl.value)
+  }
+  catch (error) {
+    console.log('登录失败', error)
+  }
   let path = redirectUrl.value
   if (!path.startsWith('/')) {
     path = `/${path}`

+ 5 - 3
src/pages/me/me.vue

@@ -39,7 +39,9 @@ async function handleLogin() {
   await tokenStore.wxLogin()
   // #endif
   // #ifndef MP-WEIXIN
-  uni.navigateTo({ url: LOGIN_PAGE })
+  uni.navigateTo({
+    url: `${LOGIN_PAGE}?redirect=${encodeURIComponent('/pages/me/me')}`,
+  })
   // #endif
 }
 
@@ -90,7 +92,7 @@ function handleLogout() {
         // #endif
         // #ifndef MP-WEIXIN
         // 非微信小程序,去登录页
-        uni.navigateTo({ url: LOGIN_PAGE })
+        // uni.navigateTo({ url: LOGIN_PAGE })
         // #endif
       }
     },
@@ -127,7 +129,7 @@ function handleLogout() {
         </view>
         <!-- #endif -->
         <view class="user-id">
-          ID: {{ userInfo.id }}
+          ID: {{ userInfo.userId }}
         </view>
       </view>
     </view>

+ 26 - 8
src/router/README.md

@@ -15,13 +15,31 @@
 
 比如大部分2B和后台管理类的应用,比如企业微信、钉钉、飞书、内部报表系统、CMS系统等,都需要登录,只有登录后,才能使用。
 
-### EXCLUDE_PAGE_LIST
-`EXCLUDE_PAGE_LIST` 表示排除的路由列表。
-
-在 `默认无需登录策略: DEFAULT_NO_NEED_LOGIN` 中,只有路由在 `EXCLUDE_PAGE_LIST` 中,才需要登录,相当于黑名单。
-
-在 `默认需要登录策略: DEFAULT_NEED_LOGIN` 中,只有路由在 `EXCLUDE_PAGE_LIST` 中,才不需要登录,相当于白名单。
-
+### EXCLUDE_LOGIN_PATH_LIST
+`EXCLUDE_LOGIN_PATH_LIST` 表示排除的路由列表。
+
+在 `默认无需登录策略: DEFAULT_NO_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才需要登录,相当于黑名单。
+
+在 `默认需要登录策略: DEFAULT_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才不需要登录,相当于白名单。
+
+### excludeLoginPath
+definePage 中可以通过 `excludeLoginPath` 来配置路由是否需要登录。(类似过去的 needLogin 的功能)
+
+```ts
+definePage({
+  style: {
+    navigationBarTitleText: '关于',
+  },
+  // 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 src/router 文件夹
+  excludeLoginPath: true,
+  // 角色授权(可选):如果需要根据角色授权,就配置这个
+  roleAuth: {
+    field: 'role',
+    value: 'admin',
+    redirect: '/pages/auth/403',
+  },
+})
+```
 
 ## 登录注册页路由
 
@@ -34,4 +52,4 @@
 
 特殊情况例外,如业务需要跨平台复用登录注册页时,也可以用在 `小程序` 上,所以主要还是看业务需求。
 
-通过一个参数 `IS_USE_WX_LOGIN_IN_MP` 来控制是否在 `小程序` 中使用 `小程序` 默认的登录逻辑。
+通过一个参数 `LOGIN_PAGE_ENABLE_IN_MP` 来控制是否在 `小程序` 中使用 `H5登录页` 的登录逻辑。

+ 13 - 5
src/router/config.ts

@@ -1,8 +1,10 @@
+import { getAllPages } from '@/utils'
+
 export const LOGIN_STRATEGY_MAP = {
   DEFAULT_NO_NEED_LOGIN: 0, // 黑名单策略,默认可以进入APP
   DEFAULT_NEED_LOGIN: 1, // 白名单策略,默认不可以进入APP,需要强制登录
 }
-// 登录策略,默认使用`无需登录策略`,即默认不需要登录就可以访问
+// TODO: 1/3 登录策略,默认使用`无需登录策略`,即默认不需要登录就可以访问
 export const LOGIN_STRATEGY = LOGIN_STRATEGY_MAP.DEFAULT_NO_NEED_LOGIN
 export const isNeedLoginMode = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN
 
@@ -11,11 +13,17 @@ export const REGISTER_PAGE = '/pages/login/register'
 
 export const LOGIN_PAGE_LIST = [LOGIN_PAGE, REGISTER_PAGE]
 
+// 在 definePage 里面配置了 excludeLoginPath 的页面,功能与 EXCLUDE_LOGIN_PATH_LIST 相同
+export const excludeLoginPathList = getAllPages('excludeLoginPath').map(page => page.path)
+
 // 排除在外的列表,白名单策略指白名单列表,黑名单策略指黑名单列表
-export const EXCLUDE_PAGE_LIST = [
+// TODO: 2/3 在 definePage 配置 excludeLoginPath,或者在下面配置 EXCLUDE_LOGIN_PATH_LIST
+export const EXCLUDE_LOGIN_PATH_LIST = [
   '/pages/xxx/index',
+  ...excludeLoginPathList, // 都是以 / 开头的 path
 ]
 
-// 在微信小程序里面是否使用小程序默认的登录,默认为true
-// 如果为 false 则复用 h5 的登录逻辑
-export const IS_USE_WX_LOGIN_IN_MP = true // 暂时还没用到,没想好怎么整合
+// 在小程序里面是否使用H5的登录页,默认为 false
+// 如果为 true 则复用 h5 的登录逻辑
+// TODO: 3/3 确定自己的登录页是否需要在小程序里面使用
+export const LOGIN_PAGE_ENABLE_IN_MP = false

+ 53 - 28
src/router/interceptor.ts

@@ -1,16 +1,24 @@
+import { isMp } from '@uni-helper/uni-env'
 /**
  * by 菲鸽 on 2025-08-19
  * 路由拦截,通常也是登录拦截
- * 黑白名单的配置,请看 config.ts 文件, EXCLUDE_PAGE_LIST
+ * 黑白名单的配置,请看 config.ts 文件, EXCLUDE_LOGIN_PATH_LIST
  */
 import { useTokenStore } from '@/store/token'
-import { tabbarStore } from '@/tabbar/store'
-import { getLastPage, parseUrlToObj } from '@/utils/index'
-import { EXCLUDE_PAGE_LIST, isNeedLoginMode, LOGIN_PAGE, LOGIN_PAGE_LIST } from './config'
+import { isPageTabbar, tabbarStore } from '@/tabbar/store'
+import { getAllPages, getLastPage, HOME_PAGE, parseUrlToObj } from '@/utils/index'
+import { EXCLUDE_LOGIN_PATH_LIST, isNeedLoginMode, LOGIN_PAGE, LOGIN_PAGE_ENABLE_IN_MP } from './config'
 
 export const FG_LOG_ENABLE = false
+export function judgeIsExcludePath(path: string) {
+  const isDev = import.meta.env.DEV
+  if (!isDev) {
+    return EXCLUDE_LOGIN_PATH_LIST.includes(path)
+  }
+  const allExcludeLoginPages = getAllPages('excludeLoginPath') // dev 环境下,需要每次都重新获取,否则新配置就不会生效
+  return EXCLUDE_LOGIN_PATH_LIST.includes(path) || (isDev && allExcludeLoginPages.some(page => page.path === path))
+}
 
-// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
 export const navigateToInterceptor = {
   // 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
   // 增加对相对路径的处理,BY 网友 @ideal
@@ -20,6 +28,7 @@ export const navigateToInterceptor = {
     }
     let { path, query: _query } = parseUrlToObj(url)
 
+    FG_LOG_ENABLE && console.log('\n\n路由拦截器:-------------------------------------')
     FG_LOG_ENABLE && console.log('路由拦截器 1: url->', url, ', query ->', query)
     const myQuery = { ..._query, ...query }
     // /pages/route-interceptor/index?name=feige&age=30
@@ -37,50 +46,66 @@ export const navigateToInterceptor = {
     // 处理直接进入路由非首页时,tabbarIndex 不正确的问题
     tabbarStore.setAutoCurIdx(path)
 
-    if (LOGIN_PAGE_LIST.includes(path)) {
-      FG_LOG_ENABLE && console.log('命中了 LOGIN_PAGE_LIST')
+    // 小程序里面使用平台自带的登录,则不走下面的逻辑
+    if (isMp && LOGIN_PAGE_ENABLE_IN_MP) {
       return true // 明确表示允许路由继续执行
     }
+
+    const tokenStore = useTokenStore()
+    FG_LOG_ENABLE && console.log('tokenStore.hasLogin:', tokenStore.hasLogin)
+
+    // 不管黑白名单,登录了就直接去吧(但是当前不能是登录页)
+    if (tokenStore.hasLogin) {
+      if (path !== LOGIN_PAGE) {
+        return true // 明确表示允许路由继续执行
+      }
+      else {
+        console.log('已经登录,但是还在登录页', myQuery.redirect)
+        const url = myQuery.redirect || HOME_PAGE
+        if (isPageTabbar(url)) {
+          uni.switchTab({ url })
+        }
+        else {
+          uni.navigateTo({ url })
+        }
+        return false // 明确表示阻止原路由继续执行
+      }
+    }
     let fullPath = path
 
-    if (myQuery) {
+    if (Object.keys(myQuery).length) {
       fullPath += `?${Object.keys(myQuery).map(key => `${key}=${myQuery[key]}`).join('&')}`
     }
     const redirectUrl = `${LOGIN_PAGE}?redirect=${encodeURIComponent(fullPath)}`
 
-    const tokenStore = useTokenStore()
-    FG_LOG_ENABLE && console.log('tokenStore.hasLogin:', tokenStore.hasLogin)
-
-    // #region 1/2 需要登录的情况 ---------------------------
+    // #region 1/2 默认需要登录的情况(白名单策略) ---------------------------
     if (isNeedLoginMode) {
-      if (tokenStore.hasLogin) {
+      // 需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示白名单,可以直接通过
+      if (judgeIsExcludePath(path)) {
         return true // 明确表示允许路由继续执行
       }
+      // 否则需要重定向到登录页
       else {
-        // 需要登录里面的 EXCLUDE_PAGE_LIST 表示白名单,可以直接通过
-        if (EXCLUDE_PAGE_LIST.includes(path)) {
+        if (path === LOGIN_PAGE) {
           return true // 明确表示允许路由继续执行
         }
-        // 否则需要重定向到登录页
-        else {
-          FG_LOG_ENABLE && console.log('1 isNeedLogin redirectUrl:', redirectUrl)
-          uni.navigateTo({ url: redirectUrl })
-          return false // 明确表示阻止原路由继续执行
-        }
+        FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) redirectUrl:', redirectUrl)
+        uni.navigateTo({ url: redirectUrl })
+        return false // 明确表示阻止原路由继续执行
       }
     }
-    // #endregion 1/2 需要登录的情况 ---------------------------
+    // #endregion 1/2 默认需要登录的情况(白名单策略) ---------------------------
 
-    // #region 2/2 不需要登录的情况 ---------------------------
+    // #region 2/2 默认不需要登录的情况(黑名单策略) ---------------------------
     else {
-      // 不需要登录里面的 EXCLUDE_PAGE_LIST 表示黑名单,需要重定向到登录页
-      if (EXCLUDE_PAGE_LIST.includes(path)) {
-        FG_LOG_ENABLE && console.log('2 isNeedLogin redirectUrl:', redirectUrl)
+      // 不需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示黑名单,需要重定向到登录页
+      if (judgeIsExcludePath(path)) {
+        FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) redirectUrl:', redirectUrl)
         uni.navigateTo({ url: redirectUrl })
-        return false // 明确表示阻止原路由继续执行
+        return false // 修改为false,阻止原路由继续执行
       }
     }
-    // #endregion 2/2 不需要登录的情况 ---------------------------
+    // #endregion 2/2 默认不需要登录的情况(黑名单策略) ---------------------------
     return true // 明确表示允许路由继续执行
   },
 }

+ 2 - 2
src/service/user.ts

@@ -119,7 +119,7 @@ export async function userCreateWithListUsingPost({
   });
 }
 
-/** Logs user into the system GET /user/login */
+/** Logs user into the system GET /auth/login */
 export async function userLoginUsingGet({
   params,
   options,
@@ -128,7 +128,7 @@ export async function userLoginUsingGet({
   params: API.userLoginUsingGetParams;
   options?: CustomRequestOptions;
 }) {
-  return request<string>('/user/login', {
+  return request<string>('/auth/login', {
     method: 'GET',
     params: {
       ...params,

+ 1 - 1
src/service/user.vuequery.ts

@@ -122,7 +122,7 @@ export function useUserCreateWithListUsingPostMutation(options?: {
   return response;
 }
 
-/** Logs user into the system GET /user/login */
+/** Logs user into the system GET /auth/login */
 export function userLoginUsingGetQueryOptions(options: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
   params: API.userLoginUsingGetParams;

+ 2 - 1
src/store/index.ts

@@ -13,6 +13,7 @@ store.use(
 
 export default store
 
-export * from './theme'
 // 模块统一导出
+export * from './theme'
+export * from './token'
 export * from './user'

+ 19 - 8
src/store/token.ts

@@ -12,9 +12,6 @@ import { isDoubleTokenRes, isSingleTokenRes } from '@/api/types/login'
 import { isDoubleTokenMode } from '@/utils'
 import { useUserStore } from './user'
 
-// 修复:添加 isSingleTokenMode 变量
-export const isSingleTokenMode = !isDoubleTokenMode
-
 // 初始化状态
 const tokenInfoState = isDoubleTokenMode
   ? {
@@ -57,6 +54,10 @@ export const useTokenStore = defineStore(
      * 判断token是否过期
      */
     const isTokenExpired = computed(() => {
+      if (!tokenInfo.value) {
+        return true
+      }
+
       const now = Date.now()
       const expireTime = uni.getStorageSync('accessTokenExpireTime')
 
@@ -154,18 +155,22 @@ export const useTokenStore = defineStore(
      */
     const logout = async () => {
       try {
+        // TODO 实现自己的退出登录逻辑
         await _logout()
-        // 清除存储的过期时间
-        uni.removeStorageSync('accessTokenExpireTime')
-        uni.removeStorageSync('refreshTokenExpireTime')
       }
       catch (error) {
         console.error('退出登录失败:', error)
       }
       finally {
         // 无论成功失败,都需要清除本地token信息
+        // 清除存储的过期时间
+        uni.removeStorageSync('accessTokenExpireTime')
+        uni.removeStorageSync('refreshTokenExpireTime')
+        console.log('退出登录-清除用户信息')
+        tokenInfo.value = { ...tokenInfoState }
+        uni.removeStorageSync('token')
         const userStore = useUserStore()
-        await userStore.removeUserInfo()
+        userStore.clearUserInfo()
       }
     }
 
@@ -208,7 +213,7 @@ export const useTokenStore = defineStore(
         return ''
       }
 
-      if (isSingleTokenMode) {
+      if (!isDoubleTokenMode) {
         return isSingleTokenRes(tokenInfo.value) ? tokenInfo.value.token : ''
       }
       else {
@@ -220,6 +225,9 @@ export const useTokenStore = defineStore(
      * 检查是否有登录信息(不考虑token是否过期)
      */
     const hasLoginInfo = computed(() => {
+      if (!tokenInfo.value) {
+        return false
+      }
       if (isDoubleTokenMode) {
         return isDoubleTokenRes(tokenInfo.value) && !!tokenInfo.value.accessToken
       }
@@ -232,6 +240,7 @@ export const useTokenStore = defineStore(
      * 检查是否已登录且token有效
      */
     const hasValidLogin = computed(() => {
+      console.log('hasValidLogin', hasLoginInfo.value, !isTokenExpired.value)
       return hasLoginInfo.value && !isTokenExpired.value
     })
 
@@ -265,9 +274,11 @@ export const useTokenStore = defineStore(
       // 内部系统使用的方法
       refreshToken,
       tryGetValidToken,
+      validToken: getValidToken,
 
       // 调试或特殊场景可能需要直接访问的信息
       tokenInfo,
+      setTokenInfo,
     }
   },
   {

+ 3 - 3
src/store/user.ts

@@ -7,7 +7,7 @@ import {
 
 // 初始化状态
 const userInfoState: IUserInfoRes = {
-  userId: 0,
+  userId: -1,
   username: '',
   nickname: '',
   avatar: '/static/images/default-avatar.png',
@@ -33,7 +33,7 @@ export const useUserStore = defineStore(
       console.log('userInfo', userInfo.value)
     }
     // 删除用户信息
-    const removeUserInfo = () => {
+    const clearUserInfo = () => {
       userInfo.value = { ...userInfoState }
       uni.removeStorageSync('user')
     }
@@ -49,7 +49,7 @@ export const useUserStore = defineStore(
 
     return {
       userInfo,
-      removeUserInfo,
+      clearUserInfo,
       fetchUserInfo,
       setUserInfo,
       setUserAvatar,

+ 1 - 2
src/tabbar/config.ts

@@ -152,5 +152,4 @@ const _tabbar: TabBar = {
   list: _tabbarList as unknown as TabBar['list'],
 }
 
-// 0和1 需要显示底部的tabbar的各种配置,以利用缓存
-export const tabBar = tabbarCacheEnable ? _tabbar : {}
+export const tabBar = tabbarCacheEnable ? _tabbar : undefined

+ 1 - 0
src/tabbar/index.vue

@@ -1,4 +1,5 @@
 <script setup lang="ts">
+// i-carbon-code
 import type { CustomTabBarItem } from './config'
 import { customTabbarEnable, needHideNativeTabbar, tabbarCacheEnable } from './config'
 import { tabbarList, tabbarStore } from './store'

+ 2 - 1
src/tabbar/store.ts

@@ -23,7 +23,8 @@ if (customTabbarEnable && BULGE_ENABLE) {
 }
 
 export function isPageTabbar(path: string) {
-  return tabbarList.some(item => item.pagePath === path)
+  const _path = path.split('?')[0]
+  return tabbarList.some(item => item.pagePath === _path)
 }
 
 /**

+ 14 - 2
src/utils/index.ts

@@ -16,6 +16,12 @@ export function getLastPage() {
  */
 export function currRoute() {
   const lastPage = getLastPage()
+  if (!lastPage) {
+    return {
+      path: '',
+      query: {},
+    }
+  }
   const currRoute = (lastPage as any).$page
   // console.log('lastPage.$page:', currRoute)
   // console.log('lastPage.$page.fullpath:', currRoute.fullPath)
@@ -60,10 +66,10 @@ export function parseUrlToObj(url: string) {
 }
 /**
  * 得到所有的需要登录的 pages,包括主包和分包的
- * 这里设计得通用一点,可以传递 key 作为判断依据,默认是 needLogin, 与 route-block 配对使用
+ * 这里设计得通用一点,可以传递 key 作为判断依据,默认是 excludeLoginPath, 与 route-block 配对使用
  * 如果没有传 key,则表示所有的 pages,如果传递了 key, 则表示通过 key 过滤
  */
-export function getAllPages(key = 'needLogin') {
+export function getAllPages(key = 'excludeLoginPath') {
   // 这里处理主包
   const mainPages = pages
     .filter(page => !key || page[key])
@@ -175,3 +181,9 @@ export function getEnvBaseUploadUrl() {
  * 是否是双token模式
  */
 export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double'
+
+/**
+ * 首页路径,通过 page.json 里面的 type 为 home 的页面获取,如果没有,则默认是第一个页面
+ * 通常为 /pages/index/index
+ */
+export const HOME_PAGE = `/${pages.find(page => page.type === 'home')?.path || pages[0].path}`

+ 1 - 1
tsconfig.json

@@ -39,5 +39,5 @@
     "src/**/*.vue",
     "src/**/*.json"
   ],
-  "exclude": ["node_modules"]
+  "exclude": ["node_modules", "dist"]
 }

+ 14 - 0
uno.config.ts

@@ -1,5 +1,11 @@
+import type {
+  Preset,
+} from 'unocss'
 // https://www.npmjs.com/package/@uni-helper/unocss-preset-uni
 import { presetUni } from '@uni-helper/unocss-preset-uni'
+
+// @see https://unocss.dev/presets/legacy-compat
+import { presetLegacyCompat } from '@unocss/preset-legacy-compat'
 import {
   defineConfig,
   presetAttributify,
@@ -23,6 +29,14 @@ export default defineConfig({
     }),
     // 支持css class属性化
     presetAttributify(),
+    // TODO: check 是否会有别的影响
+    // 处理低端安卓机的样式问题
+    // 将颜色函数 (rgb()和hsl()) 从空格分隔转换为逗号分隔,更好的兼容性app端,example:
+    // `rgb(255 0 0)` -> `rgb(255, 0, 0)`
+    // `rgba(255 0 0 / 0.5)` -> `rgba(255, 0, 0, 0.5)`
+    presetLegacyCompat({
+      commaStyleColorFunction: true,
+    }) as Preset,
   ],
   transformers: [
     // 启用指令功能:主要用于支持 @apply、@screen 和 theme() 等 CSS 指令

+ 7 - 3
vite.config.ts

@@ -48,10 +48,11 @@ export default ({ command, mode }) => {
   const {
     VITE_APP_PORT,
     VITE_SERVER_BASEURL,
+    VITE_APP_TITLE,
     VITE_DELETE_CONSOLE,
-    VITE_SHOW_SOURCEMAP,
     VITE_APP_PUBLIC_BASE,
     VITE_APP_PROXY_ENABLE,
+    VITE_SERVER_HAS_API_PREFIX,
     VITE_APP_PROXY_PREFIX,
   } = env
   console.log('环境变量 env -> ', env)
@@ -111,7 +112,7 @@ export default ({ command, mode }) => {
       UNI_PLATFORM === 'h5' && {
         name: 'html-transform',
         transformIndexHtml(html) {
-          return html.replace('%BUILD_TIME%', dayjs().format('YYYY-MM-DD HH:mm:ss'))
+          return html.replace('%BUILD_TIME%', dayjs().format('YYYY-MM-DD HH:mm:ss')).replace('%VITE_APP_TITLE%', VITE_APP_TITLE)
         },
       },
       // 打包分析插件,h5 + 生产环境才弹出
@@ -166,7 +167,10 @@ export default ({ command, mode }) => {
             [VITE_APP_PROXY_PREFIX]: {
               target: VITE_SERVER_BASEURL,
               changeOrigin: true,
-              rewrite: path => path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''),
+              // 后端有/api前缀则不做处理,没有则需要去掉
+              rewrite: path => JSON.parse(VITE_SERVER_HAS_API_PREFIX)
+                ? path
+                : path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''),
             },
           }
         : undefined,