Jelajahi Sumber

Merge branch 'main' into main

# Conflicts:
#	.github/workflows/auto-merge.yml
#	pnpm-lock.yaml
Z.X.PING 10 bulan lalu
induk
melakukan
b3d9615159
56 mengubah file dengan 2665 tambahan dan 2251 penghapusan
  1. 0 103
      .commitlintrc.cjs
  2. 30 30
      .github/release.yml
  3. 44 0
      .github/workflows/auto-merge.yml
  4. 0 34
      .oxlintrc.json
  5. 2 0
      .prettierignore
  6. 63 10
      .vscode/settings.json
  7. 19 7
      .vscode/vue3.code-snippets
  8. 2 2
      README.md
  9. 22 0
      eslint.config.mjs
  10. 17 16
      manifest.config.ts
  11. 18 21
      package.json
  12. 3 25
      pages.config.ts
  13. 13 0
      patches/@dcloudio__uni-h5.patch
  14. 1871 1612
      pnpm-lock.yaml
  15. 2 0
      pnpm-workspace.yaml
  16. 1 2
      scripts/postupgrade.js
  17. 2 2
      src/App.vue
  18. 11 11
      src/api/login.ts
  19. 6 6
      src/api/login.typings.ts
  20. 2 2
      src/env.d.ts
  21. 4 3
      src/hooks/usePageAuth.ts
  22. 2 2
      src/hooks/useRequest.ts
  23. 53 49
      src/hooks/useUpload.ts
  24. 2 2
      src/interceptors/index.ts
  25. 4 3
      src/interceptors/prototype.ts
  26. 5 4
      src/interceptors/request.ts
  27. 4 3
      src/interceptors/route.ts
  28. 8 9
      src/layouts/default.vue
  29. 8 8
      src/layouts/demo.vue
  30. 67 0
      src/layouts/fg-tabbar/fg-tabbar.vue
  31. 16 0
      src/layouts/fg-tabbar/tabbar.md
  32. 11 0
      src/layouts/fg-tabbar/tabbar.ts
  33. 65 0
      src/layouts/fg-tabbar/tabbarList.ts
  34. 19 0
      src/layouts/tabbar.vue
  35. 3 3
      src/main.ts
  36. 10 6
      src/pages-sub/demo/index.vue
  37. 10 6
      src/pages.json
  38. 15 25
      src/pages/about/about.vue
  39. 47 30
      src/pages/about/components/request.vue
  40. 18 10
      src/pages/about/components/upload.vue
  41. 35 23
      src/pages/index/index.vue
  42. 6 5
      src/service/index/foo.ts
  43. 20 18
      src/store/user.ts
  44. 1 1
      src/style/index.scss
  45. 3 3
      src/typings.d.ts
  46. 1 1
      src/typings.ts
  47. 15 35
      src/utils/http.ts
  48. 13 49
      src/utils/index.ts
  49. 11 9
      src/utils/request.ts
  50. 2 2
      src/utils/toast.ts
  51. 10 16
      src/utils/uploadFile.ts
  52. 12 12
      tsconfig.json
  53. 3 2
      uno.config.ts
  54. 4 2
      vite-plugins/copyNativeRes.ts
  55. 10 7
      vite-plugins/updatePackageJson.ts
  56. 20 20
      vite.config.ts

+ 0 - 103
.commitlintrc.cjs

@@ -1,106 +1,3 @@
-const fs = require('fs')
-const path = require('path')
-const { execSync } = require('child_process')
-
-const scopes = fs
-  .readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true })
-  .filter((dirent) => dirent.isDirectory())
-  .map((dirent) => dirent.name.replace(/s$/, ''))
-
-// precomputed scope
-const scopeComplete = execSync('git status --porcelain || true')
-  .toString()
-  .trim()
-  .split('\n')
-  .find((r) => ~r.indexOf('M  src'))
-  ?.replace(/(\/)/g, '%%')
-  ?.match(/src%%((\w|-)*)/)?.[1]
-  ?.replace(/s$/, '')
-
 module.exports = {
-  ignores: [(commit) => commit.includes('init')],
   extends: ['@commitlint/config-conventional'],
-  rules: {
-    'body-leading-blank': [2, 'always'],
-    'footer-leading-blank': [1, 'always'],
-    'header-max-length': [2, 'always', 108],
-    'subject-empty': [2, 'never'],
-    'type-empty': [2, 'never'],
-    'subject-case': [0],
-    'type-enum': [
-      2,
-      'always',
-      [
-        'feat',
-        'fix',
-        'perf',
-        'style',
-        'docs',
-        'test',
-        'refactor',
-        'build',
-        'ci',
-        'chore',
-        'revert',
-        'wip',
-        'workflow',
-        'types',
-        'release',
-      ],
-    ],
-  },
-  prompt: {
-    /** @use `pnpm commit :f` */
-    alias: {
-      f: 'docs: fix typos',
-      r: 'docs: update README',
-      s: 'style: update code format',
-      b: 'build: bump dependencies',
-      c: 'chore: update config',
-    },
-    customScopesAlign: !scopeComplete ? 'top' : 'bottom',
-    defaultScope: scopeComplete,
-    scopes: [...scopes, 'mock'],
-    allowEmptyIssuePrefixs: false,
-    allowCustomIssuePrefixs: false,
-
-    // English
-    typesAppend: [
-      { value: 'wip', name: 'wip:      work in process' },
-      { value: 'workflow', name: 'workflow: workflow improvements' },
-      { value: 'types', name: 'types:    type definition file changes' },
-    ],
-
-    // 中英文对照版
-    // messages: {
-    //   type: '选择你要提交的类型 :',
-    //   scope: '选择一个提交范围 (可选):',
-    //   customScope: '请输入自定义的提交范围 :',
-    //   subject: '填写简短精炼的变更描述 :\n',
-    //   body: '填写更加详细的变更描述 (可选)。使用 "|" 换行 :\n',
-    //   breaking: '列举非兼容性重大的变更 (可选)。使用 "|" 换行 :\n',
-    //   footerPrefixsSelect: '选择关联issue前缀 (可选):',
-    //   customFooterPrefixs: '输入自定义issue前缀 :',
-    //   footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
-    //   confirmCommit: '是否提交或修改commit ?',
-    // },
-    // types: [
-    //   { value: 'feat', name: 'feat:     新增功能' },
-    //   { value: 'fix', name: 'fix:      修复缺陷' },
-    //   { value: 'docs', name: 'docs:     文档变更' },
-    //   { value: 'style', name: 'style:    代码格式' },
-    //   { value: 'refactor', name: 'refactor: 代码重构' },
-    //   { value: 'perf', name: 'perf:     性能优化' },
-    //   { value: 'test', name: 'test:     添加疏漏测试或已有测试改动' },
-    //   { value: 'build', name: 'build:    构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
-    //   { value: 'ci', name: 'ci:       修改 CI 配置、脚本' },
-    //   { value: 'revert', name: 'revert:   回滚 commit' },
-    //   { value: 'chore', name: 'chore:    对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
-    //   { value: 'wip', name: 'wip:      正在开发中' },
-    //   { value: 'workflow', name: 'workflow: 工作流程改进' },
-    //   { value: 'types', name: 'types:    类型定义文件修改' },
-    // ],
-    // emptyScopesAlias: 'empty:      不填写',
-    // customScopesAlias: 'custom:     自定义',
-  },
 }

+ 30 - 30
.github/release.yml

@@ -1,31 +1,31 @@
 categories:
-  - title: '🚀 新功能'
-    labels: ['feat', 'feature']
-  - title: '🛠️ 修复'
-    labels: ['fix', 'bugfix']
-  - title: '💅 样式'
-    labels: ['style']
-  - title: '📄 文档'
-    labels: ['docs']
-  - title: '⚡️ 性能'
-    labels: ['perf']
-  - title: '🧪 测试'
-    labels: ['test']
-  - title: '♻️ 重构'
-    labels: ['refactor']
-  - title: '📦 构建'
-    labels: ['build']
-  - title: '🚨 补丁'
-    labels: ['patch', 'hotfix']
-  - title: '🌐 发布'
-    labels: ['release', 'publish']
-  - title: '🔧 流程'
-    labels: ['ci', 'cd', 'workflow']
-  - title: '⚙️ 配置'
-    labels: ['config', 'chore']
-  - title: '📁 文件'
-    labels: ['file']
-  - title: '🎨 格式化'
-    labels: ['format']
-  - title: '🔀 其他'
-    labels: ['other', 'misc']
+  - title: 🚀 新功能
+    labels: [feat, feature]
+  - title: 🛠️ 修复
+    labels: [fix, bugfix]
+  - title: 💅 样式
+    labels: [style]
+  - title: 📄 文档
+    labels: [docs]
+  - title: ⚡️ 性能
+    labels: [perf]
+  - title: 🧪 测试
+    labels: [test]
+  - title: ♻️ 重构
+    labels: [refactor]
+  - title: 📦 构建
+    labels: [build]
+  - title: 🚨 补丁
+    labels: [patch, hotfix]
+  - title: 🌐 发布
+    labels: [release, publish]
+  - title: 🔧 流程
+    labels: [ci, cd, workflow]
+  - title: ⚙️ 配置
+    labels: [config, chore]
+  - title: 📁 文件
+    labels: [file]
+  - title: 🎨 格式化
+    labels: [format]
+  - title: 🔀 其他
+    labels: [other, misc]

