Quellcode durchsuchen

Merge branch 'main'

Z.X.PING vor 8 Monaten
Ursprung
Commit
88c699673c
100 geänderte Dateien mit 8148 neuen und 4302 gelöschten Zeilen
  1. 51 0
      .cursor/rules/api-http-patterns.mdc
  2. 41 0
      .cursor/rules/development-workflow.mdc
  3. 34 0
      .cursor/rules/project-overview.mdc
  4. 54 0
      .cursor/rules/styling-css-patterns.mdc
  5. 62 0
      .cursor/rules/uni-app-patterns.mdc
  6. 52 0
      .cursor/rules/vue-typescript-patterns.mdc
  7. 106 16
      .github/workflows/auto-merge.yml
  8. 118 0
      .trae/rules/project_rules.md
  9. 3 0
      .vscode/settings.json
  10. 5 9
      .vscode/vue3.code-snippets
  11. 2 2
      README.md
  12. 3 0
      codes/README.md
  13. 222 0
      codes/router.txt
  14. 10 17
      env/.env
  15. 4 1
      env/.env.development
  16. 4 1
      env/.env.production
  17. 5 0
      env/.env.test
  18. 7 0
      eslint.config.mjs
  19. 4 2
      manifest.config.ts
  20. 2 2
      openapi-ts-request.config.ts
  21. 66 49
      package.json
  22. 2 2
      pages.config.ts
  23. 0 13
      patches/@dcloudio__uni-h5.patch
  24. 2701 3387
      pnpm-lock.yaml
  25. 0 6
      pnpm-workspace.yaml
  26. 29 0
      scripts/window-path-loader.js
  27. 39 0
      src/App.ku.vue
  28. 13 7
      src/App.vue
  29. 1 1
      src/api/foo-alova.ts
  30. 11 0
      src/api/foo-vue-query.ts
  31. 27 0
      src/api/foo.ts
  32. 13 9
      src/api/login.ts
  33. 51 11
      src/api/types/login.ts
  34. 3 1
      src/env.d.ts
  35. 0 0
      src/hooks/.gitkeep
  36. 0 50
      src/hooks/usePageAuth.ts
  37. 13 0
      src/http/README.md
  38. 12 6
      src/http/request/alova.ts
  39. 72 8
      src/http/http.ts
  40. 4 9
      src/http/interceptor.ts
  41. 0 0
      src/http/tools/enum.ts
  42. 0 0
      src/http/tools/queryString.ts
  43. 9 0
      src/http/request/types.ts
  44. 30 0
      src/http/vue-query.ts
  45. 5 12
      src/layouts/default.vue
  46. 0 68
      src/layouts/fg-tabbar/fg-tabbar.vue
  47. 0 17
      src/layouts/fg-tabbar/tabbar.md
  48. 0 11
      src/layouts/fg-tabbar/tabbar.ts
  49. 0 74
      src/layouts/fg-tabbar/tabbarList.ts
  50. 0 19
      src/layouts/tabbar.vue
  51. 4 2
      src/manifest.json
  52. 48 0
      src/pages-sub/demo/components/request.vue
  53. 14 10
      src/pages-sub/demo/index.vue
  54. 36 15
      src/pages.json
  55. 102 36
      src/pages/about/about.vue
  56. 8 11
      src/pages/about/alova.vue
  57. 28 0
      src/pages/about/components/VBindCss.vue
  58. 15 36
      src/pages/about/components/request.vue
  59. 0 38
      src/pages/about/components/upload.vue
  60. 50 0
      src/pages/about/vue-query.vue
  61. 64 41
      src/pages/index/index.vue
  62. 76 0
      src/pages/login/login.vue
  63. 34 0
      src/pages/login/register.vue
  64. 212 0
      src/pages/me/me.vue
  65. 37 0
      src/router/README.md
  66. 21 0
      src/router/config.ts
  67. 64 34
      src/router/interceptor.ts
  68. 2 2
      src/service/app/displayEnumLabel.ts
  69. 0 0
      src/service/index.ts
  70. 0 28
      src/service/index/foo.ts
  71. 18 26
      src/service/app/pet.ts
  72. 24 24
      src/service/app/pet.vuequery.ts
  73. 9 9
      src/service/app/store.ts
  74. 13 13
      src/service/app/store.vuequery.ts
  75. 80 62
      src/service/app/types.ts
  76. 16 16
      src/service/app/user.ts
  77. 23 23
      src/service/app/user.vuequery.ts
  78. 0 0
      src/static/images/.gitkeep
  79. BIN
      src/static/tabbar/scan.png
  80. 1 0
      src/store/index.ts
  81. 42 0
      src/store/theme.ts
  82. 277 0
      src/store/token.ts
  83. 15 65
      src/store/user.ts
  84. 17 0
      src/style/index.scss
  85. 78 0
      src/tabbar/README.md
  86. 156 0
      src/tabbar/config.ts
  87. 159 0
      src/tabbar/index.vue
  88. 71 0
      src/tabbar/store.ts
  89. 119 1
      src/typings.d.ts
  90. 42 0
      src/uni_modules/uni-icons/changelog.md
  91. 91 0
      src/uni_modules/uni-icons/components/uni-icons/uni-icons.uvue
  92. 110 0
      src/uni_modules/uni-icons/components/uni-icons/uni-icons.vue
  93. 664 0
      src/uni_modules/uni-icons/components/uni-icons/uniicons.css
  94. BIN
      src/uni_modules/uni-icons/components/uni-icons/uniicons.ttf
  95. 664 0
      src/uni_modules/uni-icons/components/uni-icons/uniicons_file.ts
  96. 649 0
      src/uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js
  97. 89 0
      src/uni_modules/uni-icons/package.json
  98. 8 0
      src/uni_modules/uni-icons/readme.md
  99. 8 0
      src/uni_modules/uni-scss/changelog.md
  100. 0 0
      src/uni_modules/uni-scss/index.scss

+ 51 - 0
.cursor/rules/api-http-patterns.mdc

@@ -0,0 +1,51 @@
+# API 和 HTTP 请求规范
+
+## HTTP 请求封装
+- 可以使用 `简单http` 或者 `alova` 或者 `@tanstack/vue-query` 进行请求管理
+- HTTP 配置在 [src/http/](mdc:src/http/) 目录下
+- `简单http` - [src/http/http.ts](mdc:src/http/http.ts)
+- `alova` - [src/http/alova.ts](mdc:src/http/alova.ts)
+- `vue-query` - [src/http/vue-query.ts](mdc:src/http/vue-query.ts)
+- 请求拦截器在 [src/http/interceptor.ts](mdc:src/http/interceptor.ts)
+- 支持请求重试、缓存、错误处理
+
+## API 接口规范
+- API 接口定义在 [src/api/](mdc:src/api/) 目录下
+- 按功能模块组织 API 文件
+- 使用 TypeScript 定义请求和响应类型
+- 支持 `简单http`、`alova` 和 `vue-query` 三种请求方式
+
+
+## 示例代码结构
+```typescript
+// API 接口定义
+export interface LoginParams {
+  username: string
+  password: string
+}
+
+export interface LoginResponse {
+  token: string
+  userInfo: UserInfo
+}
+
+// alova 方式
+export const login = (params: LoginParams) => 
+  http.Post<LoginResponse>('/api/login', params)
+
+// vue-query 方式
+export const useLogin = () => {
+  return useMutation({
+    mutationFn: (params: LoginParams) => 
+      http.post<LoginResponse>('/api/login', params)
+  })
+}
+```
+
+## 错误处理
+- 统一错误处理在拦截器中配置
+- 支持网络错误、业务错误、认证错误等
+- 自动处理 token 过期和刷新
+---
+globs: src/api/*.ts,src/http/*.ts
+---

+ 41 - 0
.cursor/rules/development-workflow.mdc

@@ -0,0 +1,41 @@
+# 开发工作流程
+
+## 项目启动
+1. 安装依赖:`pnpm install`
+2. 开发环境:
+   - H5: `pnpm dev` 或 `pnpm dev:h5`
+   - 微信小程序: `pnpm dev:mp`
+   - APP: `pnpm dev:app`
+
+## 代码规范
+- 使用 ESLint 进行代码检查:`pnpm lint`
+- 自动修复代码格式:`pnpm lint:fix`
+- 使用 eslint 格式化代码
+- 遵循 TypeScript 严格模式
+
+## 构建和部署
+- H5 构建:`pnpm build:h5`
+- 小程序构建:`pnpm build:mp`
+- APP 构建:`pnpm build:app`
+- 类型检查:`pnpm type-check`
+
+## 开发工具
+- 推荐使用 VSCode 编辑器
+- 安装 Vue 和 TypeScript 相关插件
+- 使用 uni-app 开发者工具调试小程序
+- 使用 HBuilderX 调试 APP
+
+## 调试技巧
+- 使用 console.log 和 uni.showToast 调试
+- 利用 Vue DevTools 调试组件状态
+- 使用网络面板调试 API 请求
+- 平台差异测试和兼容性检查
+
+## 性能优化
+- 使用懒加载和代码分割
+- 优化图片和静态资源
+- 减少不必要的重渲染
+- 合理使用缓存策略
+---
+description: 开发工作流程和最佳实践指南
+---

+ 34 - 0
.cursor/rules/project-overview.mdc

@@ -0,0 +1,34 @@
+---
+alwaysApply: true
+---
+# unibest 项目概览
+
+这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。
+
+## 项目特点
+- 支持 H5、小程序、APP 多平台开发
+- 使用最新的前端技术栈
+- 内置约定式路由、layout布局、请求封装等功能
+- 无需依赖 HBuilderX,支持命令行开发
+
+## 核心配置文件
+- [package.json](mdc:package.json) - 项目依赖和脚本配置
+- [vite.config.ts](mdc:vite.config.ts) - Vite 构建配置
+- [pages.config.ts](mdc:pages.config.ts) - 页面路由配置
+- [manifest.config.ts](mdc:manifest.config.ts) - 应用清单配置
+- [uno.config.ts](mdc:uno.config.ts) - UnoCSS 配置
+
+## 主要目录结构
+- `src/pages/` - 页面文件
+- `src/components/` - 组件文件
+- `src/layouts/` - 布局文件
+- `src/api/` - API 接口
+- `src/http/` - HTTP 请求封装
+- `src/store/` - 状态管理
+- `src/tabbar/` - 底部导航栏
+
+## 开发命令
+- `pnpm dev` - 开发 H5 版本
+- `pnpm dev:mp` - 开发微信小程序
+- `pnpm dev:app` - 开发 APP 版本
+- `pnpm build` - 构建生产版本

+ 54 - 0
.cursor/rules/styling-css-patterns.mdc

@@ -0,0 +1,54 @@
+# 样式和 CSS 开发规范
+
+## UnoCSS 原子化 CSS
+- 项目使用 UnoCSS 作为原子化 CSS 框架
+- 配置在 [uno.config.ts](mdc:uno.config.ts)
+- 支持预设和自定义规则
+- 优先使用原子化类名,减少自定义 CSS
+
+## SCSS 规范
+- 使用 SCSS 预处理器
+- 样式文件使用 `lang="scss"` 和 `scoped` 属性
+- 遵循 BEM 命名规范
+- 使用变量和混入提高复用性
+
+## 样式组织
+- 全局样式在 [src/style/](mdc:src/style/) 目录下
+- 组件样式使用 scoped 作用域
+- 图标字体在 [src/style/iconfont.css](mdc:src/style/iconfont.css)
+- 主题变量在 [src/uni_modules/uni-scss/](mdc:src/uni_modules/uni-scss/) 目录下
+
+## 示例代码结构
+```vue
+<template>
+  <view class="container flex flex-col items-center p-4">
+    <text class="title text-lg font-bold mb-2">标题</text>
+    <view class="content bg-gray-100 rounded-lg p-3">
+      <!-- 内容 -->
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.container {
+  min-height: 100vh;
+  
+  .title {
+    color: var(--primary-color);
+  }
+  
+  .content {
+    width: 100%;
+    max-width: 600rpx;
+  }
+}
+</style>
+
+## 响应式设计
+- 使用 rpx 单位适配不同屏幕
+- 支持横屏和竖屏布局
+- 使用 flexbox 和 grid 布局
+- 考虑不同平台的样式差异
+---
+globs: *.vue,*.scss,*.css
+---

+ 62 - 0
.cursor/rules/uni-app-patterns.mdc

@@ -0,0 +1,62 @@
+# uni-app 开发规范
+
+## 页面开发
+- 页面文件放在 [src/pages/](mdc:src/pages/) 目录下
+- 使用约定式路由,文件名即路由路径
+- 页面配置在仅需要在 `route-block` 中配置标题等内容即可,会自动生成到 `pages.json` 中
+
+## 组件开发
+- 组件文件放在 [src/components/](mdc:src/components/) 目录下
+- 使用 uni-app 内置组件和第三方组件库
+- 支持 wot-design-uni\uv-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
+- 自定义组件遵循 uni-app 组件规范
+
+## 平台适配
+- 使用条件编译处理平台差异
+- 支持 H5、小程序、APP 多平台
+- 注意各平台的 API 差异
+- 使用 uni.xxx API 替代原生 API
+
+## 示例代码结构
+```vue
+<script setup lang="ts">
+// #ifdef H5
+import { h5Api } from '@/utils/h5'
+// #endif
+
+// #ifdef MP-WEIXIN
+import { mpApi } from '@/utils/mp'
+// #endif
+
+const handleClick = () => {
+  // #ifdef H5
+  h5Api.showToast('H5 平台')
+  // #endif
+  
+  // #ifdef MP-WEIXIN
+  mpApi.showToast('微信小程序')
+  // #endif
+}
+</script>
+
+<template>
+  <view class="page">
+    <!-- uni-app 组件 -->
+    <button @click="handleClick">点击</button>
+    
+    <!-- 条件渲染 -->
+    <!-- #ifdef H5 -->
+    <view>H5 特有内容</view>
+    <!-- #endif -->
+  </view>
+</template>
+```
+
+## 生命周期
+- 使用 uni-app 页面生命周期
+- onLoad、onShow、onReady、onHide、onUnload
+- 组件生命周期遵循 Vue3 规范
+- 注意页面栈和导航管理
+---
+globs: src/pages/*.vue,src/components/*.vue
+---

+ 52 - 0
.cursor/rules/vue-typescript-patterns.mdc

@@ -0,0 +1,52 @@
+# Vue3 + TypeScript 开发规范
+
+## Vue 组件规范
+- 使用 Composition API 和 `<script setup>` 语法
+- 组件文件使用 PascalCase 命名
+- 页面文件放在 `src/pages/` 目录下
+- 组件文件放在 `src/components/` 目录下
+
+## Vue SFC 组件规范
+- `<script setup>` 标签必须是第一个子元素
+- `<template>` 标签必须是第二个子元素
+- `<style scoped>` 标签必须是最后一个子元素(因为推荐使用原子化类名,所以很可能没有)
+
+## TypeScript 规范
+- 严格使用 TypeScript,避免使用 `any` 类型
+- 为 API 响应数据定义接口类型
+- 使用 `interface` 定义对象类型,`type` 定义联合类型
+- 导入类型时使用 `import type` 语法
+
+## 状态管理
+- 使用 Pinia 进行状态管理
+- Store 文件放在 `src/store/` 目录下
+- 使用 `defineStore` 定义 store
+- 支持持久化存储
+
+## 示例代码结构
+```vue
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import type { UserInfo } from '@/types/user'
+
+const userInfo = ref<UserInfo | null>(null)
+
+onMounted(() => {
+  // 初始化逻辑
+})
+</script>
+
+<template>
+  <view class="container">
+    <!-- 模板内容 -->
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.container {
+  // 样式
+}
+</style>
+---
+globs: *.vue,*.ts,*.tsx
+---

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

@@ -7,23 +7,23 @@ 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 }}
+  # 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
+  #     - 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
@@ -79,6 +79,24 @@ jobs:
           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
+    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-pro
+        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
+
   merge-to-base-uview-plus:
     name: Merge main into base-uview-plus
     runs-on: ubuntu-latest
@@ -96,3 +114,75 @@ jobs:
           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
+        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
+
+  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
+        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 }}
+
+      - name: Merge main into i18n-v4
+        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

+ 118 - 0
.trae/rules/project_rules.md

@@ -0,0 +1,118 @@
+# unibest 项目概览
+
+这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。
+
+## 项目特点
+- 支持 H5、小程序、APP 多平台开发
+- 使用最新的前端技术栈
+- 内置约定式路由、layout布局、请求封装等功能
+- 无需依赖 HBuilderX,支持命令行开发
+
+## 核心配置文件
+- [package.json](mdc:package.json) - 项目依赖和脚本配置
+- [vite.config.ts](mdc:vite.config.ts) - Vite 构建配置
+- [pages.config.ts](mdc:pages.config.ts) - 页面路由配置
+- [manifest.config.ts](mdc:manifest.config.ts) - 应用清单配置
+- [uno.config.ts](mdc:uno.config.ts) - UnoCSS 配置
+
+## 主要目录结构
+- `src/pages/` - 页面文件
+- `src/components/` - 组件文件
+- `src/layouts/` - 布局文件
+- `src/api/` - API 接口
+- `src/http/` - HTTP 请求封装
+- `src/store/` - 状态管理
+- `src/tabbar/` - 底部导航栏
+
+## 开发命令
+- `pnpm dev` - 开发 H5 版本
+- `pnpm dev:mp` - 开发微信小程序
+- `pnpm dev:app` - 开发 APP 版本
+- `pnpm build` - 构建生产版本
+
+## Vue 组件规范
+- 使用 Composition API 和 `<script setup>` 语法
+- 组件文件使用 PascalCase 命名
+- 页面文件放在 `src/pages/` 目录下
+- 组件文件放在 `src/components/` 目录下
+
+## TypeScript 规范
+- 严格使用 TypeScript,避免使用 `any` 类型
+- 为 API 响应数据定义接口类型
+- 使用 `interface` 定义对象类型,`type` 定义联合类型
+- 导入类型时使用 `import type` 语法
+
+## 状态管理
+- 使用 Pinia 进行状态管理
+- Store 文件放在 `src/store/` 目录下
+- 使用 `defineStore` 定义 store
+- 支持持久化存储
+
+## UnoCSS 原子化 CSS
+- 项目使用 UnoCSS 作为原子化 CSS 框架
+- 配置在 [uno.config.ts](mdc:uno.config.ts)
+- 支持预设和自定义规则
+- 优先使用原子化类名,减少自定义 CSS
+
+## Vue SFC 组件规范
+- `<script setup>` 标签必须是第一个子元素
+- `<template>` 标签必须是第二个子元素
+- `<style scoped>` 标签必须是最后一个子元素(因为推荐使用原子化类名,所以很可能没有)
+
+## 页面开发
+- 页面文件放在 [src/pages/](mdc:src/pages/) 目录下
+- 使用约定式路由,文件名即路由路径
+- 页面配置在仅需要在 `route-block` 中配置标题等内容即可,会自动生成到 `pages.json` 中
+
+## 组件开发
+- 组件文件放在 [src/components/](mdc:src/components/) 目录下
+- 使用 uni-app 内置组件和第三方组件库
+- 支持 wot-design-uni\uv-ui\uview-plus 等多种第三方组件库 和 z-paging 组件
+- 自定义组件遵循 uni-app 组件规范
+
+## 平台适配
+- 使用条件编译处理平台差异
+- 支持 H5、小程序、APP 多平台
+- 注意各平台的 API 差异
+- 使用 uni.xxx API 替代原生 API
+
+## 示例代码结构
+```vue
+<script setup lang="ts">
+// #ifdef H5
+import { h5Api } from '@/utils/h5'
+// #endif
+
+// #ifdef MP-WEIXIN
+import { mpApi } from '@/utils/mp'
+// #endif
+
+const handleClick = () => {
+  // #ifdef H5
+  h5Api.showToast('H5 平台')
+  // #endif
+  
+  // #ifdef MP-WEIXIN
+  mpApi.showToast('微信小程序')
+  // #endif
+}
+</script>
+
+<template>
+  <view class="page">
+    <!-- uni-app 组件 -->
+    <button @click="handleClick">点击</button>
+    
+    <!-- 条件渲染 -->
+    <!-- #ifdef H5 -->
+    <view>H5 特有内容</view>
+    <!-- #endif -->
+  </view>
+</template>
+```
+
+## 生命周期
+- 使用 uni-app 页面生命周期
+- onLoad、onShow、onReady、onHide、onUnload
+- 组件生命周期遵循 Vue3 规范
+- 注意页面栈和导航管理

+ 3 - 0
.vscode/settings.json

@@ -71,6 +71,8 @@
   "cSpell.words": [
     "alova",
     "Aplipay",
+    "attributify",
+    "chooseavatar",
     "climblee",
     "commitlint",
     "dcloudio",
@@ -83,6 +85,7 @@
     "Toutiao",
     "uniapp",
     "unibest",
+    "unocss",
     "uview",
     "uvui",
     "Wechat",

+ 5 - 9
.vscode/vue3.code-snippets

@@ -19,16 +19,12 @@
     "scope": "vue",
     "prefix": "v3",
     "body": [
-      "<route lang=\"jsonc\" type=\"page\">",
-      "{",
-      "  \"layout\": \"default\",",
-      "  \"style\": {",
-      "    \"navigationBarTitleText\": \"$1\"",
-      "  }",
-      "}",
-      "</route>\n",
       "<script lang=\"ts\" setup>",
-      "//$2",
+      "definePage({",
+      "  style: {",
+      "    navigationBarTitleText: '$1',",
+      "  },",
+      "})",
       "</script>\n",
       "<template>",
       "  <view class=\"\">$3</view>",

+ 2 - 2
README.md

@@ -71,13 +71,13 @@
 
 - web平台: `pnpm dev:h5`, 然后打开 [http://localhost:9000/](http://localhost:9000/)。
 - weixin平台:`pnpm dev:mp` 然后打开微信开发者工具,导入本地文件夹,选择本项目的`dist/dev/mp-weixin` 文件。
-- APP平台:`pnpm dev:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/dev/app` 文件夹,选择运行到模拟器(开发时优先使用),或者运行的安卓/ios基座。
+- APP平台:`pnpm dev:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/dev/app` 文件夹,选择运行到模拟器(开发时优先使用),或者运行的安卓/ios基座。(如果是 `安卓` 和 `鸿蒙` 平台,则不用这个方式,可以把整个unibest项目导入到hbx,通过hbx的菜单来运行到对应的平台。)
 
 ## 🔗 发布
 
 - web平台: `pnpm build:h5`,打包后的文件在 `dist/build/h5`,可以放到web服务器,如nginx运行。如果最终不是放在根目录,可以在 `manifest.config.ts` 文件的 `h5.router.base` 属性进行修改。
 - weixin平台:`pnpm build:mp`, 打包后的文件在 `dist/build/mp-weixin`,然后通过微信开发者工具导入,并点击右上角的“上传”按钮进行上传。
-- APP平台:`pnpm build:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/build/app` 文件夹,选择发行 - APP云打包。
+- APP平台:`pnpm build:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/build/app` 文件夹,选择发行 - APP云打包。(如果是 `安卓` 和 `鸿蒙` 平台,则不用这个方式,可以把整个unibest项目导入到hbx,通过hbx的菜单来发行到对应的平台。)
 
 ## 📄 License
 

+ 3 - 0
codes/README.md

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

+ 222 - 0
codes/router.txt

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

+ 10 - 17
env/.env

@@ -5,27 +5,20 @@ VITE_UNI_APPID = '__UNI__D1E5001'
 VITE_WX_APPID = 'wxa2abb91f64032a2b'
 
 # h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
+# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
 VITE_APP_PUBLIC_BASE=/
 
-# 登录页面
-VITE_LOGIN_URL = '/pages/login/index'
-# 第一个请求地址
+# 后台请求地址
 VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
-# 第二个请求地址
-VITE_API_SECONDARY_URL = 'https://ukw0y1.laf.run'
-
+# 后台上传地址
 VITE_UPLOAD_BASEURL = 'https://ukw0y1.laf.run/upload'
 
-# 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
-# 下面的变量如果没有设置,会默认使用 VITE_SERVER_BASEURL or VITE_UPLOAD_BASEURL
-VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
-VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
-VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
-
-VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run/upload'
-VITE_UPLOAD_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run/upload'
-VITE_UPLOAD_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run/upload'
-
 # h5是否需要配置代理
-VITE_APP_PROXY=false
+VITE_APP_PROXY_ENABLE = true
 VITE_APP_PROXY_PREFIX = '/api'
+
+# 第二个请求地址 (目前alova中可以使用)
+VITE_API_SECONDARY_URL = 'https://ukw0y1.laf.run'
+
+# 认证模式,'single' | 'double' ==> 单token | 双token
+VITE_AUTH_MODE = 'single'

+ 4 - 1
env/.env.development

@@ -3,4 +3,7 @@ NODE_ENV = 'development'
 # 是否去除console 和 debugger
 VITE_DELETE_CONSOLE = false
 # 是否开启sourcemap
-VITE_SHOW_SOURCEMAP = true
+VITE_SHOW_SOURCEMAP = false
+
+# 后台请求地址
+# VITE_SERVER_BASEURL = 'https://dev.xxx.com'

+ 4 - 1
env/.env.production

@@ -1,6 +1,9 @@
 # 变量必须以 VITE_ 为前缀才能暴露给外部读取
-NODE_ENV = 'development'
+NODE_ENV = 'production'
 # 是否去除console 和 debugger
 VITE_DELETE_CONSOLE = true
 # 是否开启sourcemap
 VITE_SHOW_SOURCEMAP = false
+
+# 后台请求地址
+# VITE_SERVER_BASEURL = 'https://prod.xxx.com'

+ 5 - 0
env/.env.test

@@ -2,3 +2,8 @@
 NODE_ENV = 'development'
 # 是否去除console 和 debugger
 VITE_DELETE_CONSOLE = false
+# 是否开启sourcemap
+VITE_SHOW_SOURCEMAP = false
+
+# 后台请求地址
+# VITE_SERVER_BASEURL = 'https://test.xxx.com'

+ 7 - 0
eslint.config.mjs

@@ -18,6 +18,7 @@ export default uniHelper({
     'src/service/app/**',
   ],
   rules: {
+    'no-useless-return': 'off',
     'no-console': 'off',
     'no-unused-vars': 'off',
     'vue/no-unused-refs': 'off',
@@ -27,6 +28,12 @@ export default uniHelper({
     'jsdoc/require-returns-description': 'off',
     'ts/no-empty-object-type': 'off',
     'no-extend-native': 'off',
+    'vue/singleline-html-element-content-newline': [
+      'error',
+      {
+        externalIgnores: ['text'],
+      },
+    ],
   },
   formatters: {
     /**

+ 4 - 2
manifest.config.ts

@@ -19,6 +19,7 @@ const {
   VITE_APP_PUBLIC_BASE,
   VITE_FALLBACK_LOCALE,
 } = env
+// console.log('manifest.config.ts env:', env)
 
 export default defineManifestConfig({
   'name': VITE_APP_TITLE,
@@ -30,7 +31,7 @@ export default defineManifestConfig({
   'locale': VITE_FALLBACK_LOCALE, // 'zh-Hans'
   'h5': {
     router: {
-      // base: VITE_APP_PUBLIC_BASE,
+      base: VITE_APP_PUBLIC_BASE,
     },
   },
   /* 5+App特有相关 */
