Pārlūkot izejas kodu

Chore: Chore Update

Z.X.PING 5 mēneši atpakaļ
vecāks
revīzija
05cb9eba9c
67 mainītis faili ar 14605 papildinājumiem un 1860 dzēšanām
  1. 3 1
      .cursor/rules/development-workflow.mdc
  2. 3 1
      .cursor/rules/project-overview.mdc
  3. 3 3
      .cursor/rules/uni-app-patterns.mdc
  4. 3 2
      .cursor/rules/vue-typescript-patterns.mdc
  5. 0 31
      .dockerignore
  6. 18 156
      .github/workflows/auto-merge.yml
  7. 3 3
      .gitignore
  8. 1 0
      .npmrc
  9. 11 7
      .trae/rules/project_rules.md
  10. 1 3
      .vscode/extensions.json
  11. 1 1
      .vscode/settings.json
  12. 0 38
      Dockerfile
  13. 6 1
      README.md
  14. 0 3
      codes/README.md
  15. 0 28
      docker.md
  16. 9 0
      manifest.config.ts
  17. 0 145
      nginx.conf
  18. 6 5
      openapi-ts-request.config.ts
  19. 14 10
      package.json
  20. 1 4
      pages.config.ts
  21. 13980 0
      pnpm-lock.yaml
  22. 5 171
      scripts/create-base-files.js
  23. 0 6
      scripts/postupgrade.js
  24. 2 7
      src/App.ku.vue
  25. 2 13
      src/App.vue
  26. 116 0
      src/hooks/useScroll.md
  27. 2 2
      src/http/alova.ts
  28. 8 5
      src/http/http.ts
  29. 3 0
      src/http/types.ts
  30. 2 2
      src/http/vue-query.ts
  31. 0 7
      src/layouts/default.vue
  32. 0 3
      src/pages-fg/404/README.md
  33. 0 30
      src/pages-fg/404/index.vue
  34. 0 3
      src/pages-fg/REAME.md
  35. 0 20
      src/pages-fg/login/README.md
  36. 0 87
      src/pages-fg/login/login.vue
  37. 0 34
      src/pages-fg/login/register.vue
  38. 0 135
      src/pages-sub/about/about.vue
  39. 0 53
      src/pages-sub/about/alova.vue
  40. 0 25
      src/pages-sub/about/components/Upload.vue
  41. 0 28
      src/pages-sub/about/components/VBindCss.vue
  42. 0 64
      src/pages-sub/about/components/request-openapi.vue
  43. 0 75
      src/pages-sub/about/components/request.vue
  44. 0 48
      src/pages-sub/demo/components/request.vue
  45. 0 40
      src/pages-sub/demo/index.vue
  46. 0 72
      src/pages-sub/demo/scroll.vue
  47. 1 62
      src/pages/index/index.vue
  48. 2 203
      src/pages/me/me.vue
  49. 0 31
      src/router/config.ts
  50. 9 77
      src/router/interceptor.ts
  51. 3 7
      src/service/info.ts
  52. 4 5
      src/service/listAll.ts
  53. 1 0
      src/static/my-icons/copyright.svg
  54. 3 2
      src/store/index.ts
  55. 0 42
      src/store/theme.ts
  56. 11 5
      src/tabbar/README.md
  57. 1 14
      src/tabbar/config.ts
  58. 20 4
      src/tabbar/index.vue
  59. 7 12
      src/tabbar/store.ts
  60. 34 0
      src/tabbar/types.ts
  61. 25 0
      src/typings.d.ts
  62. 6 0
      src/typings.ts
  63. 166 0
      src/utils/debounce.ts
  64. 44 0
      src/utils/toLoginPage.ts
  65. 2 3
      tsconfig.json
  66. 46 4
      uno.config.ts
  67. 17 17
      vite.config.ts

+ 3 - 1
.cursor/rules/development-workflow.mdc

@@ -5,6 +5,7 @@
 2. 开发环境:
    - H5: `pnpm dev` 或 `pnpm dev:h5`
    - 微信小程序: `pnpm dev:mp`
+   - 支付宝小程序: `pnpm dev:mp-alipay`
    - APP: `pnpm dev:app`
 
 ## 代码规范
@@ -15,7 +16,8 @@
 
 ## 构建和部署
 - H5 构建:`pnpm build:h5`
-- 小程序构建:`pnpm build:mp`
+- 微信小程序构建:`pnpm build:mp`
+- 支付宝小程序构建:`pnpm build:mp-alipay`
 - APP 构建:`pnpm build:app`
 - 类型检查:`pnpm type-check`
 

+ 3 - 1
.cursor/rules/project-overview.mdc

@@ -8,7 +8,7 @@ alwaysApply: true
 ## 项目特点
 - 支持 H5、小程序、APP 多平台开发
 - 使用最新的前端技术栈
-- 内置约定式路由、layout布局、请求封装等功能
+- 内置约定式路由、layout布局、请求封装、登录拦截、自定义tabbar等功能
 - 无需依赖 HBuilderX,支持命令行开发
 
 ## 核心配置文件
@@ -26,9 +26,11 @@ alwaysApply: true
 - `src/http/` - HTTP 请求封装
 - `src/store/` - 状态管理
 - `src/tabbar/` - 底部导航栏
+- `src/App.ku.vue` - 全局根组件(类似 App.vue 里面的 template作用)
 
 ## 开发命令
 - `pnpm dev` - 开发 H5 版本
 - `pnpm dev:mp` - 开发微信小程序
+- `pnpm dev:mp-alipay` - 开发支付宝小程序(含钉钉)
 - `pnpm dev:app` - 开发 APP 版本
 - `pnpm build` - 构建生产版本

+ 3 - 3
.cursor/rules/uni-app-patterns.mdc

@@ -3,12 +3,12 @@
 ## 页面开发
 - 页面文件放在 [src/pages/](mdc:src/pages/) 目录下
 - 使用约定式路由,文件名即路由路径
-- 页面配置在仅需要在 `route-block` 中配置标题等内容即可,会自动生成到 `pages.json` 中
+- 页面配置在仅需要在 宏`definePage` 中配置标题等内容即可,会自动生成到 `pages.json` 中
 
 ## 组件开发
-- 组件文件放在 [src/components/](mdc:src/components/) 目录下
+- 组件文件放在 [src/components/](mdc:src/components/) 或者 [src/pages/xx/components/](mdc:src/pages/xx/components/) 目录下
 - 使用 uni-app 内置组件和第三方组件库
-- 支持 wot-design-uni\uv-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
+- 支持 wot-ui\uview-pro\uv-ui\sard-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
 - 自定义组件遵循 uni-app 组件规范
 
 ## 平台适配

+ 3 - 2
.cursor/rules/vue-typescript-patterns.mdc

@@ -4,10 +4,11 @@
 - 使用 Composition API 和 `<script setup>` 语法
 - 组件文件使用 PascalCase 命名
 - 页面文件放在 `src/pages/` 目录下
-- 组件文件放在 `src/components/` 目录下
+- 全局组件文件放在 `src/components/` 目录下
+- 局部组件文件放在页面的 `/components/` 目录下
 
 ## Vue SFC 组件规范
-- `<script setup>` 标签必须是第一个子元素
+- `<script setup lang="ts">` 标签必须是第一个子元素
 - `<template>` 标签必须是第二个子元素
 - `<style scoped>` 标签必须是最后一个子元素(因为推荐使用原子化类名,所以很可能没有)
 

+ 0 - 31
.dockerignore

@@ -1,31 +0,0 @@
-# 依赖目录
-node_modules
-
-# 版本控制
-.git
-.gitignore
-
-# 构建产物
-/dist
-
-# 开发工具配置
-.vscode/
-.idea/
-.trae/
-.cursor/
-
-# 其他配置文件
-.github/
-.husky/
-
-# 日志文件
-logs/
-
-# 缓存文件
-.cache/
-
-*.swp
-*.swo
-
-# 操作系统文件
-.DS_Store

+ 18 - 156
.github/workflows/auto-merge.yml

@@ -7,80 +7,8 @@ on:
   workflow_dispatch: # 手动触发
 
 jobs:
-  # merge-to-release:
-  #   name: Merge main into release
-  #   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 release
-  #       run: |
-  #         git config user.name "GitHub Actions"
-  #         git config user.email "actions@github.com"
-  #         git checkout release
-  #         git merge main --no-ff -m "Auto merge main into release"
-  #         git push origin release
-
-  merge-to-i18n:
-    name: Merge main into i18n
-    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 i18n
-        run: |
-          git config user.name "GitHub Actions"
-          git config user.email "actions@github.com"
-          git checkout i18n
-          git merge main --no-ff -m "Auto merge main into i18n"
-          git push origin i18n
-
-  merge-to-base-sard-ui:
-    name: Merge main into base-sard-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-sard-ui
-        run: |
-          git config user.name "GitHub Actions"
-          git config user.email "actions@github.com"
-          git checkout base-sard-ui
-          git merge main --no-ff -m "Auto merge main into base-sard-ui"
-          git push origin base-sard-ui
-
-  merge-to-base-uv-ui:
-    name: Merge main into base-uv-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-uv-ui
-        run: |
-          git config user.name "GitHub Actions"
-          git config user.email "actions@github.com"
-          git checkout base-uv-ui
-          git merge main --no-ff -m "Auto merge main into base-uv-ui"
-          git push origin base-uv-ui
-
-  merge-to-base-uview-pro:
-    name: Merge main into base-uview-pro
+  merge-to-base:
+    name: Merge main into base
     runs-on: ubuntu-latest
     steps:
       - name: Checkout repository
@@ -89,100 +17,34 @@ jobs:
           fetch-depth: 0
           token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
 
-      - name: Merge main into base-uview-pro
+      - name: Merge main into base
         run: |
           git config user.name "GitHub Actions"
           git config user.email "actions@github.com"
-          git checkout base-uview-pro
-          git merge main --no-ff -m "Auto merge main into base-uview-pro"
-          git push origin base-uview-pro
+          git checkout base
+          git merge main --no-ff -m "Auto merge main into base"
+          git push origin base
 
-  merge-to-base-uview-plus:
-    name: Merge main into base-uview-plus
-    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-uview-plus
-        run: |
-          git config user.name "GitHub Actions"
-          git config user.email "actions@github.com"
-          git checkout base-uview-plus
-          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 }}
-
-  #     - 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
-    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-skiyee-ui
+      - name: Merge base into base-login
         run: |
           git config user.name "GitHub Actions"
           git config user.email "actions@github.com"
-          git checkout base-skiyee-ui
-          git merge main --no-ff -m "Auto merge main into base-skiyee-ui"
-          git push origin base-skiyee-ui
+          git checkout base-login
+          git merge base --no-ff -m "Auto merge base into base-login"
+          git push origin base-login
 
-  merge-to-main-v4:
-    name: Merge main into main-v4
-    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 main-v4
+      - name: Merge base into base-i18n
         run: |
           git config user.name "GitHub Actions"
           git config user.email "actions@github.com"
-          git checkout main-v4
-          git merge main --no-ff -m "Auto merge main into main-v4"
-          git push origin main-v4
-
-  merge-to-i18n-v4:
-    name: Merge main into i18n-v4
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-          token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
+          git checkout base-i18n
+          git merge base --no-ff -m "Auto merge base into base-i18n"
+          git push origin base-i18n
 
-      - name: Merge main into i18n-v4
+      - name: Merge base into base-login-i18n
         run: |
           git config user.name "GitHub Actions"
           git config user.email "actions@github.com"
-          git checkout i18n-v4
-          git merge main --no-ff -m "Auto merge main into i18n-v4"
-          git push origin i18n-v4
+          git checkout base-login-i18n
+          git merge base --no-ff -m "Auto merge base into base-login-i18n"
+          git push origin base-login-i18n

+ 3 - 3
.gitignore

@@ -33,9 +33,9 @@ src/types
 src/manifest.json
 src/pages.json
 
-# lock 文件还是不要了,我主要的版本写死就好了
-pnpm-lock.yaml
-package-lock.json
+# 2025-10-15 by 菲鸽: lock 文件还是需要加入版本管理,今天又遇到版本不一致导致无法运行的问题了。
+# pnpm-lock.yaml
+# package-lock.json
 
 # TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
 # `git rm -r --cached .` 然后提交 commit 即可。

+ 1 - 0
.npmrc

@@ -6,3 +6,4 @@ auto-install-peers=true
 shamefully-hoist=true
 ignore-workspace-root-check=true
 install-workspace-root=true
+node-options=--max-old-space-size=8192

+ 11 - 7
.trae/rules/project_rules.md

@@ -23,10 +23,12 @@
 - `src/http/` - HTTP 请求封装
 - `src/store/` - 状态管理
 - `src/tabbar/` - 底部导航栏
+- `src/App.ku.vue` - 全局根组件(类似 App.vue 里面的 template作用)
 
 ## 开发命令
 - `pnpm dev` - 开发 H5 版本
 - `pnpm dev:mp` - 开发微信小程序