+ 44 - 0
.github/workflows/auto-merge.yml

@@ -0,0 +1,44 @@
+name: Auto Merge Main to Other Branches
+
+on:
+  push:
+    branches:
+      - main
+  workflow_dispatch: # 手动触发
+
+jobs:
+  merge-to-i18n:
+    name: Merge main into i18n
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
+
+      - name: Merge main into i18n
+        run: |
+          git config user.name "GitHub Actions"
+          git config user.email "actions@github.com"
+          git checkout i18n
+          git merge main --no-ff -m "Auto merge main into i18n"
+          git push origin i18n
+
+  merge-to-base-sard-ui:
+    name: Merge main into base-sard-ui
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+          token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
+
+      - name: Merge main into base-sard-ui
+        run: |
+          git config user.name "GitHub Actions"
+          git config user.email "actions@github.com"
+          git checkout base-sard-ui
+          git merge main --no-ff -m "Auto merge main into base-sard-ui"
+          git push origin base-sard-ui

+ 0 - 34
.oxlintrc.json

@@ -1,34 +0,0 @@
-{
-  "$schema": "./node_modules/oxlint/configuration_schema.json",
-  "extends": ["config:recommended"],
-  "plugins": ["import", "typescript", "unicorn"],
-  "rules": {
-    "no-console": "off",
-    "no-unused-vars": "off"
-  },
-  "env": {
-    "es6": true
-  },
-  "globals": {
-    "foo": "readonly"
-  },
-  "ignorePatterns": [
-    "node_modules",
-    "dist",
-    "src/static/**",
-    "src/uni_modules/**",
-    "vite.config.ts",
-    "uno.config.ts",
-    "pages.config.ts",
-    "manifest.config.ts"
-  ],
-  "settings": {},
-  "overrides": [
-    {
-      "files": ["*.test.ts", "*.spec.ts"],
-      "rules": {
-        "@typescript-eslint/no-explicit-any": "off"
-      }
-    }
-  ]
-}

+ 2 - 0
.prettierignore

@@ -1,3 +1,5 @@
+node_modules
+
 # unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
 auto-import.d.ts
 

+ 63 - 10
.vscode/settings.json

@@ -1,14 +1,7 @@
 {
   // 默认格式化工具选择prettier
   "editor.defaultFormatter": "esbenp.prettier-vscode",
-  // 保存的时候自动格式化
-  "editor.formatOnSave": true,
-  //开启自动修复
-  "editor.codeActionsOnSave": {
-    "source.fixAll": "explicit",
-    "source.fixAll.eslint": "explicit",
-    "source.fixAll.stylelint": "explicit"
-  },
+
   // 配置stylelint检查的文件类型范围
   "stylelint.validate": ["css", "scss", "vue", "html"], // 与package.json的scripts对应
   "stylelint.enable": true,
@@ -62,6 +55,66 @@
     "README.md": "index.html,favicon.ico,robots.txt,CHANGELOG.md",
     "pages.config.ts": "manifest.config.ts,openapi-ts-request.config.ts",
     "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
-    ".oxlintrc.json": "tsconfig.json,.commitlintrc.*,.prettier*,.editorconfig,.commitlint.cjs,.eslint*"
-  }
+    "eslint.config.mjs": "tsconfig.json,.commitlintrc.*,.prettier*,.editorconfig,.commitlint.cjs,.eslint*"
+  },
+
+  // // 保存的时候自动格式化
+  // "prettier.enable": true,
+  // "editor.formatOnSave": true,
+  // // 开启自动修复
+  // "editor.codeActionsOnSave": {
+  //   "source.fixAll": "explicit",
+  //   "source.fixAll.eslint": "explicit",
+  //   "source.fixAll.stylelint": "explicit"
+  // },
+
+  // Disable the default formatter, use eslint instead
+  "prettier.enable": false,
+  "editor.formatOnSave": false,
+
+  // Auto fix
+  "editor.codeActionsOnSave": {
+    "source.fixAll.eslint": "explicit",
+    "source.organizeImports": "never"
+  },
+
+  // Silent the stylistic rules in you IDE, but still auto fix them
+  "eslint.rules.customizations": [
+    { "rule": "style/*", "severity": "off", "fixable": true },
+    { "rule": "format/*", "severity": "off", "fixable": true },
+    { "rule": "*-indent", "severity": "off", "fixable": true },
+    { "rule": "*-spacing", "severity": "off", "fixable": true },
+    { "rule": "*-spaces", "severity": "off", "fixable": true },
+    { "rule": "*-order", "severity": "off", "fixable": true },
+    { "rule": "*-dangle", "severity": "off", "fixable": true },
+    { "rule": "*-newline", "severity": "off", "fixable": true },
+    { "rule": "*quotes", "severity": "off", "fixable": true },
+    { "rule": "*semi", "severity": "off", "fixable": true }
+  ],
+
+  // Enable eslint for all supported languages
+  "eslint.validate": [
+    "javascript",
+    "javascriptreact",
+    "typescript",
+    "typescriptreact",
+    "vue",
+    "html",
+    "markdown",
+    "json",
+    "json5",
+    "jsonc",
+    "yaml",
+    "toml",
+    "xml",
+    "gql",
+    "graphql",
+    "astro",
+    "svelte",
+    "css",
+    "less",
+    "scss",
+    "pcss",
+    "postcss"
+  ]
 }

+ 19 - 7
.vscode/vue3.code-snippets

@@ -27,12 +27,12 @@
       "  },",
       "}",
       "</route>\n",
-      "<template>",
-      "  <view class=\"\">$2</view>",
-      "</template>\n",
       "<script lang=\"ts\" setup>",
       "//$3",
       "</script>\n",
+      "<template>",
+      "  <view class=\"\">$2</view>",
+      "</template>\n",
       "<style lang=\"scss\" scoped>",
       "//$4",
       "</style>\n",
@@ -41,16 +41,28 @@
   "Print unibest style": {
     "scope": "vue",
     "prefix": "st",
-    "body": ["<style lang=\"scss\" scoped>", "//", "</style>\n"],
+    "body": [
+      "<style lang=\"scss\" scoped>",
+      "//",
+      "</style>\n"
+    ],
   },
   "Print unibest script": {
     "scope": "vue",
     "prefix": "sc",
-    "body": ["<script lang=\"ts\" setup>", "//$3", "</script>\n"],
+    "body": [
+      "<script lang=\"ts\" setup>",
+      "//$3",
+      "</script>\n"
+    ],
   },
   "Print unibest template": {
     "scope": "vue",
     "prefix": "te",
-    "body": ["<template>", "  <view class=\"\">$1</view>", "</template>\n"],
+    "body": [
+      "<template>",
+      "  <view class=\"\">$1</view>",
+      "</template>\n"
+    ],
   },
-}
+}

+ 2 - 2
README.md

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

+ 22 - 0
eslint.config.mjs

@@ -0,0 +1,22 @@
+import uniHelper from '@uni-helper/eslint-config'
+
+export default uniHelper({
+  unocss: true,
+  vue: true,
+  markdown: false,
+  ignores: [
+    'src/uni_modules/',
+    'dist',
+  ],
+  rules: {
+    'no-console': 'off',
+    'no-unused-vars': 'off',
+    'vue/no-unused-refs': 'off',
+    'unused-imports/no-unused-vars': 'off',
+    'eslint-comments/no-unlimited-disable': 'off',
+    'jsdoc/check-param-names': 'off',
+    'jsdoc/require-returns-description': 'off',
+    'ts/no-empty-object-type': 'off',
+    'no-extend-native': 'off',
+  },
+})

+ 17 - 16
manifest.config.ts

@@ -1,6 +1,7 @@
+import path from 'node:path'
+import process from 'node:process'
 // manifest.config.ts
 import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
-import path from 'node:path'
 import { loadEnv } from 'vite'
 
 // 获取环境变量的范例