@@ -53,7 +54,7 @@ export default defineManifestConfig({
     distribute: {
       /* android打包配置 */
       android: {
-        minSdkVersion: 30,
+        minSdkVersion: 21,
         targetSdkVersion: 30,
         abiFilters: ['armeabi-v7a', 'arm64-v8a'],
         permissions: [
@@ -127,6 +128,7 @@ export default defineManifestConfig({
     optimization: {
       subPackages: true,
     },
+    // styleIsolation: 'shared',
     usingComponents: true,
     // __usePrivacyCheck__: true,
   },

+ 2 - 2
openapi-ts-request.config.ts

@@ -3,8 +3,8 @@ import type { GenerateServiceProps } from 'openapi-ts-request'
 export default [
   {
     schemaPath: 'http://petstore.swagger.io/v2/swagger.json',
-    serversPath: './src/service/app',
-    requestLibPath: `import request from '@/utils/request';\n import { CustomRequestOptions } from '@/http/interceptor';`,
+    serversPath: './src/service',
+    requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions } from '@/http/types';`,
     requestOptionsType: 'CustomRequestOptions',
     isGenReactQuery: true,
     reactQueryMode: 'vue',

+ 66 - 49
package.json

@@ -1,9 +1,8 @@
 {
   "name": "unibest",
-  "type": "commonjs",
-  "version": "3.3.0",
-  "unibest-version": "3.3.0",
-  "packageManager": "pnpm@10.10.0",
+  "type": "module",
+  "version": "3.12.4",
+  "unibest-version": "3.12.4",
   "description": "unibest - 最好的 uniapp 开发模板",
   "generate-time": "用户创建项目时生成",
   "author": {
@@ -21,8 +20,8 @@
     "url-old": "https://github.com/codercup/unibest/issues"
   },
   "engines": {
-    "node": ">=18",
-    "pnpm": ">=7.30"
+    "node": ">=22",
+    "pnpm": ">=9 <=10.12"
   },
   "scripts": {
     "preinstall": "npx only-allow pnpm",
@@ -30,13 +29,21 @@
     "uvm-rm": "node ./scripts/postupgrade.js",
     "postuvm": "echo upgrade uni-app success!",
     "dev:app": "uni -p app",
+    "dev:app:test": "uni -p app --mode test",
+    "dev:app:prod": "uni -p app --mode production",
     "dev:app-android": "uni -p app-android",
     "dev:app-ios": "uni -p app-ios",
     "dev:custom": "uni -p",
-    "dev": "uni",
+    "dev": "node --experimental-loader ./scripts/window-path-loader.js node_modules/@dcloudio/vite-plugin-uni/bin/uni.js",
+    "dev:test": "uni --mode test",
+    "dev:prod": "uni --mode production",
     "dev:h5": "uni",
+    "dev:h5:test": "uni --mode test",
+    "dev:h5:prod": "uni --mode production",
     "dev:h5:ssr": "uni --ssr",
     "dev:mp": "uni -p mp-weixin",
+    "dev:mp:test": "uni -p mp-weixin --mode test",
+    "dev:mp:prod": "uni -p mp-weixin --mode production",
     "dev:mp-alipay": "uni -p mp-alipay",
     "dev:mp-baidu": "uni -p mp-baidu",
     "dev:mp-jd": "uni -p mp-jd",
@@ -50,14 +57,22 @@
     "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
     "dev:quickapp-webview-union": "uni -p quickapp-webview-union",
     "build:app": "uni build -p app",
+    "build:app:test": "uni build -p app --mode test",
+    "build:app:prod": "uni build -p app --mode production",
     "build:app-android": "uni build -p app-android",
     "build:app-ios": "uni build -p app-ios",
     "build:custom": "uni build -p",
     "build:h5": "uni build",
+    "build:h5:test": "uni build --mode test",
+    "build:h5:prod": "uni build --mode production",
     "build": "uni build",
+    "build:test": "uni build --mode test",
+    "build:prod": "uni build --mode production",
     "build:h5:ssr": "uni build --ssr",
     "build:mp-alipay": "uni build -p mp-alipay",
     "build:mp": "uni build -p mp-weixin",
+    "build:mp:test": "uni build -p mp-weixin --mode test",
+    "build:mp:prod": "uni build -p mp-weixin --mode production",
     "build:mp-baidu": "uni build -p mp-baidu",
     "build:mp-jd": "uni build -p mp-jd",
     "build:mp-kuaishou": "uni build -p mp-kuaishou",
@@ -78,22 +93,22 @@
   "dependencies": {
     "@alova/adapter-uniapp": "^2.0.14",
     "@alova/shared": "^1.3.1",
-    "@dcloudio/uni-app": "3.0.0-4060620250520001",
-    "@dcloudio/uni-app-harmony": "3.0.0-4060620250520001",
-    "@dcloudio/uni-app-plus": "3.0.0-4060620250520001",
-    "@dcloudio/uni-components": "3.0.0-4060620250520001",
-    "@dcloudio/uni-h5": "3.0.0-4060620250520001",
-    "@dcloudio/uni-mp-alipay": "3.0.0-4060620250520001",
-    "@dcloudio/uni-mp-baidu": "3.0.0-4060620250520001",
-    "@dcloudio/uni-mp-harmony": "3.0.0-4060620250520001",
-    "@dcloudio/uni-mp-jd": "3.0.0-4060620250520001",
-    "@dcloudio/uni-mp-kuaishou": "3.0.0-4060620250520001",
-    "@dcloudio/uni-mp-lark": "3.0.0-4060620250520001",
-    "@dcloudio/uni-mp-qq": "3.0.0-4060620250520001",
-    "@dcloudio/uni-mp-toutiao": "3.0.0-4060620250520001",
-    "@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001",
-    "@dcloudio/uni-mp-xhs": "3.0.0-4060620250520001",
-    "@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
+    "@dcloudio/uni-app": "3.0.0-4070520250711001",
+    "@dcloudio/uni-app-harmony": "3.0.0-4070520250711001",
+    "@dcloudio/uni-app-plus": "3.0.0-4070520250711001",
+    "@dcloudio/uni-components": "3.0.0-4070520250711001",
+    "@dcloudio/uni-h5": "3.0.0-4070520250711001",
+    "@dcloudio/uni-mp-alipay": "3.0.0-4070520250711001",
+    "@dcloudio/uni-mp-baidu": "3.0.0-4070520250711001",
+    "@dcloudio/uni-mp-harmony": "3.0.0-4070520250711001",
+    "@dcloudio/uni-mp-jd": "3.0.0-4070520250711001",
+    "@dcloudio/uni-mp-kuaishou": "3.0.0-4070520250711001",
+    "@dcloudio/uni-mp-lark": "3.0.0-4070520250711001",
+    "@dcloudio/uni-mp-qq": "3.0.0-4070520250711001",
+    "@dcloudio/uni-mp-toutiao": "3.0.0-4070520250711001",
+    "@dcloudio/uni-mp-weixin": "3.0.0-4070520250711001",
+    "@dcloudio/uni-mp-xhs": "3.0.0-4070520250711001",
+    "@dcloudio/uni-quickapp-webview": "3.0.0-4070520250711001",
     "@tanstack/vue-query": "^5.62.16",
     "abortcontroller-polyfill": "^1.7.8",
     "alova": "^3.3.3",
@@ -101,55 +116,57 @@
     "js-cookie": "^3.0.5",
     "pinia": "2.0.36",
     "pinia-plugin-persistedstate": "3.2.1",
-    "vue": "^3.4.21",
-    "wot-design-uni": "^1.9.1",
+    "vue": "3.4.21",
+    "wot-design-uni": "^1.11.1",
     "z-paging": "2.8.7"
   },
   "devDependencies": {
-    "@antfu/eslint-config": "^4.15.0",
     "@commitlint/cli": "^19.8.1",
     "@commitlint/config-conventional": "^19.8.1",
-    "@dcloudio/types": "^3.4.8",
-    "@dcloudio/uni-automator": "3.0.0-4060620250520001",
-    "@dcloudio/uni-cli-shared": "3.0.0-4060620250520001",
-    "@dcloudio/uni-stacktracey": "3.0.0-4060620250520001",
-    "@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001",
+    "@dcloudio/types": "^3.4.19",
+    "@dcloudio/uni-automator": "3.0.0-4070520250711001",
+    "@dcloudio/uni-cli-shared": "3.0.0-4070520250711001",
+    "@dcloudio/uni-stacktracey": "3.0.0-4070520250711001",
+    "@dcloudio/vite-plugin-uni": "3.0.0-4070520250711001",
     "@esbuild/darwin-arm64": "0.20.2",
     "@esbuild/darwin-x64": "0.20.2",
     "@iconify-json/carbon": "^1.2.4",
     "@rollup/rollup-darwin-x64": "^4.28.0",
     "@types/node": "^20.17.9",
-    "@types/wechat-miniprogram": "^3.4.8",
-    "@uni-helper/eslint-config": "^0.4.0",
-    "@uni-helper/uni-types": "1.0.0-alpha.3",
+    "@uni-helper/eslint-config": "^0.5.0",
+    "@uni-helper/plugin-uni": "0.1.0",
+    "@uni-helper/uni-env": "^0.1.8",
+    "@uni-helper/uni-types": "^1.0.0-alpha.6",
     "@uni-helper/unocss-preset-uni": "^0.2.11",
     "@uni-helper/vite-plugin-uni-components": "0.2.0",
-    "@uni-helper/vite-plugin-uni-layouts": "0.1.10",
-    "@uni-helper/vite-plugin-uni-manifest": "0.2.8",
-    "@uni-helper/vite-plugin-uni-pages": "0.2.28",
-    "@uni-helper/vite-plugin-uni-platform": "0.0.4",
+    "@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.8",
+    "@uni-helper/vite-plugin-uni-platform": "^0.0.5",
     "@uni-ku/bundle-optimizer": "^1.3.3",
+    "@uni-ku/root": "^1.3.4",
     "@unocss/eslint-plugin": "^66.2.3",
-    "@unocss/preset-legacy-compat": "^0.59.4",
-    "@vue/runtime-core": "^3.4.21",
+    "@vue/runtime-core": "3.4.21",
     "@vue/tsconfig": "^0.1.3",
     "autoprefixer": "^10.4.20",
-    "eslint": "^9.29.0",
+    "cross-env": "^10.0.0",
+    "eslint": "^9.31.0",
     "eslint-plugin-format": "^1.0.1",
     "husky": "^9.1.7",
     "lint-staged": "^15.2.10",
-    "openapi-ts-request": "^1.1.2",
+    "miniprogram-api-typings": "^4.1.0",
+    "openapi-ts-request": "^1.6.7",
     "postcss": "^8.4.49",
-    "postcss-html": "^1.7.0",
+    "postcss-html": "^1.8.0",
     "postcss-scss": "^4.0.9",
-    "rollup-plugin-visualizer": "^5.12.0",
+    "rollup-plugin-visualizer": "^6.0.3",
     "sass": "1.77.8",
-    "typescript": "^5.7.2",
-    "unocss": "65.4.2",
-    "unplugin-auto-import": "^0.17.8",
+    "typescript": "~5.8.0",
+    "unocss": "66.0.0",
+    "unplugin-auto-import": "^20.0.0",
     "vite": "5.2.8",
-    "vite-plugin-restart": "^0.4.2",
-    "vue-tsc": "^2.2.10"
+    "vite-plugin-restart": "^1.0.0",
+    "vue-tsc": "^3.0.6"
   },
   "resolutions": {
     "bin-wrapper": "npm:bin-wrapper-china"

+ 2 - 2
pages.config.ts

@@ -1,5 +1,5 @@
 import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
-import { tabBar } from './src/layouts/fg-tabbar/tabbarList'
+import { tabBar } from './src/tabbar/config'
 
 export default defineUniPages({
   globalStyle: {
@@ -18,6 +18,6 @@ export default defineUniPages({
         'z-paging/components/z-paging$1/z-paging$1.vue',
     },
   },
-  // tabbar 的配置统一在 “./src/layouts/fg-tabbar/tabbarList.ts” 文件中
+  // tabbar 的配置统一在 “./src/tabbar/config.ts” 文件中
   tabBar: tabBar as any,
 })

+ 0 - 13
patches/@dcloudio__uni-h5.patch

@@ -1,13 +0,0 @@
-diff --git a/dist/uni-h5.es.js b/dist/uni-h5.es.js
-index 7421bad97d94ad34a3d4d94292a9ee9071430662..19c6071ee4036ceb8d1cfa09030e471c002d2cda 100644
---- a/dist/uni-h5.es.js
-+++ b/dist/uni-h5.es.js
-@@ -23410,7 +23410,7 @@ function useShowTabBar(emit2) {
-   const tabBar2 = useTabBar();
-   const showTabBar2 = computed(() => route.meta.isTabBar && tabBar2.shown);
-   updateCssVar({
--    "--tab-bar-height": tabBar2.height
-+    "--tab-bar-height": tabBar2?.height || 0
-   });
-   return showTabBar2;
- }

Datei-Diff unterdrückt, da er zu groß ist
+ 2701 - 3387
pnpm-lock.yaml


+ 0 - 6
pnpm-workspace.yaml

@@ -1,6 +0,0 @@
-packages:
-  - '**'
-  - '!node_modules'
-
-patchedDependencies:
-  '@dcloudio/uni-h5': patches/@dcloudio__uni-h5.patch

+ 29 - 0
scripts/window-path-loader.js

@@ -0,0 +1,29 @@
+// fix: https://github.com/unibest-tech/unibest/issues/219
+
+// Windows path loader for Node.js ESM
+// This loader converts Windows absolute paths to file:// URLs
+
+import { pathToFileURL } from 'node:url'
+
+/**
+ * Resolve hook for ESM loader
+ * Converts Windows absolute paths to file:// URLs
+ */
+export function resolve(specifier, context, defaultResolve) {
+  // Check if this is a Windows absolute path (starts with drive letter like C:)
+  if (specifier.match(/^[a-z]:\\/i) || specifier.match(/^[a-z]:\//i)) {
+    // Convert Windows path to file:// URL
+    const fileUrl = pathToFileURL(specifier).href
+    return defaultResolve(fileUrl, context, defaultResolve)
+  }
+
+  // For all other specifiers, use the default resolve
+  return defaultResolve(specifier, context, defaultResolve)
+}
+
+/**
+ * Load hook for ESM loader
+ */
+export function load(url, context, defaultLoad) {
+  return defaultLoad(url, context, defaultLoad)
+}

+ 39 - 0
src/App.ku.vue

@@ -0,0 +1,39 @@
+<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())
+  const { path } = currRoute()
+  isCurrentPageTabbar.value = isPageTabbar(path)
+})
+
+const helloKuRoot = ref('Hello AppKuVue')
+
+const exposeRef = ref('this is form app.Ku.vue')
+
+defineExpose({
+  exposeRef,
+})
+</script>
+
+<template>
+  <wd-config-provider :theme-vars="themeStore.themeVars" :theme="themeStore.theme">
+    <!-- 这个先隐藏了,知道这样用就行 -->
+    <view class="hidden text-center">
+      {{ helloKuRoot }},这里可以配置全局的东西
+    </view>
+
+    <KuRootView />
+
+    <FgTabbar v-if="isCurrentPageTabbar" />
+    <wd-toast />
+    <wd-message-box />
+  </wd-config-provider>
+</template>

+ 13 - 7
src/App.vue

@@ -1,15 +1,21 @@
 <script setup lang="ts">
 import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
-import { usePageAuth } from '@/hooks/usePageAuth'
+import { navigateToInterceptor } from '@/router/interceptor'
 import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
 
-usePageAuth()
-
-onLaunch(() => {
-  console.log('App Launch')
+onLaunch((options) => {
+  console.log('App Launch', options)
 })
-onShow(() => {
-  console.log('App Show')
+onShow((options) => {
+  console.log('App Show', options)
+  // 处理直接进入页面路由的情况:如h5直接输入路由、微信小程序分享后进入等
+  // https://github.com/unibest-tech/unibest/issues/192
+  if (options?.path) {
+    navigateToInterceptor.invoke({ url: `/${options.path}`, query: options.query })
+  }
+  else {
+    navigateToInterceptor.invoke({ url: '/' })
+  }
 })
 onHide(() => {
   console.log('App Hide')

+ 1 - 1
src/api/foo-alova.ts

@@ -1,4 +1,4 @@
-import { API_DOMAINS, http } from '@/http/request/alova'
+import { API_DOMAINS, http } from '@/http/alova'
 
 export interface IFoo {
   id: number

+ 11 - 0
src/api/foo-vue-query.ts

@@ -0,0 +1,11 @@
+import { queryOptions } from '@tanstack/vue-query'
+import { getFooAPI } from './foo'
+
+export function getFooQueryOptions(name: string) {
+  return queryOptions({
+    queryFn: async ({ queryKey }) => {
+      return getFooAPI(queryKey[1])
+    },
+    queryKey: ['getFoo', name],
+  })
+}

+ 27 - 0
src/api/foo.ts

@@ -14,3 +14,30 @@ export function foo() {
     },
   })
 }
+
+export interface IFooItem {
+  id: string
+  name: string
+}
+
+/** GET 请求 */
+export function getFooAPI(name: string) {
+  return http.get<IFooItem>('/foo', { name })
+}
+/** GET 请求;支持 传递 header 的范例 */
+export function getFooAPI2(name: string) {
+  return http.get<IFooItem>('/foo', { name }, { 'Content-Type-100': '100' })
+}
+
+/** POST 请求 */
+export function postFooAPI(name: string) {
+  return http.post<IFooItem>('/foo', { name })
+}
+/** POST 请求;需要传递 query 参数的范例;微信小程序经常有同时需要query参数和body参数的场景 */
+export function postFooAPI2(name: string) {
+  return http.post<IFooItem>('/foo', { name })
+}
+/** POST 请求;支持 传递 header 的范例 */
+export function postFooAPI3(name: string) {
+  return http.post<IFooItem>('/foo', { name }, { name }, { 'Content-Type-100': '100' })
+}

+ 13 - 9
src/api/login.ts

@@ -1,4 +1,4 @@
-import type { ICaptcha, IUpdateInfo, IUpdatePassword, IUserInfoVo, IUserLogin } from './types/login'
+import type { IAuthLoginRes, ICaptcha, IDoubleTokenRes, IUpdateInfo, IUpdatePassword, IUserInfoRes } from './types/login'
 import { http } from '@/http/http'
 
 /**
@@ -24,21 +24,29 @@ export function getCode() {
  * @param loginForm 登录表单
  */
 export function login(loginForm: ILoginForm) {
-  return http.post<IUserLogin>('/user/login', loginForm)
+  return http.post<IAuthLoginRes>('/auth/login', loginForm)
+}
+
+/**
+ * 刷新token
+ * @param refreshToken 刷新token
+ */
+export function refreshToken(refreshToken: string) {
+  return http.post<IDoubleTokenRes>('/auth/refreshToken', { refreshToken })
 }
 
 /**
  * 获取用户信息
  */
 export function getUserInfo() {
-  return http.get<IUserInfoVo>('/user/info')
+  return http.get<IUserInfoRes>('/user/info')
 }
 
 /**
  * 退出登录
  */
 export function logout() {
-  return http.get<void>('/user/logout')
+  return http.get<void>('/auth/logout')
 }
 
 /**
@@ -69,15 +77,11 @@ export function getWxCode() {
   })
 }
 
-/**
- * 微信登录参数
- */
-
 /**
  * 微信登录
  * @param params 微信登录参数,包含code
  * @returns Promise 包含登录结果
  */
 export function wxLogin(data: { code: string }) {
-  return http.post<IUserLogin>('/user/wxLogin', data)
+  return http.post<IAuthLoginRes>('/auth/wxLogin', data)
 }

+ 51 - 11
src/api/types/login.ts

@@ -1,20 +1,42 @@
-/**
- * 用户信息
- */
-export interface IUserInfoVo {
-  id: number
-  username: string
-  avatar: string
+// 认证模式类型
+export type AuthMode = 'single' | 'double'
+
+// 单Token响应类型
+export interface ISingleTokenRes {
   token: string
+  expiresIn: number // 有效期(秒)
 }
 
+// 双Token响应类型
+export interface IDoubleTokenRes {
+  accessToken: string
+  refreshToken: string
+  accessExpiresIn: number // 访问令牌有效期(秒)
+  refreshExpiresIn: number // 刷新令牌有效期(秒)
+}
+
+/**
+ * 登录返回的信息,其实就是 token 信息
+ */
+export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes
+
 /**
- * 登录返回的信息
+ * 用户信息
  */
-export interface IUserLogin {
-  id: string
+export interface IUserInfoRes {
+  userId: number
   username: string
-  token: string
+  nickname: string
+  avatar?: string
+  [key: string]: any // 允许其他扩展字段
+}
+
+// 认证存储数据结构
+export interface AuthStorage {
+  mode: AuthMode
+  tokens: ISingleTokenRes | IDoubleTokenRes
+  userInfo?: IUserInfoRes
+  loginTime: number // 登录时间戳
 }
 
 /**
@@ -55,3 +77,21 @@ export interface IUpdatePassword {
   newPassword: string
   confirmPassword: string
 }
+
+/**
+ * 判断是否为单Token响应
+ * @param tokenRes 登录响应数据
+ * @returns 是否为单Token响应
+ */
+export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes {
+  return 'token' in tokenRes && !('refreshToken' in tokenRes)
+}
+
+/**
+ * 判断是否为双Token响应
+ * @param tokenRes 登录响应数据
+ * @returns 是否为双Token响应
+ */
+export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes {
+  return 'accessToken' in tokenRes && 'refreshToken' in tokenRes
+}

+ 3 - 1
src/env.d.ts

@@ -16,9 +16,11 @@ interface ImportMetaEnv {
   /** 后台接口地址 */
   readonly VITE_SERVER_BASEURL: string
   /** H5是否需要代理 */
-  readonly VITE_APP_PROXY: 'true' | 'false'
+  readonly VITE_APP_PROXY_ENABLE: 'true' | 'false'
   /** H5是否需要代理,需要的话有个前缀 */
   readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
+  /** 认证模式,'single' | 'double' ==> 单token | 双token */
+  readonly VITE_AUTH_MODE: 'single' | 'double'
   /** 上传图片地址 */
   readonly VITE_UPLOAD_BASEURL: string
   /** 是否清除console */

+ 0 - 0
src/hooks/.gitkeep


+ 0 - 50
src/hooks/usePageAuth.ts

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

+ 13 - 0
src/http/README.md

@@ -0,0 +1,13 @@
+# 请求库
+
+目前unibest支持3种请求库:
+- 菲鸽简单封装的 `简单版本http`,路径(src/http/http.ts),对应的示例在 src/api/foo.ts
+- `alova 的 http`,路径(src/http/alova.ts),对应的示例在 src/api/foo-alova.ts
+- `vue-query`, 路径(src/http/vue-query.ts), 目前主要用在自动生成接口,详情看(https://unibest.tech/base/17-generate),示例在 src/service/app 文件夹
+
+## 如何选择
+如果您以前用过 alova 或者 vue-query,可以优先使用您熟悉的。
+如果您的项目简单,简单版本的http 就够了,也不会增加包体积。(发版的时候可以去掉alova和vue-query,如果没有超过包体积,留着也无所谓 ^_^)
+
+## roadmap
+菲鸽最近在优化脚手架,后续可以选择是否使用第三方的请求库,以及选择什么请求库。还在开发中,大概月底出来(8月31号)。

+ 12 - 6
src/http/request/alova.ts

@@ -4,8 +4,8 @@ import AdapterUniapp from '@alova/adapter-uniapp'
 import { createAlova } from 'alova'
 import { createServerTokenAuthentication } from 'alova/client'
 import VueHook from 'alova/vue'
-import { toast } from '@/utils/toast'
-import { ContentTypeEnum, ResultEnum, ShowMessage } from './enum'
+import { LOGIN_PAGE } from '@/router/config'
+import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
 
 // 配置动态Tag
 export const API_DOMAINS = {
@@ -30,7 +30,7 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
       }
       catch (error) {
         // 切换到登录页
-        await uni.reLaunch({ url: '/pages/common/login/index' })
+        await uni.reLaunch({ url: LOGIN_PAGE })
         throw error
       }
     },
@@ -41,7 +41,7 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
  * alova 请求实例
  */
 const alovaInstance = createAlova({
-  baseURL: import.meta.env.VITE_API_BASE_URL,
+  baseURL: import.meta.env.VITE_APP_PROXY_PREFIX,
   ...AdapterUniapp(),
   timeout: 5000,
   statesHook: VueHook,
@@ -91,7 +91,10 @@ const alovaInstance = createAlova({
     if (statusCode !== 200) {
       const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
       console.error('errorMessage===>', errorMessage)
-      toast.error(errorMessage)
+      uni.showToast({
+        title: errorMessage,
+        icon: 'error',
+      })
       throw new Error(`${errorMessage}:${errMsg}`)
     }
 
@@ -99,7 +102,10 @@ const alovaInstance = createAlova({
     const { code, message, data } = rawData as IResponse
     if (code !== ResultEnum.Success) {
       if (config.meta?.toast !== false) {
-        toast.warning(message)
+        uni.showToast({
+          title: message,
+          icon: 'none',
+        })
       }
       throw new Error(`请求错误[${code}]:${message}`)
     }

+ 72 - 8
src/http/http.ts

@@ -1,4 +1,13 @@
-import type { CustomRequestOptions } from '@/http/interceptor'
+import type { IDoubleTokenRes } from '@/api/types/login'
+import type { CustomRequestOptions } from '@/http/types'
+import { nextTick } from 'vue'
+import { LOGIN_PAGE } from '@/router/config'
+import { useTokenStore } from '@/store/token'
+import { isDoubleTokenMode } from '@/utils'
+
+// 刷新 token 状态管理
+let refreshing = false // 防止重复刷新 token 标识
+let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
 
 export function http<T>(options: CustomRequestOptions) {
   // 1. 返回 Promise 对象
@@ -10,17 +19,72 @@ export function http<T>(options: CustomRequestOptions) {
       responseType: 'json',
       // #endif
       // 响应成功
-      success(res) {
+      success: async (res) => {
         // 状态码 2xx,参考 axios 的设计
         if (res.statusCode >= 200 && res.statusCode < 300) {
           // 2.1 提取核心数据 res.data
-          resolve(res.data as IResData<T>)
+          return resolve(res.data as IResData<T>)
         }
-        else if (res.statusCode === 401) {
-          // 401错误  -> 清理用户信息,跳转到登录页
-          // userStore.clearUserInfo()
-          // uni.navigateTo({ url: '/pages/login/login' })
-          reject(res)
+        const resData: IResData<T> = res.data as IResData<T>
+        if ((res.statusCode === 401) || (resData.code === 401)) {
+          const tokenStore = useTokenStore()
+          if (!isDoubleTokenMode) {
+            // 未启用双token策略,清理用户信息,跳转到登录页
+            tokenStore.logout()
+            uni.navigateTo({ url: LOGIN_PAGE })
+            return reject(res)
+          }
+          /* -------- 无感刷新 token ----------- */
+          const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
+          // token 失效的,且有刷新 token 的,才放到请求队列里
+          if ((res.statusCode === 401 || resData.code === 401) && refreshToken) {
+            taskQueue.push(() => {
+              resolve(http<T>(options))
+            })
+          }
+          // 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
+          if ((res.statusCode === 401 || resData.code === 401) && refreshToken && !refreshing) {
+            refreshing = true
+            try {
+              // 发起刷新 token 请求(使用 store 的 refreshToken 方法)
+              await tokenStore.refreshToken()
+              // 刷新 token 成功
+              refreshing = false
+              nextTick(() => {
+                // 关闭其他弹窗
+                uni.hideToast()
+                uni.showToast({
+                  title: 'token 刷新成功',
+                  icon: 'none',
+                })
+              })
+              // 将任务队列的所有任务重新请求
+              taskQueue.forEach(task => task())
+            }
+            catch (refreshErr) {
+              console.error('刷新 token 失败:', refreshErr)
+              refreshing = false
+              // 刷新 token 失败,跳转到登录页
+              nextTick(() => {
+                // 关闭其他弹窗
+                uni.hideToast()
+                uni.showToast({
+                  title: '登录已过期,请重新登录',
+                  icon: 'none',
+                })
+              })
+              // 清除用户信息
+              await tokenStore.logout()
+              // 跳转到登录页
+              setTimeout(() => {
+                uni.navigateTo({ url: LOGIN_PAGE })
+              }, 2000)
+            }
+            finally {
+              // 不管刷新 token 成功与否,都清空任务队列
+              taskQueue = []
+            }
+          }
         }
         else {
           // 其他错误 -> 根据后端错误信息轻提示

+ 4 - 9
src/http/interceptor.ts

@@ -1,13 +1,8 @@
+import type { CustomRequestOptions } from '@/http/types'
 import { useUserStore } from '@/store'
 import { getEnvBaseUrl } from '@/utils'
 import { platform } from '@/utils/platform'
-import { stringifyQuery } from './queryString'
-
-export type CustomRequestOptions = UniApp.RequestOptions & {
-  query?: Record<string, any>
-  /** 出错时是否隐藏错误提示 */
-  hideErrorToast?: boolean
-} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
+import { stringifyQuery } from './tools/queryString'
 
 // 请求基准地址
 const baseUrl = getEnvBaseUrl()
@@ -45,7 +40,7 @@ const httpInterceptor = {
       // TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
     }
     // 1. 请求超时
-    options.timeout = 10000 // 10s
+    options.timeout = 60000 // 60s
     // 2. (可选)添加小程序端请求头标识
     options.header = {
       platform, // 可选,与 uniapp 定义的平台一致,告诉后台来源
@@ -53,7 +48,7 @@ const httpInterceptor = {
     }
     // 3. 添加 token 请求头标识
     const userStore = useUserStore()
-    const { token } = userStore.userInfo as unknown as IUserInfo
+    const { token } = userStore.userInfo as unknown as IUserToken
     if (token) {
       options.header.Authorization = `Bearer ${token}`
     }

src/http/request/enum.ts → src/http/tools/enum.ts


src/http/queryString.ts → src/http/tools/queryString.ts


+ 9 - 0
src/http/request/types.ts

@@ -1,3 +1,12 @@
+/**
+ * 在 uniapp 的 RequestOptions 和 IUniUploadFileOptions 基础上,添加自定义参数
+ */
+export type CustomRequestOptions = UniApp.RequestOptions & {
+  query?: Record<string, any>
+  /** 出错时是否隐藏错误提示 */
+  hideErrorToast?: boolean
+} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
+
 // 通用响应格式
 export interface IResponse<T = any> {
   code: number | string

+ 30 - 0
src/http/vue-query.ts

@@ -0,0 +1,30 @@
+import type { CustomRequestOptions } from '@/http/types'
+import { http } from './http'
+
+/*
+ * openapi-ts-request 工具的 request 跨客户端适配方法
+ */
+export default function request<T = unknown>(
+  url: string,
+  options: Omit<CustomRequestOptions, 'url'> & {
+    params?: Record<string, unknown>
+    headers?: Record<string, unknown>
+  },
+) {
+  const requestOptions = {
+    url,
+    ...options,
+  }
+
+  if (options.params) {
+    requestOptions.query = requestOptions.params
+    delete requestOptions.params
+  }
+
+  if (options.headers) {
+    requestOptions.header = options.headers
+    delete requestOptions.headers
+  }
+
+  return http<T>(requestOptions)
+}

+ 5 - 12
src/layouts/default.vue

@@ -1,17 +1,10 @@
 <script lang="ts" setup>
-import type { ConfigProviderThemeVars } from 'wot-design-uni'
-
-const themeVars: ConfigProviderThemeVars = {
-  // colorTheme: 'red',
-  // buttonPrimaryBgColor: '#07c160',
-  // buttonPrimaryColor: '#07c160',
-}
+const testUniLayoutExposedData = ref('testUniLayoutExposedData')
+defineExpose({
+  testUniLayoutExposedData,
+})
 </script>
 
 <template>
-  <wd-config-provider :theme-vars="themeVars">
-    <slot />
-    <wd-toast />
-    <wd-message-box />
-  </wd-config-provider>
+  <slot />
 </template>

+ 0 - 68
src/layouts/fg-tabbar/fg-tabbar.vue

@@ -1,68 +0,0 @@
-<script setup lang="ts">
-import { tabbarStore } from './tabbar'
-// 'i-carbon-code',
-import { tabbarList as _tabBarList, cacheTabbarEnable, selectedTabbarStrategy, TABBAR_MAP } from './tabbarList'
-
-const customTabbarEnable
-= selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITH_CACHE
-  || selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITHOUT_CACHE
-
-/** tabbarList 里面的 path 从 pages.config.ts 得到 */
-const tabbarList = _tabBarList.map(item => ({ ...item, path: `/${item.pagePath}` }))
-function selectTabBar({ value: index }: { value: number }) {
-  const url = tabbarList[index].path
-  tabbarStore.setCurIdx(index)
-  if (cacheTabbarEnable) {
-    uni.switchTab({ url })
-  }
-  else {
-    uni.navigateTo({ url })
-  }
-}
-onLoad(() => {
-  // 解决原生 tabBar 未隐藏导致有2个 tabBar 的问题
-  const hideRedundantTabbarEnable = selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITH_CACHE
-  hideRedundantTabbarEnable
-  && uni.hideTabBar({
-    fail(err) {
-      console.log('hideTabBar fail: ', err)
-    },
-    success(res) {
-      console.log('hideTabBar success: ', res)
-    },
-  })
-})
-</script>
-
-<template>
-  <wd-tabbar
-    v-if="customTabbarEnable"
-    v-model="tabbarStore.curIdx"
-    bordered
-    safe-area-inset-bottom
-    placeholder
-    fixed
-    @change="selectTabBar"
-  >
-    <block v-for="(item, idx) in tabbarList" :key="item.path">
-      <wd-tabbar-item v-if="item.iconType === 'uiLib'" :title="item.text" :icon="item.icon" />
-      <wd-tabbar-item
-        v-else-if="item.iconType === 'unocss' || item.iconType === 'iconfont'"
-        :title="item.text"
-      >
-        <template #icon>
-          <view
-            h-40rpx
-            w-40rpx
-            :class="[item.icon, idx === tabbarStore.curIdx ? 'is-active' : 'is-inactive']"
-          />
-        </template>
-      </wd-tabbar-item>
-      <wd-tabbar-item v-else-if="item.iconType === 'local'" :title="item.text">
-        <template #icon>
-          <image :src="item.icon" h-40rpx w-40rpx />
-        </template>
-      </wd-tabbar-item>
-    </block>
-  </wd-tabbar>
-</template>

+ 0 - 17
src/layouts/fg-tabbar/tabbar.md

@@ -1,17 +0,0 @@
-# tabbar 说明
-
-`tabbar` 分为 `4 种` 情况:
-
-- 0 `无 tabbar`,只有一个页面入口,底部无 `tabbar` 显示;常用语临时活动页。
-- 1 `原生 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。
-  - 优势:原生自带的 tabbar,最先渲染,有缓存。
-  - 劣势:只能使用 2 组图片来切换选中和非选中状态,修改颜色只能重新换图片(或者用 iconfont)。
-- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。
-  - 优势:可以随意配置自己想要的 `svg icon`,切换字体颜色方便。有缓存。可以实现各种花里胡哨的动效等。
-  - 劣势:首次点击 tababr 会闪烁。
-- 3 `无缓存自定义 tabbar`,使用 `navigateTo` 切换 `tabbar`,`tabbar` 页面无缓存。使用了第三方 UI 库的 `tabbar` 组件。
-  - 优势:可以随意配置自己想要的 svg icon,切换字体颜色方便。可以实现各种花里胡哨的动效等。
-  - 劣势:首次点击 `tababr` 会闪烁,无缓存。
-
-
-> 注意:花里胡哨的效果需要自己实现,本模版不提供。

+ 0 - 11
src/layouts/fg-tabbar/tabbar.ts

@@ -1,11 +0,0 @@
-/**
- * tabbar 状态,增加 storageSync 保证刷新浏览器时在正确的 tabbar 页面
- * 使用reactive简单状态,而不是 pinia 全局状态
- */
-export const tabbarStore = reactive({
-  curIdx: uni.getStorageSync('app-tabbar-index') || 0,
-  setCurIdx(idx: number) {
-    this.curIdx = idx
-    uni.setStorageSync('app-tabbar-index', idx)
-  },
-})

+ 0 - 74
src/layouts/fg-tabbar/tabbarList.ts

@@ -1,74 +0,0 @@
-import type { TabBar } from '@uni-helper/vite-plugin-uni-pages'
-
-/**
- * tabbar 选择的策略,更详细的介绍见 tabbar.md 文件
- * 0: 'NO_TABBAR' `无 tabbar`
- * 1: 'NATIVE_TABBAR'  `完全原生 tabbar`
- * 2: 'CUSTOM_TABBAR_WITH_CACHE' `有缓存自定义 tabbar`
- * 3: 'CUSTOM_TABBAR_WITHOUT_CACHE' `无缓存自定义 tabbar`
- *
- * 温馨提示:本文件的任何代码更改了之后,都需要重新运行,否则 pages.json 不会更新导致错误
- */
-export const TABBAR_MAP = {
-  NO_TABBAR: 0,
-  NATIVE_TABBAR: 1,
-  CUSTOM_TABBAR_WITH_CACHE: 2,
-  CUSTOM_TABBAR_WITHOUT_CACHE: 3,
-}
-// TODO:通过这里切换使用tabbar的策略
-export const selectedTabbarStrategy = TABBAR_MAP.NATIVE_TABBAR
-
-// selectedTabbarStrategy==NATIVE_TABBAR(1) 时,需要填 iconPath 和 selectedIconPath
-// selectedTabbarStrategy==CUSTOM_TABBAR(2,3) 时,需要填 icon 和 iconType
-// selectedTabbarStrategy==NO_TABBAR(0) 时,tabbarList 不生效
-export const tabbarList: TabBar['list'] = [
-  {
-    iconPath: 'static/tabbar/home.png',
-    selectedIconPath: 'static/tabbar/homeHL.png',
-    pagePath: 'pages/index/index',
-    text: '首页',
-    icon: 'home',
-    // 选用 UI 框架自带的 icon 时,iconType 为 uiLib
-    iconType: 'uiLib',
-  },
-  {
-    iconPath: 'static/tabbar/example.png',
-    selectedIconPath: 'static/tabbar/exampleHL.png',
-    pagePath: 'pages/about/about',
-    text: '关于',
-    icon: 'i-carbon-code',
-    // 注意 unocss 的图标需要在 页面上引入一下,或者配置到 unocss.config.ts 的 safelist 中
-    iconType: 'unocss',
-  },
-  // {
-  //   pagePath: 'pages/my/index',
-  //   text: '我的',
-  //   icon: '/static/logo.svg',
-  //   iconType: 'local',
-  // },
-  // {
-  //   pagePath: 'pages/mine/index',
-  //   text: '我的',
-  //   icon: 'iconfont icon-my',
-  //   iconType: 'iconfont',
-  // },
-]
-
-// NATIVE_TABBAR(1) 和 CUSTOM_TABBAR_WITH_CACHE(2) 时,需要tabbar缓存
-export const cacheTabbarEnable = selectedTabbarStrategy === TABBAR_MAP.NATIVE_TABBAR
-  || selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITH_CACHE
-
-const _tabbar: TabBar = {
-  color: '#999999',
-  selectedColor: '#018d71',
-  backgroundColor: '#F8F8F8',
-  borderStyle: 'black',
-  height: '50px',
-  fontSize: '10px',
-  iconWidth: '24px',
-  spacing: '3px',
-  list: tabbarList,
-}
-
-// 0和1 需要显示底部的tabbar的各种配置,以利用缓存
-export const tabBar = cacheTabbarEnable ? _tabbar : undefined

+ 0 - 19
src/layouts/tabbar.vue

@@ -1,19 +0,0 @@
-<script lang="ts" setup>
-import type { ConfigProviderThemeVars } from 'wot-design-uni'
-import FgTabbar from './fg-tabbar/fg-tabbar.vue'
-
-const themeVars: ConfigProviderThemeVars = {
-  // colorTheme: 'red',
-  // buttonPrimaryBgColor: '#07c160',
-  // buttonPrimaryColor: '#07c160',
-}
-</script>
-
-<template>
-  <wd-config-provider :theme-vars="themeVars">
-    <slot />
-    <FgTabbar />
-    <wd-toast />
-    <wd-message-box />
-  </wd-config-provider>
-</template>

+ 4 - 2
src/manifest.json

@@ -35,7 +35,7 @@
           "<uses-feature android:name=\"android.hardware.camera\"/>",
           "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
         ],
-        "minSdkVersion": 30,
+        "minSdkVersion": 21,
         "targetSdkVersion": 30,
         "abiFilters": [
           "armeabi-v7a",
@@ -109,6 +109,8 @@
   },
   "vueVersion": "3",
   "h5": {
-    "router": {}
+    "router": {
+      "base": "/"
+    }
   }
 }

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

@@ -0,0 +1,48 @@
+<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>

+ 14 - 10
src/pages-sub/demo/index.vue

@@ -1,14 +1,12 @@
-<route lang="jsonc" type="page">
-{
-  "layout": "default",
-  "style": {
-    "navigationBarTitleText": "分包页面"
-  }
-}
-</route>
-
 <script lang="ts" setup>
 // code here
+import RequestComp from './components/request.vue'
+
+definePage({
+  style: {
+    navigationBarTitleText: '分包页面',
+  },
+})
 </script>
 
 <template>
@@ -16,9 +14,15 @@
     <view class="m-8">
       http://localhost:9000/#/pages-sub/demo/index
     </view>
-    <view class="text-green-500">
+    <view class="my-4 text-green-500">
       分包页面demo
     </view>
+    <view class="text-blue-500">
+      分包页面里面的components示例
+    </view>
+    <view>
+      <RequestComp />
+    </view>
   </view>
 </template>
 

+ 36 - 15
src/pages.json

@@ -15,6 +15,7 @@
     }
   },
   "tabBar": {
+    "custom": true,
     "color": "#999999",
     "selectedColor": "#018d71",
     "backgroundColor": "#F8F8F8",
@@ -25,20 +26,16 @@
     "spacing": "3px",
     "list": [
       {
-        "iconPath": "static/tabbar/home.png",
-        "selectedIconPath": "static/tabbar/homeHL.png",
-        "pagePath": "pages/index/index",
         "text": "首页",
-        "icon": "home",
-        "iconType": "uiLib"
+        "pagePath": "pages/index/index"
       },
       {
-        "iconPath": "static/tabbar/example.png",
-        "selectedIconPath": "static/tabbar/exampleHL.png",
-        "pagePath": "pages/about/about",
         "text": "关于",
-        "icon": "i-carbon-code",
-        "iconType": "unocss"
+        "pagePath": "pages/about/about"
+      },
+      {
+        "text": "我的",
+        "pagePath": "pages/me/me"
       }
     ]
   },
@@ -46,7 +43,6 @@
     {
       "path": "pages/index/index",
       "type": "home",
-      "layout": "tabbar",
       "style": {
         "navigationStyle": "custom",
         "navigationBarTitleText": "首页"
@@ -55,7 +51,6 @@
     {
       "path": "pages/about/about",
       "type": "page",
-      "layout": "tabbar",
       "style": {
         "navigationBarTitleText": "关于"
       }
@@ -63,9 +58,36 @@
     {
       "path": "pages/about/alova",
       "type": "page",
-      "layout": "default",
       "style": {
-        "navigationBarTitleText": "Alova 请求演示"
+        "navigationBarTitleText": "Alova 演示"
+      }
+    },
+    {
+      "path": "pages/about/vue-query",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "Vue Query 演示"
+      }
+    },
+    {
+      "path": "pages/login/login",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "登录"
+      }
+    },
+    {
+      "path": "pages/login/register",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "注册"
+      }
+    },
+    {
+      "path": "pages/me/me",
+      "type": "page",
+      "style": {
+        "navigationBarTitleText": "我的"
       }
     }
   ],
@@ -76,7 +98,6 @@
         {
           "path": "demo/index",
           "type": "page",
-          "layout": "default",
           "style": {
             "navigationBarTitleText": "分包页面"
           }

+ 102 - 36
src/pages/about/about.vue

@@ -1,52 +1,118 @@
-<route lang="jsonc" type="page">
-{
-  "layout": "tabbar",
-  "style": {
-    "navigationBarTitleText": "关于"
-  }
-}
-</route>
-
 <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 { tabbarStore } from '@/tabbar/store'
 import RequestComp from './components/request.vue'
-import UploadComp from './components/upload.vue'
+import VBindCss from './components/VBindCss.vue'
+
+definePage({
+  style: {
+    navigationBarTitleText: '关于',
+  },
+})
+
+// 浏览器打印 isH5为true, isWeb为false,大家尽量用 isH5
+console.log({ isApp, isAppAndroid, isAppHarmony, isAppIOS, isAppPlus, isH5, isMpWeixin, isWeb })
 
-// 奇怪:同样的代码放在 vue 里面不会校验到错误,放在 .ts 文件里面会校验到错误
-// const testOxlint = (name: string) => {
-//   console.log('oxlint')
-// }
-// testOxlint('oxlint')
-console.log('about')
+function toLogin() {
+  uni.navigateTo({
+    url: `${LOGIN_PAGE}?redirect=${encodeURIComponent('/pages/about/about')}`,
+  })
+}
 
 function gotoAlova() {
   uni.navigateTo({
     url: '/pages/about/alova',
   })
 }
+function gotoVueQuery() {
+  uni.navigateTo({
+    url: '/pages/about/vue-query',
+  })
+}
+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
+})
+
+function gotoTabbar() {
+  uni.switchTab({
+    url: '/pages/index/index',
+  })
+}
+// #region setTabbarBadge
+function setTabbarBadge() {
+  tabbarStore.setTabbarItemBadge(1, 100)
+}
+// #endregion
+
+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>
+<template root="uniKuRoot">
   <view>
-    <view class="mt-8 text-center text-3xl">
-      鸽友们好,我是
-      <text class="text-red-500">
-        菲鸽
-      </text>
+    <view class="mt-8 text-center text-xl text-gray-400">
+      请求调用、unocss、static图片
     </view>
-    <RequestComp />
-    <UploadComp />
-    <button class="w-200px text-green" @click="gotoAlova">
-      前往 alova 页面
+    <view class="my-2 text-center">
+      <image src="/static/images/avatar.jpg" class="h-100px w-100px" />
+    </view>
+    <button class="mt-4 w-40 text-center" @click="toLogin">
+      点击去登录页
+    </button>
+    <button class="mt-4 w-60 text-center" @click="setTabbarBadge">
+      设置tabbarBadge
     </button>
+    <RequestComp />
+    <VBindCss />
+    <view class="mb-6 h-1px bg-#eee" />
+    <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="gotoTabbar">
+        切换tabbar
+      </button>
+    </view>
+    <view class="text-center">
+      <button type="primary" size="mini" class="w-160px" @click="gotoVueQuery">
+        vue-query 示例页面
+      </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>
-
-<style lang="scss" scoped>
-.test-css {
-  // 16rpx=>0.5rem
-  padding-bottom: 16rpx;
-  // mt-4=>1rem=>16px;
-  margin-top: 16px;
-  text-align: center;
-}
-</style>

+ 8 - 11
src/pages/about/alova.vue

@@ -1,16 +1,13 @@
-<route lang="jsonc" type="page">
-{
-  "layout": "default",
-  "style": {
-    "navigationBarTitleText": "Alova 请求演示"
-  }
-}
-</route>
-
 <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, {
@@ -25,7 +22,7 @@ function reset() {
 
 <template>
   <view class="p-6 text-center">
-    <button class="my-6 w-200px text-green" @click="send">
+    <button type="primary" size="mini" class="my-6 w-160px" @click="send">
       发送请求
     </button>
     <view class="h-16">
@@ -45,7 +42,7 @@ function reset() {
         {{ data?.id }}
       </view>
     </view>
-    <button class="my-6 w-200px text-red" :disabled="!data" @click="reset">
+    <button type="default" size="mini" class="my-6 w-160px" @click="reset">
       重置数据
     </button>
   </view>

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

@@ -0,0 +1,28 @@
+<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>

+ 15 - 36
src/pages/about/components/request.vue

@@ -1,30 +1,18 @@
 <script lang="ts" setup>
-import type { IFooItem } from '@/service/index/foo'
-import { getFooAPI } from '@/service/index/foo'
-// import { findPetsByStatusQueryOptions } from '@/service/app'
-// import { useQuery } from '@tanstack/vue-query'
-
-const recommendUrl = ref('http://laf.run/signup?code=ohaOgIX')
+import type { IFooItem } from '@/api/foo'
+import { getFooAPI } from '@/api/foo'
 
 // const initialData = {
 //   name: 'initialData',
 //   id: '1234',
 // }
 const initialData = undefined
-// 适合少部分全局性的接口————多个页面都需要的请求接口,额外编写一个 Service 层
+
 const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
   immediate: true,
   initialData,
 })
 
-// 使用 vue-query 的 useQuery 来请求数据,只做参考,是否使用请根据实际情况而定
-// const {
-//   data: data2,
-//   error: error2,
-//   isLoading: isLoading2,
-//   refetch,
-// } = useQuery(findPetsByStatusQueryOptions({ params: { status: ['available'] } }))
-
 function reset() {
   data.value = initialData
 }
@@ -33,28 +21,17 @@ function reset() {
 <template>
   <view class="p-6 text-center">
     <view class="my-2">
-      使用的是 laf 云后台
-    </view>
-    <view class="text-green-400">
-      我的推荐码,可以获得佣金
+      pages 里面的 vue 文件会扫描成页面,将自动添加到 pages.json 里面。
     </view>
-
-    <!-- #ifdef H5 -->
-    <view class="my-2">
-      <a class="my-2" :href="recommendUrl" target="_blank">{{ recommendUrl }}</a>
+    <view class="my-2 text-green-400">
+      但是 pages/components 里面的 vue 不会。
     </view>
-    <!-- #endif -->
 
-    <!-- #ifndef H5 -->
-    <view class="my-2 text-left text-sm">
-      {{ recommendUrl }}
+    <view class="my-4 text-center">
+      <button type="primary" size="mini" class="w-160px" @click="run">
+        发送请求
+      </button>
     </view>
-    <!-- #endif -->
-
-    <!-- http://localhost:9000/#/pages/index/request -->
-    <wd-button class="my-6" @click="run">
-      发送请求
-    </wd-button>
     <view class="h-16">
       <view v-if="loading">
         loading...
@@ -68,8 +45,10 @@ function reset() {
         </view>
       </block>
     </view>
-    <wd-button type="error" class="my-6" :disabled="!data" @click="reset">
-      重置数据
-    </wd-button>
+    <view class="my-4 text-center">
+      <button type="warn" size="mini" class="w-160px" :disabled="!data" @click="reset">
+        重置数据
+      </button>
+    </view>
   </view>
 </template>

+ 0 - 38
src/pages/about/components/upload.vue

@@ -1,38 +0,0 @@
-<route lang="jsonc" type="page">
-{
-  "layout": "default",
-  "style": {
-    "navigationBarTitleText": "上传-状态一体化"
-  }
-}
-</route>
-
-<script lang="ts" setup>
-const { loading, data, run } = useUpload()
-</script>
-
-<template>
-  <view class="p-4 text-center">
-    <wd-button @click="run">
-      选择图片并上传
-    </wd-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 v-if="data" class="h-80 w-full">
-        <image :src="data.url" mode="scaleToFill" />
-      </view>
-    </template>
-  </view>
-</template>
-
-<style lang="scss" scoped>
-//
-</style>

+ 50 - 0
src/pages/about/vue-query.vue

@@ -0,0 +1,50 @@
+<script lang="ts" setup>
+import { useQuery } from '@tanstack/vue-query'
+import { foo } from '@/api/foo'
+import { getFooQueryOptions } from '@/api/foo-vue-query'
+
+definePage({
+  style: {
+    navigationBarTitleText: 'Vue Query 演示',
+  },
+})
+
+// 简单使用
+onShow(async () => {
+  const res = await foo()
+  console.log('res: ', res)
+})
+
+// vue-query 版
+const {
+  data,
+  error,
+  isLoading: loading,
+  refetch: send,
+} = useQuery(getFooQueryOptions('菲鸽-vue-query'))
+</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>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+//
+</style>

+ 64 - 41
src/pages/index/index.vue

@@ -1,54 +1,38 @@
-<!-- 使用 type="home" 属性设置首页,其他页面不需要设置,默认为page -->
-<route lang="jsonc" type="home">
-{
-  "layout": "tabbar",
-  "style": {
-    // 'custom' 表示开启自定义导航栏,默认 'default'
-    "navigationStyle": "custom",
-    "navigationBarTitleText": "首页"
-  }
-}
-</route>
-
 <script lang="ts" setup>
-import PLATFORM from '@/utils/platform'
+import { LOGIN_PAGE } from '@/router/config'
+import { useThemeStore } from '@/store'
+import { safeAreaInsets } from '@/utils/systemInfo'
 
 defineOptions({
   name: 'Home',
 })
+definePage({
+  // 使用 type: "home" 属性设置首页,其他页面不需要设置,默认为page
+  type: 'home',
+  style: {
+    // 'custom' 表示开启自定义导航栏,默认 'default'
+    navigationStyle: 'custom',
+    navigationBarTitleText: '首页',
+  },
+})
 
-// 获取屏幕边界到安全区域距离
-let safeAreaInsets
-let systemInfo
-
-// #ifdef MP-WEIXIN
-// 微信小程序使用新的API
-systemInfo = uni.getWindowInfo()
-safeAreaInsets = systemInfo.safeArea
-  ? {
-      top: systemInfo.safeArea.top,
-      right: systemInfo.windowWidth - systemInfo.safeArea.right,
-      bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom,
-      left: systemInfo.safeArea.left,
-    }
-  : null
-// #endif
+const themeStore = useThemeStore()
 
-// #ifndef MP-WEIXIN
-// 其他平台继续使用uni API
-systemInfo = uni.getSystemInfoSync()
-safeAreaInsets = systemInfo.safeAreaInsets
-// #endif
 const author = ref('菲鸽')
 const description = ref(
   'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite5 + UnoCss + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
 )
-// 测试 uni API 自动引入
+console.log('index/index 首页打印了')
+
 onLoad(() => {
-  console.log('项目作者:', author.value)
+  console.log('测试 uni API 自动引入: onLoad')
 })
 
-console.log('index')
+function toLogin() {
+  uni.navigateTo({
+    url: LOGIN_PAGE,
+  })
+}
 </script>
 
 <template>
@@ -78,12 +62,51 @@ console.log('index')
         https://unibest.tech
       </text>
     </view>
-    <view class="mt-6 h-1px bg-#eee" />
-    <view class="mt-8 text-center">
-      当前平台是:
+
+    <!-- #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">
-        {{ PLATFORM.platform }}
+        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>
+    <button class="mt-4 w-40 text-center" @click="toLogin">
+      点击去登录页
+    </button>
+    <view class="h-6" />
   </view>
 </template>

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

@@ -0,0 +1,76 @@
+<script lang="ts" setup>
+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()
+function doLogin() {
+  userStore.setUserInfo({
+    userId: 123456,
+    username: 'abc123456',
+    nickname: '菲鸽',
+    avatar: 'https://oss.laf.run/ukw0y1-site/avatar.jpg',
+  })
+  console.log(redirectUrl.value)
+  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 {
+    console.log('redirectTo:', path)
+    uni.redirectTo({
+      url: path,
+    })
+  }
+}
+</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>

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

@@ -0,0 +1,34 @@
+<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>

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

@@ -0,0 +1,212 @@
+<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>(
+  import.meta.env.VITE_UPLOAD_BASEURL,
+  {},
+  {
+    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 })
+  // #endif
+}
+
+// #ifdef MP-WEIXIN
+
+// 微信小程序下选择头像事件
+function onChooseAvatar(e: any) {
+  console.log('选择头像', e.detail)
+  const { avatarUrl } = e.detail
+  const { run } = useUpload<IUploadSuccessInfo>(
+    import.meta.env.VITE_UPLOAD_BASEURL,
+    {},
+    {
+      onSuccess: (res) => {
+        console.log('wx头像上传成功', res)
+        useUserStore().setUserAvatar(res.url)
+      },
+    },
+    avatarUrl,
+  )
+  run()
+}
+// #endif
+// #ifdef MP-WEIXIN
+// 微信小程序下设置用户名
+function getUserInfo(e: any) {
+  console.log(e.detail)
+}
+// #endif
+
+// 退出登录
+function handleLogout() {
+  uni.showModal({
+    title: '提示',
+    content: '确定要退出登录吗?',
+    success: (res) => {
+      if (res.confirm) {
+        // 清空用户信息
+        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.id }}
+        </view>
+      </view>
+    </view>
+
+    <view class="mt-3 break-all px-3">
+      {{ JSON.stringify(userInfo, null, 2) }}
+    </view>
+
+    <view class="mt-20 px-3">
+      <view class="m-auto w-160px text-center">
+        <button v-if="tokenStore.hasLogin" type="warn" class="w-full" @click="handleLogout">
+          退出登录
+        </button>
+        <button v-else type="primary" class="w-full" @click="handleLogin">
+          登录
+        </button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+/* 基础样式 */
+.profile-container {
+  overflow: hidden;
+  font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
+  // background-color: #f7f8fa;
+}
+/* 用户信息区域 */
+.user-info-section {
+  display: flex;
+  align-items: center;
+  padding: 40rpx;
+  margin: 30rpx 30rpx 20rpx;
+  background-color: #fff;
+  border-radius: 24rpx;
+  box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
+  transition: all 0.3s ease;
+}
+
+.avatar-wrapper {
+  width: 160rpx;
+  height: 160rpx;
+  margin-right: 40rpx;
+  overflow: hidden;
+  border: 4rpx solid #f5f5f5;
+  border-radius: 50%;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+}
+.avatar-button {
+  height: 160rpx;
+  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>

+ 37 - 0
src/router/README.md

@@ -0,0 +1,37 @@
+# 登录 说明
+
+## 登录 2种策略
+- 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
+- 默认需要登录策略: DEFAULT_NEED_LOGIN
+
+### 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
+进入任何页面都不需要登录,只有进入到黑名单中的页面/或者页面中某些动作需要登录,才需要登录。
+
+比如大部分2C的应用,美团、今日头条、抖音等,都可以直接浏览,只有点赞、评论、分享等操作或者去特殊页面(比如个人中心),才需要登录。
+
+### 默认需要登录策略: DEFAULT_NEED_LOGIN
+
+进入任何页面都需要登录,只有进入到白名单中的页面,才不需要登录。默认进入应用需要先去登录页。
+
+比如大部分2B和后台管理类的应用,比如企业微信、钉钉、飞书、内部报表系统、CMS系统等,都需要登录,只有登录后,才能使用。
+
+### EXCLUDE_PAGE_LIST
+`EXCLUDE_PAGE_LIST` 表示排除的路由列表。
+
+在 `默认无需登录策略: DEFAULT_NO_NEED_LOGIN` 中,只有路由在 `EXCLUDE_PAGE_LIST` 中,才需要登录,相当于黑名单。
+
+在 `默认需要登录策略: DEFAULT_NEED_LOGIN` 中,只有路由在 `EXCLUDE_PAGE_LIST` 中,才不需要登录,相当于白名单。
+
+
+## 登录注册页路由
+
+登录页 `login.vue` 对应路由是 `/pages/login/login`.
+注册页 `register.vue` 对应路由是 `/pages/login/register`.
+
+## 登录注册页适用性
+
+登录注册页主要适用于 `h5` 和 `App`,默认不适用于 `小程序`,因为 `小程序` 通常会使用平台提供的快捷登录。
+
+特殊情况例外,如业务需要跨平台复用登录注册页时,也可以用在 `小程序` 上,所以主要还是看业务需求。
+
+通过一个参数 `IS_USE_WX_LOGIN_IN_MP` 来控制是否在 `小程序` 中使用 `小程序` 默认的登录逻辑。

+ 21 - 0
src/router/config.ts

@@ -0,0 +1,21 @@
+export const LOGIN_STRATEGY_MAP = {
+  DEFAULT_NO_NEED_LOGIN: 0, // 黑名单策略,默认可以进入APP
+  DEFAULT_NEED_LOGIN: 1, // 白名单策略,默认不可以进入APP,需要强制登录
+}
+// 登录策略,默认使用`无需登录策略`,即默认不需要登录就可以访问
+export const LOGIN_STRATEGY = LOGIN_STRATEGY_MAP.DEFAULT_NO_NEED_LOGIN
+export const isNeedLoginMode = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN
+
+export const LOGIN_PAGE = '/pages/login/login'
+export const REGISTER_PAGE = '/pages/login/register'
+
+export const LOGIN_PAGE_LIST = [LOGIN_PAGE, REGISTER_PAGE]
+
+// 排除在外的列表,白名单策略指白名单列表,黑名单策略指黑名单列表
+export const EXCLUDE_PAGE_LIST = [
+  '/pages/xxx/index',
+]
+
+// 在微信小程序里面是否使用小程序默认的登录,默认为true
+// 如果为 false 则复用 h5 的登录逻辑
+export const IS_USE_WX_LOGIN_IN_MP = true // 暂时还没用到,没想好怎么整合

+ 64 - 34
src/router/interceptor.ts

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

+ 2 - 2
src/service/app/displayEnumLabel.ts

@@ -2,11 +2,11 @@
 // @ts-ignore
 import * as API from './types';
 
-export function displayStatusEnum(field: API.IStatusEnum) {
+export function displayStatusEnum(field: API.StatusEnum) {
   return { available: 'available', pending: 'pending', sold: 'sold' }[field];
 }
 
-export function displayStatusEnum2(field: API.IStatusEnum2) {
+export function displayStatusEnum2(field: API.StatusEnum2) {
   return { placed: 'placed', approved: 'approved', delivered: 'delivered' }[
     field
   ];

src/service/app/index.ts → src/service/index.ts


+ 0 - 28
src/service/index/foo.ts

@@ -1,28 +0,0 @@
-import { http } from '@/http/http'
-
-export interface IFooItem {
-  id: string
-  name: string
-}
-
-/** GET 请求 */
-export function getFooAPI(name: string) {
-  return http.get<IFooItem>('/foo', { name })
-}
-/** GET 请求;支持 传递 header 的范例 */
-export function getFooAPI2(name: string) {
-  return http.get<IFooItem>('/foo', { name }, { 'Content-Type-100': '100' })
-}
-
-/** POST 请求 */
-export function postFooAPI(name: string) {
-  return http.post<IFooItem>('/foo', { name })
-}
-/** POST 请求;需要传递 query 参数的范例;微信小程序经常有同时需要query参数和body参数的场景 */
-export function postFooAPI2(name: string) {
-  return http.post<IFooItem>('/foo', { name })
-}
-/** POST 请求;支持 传递 header 的范例 */
-export function postFooAPI3(name: string) {
-  return http.post<IFooItem>('/foo', { name }, { name }, { 'Content-Type-100': '100' })
-}

+ 18 - 26
src/service/app/pet.ts

@@ -1,12 +1,12 @@
 /* eslint-disable */
 // @ts-ignore
-import request from '@/utils/request';
-import { CustomRequestOptions } from '@/http/interceptor';
+import request from '@/http/vue-query';
+import type { CustomRequestOptions } from '@/http/types';
 
 import * as API from './types';
 
 /** Update an existing pet PUT /pet */
-export async function updatePet({
+export async function petUsingPut({
   body,
   options,
 }: {
@@ -24,7 +24,7 @@ export async function updatePet({
 }
 
 /** Add a new pet to the store POST /pet */
-export async function addPet({
+export async function petUsingPost({
   body,
   options,
 }: {
@@ -42,12 +42,12 @@ export async function addPet({
 }
 
 /** Find pet by ID Returns a single pet GET /pet/${param0} */
-export async function getPetById({
+export async function petPetIdUsingGet({
   params,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.getPetByIdParams;
+  params: API.petPetIdUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   const { petId: param0, ...queryParams } = params;
@@ -60,19 +60,14 @@ export async function getPetById({
 }
 
 /** Updates a pet in the store with form data POST /pet/${param0} */
-export async function updatePetWithForm({
+export async function petPetIdUsingPost({
   params,
   body,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.updatePetWithFormParams;
-  body: {
-    /** Updated name of the pet */
-    name?: string;
-    /** Updated status of the pet */
-    status?: string;
-  };
+  params: API.petPetIdUsingPostParams;
+  body: API.PetPetIdUsingPostBody;
   options?: CustomRequestOptions;
 }) {
   const { petId: param0, ...queryParams } = params;
@@ -89,12 +84,12 @@ export async function updatePetWithForm({
 }
 
 /** Deletes a pet DELETE /pet/${param0} */
-export async function deletePet({
+export async function petPetIdUsingDelete({
   params,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.deletePetParams;
+  params: API.petPetIdUsingDeleteParams;
   options?: CustomRequestOptions;
 }) {
   const { petId: param0, ...queryParams } = params;
@@ -107,18 +102,15 @@ export async function deletePet({
 }
 
 /** uploads an image POST /pet/${param0}/uploadImage */
-export async function uploadFile({
+export async function petPetIdUploadImageUsingPost({
   params,
   body,
   file,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.uploadFileParams;
-  body: {
-    /** Additional data to pass to server */
-    additionalMetadata?: string;
-  };
+  params: API.petPetIdUploadImageUsingPostParams;
+  body: API.PetPetIdUploadImageUsingPostBody;
   file?: File;
   options?: CustomRequestOptions;
 }) {
@@ -157,12 +149,12 @@ export async function uploadFile({
 }
 
 /** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */
-export async function findPetsByStatus({
+export async function petFindByStatusUsingGet({
   params,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.findPetsByStatusParams;
+  params: API.petFindByStatusUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   return request<API.Pet[]>('/pet/findByStatus', {
@@ -175,12 +167,12 @@ export async function findPetsByStatus({
 }
 
 /** Finds Pets by tags Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */
-export async function findPetsByTags({
+export async function petFindByTagsUsingGet({
   params,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.findPetsByTagsParams;
+  params: API.petFindByTagsUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   return request<API.Pet[]>('/pet/findByTags', {

+ 24 - 24
src/service/app/pet.vuequery.ts

@@ -2,21 +2,21 @@
 // @ts-ignore
 import { queryOptions, useMutation } from '@tanstack/vue-query';
 import type { DefaultError } from '@tanstack/vue-query';
-import request from '@/utils/request';
-import { CustomRequestOptions } from '@/http/interceptor';
+import request from '@/http/vue-query';
+import type { CustomRequestOptions } from '@/http/types';
 
 import * as apis from './pet';
 import * as API from './types';
 
 /** Update an existing pet PUT /pet */
-export function useUpdatePetMutation(options?: {
+export function usePetUsingPutMutation(options?: {
   onSuccess?: (value?: unknown) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.updatePet,
+    mutationFn: apis.petUsingPut,
     onSuccess(data: unknown) {
       onSuccess?.(data);
     },
@@ -29,14 +29,14 @@ export function useUpdatePetMutation(options?: {
 }
 
 /** Add a new pet to the store POST /pet */
-export function useAddPetMutation(options?: {
+export function usePetUsingPostMutation(options?: {
   onSuccess?: (value?: unknown) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.addPet,
+    mutationFn: apis.petUsingPost,
     onSuccess(data: unknown) {
       onSuccess?.(data);
     },
@@ -49,28 +49,28 @@ export function useAddPetMutation(options?: {
 }
 
 /** Find pet by ID Returns a single pet GET /pet/${param0} */
-export function getPetByIdQueryOptions(options: {
+export function petPetIdUsingGetQueryOptions(options: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.getPetByIdParams;
+  params: API.petPetIdUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   return queryOptions({
     queryFn: async ({ queryKey }) => {
-      return apis.getPetById(queryKey[1] as typeof options);
+      return apis.petPetIdUsingGet(queryKey[1] as typeof options);
     },
-    queryKey: ['getPetById', options],
+    queryKey: ['petPetIdUsingGet', options],
   });
 }
 
 /** Updates a pet in the store with form data POST /pet/${param0} */
-export function useUpdatePetWithFormMutation(options?: {
+export function usePetPetIdUsingPostMutation(options?: {
   onSuccess?: (value?: unknown) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.updatePetWithForm,
+    mutationFn: apis.petPetIdUsingPost,
     onSuccess(data: unknown) {
       onSuccess?.(data);
     },
@@ -83,14 +83,14 @@ export function useUpdatePetWithFormMutation(options?: {
 }
 
 /** Deletes a pet DELETE /pet/${param0} */
-export function useDeletePetMutation(options?: {
+export function usePetPetIdUsingDeleteMutation(options?: {
   onSuccess?: (value?: unknown) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.deletePet,
+    mutationFn: apis.petPetIdUsingDelete,
     onSuccess(data: unknown) {
       onSuccess?.(data);
     },
@@ -103,14 +103,14 @@ export function useDeletePetMutation(options?: {
 }
 
 /** uploads an image POST /pet/${param0}/uploadImage */
-export function useUploadFileMutation(options?: {
+export function usePetPetIdUploadImageUsingPostMutation(options?: {
   onSuccess?: (value?: API.ApiResponse) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.uploadFile,
+    mutationFn: apis.petPetIdUploadImageUsingPost,
     onSuccess(data: API.ApiResponse) {
       onSuccess?.(data);
     },
@@ -123,29 +123,29 @@ export function useUploadFileMutation(options?: {
 }
 
 /** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */
-export function findPetsByStatusQueryOptions(options: {
+export function petFindByStatusUsingGetQueryOptions(options: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.findPetsByStatusParams;
+  params: API.petFindByStatusUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   return queryOptions({
     queryFn: async ({ queryKey }) => {
-      return apis.findPetsByStatus(queryKey[1] as typeof options);
+      return apis.petFindByStatusUsingGet(queryKey[1] as typeof options);
     },
-    queryKey: ['findPetsByStatus', options],
+    queryKey: ['petFindByStatusUsingGet', options],
   });
 }
 
 /** Finds Pets by tags Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */
-export function findPetsByTagsQueryOptions(options: {
+export function petFindByTagsUsingGetQueryOptions(options: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.findPetsByTagsParams;
+  params: API.petFindByTagsUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   return queryOptions({
     queryFn: async ({ queryKey }) => {
-      return apis.findPetsByTags(queryKey[1] as typeof options);
+      return apis.petFindByTagsUsingGet(queryKey[1] as typeof options);
     },
-    queryKey: ['findPetsByTags', options],
+    queryKey: ['petFindByTagsUsingGet', options],
   });
 }

+ 9 - 9
src/service/app/store.ts

@@ -1,24 +1,24 @@
 /* eslint-disable */
 // @ts-ignore
-import request from '@/utils/request';
-import { CustomRequestOptions } from '@/http/interceptor';
+import request from '@/http/vue-query';
+import type { CustomRequestOptions } from '@/http/types';
 
 import * as API from './types';
 
 /** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */
-export async function getInventory({
+export async function storeInventoryUsingGet({
   options,
 }: {
   options?: CustomRequestOptions;
 }) {
-  return request<Record<string, unknown>>('/store/inventory', {
+  return request<Record<string, number>>('/store/inventory', {
     method: 'GET',
     ...(options || {}),
   });
 }
 
 /** Place an order for a pet POST /store/order */
-export async function placeOrder({
+export async function storeOrderUsingPost({
   body,
   options,
 }: {
@@ -36,12 +36,12 @@ export async function placeOrder({
 }
 
 /** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */
-export async function getOrderById({
+export async function storeOrderOrderIdUsingGet({
   params,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.getOrderByIdParams;
+  params: API.storeOrderOrderIdUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   const { orderId: param0, ...queryParams } = params;
@@ -54,12 +54,12 @@ export async function getOrderById({
 }
 
 /** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */
-export async function deleteOrder({
+export async function storeOrderOrderIdUsingDelete({
   params,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.deleteOrderParams;
+  params: API.storeOrderOrderIdUsingDeleteParams;
   options?: CustomRequestOptions;
 }) {
   const { orderId: param0, ...queryParams } = params;

+ 13 - 13
src/service/app/store.vuequery.ts

@@ -2,33 +2,33 @@
 // @ts-ignore
 import { queryOptions, useMutation } from '@tanstack/vue-query';
 import type { DefaultError } from '@tanstack/vue-query';
-import request from '@/utils/request';
-import { CustomRequestOptions } from '@/http/interceptor';
+import request from '@/http/vue-query';
+import type { CustomRequestOptions } from '@/http/types';
 
 import * as apis from './store';
 import * as API from './types';
 
 /** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */
-export function getInventoryQueryOptions(options: {
+export function storeInventoryUsingGetQueryOptions(options: {
   options?: CustomRequestOptions;
 }) {
   return queryOptions({
     queryFn: async ({ queryKey }) => {
-      return apis.getInventory(queryKey[1] as typeof options);
+      return apis.storeInventoryUsingGet(queryKey[1] as typeof options);
     },
-    queryKey: ['getInventory', options],
+    queryKey: ['storeInventoryUsingGet', options],
   });
 }
 
 /** Place an order for a pet POST /store/order */
-export function usePlaceOrderMutation(options?: {
+export function useStoreOrderUsingPostMutation(options?: {
   onSuccess?: (value?: API.Order) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.placeOrder,
+    mutationFn: apis.storeOrderUsingPost,
     onSuccess(data: API.Order) {
       onSuccess?.(data);
     },
@@ -41,28 +41,28 @@ export function usePlaceOrderMutation(options?: {
 }
 
 /** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */
-export function getOrderByIdQueryOptions(options: {
+export function storeOrderOrderIdUsingGetQueryOptions(options: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.getOrderByIdParams;
+  params: API.storeOrderOrderIdUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   return queryOptions({
     queryFn: async ({ queryKey }) => {
-      return apis.getOrderById(queryKey[1] as typeof options);
+      return apis.storeOrderOrderIdUsingGet(queryKey[1] as typeof options);
     },
-    queryKey: ['getOrderById', options],
+    queryKey: ['storeOrderOrderIdUsingGet', options],
   });
 }
 
 /** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */
-export function useDeleteOrderMutation(options?: {
+export function useStoreOrderOrderIdUsingDeleteMutation(options?: {
   onSuccess?: (value?: unknown) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.deleteOrder,
+    mutationFn: apis.storeOrderOrderIdUsingDelete,
     onSuccess(data: unknown) {
       onSuccess?.(data);
     },

+ 80 - 62
src/service/app/types.ts

@@ -12,107 +12,99 @@ export type Category = {
   name?: string;
 };
 
-export type deleteOrderParams = {
-  /** ID of the order that needs to be deleted */
-  orderId: number;
-};
-
-export type deletePetParams = {
-  /** Pet id to delete */
-  petId: number;
+export type Order = {
+  id?: number;
+  petId?: number;
+  quantity?: number;
+  shipDate?: string;
+  /** Order Status */
+  status?: 'placed' | 'approved' | 'delivered';
+  complete?: boolean;
 };
 
-export type deleteUserParams = {
-  /** The name that needs to be deleted */
-  username: string;
+export type Pet = {
+  id?: number;
+  category?: Category;
+  name: string;
+  photoUrls: string[];
+  tags?: Tag[];
+  /** pet status in the store */
+  status?: 'available' | 'pending' | 'sold';
 };
 
-export type findPetsByStatusParams = {
+export type petFindByStatusUsingGetParams = {
   /** Status values that need to be considered for filter */
   status: ('available' | 'pending' | 'sold')[];
 };
 
-export type findPetsByTagsParams = {
+export type petFindByTagsUsingGetParams = {
   /** Tags to filter by */
   tags: string[];
 };
 
-export type getOrderByIdParams = {
-  /** ID of pet that needs to be fetched */
-  orderId: number;
+export type PetPetIdUploadImageUsingPostBody = {
+  /** Additional data to pass to server */
+  additionalMetadata?: string;
+  /** file to upload */
+  file?: string;
 };
 
-export type getPetByIdParams = {
-  /** ID of pet to return */
+export type petPetIdUploadImageUsingPostParams = {
+  /** ID of pet to update */
   petId: number;
 };
 
-export type getUserByNameParams = {
-  /** The name that needs to be fetched. Use user1 for testing.  */
-  username: string;
+export type petPetIdUsingDeleteParams = {
+  /** Pet id to delete */
+  petId: number;
 };
 
-export type loginUserParams = {
-  /** The user name for login */
-  username: string;
-  /** The password for login in clear text */
-  password: string;
+export type petPetIdUsingGetParams = {
+  /** ID of pet to return */
+  petId: number;
 };
 
-export type Order = {
-  id?: number;
-  petId?: number;
-  quantity?: number;
-  shipDate?: string;
-  /** Order Status */
-  status?: 'placed' | 'approved' | 'delivered';
-  complete?: boolean;
+export type PetPetIdUsingPostBody = {
+  /** Updated name of the pet */
+  name?: string;
+  /** Updated status of the pet */
+  status?: string;
 };
 
-export type Pet = {
-  id?: number;
-  category?: Category;
-  name: string;
-  photoUrls: string[];
-  tags?: Tag[];
-  /** pet status in the store */
-  status?: 'available' | 'pending' | 'sold';
+export type petPetIdUsingPostParams = {
+  /** ID of pet that needs to be updated */
+  petId: number;
 };
 
 export enum StatusEnum {
-  available = 'available',
-  pending = 'pending',
-  sold = 'sold',
+  'available' = 'available',
+  'pending' = 'pending',
+  'sold' = 'sold',
 }
 
 export type IStatusEnum = keyof typeof StatusEnum;
 
 export enum StatusEnum2 {
-  placed = 'placed',
-  approved = 'approved',
-  delivered = 'delivered',
+  'placed' = 'placed',
+  'approved' = 'approved',
+  'delivered' = 'delivered',
 }
 
 export type IStatusEnum2 = keyof typeof StatusEnum2;
 
-export type Tag = {
-  id?: number;
-  name?: string;
-};
-
-export type updatePetWithFormParams = {
-  /** ID of pet that needs to be updated */
-  petId: number;
+export type storeOrderOrderIdUsingDeleteParams = {
+  /** ID of the order that needs to be deleted */
+  orderId: number;
 };
 
-export type updateUserParams = {
-  /** name that need to be updated */
-  username: string;
+export type storeOrderOrderIdUsingGetParams = {
+  /** ID of pet that needs to be fetched */
+  orderId: number;
 };
 
-export type uploadFileParams = {
-  /** ID of pet to update */
-  petId: number;
+export type Tag = {
+  id?: number;
+  name?: string;
 };
 
 export type User = {
@@ -126,3 +118,29 @@ export type User = {
   /** User Status */
   userStatus?: number;
 };
+
+export type UserCreateWithArrayUsingPostBody = User[];
+
+export type UserCreateWithListUsingPostBody = User[];
+
+export type userLoginUsingGetParams = {
+  /** The user name for login */
+  username: string;
+  /** The password for login in clear text */
+  password: string;
+};
+
+export type userUsernameUsingDeleteParams = {
+  /** The name that needs to be deleted */
+  username: string;
+};
+
+export type userUsernameUsingGetParams = {
+  /** The name that needs to be fetched. Use user1 for testing.  */
+  username: string;
+};
+
+export type userUsernameUsingPutParams = {
+  /** name that need to be updated */
+  username: string;
+};

+ 16 - 16
src/service/app/user.ts

@@ -1,12 +1,12 @@
 /* eslint-disable */
 // @ts-ignore
-import request from '@/utils/request';
-import { CustomRequestOptions } from '@/http/interceptor';
+import request from '@/http/vue-query';
+import type { CustomRequestOptions } from '@/http/types';
 
 import * as API from './types';
 
 /** Create user This can only be done by the logged in user. 返回值: successful operation POST /user */
-export async function createUser({
+export async function userUsingPost({
   body,
   options,
 }: {
@@ -24,12 +24,12 @@ export async function createUser({
 }
 
 /** Get user by user name GET /user/${param0} */
-export async function getUserByName({
+export async function userUsernameUsingGet({
   params,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.getUserByNameParams;
+  params: API.userUsernameUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   const { username: param0, ...queryParams } = params;
@@ -42,13 +42,13 @@ export async function getUserByName({
 }
 
 /** Updated user This can only be done by the logged in user. PUT /user/${param0} */
-export async function updateUser({
+export async function userUsernameUsingPut({
   params,
   body,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.updateUserParams;
+  params: API.userUsernameUsingPutParams;
   body: API.User;
   options?: CustomRequestOptions;
 }) {
@@ -66,12 +66,12 @@ export async function updateUser({
 }
 
 /** Delete user This can only be done by the logged in user. DELETE /user/${param0} */
-export async function deleteUser({
+export async function userUsernameUsingDelete({
   params,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.deleteUserParams;
+  params: API.userUsernameUsingDeleteParams;
   options?: CustomRequestOptions;
 }) {
   const { username: param0, ...queryParams } = params;
@@ -84,11 +84,11 @@ export async function deleteUser({
 }
 
 /** Creates list of users with given input array 返回值: successful operation POST /user/createWithArray */
-export async function createUsersWithArrayInput({
+export async function userCreateWithArrayUsingPost({
   body,
   options,
 }: {
-  body: API.User[];
+  body: API.UserCreateWithArrayUsingPostBody;
   options?: CustomRequestOptions;
 }) {
   return request<unknown>('/user/createWithArray', {
@@ -102,11 +102,11 @@ export async function createUsersWithArrayInput({
 }
 
 /** Creates list of users with given input array 返回值: successful operation POST /user/createWithList */
-export async function createUsersWithListInput({
+export async function userCreateWithListUsingPost({
   body,
   options,
 }: {
-  body: API.User[];
+  body: API.UserCreateWithListUsingPostBody;
   options?: CustomRequestOptions;
 }) {
   return request<unknown>('/user/createWithList', {
@@ -120,12 +120,12 @@ export async function createUsersWithListInput({
 }
 
 /** Logs user into the system GET /user/login */
-export async function loginUser({
+export async function userLoginUsingGet({
   params,
   options,
 }: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.loginUserParams;
+  params: API.userLoginUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   return request<string>('/user/login', {
@@ -138,7 +138,7 @@ export async function loginUser({
 }
 
 /** Logs out current logged in user session 返回值: successful operation GET /user/logout */
-export async function logoutUser({
+export async function userLogoutUsingGet({
   options,
 }: {
   options?: CustomRequestOptions;

+ 23 - 23
src/service/app/user.vuequery.ts

@@ -2,21 +2,21 @@
 // @ts-ignore
 import { queryOptions, useMutation } from '@tanstack/vue-query';
 import type { DefaultError } from '@tanstack/vue-query';
-import request from '@/utils/request';
-import { CustomRequestOptions } from '@/http/interceptor';
+import request from '@/http/vue-query';
+import type { CustomRequestOptions } from '@/http/types';
 
 import * as apis from './user';
 import * as API from './types';
 
 /** Create user This can only be done by the logged in user. 返回值: successful operation POST /user */
-export function useCreateUserMutation(options?: {
+export function useUserUsingPostMutation(options?: {
   onSuccess?: (value?: unknown) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.createUser,
+    mutationFn: apis.userUsingPost,
     onSuccess(data: unknown) {
       onSuccess?.(data);
     },
@@ -29,28 +29,28 @@ export function useCreateUserMutation(options?: {
 }
 
 /** Get user by user name GET /user/${param0} */
-export function getUserByNameQueryOptions(options: {
+export function userUsernameUsingGetQueryOptions(options: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.getUserByNameParams;
+  params: API.userUsernameUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   return queryOptions({
     queryFn: async ({ queryKey }) => {
-      return apis.getUserByName(queryKey[1] as typeof options);
+      return apis.userUsernameUsingGet(queryKey[1] as typeof options);
     },
-    queryKey: ['getUserByName', options],
+    queryKey: ['userUsernameUsingGet', options],
   });
 }
 
 /** Updated user This can only be done by the logged in user. PUT /user/${param0} */
-export function useUpdateUserMutation(options?: {
+export function useUserUsernameUsingPutMutation(options?: {
   onSuccess?: (value?: unknown) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.updateUser,
+    mutationFn: apis.userUsernameUsingPut,
     onSuccess(data: unknown) {
       onSuccess?.(data);
     },
@@ -63,14 +63,14 @@ export function useUpdateUserMutation(options?: {
 }
 
 /** Delete user This can only be done by the logged in user. DELETE /user/${param0} */
-export function useDeleteUserMutation(options?: {
+export function useUserUsernameUsingDeleteMutation(options?: {
   onSuccess?: (value?: unknown) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.deleteUser,
+    mutationFn: apis.userUsernameUsingDelete,
     onSuccess(data: unknown) {
       onSuccess?.(data);
     },
@@ -83,14 +83,14 @@ export function useDeleteUserMutation(options?: {
 }
 
 /** Creates list of users with given input array 返回值: successful operation POST /user/createWithArray */
-export function useCreateUsersWithArrayInputMutation(options?: {
+export function useUserCreateWithArrayUsingPostMutation(options?: {
   onSuccess?: (value?: unknown) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.createUsersWithArrayInput,
+    mutationFn: apis.userCreateWithArrayUsingPost,
     onSuccess(data: unknown) {
       onSuccess?.(data);
     },
@@ -103,14 +103,14 @@ export function useCreateUsersWithArrayInputMutation(options?: {
 }
 
 /** Creates list of users with given input array 返回值: successful operation POST /user/createWithList */
-export function useCreateUsersWithListInputMutation(options?: {
+export function useUserCreateWithListUsingPostMutation(options?: {
   onSuccess?: (value?: unknown) => void;
   onError?: (error?: DefaultError) => void;
 }) {
   const { onSuccess, onError } = options || {};
 
   const response = useMutation({
-    mutationFn: apis.createUsersWithListInput,
+    mutationFn: apis.userCreateWithListUsingPost,
     onSuccess(data: unknown) {
       onSuccess?.(data);
     },
@@ -123,27 +123,27 @@ export function useCreateUsersWithListInputMutation(options?: {
 }
 
 /** Logs user into the system GET /user/login */
-export function loginUserQueryOptions(options: {
+export function userLoginUsingGetQueryOptions(options: {
   // 叠加生成的Param类型 (非body参数openapi默认没有生成对象)
-  params: API.loginUserParams;
+  params: API.userLoginUsingGetParams;
   options?: CustomRequestOptions;
 }) {
   return queryOptions({
     queryFn: async ({ queryKey }) => {
-      return apis.loginUser(queryKey[1] as typeof options);
+      return apis.userLoginUsingGet(queryKey[1] as typeof options);
     },
-    queryKey: ['loginUser', options],
+    queryKey: ['userLoginUsingGet', options],
   });
 }
 
 /** Logs out current logged in user session 返回值: successful operation GET /user/logout */
-export function logoutUserQueryOptions(options: {
+export function userLogoutUsingGetQueryOptions(options: {
   options?: CustomRequestOptions;
 }) {
   return queryOptions({
     queryFn: async ({ queryKey }) => {
-      return apis.logoutUser(queryKey[1] as typeof options);
+      return apis.userLogoutUsingGet(queryKey[1] as typeof options);
     },
-    queryKey: ['logoutUser', options],
+    queryKey: ['userLogoutUsingGet', options],
   });
 }

+ 0 - 0
src/static/images/.gitkeep


BIN
src/static/tabbar/scan.png


+ 1 - 0
src/store/index.ts

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

+ 42 - 0
src/store/theme.ts

@@ -0,0 +1,42 @@
+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,
+  },
+)

+ 277 - 0
src/store/token.ts

@@ -0,0 +1,277 @@
+import type { IAuthLoginRes } from '@/api/types/login'
+import { defineStore } from 'pinia'
+import { computed, ref } from 'vue' // 修复:导入 computed
+import {
+  login as _login,
+  logout as _logout,
+  refreshToken as _refreshToken,
+  wxLogin as _wxLogin,
+  getWxCode,
+} from '@/api/login'
+import { isDoubleTokenRes, isSingleTokenRes } from '@/api/types/login'
+import { isDoubleTokenMode } from '@/utils'
+import { useUserStore } from './user'
+
+// 修复:添加 isSingleTokenMode 变量
+export const isSingleTokenMode = !isDoubleTokenMode
+
+// 初始化状态
+const tokenInfoState = isDoubleTokenMode
+  ? {
+      accessToken: '',
+      accessExpiresIn: 0,
+      refreshToken: '',
+      refreshExpiresIn: 0,
+    }
+  : {
+      token: '',
+      expiresIn: 0,
+    }
+
+export const useTokenStore = defineStore(
+  'token',
+  () => {
+    // 定义用户信息
+    const tokenInfo = ref<IAuthLoginRes>({ ...tokenInfoState })
+    // 设置用户信息
+    const setTokenInfo = (val: IAuthLoginRes) => {
+      tokenInfo.value = val
+
+      // 计算并存储过期时间
+      const now = Date.now()
+      if (isSingleTokenRes(val)) {
+        // 单token模式
+        const expireTime = now + val.expiresIn * 1000
+        uni.setStorageSync('accessTokenExpireTime', expireTime)
+      }
+      else if (isDoubleTokenRes(val)) {
+        // 双token模式
+        const accessExpireTime = now + val.accessExpiresIn * 1000
+        const refreshExpireTime = now + val.refreshExpiresIn * 1000
+        uni.setStorageSync('accessTokenExpireTime', accessExpireTime)
+        uni.setStorageSync('refreshTokenExpireTime', refreshExpireTime)
+      }
+    }
+
+    /**
+     * 判断token是否过期
+     */
+    const isTokenExpired = computed(() => {
+      const now = Date.now()
+      const expireTime = uni.getStorageSync('accessTokenExpireTime')
+
+      if (!expireTime)
+        return true
+      return now >= expireTime
+    })
+
+    /**
+     * 判断refreshToken是否过期
+     */
+    const isRefreshTokenExpired = computed(() => {
+      if (!isDoubleTokenMode)
+        return true
+
+      const now = Date.now()
+      const refreshExpireTime = uni.getStorageSync('refreshTokenExpireTime')
+
+      if (!refreshExpireTime)
+        return true
+      return now >= refreshExpireTime
+    })
+
+    /**
+     * 登录成功后处理逻辑
+     * @param tokenInfo 登录返回的token信息
+     */
+    async function _postLogin(tokenInfo: IAuthLoginRes) {
+      setTokenInfo(tokenInfo)
+      const userStore = useUserStore()
+      await userStore.fetchUserInfo()
+    }
+
+    /**
+     * 用户登录
+     * @param credentials 登录参数
+     * @returns 登录结果
+     */
+    const login = async (credentials: {
+      username: string
+      password: string
+      code: string
+      uuid: string
+    }) => {
+      try {
+        const res = await _login(credentials)
+        console.log('普通登录-res: ', res)
+        await _postLogin(res.data)
+        uni.showToast({
+          title: '登录成功',
+          icon: 'success',
+        })
+        return res
+      }
+      catch (error) {
+        console.error('登录失败:', error)
+        uni.showToast({
+          title: '登录失败,请重试',
+          icon: 'error',
+        })
+        throw error
+      }
+    }
+
+    /**
+     * 微信登录
+     * @returns 登录结果
+     */
+    const wxLogin = async () => {
+      try {
+        // 获取微信小程序登录的code
+        const code = await getWxCode()
+        console.log('微信登录-code: ', code)
+        const res = await _wxLogin(code)
+        console.log('微信登录-res: ', res)
+        await _postLogin(res.data)
+        uni.showToast({
+          title: '登录成功',
+          icon: 'success',
+        })
+        return res
+      }
+      catch (error) {
+        console.error('微信登录失败:', error)
+        uni.showToast({
+          title: '微信登录失败,请重试',
+          icon: 'error',
+        })
+        throw error
+      }
+    }
+
+    /**
+     * 退出登录 并 删除用户信息
+     */
+    const logout = async () => {
+      try {
+        await _logout()
+        // 清除存储的过期时间
+        uni.removeStorageSync('accessTokenExpireTime')
+        uni.removeStorageSync('refreshTokenExpireTime')
+      }
+      catch (error) {
+        console.error('退出登录失败:', error)
+      }
+      finally {
+        // 无论成功失败,都需要清除本地token信息
+        const userStore = useUserStore()
+        await userStore.removeUserInfo()
+      }
+    }
+
+    /**
+     * 刷新token
+     * @returns 刷新结果
+     */
+    const refreshToken = async () => {
+      if (!isDoubleTokenMode) {
+        console.error('单token模式不支持刷新token')
+        throw new Error('单token模式不支持刷新token')
+      }
+
+      try {
+        // 安全检查,确保refreshToken存在
+        if (!isDoubleTokenRes(tokenInfo.value) || !tokenInfo.value.refreshToken) {
+          throw new Error('无效的refreshToken')
+        }
+
+        const refreshToken = tokenInfo.value.refreshToken
+        const res = await _refreshToken(refreshToken)
+        console.log('刷新token-res: ', res)
+        setTokenInfo(res.data)
+        return res
+      }
+      catch (error) {
+        console.error('刷新token失败:', error)
+        throw error
+      }
+    }
+
+    /**
+     * 获取有效的token
+     * 注意:在computed中不直接调用异步函数,只做状态判断
+     * 实际的刷新操作应由调用方处理
+     */
+    const getValidToken = computed(() => {
+      // token已过期,返回空
+      if (isTokenExpired.value) {
+        return ''
+      }
+
+      if (isSingleTokenMode) {
+        return isSingleTokenRes(tokenInfo.value) ? tokenInfo.value.token : ''
+      }
+      else {
+        return isDoubleTokenRes(tokenInfo.value) ? tokenInfo.value.accessToken : ''
+      }
+    })
+
+    /**
+     * 检查是否有登录信息(不考虑token是否过期)
+     */
+    const hasLoginInfo = computed(() => {
+      if (isDoubleTokenMode) {
+        return isDoubleTokenRes(tokenInfo.value) && !!tokenInfo.value.accessToken
+      }
+      else {
+        return isSingleTokenRes(tokenInfo.value) && !!tokenInfo.value.token
+      }
+    })
+
+    /**
+     * 检查是否已登录且token有效
+     */
+    const hasValidLogin = computed(() => {
+      return hasLoginInfo.value && !isTokenExpired.value
+    })
+
+    /**
+     * 尝试获取有效的token,如果过期且可刷新,则刷新token
+     * @returns 有效的token或空字符串
+     */
+    const tryGetValidToken = async (): Promise<string> => {
+      if (!getValidToken.value && isDoubleTokenMode && !isRefreshTokenExpired.value) {
+        try {
+          await refreshToken()
+          return getValidToken.value
+        }
+        catch (error) {
+          console.error('尝试刷新token失败:', error)
+          return ''
+        }
+      }
+      return getValidToken.value
+    }
+
+    return {
+      // 核心API方法
+      login,
+      wxLogin,
+      logout,
+
+      // 认证状态判断(最常用的)
+      hasLogin: hasValidLogin,
+
+      // 内部系统使用的方法
+      refreshToken,
+      tryGetValidToken,
+
+      // 调试或特殊场景可能需要直接访问的信息
+      tokenInfo,
+    }
+  },
+  {
+    // 添加持久化配置,确保刷新页面后token信息不丢失
+    persist: true,
+  },
+)

+ 15 - 65
src/store/user.ts

@@ -1,38 +1,30 @@
-import type { IUserInfoVo } from '@/api/types/login'
+import type { IUserInfoRes } from '@/api/types/login'
 import { defineStore } from 'pinia'
 import { ref } from 'vue'
 import {
-  getUserInfo as _getUserInfo,
-  login as _login,
-  logout as _logout,
-  wxLogin as _wxLogin,
-  getWxCode,
+  getUserInfo,
 } from '@/api/login'
-import { toast } from '@/utils/toast'
 
 // 初始化状态
-const userInfoState: IUserInfoVo = {
-  id: 0,
+const userInfoState: IUserInfoRes = {
+  userId: 0,
   username: '',
+  nickname: '',
   avatar: '/static/images/default-avatar.png',
-  token: '',
 }
 
 export const useUserStore = defineStore(
   'user',
   () => {
     // 定义用户信息
-    const userInfo = ref<IUserInfoVo>({ ...userInfoState })
+    const userInfo = ref<IUserInfoRes>({ ...userInfoState })
     // 设置用户信息
-    const setUserInfo = (val: IUserInfoVo) => {
+    const setUserInfo = (val: IUserInfoRes) => {
       console.log('设置用户信息', val)
       // 若头像为空 则使用默认头像
       if (!val.avatar) {
         val.avatar = userInfoState.avatar
       }
-      else {
-        val.avatar = 'https://oss.laf.run/ukw0y1-site/avatar.jpg?feige'
-      }
       userInfo.value = val
     }
     const setUserAvatar = (avatar: string) => {
@@ -43,66 +35,24 @@ export const useUserStore = defineStore(
     // 删除用户信息
     const removeUserInfo = () => {
       userInfo.value = { ...userInfoState }
-      uni.removeStorageSync('userInfo')
-      uni.removeStorageSync('token')
-    }
-    /**
-     * 获取用户信息
-     */
-    const getUserInfo = async () => {
-      const res = await _getUserInfo()
-      const userInfo = res.data
-      setUserInfo(userInfo)
-      uni.setStorageSync('userInfo', userInfo)
-      uni.setStorageSync('token', userInfo.token)
-      // TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由
-      return res
-    }
-    /**
-     * 用户登录
-     * @param credentials 登录参数
-     * @returns R<IUserLogin>
-     */
-    const login = async (credentials: {
-      username: string
-      password: string
-      code: string
-      uuid: string
-    }) => {
-      const res = await _login(credentials)
-      console.log('登录信息', res)
-      toast.success('登录成功')
-      await getUserInfo()
-      return res
+      uni.removeStorageSync('user')
     }
 
     /**
-     * 退出登录 并 删除用户信息
-     */
-    const logout = async () => {
-      _logout()
-      removeUserInfo()
-    }
-    /**
-     * 微信登录
+     * 获取用户信息
      */
-    const wxLogin = async () => {
-      // 获取微信小程序登录的code
-      const data = await getWxCode()
-      console.log('微信登录code', data)
-
-      const res = await _wxLogin(data)
-      await getUserInfo()
+    const fetchUserInfo = async () => {
+      const res = await getUserInfo()
+      setUserInfo(res.data)
       return res
     }
 
     return {
       userInfo,
-      login,
-      wxLogin,
-      getUserInfo,
+      removeUserInfo,
+      fetchUserInfo,
+      setUserInfo,
       setUserAvatar,
-      logout,
     }
   },
   {

+ 17 - 0
src/style/index.scss

@@ -17,3 +17,20 @@ page {
   // 修改按钮背景色
   // --wot-button-primary-bg-color: green;
 }
+
+/*
+border-t-1
+由于uniapp中无法使用*选择器,使用魔法代替*,加上此规则可以简化border与divide的使用,并提升布局的兼容性
+1. 防止padding和border影响元素宽度。 (https://github.com/mozdevs/cssremedy/issues/4)
+2. 允许仅通过添加边框宽度来向元素添加边框。 (https://github.com/tailwindcss/tailwindcss/pull/116)
+3. [UnoCSS]: 允许使用css变量'--un-default-border-color'覆盖默认边框颜色
+*/
+// 这个样式有重大BUG,先去掉!!(2025-08-15)
+// :not(not),
+// ::before,
+// ::after {
+//   box-sizing: border-box; /* 1 */
+//   border-width: 0; /* 2 */
+//   border-style: solid; /* 2 */
+//   border-color: var(--un-default-border-color, #e5e7eb); /* 3 */
+// }

+ 78 - 0
src/tabbar/README.md

@@ -0,0 +1,78 @@
+# tabbar 说明
+
+## tabbar 4种策略
+
+`tabbar` 分为 `4 种` 情况:
+
+- 0 `无 tabbar`,只有一个页面入口,底部无 `tabbar` 显示;常用语临时活动页。
+- 1 `原生 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。
+  - 优势:原生自带的 tabbar,最先渲染,有缓存。
+  - 劣势:只能使用 2 组图片来切换选中和非选中状态,修改颜色只能重新换图片(或者用 iconfont)。
+- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。
+  - 优势:可以随意配置自己想要的 `svg icon`,切换字体颜色方便。有缓存。可以实现各种花里胡哨的动效等。
+  - 劣势:首次点击 tababr 会闪烁。
+- 3 `无缓存自定义 tabbar`,使用 `navigateTo` 切换 `tabbar`,`tabbar` 页面无缓存。使用了第三方 UI 库的 `tabbar` 组件。
+  - 优势:可以随意配置自己想要的 svg icon,切换字体颜色方便。可以实现各种花里胡哨的动效等。
+  - 劣势:首次点击 `tababr` 会闪烁,无缓存。
+
+
+> 注意:花里胡哨的效果需要自己实现,本模版不提供。
+
+## tabbar 配置说明
+
+- 如果使用的是 `原生tabbar`,需要配置 `nativeTabbarList`,每个 `item` 需要配置 `path`、`text`、`iconPath`、`selectedIconPath` 等属性。
+- 如果使用的是  `自定义tabbar`,需要配置 `customTabbarList`,每个 `item` 需要配置 `path`、`text`、`icon` 、`iconType` 等属性(如果是 `image` 图片还需要配置2种图片)。
+
+## 文件说明
+
+`config.ts` 专门配置 `nativeTabbarList` 和 `customTabbarList` 的相关信息,请按照文件里面的注释配置相关项。
+
+使用 `原生tabbar` 时,不需要关心下面2个文件:
+- `store.ts` ,专门给 `自定义 tabbar` 提供状态管理,代码几乎不需要修改。
+- `index.vue` ,专门给 `自定义 tabbar` 提供渲染逻辑,代码可以稍微修改,以符合自己的需求。
+
+## 自定义tabbar的不同类型的配置
+
+- uniUi 图标
+
+ ```js
+  {
+    // ... 其他配置
+    "iconType": "uniUi",
+    "icon": "home",
+  }
+  ```
+- unocss 图标
+
+ ```js
+  {
+    // ... 其他配置
+    // 注意 unocss 图标需要如下处理:(二选一)
+    // 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行)
+    // 2)配置到 unocss.config.ts 的 safelist 中
+    iconType: 'unocss',
+    icon: 'i-carbon-code',
+  }
+  ```
+- iconfont 图标
+
+ ```js
+  {
+    // ... 其他配置
+    // 注意 iconfont 图标需要额外加上 'iconfont',如下
+    iconType: 'iconfont',
+    icon: 'iconfont icon-my',
+  }
+  ```
+- image 本地图片
+
+ ```js
+  {
+    // ... 其他配置
+    // 使用 ‘image’时,需要配置 icon + iconActive 2张图片(不推荐)
+    // 既然已经用了自定义tabbar了,就不建议用图片了,所以不推荐
+    iconType: 'image',
+    icon: '/static/tabbar/home.png',
+    iconActive: '/static/tabbar/homeHL.png',
+  }
+  ```

+ 156 - 0
src/tabbar/config.ts

@@ -0,0 +1,156 @@
+import type { TabBar } from '@uni-helper/vite-plugin-uni-pages'
+
+/**
+ * tabbar 选择的策略,更详细的介绍见 tabbar.md 文件
+ * 0: 'NO_TABBAR' `无 tabbar`
+ * 1: 'NATIVE_TABBAR'  `完全原生 tabbar`
+ * 2: 'CUSTOM_TABBAR_WITH_CACHE' `有缓存自定义 tabbar`
+ * 3: 'CUSTOM_TABBAR_WITHOUT_CACHE' `无缓存自定义 tabbar`
+ *
+ * 温馨提示:本文件的任何代码更改了之后,都需要重新运行,否则 pages.json 不会更新导致配置不生效
+ */
+export const TABBAR_STRATEGY_MAP = {
+  NO_TABBAR: 0,
+  NATIVE_TABBAR: 1,
+  CUSTOM_TABBAR_WITH_CACHE: 2,
+  CUSTOM_TABBAR_WITHOUT_CACHE: 3,
+}
+
+// TODO: 1/3. 通过这里切换使用tabbar的策略
+// 如果是使用 NO_TABBAR(0),nativeTabbarList 和 customTabbarList 都不生效(里面的配置不用管)
+// 如果是使用 NATIVE_TABBAR(1),只需要配置 nativeTabbarList,customTabbarList 不生效
+// 如果是使用 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[] = [
+  {
+    iconPath: 'static/tabbar/home.png',
+    selectedIconPath: 'static/tabbar/homeHL.png',
+    pagePath: 'pages/index/index',
+    text: '首页',
+  },
+  {
+    iconPath: 'static/tabbar/example.png',
+    selectedIconPath: 'static/tabbar/exampleHL.png',
+    pagePath: 'pages/about/about',
+    text: '关于',
+  },
+  {
+    iconPath: 'static/tabbar/personal.png',
+    selectedIconPath: 'static/tabbar/personalHL.png',
+    pagePath: 'pages/me/me',
+    text: '个人',
+  },
+]
+
+// badge 显示一个数字或 小红点(样式可以直接在 tabbar/index.vue 里面修改)
+export type CustomTabBarItemBadge = number | 'dot'
+
+export interface CustomTabBarItem {
+  text: string
+  pagePath: string
+  iconType: 'uniUi' | '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[] = [
+  {
+    text: '首页',
+    pagePath: 'pages/index/index',
+    // 本框架内置了 uniapp 官方UI库 (uni-ui)的图标库
+    // 使用方式如:<uni-icons type="home" size="30"/>
+    // 图标列表地址:https://uniapp.dcloud.net.cn/component/uniui/uni-icons.html
+    iconType: 'uniUi',
+    icon: 'home',
+    // badge: 'dot',
+  },
+  {
+    text: '关于',
+    pagePath: 'pages/about/about',
+    // 注意 unocss 图标需要如下处理:(二选一)
+    // 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行)
+    // 2)配置到 unocss.config.ts 的 safelist 中
+    iconType: 'unocss',
+    icon: 'i-carbon-code',
+    // badge: 10,
+  },
+  {
+    pagePath: 'pages/me/me',
+    text: '我的',
+    iconType: 'uniUi',
+    icon: 'contact',
+    // badge: 100,
+  },
+  // 其他类型演示
+  // 1、uiLib
+  // {
+  //   pagePath: 'pages/index/index',
+  //   text: '首页',
+  //   iconType: 'uiLib',
+  //   icon: 'home',
+  // },
+  // 2、iconfont
+  // {
+  //   pagePath: 'pages/index/index',
+  //   text: '首页',
+  //   // 注意 iconfont 图标需要额外加上 'iconfont',如下
+  //   iconType: 'iconfont',
+  //   icon: 'iconfont icon-my',
+  // },
+  // 3、image
+  // {
+  //   pagePath: 'pages/index/index',
+  //   text: '首页',
+  //   // 使用 ‘image’时,需要配置 icon + iconActive 2张图片
+  //   iconType: 'image',
+  //   icon: '/static/tabbar/home.png',
+  //   iconActive: '/static/tabbar/homeHL.png',
+  // },
+]
+
+/**
+ * 是否启用 tabbar 缓存
+ * NATIVE_TABBAR(1) 和 CUSTOM_TABBAR_WITH_CACHE(2) 时,需要tabbar缓存
+ */
+export const tabbarCacheEnable
+  = [TABBAR_STRATEGY_MAP.NATIVE_TABBAR, TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE].includes(selectedTabbarStrategy)
+
+/**
+ * 是否启用自定义 tabbar
+ * CUSTOM_TABBAR(2,3) 时,启用自定义tabbar
+ */
+export const customTabbarEnable
+  = [TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE, TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITHOUT_CACHE].includes(selectedTabbarStrategy)
+
+/**
+ * 是否需要隐藏原生 tabbar
+ * CUSTOM_TABBAR_WITH_CACHE(2) 时,需要隐藏原生tabbar
+ */
+export const needHideNativeTabbar = selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE
+
+const _tabbarList = customTabbarEnable ? customTabbarList.map(item => ({ text: item.text, pagePath: item.pagePath })) : nativeTabbarList
+export const tabbarList = customTabbarEnable ? customTabbarList : nativeTabbarList
+
+const _tabbar: TabBar = {
+  // 只有微信小程序支持 custom。App 和 H5 不生效
+  custom: selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE,
+  color: '#999999',
+  selectedColor: '#018d71',
+  backgroundColor: '#F8F8F8',
+  borderStyle: 'black',
+  height: '50px',
+  fontSize: '10px',
+  iconWidth: '24px',
+  spacing: '3px',
+  list: _tabbarList as unknown as TabBar['list'],
+}
+
+// 0和1 需要显示底部的tabbar的各种配置,以利用缓存
+export const tabBar = tabbarCacheEnable ? _tabbar : {}

+ 159 - 0
src/tabbar/index.vue

@@ -0,0 +1,159 @@
+<script setup lang="ts">
+import type { CustomTabBarItem } from './config'
+import { customTabbarEnable, needHideNativeTabbar, tabbarCacheEnable } from './config'
+import { tabbarList, tabbarStore } from './store'
+
+// #ifdef MP-WEIXIN
+// 将自定义节点设置成虚拟的(去掉自定义组件包裹层),更加接近Vue组件的表现,能更好的使用flex属性
+defineOptions({
+  virtualHost: true,
+})
+// #endif
+
+/**
+ * 中间的鼓包tabbarItem的点击事件
+ */
+function handleClickBulge() {
+  uni.showToast({
+    title: '点击了中间的鼓包tabbarItem',
+    icon: 'none',
+  })
+}
+
+function handleClick(index: number) {
+  // 点击原来的不做操作
+  if (index === tabbarStore.curIdx) {
+    return
+  }
+  if (tabbarList[index].isBulge) {
+    handleClickBulge()
+    return
+  }
+  const url = tabbarList[index].pagePath
+  tabbarStore.setCurIdx(index)
+  if (tabbarCacheEnable) {
+    uni.switchTab({ url })
+  }
+  else {
+    uni.navigateTo({ url })
+  }
+}
+// #ifndef MP-WEIXIN
+// 因为有了 custom:true, 微信里面不需要多余的hide操作
+onLoad(() => {
+  // 解决原生 tabBar 未隐藏导致有2个 tabBar 的问题
+  needHideNativeTabbar
+  && 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) {
+  return tabbarStore.curIdx === index ? activeColor : inactiveColor
+}
+
+function getImageByIndex(index: number, item: CustomTabBarItem) {
+  if (!item.iconActive) {
+    console.warn('image 模式下,需要配置 iconActive (高亮时的图片),否则无法切换高亮图片')
+    return item.icon
+  }
+  return tabbarStore.curIdx === index ? item.iconActive : item.icon
+}
+</script>
+
+<template>
+  <view v-if="customTabbarEnable" class="h-50px pb-safe">
+    <view class="border-and-fixed bg-white" @touchmove.stop.prevent>
+      <view class="h-50px flex items-center">
+        <view
+          v-for="(item, index) in tabbarList" :key="index"
+          class="flex flex-1 flex-col items-center justify-center"
+          :style="{ color: getColorByIndex(index) }"
+          @click="handleClick(index)"
+        >
+          <view v-if="item.isBulge" class="relative">
+            <!-- 中间一个鼓包tabbarItem的处理 -->
+            <view class="bulge">
+              <!-- TODO 2/2: 中间鼓包tabbarItem配置:通常是一个图片,或者icon,点击触发业务逻辑 -->
+              <!-- 常见的是:扫描按钮、发布按钮、更多按钮等 -->
+              <image class="mt-6rpx h-200rpx w-200rpx" src="/static/tabbar/scan.png" />
+            </view>
+          </view>
+          <view v-else class="relative px-3 text-center">
+            <template v-if="item.iconType === 'uniUi'">
+              <uni-icons :type="item.icon" size="20" :color="getColorByIndex(index)" />
+            </template>
+            <template v-if="item.iconType === 'uiLib'">
+              <!-- TODO: 以下内容请根据选择的UI库自行替换 -->
+              <!-- 如:<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" />
+            </template>
+            <template v-if="item.iconType === 'unocss' || item.iconType === 'iconfont'">
+              <view :class="item.icon" class="text-20px" />
+            </template>
+            <template v-if="item.iconType === 'image'">
+              <image :src="getImageByIndex(index, item)" mode="scaleToFill" class="h-20px w-20px" />
+            </template>
+            <view class="mt-2px text-12px">
+              {{ item.text }}
+            </view>
+            <!-- 角标显示 -->
+            <view v-if="item.badge">
+              <template v-if="item.badge === 'dot'">
+                <view class="absolute right-0 top-0 h-2 w-2 rounded-full bg-#f56c6c" />
+              </template>
+              <template v-else>
+                <view class="absolute top-0 box-border h-5 min-w-5 center rounded-full bg-#f56c6c px-1 text-center text-xs text-white -right-3">
+                  {{ item.badge > 99 ? '99+' : item.badge }}
+                </view>
+              </template>
+            </view>
+          </view>
+        </view>
+      </view>
+
+      <view class="pb-safe" />
+    </view>
+  </view>
+</template>
+
+<style scoped lang="scss">
+.border-and-fixed {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+
+  border-top: 1px solid #eee;
+  box-sizing: border-box;
+}
+// 中间鼓包的样式
+.bulge {
+  position: absolute;
+  top: -20px;
+  left: 50%;
+  transform-origin: top center;
+  transform: translateX(-50%) scale(0.5) translateY(-33%);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 250rpx;
+  height: 250rpx;
+  border-radius: 50%;
+  background-color: #fff;
+  box-shadow: inset 0 0 0 1px #fefefe;
+
+  &:active {
+    // opacity: 0.8;
+  }
+}
+</style>

+ 71 - 0
src/tabbar/store.ts

@@ -0,0 +1,71 @@
+import type { CustomTabBarItem, CustomTabBarItemBadge } from './config'
+import { reactive } from 'vue'
+
+import { FG_LOG_ENABLE } from '@/router/interceptor'
+import { tabbarList as _tabbarList, customTabbarEnable } from './config'
+
+// TODO 1/2: 中间的鼓包tabbarItem的开关
+const BULGE_ENABLE = false
+
+/** tabbarList 里面的 path 从 pages.config.ts 得到 */
+const tabbarList = reactive<CustomTabBarItem[]>(_tabbarList.map(item => ({
+  ...item,
+  pagePath: item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}`,
+})))
+
+if (customTabbarEnable && BULGE_ENABLE) {
+  if (tabbarList.length % 2) {
+    console.error('有鼓包时 tabbar 数量必须是偶数,否则样式很奇怪!!')
+  }
+  tabbarList.splice(tabbarList.length / 2, 0, {
+    isBulge: true,
+  } as CustomTabBarItem)
+}
+
+export function isPageTabbar(path: string) {
+  return tabbarList.some(item => item.pagePath === path)
+}
+
+/**
+ * 自定义 tabbar 的状态管理,原生 tabbar 无需关注本文件
+ * tabbar 状态,增加 storageSync 保证刷新浏览器时在正确的 tabbar 页面
+ * 使用reactive简单状态,而不是 pinia 全局状态
+ */
+const tabbarStore = reactive({
+  curIdx: uni.getStorageSync('app-tabbar-index') || 0,
+  prevIdx: uni.getStorageSync('app-tabbar-index') || 0,
+  setCurIdx(idx: number) {
+    this.curIdx = idx
+    uni.setStorageSync('app-tabbar-index', idx)
+  },
+  setTabbarItemBadge(idx: number, badge: CustomTabBarItemBadge) {
+    if (tabbarList[idx]) {
+      tabbarList[idx].badge = badge
+    }
+  },
+  setAutoCurIdx(path: string) {
+    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}`)
+      // console.log(pagesPathList)
+      const flag = tabbarList.some(item => pagesPathList.includes(item.pagePath))
+      if (!flag) {
+        this.setCurIdx(0)
+        return
+      }
+    }
+    else {
+      this.setCurIdx(index)
+    }
+  },
+  restorePrevIdx() {
+    if (this.prevIdx === this.curIdx)
+      return
+    this.setCurIdx(this.prevIdx)
+    this.prevIdx = uni.getStorageSync('app-tabbar-index') || 0
+  },
+})
+
+export { tabbarList, tabbarStore }

+ 119 - 1
src/typings.d.ts

@@ -21,7 +21,125 @@ declare global {
     avatar?: string
     /** 微信的 openid,非微信没有这个字段 */
     openid?: string
-    token?: string
+  }
+
+  interface IUserToken {
+    token: string
+    refreshToken?: string
+    refreshExpire?: number
+  }
+}
+
+// patch uni 类型
+// 1. 补全 uni.hideToast() 的 options 类型
+// 2. 补全 uni.hideLoading() 的 options 类型
+// 3. 使用方式见:https://github.com/unibest-tech/unibest/pull/241
+declare global {
+  declare namespace UniNamespace {
+    /** 接口调用结束的回调函数(调用成功、失败都会执行) */
+    type HideLoadingCompleteCallback = (res: GeneralCallbackResult) => void
+    /** 接口调用失败的回调函数 */
+    type HideLoadingFailCallback = (res: GeneralCallbackResult) => void
+    /** 接口调用成功的回调函数 */
+    type HideLoadingSuccessCallback = (res: GeneralCallbackResult) => void
+
+    interface HideLoadingOption {
+      /** 接口调用结束的回调函数(调用成功、失败都会执行) */
+      complete?: HideLoadingCompleteCallback
+      /** 接口调用失败的回调函数 */
+      fail?: HideLoadingFailCallback
+      test: UniNamespace.GeneralCallbackResult
+      /**
+       * 微信小程序:需要基础库: `2.22.1`
+       *
+       * 微信小程序:目前 toast 和 loading 相关接口可以相互混用,此参数可用于取消混用特性
+       */
+      noConflict?: boolean
+      /** 接口调用成功的回调函数 */
+      success?: HideLoadingSuccessCallback
+    }
+
+    // ----------------------------------------------------------
+
+    /** 接口调用结束的回调函数(调用成功、失败都会执行) */
+    type HideToastCompleteCallback = (res: GeneralCallbackResult) => void
+    /** 接口调用失败的回调函数 */
+    type HideToastFailCallback = (res: GeneralCallbackResult) => void
+    /** 接口调用成功的回调函数 */
+    type HideToastSuccessCallback = (res: GeneralCallbackResult) => void
+    interface HideToastOption {
+      /** 接口调用结束的回调函数(调用成功、失败都会执行) */
+      complete?: HideToastCompleteCallback
+      /** 接口调用失败的回调函数 */
+      fail?: HideToastFailCallback
+      /**
+       * 微信小程序:需要基础库: `2.22.1`
+       *
+       * 微信小程序:目前 toast 和 loading 相关接口可以相互混用,此参数可用于取消混用特性
+       */
+      noConflict?: boolean
+      /** 接口调用成功的回调函数 */
+      success?: HideToastSuccessCallback
+    }
+  }
+  interface Uni {
+    /**
+     * 隐藏 loading 提示框
+     *
+     * 文档: [http://uniapp.dcloud.io/api/ui/prompt?id=hideloading](http://uniapp.dcloud.io/api/ui/prompt?id=hideloading)
+     * @example ```typescript
+     * uni.showLoading({
+     *   title: '加载中'
+     * });
+     *
+     * setTimeout(function () {
+     *   uni.hideLoading();
+     * }, 2000);
+     *
+     * ```
+     * @tutorial [](https://uniapp.dcloud.net.cn/api/ui/prompt.html#hideloading)
+     * @uniPlatform {
+     * "app": {
+     * "android": {
+     * "osVer": "4.4.4",
+     * "uniVer": "√",
+     * "unixVer": "3.9.0"
+     * },
+     * "ios": {
+     * "osVer": "9.0",
+     * "uniVer": "√",
+     * "unixVer": "3.9.0"
+     * }
+     * }
+     * }
+     */
+    // eslint-disable-next-line ts/method-signature-style
+    hideLoading<T extends UniNamespace.HideToastOption = UniNamespace.HideToastOption>(options?: T): void
+    /**
+     * 隐藏消息提示框
+     *
+     * 文档: [http://uniapp.dcloud.io/api/ui/prompt?id=hidetoast](http://uniapp.dcloud.io/api/ui/prompt?id=hidetoast)
+     * @example ```typescript
+     *    uni.hideToast();
+     * ```
+     * @tutorial [](https://uniapp.dcloud.net.cn/api/ui/prompt.html#hidetoast)
+     * @uniPlatform {
+     * "app": {
+     * "android": {
+     * "osVer": "4.4.4",
+     * "uniVer": "√",
+     * "unixVer": "3.9.0"
+     * },
+     * "ios": {
+     * "osVer": "9.0",
+     * "uniVer": "√",
+     * "unixVer": "3.9.0"
+     * }
+     * }
+     * }
+     */
+    // eslint-disable-next-line ts/method-signature-style
+    hideToast<T extends UniNamespace.HideLoadingOption = UniNamespace.HideLoadingOption>(options?: T): void
   }
 }
 

+ 42 - 0
src/uni_modules/uni-icons/changelog.md

@@ -0,0 +1,42 @@
+## 2.0.10(2024-06-07)
+- 优化 uni-app x 中,size 属性的类型
+## 2.0.9(2024-01-12)
+fix: 修复图标大小默认值错误的问题
+## 2.0.8(2023-12-14)
+- 修复 项目未使用 ts 情况下,打包报错的bug
+## 2.0.7(2023-12-14)
+- 修复 size 属性为 string 时,不加单位导致尺寸异常的bug
+## 2.0.6(2023-12-11)
+- 优化 兼容老版本icon类型,如 top ,bottom 等
+## 2.0.5(2023-12-11)
+- 优化 兼容老版本icon类型,如 top ,bottom 等
+## 2.0.4(2023-12-06)
+- 优化 uni-app x 下示例项目图标排序
+## 2.0.3(2023-12-06)
+- 修复 nvue下引入组件报错的bug
+## 2.0.2(2023-12-05)
+-优化 size 属性支持单位
+## 2.0.1(2023-12-05)
+- 新增 uni-app x 支持定义图标
+## 1.3.5(2022-01-24)
+- 优化 size 属性可以传入不带单位的字符串数值
+## 1.3.4(2022-01-24)
+- 优化 size 支持其他单位
+## 1.3.3(2022-01-17)
+- 修复 nvue 有些图标不显示的bug,兼容老版本图标
+## 1.3.2(2021-12-01)
+- 优化 示例可复制图标名称
+## 1.3.1(2021-11-23)
+- 优化 兼容旧组件 type 值
+## 1.3.0(2021-11-19)
+- 新增 更多图标
+- 优化 自定义图标使用方式
+- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-icons](https://uniapp.dcloud.io/component/uniui/uni-icons)
+## 1.1.7(2021-11-08)
+## 1.2.0(2021-07-30)
+- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
+## 1.1.5(2021-05-12)
+- 新增 组件示例地址
+## 1.1.4(2021-02-05)
+- 调整为uni_modules目录规范

+ 91 - 0
src/uni_modules/uni-icons/components/uni-icons/uni-icons.uvue

@@ -0,0 +1,91 @@
+<template>
+  <text class="uni-icons" :style="styleObj">
+    <slot>{{unicode}}</slot>
+  </text>
+</template>
+
+<script>
+  import { fontData, IconsDataItem } from './uniicons_file'
+
+  /**
+   * Icons 图标
+   * @description 用于展示 icon 图标
+   * @tutorial https://ext.dcloud.net.cn/plugin?id=28
+   * @property {Number,String} size 图标大小
+   * @property {String} type 图标图案,参考示例
+   * @property {String} color 图标颜色
+   * @property {String} customPrefix 自定义图标
+   * @event {Function} click 点击 Icon 触发事件
+   */
+  export default {
+    name: "uni-icons",
+    props: {
+      type: {
+        type: String,
+        default: ''
+      },
+      color: {
+        type: String,
+        default: '#333333'
+      },
+      size: {
+        type: [Number, String],
+        default: 16
+      },
+      fontFamily: {
+        type: String,
+        default: ''
+      }
+    },
+    data() {
+      return {};
+    },
+    computed: {
+      unicode() : string {
+        let codes = fontData.find((item : IconsDataItem) : boolean => { return item.font_class == this.type })
+        if (codes !== null) {
+          return codes.unicode
+        }
+        return ''
+      },
+      iconSize() : string {
+        const size = this.size
+        if (typeof size == 'string') {
+          const reg = /^[0-9]*$/g
+          return reg.test(size as string) ? '' + size + 'px' : '' + size;
+          // return '' + this.size
+        }
+        return this.getFontSize(size as number)
+      },
+      styleObj() : UTSJSONObject {
+        if (this.fontFamily !== '') {
+          return { color: this.color, fontSize: this.iconSize, fontFamily: this.fontFamily }
+        }
+        return { color: this.color, fontSize: this.iconSize }
+      }
+    },
+    created() { },
+    methods: {
+      /**
+       * 字体大小
+       */
+      getFontSize(size : number) : string {
+        return size + 'px';
+      },
+    },
+  }
+</script>
+
+<style scoped>
+  @font-face {
+    font-family: UniIconsFontFamily;
+    src: url('./uniicons.ttf');
+  }
+
+  .uni-icons {
+    font-family: UniIconsFontFamily;
+    font-size: 18px;
+    font-style: normal;
+    color: #333;
+  }
+</style>

+ 110 - 0
src/uni_modules/uni-icons/components/uni-icons/uni-icons.vue

@@ -0,0 +1,110 @@
+<template>
+	<!-- #ifdef APP-NVUE -->
+	<text :style="styleObj" class="uni-icons" @click="_onClick">{{unicode}}</text>
+	<!-- #endif -->
+	<!-- #ifndef APP-NVUE -->
+	<text :style="styleObj" class="uni-icons" :class="['uniui-'+type,customPrefix,customPrefix?type:'']" @click="_onClick">
+		<slot></slot>
+	</text>
+	<!-- #endif -->
+</template>
+
+<script>
+	import { fontData } from './uniicons_file_vue.js';
+
+	const getVal = (val) => {
+		const reg = /^[0-9]*$/g
+		return (typeof val === 'number' || reg.test(val)) ? val + 'px' : val;
+	}
+
+	// #ifdef APP-NVUE
+	var domModule = weex.requireModule('dom');
+	import iconUrl from './uniicons.ttf'
+	domModule.addRule('fontFace', {
+		'fontFamily': "uniicons",
+		'src': "url('" + iconUrl + "')"
+	});
+	// #endif
+
+	/**
+	 * Icons 图标
+	 * @description 用于展示 icons 图标
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=28
+	 * @property {Number} size 图标大小
+	 * @property {String} type 图标图案,参考示例
+	 * @property {String} color 图标颜色
+	 * @property {String} customPrefix 自定义图标
+	 * @event {Function} click 点击 Icon 触发事件
+	 */
+	export default {
+		name: 'UniIcons',
+		emits: ['click'],
+		props: {
+			type: {
+				type: String,
+				default: ''
+			},
+			color: {
+				type: String,
+				default: '#333333'
+			},
+			size: {
+				type: [Number, String],
+				default: 16
+			},
+			customPrefix: {
+				type: String,
+				default: ''
+			},
+			fontFamily: {
+				type: String,
+				default: ''
+			}
+		},
+		data() {
+			return {
+				icons: fontData
+			}
+		},
+		computed: {
+			unicode() {
+				let code = this.icons.find(v => v.font_class === this.type)
+				if (code) {
+					return code.unicode
+				}
+				return ''
+			},
+			iconSize() {
+				return getVal(this.size)
+			},
+			styleObj() {
+				if (this.fontFamily !== '') {
+					return `color: ${this.color}; font-size: ${this.iconSize}; font-family: ${this.fontFamily};`
+				}
+				return `color: ${this.color}; font-size: ${this.iconSize};`
+			}
+		},
+		methods: {
+			_onClick() {
+				this.$emit('click')
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	/* #ifndef APP-NVUE */
+	@import './uniicons.css';
+
+	@font-face {
+		font-family: uniicons;
+		src: url('./uniicons.ttf');
+	}
+
+	/* #endif */
+	.uni-icons {
+		font-family: uniicons;
+		text-decoration: none;
+		text-align: center;
+	}
+</style>

+ 664 - 0
src/uni_modules/uni-icons/components/uni-icons/uniicons.css

@@ -0,0 +1,664 @@
+
+.uniui-cart-filled:before {
+  content: "\e6d0";
+}
+
+.uniui-gift-filled:before {
+  content: "\e6c4";
+}
+
+.uniui-color:before {
+  content: "\e6cf";
+}
+
+.uniui-wallet:before {
+  content: "\e6b1";
+}
+
+.uniui-settings-filled:before {
+  content: "\e6ce";
+}
+
+.uniui-auth-filled:before {
+  content: "\e6cc";
+}
+
+.uniui-shop-filled:before {
+  content: "\e6cd";
+}
+
+.uniui-staff-filled:before {
+  content: "\e6cb";
+}
+
+.uniui-vip-filled:before {
+  content: "\e6c6";
+}
+
+.uniui-plus-filled:before {
+  content: "\e6c7";
+}
+
+.uniui-folder-add-filled:before {
+  content: "\e6c8";
+}
+
+.uniui-color-filled:before {
+  content: "\e6c9";
+}
+
+.uniui-tune-filled:before {
+  content: "\e6ca";
+}
+
+.uniui-calendar-filled:before {
+  content: "\e6c0";
+}
+
+.uniui-notification-filled:before {
+  content: "\e6c1";
+}
+
+.uniui-wallet-filled:before {
+  content: "\e6c2";
+}
+
+.uniui-medal-filled:before {
+  content: "\e6c3";
+}
+
+.uniui-fire-filled:before {
+  content: "\e6c5";
+}
+
+.uniui-refreshempty:before {
+  content: "\e6bf";
+}
+
+.uniui-location-filled:before {
+  content: "\e6af";
+}
+
+.uniui-person-filled:before {
+  content: "\e69d";
+}
+
+.uniui-personadd-filled:before {
+  content: "\e698";
+}
+
+.uniui-arrowthinleft:before {
+  content: "\e6d2";
+}
+
+.uniui-arrowthinup:before {
+  content: "\e6d3";
+}
+
+.uniui-arrowthindown:before {
+  content: "\e6d4";
+}
+
+.uniui-back:before {
+  content: "\e6b9";
+}
+
+.uniui-forward:before {
+  content: "\e6ba";
+}
+
+.uniui-arrow-right:before {
+  content: "\e6bb";
+}
+
+.uniui-arrow-left:before {
+  content: "\e6bc";
+}
+
+.uniui-arrow-up:before {
+  content: "\e6bd";
+}
+
+.uniui-arrow-down:before {
+  content: "\e6be";
+}
+
+.uniui-arrowthinright:before {
+  content: "\e6d1";
+}
+
+.uniui-down:before {
+  content: "\e6b8";
+}
+
+.uniui-bottom:before {
+  content: "\e6b8";
+}
+
+.uniui-arrowright:before {
+  content: "\e6d5";
+}
+
+.uniui-right:before {
+  content: "\e6b5";
+}
+
+.uniui-up:before {
+  content: "\e6b6";
+}
+
+.uniui-top:before {
+  content: "\e6b6";
+}
+
+.uniui-left:before {
+  content: "\e6b7";
+}
+
+.uniui-arrowup:before {
+  content: "\e6d6";
+}
+
+.uniui-eye:before {
+  content: "\e651";
+}
+
+.uniui-eye-filled:before {
+  content: "\e66a";
+}
+
+.uniui-eye-slash:before {
+  content: "\e6b3";
+}
+
+.uniui-eye-slash-filled:before {
+  content: "\e6b4";
+}
+
+.uniui-info-filled:before {
+  content: "\e649";
+}
+
+.uniui-reload:before {
+  content: "\e6b2";
+}
+
+.uniui-micoff-filled:before {
+  content: "\e6b0";
+}
+
+.uniui-map-pin-ellipse:before {
+  content: "\e6ac";
+}
+
+.uniui-map-pin:before {
+  content: "\e6ad";
+}
+
+.uniui-location:before {
+  content: "\e6ae";
+}
+
+.uniui-starhalf:before {
+  content: "\e683";
+}
+
+.uniui-star:before {
+  content: "\e688";
+}
+
+.uniui-star-filled:before {
+  content: "\e68f";
+}
+
+.uniui-calendar:before {
+  content: "\e6a0";
+}
+
+.uniui-fire:before {
+  content: "\e6a1";
+}
+
+.uniui-medal:before {
+  content: "\e6a2";
+}
+
+.uniui-font:before {
+  content: "\e6a3";
+}
+
+.uniui-gift:before {
+  content: "\e6a4";
+}
+
+.uniui-link:before {
+  content: "\e6a5";
+}
+
+.uniui-notification:before {
+  content: "\e6a6";
+}
+
+.uniui-staff:before {
+  content: "\e6a7";
+}
+
+.uniui-vip:before {
+  content: "\e6a8";
+}
+
+.uniui-folder-add:before {
+  content: "\e6a9";
+}
+
+.uniui-tune:before {
+  content: "\e6aa";
+}
+
+.uniui-auth:before {
+  content: "\e6ab";
+}
+
+.uniui-person:before {
+  content: "\e699";
+}
+
+.uniui-email-filled:before {
+  content: "\e69a";
+}
+
+.uniui-phone-filled:before {
+  content: "\e69b";
+}
+
+.uniui-phone:before {
+  content: "\e69c";
+}
+
+.uniui-email:before {
+  content: "\e69e";
+}
+
+.uniui-personadd:before {
+  content: "\e69f";
+}
+
+.uniui-chatboxes-filled:before {
+  content: "\e692";
+}
+
+.uniui-contact:before {
+  content: "\e693";
+}
+
+.uniui-chatbubble-filled:before {
+  content: "\e694";
+}
+
+.uniui-contact-filled:before {
+  content: "\e695";
+}
+
+.uniui-chatboxes:before {
+  content: "\e696";
+}
+
+.uniui-chatbubble:before {
+  content: "\e697";
+}
+
+.uniui-upload-filled:before {
+  content: "\e68e";
+}
+
+.uniui-upload:before {
+  content: "\e690";
+}
+
+.uniui-weixin:before {
+  content: "\e691";
+}
+
+.uniui-compose:before {
+  content: "\e67f";
+}
+
+.uniui-qq:before {
+  content: "\e680";
+}
+
+.uniui-download-filled:before {
+  content: "\e681";
+}
+
+.uniui-pyq:before {
+  content: "\e682";
+}
+
+.uniui-sound:before {
+  content: "\e684";
+}
+
+.uniui-trash-filled:before {
+  content: "\e685";
+}
+
+.uniui-sound-filled:before {
+  content: "\e686";
+}
+
+.uniui-trash:before {
+  content: "\e687";
+}
+
+.uniui-videocam-filled:before {
+  content: "\e689";
+}
+
+.uniui-spinner-cycle:before {
+  content: "\e68a";
+}
+
+.uniui-weibo:before {
+  content: "\e68b";
+}
+
+.uniui-videocam:before {
+  content: "\e68c";
+}
+
+.uniui-download:before {
+  content: "\e68d";
+}
+
+.uniui-help:before {
+  content: "\e679";
+}
+
+.uniui-navigate-filled:before {
+  content: "\e67a";
+}
+
+.uniui-plusempty:before {
+  content: "\e67b";
+}
+
+.uniui-smallcircle:before {
+  content: "\e67c";
+}
+
+.uniui-minus-filled:before {
+  content: "\e67d";
+}
+
+.uniui-micoff:before {
+  content: "\e67e";
+}
+
+.uniui-closeempty:before {
+  content: "\e66c";
+}
+
+.uniui-clear:before {
+  content: "\e66d";
+}
+
+.uniui-navigate:before {
+  content: "\e66e";
+}
+
+.uniui-minus:before {
+  content: "\e66f";
+}
+
+.uniui-image:before {
+  content: "\e670";
+}
+
+.uniui-mic:before {
+  content: "\e671";
+}
+
+.uniui-paperplane:before {
+  content: "\e672";
+}
+
+.uniui-close:before {
+  content: "\e673";
+}
+
+.uniui-help-filled:before {
+  content: "\e674";
+}
+
+.uniui-paperplane-filled:before {
+  content: "\e675";
+}
+
+.uniui-plus:before {
+  content: "\e676";
+}
+
+.uniui-mic-filled:before {
+  content: "\e677";
+}
+
+.uniui-image-filled:before {
+  content: "\e678";
+}
+
+.uniui-locked-filled:before {
+  content: "\e668";
+}
+
+.uniui-info:before {
+  content: "\e669";
+}
+
+.uniui-locked:before {
+  content: "\e66b";
+}
+
+.uniui-camera-filled:before {
+  content: "\e658";
+}
+
+.uniui-chat-filled:before {
+  content: "\e659";
+}
+
+.uniui-camera:before {
+  content: "\e65a";
+}
+
+.uniui-circle:before {
+  content: "\e65b";
+}
+
+.uniui-checkmarkempty:before {
+  content: "\e65c";
+}
+
+.uniui-chat:before {
+  content: "\e65d";
+}
+
+.uniui-circle-filled:before {
+  content: "\e65e";
+}
+
+.uniui-flag:before {
+  content: "\e65f";
+}
+
+.uniui-flag-filled:before {
+  content: "\e660";
+}
+
+.uniui-gear-filled:before {
+  content: "\e661";
+}
+
+.uniui-home:before {
+  content: "\e662";
+}
+
+.uniui-home-filled:before {
+  content: "\e663";
+}
+
+.uniui-gear:before {
+  content: "\e664";
+}
+
+.uniui-smallcircle-filled:before {
+  content: "\e665";
+}
+
+.uniui-map-filled:before {
+  content: "\e666";
+}
+
+.uniui-map:before {
+  content: "\e667";
+}
+
+.uniui-refresh-filled:before {
+  content: "\e656";
+}
+
+.uniui-refresh:before {
+  content: "\e657";
+}
+
+.uniui-cloud-upload:before {
+  content: "\e645";
+}
+
+.uniui-cloud-download-filled:before {
+  content: "\e646";
+}
+
+.uniui-cloud-download:before {
+  content: "\e647";
+}
+
+.uniui-cloud-upload-filled:before {
+  content: "\e648";
+}
+
+.uniui-redo:before {
+  content: "\e64a";
+}
+
+.uniui-images-filled:before {
+  content: "\e64b";
+}
+
+.uniui-undo-filled:before {
+  content: "\e64c";
+}
+
+.uniui-more:before {
+  content: "\e64d";
+}
+
+.uniui-more-filled:before {
+  content: "\e64e";
+}
+
+.uniui-undo:before {
+  content: "\e64f";
+}
+
+.uniui-images:before {
+  content: "\e650";
+}
+
+.uniui-paperclip:before {
+  content: "\e652";
+}
+
+.uniui-settings:before {
+  content: "\e653";
+}
+
+.uniui-search:before {
+  content: "\e654";
+}
+
+.uniui-redo-filled:before {
+  content: "\e655";
+}
+
+.uniui-list:before {
+  content: "\e644";
+}
+
+.uniui-mail-open-filled:before {
+  content: "\e63a";
+}
+
+.uniui-hand-down-filled:before {
+  content: "\e63c";
+}
+
+.uniui-hand-down:before {
+  content: "\e63d";
+}
+
+.uniui-hand-up-filled:before {
+  content: "\e63e";
+}
+
+.uniui-hand-up:before {
+  content: "\e63f";
+}
+
+.uniui-heart-filled:before {
+  content: "\e641";
+}
+
+.uniui-mail-open:before {
+  content: "\e643";
+}
+
+.uniui-heart:before {
+  content: "\e639";
+}
+
+.uniui-loop:before {
+  content: "\e633";
+}
+
+.uniui-pulldown:before {
+  content: "\e632";
+}
+
+.uniui-scan:before {
+  content: "\e62a";
+}
+
+.uniui-bars:before {
+  content: "\e627";
+}
+
+.uniui-checkbox:before {
+  content: "\e62b";
+}
+
+.uniui-checkbox-filled:before {
+  content: "\e62c";
+}
+
+.uniui-shop:before {
+  content: "\e62f";
+}
+
+.uniui-headphones:before {
+  content: "\e630";
+}
+
+.uniui-cart:before {
+  content: "\e631";
+}

BIN
src/uni_modules/uni-icons/components/uni-icons/uniicons.ttf


+ 664 - 0
src/uni_modules/uni-icons/components/uni-icons/uniicons_file.ts

@@ -0,0 +1,664 @@
+
+export type IconsData = {
+	id : string
+	name : string
+	font_family : string
+	css_prefix_text : string
+	description : string
+	glyphs : Array<IconsDataItem>
+}
+
+export type IconsDataItem = {
+	font_class : string
+	unicode : string
+}
+
+
+export const fontData = [
+  {
+    "font_class": "arrow-down",
+    "unicode": "\ue6be"
+  },
+  {
+    "font_class": "arrow-left",
+    "unicode": "\ue6bc"
+  },
+  {
+    "font_class": "arrow-right",
+    "unicode": "\ue6bb"
+  },
+  {
+    "font_class": "arrow-up",
+    "unicode": "\ue6bd"
+  },
+  {
+    "font_class": "auth",
+    "unicode": "\ue6ab"
+  },
+  {
+    "font_class": "auth-filled",
+    "unicode": "\ue6cc"
+  },
+  {
+    "font_class": "back",
+    "unicode": "\ue6b9"
+  },
+  {
+    "font_class": "bars",
+    "unicode": "\ue627"
+  },
+  {
+    "font_class": "calendar",
+    "unicode": "\ue6a0"
+  },
+  {
+    "font_class": "calendar-filled",
+    "unicode": "\ue6c0"
+  },
+  {
+    "font_class": "camera",
+    "unicode": "\ue65a"
+  },
+  {
+    "font_class": "camera-filled",
+    "unicode": "\ue658"
+  },
+  {
+    "font_class": "cart",
+    "unicode": "\ue631"
+  },
+  {
+    "font_class": "cart-filled",
+    "unicode": "\ue6d0"
+  },
+  {
+    "font_class": "chat",
+    "unicode": "\ue65d"
+  },
+  {
+    "font_class": "chat-filled",
+    "unicode": "\ue659"
+  },
+  {
+    "font_class": "chatboxes",
+    "unicode": "\ue696"
+  },
+  {
+    "font_class": "chatboxes-filled",
+    "unicode": "\ue692"
+  },
+  {
+    "font_class": "chatbubble",
+    "unicode": "\ue697"
+  },
+  {
+    "font_class": "chatbubble-filled",
+    "unicode": "\ue694"
+  },
+  {
+    "font_class": "checkbox",
+    "unicode": "\ue62b"
+  },
+  {
+    "font_class": "checkbox-filled",
+    "unicode": "\ue62c"
+  },
+  {
+    "font_class": "checkmarkempty",
+    "unicode": "\ue65c"
+  },
+  {
+    "font_class": "circle",
+    "unicode": "\ue65b"
+  },
+  {
+    "font_class": "circle-filled",
+    "unicode": "\ue65e"
+  },
+  {
+    "font_class": "clear",
+    "unicode": "\ue66d"
+  },
+  {
+    "font_class": "close",
+    "unicode": "\ue673"
+  },
+  {
+    "font_class": "closeempty",
+    "unicode": "\ue66c"
+  },
+  {
+    "font_class": "cloud-download",
+    "unicode": "\ue647"
+  },
+  {
+    "font_class": "cloud-download-filled",
+    "unicode": "\ue646"
+  },
+  {
+    "font_class": "cloud-upload",
+    "unicode": "\ue645"
+  },
+  {
+    "font_class": "cloud-upload-filled",
+    "unicode": "\ue648"
+  },
+  {
+    "font_class": "color",
+    "unicode": "\ue6cf"
+  },
+  {
+    "font_class": "color-filled",
+    "unicode": "\ue6c9"
+  },
+  {
+    "font_class": "compose",
+    "unicode": "\ue67f"
+  },
+  {
+    "font_class": "contact",
+    "unicode": "\ue693"
+  },
+  {
+    "font_class": "contact-filled",
+    "unicode": "\ue695"
+  },
+  {
+    "font_class": "down",
+    "unicode": "\ue6b8"
+  },
+	{
+	  "font_class": "bottom",
+	  "unicode": "\ue6b8"
+	},
+  {
+    "font_class": "download",
+    "unicode": "\ue68d"
+  },
+  {
+    "font_class": "download-filled",
+    "unicode": "\ue681"
+  },
+  {
+    "font_class": "email",
+    "unicode": "\ue69e"
+  },
+  {
+    "font_class": "email-filled",
+    "unicode": "\ue69a"
+  },
+  {
+    "font_class": "eye",
+    "unicode": "\ue651"
+  },
+  {
+    "font_class": "eye-filled",
+    "unicode": "\ue66a"
+  },
+  {
+    "font_class": "eye-slash",
+    "unicode": "\ue6b3"
+  },
+  {
+    "font_class": "eye-slash-filled",
+    "unicode": "\ue6b4"
+  },
+  {
+    "font_class": "fire",
+    "unicode": "\ue6a1"
+  },
+  {
+    "font_class": "fire-filled",
+    "unicode": "\ue6c5"
+  },
+  {
+    "font_class": "flag",
+    "unicode": "\ue65f"
+  },
+  {
+    "font_class": "flag-filled",
+    "unicode": "\ue660"
+  },
+  {
+    "font_class": "folder-add",
+    "unicode": "\ue6a9"
+  },
+  {
+    "font_class": "folder-add-filled",
+    "unicode": "\ue6c8"
+  },
+  {
+    "font_class": "font",
+    "unicode": "\ue6a3"
+  },
+  {
+    "font_class": "forward",
+    "unicode": "\ue6ba"
+  },
+  {
+    "font_class": "gear",
+    "unicode": "\ue664"
+  },
+  {
+    "font_class": "gear-filled",
+    "unicode": "\ue661"
+  },
+  {
+    "font_class": "gift",
+    "unicode": "\ue6a4"
+  },
+  {
+    "font_class": "gift-filled",
+    "unicode": "\ue6c4"
+  },
+  {
+    "font_class": "hand-down",
+    "unicode": "\ue63d"
+  },
+  {
+    "font_class": "hand-down-filled",
+    "unicode": "\ue63c"
+  },
+  {
+    "font_class": "hand-up",
+    "unicode": "\ue63f"
+  },
+  {
+    "font_class": "hand-up-filled",
+    "unicode": "\ue63e"
+  },
+  {
+    "font_class": "headphones",
+    "unicode": "\ue630"
+  },
+  {
+    "font_class": "heart",
+    "unicode": "\ue639"
+  },
+  {
+    "font_class": "heart-filled",
+    "unicode": "\ue641"
+  },
+  {
+    "font_class": "help",
+    "unicode": "\ue679"
+  },
+  {
+    "font_class": "help-filled",
+    "unicode": "\ue674"
+  },
+  {
+    "font_class": "home",
+    "unicode": "\ue662"
+  },
+  {
+    "font_class": "home-filled",
+    "unicode": "\ue663"
+  },
+  {
+    "font_class": "image",
+    "unicode": "\ue670"
+  },
+  {
+    "font_class": "image-filled",
+    "unicode": "\ue678"
+  },
+  {
+    "font_class": "images",
+    "unicode": "\ue650"
+  },
+  {
+    "font_class": "images-filled",
+    "unicode": "\ue64b"
+  },
+  {
+    "font_class": "info",
+    "unicode": "\ue669"
+  },
+  {
+    "font_class": "info-filled",
+    "unicode": "\ue649"
+  },
+  {
+    "font_class": "left",
+    "unicode": "\ue6b7"
+  },
+  {
+    "font_class": "link",
+    "unicode": "\ue6a5"
+  },
+  {
+    "font_class": "list",
+    "unicode": "\ue644"
+  },
+  {
+    "font_class": "location",
+    "unicode": "\ue6ae"
+  },
+  {
+    "font_class": "location-filled",
+    "unicode": "\ue6af"
+  },
+  {
+    "font_class": "locked",
+    "unicode": "\ue66b"
+  },
+  {
+    "font_class": "locked-filled",
+    "unicode": "\ue668"
+  },
+  {
+    "font_class": "loop",
+    "unicode": "\ue633"
+  },
+  {
+    "font_class": "mail-open",
+    "unicode": "\ue643"
+  },
+  {
+    "font_class": "mail-open-filled",
+    "unicode": "\ue63a"
+  },
+  {
+    "font_class": "map",
+    "unicode": "\ue667"
+  },
+  {
+    "font_class": "map-filled",
+    "unicode": "\ue666"
+  },
+  {
+    "font_class": "map-pin",
+    "unicode": "\ue6ad"
+  },
+  {
+    "font_class": "map-pin-ellipse",
+    "unicode": "\ue6ac"
+  },
+  {
+    "font_class": "medal",
+    "unicode": "\ue6a2"
+  },
+  {
+    "font_class": "medal-filled",
+    "unicode": "\ue6c3"
+  },
+  {
+    "font_class": "mic",
+    "unicode": "\ue671"
+  },
+  {
+    "font_class": "mic-filled",
+    "unicode": "\ue677"
+  },
+  {
+    "font_class": "micoff",
+    "unicode": "\ue67e"
+  },
+  {
+    "font_class": "micoff-filled",
+    "unicode": "\ue6b0"
+  },
+  {
+    "font_class": "minus",
+    "unicode": "\ue66f"
+  },
+  {
+    "font_class": "minus-filled",
+    "unicode": "\ue67d"
+  },
+  {
+    "font_class": "more",
+    "unicode": "\ue64d"
+  },
+  {
+    "font_class": "more-filled",
+    "unicode": "\ue64e"
+  },
+  {
+    "font_class": "navigate",
+    "unicode": "\ue66e"
+  },
+  {
+    "font_class": "navigate-filled",
+    "unicode": "\ue67a"
+  },
+  {
+    "font_class": "notification",
+    "unicode": "\ue6a6"
+  },
+  {
+    "font_class": "notification-filled",
+    "unicode": "\ue6c1"
+  },
+  {
+    "font_class": "paperclip",
+    "unicode": "\ue652"
+  },
+  {
+    "font_class": "paperplane",
+    "unicode": "\ue672"
+  },
+  {
+    "font_class": "paperplane-filled",
+    "unicode": "\ue675"
+  },
+  {
+    "font_class": "person",
+    "unicode": "\ue699"
+  },
+  {
+    "font_class": "person-filled",
+    "unicode": "\ue69d"
+  },
+  {
+    "font_class": "personadd",
+    "unicode": "\ue69f"
+  },
+  {
+    "font_class": "personadd-filled",
+    "unicode": "\ue698"
+  },
+  {
+    "font_class": "personadd-filled-copy",
+    "unicode": "\ue6d1"
+  },
+  {
+    "font_class": "phone",
+    "unicode": "\ue69c"
+  },
+  {
+    "font_class": "phone-filled",
+    "unicode": "\ue69b"
+  },
+  {
+    "font_class": "plus",
+    "unicode": "\ue676"
+  },
+  {
+    "font_class": "plus-filled",
+    "unicode": "\ue6c7"
+  },
+  {
+    "font_class": "plusempty",
+    "unicode": "\ue67b"
+  },
+  {
+    "font_class": "pulldown",
+    "unicode": "\ue632"
+  },
+  {
+    "font_class": "pyq",
+    "unicode": "\ue682"
+  },
+  {
+    "font_class": "qq",
+    "unicode": "\ue680"
+  },
+  {
+    "font_class": "redo",
+    "unicode": "\ue64a"
+  },
+  {
+    "font_class": "redo-filled",
+    "unicode": "\ue655"
+  },
+  {
+    "font_class": "refresh",
+    "unicode": "\ue657"
+  },
+  {
+    "font_class": "refresh-filled",
+    "unicode": "\ue656"
+  },
+  {
+    "font_class": "refreshempty",
+    "unicode": "\ue6bf"
+  },
+  {
+    "font_class": "reload",
+    "unicode": "\ue6b2"
+  },
+  {
+    "font_class": "right",
+    "unicode": "\ue6b5"
+  },
+  {
+    "font_class": "scan",
+    "unicode": "\ue62a"
+  },
+  {
+    "font_class": "search",
+    "unicode": "\ue654"
+  },
+  {
+    "font_class": "settings",
+    "unicode": "\ue653"
+  },
+  {
+    "font_class": "settings-filled",
+    "unicode": "\ue6ce"
+  },
+  {
+    "font_class": "shop",
+    "unicode": "\ue62f"
+  },
+  {
+    "font_class": "shop-filled",
+    "unicode": "\ue6cd"
+  },
+  {
+    "font_class": "smallcircle",
+    "unicode": "\ue67c"
+  },
+  {
+    "font_class": "smallcircle-filled",
+    "unicode": "\ue665"
+  },
+  {
+    "font_class": "sound",
+    "unicode": "\ue684"
+  },
+  {
+    "font_class": "sound-filled",
+    "unicode": "\ue686"
+  },
+  {
+    "font_class": "spinner-cycle",
+    "unicode": "\ue68a"
+  },
+  {
+    "font_class": "staff",
+    "unicode": "\ue6a7"
+  },
+  {
+    "font_class": "staff-filled",
+    "unicode": "\ue6cb"
+  },
+  {
+    "font_class": "star",
+    "unicode": "\ue688"
+  },
+  {
+    "font_class": "star-filled",
+    "unicode": "\ue68f"
+  },
+  {
+    "font_class": "starhalf",
+    "unicode": "\ue683"
+  },
+  {
+    "font_class": "trash",
+    "unicode": "\ue687"
+  },
+  {
+    "font_class": "trash-filled",
+    "unicode": "\ue685"
+  },
+  {
+    "font_class": "tune",
+    "unicode": "\ue6aa"
+  },
+  {
+    "font_class": "tune-filled",
+    "unicode": "\ue6ca"
+  },
+  {
+    "font_class": "undo",
+    "unicode": "\ue64f"
+  },
+  {
+    "font_class": "undo-filled",
+    "unicode": "\ue64c"
+  },
+  {
+    "font_class": "up",
+    "unicode": "\ue6b6"
+  },
+	{
+	  "font_class": "top",
+	  "unicode": "\ue6b6"
+	},
+  {
+    "font_class": "upload",
+    "unicode": "\ue690"
+  },
+  {
+    "font_class": "upload-filled",
+    "unicode": "\ue68e"
+  },
+  {
+    "font_class": "videocam",
+    "unicode": "\ue68c"
+  },
+  {
+    "font_class": "videocam-filled",
+    "unicode": "\ue689"
+  },
+  {
+    "font_class": "vip",
+    "unicode": "\ue6a8"
+  },
+  {
+    "font_class": "vip-filled",
+    "unicode": "\ue6c6"
+  },
+  {
+    "font_class": "wallet",
+    "unicode": "\ue6b1"
+  },
+  {
+    "font_class": "wallet-filled",
+    "unicode": "\ue6c2"
+  },
+  {
+    "font_class": "weibo",
+    "unicode": "\ue68b"
+  },
+  {
+    "font_class": "weixin",
+    "unicode": "\ue691"
+  }
+] as IconsDataItem[]
+
+// export const fontData = JSON.parse<IconsDataItem>(fontDataJson)

+ 649 - 0
src/uni_modules/uni-icons/components/uni-icons/uniicons_file_vue.js

@@ -0,0 +1,649 @@
+
+export const fontData = [
+  {
+    "font_class": "arrow-down",
+    "unicode": "\ue6be"
+  },
+  {
+    "font_class": "arrow-left",
+    "unicode": "\ue6bc"
+  },
+  {
+    "font_class": "arrow-right",
+    "unicode": "\ue6bb"
+  },
+  {
+    "font_class": "arrow-up",
+    "unicode": "\ue6bd"
+  },
+  {
+    "font_class": "auth",
+    "unicode": "\ue6ab"
+  },
+  {
+    "font_class": "auth-filled",
+    "unicode": "\ue6cc"
+  },
+  {
+    "font_class": "back",
+    "unicode": "\ue6b9"
+  },
+  {
+    "font_class": "bars",
+    "unicode": "\ue627"
+  },
+  {
+    "font_class": "calendar",
+    "unicode": "\ue6a0"
+  },
+  {
+    "font_class": "calendar-filled",
+    "unicode": "\ue6c0"
+  },
+  {
+    "font_class": "camera",
+    "unicode": "\ue65a"
+  },
+  {
+    "font_class": "camera-filled",
+    "unicode": "\ue658"
+  },
+  {
+    "font_class": "cart",
+    "unicode": "\ue631"
+  },
+  {
+    "font_class": "cart-filled",
+    "unicode": "\ue6d0"
+  },
+  {
+    "font_class": "chat",
+    "unicode": "\ue65d"
+  },
+  {
+    "font_class": "chat-filled",
+    "unicode": "\ue659"
+  },
+  {
+    "font_class": "chatboxes",
+    "unicode": "\ue696"
+  },
+  {
+    "font_class": "chatboxes-filled",
+    "unicode": "\ue692"
+  },
+  {
+    "font_class": "chatbubble",
+    "unicode": "\ue697"
+  },
+  {
+    "font_class": "chatbubble-filled",
+    "unicode": "\ue694"
+  },
+  {
+    "font_class": "checkbox",
+    "unicode": "\ue62b"
+  },
+  {
+    "font_class": "checkbox-filled",
+    "unicode": "\ue62c"
+  },
+  {
+    "font_class": "checkmarkempty",
+    "unicode": "\ue65c"
+  },
+  {
+    "font_class": "circle",
+    "unicode": "\ue65b"
+  },
+  {
+    "font_class": "circle-filled",
+    "unicode": "\ue65e"
+  },
+  {
+    "font_class": "clear",
+    "unicode": "\ue66d"
+  },
+  {
+    "font_class": "close",
+    "unicode": "\ue673"
+  },
+  {
+    "font_class": "closeempty",
+    "unicode": "\ue66c"
+  },
+  {
+    "font_class": "cloud-download",
+    "unicode": "\ue647"
+  },
+  {
+    "font_class": "cloud-download-filled",
+    "unicode": "\ue646"
+  },
+  {
+    "font_class": "cloud-upload",
+    "unicode": "\ue645"
+  },
+  {
+    "font_class": "cloud-upload-filled",
+    "unicode": "\ue648"
+  },
+  {
+    "font_class": "color",
+    "unicode": "\ue6cf"
+  },
+  {
+    "font_class": "color-filled",
+    "unicode": "\ue6c9"
+  },
+  {
+    "font_class": "compose",
+    "unicode": "\ue67f"
+  },
+  {
+    "font_class": "contact",
+    "unicode": "\ue693"
+  },
+  {
+    "font_class": "contact-filled",
+    "unicode": "\ue695"
+  },
+  {
+    "font_class": "down",
+    "unicode": "\ue6b8"
+  },
+	{
+	  "font_class": "bottom",
+	  "unicode": "\ue6b8"
+	},
+  {
+    "font_class": "download",
+    "unicode": "\ue68d"
+  },
+  {
+    "font_class": "download-filled",
+    "unicode": "\ue681"
+  },
+  {
+    "font_class": "email",
+    "unicode": "\ue69e"
+  },
+  {
+    "font_class": "email-filled",
+    "unicode": "\ue69a"
+  },
+  {
+    "font_class": "eye",
+    "unicode": "\ue651"
+  },
+  {
+    "font_class": "eye-filled",
+    "unicode": "\ue66a"
+  },
+  {
+    "font_class": "eye-slash",
+    "unicode": "\ue6b3"
+  },
+  {
+    "font_class": "eye-slash-filled",
+    "unicode": "\ue6b4"
+  },
+  {
+    "font_class": "fire",
+    "unicode": "\ue6a1"
+  },
+  {
+    "font_class": "fire-filled",
+    "unicode": "\ue6c5"
+  },
+  {
+    "font_class": "flag",
+    "unicode": "\ue65f"
+  },
+  {
+    "font_class": "flag-filled",
+    "unicode": "\ue660"
+  },
+  {
+    "font_class": "folder-add",
+    "unicode": "\ue6a9"
+  },
+  {
+    "font_class": "folder-add-filled",
+    "unicode": "\ue6c8"
+  },
+  {
+    "font_class": "font",
+    "unicode": "\ue6a3"
+  },
+  {
+    "font_class": "forward",
+    "unicode": "\ue6ba"
+  },
+  {
+    "font_class": "gear",
+    "unicode": "\ue664"
+  },
+  {
+    "font_class": "gear-filled",
+    "unicode": "\ue661"
+  },
+  {
+    "font_class": "gift",
+    "unicode": "\ue6a4"
+  },
+  {
+    "font_class": "gift-filled",
+    "unicode": "\ue6c4"
+  },
+  {
+    "font_class": "hand-down",
+    "unicode": "\ue63d"
+  },
+  {
+    "font_class": "hand-down-filled",
+    "unicode": "\ue63c"
+  },
+  {
+    "font_class": "hand-up",
+    "unicode": "\ue63f"
+  },
+  {
+    "font_class": "hand-up-filled",
+    "unicode": "\ue63e"
+  },
+  {
+    "font_class": "headphones",
+    "unicode": "\ue630"
+  },
+  {
+    "font_class": "heart",
+    "unicode": "\ue639"
+  },
+  {
+    "font_class": "heart-filled",
+    "unicode": "\ue641"
+  },
+  {
+    "font_class": "help",
+    "unicode": "\ue679"
+  },
+  {
+    "font_class": "help-filled",
+    "unicode": "\ue674"
+  },
+  {
+    "font_class": "home",
+    "unicode": "\ue662"
+  },
+  {
+    "font_class": "home-filled",
+    "unicode": "\ue663"
+  },
+  {
+    "font_class": "image",
+    "unicode": "\ue670"
+  },
+  {
+    "font_class": "image-filled",
+    "unicode": "\ue678"
+  },
+  {
+    "font_class": "images",
+    "unicode": "\ue650"
+  },
+  {
+    "font_class": "images-filled",
+    "unicode": "\ue64b"
+  },
+  {
+    "font_class": "info",
+    "unicode": "\ue669"
+  },
+  {
+    "font_class": "info-filled",
+    "unicode": "\ue649"
+  },
+  {
+    "font_class": "left",
+    "unicode": "\ue6b7"
+  },
+  {
+    "font_class": "link",
+    "unicode": "\ue6a5"
+  },
+  {
+    "font_class": "list",
+    "unicode": "\ue644"
+  },
+  {
+    "font_class": "location",
+    "unicode": "\ue6ae"
+  },
+  {
+    "font_class": "location-filled",
+    "unicode": "\ue6af"
+  },
+  {
+    "font_class": "locked",
+    "unicode": "\ue66b"
+  },
+  {
+    "font_class": "locked-filled",
+    "unicode": "\ue668"
+  },
+  {
+    "font_class": "loop",
+    "unicode": "\ue633"
+  },
+  {
+    "font_class": "mail-open",
+    "unicode": "\ue643"
+  },
+  {
+    "font_class": "mail-open-filled",
+    "unicode": "\ue63a"
+  },
+  {
+    "font_class": "map",
+    "unicode": "\ue667"
+  },
+  {
+    "font_class": "map-filled",
+    "unicode": "\ue666"
+  },
+  {
+    "font_class": "map-pin",
+    "unicode": "\ue6ad"
+  },
+  {
+    "font_class": "map-pin-ellipse",
+    "unicode": "\ue6ac"
+  },
+  {
+    "font_class": "medal",
+    "unicode": "\ue6a2"
+  },
+  {
+    "font_class": "medal-filled",
+    "unicode": "\ue6c3"
+  },
+  {
+    "font_class": "mic",
+    "unicode": "\ue671"
+  },
+  {
+    "font_class": "mic-filled",
+    "unicode": "\ue677"
+  },
+  {
+    "font_class": "micoff",
+    "unicode": "\ue67e"
+  },
+  {
+    "font_class": "micoff-filled",
+    "unicode": "\ue6b0"
+  },
+  {
+    "font_class": "minus",
+    "unicode": "\ue66f"
+  },
+  {
+    "font_class": "minus-filled",
+    "unicode": "\ue67d"
+  },
+  {
+    "font_class": "more",
+    "unicode": "\ue64d"
+  },
+  {
+    "font_class": "more-filled",
+    "unicode": "\ue64e"
+  },
+  {
+    "font_class": "navigate",
+    "unicode": "\ue66e"
+  },
+  {
+    "font_class": "navigate-filled",
+    "unicode": "\ue67a"
+  },
+  {
+    "font_class": "notification",
+    "unicode": "\ue6a6"
+  },
+  {
+    "font_class": "notification-filled",
+    "unicode": "\ue6c1"
+  },
+  {
+    "font_class": "paperclip",
+    "unicode": "\ue652"
+  },
+  {
+    "font_class": "paperplane",
+    "unicode": "\ue672"
+  },
+  {
+    "font_class": "paperplane-filled",
+    "unicode": "\ue675"
+  },
+  {
+    "font_class": "person",
+    "unicode": "\ue699"
+  },
+  {
+    "font_class": "person-filled",
+    "unicode": "\ue69d"
+  },
+  {
+    "font_class": "personadd",
+    "unicode": "\ue69f"
+  },
+  {
+    "font_class": "personadd-filled",
+    "unicode": "\ue698"
+  },
+  {
+    "font_class": "personadd-filled-copy",
+    "unicode": "\ue6d1"
+  },
+  {
+    "font_class": "phone",
+    "unicode": "\ue69c"
+  },
+  {
+    "font_class": "phone-filled",
+    "unicode": "\ue69b"
+  },
+  {
+    "font_class": "plus",
+    "unicode": "\ue676"
+  },
+  {
+    "font_class": "plus-filled",
+    "unicode": "\ue6c7"
+  },
+  {
+    "font_class": "plusempty",
+    "unicode": "\ue67b"
+  },
+  {
+    "font_class": "pulldown",
+    "unicode": "\ue632"
+  },
+  {
+    "font_class": "pyq",
+    "unicode": "\ue682"
+  },
+  {
+    "font_class": "qq",
+    "unicode": "\ue680"
+  },
+  {
+    "font_class": "redo",
+    "unicode": "\ue64a"
+  },
+  {
+    "font_class": "redo-filled",
+    "unicode": "\ue655"
+  },
+  {
+    "font_class": "refresh",
+    "unicode": "\ue657"
+  },
+  {
+    "font_class": "refresh-filled",
+    "unicode": "\ue656"
+  },
+  {
+    "font_class": "refreshempty",
+    "unicode": "\ue6bf"
+  },
+  {
+    "font_class": "reload",
+    "unicode": "\ue6b2"
+  },
+  {
+    "font_class": "right",
+    "unicode": "\ue6b5"
+  },
+  {
+    "font_class": "scan",
+    "unicode": "\ue62a"
+  },
+  {
+    "font_class": "search",
+    "unicode": "\ue654"
+  },
+  {
+    "font_class": "settings",
+    "unicode": "\ue653"
+  },
+  {
+    "font_class": "settings-filled",
+    "unicode": "\ue6ce"
+  },
+  {
+    "font_class": "shop",
+    "unicode": "\ue62f"
+  },
+  {
+    "font_class": "shop-filled",
+    "unicode": "\ue6cd"
+  },
+  {
+    "font_class": "smallcircle",
+    "unicode": "\ue67c"
+  },
+  {
+    "font_class": "smallcircle-filled",
+    "unicode": "\ue665"
+  },
+  {
+    "font_class": "sound",
+    "unicode": "\ue684"
+  },
+  {
+    "font_class": "sound-filled",
+    "unicode": "\ue686"
+  },
+  {
+    "font_class": "spinner-cycle",
+    "unicode": "\ue68a"
+  },
+  {
+    "font_class": "staff",
+    "unicode": "\ue6a7"
+  },
+  {
+    "font_class": "staff-filled",
+    "unicode": "\ue6cb"
+  },
+  {
+    "font_class": "star",
+    "unicode": "\ue688"
+  },
+  {
+    "font_class": "star-filled",
+    "unicode": "\ue68f"
+  },
+  {
+    "font_class": "starhalf",
+    "unicode": "\ue683"
+  },
+  {
+    "font_class": "trash",
+    "unicode": "\ue687"
+  },
+  {
+    "font_class": "trash-filled",
+    "unicode": "\ue685"
+  },
+  {
+    "font_class": "tune",
+    "unicode": "\ue6aa"
+  },
+  {
+    "font_class": "tune-filled",
+    "unicode": "\ue6ca"
+  },
+  {
+    "font_class": "undo",
+    "unicode": "\ue64f"
+  },
+  {
+    "font_class": "undo-filled",
+    "unicode": "\ue64c"
+  },
+  {
+    "font_class": "up",
+    "unicode": "\ue6b6"
+  },
+	{
+	  "font_class": "top",
+	  "unicode": "\ue6b6"
+	},
+  {
+    "font_class": "upload",
+    "unicode": "\ue690"
+  },
+  {
+    "font_class": "upload-filled",
+    "unicode": "\ue68e"
+  },
+  {
+    "font_class": "videocam",
+    "unicode": "\ue68c"
+  },
+  {
+    "font_class": "videocam-filled",
+    "unicode": "\ue689"
+  },
+  {
+    "font_class": "vip",
+    "unicode": "\ue6a8"
+  },
+  {
+    "font_class": "vip-filled",
+    "unicode": "\ue6c6"
+  },
+  {
+    "font_class": "wallet",
+    "unicode": "\ue6b1"
+  },
+  {
+    "font_class": "wallet-filled",
+    "unicode": "\ue6c2"
+  },
+  {
+    "font_class": "weibo",
+    "unicode": "\ue68b"
+  },
+  {
+    "font_class": "weixin",
+    "unicode": "\ue691"
+  }
+]
+
+// export const fontData = JSON.parse<IconsDataItem>(fontDataJson)

+ 89 - 0
src/uni_modules/uni-icons/package.json

@@ -0,0 +1,89 @@
+{
+  "id": "uni-icons",
+  "displayName": "uni-icons 图标",
+  "version": "2.0.10",
+  "description": "图标组件,用于展示移动端常见的图标,可自定义颜色、大小。",
+  "keywords": [
+    "uni-ui",
+    "uniui",
+    "icon",
+    "图标"
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": "^3.2.14"
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": ["uni-scss"],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y",
+        "alipay": "n"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y",
+          "app-uvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y",
+					"钉钉": "y",
+					"快手": "y",
+					"飞书": "y",
+					"京东": "y"
+        },
+        "快应用": {
+          "华为": "y",
+          "联盟": "y"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 8 - 0
src/uni_modules/uni-icons/readme.md

@@ -0,0 +1,8 @@
+## Icons 图标
+> **组件名:uni-icons**
+> 代码块: `uIcons`
+
+用于展示 icons 图标 。
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-icons)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 

+ 8 - 0
src/uni_modules/uni-scss/changelog.md

@@ -0,0 +1,8 @@
+## 1.0.3(2022-01-21)
+- 优化 组件示例
+## 1.0.2(2021-11-22)
+- 修复 / 符号在 vue 不同版本兼容问题引起的报错问题
+## 1.0.1(2021-11-22)
+- 修复 vue3中scss语法兼容问题
+## 1.0.0(2021-11-18)
+- init

+ 0 - 0
src/uni_modules/uni-scss/index.scss


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.