+- `pnpm dev:mp-alipay` - 开发支付宝小程序(含钉钉)
 - `pnpm dev:app` - 开发 APP 版本
 - `pnpm build` - 构建生产版本
 
@@ -34,7 +36,8 @@
 - 使用 Composition API 和 `<script setup>` 语法
 - 组件文件使用 PascalCase 命名
 - 页面文件放在 `src/pages/` 目录下
-- 组件文件放在 `src/components/` 目录下
+- 全局组件文件放在 `src/components/` 目录下
+- 局部组件文件放在页面的 `/components/` 目录下
 
 ## TypeScript 规范
 - 严格使用 TypeScript,避免使用 `any` 类型
@@ -50,24 +53,25 @@
 
 ## UnoCSS 原子化 CSS
 - 项目使用 UnoCSS 作为原子化 CSS 框架
-- 配置在 [uno.config.ts](mdc:uno.config.ts)
+- 配置在 [uno.config.ts]
 - 支持预设和自定义规则
 - 优先使用原子化类名,减少自定义 CSS
 
 ## Vue SFC 组件规范
-- `<script setup>` 标签必须是第一个子元素
+- `<script setup lang="ts">` 标签必须是第一个子元素
 - `<template>` 标签必须是第二个子元素
 - `<style scoped>` 标签必须是最后一个子元素(因为推荐使用原子化类名,所以很可能没有)
 
 ## 页面开发
-- 页面文件放在 [src/pages/](mdc:src/pages/) 目录下
+- 页面文件放在 [src/pages/]目录下
 - 使用约定式路由,文件名即路由路径
-- 页面配置在仅需要在 `route-block` 中配置标题等内容即可,会自动生成到 `pages.json` 中
+- 页面配置在仅需要在 宏`definePage` 中配置标题等内容即可,会自动生成到 `pages.json` 中
 
 ## 组件开发
-- 组件文件放在 [src/components/](mdc:src/components/) 目录下
+- 全局组件文件放在 `src/components/` 目录下
+- 局部组件文件放在页面的 `/components/` 目录下
 - 使用 uni-app 内置组件和第三方组件库
-- 支持 wot-design-uni\uv-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
+- 支持 wot-ui\uview-pro\uv-ui\sard-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
 - 自定义组件遵循 uni-app 组件规范
 
 ## 平台适配

+ 1 - 3
.vscode/extensions.json

@@ -1,7 +1,6 @@
 {
   "recommendations": [
     "vue.volar",
-    "esbenp.prettier-vscode",
     "dbaeumer.vscode-eslint",
     "antfu.unocss",
     "antfu.iconify",
@@ -11,7 +10,6 @@
     "uni-helper.uni-highlight-vscode",
     "uni-helper.uni-ui-snippets-vscode",
     "uni-helper.uni-app-snippets-vscode",
-    "streetsidesoftware.code-spell-checker",
-    "christian-kohler.path-intellisense"
+    "streetsidesoftware.code-spell-checker"
   ]
 }

+ 1 - 1
.vscode/settings.json

@@ -10,7 +10,7 @@
   "scss.validate": false, // 禁用 SCSS 内置验证
   "less.validate": false, // 禁用 LESS 内置验证
 
-  "typescript.tsdk": "node_modules\\typescript\\lib",
+  "typescript.tsdk": "node_modules/typescript/lib",
   "explorer.fileNesting.enabled": true,
   "explorer.fileNesting.expand": false,
   "explorer.fileNesting.patterns": {

+ 0 - 38
Dockerfile

@@ -1,38 +0,0 @@
-# 使用 node:24-alpine 作为基础镜像,固定版本+减少体积
-FROM node:24-alpine AS builder
-
-# 在容器中创建目录
-WORKDIR /app
-
-# 安装pnpm(使用 npm 的 --global-style 可以减少依赖安装体积)
-RUN npm install -g pnpm@10.10.0 --global-style
-# 设置pnpm镜像源
-RUN pnpm config set registry https://registry.npmmirror.com
-# 复制依赖文件
-COPY package.json pnpm-lock.yaml ./
-# 先复制scripts目录,因为prepare脚本需要用到其中的文件
-COPY scripts ./scripts
-# 安装依赖,但跳过prepare脚本(这一步会缓存,只有 package.json 或 pnpm-lock.yaml 变化时才会重新运行)
-RUN pnpm install --ignore-scripts --frozen-lockfile
-# 手动执行我们需要的docker:prepare脚本
-RUN pnpm run docker:prepare
-# 复制其余源代码
-COPY . .
-# 构建项目
-RUN pnpm run build
-
-
-# 使用nginx作为服务
-FROM nginx:1.29.1-alpine3.22 AS production-stage
-
-# 将构建好的项目复制到nginx下
-COPY --from=builder /app/dist/build/h5 /usr/share/nginx/html
-
-COPY nginx.conf /etc/nginx/nginx.conf
-
-# 暴露端口
-EXPOSE 80
-EXPOSE 443
-
-# 启动nginx
-CMD ["nginx", "-g", "daemon off;"]

+ 6 - 1
README.md

@@ -38,7 +38,7 @@
 <p align="center">
   <a href="https://unibest.tech/" target="_blank">📖 文档地址(new)</a>
   <span style="margin:0 10px;">|</span>
-  <a href="https://feige996.github.io/hello-unibest/" target="_blank">📱 DEMO 地址</a>
+  <a href="https://unibest-tech.github.io/hello-unibest" target="_blank">📱 DEMO 地址</a>
 </p>
 
 ---
@@ -60,6 +60,11 @@
 - Vue Official>=2.1.10
 - TypeScript>=5.0
 
+## 新版分支 
+- main == base
+- base --> base-i18n
+- base-login --> base-login-i18n
+
 ## &#x1F4C2; 快速开始
 
 执行 `pnpm create unibest` 创建项目

+ 0 - 3
codes/README.md

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

+ 0 - 28
docker.md

@@ -1,28 +0,0 @@
-## Docker
-
-根据提供的 `Dockerfile`,可以通过以下步骤构建并运行镜像:
-
-### 1. 构建Docker镜像
-
-在项目根目录执行以下命令:
-
-- `-t unibest:v1-2025091701`:为镜像指定名称和标签,YYYYMMDD+编号
-- `.`:表示使用当前目录的Dockerfile
-
-```bash
-docker build -t unibest:v1-2025091701 .
-docker build -t unibest:v1-2025091702 .
-```
-### 2. 运行Docker容器
-使用以下命令运行容器:
-
-```bash
-docker run -d --name unibest-v1-2025091701 -p 80:80 unibest:v1-2025091701
-docker run -d --name unibest-v1-2025091702 -p 80:80 unibest:v1-2025091702
-```
-
-- `-d`:表示在后台运行容器
-- `-p 80:80`:将容器的80端口映射到主机的80端口
-- `--name unibest-v1-2025091701`:为容器指定一个名称
-
-

+ 9 - 0
manifest.config.ts

@@ -141,6 +141,15 @@ export default defineManifestConfig({
     optimization: {
       subPackages: true,
     },
+    // 解决支付宝小程序开发工具报错 【globalThis is not defined】
+    compileOptions: {
+      globalObjectMode: 'enable',
+      transpile: {
+        script: {
+          ignore: ['node_modules/**'],
+        },
+      },
+    },
   },
   'mp-baidu': {
     usingComponents: true,

+ 0 - 145
nginx.conf

@@ -1,145 +0,0 @@
-# 配置工作进程数,通常设置为 CPU 核心数
-worker_processes auto;
-
-# 错误日志配置
-error_log /var/log/nginx/error.log warn;
-pid /var/run/nginx.pid;
-
-events {
-  worker_connections 1024;
-  # 开启多路复用
-  use epoll;
-}
-
-# 文件描述符限制 - 移到这里,在http块之前
-worker_rlimit_nofile 65535;
-
-http {
-  # 日志格式定义
-  log_format main '$remote_addr - $remote_user [$time_local] "$request" '
-                  '$status $body_bytes_sent "$http_referer" '
-                  '"$http_user_agent" "$http_x_forwarded_for"';
-  
-  # 访问日志配置
-  access_log /var/log/nginx/access.log main;
-
-  # 高效文件传输设置
-  sendfile on;
-  tcp_nopush on;
-  tcp_nodelay on;
-  
-  # 连接超时设置
-  keepalive_timeout 65;
-  keepalive_requests 100;
-
-  # gzip 压缩优化
-  gzip on;
-  gzip_vary on;
-  gzip_comp_level 6;
-  gzip_min_length 1000;
-  gzip_buffers 16 8k;
-  gzip_http_version 1.1;
-  # 增加更多文件类型
-  gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;
-
-  # 全局设置
-  # 合理限制请求体大小,根据实际需求调整
-  client_max_body_size 10m;
-  client_body_buffer_size 128k;
-  client_header_timeout 60s;
-  client_body_timeout 60s;
-  
-  server {
-    listen 80;
-    server_name _;
-    gunzip on;
-    gzip_static always;
-    include /etc/nginx/mime.types;
-    absolute_redirect off;
-    root /usr/share/nginx/html;
-
-    # 安全相关响应头
-    add_header X-Frame-Options SAMEORIGIN;
-    add_header X-XSS-Protection "1; mode=block";
-    add_header X-Content-Type-Options nosniff;
-    # 根据实际情况调整 CSP
-    # add_header Content-Security-Policy "default-src 'self'";
-
-    # 处理 SPA 应用路由
-    location / {
-      try_files $uri $uri/ /index.html;
-      index index.html index.htm;
-    }
-
-    # HTML 和 JSON 文件 - 短缓存策略
-    location ~ .*\.(html|json)$ {
-      add_header Cache-Control "public, max-age=300, must-revalidate";
-    }
-
-    # 静态资源 - 长缓存策略
-    location ~ .*\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|ttf|woff|woff2|eot|mp4|mp3|swf)$ {
-      add_header Cache-Control "public, max-age=31536000, immutable";
-      expires 365d;
-      access_log off;
-    }
-
-    # JS 和 CSS - 带版本号的长缓存
-    location ~ .*\.(js|css)$ {
-      add_header Cache-Control "public, max-age=31536000, immutable";
-      expires 365d;
-      access_log off;
-    }
-
-    # 接口转发 - 替换为实际后端地址
-    # location ^~ /fg-api {
-    #   proxy_http_version 1.1;
-    #   proxy_set_header X-Real-IP $remote_addr;
-    #   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-    #   proxy_set_header X-Forwarded-Proto $scheme;
-    #   proxy_set_header Host $host;
-
-    #   # 后端是HTTPS时的必要配置
-    #   proxy_ssl_server_name on;
-    #   proxy_ssl_protocols TLSv1.2 TLSv1.3;
-    #   proxy_ssl_session_reuse on;
-      
-    #   # 对于生产环境,应该尽量使用有效的证书而不是依赖``proxy_ssl_verify off;`` ,因为这会带来安全风险
-    #   proxy_ssl_verify off;
-
-    #   # TODO:替换为实际后端服务地址
-    #   # 注意在URL末尾添加了斜杠,这样Nginx会去掉 /fg-api 前缀
-    #   # 前端请求 http://your-domain.com/fg-api/users 转发到 https://ukw0y1.laf.run/users
-    #   proxy_pass https://ukw0y1.laf.run/;
-
-    #   # 上面一行的效果与下面2行一样的效果,都是为了去掉 /fg-api 前缀
-    #   # 显式移除/fg-api前缀
-    #   # rewrite ^/fg-api(.*)$ $1 break; 
-    #   # 域名末尾不需要斜杠了
-    #   # proxy_pass https://ukw0y1.laf.run;
-
-    #   proxy_connect_timeout 60s;
-    #   proxy_send_timeout 60s;
-    #   proxy_read_timeout 60s;
-
-    #   proxy_buffers 8 32k;
-    #   proxy_buffer_size 64k;
-    #   proxy_busy_buffers_size 128k;
-
-    #   proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
-    # }
-
-    # 错误页面配置
-    error_page 404 /index.html;
-    error_page 500 502 503 504 /50x.html;
-    location = /50x.html {
-      root /usr/share/nginx/html;
-    }
-
-    # 禁止访问隐藏文件
-    location ~ /\. {
-      deny all;
-      access_log off;
-      log_not_found off;
-    }
-  }
-}

+ 6 - 5
openapi-ts-request.config.ts

@@ -1,13 +1,14 @@
-import type { GenerateServiceProps } from 'openapi-ts-request'
+import { defineConfig } from 'openapi-ts-request'
 
-export default [
+export default defineConfig([
   {
+    describe: 'unibest-openapi-test',
     schemaPath: 'https://ukw0y1.laf.run/unibest-opapi-test.json',
     serversPath: './src/service',
-    requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions } from '@/http/types';`,
-    requestOptionsType: 'CustomRequestOptions',
+    requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions_ } from '@/http/types';`,
+    requestOptionsType: 'CustomRequestOptions_',
     isGenReactQuery: false,
     reactQueryMode: 'vue',
     isGenJavaScript: false,
   },
-] as GenerateServiceProps[]
+])

+ 14 - 10
package.json

@@ -1,9 +1,9 @@
 {
   "name": "unibest",
   "type": "module",
-  "version": "3.18.5",
-  "unibest-version": "3.18.5",
-  "update-time": "2025-10-03",
+  "version": "4.1.0",
+  "unibest-version": "4.1.0",
+  "unibest-update-time": "2025-11-07",
   "packageManager": "pnpm@10.10.0",
   "description": "unibest - 最好的 uniapp 开发模板",
   "generate-time": "用户创建项目时生成",
@@ -36,6 +36,7 @@
     "dev:app-android": "uni -p app-android",
     "dev:app-ios": "uni -p app-ios",
     "dev:custom": "uni -p",
+    "predev": "pnpm init-baseFiles",
     "dev": "uni",
     "dev:test": "uni --mode test",
     "dev:prod": "uni --mode production",
@@ -88,8 +89,10 @@
     "build:quickapp-webview-union": "uni build -p quickapp-webview-union",
     "type-check": "vue-tsc --noEmit",
     "openapi": "openapi-ts",
-    "prepare": "git init && husky && node ./scripts/create-base-files.js",
-    "docker:prepare": "node ./scripts/create-base-files.js",
+    "init-husky": "git init && husky",
+    "init-baseFiles": "node ./scripts/create-base-files.js",
+    "init-json": "pnpm init-baseFiles",
+    "prepare": "pnpm init-husky & pnpm init-baseFiles",
     "lint": "eslint",
     "lint:fix": "eslint --fix"
   },
@@ -115,11 +118,11 @@
     "abortcontroller-polyfill": "^1.7.8",
     "alova": "^3.3.3",
     "dayjs": "1.11.10",
-    "js-cookie": "^3.0.5",
     "pinia": "2.0.36",
     "pinia-plugin-persistedstate": "3.2.1",
     "vue": "^3.4.21",
-    "wot-design-uni": "^1.12.4",
+    "vue-i18n": "9.1.9",
+    "vue-router": "4.5.1",
     "z-paging": "2.8.7"
   },
   "devDependencies": {
@@ -133,6 +136,7 @@
     "@esbuild/darwin-arm64": "0.20.2",
     "@esbuild/darwin-x64": "0.20.2",
     "@iconify-json/carbon": "^1.2.4",
+    "@iconify/utils": "^3.0.2",
     "@rollup/rollup-darwin-x64": "^4.28.0",
     "@types/node": "^20.17.9",
     "@uni-helper/eslint-config": "0.5.0",
@@ -142,8 +146,8 @@
     "@uni-helper/unocss-preset-uni": "0.2.11",
     "@uni-helper/vite-plugin-uni-components": "0.2.3",
     "@uni-helper/vite-plugin-uni-layouts": "0.1.11",
-    "@uni-helper/vite-plugin-uni-manifest": "0.2.8",
-    "@uni-helper/vite-plugin-uni-pages": "0.3.13",
+    "@uni-helper/vite-plugin-uni-manifest": "0.2.11",
+    "@uni-helper/vite-plugin-uni-pages": "0.3.19",
     "@uni-helper/vite-plugin-uni-platform": "0.0.5",
     "@uni-ku/bundle-optimizer": "v1.3.15-beta.2",
     "@uni-ku/root": "1.4.1",
@@ -158,7 +162,7 @@
     "husky": "^9.1.7",
     "lint-staged": "^15.2.10",
     "miniprogram-api-typings": "^4.1.0",
-    "openapi-ts-request": "^1.6.7",
+    "openapi-ts-request": "^1.10.0",
     "postcss": "^8.4.49",
     "postcss-html": "^1.8.0",
     "postcss-scss": "^4.0.9",

+ 1 - 4
pages.config.ts

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

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 13980 - 0
pnpm-lock.yaml


+ 5 - 171
scripts/create-base-files.js

@@ -9,162 +9,9 @@ import { fileURLToPath } from 'node:url'
 const __filename = fileURLToPath(import.meta.url)
 const __dirname = path.dirname(__filename)
 
-const manifest = {
-  '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',
-    optimization: {
-      subPackages: true,
-    },
-  },
-  'mp-baidu': {
-    usingComponents: true,
-  },
-  'mp-toutiao': {
-    usingComponents: true,
-  },
-  'uniStatistics': {
-    enable: false,
-  },
-  'vueVersion': '3',
-  'h5': {
-    router: {
-      base: '/',
-    },
-  },
-}
-
+// 最简可运行配置
+const manifest = { }
 const pages = {
-  globalStyle: {
-    navigationStyle: 'default',
-    navigationBarTitleText: 'unibest',
-    navigationBarBackgroundColor: '#f8f8f8',
-    navigationBarTextStyle: 'black',
-    backgroundColor: '#FFFFFF',
-  },
-  easycom: {
-    autoscan: true,
-    custom: {
-      '^fg-(.*)': '@/components/fg-$1/fg-$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/me/me',
-      },
-    ],
-  },
   pages: [
     {
       path: 'pages/index/index',
@@ -182,20 +29,7 @@ const pages = {
       },
     },
   ],