@@ -14,14 +15,14 @@ const {
 } = env
 
 export default defineManifestConfig({
-  name: VITE_APP_TITLE,
-  appid: VITE_UNI_APPID,
-  description: '',
-  versionName: '1.0.0',
-  versionCode: '100',
-  transformPx: false,
-  locale: VITE_FALLBACK_LOCALE, // 'zh-Hans'
-  h5: {
+  'name': VITE_APP_TITLE,
+  'appid': VITE_UNI_APPID,
+  'description': '',
+  'versionName': '1.0.0',
+  'versionCode': '100',
+  'transformPx': false,
+  'locale': VITE_FALLBACK_LOCALE, // 'zh-Hans'
+  'h5': {
     router: {
       base: VITE_APP_PUBLIC_BASE,
     },
@@ -82,14 +83,14 @@ export default defineManifestConfig({
         ios: {
           appstore: 'static/app/icons/1024x1024.png',
           ipad: {
-            app: 'static/app/icons/76x76.png',
+            'app': 'static/app/icons/76x76.png',
             'app@2x': 'static/app/icons/152x152.png',
-            notification: 'static/app/icons/20x20.png',
+            'notification': 'static/app/icons/20x20.png',
             'notification@2x': 'static/app/icons/40x40.png',
             'proapp@2x': 'static/app/icons/167x167.png',
-            settings: 'static/app/icons/29x29.png',
+            'settings': 'static/app/icons/29x29.png',
             'settings@2x': 'static/app/icons/58x58.png',
-            spotlight: 'static/app/icons/40x40.png',
+            'spotlight': 'static/app/icons/40x40.png',
             'spotlight@2x': 'static/app/icons/80x80.png',
           },
           iphone: {
@@ -107,7 +108,7 @@ export default defineManifestConfig({
     },
   },
   /* 快应用特有相关 */
-  quickapp: {},
+  'quickapp': {},
   /* 小程序特有相关 */
   'mp-weixin': {
     appid: VITE_WX_APPID,
@@ -130,8 +131,8 @@ export default defineManifestConfig({
   'mp-toutiao': {
     usingComponents: true,
   },
-  uniStatistics: {
+  'uniStatistics': {
     enable: false,
   },
-  vueVersion: '3',
+  'vueVersion': '3',
 })

+ 18 - 21
package.json

@@ -1,9 +1,9 @@
 {
   "name": "unibest",
   "type": "commonjs",
-  "version": "2.13.1",
+  "version": "3.0.0",
   "description": "unibest - 最好的 uniapp 开发模板",
-  "update-time": "2025-06-17",
+  "update-time": "2025-06-21",
   "author": {
     "name": "feige996",
     "zhName": "菲鸽",
@@ -11,8 +11,8 @@
     "github": "https://github.com/feige996",
     "gitee": "https://gitee.com/feige996"
   },
-  "homepage": "https://unibest.tech",
   "license": "MIT",
+  "homepage": "https://unibest.tech",
   "repository": "https://github.com/feige996/unibest",
   "repository-gitee": "https://gitee.com/feige996/unibest",
   "repository-old": "https://github.com/codercup/unibest",
@@ -75,21 +75,8 @@
     "type-check": "vue-tsc --noEmit",
     "openapi-ts-request": "openapi-ts",
     "prepare": "git init && husky",
-    "lint": "oxlint",
-    "lint-fix": "oxlint --fix"
-  },
-  "lint-staged": {
-    "**/*.{html,cjs,json,md,scss,css,txt}": [
-      "prettier --write --cache"
-    ],
-    "**/*.{js,jsx,ts,tsx,vue,mjs,cjs,mts,cts}": [
-      "oxlint --fix",
-      "prettier --write --cache"
-    ],
-    "!**/{node_modules,dist}/**": []
-  },
-  "resolutions": {
-    "bin-wrapper": "npm:bin-wrapper-china"
+    "lint": "eslint",
+    "lint:fix": "eslint --fix"
   },
   "dependencies": {
     "@dcloudio/uni-app": "3.0.0-4060620250520001",
@@ -121,6 +108,7 @@
     "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",
@@ -128,12 +116,13 @@
     "@dcloudio/uni-cli-shared": "3.0.0-4060620250520001",
     "@dcloudio/uni-stacktracey": "3.0.0-4060620250520001",
     "@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001",
-    "@esbuild/darwin-arm64": "0.25.5",
-    "@esbuild/darwin-x64": "0.25.5",
+    "@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/unocss-preset-uni": "^0.2.11",
     "@uni-helper/vite-plugin-uni-components": "0.2.0",
@@ -142,17 +131,19 @@
     "@uni-helper/vite-plugin-uni-pages": "0.2.28",
     "@uni-helper/vite-plugin-uni-platform": "0.0.4",
     "@uni-ku/bundle-optimizer": "^1.3.3",
+    "@unocss/eslint-plugin": "^66.2.3",
     "@unocss/preset-legacy-compat": "^0.59.4",
     "@vue/runtime-core": "^3.4.21",
     "@vue/tsconfig": "^0.1.3",
     "autoprefixer": "^10.4.20",
+    "eslint": "^9.29.0",
     "husky": "^9.1.7",
     "lint-staged": "^15.2.10",
     "openapi-ts-request": "^1.1.2",
-    "oxlint": "1.0.0",
     "postcss": "^8.4.49",
     "postcss-html": "^1.7.0",
     "postcss-scss": "^4.0.9",
+    "prettier": "^3.5.3",
     "rollup-plugin-visualizer": "^5.12.0",
     "sass": "1.77.8",
     "terser": "^5.36.0",
@@ -164,5 +155,11 @@
     "vitepress": "^1.5.0",
     "vitepress-plugin-llms": "^1.3.4",
     "vue-tsc": "^2.2.10"
+  },
+  "resolutions": {
+    "bin-wrapper": "npm:bin-wrapper-china"
+  },
+  "lint-staged": {
+    "*": "eslint --fix"
   }
 }

+ 3 - 25
pages.config.ts

@@ -1,4 +1,5 @@
 import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
+import { tabBar } from './src/layouts/fg-tabbar/tabbarList'
 
 export default defineUniPages({
   globalStyle: {
@@ -17,29 +18,6 @@ export default defineUniPages({
         'z-paging/components/z-paging$1/z-paging$1.vue',
     },
   },
-  // 如果不需要tabBar,推荐使用 spa 模板。(pnpm create xxx -t spa)
-  tabBar: {
-    color: '#999999',
-    selectedColor: '#018d71',
-    backgroundColor: '#F8F8F8',
-    borderStyle: 'black',
-    height: '50px',
-    fontSize: '10px',
-    iconWidth: '24px',
-    spacing: '3px',
-    list: [
-      {
-        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: '关于',
-      },
-    ],
-  },
+  // tabbar 的配置统一在 “./src/layouts/fg-tabbar/tabbarList.ts” 文件中
+  tabBar: tabBar as any,
 })

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

@@ -0,0 +1,13 @@
+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;
+ }

File diff ditekan karena terlalu besar
+ 1871 - 1612
pnpm-lock.yaml


+ 2 - 0
pnpm-workspace.yaml

@@ -0,0 +1,2 @@
+patchedDependencies:
+  '@dcloudio/uni-h5': patches/@dcloudio__uni-h5.patch

+ 1 - 2
scripts/postupgrade.js

@@ -2,8 +2,7 @@
 // # 在升级完后,会自动添加很多无用依赖,这需要删除以减小依赖包体积
 // # 只需要执行下面的命令即可
 
-// eslint-disable-next-line @typescript-eslint/no-var-requires
-const { exec } = require('child_process')
+const { exec } = require('node:child_process')
 
 // 定义要执行的命令
 const dependencies = [

+ 2 - 2
src/App.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
-import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
-import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
+import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
 import { usePageAuth } from '@/hooks/usePageAuth'
+import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
 
 usePageAuth()
 

+ 11 - 11
src/api/login.ts

@@ -1,4 +1,4 @@
-import { ICaptcha, IUpdateInfo, IUpdatePassword, IUserInfoVo, IUserLogin } from './login.typings'
+import type { ICaptcha, IUpdateInfo, IUpdatePassword, IUserInfoVo, IUserLogin } from './login.typings'
 import { http } from '@/utils/http'
 
 /**
@@ -15,7 +15,7 @@ export interface ILoginForm {
  * 获取验证码
  * @returns ICaptcha 验证码
  */
-export const getCode = () => {
+export function getCode() {
   return http.get<ICaptcha>('/user/getCode')
 }
 
@@ -23,35 +23,35 @@ export const getCode = () => {
  * 用户登录
  * @param loginForm 登录表单
  */
-export const login = (loginForm: ILoginForm) => {
+export function login(loginForm: ILoginForm) {
   return http.post<IUserLogin>('/user/login', loginForm)
 }
 
 /**
  * 获取用户信息
  */
-export const getUserInfo = () => {
+export function getUserInfo() {
   return http.get<IUserInfoVo>('/user/info')
 }
 
 /**
  * 退出登录
  */
-export const logout = () => {
+export function logout() {
   return http.get<void>('/user/logout')
 }
 
 /**
  * 修改用户信息
  */
-export const updateInfo = (data: IUpdateInfo) => {
+export function updateInfo(data: IUpdateInfo) {
   return http.post('/user/updateInfo', data)
 }
 
 /**
  * 修改用户密码
  */
-export const updateUserPassword = (data: IUpdatePassword) => {
+export function updateUserPassword(data: IUpdatePassword) {
   return http.post('/user/updatePassword', data)
 }
 
@@ -59,12 +59,12 @@ export const updateUserPassword = (data: IUpdatePassword) => {
  * 获取微信登录凭证
  * @returns Promise 包含微信登录凭证(code)
  */
-export const getWxCode = () => {
+export function getWxCode() {
   return new Promise<UniApp.LoginRes>((resolve, reject) => {
     uni.login({
       provider: 'weixin',
-      success: (res) => resolve(res),
-      fail: (err) => reject(new Error(err)),
+      success: res => resolve(res),
+      fail: err => reject(new Error(err)),
     })
   })
 }
@@ -78,6 +78,6 @@ export const getWxCode = () => {
  * @param params 微信登录参数,包含code
  * @returns Promise 包含登录结果
  */
-export const wxLogin = (data: { code: string }) => {
+export function wxLogin(data: { code: string }) {
   return http.post<IUserLogin>('/user/wxLogin', data)
 }

+ 6 - 6
src/api/login.typings.ts

@@ -1,7 +1,7 @@
 /**
  * 用户信息
  */
-export type IUserInfoVo = {
+export interface IUserInfoVo {
   id: number
   username: string
   avatar: string
@@ -11,7 +11,7 @@ export type IUserInfoVo = {
 /**
  * 登录返回的信息
  */
-export type IUserLogin = {
+export interface IUserLogin {
   id: string
   username: string
   token: string
@@ -20,7 +20,7 @@ export type IUserLogin = {
 /**
  * 获取验证码
  */
-export type ICaptcha = {
+export interface ICaptcha {
   captchaEnabled: boolean
   uuid: string
   image: string
@@ -28,7 +28,7 @@ export type ICaptcha = {
 /**
  * 上传成功的信息
  */
-export type IUploadSuccessInfo = {
+export interface IUploadSuccessInfo {
   fileId: number
   originalName: string
   fileName: string
@@ -41,7 +41,7 @@ export type IUploadSuccessInfo = {
 /**
  * 更新用户信息
  */
-export type IUpdateInfo = {
+export interface IUpdateInfo {
   id: number
   name: string
   sex: string
@@ -49,7 +49,7 @@ export type IUpdateInfo = {
 /**
  * 更新用户信息
  */
-export type IUpdatePassword = {
+export interface IUpdatePassword {
   id: number
   oldPassword: string
   newPassword: string

+ 2 - 2
src/env.d.ts

@@ -2,8 +2,8 @@
 /// <reference types="vite-svg-loader" />
 
 declare module '*.vue' {
-  import { DefineComponent } from 'vue'
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+  import type { DefineComponent } from 'vue'
+
   const component: DefineComponent<{}, {}, any>
   export default component
 }

+ 4 - 3
src/hooks/usePageAuth.ts

@@ -1,10 +1,10 @@
 import { onLoad } from '@dcloudio/uni-app'
-import { needLoginPages as _needLoginPages, getNeedLoginPages } from '@/utils'
 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
-const isLogined = () => {
+function isLogined() {
   const userStore = useUserStore()
   return !!userStore.userInfo.username
 }
@@ -20,7 +20,8 @@ export function usePageAuth() {
     let needLoginPages: string[] = []
     if (isDev) {
       needLoginPages = getNeedLoginPages()
-    } else {
+    }
+    else {
       needLoginPages = _needLoginPages
     }
 

+ 2 - 2
src/hooks/useRequest.ts

@@ -1,6 +1,6 @@
-import { UnwrapRef } from 'vue'
+import type { UnwrapRef } from 'vue'
 
-type IUseRequestOptions<T> = {
+interface IUseRequestOptions<T> {
   /** 是否立即执行 */
   immediate?: boolean
   /** 初始化数据 */

+ 53 - 49
src/hooks/useUpload.ts

@@ -7,7 +7,7 @@ type TfileType = 'image' | 'file'
 type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
 type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
 
-type TOptions<T extends TfileType> = {
+interface TOptions<T extends TfileType> {
   formData?: Record<string, any>
   maxSize?: number
   accept?: T extends 'image' ? TImage[] : TFile[]
@@ -30,6 +30,46 @@ export default function useUpload<T extends TfileType>(options: TOptions<T> = {}
   const error = ref<Error | null>(null)
   const data = ref<any>(null)
 
+  const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => {
+    if (size > maxSize) {
+      uni.showToast({
+        title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`,
+        icon: 'none',
+      })
+      return
+    }
+
+    // const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase()
+    // const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension)
+
+    // if (!isTypeValid) {
+    //   uni.showToast({
+    //     title: `仅支持 ${accept.join(', ')} 格式的文件`,
+    //     icon: 'none',
+    //   })
+    //   return
+    // }
+
+    loading.value = true
+    uploadFile({
+      tempFilePath,
+      formData,
+      onSuccess: (res) => {
+        const { data: _data } = JSON.parse(res)
+        data.value = _data
+        // console.log('上传成功', res)
+        success?.(_data)
+      },
+      onError: (err) => {
+        error.value = err
+        onError?.(err)
+      },
+      onComplete: () => {
+        loading.value = false
+      },
+    })
+  }
+
   const run = () => {
     // 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
     // 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议
@@ -37,19 +77,21 @@ export default function useUpload<T extends TfileType>(options: TOptions<T> = {}
       count: 1,
       success: (res: any) => {
         console.log('File selected successfully:', res)
-        // h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
-        // h5的File有一下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
         // 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
+        // h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
+        // h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
+        // App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
+        // App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
         let tempFilePath = ''
         let size = 0
-        // #ifdef H5
-        tempFilePath = res.tempFilePaths[0]
-        size = res.tempFiles[0].size
-        // #endif
         // #ifdef MP-WEIXIN
         tempFilePath = res.tempFiles[0].tempFilePath
         size = res.tempFiles[0].size
         // #endif
+        // #ifndef MP-WEIXIN
+        tempFilePath = res.tempFilePaths[0]
+        size = res.tempFiles[0].size
+        // #endif
         handleFileChoose({ tempFilePath, size })
       },
       fail: (err: any) => {
@@ -70,7 +112,8 @@ export default function useUpload<T extends TfileType>(options: TOptions<T> = {}
       // #ifndef MP-WEIXIN
       uni.chooseImage(chooseFileOptions)
       // #endif
-    } else {
+    }
+    else {
       uni.chooseFile({
         ...chooseFileOptions,
         type: 'all',
@@ -78,46 +121,6 @@ export default function useUpload<T extends TfileType>(options: TOptions<T> = {}
     }
   }
 
-  const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string; size: number }) => {
-    if (size > maxSize) {
-      uni.showToast({
-        title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`,
-        icon: 'none',
-      })
-      return
-    }
-
-    // const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase()
-    // const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension)
-
-    // if (!isTypeValid) {
-    //   uni.showToast({
-    //     title: `仅支持 ${accept.join(', ')} 格式的文件`,
-    //     icon: 'none',
-    //   })
-    //   return
-    // }
-
-    loading.value = true
-    uploadFile({
-      tempFilePath: tempFilePath,
-      formData,
-      onSuccess: (res) => {
-        const { data: _data } = JSON.parse(res)
-        data.value = _data
-        // console.log('上传成功', res)
-        success?.(_data)
-      },
-      onError: (err) => {
-        error.value = err
-        onError?.(err)
-      },
-      onComplete: () => {
-        loading.value = false
-      },
-    })
-  }
-
   return { loading, error, data, run }
 }
 
@@ -143,7 +146,8 @@ async function uploadFile({
       try {
         const data = uploadFileRes.data
         onSuccess(data)
-      } catch (err) {
+      }
+      catch (err) {
         onError(err)
       }
     },

+ 2 - 2
src/interceptors/index.ts

@@ -1,3 +1,3 @@
-export { routeInterceptor } from './route'
-export { requestInterceptor } from './request'
 export { prototypeInterceptor } from './prototype'
+export { requestInterceptor } from './request'
+export { routeInterceptor } from './route'

+ 4 - 3
src/interceptors/prototype.ts

@@ -2,10 +2,11 @@ export const prototypeInterceptor = {
   install() {
     // 解决低版本手机不识别 array.at() 导致运行报错的问题
     if (typeof Array.prototype.at !== 'function') {
-      // eslint-disable-next-line no-extend-native
       Array.prototype.at = function (index: number) {
-        if (index < 0) return this[this.length + index]
-        if (index >= this.length) return undefined
+        if (index < 0)
+          return this[this.length + index]
+        if (index >= this.length)
+          return undefined
         return this[index]
       }
     }

+ 5 - 4
src/interceptors/request.ts

@@ -1,8 +1,7 @@
-/* eslint-disable no-param-reassign */
 import qs from 'qs'
 import { useUserStore } from '@/store'
-import { platform } from '@/utils/platform'
 import { getEnvBaseUrl } from '@/utils'
+import { platform } from '@/utils/platform'
 
 export type CustomRequestOptions = UniApp.RequestOptions & {
   query?: Record<string, any>
@@ -22,7 +21,8 @@ const httpInterceptor = {
       const queryStr = qs.stringify(options.query)
       if (options.url.includes('?')) {
         options.url += `&${queryStr}`
-      } else {
+      }
+      else {
         options.url += `?${queryStr}`
       }
     }
@@ -33,7 +33,8 @@ const httpInterceptor = {
       if (JSON.parse(__VITE_APP_PROXY__)) {
         // 自动拼接代理前缀
         options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url
-      } else {
+      }
+      else {
         options.url = baseUrl + options.url
       }
       // #endif

+ 4 - 3
src/interceptors/route.ts

@@ -5,12 +5,12 @@
  * 我这里应为大部分都可以随便进入,所以使用黑名单
  */
 import { useUserStore } from '@/store'
-import { needLoginPages as _needLoginPages, getNeedLoginPages, getLastPage } from '@/utils'
+import { needLoginPages as _needLoginPages, getLastPage, getNeedLoginPages } from '@/utils'
 
 // TODO Check
 const loginRoute = import.meta.env.VITE_LOGIN_URL
 
-const isLogined = () => {
+function isLogined() {
   const userStore = useUserStore()
   return !!userStore.userInfo.username
 }
@@ -37,7 +37,8 @@ const navigateToInterceptor = {
     // 为了防止开发时出现BUG,这里每次都获取一下。生产环境可以移到函数外,性能更好
     if (isDev) {
       needLoginPages = getNeedLoginPages()
-    } else {
+    }
+    else {
       needLoginPages = _needLoginPages
     }
     const isNeedLogin = needLoginPages.includes(path)

+ 8 - 9
src/layouts/default.vue

@@ -1,12 +1,3 @@
-<template>
-  <wd-config-provider :themeVars="themeVars">
-    <slot />
-    <wd-toast />
-    <wd-message-box />
-    <privacy-popup />
-  </wd-config-provider>
-</template>
-
 <script lang="ts" setup>
 import type { ConfigProviderThemeVars } from 'wot-design-uni'
 
@@ -16,3 +7,11 @@ const themeVars: ConfigProviderThemeVars = {
   // buttonPrimaryColor: '#07c160',
 }
 </script>
+
+<template>
+  <wd-config-provider :theme-vars="themeVars">
+    <slot />
+    <wd-toast />
+    <wd-message-box />
+  </wd-config-provider>
+</template>

+ 8 - 8
src/layouts/demo.vue

@@ -1,11 +1,3 @@
-<template>
-  <wd-config-provider :themeVars="themeVars">
-    <slot />
-    <wd-toast />
-    <wd-message-box />
-  </wd-config-provider>
-</template>
-
 <script lang="ts" setup>
 import type { ConfigProviderThemeVars } from 'wot-design-uni'
 
@@ -15,3 +7,11 @@ const themeVars: ConfigProviderThemeVars = {
   // buttonPrimaryColor: '#07c160',
 }
 </script>
+
+<template>
+  <wd-config-provider :theme-vars="themeVars">
+    <slot />
+    <wd-toast />
+    <wd-message-box />
+  </wd-config-provider>
+</template>

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

@@ -0,0 +1,67 @@
+<script setup lang="ts">
+import { tabbarStore } from './tabbar'
+// 'i-carbon-code',
+import { tabbarList as _tabBarList, cacheTabbarEnable, selectedTabbarStrategy } from './tabbarList'
+
+// @ts-expect-error 预知的判断
+const customTabbarEnable = selectedTabbarStrategy === 1 || selectedTabbarStrategy === 2
+/** 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 的问题
+  // @ts-expect-error 预知的判断
+  const hideRedundantTabbarEnable = selectedTabbarStrategy === 1
+  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
+    safeareainsetbottom
+    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>

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

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

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

@@ -0,0 +1,11 @@
+/**
+ * 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)
+  },
+})

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

@@ -0,0 +1,65 @@
+/**
+ * tabbar 选择的策略,更详细的介绍见 tabbar.md 文件
+ * 0: 'NATIVE_TABBAR'  `完全原生 tabbar`
+ * 2: 'FULL_CUSTOM_TABBAR' `全自定义 tabbar`
+ * 1: 'HALF_CUSTOM_TABBAR' `半自定义 tabbar`
+ * 3: 'NO_TABBAR' `无 tabbar`
+ *
+ * 温馨提示:本文件的任何代码更改了之后,都需要重新运行,否则 pages.json 不会更新导致错误
+ */
+
+// TODO:通过这里切换使用tabbar的策略
+export const selectedTabbarStrategy = 0
+
+// 0 和 1 时,需要tabbar缓存
+export const cacheTabbarEnable = selectedTabbarStrategy < 2
+
+// selectedTabbarStrategy==0 时,需要填 iconPath 和 selectedIconPath
+// selectedTabbarStrategy==1 or 2 时,需要填 icon 和 iconType
+// selectedTabbarStrategy==3 时,tabbarList 不生效
+export const tabbarList = [
+  {
+    iconPath: 'static/tabbar/home.png',
+    selectedIconPath: 'static/tabbar/homeHL.png',
+    pagePath: 'pages/index/index',
+    text: '首页',
+    icon: 'home',
+    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',
+  // },
+]
+
+const _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

+ 19 - 0
src/layouts/tabbar.vue

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

+ 3 - 3
src/main.ts

@@ -1,11 +1,11 @@
-import '@/style/index.scss'
 import { VueQueryPlugin } from '@tanstack/vue-query'
-import 'uno.css'
 import { createSSRApp } from 'vue'
-
 import App from './App.vue'
 import { prototypeInterceptor, requestInterceptor, routeInterceptor } from './interceptors'
+
 import store from './store'
+import '@/style/index.scss'
+import 'virtual:uno.css'
 
 export function createApp() {
   const app = createSSRApp(App)

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

@@ -7,17 +7,21 @@
 }
 </route>
 
+<script lang="ts" setup>
+// code here
+</script>
+
 <template>
   <view class="text-center">
-    <view class="m-8">http://localhost:9000/#/pages-sub/demo/index</view>
-    <view class="text-green-500">分包页面demo</view>
+    <view class="m-8">
+      http://localhost:9000/#/pages-sub/demo/index
+    </view>
+    <view class="text-green-500">
+      分包页面demo
+    </view>
   </view>
 </template>
 
-<script lang="ts" setup>
-// code here
-</script>
-
 <style lang="scss" scoped>
 //
 </style>

+ 10 - 6
src/pages.json

@@ -28,21 +28,25 @@
         "iconPath": "static/tabbar/home.png",
         "selectedIconPath": "static/tabbar/homeHL.png",
         "pagePath": "pages/index/index",
-        "text": "首页"
+        "text": "首页",
+        "icon": "home",
+        "iconType": "uiLib"
       },
       {
         "iconPath": "static/tabbar/example.png",
         "selectedIconPath": "static/tabbar/exampleHL.png",
         "pagePath": "pages/about/about",
-        "text": "关于"
+        "text": "关于",
+        "icon": "i-carbon-code",
+        "iconType": "unocss"
       }
     ]
   },
-  "__esModule": true,
   "pages": [
     {
       "path": "pages/index/index",
       "type": "home",
+      "layout": "tabbar",
       "style": {
         "navigationStyle": "custom",
         "navigationBarTitleText": "首页"
@@ -51,11 +55,11 @@
     {
       "path": "pages/about/about",
       "type": "page",
+      "layout": "tabbar",
       "style": {
-        "navigationBarTitleText": "关于",
-        "navigationStyle": "custom"
+        "navigationBarTitleText": "关于"
       }
     }
   ],
   "subPackages": []
-}
+}

+ 15 - 25
src/pages/about/about.vue

@@ -1,50 +1,40 @@
 <route lang="json5">
 {
+  layout: 'tabbar',
   style: {
     navigationBarTitleText: '关于',
-    navigationStyle: 'custom', // 开启自定义导航栏
   },
 }
 </route>
 
-<template>
-  <view>
-    <fg-navbar>关于</fg-navbar>
-    <view
-      class="bg-white overflow-hidden pt-2 px-4"
-      :style="{ marginTop: safeAreaInsets?.top + 'px' }"
-    >
-      <view class="text-center text-3xl mt-8">
-        鸽友们好,我是
-        <text class="text-red-500">菲鸽</text>
-      </view>
-      <!-- <button @click="toSubPage()">去分包</button> -->
-      <view class="test-css">测试 scss 样式</view>
-      <RequestComp />
-      <UploadComp />
-    </view>
-  </view>
-</template>
-
 <script lang="ts" setup>
 import RequestComp from './components/request.vue'
 import UploadComp from './components/upload.vue'
 
 // 获取屏幕边界到安全区域距离
 const { safeAreaInsets } = uni.getSystemInfoSync()
-const toSubPage = () => {
-  uni.navigateTo({
-    url: '/pages-sub/demo/index',
-  })
-}
 
 // 奇怪:同样的代码放在 vue 里面不会校验到错误,放在 .ts 文件里面会校验到错误
 // const testOxlint = (name: string) => {
 //   console.log('oxlint')
 // }
 // testOxlint('oxlint')
+console.log('about')
 </script>
 
+<template>
+  <view>
+    <view class="mt-8 text-center text-3xl">
+      鸽友们好,我是
+      <text class="text-red-500">
+        菲鸽
+      </text>
+    </view>
+    <RequestComp />
+    <UploadComp />
+  </view>
+</template>
+
 <style lang="scss" scoped>
 .test-css {
   // 16rpx=>0.5rem

+ 47 - 30
src/pages/about/components/request.vue

@@ -7,36 +7,9 @@
 }
 </route>
 
-<template>
-  <view class="p-6 text-center">
-    <view class="my-2">使用的是 laf 云后台</view>
-    <view class="text-green-400">我的推荐码,可以获得佣金</view>
-
-    <!-- #ifdef H5 -->
-    <view class="my-2">
-      <a class="my-2" :href="recommendUrl" target="_blank">{{ recommendUrl }}</a>
-    </view>
-    <!-- #endif -->
-
-    <!-- #ifndef H5 -->
-    <view class="my-2 text-left text-sm">{{ recommendUrl }}</view>
-    <!-- #endif -->
-
-    <!-- http://localhost:9000/#/pages/index/request -->
-    <wd-button @click="run" class="my-6">发送请求</wd-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>
-    <wd-button type="error" @click="reset" class="my-6" :disabled="!data">重置数据</wd-button>
-  </view>
-</template>
-
 <script lang="ts" setup>
-import { getFooAPI, postFooAPI, IFooItem } from '@/service/index/foo'
+import type { IFooItem } from '@/service/index/foo'
+import { getFooAPI } from '@/service/index/foo'
 // import { findPetsByStatusQueryOptions } from '@/service/app'
 // import { useQuery } from '@tanstack/vue-query'
 
@@ -61,7 +34,51 @@ const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲
 //   refetch,
 // } = useQuery(findPetsByStatusQueryOptions({ params: { status: ['available'] } }))
 
-const reset = () => {
+function reset() {
   data.value = initialData
 }
 </script>
+
+<template>
+  <view class="p-6 text-center">
+    <view class="my-2">
+      使用的是 laf 云后台
+    </view>
+    <view class="text-green-400">
+      我的推荐码,可以获得佣金
+    </view>
+
+    <!-- #ifdef H5 -->
+    <view class="my-2">
+      <a class="my-2" :href="recommendUrl" target="_blank">{{ recommendUrl }}</a>
+    </view>
+    <!-- #endif -->
+
+    <!-- #ifndef H5 -->
+    <view class="my-2 text-left text-sm">
+      {{ recommendUrl }}
+    </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...
+      </view>
+      <block v-else>
+        <view class="text-xl">
+          请求数据如下
+        </view>
+        <view class="text-green leading-8">
+          {{ JSON.stringify(data) }}
+        </view>
+      </block>
+    </view>
+    <wd-button type="error" class="my-6" :disabled="!data" @click="reset">
+      重置数据
+    </wd-button>
+  </view>
+</template>

+ 18 - 10
src/pages/about/components/upload.vue

@@ -7,24 +7,32 @@
 }
 </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="text-blue h-10">上传...</view>
+    <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 class="h-80 w-full">
-        <image v-if="data" :src="data.url" mode="scaleToFill" />
+      <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>
 
-<script lang="ts" setup>
-const { loading, data, run } = useUpload()
-</script>
-
 <style lang="scss" scoped>
 //
 </style>

+ 35 - 23
src/pages/index/index.vue

@@ -1,34 +1,14 @@
 <!-- 使用 type="home" 属性设置首页,其他页面不需要设置,默认为page;推荐使用json5,更强大,且允许注释 -->
 <route lang="json5" type="home">
 {
+  layout: 'tabbar',
   style: {
+    // 'custom' 表示开启自定义导航栏,默认 'default'
     navigationStyle: 'custom',
     navigationBarTitleText: '首页',
   },
 }
 </route>
-<template>
-  <view
-    class="bg-white overflow-hidden pt-2 px-4"
-    :style="{ marginTop: safeAreaInsets?.top + 'px' }"
-  >
-    <view class="mt-12">
-      <image src="/static/logo.svg" alt="" class="w-28 h-28 block mx-auto" />
-    </view>
-    <view class="text-center text-4xl text-[#d14328] mt-4">unibest</view>
-    <view class="text-center text-2xl mt-2 mb-8">最好用的 uniapp 开发模板</view>
-
-    <view class="text-justify max-w-100 m-auto text-4 indent mb-2">{{ description }}</view>
-    <view class="text-center mt-8">
-      当前平台是:
-      <text class="text-green-500">{{ PLATFORM.platform }}</text>
-    </view>
-    <view class="text-center mt-4">
-      模板分支是:
-      <text class="text-green-500">base</text>
-    </view>
-  </view>
-</template>
 
 <script lang="ts" setup>
 import PLATFORM from '@/utils/platform'
@@ -61,10 +41,42 @@ safeAreaInsets = systemInfo.safeAreaInsets
 // #endif
 const author = ref('菲鸽')
 const description = ref(
-  'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite6 + UnoCss + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
+  'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite5 + UnoCss + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
 )
 // 测试 uni API 自动引入
 onLoad(() => {
   console.log('项目作者:', author.value)
 })
+
+console.log('index')
 </script>
+
+<template>
+  <view class="bg-white px-4 pt-2" :style="{ marginTop: `${safeAreaInsets?.top}px` }">
+    <view class="mt-12">
+      <image src="/static/logo.svg" alt="" class="mx-auto block h-28 w-28" />
+    </view>
+    <view class="mt-4 text-center text-4xl text-[#d14328]">
+      unibest
+    </view>
+    <view class="mb-8 mt-2 text-center text-2xl">
+      最好用的 uniapp 开发模板
+    </view>
+
+    <view class="m-auto mb-2 max-w-100 text-justify indent text-4">
+      {{ description }}
+    </view>
+    <view class="mt-8 text-center">
+      当前平台是:
+      <text class="text-green-500">
+        {{ PLATFORM.platform }}
+      </text>
+    </view>
+    <view class="mt-4 text-center">
+      模板分支是:
+      <text class="text-green-500">
+        base
+      </text>
+    </view>
+  </view>
+</template>

+ 6 - 5
src/service/index/foo.ts

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

+ 20 - 18
src/store/user.ts

@@ -1,14 +1,14 @@
+import type { IUserInfoVo } from '@/api/login.typings'
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
 import {
-  login as _login,
   getUserInfo as _getUserInfo,
-  wxLogin as _wxLogin,
+  login as _login,
   logout as _logout,
+  wxLogin as _wxLogin,
   getWxCode,
 } from '@/api/login'
-import { defineStore } from 'pinia'
-import { ref } from 'vue'
 import { toast } from '@/utils/toast'
-import { IUserInfoVo } from '@/api/login.typings'
 
 // 初始化状态
 const userInfoState: IUserInfoVo = {
@@ -29,7 +29,8 @@ export const useUserStore = defineStore(
       // 若头像为空 则使用默认头像
       if (!val.avatar) {
         val.avatar = userInfoState.avatar
-      } else {
+      }
+      else {
         val.avatar = 'https://oss.laf.run/ukw0y1-site/avatar.jpg?feige'
       }
       userInfo.value = val
@@ -45,6 +46,18 @@ export const useUserStore = defineStore(
       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 登录参数
@@ -62,18 +75,7 @@ export const useUserStore = defineStore(
       await getUserInfo()
       return res
     }
-    /**
-     * 获取用户信息
-     */
-    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
-    }
+
     /**
      * 退出登录 并 删除用户信息
      */

+ 1 - 1
src/style/index.scss

@@ -1,4 +1,4 @@
-// @import './iconfont.css';
+@import './iconfont.css';
 
 .test {
   // 可以通过 @apply 多个样式封装整体样式

+ 3 - 3
src/typings.d.ts

@@ -1,14 +1,14 @@
 // 全局要用的类型放到这里
 
 declare global {
-  type IResData<T> = {
+  interface IResData<T> {
     code: number
     msg: string
     data: T
   }
 
   // uni.uploadFile文件上传参数
-  type IUniUploadFileOptions = {
+  interface IUniUploadFileOptions {
     file?: File
     files?: UniApp.UploadFileOptionFiles[]
     filePath?: string
@@ -16,7 +16,7 @@ declare global {
     formData?: any
   }
 
-  type IUserInfo = {
+  interface IUserInfo {
     nickname?: string
     avatar?: string
     /** 微信的 openid,非微信没有这个字段 */

+ 1 - 1
src/typings.ts

@@ -6,7 +6,7 @@ export enum TestEnum {
 }
 
 // uni.uploadFile文件上传参数
-export type IUniUploadFileOptions = {
+export interface IUniUploadFileOptions {
   file?: File
   files?: UniApp.UploadFileOptionFiles[]
   filePath?: string

+ 15 - 35
src/utils/http.ts

@@ -1,6 +1,6 @@
-import { CustomRequestOptions } from '@/interceptors/request'
+import type { CustomRequestOptions } from '@/interceptors/request'
 
-export const http = <T>(options: CustomRequestOptions) => {
+export function http<T>(options: CustomRequestOptions) {
   // 1. 返回 Promise 对象
   return new Promise<IResData<T>>((resolve, reject) => {
     uni.request({
@@ -15,18 +15,20 @@ export const http = <T>(options: CustomRequestOptions) => {
         if (res.statusCode >= 200 && res.statusCode < 300) {
           // 2.1 提取核心数据 res.data
           resolve(res.data as IResData<T>)
-        } else if (res.statusCode === 401) {
+        }
+        else if (res.statusCode === 401) {
           // 401错误  -> 清理用户信息,跳转到登录页
           // userStore.clearUserInfo()
           // uni.navigateTo({ url: '/pages/login/login' })
           reject(res)
-        } else {
+        }
+        else {
           // 其他错误 -> 根据后端错误信息轻提示
-          !options.hideErrorToast &&
-            uni.showToast({
-              icon: 'none',
-              title: (res.data as IResData<T>).msg || '请求错误',
-            })
+          !options.hideErrorToast
+          && uni.showToast({
+            icon: 'none',
+            title: (res.data as IResData<T>).msg || '请求错误',
+          })
           reject(res)
         }
       },
@@ -49,12 +51,7 @@ export const http = <T>(options: CustomRequestOptions) => {
  * @param header 请求头,默认为json格式
  * @returns
  */
-export const httpGet = <T>(
-  url: string,
-  query?: Record<string, any>,
-  header?: Record<string, any>,
-  options?: Partial<CustomRequestOptions>,
-) => {
+export function httpGet<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
   return http<T>({
     url,
     query,
@@ -72,13 +69,7 @@ export const httpGet = <T>(
  * @param header 请求头,默认为json格式
  * @returns
  */
-export const httpPost = <T>(
-  url: string,
-  data?: Record<string, any>,
-  query?: Record<string, any>,
-  header?: Record<string, any>,
-  options?: Partial<CustomRequestOptions>,
-) => {
+export function httpPost<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
   return http<T>({
     url,
     query,
@@ -91,13 +82,7 @@ export const httpPost = <T>(
 /**
  * PUT 请求
  */
-export const httpPut = <T>(
-  url: string,
-  data?: Record<string, any>,
-  query?: Record<string, any>,
-  header?: Record<string, any>,
-  options?: Partial<CustomRequestOptions>,
-) => {
+export function httpPut<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
   return http<T>({
     url,
     data,
@@ -111,12 +96,7 @@ export const httpPut = <T>(
 /**
  * DELETE 请求(无请求体,仅 query)
  */
-export const httpDelete = <T>(
-  url: string,
-  query?: Record<string, any>,
-  header?: Record<string, any>,
-  options?: Partial<CustomRequestOptions>,
-) => {
+export function httpDelete<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
   return http<T>({
     url,
     query,

+ 13 - 49
src/utils/index.ts

@@ -1,9 +1,7 @@
-import pagesConfig from '@/pages.json'
+import { pages, subPackages } from '@/pages.json'
 import { isMpWeixin } from './platform'
 
-const { pages, subPackages, tabBar = { list: [] } } = { ...pagesConfig }
-
-export const getLastPage = () => {
+export function getLastPage() {
   // getCurrentPages() 至少有1个元素,所以不再额外判断
   // const lastPage = getCurrentPages().at(-1)
   // 上面那个在低版本安卓中打包会报错,所以改用下面这个【虽然我加了 src/interceptions/prototype.ts,但依然报错】
@@ -11,46 +9,12 @@ export const getLastPage = () => {
   return pages[pages.length - 1]
 }
 
-export const tabBarList = tabBar?.list || []
-
-/** 判断当前页面是否是 tabbar 页  */
-export const getIsTabbar = () => {
-  try {
-    const lastPage = getLastPage()
-    const currPath = lastPage?.route
-
-    return Boolean(tabBar?.list?.some((item) => item.pagePath === currPath))
-  } catch {
-    return false
-  }
-}
-
-/**
- * 判断指定页面是否是 tabbar 页
- * @param path 页面路径
- * @returns true: 是 tabbar 页 false: 不是 tabbar 页
- */
-export const isTableBar = (path: string) => {
-  if (!tabBar) {
-    return false
-  }
-  if (!tabBar.list.length) {
-    // 通常有 tabBar 的话,list 不能有空,且至少有2个元素,这里其实不用处理
-    return false
-  }
-  // 这里需要处理一下 path,因为 tabBar 中的 pagePath 是不带 /pages 前缀的
-  if (path.startsWith('/')) {
-    path = path.substring(1)
-  }
-  return !!tabBar.list.find((e) => e.pagePath === path)
-}
-
 /**
  * 获取当前页面路由的 path 路径和 redirectPath 路径
  * path 如 '/pages/login/index'
  * redirectPath 如 '/pages/demo/base/route-interceptor'
  */
-export const currRoute = () => {
+export function currRoute() {
   const lastPage = getLastPage()
   const currRoute = (lastPage as any).$page
   // console.log('lastPage.$page:', currRoute)
@@ -65,7 +29,7 @@ export const currRoute = () => {
   return getUrlObj(fullPath)
 }
 
-const ensureDecodeURIComponent = (url: string) => {
+function ensureDecodeURIComponent(url: string) {
   if (url.startsWith('%')) {
     return ensureDecodeURIComponent(decodeURIComponent(url))
   }
@@ -76,7 +40,7 @@ const ensureDecodeURIComponent = (url: string) => {
  * 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
  * 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}}
  */
-export const getUrlObj = (url: string) => {
+export function getUrlObj(url: string) {
   const [path, queryStr] = url.split('?')
   // console.log(path, queryStr)
 
@@ -99,11 +63,11 @@ export const getUrlObj = (url: string) => {
  * 这里设计得通用一点,可以传递 key 作为判断依据,默认是 needLogin, 与 route-block 配对使用
  * 如果没有传 key,则表示所有的 pages,如果传递了 key, 则表示通过 key 过滤
  */
-export const getAllPages = (key = 'needLogin') => {
+export function getAllPages(key = 'needLogin') {
   // 这里处理主包
   const mainPages = pages
-    .filter((page) => !key || page[key])
-    .map((page) => ({
+    .filter(page => !key || page[key])
+    .map(page => ({
       ...page,
       path: `/${page.path}`,
     }))
@@ -115,7 +79,7 @@ export const getAllPages = (key = 'needLogin') => {
     const { root } = subPageObj
 
     subPageObj.pages
-      .filter((page) => !key || page[key])
+      .filter(page => !key || page[key])
       .forEach((page: { path: string } & Record<string, any>) => {
         subPages.push({
           ...page,
@@ -132,18 +96,18 @@ export const getAllPages = (key = 'needLogin') => {
  * 得到所有的需要登录的 pages,包括主包和分包的
  * 只得到 path 数组
  */
-export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map((page) => page.path)
+export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map(page => page.path)
 
 /**
  * 得到所有的需要登录的 pages,包括主包和分包的
  * 只得到 path 数组
  */
-export const needLoginPages: string[] = getAllPages('needLogin').map((page) => page.path)
+export const needLoginPages: string[] = getAllPages('needLogin').map(page => page.path)
 
 /**
  * 根据微信小程序当前环境,判断应该获取的 baseUrl
  */
-export const getEnvBaseUrl = () => {
+export function getEnvBaseUrl() {
   // 请求基准地址
   let baseUrl = import.meta.env.VITE_SERVER_BASEURL
 
@@ -172,7 +136,7 @@ export const getEnvBaseUrl = () => {
 /**
  * 根据微信小程序当前环境,判断应该获取的 UPLOAD_BASEURL
  */
-export const getEnvBaseUploadUrl = () => {
+export function getEnvBaseUploadUrl() {
   // 请求基准地址
   let baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL
 

+ 11 - 9
src/utils/request.ts

@@ -1,11 +1,11 @@
-import { CustomRequestOptions } from '@/interceptors/request'
+import type { CustomRequestOptions } from '@/interceptors/request'
 
 /**
  * 请求方法: 主要是对 uni.request 的封装,去适配 openapi-ts-request 的 request 方法
  * @param options 请求参数
  * @returns 返回 Promise 对象
  */
-const http = <T>(options: CustomRequestOptions) => {
+function http<T>(options: CustomRequestOptions) {
   // 1. 返回 Promise 对象
   return new Promise<T>((resolve, reject) => {
     uni.request({
@@ -20,18 +20,20 @@ const http = <T>(options: CustomRequestOptions) => {
         if (res.statusCode >= 200 && res.statusCode < 300) {
           // 2.1 提取核心数据 res.data
           resolve(res.data as T)
-        } else if (res.statusCode === 401) {
+        }
+        else if (res.statusCode === 401) {
           // 401错误  -> 清理用户信息,跳转到登录页
           // userStore.clearUserInfo()
           // uni.navigateTo({ url: '/pages/login/login' })
           reject(res)
-        } else {
+        }
+        else {
           // 其他错误 -> 根据后端错误信息轻提示
-          !options.hideErrorToast &&
-            uni.showToast({
-              icon: 'none',
-              title: (res.data as T & { msg?: string })?.msg || '请求错误',
-            })
+          !options.hideErrorToast
+          && uni.showToast({
+            icon: 'none',
+            title: (res.data as T & { msg?: string })?.msg || '请求错误',
+          })
           reject(res)
         }
       },

+ 2 - 2
src/utils/toast.ts

@@ -21,8 +21,8 @@ export function showToast(options: ToastOptions | string) {
     position: 'middle',
     message: '',
   }
-  const mergedOptions =
-    typeof options === 'string'
+  const mergedOptions
+    = typeof options === 'string'
       ? { ...defaultOptions, message: options }
       : { ...defaultOptions, ...options }
   // 映射position到uniapp支持的格式

+ 10 - 16
src/utils/uploadFile.ts

@@ -21,7 +21,7 @@ import { toast } from './toast'
  */
 export const uploadFileUrl = {
   /** 用户头像上传地址 */
-  USER_AVATAR: import.meta.env.VITE_SERVER_BASEURL + '/user/avatar',
+  USER_AVATAR: `${import.meta.env.VITE_SERVER_BASEURL}/user/avatar`,
 }
 
 /**
@@ -31,12 +31,7 @@ export const uploadFileUrl = {
  * @param formData 额外表单数据
  * @param options 上传选项
  */
-export const useFileUpload = <T = string>(
-  url: string,
-  filePath: string,
-  formData: Record<string, any> = {},
-  options: Omit<UploadOptions, 'sourceType' | 'sizeType' | 'count'> = {},
-) => {
+export function useFileUpload<T = string>(url: string, filePath: string, formData: Record<string, any> = {}, options: Omit<UploadOptions, 'sourceType' | 'sizeType' | 'count'> = {}) {
   return useUpload<T>(
     url,
     formData,
@@ -76,13 +71,9 @@ export interface UploadOptions {
  * @param options 上传选项
  * @returns 上传状态和控制对象
  */
-export const useUpload = <T = string>(
-  url: string,
-  formData: Record<string, any> = {},
-  options: UploadOptions = {},
+export function useUpload<T = string>(url: string, formData: Record<string, any> = {}, options: UploadOptions = {},
   /** 直接传入文件路径,跳过选择器 */
-  directFilePath?: string,
-) => {
+  directFilePath?: string) {
   /** 上传中状态 */
   const loading = ref(false)
   /** 上传错误状态 */
@@ -161,7 +152,8 @@ export const useUpload = <T = string>(
       success: (res) => {
         const file = res.tempFiles[0]
         // 检查文件大小是否符合限制
-        if (!checkFileSize(file.size)) return
+        if (!checkFileSize(file.size))
+          return
 
         // 开始上传
         loading.value = true
@@ -295,7 +287,8 @@ function uploadFile<T>({
           // 上传成功
           data.value = _data as T
           onSuccess?.(_data)
-        } catch (err) {
+        }
+        catch (err) {
           // 响应解析错误
           console.error('解析上传响应失败:', err)
           error.value = true
@@ -320,7 +313,8 @@ function uploadFile<T>({
       progress.value = res.progress
       onProgress?.(res.progress)
     })
-  } catch (err) {
+  }
+  catch (err) {
     // 创建上传任务失败
     console.error('创建上传任务失败:', err)
     error.value = true

+ 12 - 12
tsconfig.json

@@ -1,33 +1,32 @@
 {
   "compilerOptions": {
     "composite": true,
-    "skipLibCheck": true,
+    "lib": ["esnext", "dom"],
+    "baseUrl": ".",
     "module": "ESNext",
     "moduleResolution": "Node",
-    "resolveJsonModule": true,
-    "noImplicitThis": true,
-    "allowSyntheticDefaultImports": true,
-    "allowJs": true,
-    "sourceMap": true,
-    "baseUrl": ".",
     "paths": {
       "@/*": ["./src/*"],
       "@img/*": ["./src/static/*"]
     },
-    "outDir": "dist",
-    "lib": ["esnext", "dom"],
+    "resolveJsonModule": true,
     "types": [
       "@dcloudio/types",
       "@uni-helper/uni-types",
       "wot-design-uni/global.d.ts",
       "z-paging/types",
       "./src/typings.d.ts"
-    ]
+    ],
+    "allowJs": true,
+    "noImplicitThis": true,
+    "outDir": "dist",
+    "sourceMap": true,
+    "allowSyntheticDefaultImports": true,
+    "skipLibCheck": true
   },
   "vueCompilerOptions": {
     "plugins": ["@uni-helper/uni-types/volar-plugin"]
   },
-  "exclude": ["node_modules"],
   "include": [
     "src/**/*.ts",
     "src/**/*.js",
@@ -36,5 +35,6 @@
     "src/**/*.jsx",
     "src/**/*.vue",
     "src/**/*.json"
-  ]
+  ],
+  "exclude": ["node_modules"]
 }

+ 3 - 2
uno.config.ts

@@ -2,8 +2,8 @@
 import { presetUni } from '@uni-helper/unocss-preset-uni'
 import {
   defineConfig,
-  presetIcons,
   presetAttributify,
+  presetIcons,
   transformerDirectives,
   transformerVariantGroup,
 } from 'unocss'
@@ -20,7 +20,7 @@ export default defineConfig({
       scale: 1.2,
       warn: true,
       extraProperties: {
-        display: 'inline-block',
+        'display': 'inline-block',
         'vertical-align': 'middle',
       },
     }),
@@ -39,6 +39,7 @@ export default defineConfig({
       center: 'flex justify-center items-center',
     },
   ],
+  safelist: [],
   rules: [
     [
       'p-safe',

+ 4 - 2
vite-plugins/copyNativeRes.ts

@@ -1,5 +1,6 @@
+import path from 'node:path'
+import process from 'node:process'
 import fs from 'fs-extra'
-import path from 'path'
 
 export function copyNativeRes() {
   const waitPath = path.resolve(__dirname, '../src/nativeResources')
@@ -31,7 +32,8 @@ export function copyNativeRes() {
         console.log(
           `[copyNativeRes] 成功将 nativeResources 目录中的资源移动到构建目录:${buildPath}`,
         )
-      } catch (error) {
+      }
+      catch (error) {
         console.error(`[copyNativeRes] 复制资源失败:`, error)
       }
     },

+ 10 - 7
vite-plugins/updatePackageJson.ts

@@ -1,14 +1,16 @@
 // src/plugins/updatePackageJson.ts
-import { Plugin } from 'vite'
-import fs from 'fs/promises'
-import path from 'path'
+import type { Plugin } from 'vite'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import process from 'node:process'
 
-const updatePackageJson = (): Plugin => {
+function updatePackageJson(): Plugin {
   return {
     name: 'update-package-json',
     async buildStart() {
       // 只在生产环境构建时执行
-      if (process.env.NODE_ENV !== 'production') return
+      if (process.env.NODE_ENV !== 'production')
+        return
 
       const packageJsonPath = path.resolve(process.cwd(), 'package.json')
 
@@ -21,10 +23,11 @@ const updatePackageJson = (): Plugin => {
         packageJson['update-time'] = new Date().toISOString().split('T')[0] // YYYY-MM-DD
 
         // 写回文件(保持 2 空格缩进)
-        await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8')
+        await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf-8')
 
         console.log(`[update-package-json] 更新时间戳: ${packageJson['update-time']}`)
-      } catch (error) {
+      }
+      catch (error) {
         console.error('[update-package-json] 插件执行失败:', error)
       }
     },

+ 20 - 20
vite.config.ts

@@ -1,27 +1,27 @@
-import Uni from '@dcloudio/vite-plugin-uni'
-import dayjs from 'dayjs'
 import path from 'node:path'
-import { defineConfig, loadEnv } from 'vite'
-// @see https://uni-helper.js.org/vite-plugin-uni-pages
-import UniPages from '@uni-helper/vite-plugin-uni-pages'
+import process from 'node:process'
+import Uni from '@dcloudio/vite-plugin-uni'
+import Components from '@uni-helper/vite-plugin-uni-components'
 // @see https://uni-helper.js.org/vite-plugin-uni-layouts
 import UniLayouts from '@uni-helper/vite-plugin-uni-layouts'
+// @see https://github.com/uni-helper/vite-plugin-uni-manifest
+import UniManifest from '@uni-helper/vite-plugin-uni-manifest'
+// @see https://uni-helper.js.org/vite-plugin-uni-pages
+import UniPages from '@uni-helper/vite-plugin-uni-pages'
 // @see https://github.com/uni-helper/vite-plugin-uni-platform
 // 需要与 @uni-helper/vite-plugin-uni-pages 插件一起使用
 import UniPlatform from '@uni-helper/vite-plugin-uni-platform'
-// @see https://github.com/uni-helper/vite-plugin-uni-manifest
-import UniManifest from '@uni-helper/vite-plugin-uni-manifest'
 /**
  * 分包优化、模块异步跨包调用、组件异步跨包引用
  * @see https://github.com/uni-ku/bundle-optimizer
  */
 import Optimization from '@uni-ku/bundle-optimizer'
+import dayjs from 'dayjs'
 import { visualizer } from 'rollup-plugin-visualizer'
 import AutoImport from 'unplugin-auto-import/vite'
+import { defineConfig, loadEnv } from 'vite'
 import ViteRestart from 'vite-plugin-restart'
-import { copyNativeRes } from './vite-plugins/copyNativeRes'
 import updatePackageJson from './vite-plugins/updatePackageJson'
-import Components from '@uni-helper/vite-plugin-uni-components'
 
 // https://vitejs.dev/config/
 export default async ({ command, mode }) => {
@@ -75,7 +75,7 @@ export default async ({ command, mode }) => {
         // 自定义插件禁用 vite:vue 插件的 devToolsEnabled,强制编译 vue 模板时 inline 为 true
         name: 'fix-vite-plugin-vue',
         configResolved(config) {
-          const plugin = config.plugins.find((p) => p.name === 'vite:vue')
+          const plugin = config.plugins.find(p => p.name === 'vite:vue')
           if (plugin && plugin.api && plugin.api.options) {
             plugin.api.options.devToolsEnabled = false
           }
@@ -91,7 +91,7 @@ export default async ({ command, mode }) => {
       // Optimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行
       Optimization({
         enable: {
-          optimization: true,
+          'optimization': true,
           'async-import': true,
           'async-component': true,
         },
@@ -113,14 +113,14 @@ export default async ({ command, mode }) => {
         },
       },
       // 打包分析插件,h5 + 生产环境才弹出
-      UNI_PLATFORM === 'h5' &&
-        mode === 'production' &&
-        visualizer({
-          filename: './node_modules/.cache/visualizer/stats.html',
-          open: true,
-          gzipSize: true,
-          brotliSize: true,
-        }),
+      UNI_PLATFORM === 'h5'
+      && mode === 'production'
+      && visualizer({
+        filename: './node_modules/.cache/visualizer/stats.html',
+        open: true,
+        gzipSize: true,
+        brotliSize: true,
+      }),
       // 只有在 app 平台时才启用 copyNativeRes 插件
       // UNI_PLATFORM === 'app' && copyNativeRes(),
       Components({
@@ -163,7 +163,7 @@ export default async ({ command, mode }) => {
             [VITE_APP_PROXY_PREFIX]: {
               target: VITE_SERVER_BASEURL,
               changeOrigin: true,
-              rewrite: (path) => path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''),
+              rewrite: path => path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''),
             },
           }
         : undefined,