-  subPackages: [
-    {
-      root: 'pages-sub',
-      pages: [
-        {
-          path: 'demo/index',
-          type: 'page',
-          style: {
-            navigationBarTitleText: '分包页面',
-          },
-        },
-      ],
-    },
-  ],
+  subPackages: [],
 }
 
 // 使用修复后的 __dirname 来解析文件路径
@@ -209,11 +43,11 @@ if (!fs.existsSync(srcDir)) {
 }
 
 // 如果 src/manifest.json 不存在,就创建它;存在就不处理,以免覆盖
-if (!fs.existsSync(manifestPath)) {
+if (!fs.existsSync(manifestPath) || fs.statSync(manifestPath).size === 0) {
   fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
 }
 
 // 如果 src/pages.json 不存在,就创建它;存在就不处理,以免覆盖
-if (!fs.existsSync(pagesPath)) {
+if (!fs.existsSync(pagesPath) || fs.statSync(pagesPath).size === 0) {
   fs.writeFileSync(pagesPath, JSON.stringify(pages, null, 2))
 }

+ 0 - 6
scripts/postupgrade.js

@@ -13,19 +13,13 @@ const execPromise = promisify(exec)
 
 // 定义要执行的命令
 const dependencies = [
-  '@dcloudio/uni-app-harmony',
   // TODO: 如果不需要某个平台的小程序,请手动删除或注释掉
-  '@dcloudio/uni-mp-alipay',
   '@dcloudio/uni-mp-baidu',
   '@dcloudio/uni-mp-jd',
   '@dcloudio/uni-mp-kuaishou',
-  '@dcloudio/uni-mp-lark',
   '@dcloudio/uni-mp-qq',
-  '@dcloudio/uni-mp-toutiao',
   '@dcloudio/uni-mp-xhs',
   '@dcloudio/uni-quickapp-webview',
-  // i18n模板要注释掉下面的
-  'vue-i18n',
 ]
 
 /**

+ 2 - 7
src/App.ku.vue

@@ -1,12 +1,9 @@
 <script setup lang="ts">
 import { ref } from 'vue'
-import { useThemeStore } from '@/store'
 import FgTabbar from '@/tabbar/index.vue'
 import { isPageTabbar } from './tabbar/store'
 import { currRoute } from './utils'
 
-const themeStore = useThemeStore()
-
 const isCurrentPageTabbar = ref(true)
 onShow(() => {
   console.log('App.ku.vue onShow', currRoute())
@@ -31,7 +28,7 @@ defineExpose({
 </script>
 
 <template>
-  <wd-config-provider :theme-vars="themeStore.themeVars" :theme="themeStore.theme">
+  <view>
     <!-- 这个先隐藏了,知道这样用就行 -->
     <view class="hidden text-center">
       {{ helloKuRoot }},这里可以配置全局的东西
@@ -40,7 +37,5 @@ defineExpose({
     <KuRootView />
 
     <FgTabbar v-if="isCurrentPageTabbar" />
-    <wd-toast />
-    <wd-message-box />
-  </wd-config-provider>
+  </view>
 </template>

+ 2 - 13
src/App.vue

@@ -3,10 +3,10 @@ import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
 import { navigateToInterceptor } from '@/router/interceptor'
 
 onLaunch((options) => {
-  console.log('App Launch', options)
+  console.log('App.vue onLaunch', options)
 })
 onShow((options) => {
-  console.log('App Show', options)
+  console.log('App.vue onShow', options)
   // 处理直接进入页面路由的情况:如h5直接输入路由、微信小程序分享后进入等
   // https://github.com/unibest-tech/unibest/issues/192
   if (options?.path) {
@@ -22,16 +22,5 @@ onHide(() => {
 </script>
 
 <style lang="scss">
-swiper,
-scroll-view {
-  flex: 1;
-  height: 100%;
-  overflow: hidden;
-}
 
-image {
-  width: 100%;
-  height: 100%;
-  vertical-align: middle;
-}
 </style>

+ 116 - 0
src/hooks/useScroll.md

@@ -0,0 +1,116 @@
+# 上拉刷新和下拉加载更多
+
+在 unibest 框架中,我们通过组合 `useScroll` Hook 可结合 `scroll-view` 组件来轻松实现上拉刷新和下拉加载更多的功能。
+场景一 页面滚动
+
+```
+definePage({
+  style: {
+    navigationBarTitleText: '上拉刷新和下拉加载更多',
+    enablePullDownRefresh: true,
+    onReachBottomDistance: 100,
+  },
+})
+```
+
+场景二 局部滚动 结合 `scroll-view`
+
+## 关键文件
+
+- `src/hooks/useScroll.ts`: 提供了核心的滚动逻辑处理 Hook。
+- `src/pages-sub/demo/scroll.vue`: 一个具体的实现示例页面。
+
+## `useScroll` Hook
+
+`useScroll` 是一个 Vue Composition API Hook,它封装了处理下拉刷新和上拉加载的通用逻辑。
+
+### 主要功能
+
+- **管理加载状态**: 自动处理 `loading`(加载中)、`finished`(已加载全部)和 `error`(加载失败)等状态。
+- **分页逻辑**: 内部维护分页参数(页码 `page` 和每页数量 `pageSize`)。
+- **事件处理**: 提供 `onScrollToLower`(滚动到底部)、`onRefresherRefresh`(下拉刷新)等方法,用于在视图层触发。
+- **数据合并**: 自动将新加载的数据追加到现有列表 `list` 中。
+
+### 使用方法
+
+```typescript
+import { useScroll } from '@/hooks/useScroll'
+import { getList } from '@/service/list' // 你的数据请求API
+
+const {
+  list, // 响应式的数据列表
+  loading, // 是否加载中
+  finished, // 是否已全部加载
+  error, // 是否加载失败
+  onScrollToLower, // 滚动到底部时触发的事件
+  onRefresherRefresh, // 下拉刷新时触发的事件
+} = useScroll(getList) // 将获取数据的API函数传入
+```
+
+## `scroll-view` 组件
+
+`scroll-view` 是 uni-app 提供的可滚动视图区域组件,它提供了一系列属性来支持下拉刷新和上拉加载。
+
+### 关键属性
+
+- `scroll-y`: 允许纵向滚动。
+- `refresher-enabled`: 启用下拉刷新。
+- `refresher-triggered`: 控制下拉刷新动画的显示与隐藏,通过 `loading` 状态绑定。
+- `@scrolltolower`: 滚动到底部时触发的事件,绑定 `onScrollToLower` 方法。
+- `@refresherrefresh`: 触发下拉刷新时触发的事件,绑定 `onRefresherRefresh` 方法。
+
+## 示例代码
+
+以下是 `src/pages-sub/demo/scroll.vue` 中的核心代码,展示了如何将 `useScroll` 和 `scroll-view` 结合使用。
+
+```vue
+<template>
+  <view class="scroll-page">
+    <scroll-view
+      class="scroll-view"
+      scroll-y
+      :refresher-enabled="true"
+      :refresher-triggered="loading"
+      @scrolltolower="onScrollToLower"
+      @refresherrefresh="onRefresherRefresh"
+    >
+      <view v-for="item in list" :key="item.id" class="scroll-item">
+        {{ item.name }}
+      </view>
+
+      <!-- 加载状态提示 -->
+      <view v-if="loading" class="loading-tip">加载中...</view>
+      <view v-if="finished" class="finished-tip">没有更多了</view>
+      <view v-if="error" class="error-tip">加载失败,请重试</view>
+    </scroll-view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { useScroll } from '@/hooks/useScroll'
+import { getList } from '@/service/list'
+
+const { list, loading, finished, error, onScrollToLower, onRefresherRefresh } = useScroll(getList)
+</script>
+
+<style scoped>
+/* 样式省略 */
+.scroll-page, .scroll-view {
+  height: 100%;
+}
+</style>
+```
+
+## 实现步骤总结
+
+1.  **创建API**: 确保你有一个返回分页数据的API请求函数(例如 `getList`),它应该接受页码和页面大小作为参数。
+2.  **调用 `useScroll`**: 在你的页面脚本中,导入并调用 `useScroll` Hook,将你的API函数作为参数传入。
+3.  **模板绑定**:
+    -   使用 `scroll-view` 组件作为滚动容器。
+    -   将其 `refresher-triggered` 属性绑定到 `useScroll` 返回的 `loading` 状态。
+    -   将其 `@scrolltolower` 事件绑定到 `onScrollToLower` 方法。
+    -   将其 `@refresherrefresh` 事件绑定到 `onRefresherRefresh` 方法。
+4.  **渲染列表**: 使用 `v-for` 指令渲染 `useScroll` 返回的 `list` 数组。
+5.  **添加加载提示**: 根据 `loading`, `finished`, `error` 状态,在列表底部显示不同的提示信息,提升用户体验。
+
+通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。

+ 2 - 2
src/http/alova.ts

@@ -4,7 +4,7 @@ import AdapterUniapp from '@alova/adapter-uniapp'
 import { createAlova } from 'alova'
 import { createServerTokenAuthentication } from 'alova/client'
 import VueHook from 'alova/vue'
-import { LOGIN_PAGE } from '@/router/config'
+import { toLoginPage } from '@/utils/toLoginPage'
 import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
 
 // 配置动态Tag
@@ -31,7 +31,7 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
       }
       catch (error) {
         // 切换到登录页
-        await uni.reLaunch({ url: LOGIN_PAGE })
+        toLoginPage({ mode: 'reLaunch' })
         throw error
       }
     },

+ 8 - 5
src/http/http.ts

@@ -1,9 +1,9 @@
 import type { IDoubleTokenRes } from '@/api/types/login'
 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 { toLoginPage } from '@/utils/toLoginPage'
 import { ResultEnum } from './tools/enum'
 
 // 刷新 token 状态管理
@@ -32,7 +32,7 @@ export function http<T>(options: CustomRequestOptions) {
           if (!isDoubleTokenMode) {
             // 未启用双token策略,清理用户信息,跳转到登录页
             tokenStore.logout()
-            uni.navigateTo({ url: LOGIN_PAGE })
+            toLoginPage()
             return reject(res)
           }
 
@@ -80,7 +80,7 @@ export function http<T>(options: CustomRequestOptions) {
               await tokenStore.logout()
               // 跳转到登录页
               setTimeout(() => {
-                uni.navigateTo({ url: LOGIN_PAGE })
+                toLoginPage()
               }, 2000)
             }
             finally {
@@ -96,9 +96,12 @@ export function http<T>(options: CustomRequestOptions) {
         if (res.statusCode >= 200 && res.statusCode < 300) {
           // 处理业务逻辑错误
           if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
-            throw new Error(`请求错误[${code}]:${responseData.message || responseData.msg}`)
+            uni.showToast({
+              icon: 'none',
+              title: responseData.msg || responseData.message || '请求错误',
+            })
           }
-          return resolve(responseData.data as T)
+          return resolve(responseData.data)
         }
 
         // 处理其他错误

+ 3 - 0
src/http/types.ts

@@ -7,6 +7,9 @@ export type CustomRequestOptions = UniApp.RequestOptions & {
   hideErrorToast?: boolean
 } & IUniUploadFileOptions // 添加uni.uploadFile参数类型
 
+/** 主要提供给 openapi-ts-request 生成的代码使用 */
+export type CustomRequestOptions_ = Omit<CustomRequestOptions, 'url'>
+
 export interface HttpRequestResult<T> {
   promise: Promise<T>
   requestTask: UniApp.RequestTask

+ 2 - 2
src/http/vue-query.ts

@@ -4,7 +4,7 @@ import { http } from './http'
 /*
  * openapi-ts-request 工具的 request 跨客户端适配方法
  */
-export default function request<T = unknown>(
+export default function request<T extends { data?: any }>(
   url: string,
   options: Omit<CustomRequestOptions, 'url'> & {
     params?: Record<string, unknown>
@@ -26,5 +26,5 @@ export default function request<T = unknown>(
     delete requestOptions.headers
   }
 
-  return http<T>(requestOptions)
+  return http<T['data']>(requestOptions)
 }

+ 0 - 7
src/layouts/default.vue

@@ -1,10 +1,3 @@
-<script lang="ts" setup>
-const testUniLayoutExposedData = ref('testUniLayoutExposedData')
-defineExpose({
-  testUniLayoutExposedData,
-})
-</script>
-
 <template>
   <slot />
 </template>

+ 0 - 3
src/pages-fg/404/README.md

@@ -1,3 +0,0 @@
-# 404 页面
-
-`404页面` 只有在路由不存在时才会显示,如果您不需要可以删除该页面。但是建议保留。

+ 0 - 30
src/pages-fg/404/index.vue

@@ -1,30 +0,0 @@
-<script lang="ts" setup>
-import { HOME_PAGE } from '@/utils'
-
-definePage({
-  style: {
-    // 'custom' 表示开启自定义导航栏,默认 'default'
-    navigationStyle: 'custom',
-  },
-})
-
-function goBack() {
-  // 当pages.config.ts中配置了tabbar页面时,使用switchTab切换到首页
-  // 否则使用navigateTo返回首页
-  uni.switchTab({ url: HOME_PAGE })
-}
-</script>
-
-<template>
-  <view class="h-screen flex flex-col items-center justify-center">
-    <view> 404 </view>
-    <view> 页面不存在 </view>
-    <button class="mt-6 w-40 text-center" @click="goBack">
-      返回首页
-    </button>
-  </view>
-</template>
-
-<style lang="scss" scoped>
-//
-</style>

+ 0 - 3
src/pages-fg/REAME.md

@@ -1,3 +0,0 @@
-# pages-fg 说明
-
-为了尽量减少主包的大小,一些无关紧要但经常需要的页面(如登录页、注册页、404页等)放在了 `pages-fg` 目录下。

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

@@ -1,20 +0,0 @@
-# 登录页
-需要输入账号、密码/验证码的登录页。
-
-## 适用性
-
-本页面主要用于 `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` 里面,默认会在登录后自动重定向到来源/配置的页面。
-
-如果与您的业务不符,您可以自行修改。

+ 0 - 87
src/pages-fg/login/login.vue

@@ -1,87 +0,0 @@
-<script lang="ts" setup>
-import { useTokenStore } from '@/store/token'
-import { useUserStore } from '@/store/user'
-import { tabbarList } from '@/tabbar/config'
-import { isPageTabbar } from '@/tabbar/store'
-import { ensureDecodeURIComponent } from '@/utils'
-import { parseUrlToObj } from '@/utils/index'
-
-definePage({
-  style: {
-    navigationBarTitleText: '登录',
-  },
-})
-
-const redirectUrl = ref('')
-onLoad((options) => {
-  console.log('login options: ', options)
-  if (options.redirect) {
-    redirectUrl.value = ensureDecodeURIComponent(options.redirect)
-  }
-  else {
-    redirectUrl.value = tabbarList[0].pagePath
-  }
-  console.log('redirectUrl.value: ', redirectUrl.value)
-})
-
-const userStore = useUserStore()
-const tokenStore = useTokenStore()
-async function doLogin() {
-  if (tokenStore.hasLogin) {
-    uni.navigateBack()
-    return
-  }
-  try {
-    // 调用登录接口
-    await tokenStore.login({
-      username: '菲鸽',
-      password: '123456',
-    })
-    console.log(redirectUrl.value)
-  }
-  catch (error) {
-    console.log('登录失败', error)
-  }
-  let path = redirectUrl.value
-  if (!path.startsWith('/')) {
-    path = `/${path}`
-  }
-  const { path: _path, query } = parseUrlToObj(path)
-  console.log('_path:', _path, 'query:', query, 'path:', path)
-  console.log('isPageTabbar(_path):', isPageTabbar(_path))
-  if (isPageTabbar(_path)) {
-    // 经过我的测试 switchTab 不能带 query 参数, 不管是放到 url  还是放到 query ,
-    // 最后跳转过去的时候都会丢失 query 信息
-    uni.switchTab({
-      url: path,
-    })
-    // uni.switchTab({
-    //   url: _path,
-    //   query,
-    // })
-  }
-  else {
-    // 自己决定是 redirectTo 还是 navigateBack
-    // uni.redirectTo({
-    //   url: path,
-    // })
-    uni.navigateBack()
-  }
-}
-</script>
-
-<template>
-  <view class="login">
-    <!-- 本页面是非MP的登录页,主要用于 h5 和 APP -->
-    <view class="text-center">
-      登录页
-    </view>
-    <button class="mt-4 w-40 text-center" @click="doLogin">
-      点击模拟登录
-    </button>
-  </view>
-</template>
-
-<style lang="scss" scoped>
-//
-</style>

+ 0 - 34
src/pages-fg/login/register.vue

@@ -1,34 +0,0 @@
-<script lang="ts" setup>
-import { LOGIN_PAGE } from '@/router/config'
-
-definePage({
-  style: {
-    navigationBarTitleText: '注册',
-  },
-})
-
-function doRegister() {
-  uni.showToast({
-    title: '注册成功',
-  })
-  // 注册成功后跳转到登录页
-  uni.navigateTo({
-    url: LOGIN_PAGE,
-  })
-}
-</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>

+ 0 - 135
src/pages-sub/about/about.vue

@@ -1,135 +0,0 @@
-<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 RequestOpenApiComp from './components/request-openapi.vue'
-import RequestComp from './components/request.vue'
-import UploadComp from './components/Upload.vue'
-import VBindCss from './components/VBindCss.vue'
-
-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 gotoLogin() {
-  if (tokenStore.hasLogin) {
-    uni.showToast({
-      title: '已登录,不能去登录页',
-      icon: 'none',
-    })
-    return
-  }
-  uni.navigateTo({
-    url: `${LOGIN_PAGE}?redirect=${encodeURIComponent('/pages-sub/about/about?a=1&b=2')}`,
-  })
-}
-function logout() {
-  // 清空用户信息
-  tokenStore.logout()
-  // 执行退出登录逻辑
-  uni.showToast({
-    title: '退出登录成功',
-    icon: 'success',
-  })
-}
-
-function gotoScroll() {
-  uni.navigateTo({
-    url: '/pages-sub/demo/scroll',
-  })
-}
-
-function gotoAlova() {
-  uni.navigateTo({
-    url: '/pages-sub/about/alova',
-  })
-}
-function gotoSubPage() {
-  uni.navigateTo({
-    url: '/pages-sub/demo/index',
-  })
-}
-
-// uniLayout里面的变量通过 expose 暴露出来后可以在 onReady 钩子获取到(onLoad 钩子不行)
-const uniLayout = ref()
-onLoad(() => {
-  console.log('onLoad:', uniLayout.value) // onLoad: undefined
-})
-onReady(() => {
-  console.log('onReady:', uniLayout.value) // onReady: Proxy(Object)
-  console.log('onReady:', uniLayout.value.testUniLayoutExposedData) // onReady: testUniLayoutExposedData
-})
-// 结论:第一次通过onShow获取不到,但是可以通过 onReady获取到,后面就可以通过onShow获取到了
-onShow(() => {
-  console.log('onShow:', uniLayout.value) // onReady: Proxy(Object)
-  console.log('onShow:', uniLayout.value?.testUniLayoutExposedData) // onReady: testUniLayoutExposedData
-})
-
-const uniKuRoot = ref()
-// 结论:(同上)第一次通过onShow获取不到,但是可以通过 onReady获取到,后面就可以通过onShow获取到了
-onReady(() => {
-  console.log('onReady uniKuRoot exposeRef', uniKuRoot.value?.exposeRef)
-})
-onShow(() => {
-  console.log('onShow uniKuRoot exposeRef', uniKuRoot.value?.exposeRef)
-})
-</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图片
-    </view>
-    <view class="my-2 text-center">
-      <image src="/static/images/avatar.jpg" class="h-100px w-100px" />
-    </view>
-    <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>
-    <RequestOpenApiComp />
-    <RequestComp />
-    <UploadComp />
-    <VBindCss />
-    <view class="mb-6 h-1px bg-#eee" />
-    <view class="mb-2 text-center">
-      <button type="primary" size="mini" class="w-240px" @click="gotoScroll">
-        下拉刷新和下拉加载更多
-      </button>
-      <view>简单hooks(非z-paging组件)</view>
-    </view>
-    <view class="text-center">
-      <button type="primary" size="mini" class="w-160px" @click="gotoAlova">
-        前往 alova 示例页面
-      </button>
-    </view>
-    <view class="text-center">
-      <button type="primary" size="mini" class="w-160px" @click="gotoSubPage">
-        前往分包页面
-      </button>
-    </view>
-    <view class="mt-6 text-center text-sm">
-      <view class="inline-block w-80% text-gray-400">
-        为了方便脚手架动态生成不同UI模板,本页的按钮统一使用UI库无关的原生button
-      </view>
-    </view>
-    <view class="h-6" />
-  </view>
-</template>

+ 0 - 53
src/pages-sub/about/alova.vue

@@ -1,53 +0,0 @@
-<script lang="ts" setup>
-import { useRequest } from 'alova/client'
-import { foo } from '@/api/foo-alova'
-
-definePage({
-  style: {
-    navigationBarTitleText: 'Alova 演示',
-  },
-})
-
-const initialData = undefined
-
-const { loading, data, send } = useRequest(foo, {
-  initialData,
-  immediate: true,
-})
-console.log(data)
-function reset() {
-  data.value = initialData
-}
-</script>
-
-<template>
-  <view class="p-6 text-center">
-    <button type="primary" size="mini" class="my-6 w-160px" @click="send">
-      发送请求
-    </button>
-    <view class="h-16">
-      <view v-if="loading">
-        loading...
-      </view>
-      <block v-else>
-        <view class="text-xl">
-          请求数据如下
-        </view>
-        <view class="text-green leading-8">
-          {{ JSON.stringify(data) }}
-        </view>
-      </block>
-
-      <view class="text-red">
-        {{ data?.id }}
-      </view>
-    </view>
-    <button type="default" size="mini" class="my-6 w-160px" @click="reset">
-      重置数据
-    </button>
-  </view>
-</template>
-
-<style lang="scss" scoped>
-//
-</style>

+ 0 - 25
src/pages-sub/about/components/Upload.vue

@@ -1,25 +0,0 @@
-<template>
-  <view class="p-4 text-center">
-    <button type="primary" size="mini" class="w-160px" @click="run">
-      选择图片并上传
-    </button>
-    <view v-if="loading" class="h-10 text-blue">
-      上传...
-    </view>
-    <template v-else>
-      <view class="m-2">
-        上传后返回的接口数据:
-      </view>
-      <view class="m-2">
-        {{ data }}
-      </view>
-      <view class="m-auto h-40 max-w-40">
-        <image v-if="data" :src="data.url" mode="scaleToFill" />
-      </view>
-    </template>
-  </view>
-</template>
-
-<script lang="ts" setup>
-const { loading, data, run } = useUpload()
-</script>

+ 0 - 28
src/pages-sub/about/components/VBindCss.vue

@@ -1,28 +0,0 @@
-<script lang="ts" setup>
-// root 插件更新到 1.3.4之后,都正常了。
-const testBindCssVariable = ref('red')
-function changeTestBindCssVariable() {
-  if (testBindCssVariable.value === 'red') {
-    testBindCssVariable.value = 'green'
-  }
-  else {
-    testBindCssVariable.value = 'red'
-  }
-}
-</script>
-
-<template>
-  <button class="mt-4 w-60 text-center" @click="changeTestBindCssVariable">
-    toggle v-bind css变量
-  </button>
-  <view class="test-css my-2 text-center">
-    测试v-bind css变量的具体文案
-  </view>
-</template>
-
-<style lang="scss" scoped>
-.test-css {
-  color: v-bind(testBindCssVariable);
-  font-size: 24px;
-}
-</style>

+ 0 - 64
src/pages-sub/about/components/request-openapi.vue

@@ -1,64 +0,0 @@
-<script lang="ts" setup>
-import type { UserItem } from '@/service'
-import { infoUsingGet, listAllUsingGet } from '@/service'
-
-const loading = ref(false)
-const error = ref<Error | null>(null)
-const data = ref<UserItem>()
-
-// openapi 请求示例
-async function getUserInfo() {
-  try {
-    loading.value = true
-    const res = await infoUsingGet({})
-    console.log(res)
-    data.value = res
-    error.value = null
-  }
-  catch (err) {
-    error.value = err as Error
-    data.value = null
-  }
-  finally {
-    loading.value = false
-  }
-}
-
-// openapi + useRequest 请求示例
-const { data: data2, loading: loading2, run } = useRequest(() => listAllUsingGet({}), {
-  immediate: false,
-})
-</script>
-
-<template>
-  <view class="p-6 text-center">
-    <view class="my-4 text-center">
-      1)直接使用 openapi 生成的请求
-    </view>
-    <view class="my-4 text-center">
-      <button type="primary" size="mini" class="w-160px" @click="getUserInfo">
-        发送请求
-      </button>
-      <view class="text-xl">
-        请求数据如下
-      </view>
-      <view class="text-green leading-8">
-        {{ JSON.stringify(data) }}
-      </view>
-    </view>
-    <view class="my-4 text-center">
-      2)直接使用 openapi + useRequest 生成的请求
-    </view>
-    <view class="my-4 flex items-center gap-2 text-center">
-      <button type="primary" size="mini" class="w-160px" @click="run">
-        发送请求
-      </button>
-    </view>
-    <view class="text-xl">
-      请求数据如下
-    </view>
-    <view class="text-green leading-8">
-      {{ JSON.stringify(data2) }}
-    </view>
-  </view>
-</template>

+ 0 - 75
src/pages-sub/about/components/request.vue

@@ -1,75 +0,0 @@
-<script lang="ts" setup>
-import type { IFooItem } from '@/api/foo'
-import { getFooAPI } from '@/api/foo'
-
-// const initialData = {
-//   name: 'initialData',
-//   id: '1234',
-// }
-const initialData = undefined
-
-// 直接请求示例
-async function reqFooAPI() {
-  try {
-    const res = await getFooAPI('菲鸽')
-    console.log('直接请求示例res', res)
-  }
-  catch (err) {
-    console.log(err)
-  }
-}
-reqFooAPI()
-
-// 直接useRequest请求示例
-const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
-  immediate: true,
-  initialData,
-})
-
-function reset() {
-  data.value = initialData
-}
-</script>
-
-<template>
-  <view class="p-6 text-center">
-    <view class="my-2">
-      pages 里面的 vue 文件会扫描成页面,将自动添加到 pages.json 里面。
-    </view>
-    <view class="my-2 text-green-400">
-      但是 components 里面的 vue 不会。
-    </view>
-
-    <view class="my-4 text-center">
-      <button type="primary" size="mini" class="w-160px" @click="run">
-        发送请求
-      </button>
-    </view>
-    <view class="h-16">
-      <view v-if="loading">
-        loading...
-      </view>
-      <block v-else>
-        <view v-if="error instanceof Error" class="text-red leading-8">
-          错误: {{ error.message }}
-        </view>
-        <view v-else-if="error" class="text-red leading-8">
-          错误: 未知错误
-        </view>
-        <view v-else>
-          <view class="text-xl">
-            请求数据如下
-          </view>
-          <view class="text-green leading-8">
-            {{ JSON.stringify(data) }}
-          </view>
-        </view>
-      </block>
-    </view>
-    <view class="my-4 text-center">
-      <button type="warn" size="mini" class="w-160px" :disabled="!data" @click="reset">
-        重置数据
-      </button>
-    </view>
-  </view>
-</template>

+ 0 - 48
src/pages-sub/demo/components/request.vue

@@ -1,48 +0,0 @@
-<script lang="ts" setup>
-import type { IFooItem } from '@/api/foo'
-import { getFooAPI } from '@/api/foo'
-
-const recommendUrl = ref('http://laf.run/signup?code=ohaOgIX')
-
-// const initialData = {
-//   name: 'initialData',
-//   id: '1234',
-// }
-const initialData = undefined
-const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
-  immediate: true,
-  initialData,
-})
-
-function reset() {
-  data.value = initialData
-}
-</script>
-
-<template>
-  <view class="p-6 text-center">
-    <view class="my-2 text-center">
-      <button type="primary" size="mini" class="w-160px" @click="run">
-        发送请求
-      </button>
-    </view>
-    <view class="h-16">
-      <view v-if="loading">
-        loading...
-      </view>
-      <block v-else>
-        <view class="text-xl">
-          请求数据如下
-        </view>
-        <view class="text-green leading-8">
-          {{ JSON.stringify(data) }}
-        </view>
-      </block>
-    </view>
-    <view class="my-6 text-center">
-      <button type="warn" size="mini" class="w-160px" :disabled="!data" @click="reset">
-        重置数据
-      </button>
-    </view>
-  </view>
-</template>

+ 0 - 40
src/pages-sub/demo/index.vue

@@ -1,40 +0,0 @@
-<script lang="ts" setup>
-// code here
-import RequestComp from './components/request.vue'
-
-definePage({
-  style: {
-    navigationBarTitleText: '分包页面',
-  },
-})
-
-function gotoScroll() {
-  uni.navigateTo({
-    url: '/pages-sub/demo/scroll',
-  })
-}
-</script>
-
-<template>
-  <view class="text-center">
-    <view class="m-8">
-      http://localhost:9000/#/pages-sub/demo/index
-    </view>
-    <view class="my-4 text-green-500">
-      分包页面demo
-    </view>
-    <view class="text-blue-500">
-      分包页面里面的components示例
-    </view>
-    <button class="my-4" type="primary" size="mini" @click="gotoScroll">
-      跳转到上拉刷新和下拉加载更多
-    </button>
-    <view>
-      <RequestComp />
-    </view>
-  </view>
-</template>
-
-<style lang="scss" scoped>
-//
-</style>

+ 0 - 72
src/pages-sub/demo/scroll.vue

@@ -1,72 +0,0 @@
-<script setup lang="ts">
-// uniapp 页面生命周期
-import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
-
-import { useScroll } from '@/hooks/useScroll'
-
-definePage({
-  style: {
-    navigationBarTitleText: '下拉刷新和下拉加载更多',
-    enablePullDownRefresh: true,
-    onReachBottomDistance: 100,
-  },
-})
-
-// 模拟异步获取数据的函数
-function mockFetchData(page: number, pageSize: number): Promise<{ id: number, name: string }[]> {
-  return new Promise((resolve) => {
-    setTimeout(() => {
-      if (page > 5) {
-        // 模拟没有更多数据
-        resolve([])
-        return
-      }
-      const data = Array.from({ length: pageSize }, (_, i) => ({
-        id: (page - 1) * pageSize + i + 1,
-        name: `item ${(page - 1) * pageSize + i + 1}`,
-      }))
-      resolve(data)
-    }, 1000)
-  })
-}
-
-const { list, loading, finished, error, refresh, loadMore } = useScroll({
-  fetchData: mockFetchData,
-  pageSize: 10,
-})
-
-onPullDownRefresh(async () => {
-  console.log('onPullDownRefresh')
-  console.log('onPullDownRefresh')
-  console.log('onPullDownRefresh')
-  await refresh()
-  uni.stopPullDownRefresh()
-})
-
-onReachBottom(() => {
-  loadMore()
-})
-</script>
-
-<template>
-  <view class="h-screen p-4">
-    <view v-if="error" class="text-center text-red-500">
-      加载失败,请重试
-    </view>
-    <view v-else>
-      <view
-        v-for="item in list"
-        :key="item.id"
-        class="my-2 h-20 flex items-center justify-center rounded bg-gray-100"
-      >
-        {{ item.name }}
-      </view>
-      <view v-if="loading" class="py-4 text-center text-gray-500">
-        加载中...
-      </view>
-      <view v-if="finished" class="py-4 text-center text-gray-500">
-        没有更多了
-      </view>
-    </view>
-  </view>
-</template>

+ 1 - 62
src/pages/index/index.vue

@@ -1,7 +1,4 @@
 <script lang="ts" setup>
-import { useThemeStore } from '@/store'
-import { safeAreaInsets } from '@/utils/systemInfo'
-
 defineOptions({
   name: 'Home',
 })
@@ -15,8 +12,6 @@ definePage({
   },
 })
 
-const themeStore = useThemeStore()
-
 const description = ref(
   'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite5 + UnoCss + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
 )
@@ -25,18 +20,10 @@ console.log('index/index 首页打印了')
 onLoad(() => {
   console.log('测试 uni API 自动引入: onLoad')
 })
-
-// #region gotoAbout
-function gotoAbout() {
-  uni.navigateTo({
-    url: '/pages-sub/about/about',
-  })
-}
-// #endregion
 </script>
 
 <template>
-  <view class="bg-white px-4 pt-2" :style="{ marginTop: `${safeAreaInsets?.top}px` }">
+  <view class="bg-white px-4 pt-safe">
     <view class="mt-10">
       <image src="/static/logo.svg" alt="" class="mx-auto block h-28 w-28" />
     </view>
@@ -62,53 +49,5 @@ function gotoAbout() {
         https://unibest.tech
       </text>
     </view>
-
-    <!-- #ifdef H5 -->
-    <view class="mt-4 text-center">
-      <a href="https://unibest.tech/base/3-plugin" target="_blank" class="text-green-500">
-        新手请看必看章节1:
-      </a>
-    </view>
-    <!-- #endif -->
-    <!-- #ifdef MP-WEIXIN -->
-    <view class="mt-4 text-center">
-      新手请看必看章节1:
-      <text class="text-green-500">
-        https://unibest.tech/base/3-plugin
-      </text>
-    </view>
-    <!-- #endif -->
-    <!-- #ifdef H5 -->
-    <view class="mt-4 text-center">
-      <a href="https://unibest.tech/base/14-faq" target="_blank" class="text-green-500">
-        新手请看必看章节2:
-      </a>
-    </view>
-    <!-- #endif -->
-    <!-- #ifdef MP-WEIXIN -->
-    <view class="mt-4 text-center">
-      新手请看必看章节2:
-      <text class="text-green-500">
-        https://unibest.tech/base/14-faq
-      </text>
-    </view>
-    <!-- #endif -->
-
-    <view class="mt-4 text-center">
-      <wd-button type="primary" class="ml-2" @click="themeStore.setThemeVars({ colorTheme: 'red' })">
-        设置主题变量
-      </wd-button>
-    </view>
-    <view class="mt-4 text-center">
-      UI组件官网:<text class="text-green-500">
-        https://wot-design-uni.cn
-      </text>
-    </view>
-    <view class="mt-4 text-center">
-      <wd-button type="primary" class="ml-2" @click="gotoAbout">
-        前往示例页
-      </wd-button>
-    </view>
-    <view class="h-6" />
   </view>
 </template>

+ 2 - 203
src/pages/me/me.vue

@@ -1,214 +1,13 @@
 <script lang="ts" setup>
-import type { IUploadSuccessInfo } from '@/api/types/login'
-import { storeToRefs } from 'pinia'
-import { LOGIN_PAGE } from '@/router/config'
-import { useUserStore } from '@/store'
-import { useTokenStore } from '@/store/token'
-import { useUpload } from '@/utils/uploadFile'
-
 definePage({
   style: {
     navigationBarTitleText: '我的',
   },
 })
-
-const userStore = useUserStore()
-const tokenStore = useTokenStore()
-// 使用storeToRefs解构userInfo
-const { userInfo } = storeToRefs(userStore)
-
-// #ifndef MP-WEIXIN
-// 上传头像
-const { run: uploadAvatar } = useUpload<IUploadSuccessInfo>(
-  '/upload',
-  {},
-  {
-    onSuccess: (res) => {
-      console.log('h5头像上传成功', res)
-      useUserStore().setUserAvatar(res.url)
-    },
-  },
-)
-// #endif
-
-// 微信小程序下登录
-async function handleLogin() {
-  // #ifdef MP-WEIXIN
-  // 微信登录
-  await tokenStore.wxLogin()
-
-  // #endif
-  // #ifndef MP-WEIXIN
-  uni.navigateTo({
-    url: `${LOGIN_PAGE}?redirect=${encodeURIComponent('/pages/me/me')}`,
-  })
-  // #endif
-}
-
-// #ifdef MP-WEIXIN
-
-// 微信小程序下选择头像事件
-function onChooseAvatar(e: any) {
-  console.log('选择头像', e.detail)
-  const { avatarUrl } = e.detail
-  const { run } = useUpload<IUploadSuccessInfo>(
-    '/upload',
-    {},
-    {
-      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) {
-        // 清空用户信息
-        useTokenStore().logout()
-        // 执行退出登录逻辑
-        uni.showToast({
-          title: '退出登录成功',
-          icon: 'success',
-        })
-        // #ifdef MP-WEIXIN
-        // 微信小程序,去首页
-        // uni.reLaunch({ url: '/pages/index/index' })
-        // #endif
-        // #ifndef MP-WEIXIN
-        // 非微信小程序,去登录页
-        // uni.navigateTo({ url: LOGIN_PAGE })
-        // #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.userId }}
-        </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="tokenStore.hasLogin" type="warn" class="w-full" @click="handleLogout">
-          退出登录
-        </button>
-        <button v-else type="primary" class="w-full" @click="handleLogin">
-          登录
-        </button>
-      </view>
-    </view>
+  <view class="mt-10 text-center text-green-500">
+    我的页面
   </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;
-  width: 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>

+ 0 - 31
src/router/config.ts

@@ -1,31 +0,0 @@
-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
-
-export const LOGIN_PAGE = '/pages-fg/login/login'
-export const REGISTER_PAGE = '/pages-fg/login/register'
-export const NOT_FOUND_PAGE = '/pages-fg/404/index'
-
-export const LOGIN_PAGE_LIST = [LOGIN_PAGE, REGISTER_PAGE]
-
-// 在 definePage 里面配置了 excludeLoginPath 的页面,功能与 EXCLUDE_LOGIN_PATH_LIST 相同
-export const excludeLoginPathList = getAllPages('excludeLoginPath').map(page => page.path)
-
-// 排除在外的列表,白名单策略指白名单列表,黑名单策略指黑名单列表
-// TODO: 2/3 在 definePage 配置 excludeLoginPath,或者在下面配置 EXCLUDE_LOGIN_PATH_LIST
-export const EXCLUDE_LOGIN_PATH_LIST = [
-  '/pages/xxx/index', // 示例值
-  '/pages-sub/xxx/index', // 示例值
-  ...excludeLoginPathList, // 都是以 / 开头的 path
-]
-
-// 在小程序里面是否使用H5的登录页,默认为 false
-// 如果为 true 则复用 h5 的登录逻辑
-// TODO: 3/3 确定自己的登录页是否需要在小程序里面使用
-export const LOGIN_PAGE_ENABLE_IN_MP = false

+ 9 - 77
src/router/interceptor.ts

@@ -1,23 +1,12 @@
-import { isMp } from '@uni-helper/uni-env'
 /**
  * by 菲鸽 on 2025-08-19
  * 路由拦截,通常也是登录拦截
  * 黑、白名单的配置,请看 config.ts 文件, EXCLUDE_LOGIN_PATH_LIST
  */
-import { useTokenStore } from '@/store/token'
-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, NOT_FOUND_PAGE } from './config'
+import { tabbarStore } from '@/tabbar/store'
+import { getAllPages, getLastPage, parseUrlToObj } from '@/utils/index'
 
 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 不同
@@ -44,76 +33,19 @@ export const navigateToInterceptor = {
     }
 
     // 处理路由不存在的情况
-    if (getAllPages().every(page => page.path !== path) && path !== '/') {
+    if (path !== '/' && !getAllPages().some(page => page.path !== path)) {
       console.warn('路由不存在:', path)
-      uni.navigateTo({ url: NOT_FOUND_PAGE })
       return false // 明确表示阻止原路由继续执行
     }
 
-    // 处理直接进入路由非首页时,tabbarIndex 不正确的问题
-    tabbarStore.setAutoCurIdx(path)
-
-    // 小程序里面使用平台自带的登录,则不走下面的逻辑
-    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 (Object.keys(myQuery).length) {
-      fullPath += `?${Object.keys(myQuery).map(key => `${key}=${myQuery[key]}`).join('&')}`
-    }
-    const redirectUrl = `${LOGIN_PAGE}?redirect=${encodeURIComponent(fullPath)}`
-
-    // #region 1/2 默认需要登录的情况(白名单策略) ---------------------------
-    if (isNeedLoginMode) {
-      // 需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示白名单,可以直接通过
-      if (judgeIsExcludePath(path)) {
-        return true // 明确表示允许路由继续执行
-      }
-      // 否则需要重定向到登录页
-      else {
-        if (path === LOGIN_PAGE) {
-          return true // 明确表示允许路由继续执行
-        }
-        FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) redirectUrl:', redirectUrl)
-        uni.navigateTo({ url: redirectUrl })
-        return false // 明确表示阻止原路由继续执行
-      }
+    // 插件页面
+    if (url.startsWith('plugin://')) {
+      FG_LOG_ENABLE && console.log('路由拦截器 4: plugin:// 路径 ==>', url)
+      path = url
     }
-    // #endregion 1/2 默认需要登录的情况(白名单策略) ---------------------------
 
-    // #region 2/2 默认不需要登录的情况(黑名单策略) ---------------------------
-    else {
-      // 不需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示黑名单,需要重定向到登录页
-      if (judgeIsExcludePath(path)) {
-        FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) redirectUrl:', redirectUrl)
-        uni.navigateTo({ url: redirectUrl })
-        return false // 修改为false,阻止原路由继续执行
-      }
-      return true // 明确表示允许路由继续执行
-    }
-    // #endregion 2/2 默认不需要登录的情况(黑名单策略) ---------------------------
+    // 处理直接进入路由非首页时,tabbarIndex 不正确的问题
+    tabbarStore.setAutoCurIdx(path)
   },
 }
 

+ 3 - 7
src/service/info.ts

@@ -1,17 +1,13 @@
 /* eslint-disable */
 // @ts-ignore
 import request from '@/http/vue-query';
-import { CustomRequestOptions } from '@/http/types';
+import { CustomRequestOptions_ } from '@/http/types';
 
 import * as API from './types';
 
 /** 用户信息 GET /user/info */
-export async function infoUsingGet({
-  options,
-}: {
-  options?: CustomRequestOptions;
-}) {
-  return request<API.UserItem>('/user/info', {
+export function infoUsingGet({ options }: { options?: CustomRequestOptions_ }) {
+  return request<API.InfoUsingGetResponse>('/user/info', {
     method: 'GET',
     ...(options || {}),
   });

+ 4 - 5
src/service/listAll.ts

@@ -1,18 +1,17 @@
 /* eslint-disable */
 // @ts-ignore
 import request from '@/http/vue-query';
-import { CustomRequestOptions } from '@/http/types';
+import { CustomRequestOptions_ } from '@/http/types';
 
 import * as API from './types';
-const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
 
 /** 用户列表 GET /user/listAll */
-export async function listAllUsingGet({
+export function listAllUsingGet({
   options,
 }: {
-  options?: CustomRequestOptions;
+  options?: CustomRequestOptions_;
 }) {
-  return request<API.UserItem[]>('/user/listAll', {
+  return request<API.ListAllUsingGetResponse>('/user/listAll', {
     method: 'GET',
     ...(options || {}),
   });

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
src/static/my-icons/copyright.svg


+ 3 - 2
src/store/index.ts

@@ -1,4 +1,4 @@
-import { createPinia } from 'pinia'
+import { createPinia, setActivePinia } from 'pinia'
 import { createPersistedState } from 'pinia-plugin-persistedstate' // 数据持久化
 
 const store = createPinia()
@@ -10,10 +10,11 @@ store.use(
     },
   }),
 )
+// 立即激活 Pinia 实例, 这样即使在 app.use(store)之前调用 store 也能正常工作 (解决APP端白屏问题)
+setActivePinia(store)
 
 export default store
 
 // 模块统一导出
-export * from './theme'
 export * from './token'
 export * from './user'

+ 0 - 42
src/store/theme.ts

@@ -1,42 +0,0 @@
-import type { ConfigProviderThemeVars } from 'wot-design-uni'
-
-import { defineStore } from 'pinia'
-
-export const useThemeStore = defineStore(
-  'theme-store',
-  () => {
-    /** 主题 */
-    const theme = ref<'light' | 'dark'>('light')
-
-    /** 主题变量 */
-    const themeVars = ref<ConfigProviderThemeVars>({
-      // colorTheme: 'red',
-      // buttonPrimaryBgColor: '#07c160',
-      // buttonPrimaryColor: '#07c160',
-    })
-
-    /** 设置主题变量 */
-    const setThemeVars = (partialVars: Partial<ConfigProviderThemeVars>) => {
-      themeVars.value = { ...themeVars.value, ...partialVars }
-    }
-
-    /** 切换主题 */
-    const toggleTheme = () => {
-      theme.value = theme.value === 'light' ? 'dark' : 'light'
-    }
-
-    return {
-      /** 设置主题变量 */
-      setThemeVars,
-      /** 切换主题 */
-      toggleTheme,
-      /** 主题变量 */
-      themeVars,
-      /** 主题 */
-      theme,
-    }
-  },
-  {
-    persist: true,
-  },
-)

+ 11 - 5
src/tabbar/README.md

@@ -5,17 +5,19 @@
 `tabbar` 分为 `4 种` 情况:
 
 - 0 `无 tabbar`,只有一个页面入口,底部无 `tabbar` 显示;常用语临时活动页。
-- 1 `原生 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。
-  - 优势:原生自带的 tabbar,最先渲染,有缓存。
+
+- 1 `原生 tabbar`,使用 `switchTab` 切换 `tabbar`,`tabbar` 页面有缓存。
+  - 优势:原生自带的 `tabbar`,最先渲染,有缓存。
   - 劣势:只能使用 2 组图片来切换选中和非选中状态,修改颜色只能重新换图片(或者用 iconfont)。
-- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。
+
+- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 `tabbar`,`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。
   - 优势:可以随意配置自己想要的 `svg icon`,切换字体颜色方便。有缓存。可以实现各种花里胡哨的动效等。
-  - 劣势:首次点击 tababr 会闪烁。
+  - 劣势:首次点击 `tabbar` 会闪烁。
+
 - 3 `无缓存自定义 tabbar`,使用 `navigateTo` 切换 `tabbar`,`tabbar` 页面无缓存。使用了第三方 UI 库的 `tabbar` 组件。
   - 优势:可以随意配置自己想要的 svg icon,切换字体颜色方便。可以实现各种花里胡哨的动效等。
   - 劣势:首次点击 `tababr` 会闪烁,无缓存。
 
-
 > 注意:花里胡哨的效果需要自己实现,本模版不提供。
 
 ## tabbar 配置说明
@@ -28,6 +30,7 @@
 `config.ts` 专门配置 `nativeTabbarList` 和 `customTabbarList` 的相关信息,请按照文件里面的注释配置相关项。
 
 使用 `原生tabbar` 时,不需要关心下面2个文件:
+
 - `store.ts` ,专门给 `自定义 tabbar` 提供状态管理,代码几乎不需要修改。
 - `index.vue` ,专门给 `自定义 tabbar` 提供渲染逻辑,代码可以稍微修改,以符合自己的需求。
 
@@ -42,6 +45,7 @@
     "icon": "home",
   }
   ```
+
 - unocss 图标
 
  ```js
@@ -54,6 +58,7 @@
     icon: 'i-carbon-code',
   }
   ```
+
 - iconfont 图标
 
  ```js
@@ -64,6 +69,7 @@
     icon: 'iconfont icon-my',
   }
   ```
+
 - image 本地图片
 
  ```js

+ 1 - 14
src/tabbar/config.ts

@@ -1,4 +1,5 @@
 import type { TabBar } from '@uni-helper/vite-plugin-uni-pages'
+import type { CustomTabBarItem, NativeTabBarItem } from './types'
 
 /**
  * tabbar 选择的策略,更详细的介绍见 tabbar.md 文件
@@ -22,8 +23,6 @@ export const TABBAR_STRATEGY_MAP = {
 // 如果是使用 CUSTOM_TABBAR(2,3),只需要配置 customTabbarList,nativeTabbarList 不生效
 export const selectedTabbarStrategy = TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE
 
-type NativeTabBarItem = TabBar['list'][number]
-
 // TODO: 2/3. 使用 NATIVE_TABBAR 时,更新下面的 tabbar 配置
 export const nativeTabbarList: NativeTabBarItem[] = [
   {
@@ -40,18 +39,6 @@ export const nativeTabbarList: NativeTabBarItem[] = [
   },
 ]
 
-// badge 显示一个数字或 小红点(样式可以直接在 tabbar/index.vue 里面修改)
-export type CustomTabBarItemBadge = number | 'dot'
-
-export interface CustomTabBarItem {
-  text: string
-  pagePath: string
-  iconType: 'uiLib' | 'unocss' | 'iconfont' | 'image' // 不建议用 image 模式,需要配置2张图
-  icon: any // 其实是 string 类型,这里是为了避免 ts 报错 (tabbar/index.vue 里面 uni-icons 那行)
-  iconActive?: string // 只有在 image 模式下才需要,传递的是高亮的图片(PS: 不建议用 image 模式)
-  badge?: CustomTabBarItemBadge
-  isBulge?: boolean // 是否是中间的鼓包tabbarItem
-}
 // TODO: 3/3. 使用 CUSTOM_TABBAR(2,3) 时,更新下面的 tabbar 配置
 // 如果需要配置鼓包,需要在 'tabbar/store.ts' 里面设置,最后在 `tabbar/index.vue` 里面更改鼓包的图片
 export const customTabbarList: CustomTabBarItem[] = [

+ 20 - 4
src/tabbar/index.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 // i-carbon-code
-import type { CustomTabBarItem } from './config'
+import type { CustomTabBarItem } from './types'
 import { customTabbarEnable, needHideNativeTabbar, tabbarCacheEnable } from './config'
 import { tabbarList, tabbarStore } from './store'
 
@@ -39,7 +39,7 @@ function handleClick(index: number) {
     uni.navigateTo({ url })
   }
 }
-// #ifndef MP-WEIXIN
+// #ifndef MP-WEIXIN || MP-ALIPAY
 // 因为有了 custom:true, 微信里面不需要多余的hide操作
 onLoad(() => {
   // 解决原生 tabBar 未隐藏导致有2个 tabBar 的问题
@@ -54,6 +54,21 @@ onLoad(() => {
   })
 })
 // #endif
+
+// #ifdef MP-ALIPAY
+onMounted(() => {
+  // 解决支付宝自定义tabbar 未隐藏导致有2个 tabBar 的问题; 注意支付宝很特别,需要在 onMounted 钩子调用
+  customTabbarEnable // 另外,支付宝里面,只要是 customTabbar 都需要隐藏
+  && uni.hideTabBar({
+    fail(err) {
+      console.log('hideTabBar fail: ', err)
+    },
+    success(res) {
+      // console.log('hideTabBar success: ', res)
+    },
+  })
+})
+// #endif
 const activeColor = 'var(--wot-color-theme, #1890ff)'
 const inactiveColor = '#666'
 function getColorByIndex(index: number) {
@@ -93,7 +108,7 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
               <!-- 如:<wd-icon name="home" /> (https://wot-design-uni.cn/component/icon.html) -->
               <!-- 如:<uv-icon name="home" /> (https://www.uvui.cn/components/icon.html) -->
               <!-- 如:<sar-icon name="image" /> (https://sard.wzt.zone/sard-uniapp-docs/components/icon)(sar没有home图标^_^) -->
-              <wd-icon :name="item.icon" size="20" />
+              <!-- <wd-icon :name="item.icon" size="20" /> -->
             </template>
             <template v-if="item.iconType === 'unocss' || item.iconType === 'iconfont'">
               <view :class="item.icon" class="text-20px" />
@@ -130,7 +145,8 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
   bottom: 0;
   left: 0;
   right: 0;
-
+  z-index: 1000;
+  
   border-top: 1px solid #eee;
   box-sizing: border-box;
 }

+ 7 - 12
src/tabbar/store.ts

@@ -1,10 +1,7 @@
-import type { CustomTabBarItem, CustomTabBarItemBadge } from './config'
+import type { CustomTabBarItem, CustomTabBarItemBadge } from './types'
 import { reactive } from 'vue'
 
-import { isNeedLoginMode } from '@/router/config'
-import { FG_LOG_ENABLE, judgeIsExcludePath } from '@/router/interceptor'
-import { useTokenStore } from '@/store/token'
-import { tabbarList as _tabbarList, customTabbarEnable } from './config'
+import { tabbarList as _tabbarList, customTabbarEnable, selectedTabbarStrategy, TABBAR_STRATEGY_MAP } from './config'
 
 // TODO 1/2: 中间的鼓包tabbarItem的开关
 const BULGE_ENABLE = false
@@ -25,6 +22,9 @@ if (customTabbarEnable && BULGE_ENABLE) {
 }
 
 export function isPageTabbar(path: string) {
+  if (selectedTabbarStrategy === TABBAR_STRATEGY_MAP.NO_TABBAR) {
+    return false
+  }
   const _path = path.split('?')[0]
   return tabbarList.some(item => item.pagePath === _path)
 }
@@ -38,12 +38,8 @@ const tabbarStore = reactive({
   curIdx: uni.getStorageSync('app-tabbar-index') || 0,
   prevIdx: uni.getStorageSync('app-tabbar-index') || 0,
   setCurIdx(idx: number) {
-    const tokenStore = useTokenStore()
-    // 已登录 或 (url 需要登录 && 在白名单 || 不需要登录 && 不在黑名单) (关于 白名单|黑名单 逻辑: src/router/interceptor.ts)
-    if (tokenStore.hasLogin || (isNeedLoginMode && judgeIsExcludePath(tabbarList[idx].pagePath)) || (!isNeedLoginMode && !judgeIsExcludePath(tabbarList[idx].pagePath))) {
-      this.curIdx = idx
-      uni.setStorageSync('app-tabbar-index', idx)
-    }
+    this.curIdx = idx
+    uni.setStorageSync('app-tabbar-index', idx)
   },
   setTabbarItemBadge(idx: number, badge: CustomTabBarItemBadge) {
     if (tabbarList[idx]) {
@@ -57,7 +53,6 @@ const tabbarStore = reactive({
       return
     }
     const index = tabbarList.findIndex(item => item.pagePath === path)
-    FG_LOG_ENABLE && console.log('index:', index, path)
     // console.log('tabbarList:', tabbarList)
     if (index === -1) {
       const pagesPathList = getCurrentPages().map(item => item.route.startsWith('/') ? item.route : `/${item.route}`)

+ 34 - 0
src/tabbar/types.ts

@@ -0,0 +1,34 @@
+import type { TabBar } from '@uni-helper/vite-plugin-uni-pages'
+import type { RemoveLeadingSlashFromUnion } from '@/typings'
+
+/**
+ * 原生 tabbar 的单个选项配置
+ */
+export type NativeTabBarItem = TabBar['list'][number] & {
+  pagePath: RemoveLeadingSlashFromUnion<_LocationUrl>
+}
+
+/** badge 显示一个数字或 小红点(样式可以直接在 tabbar/index.vue 里面修改) */
+export type CustomTabBarItemBadge = number | 'dot'
+
+/** 自定义 tabbar 的单个选项配置 */
+export interface CustomTabBarItem {
+  text: string
+  pagePath: RemoveLeadingSlashFromUnion<_LocationUrl>
+  /** 图标类型,不建议用 image 模式,因为需要配置 2 张图,更麻烦 */
+  iconType: 'uiLib' | 'unocss' | 'iconfont' | 'image'
+  /**
+   * icon 的路径
+   * - uiLib: wot-design-uni 图标的 icon prop
+   * - unocss: unocss 图标的类名
+   * - iconfont: iconfont 图标的类名
+   * - image: 图片的路径
+   */
+  icon: string
+  /** 只有在 image 模式下才需要,传递的是高亮的图片(PS: 不建议用 image 模式) */
+  iconActive?: string
+  /** badge 显示一个数字或 小红点 */
+  badge?: CustomTabBarItemBadge
+  /** 是否是中间的鼓包tabbarItem */
+  isBulge?: boolean
+}

+ 25 - 0
src/typings.d.ts

@@ -30,6 +30,31 @@ declare global {
   }
 }
 
+// 扩展 @uni-helper/vite-plugin-uni-pages 的 definePage 参数类型
+declare module '@uni-helper/vite-plugin-uni-pages' {
+  interface UserPageMeta {
+    /**
+     * 使用 type: "home" 属性设置首页,其他页面不需要设置,默认为page
+     *
+     * 尽量保证一个项目 只有一个 这个配置,如果有多个,会按照字母顺序来排列,最终可能不是您想要的效果。
+     */
+    type?: 'home'
+    /**
+     * 页面布局类型, 模板默认只有 default, 如果在 src/layouts 下新增了 layout, 可以扩展当前属性
+     * @default 'default'
+     *
+     * 当前属性供 https://github.com/uni-helper/vite-plugin-uni-layouts 插件使用
+     */
+    layout?: 'default'
+    /**
+     * 是否从需要登录的路径中排除
+     *
+     * 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 src/router 文件夹
+     */
+    excludeLoginPath?: boolean
+  }
+}
+
 // patch uni 类型
 // 1. 补全 uni.hideToast() 的 options 类型
 // 2. 补全 uni.hideLoading() 的 options 类型

+ 6 - 0
src/typings.ts

@@ -13,3 +13,9 @@ export interface IUniUploadFileOptions {
   name?: string
   formData?: any
 }
+
+/** 工具类型:删除字符串开头的第一个斜杠 */
+export type RemoveLeadingSlash<S extends string> = S extends `/${infer Rest}` ? Rest : S
+
+/** 工具类型:删除联合类型中每个字符串的第一个斜杠 */
+export type RemoveLeadingSlashFromUnion<T extends string> = T extends any ? RemoveLeadingSlash<T> : never

+ 166 - 0
src/utils/debounce.ts

@@ -0,0 +1,166 @@
+// fork from https://github.com/toss/es-toolkit/blob/main/src/function/debounce.ts
+// 文档可查看:https://es-toolkit.dev/reference/function/debounce.html
+// 如需要 throttle 功能,可 copy https://github.com/toss/es-toolkit/blob/main/src/function/throttle.ts
+
+interface DebounceOptions {
+  /**
+   * An optional AbortSignal to cancel the debounced function.
+   */
+  signal?: AbortSignal
+
+  /**
+   * An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both.
+   * If `edges` includes "leading", the function will be invoked at the start of the delay period.
+   * If `edges` includes "trailing", the function will be invoked at the end of the delay period.
+   * If both "leading" and "trailing" are included, the function will be invoked at both the start and end of the delay period.
+   * @default ["trailing"]
+   */
+  edges?: Array<'leading' | 'trailing'>
+}
+
+export interface DebouncedFunction<F extends (...args: any[]) => void> {
+  (...args: Parameters<F>): void
+
+  /**
+   * Schedules the execution of the debounced function after the specified debounce delay.
+   * This method resets any existing timer, ensuring that the function is only invoked
+   * after the delay has elapsed since the last call to the debounced function.
+   * It is typically called internally whenever the debounced function is invoked.
+   *
+   * @returns {void}
+   */
+  schedule: () => void
+
+  /**
+   * Cancels any pending execution of the debounced function.
+   * This method clears the active timer and resets any stored context or arguments.
+   */
+  cancel: () => void
+
+  /**
+   * Immediately invokes the debounced function if there is a pending execution.
+   * This method executes the function right away if there is a pending execution.
+   */
+  flush: () => void
+}
+
+/**
+ * Creates a debounced function that delays invoking the provided function until after `debounceMs` milliseconds
+ * have elapsed since the last time the debounced function was invoked. The debounced function also has a `cancel`
+ * method to cancel any pending execution.
+ *
+ * @template F - The type of function.
+ * @param {F} func - The function to debounce.
+ * @param {number} debounceMs - The number of milliseconds to delay.
+ * @param {DebounceOptions} options - The options object
+ * @param {AbortSignal} options.signal - An optional AbortSignal to cancel the debounced function.
+ * @returns A new debounced function with a `cancel` method.
+ *
+ * @example
+ * const debouncedFunction = debounce(() => {
+ *   console.log('Function executed');
+ * }, 1000);
+ *
+ * // Will log 'Function executed' after 1 second if not called again in that time
+ * debouncedFunction();
+ *
+ * // Will not log anything as the previous call is canceled
+ * debouncedFunction.cancel();
+ *
+ * // With AbortSignal
+ * const controller = new AbortController();
+ * const signal = controller.signal;
+ * const debouncedWithSignal = debounce(() => {
+ *  console.log('Function executed');
+ * }, 1000, { signal });
+ *
+ * debouncedWithSignal();
+ *
+ * // Will cancel the debounced function call
+ * controller.abort();
+ */
+export function debounce<F extends (...args: any[]) => void>(
+  func: F,
+  debounceMs: number,
+  { signal, edges }: DebounceOptions = {},
+): DebouncedFunction<F> {
+  let pendingThis: any
+  let pendingArgs: Parameters<F> | null = null
+
+  const leading = edges != null && edges.includes('leading')
+  const trailing = edges == null || edges.includes('trailing')
+
+  const invoke = () => {
+    if (pendingArgs !== null) {
+      func.apply(pendingThis, pendingArgs)
+      pendingThis = undefined
+      pendingArgs = null
+    }
+  }
+
+  const onTimerEnd = () => {
+    if (trailing) {
+      invoke()
+    }
+
+    // eslint-disable-next-line ts/no-use-before-define
+    cancel()
+  }
+
+  let timeoutId: ReturnType<typeof setTimeout> | null = null
+
+  const schedule = () => {
+    if (timeoutId != null) {
+      clearTimeout(timeoutId)
+    }
+
+    timeoutId = setTimeout(() => {
+      timeoutId = null
+
+      onTimerEnd()
+    }, debounceMs)
+  }
+
+  const cancelTimer = () => {
+    if (timeoutId !== null) {
+      clearTimeout(timeoutId)
+      timeoutId = null
+    }
+  }
+
+  const cancel = () => {
+    cancelTimer()
+    pendingThis = undefined
+    pendingArgs = null
+  }
+
+  const flush = () => {
+    invoke()
+  }
+
+  const debounced = function (this: any, ...args: Parameters<F>) {
+    if (signal?.aborted) {
+      return
+    }
+
+    // eslint-disable-next-line ts/no-this-alias
+    pendingThis = this
+    pendingArgs = args
+
+    const isFirstCall = timeoutId == null
+
+    schedule()
+
+    if (leading && isFirstCall) {
+      invoke()
+    }
+  }
+
+  debounced.schedule = schedule
+  debounced.cancel = cancel
+  debounced.flush = flush
+
+  signal?.addEventListener('abort', cancel, { once: true })
+
+  return debounced
+}

+ 44 - 0
src/utils/toLoginPage.ts

@@ -0,0 +1,44 @@
+import { getLastPage } from '@/utils'
+import { debounce } from '@/utils/debounce'
+
+interface ToLoginPageOptions {
+  /**
+   * 跳转模式, uni.navigateTo | uni.reLaunch
+   * @default 'navigateTo'
+   */
+  mode?: 'navigateTo' | 'reLaunch'
+  /**
+   * 查询参数
+   * @example '?redirect=/pages/home/index'
+   */
+  queryString?: string
+}
+
+// TODO: 自己增加登录页
+const LOGIN_PAGE = '/pages/login/index'
+
+/**
+ * 跳转到登录页, 带防抖处理
+ *
+ * 如果要立即跳转,不做延时,可以使用 `toLoginPage.flush()` 方法
+ */
+export const toLoginPage = debounce((options: ToLoginPageOptions = {}) => {
+  const { mode = 'navigateTo', queryString = '' } = options
+
+  const url = `${LOGIN_PAGE}${queryString}`
+
+  // 获取当前页面路径
+  const currentPage = getLastPage()
+  const currentPath = `/${currentPage.route}`
+  // 如果已经在登录页,则不跳转
+  if (currentPath === LOGIN_PAGE) {
+    return
+  }
+
+  if (mode === 'navigateTo') {
+    uni.navigateTo({ url })
+  }
+  else {
+    uni.reLaunch({ url })
+  }
+}, 500)

+ 2 - 3
tsconfig.json

@@ -3,8 +3,8 @@
     "composite": true,
     "lib": ["esnext", "dom"],
     "baseUrl": ".",
-    "module": "ESNext",
-    "moduleResolution": "Node",
+    "module": "esnext",
+    "moduleResolution": "bundler",
     "paths": {
       "@/*": ["./src/*"],
       "@img/*": ["./src/static/*"]
@@ -15,7 +15,6 @@
       "@uni-helper/uni-types",
       "@uni-helper/vite-plugin-uni-pages",
       "miniprogram-api-typings",
-      "wot-design-uni/global.d.ts",
       "z-paging/types",
       "./src/types/async-component.d.ts",
       "./src/types/async-import.d.ts",

+ 46 - 4
uno.config.ts

@@ -1,14 +1,14 @@
 import type {
   Preset,
 } from 'unocss'
+import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
+
 // 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,
   presetIcons,
   transformerDirectives,
   transformerVariantGroup,
@@ -26,9 +26,27 @@ export default defineConfig({
         'display': 'inline-block',
         'vertical-align': 'middle',
       },
+      collections: {
+        // 注册本地 SVG 图标集合, 从本地文件系统加载图标
+        // 在 './src/static/my-icons' 目录下的所有 svg 文件将被注册为图标,
+        // my-icons 是图标集合名称,使用 `i-my-icons-图标名` 调用
+        'my-icons': FileSystemIconLoader(
+          './src/static/my-icons',
+          // 可选的,你可以提供一个 transform 回调来更改每个图标
+          (svg) => {
+            let svgStr = svg
+
+            // 如果 SVG 文件未定义 `fill` 属性,则默认填充 `currentColor`, 这样图标颜色会继承文本颜色,方便在不同场景下适配
+            svgStr = svgStr.includes('fill="') ? svgStr : svgStr.replace(/^<svg /, '<svg fill="currentColor" ')
+
+            // 如果 svg 有 width, 和 height 属性,将这些属性改为 1em,否则无法显示图标
+            svgStr = svgStr.replace(/(<svg.*?width=)"(.*?)"/, '$1"1em"').replace(/(<svg.*?height=)"(.*?)"/, '$1"1em"')
+
+            return svgStr
+          },
+        ),
+      },
     }),
-    // 支持css class属性化
-    presetAttributify(),
     // TODO: check 是否会有别的影响
     // 处理低端安卓机的样式问题
     // 将颜色函数 (rgb()和hsl()) 从空格分隔转换为逗号分隔,更好的兼容性app端,example:
@@ -36,6 +54,8 @@ export default defineConfig({
     // `rgba(255 0 0 / 0.5)` -> `rgba(255, 0, 0, 0.5)`
     presetLegacyCompat({
       commaStyleColorFunction: true,
+      legacyColorSpace: true, // by QQ4群-量子蔷薇
+      // @菲鸽 unocss 配置中,建议在 presetLegacyCompat 中添加 legacyColorSpace: true,以去除生成的颜色样式中的 in oklch 关键字,现在发现有些渐变色生成不符合预期
     }) as Preset,
   ],
   transformers: [
@@ -74,4 +94,26 @@ export default defineConfig({
       '3xs': ['18rpx', '26rpx'],
     },
   },
+  // windows 系统会报错:[plugin:unocss:transformers:pre] Cannot overwrite a zero-length range - use append Left or prependRight instead.
+  // 去掉下面的就正常了
+  // content: {
+  //   /**
+  //    * 解决小程序报错 `./app.wxss(78:2814): unexpected unexpected at pos 5198`
+  //    * 为什么同时使用include和exclude?虽然看起来多余,但同时配置两者是一种常见的 `防御性编程` 做法。
+  //      1. 结构变化保障 : 如果未来项目结构发生变化,某些排除目录可能被移动到包含路径下,exclude配置可以确保它们仍被排除
+  //      2. 明确性 : 明确列出要排除的目录使配置意图更加清晰
+  //      3. 性能优化 : 避免处理不必要的文件,提高构建性能
+  //      4. 防止冲突 : 排除第三方库和构建输出目录,避免潜在的CSS冲突
+  //    */
+  //   pipeline: {
+  //     exclude: [
+  //       'node_modules/**/*',
+  //       'public/**/*',
+  //       'dist/**/*',
+  //     ],
+  //     include: [
+  //       './src/**/*',
+  //     ],
+  //   },
+  // },
 })

+ 17 - 17
vite.config.ts

@@ -64,19 +64,16 @@ export default defineConfig(({ command, mode }) => {
     envDir: './env', // 自定义env目录
     base: VITE_APP_PUBLIC_BASE,
     plugins: [
+      UniLayouts(),
+      UniPlatform(),
+      UniManifest(),
       UniPages({
         exclude: ['**/components/**/**.*'],
         // pages 目录为 src/pages,分包目录不能配置在pages目录下!!
         // 是个数组,可以配置多个,但是不能为pages里面的目录!!
-        subPackages: [
-          'src/pages-fg', // 这个是相对必要的路由,尽量留着(登录页、注册页、404页等)
-          'src/pages-sub', // 这个多为示例代码,参考用的,开发完后注释掉即可(或者直接删除)
-        ],
+        subPackages: [],
         dts: 'src/types/uni-pages.d.ts',
       }),
-      UniLayouts(),
-      UniPlatform(),
-      UniManifest(),
       // Optimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行
       Optimization({
         enable: {
@@ -90,6 +87,18 @@ export default defineConfig(({ command, mode }) => {
         logger: false,
       }),
       // UniXXX 需要在 Uni 之前引入
+      // 若存在改变 pages.json 的插件,请将 UniKuRoot 放置其后
+      UniKuRoot({
+        excludePages: ['**/components/**/**.*'],
+      }),
+      // Components 需要在 Uni 之前引入
+      Components({
+        extensions: ['vue'],
+        deep: true, // 是否递归扫描子目录,
+        directoryAsNamespace: false, // 是否把目录名作为命名空间前缀,true 时组件名为 目录名+组件名,
+        dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持)
+      }),
+      Uni(),
       {
         // 临时解决 dcloudio 官方的 @dcloudio/uni-mp-compiler 出现的编译 BUG
         // 参考 github issue: https://github.com/dcloudio/uni-app/issues/4952
@@ -137,15 +146,6 @@ export default defineConfig(({ command, mode }) => {
         },
       ),
       syncManifestPlugin(),
-      Components({
-        extensions: ['vue'],
-        deep: true, // 是否递归扫描子目录,
-        directoryAsNamespace: false, // 是否把目录名作为命名空间前缀,true 时组件名为 目录名+组件名,
-        dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持)
-      }),
-      // 若存在改变 pages.json 的插件,请将 UniKuRoot 放置其后
-      UniKuRoot(),
-      Uni(),
       // 自动打开开发者工具插件 (必须修改 .env 文件中的 VITE_WX_APPID)
       openDevTools(),
     ],
@@ -186,7 +186,7 @@ export default defineConfig(({ command, mode }) => {
         : undefined,
     },
     esbuild: {
-      drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : ['debugger'],
+      drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : [],
     },
     build: {
       sourcemap: false,