├── .editorconfig ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json ├── settings.json ├── unocss.json └── vue3.code-snippets ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── config │ ├── index.ts │ └── proxy.ts └── plugins │ ├── autoImport.ts │ ├── cleanImage.ts │ ├── component.ts │ ├── index.ts │ ├── replaceUrl.ts │ ├── unocss.ts │ └── visualizer.ts ├── cz.config.js ├── env ├── .env ├── .env.development ├── .env.production └── .env.test ├── eslint.config.js ├── index.html ├── package.json ├── pnpm-lock.yaml ├── scripts ├── post-upgrade.js └── verify-commit.js ├── src ├── App.vue ├── api │ ├── common │ │ ├── index.ts │ │ └── types.ts │ ├── index.ts │ └── user │ │ ├── index.ts │ │ └── types.ts ├── components │ ├── .gitkeep │ ├── agree-privacy │ │ └── index.vue │ └── lang-select │ │ └── index.vue ├── hooks │ ├── index.ts │ ├── use-clipboard │ │ └── index.ts │ ├── use-loading │ │ └── index.ts │ ├── use-location │ │ ├── index.ts │ │ └── types.ts │ ├── use-modal │ │ └── index.ts │ ├── use-permission │ │ └── index.ts │ └── use-share │ │ ├── index.ts │ │ └── types.ts ├── locales │ ├── index.ts │ └── langs │ │ ├── en.ts │ │ └── zh-Hans.ts ├── main.ts ├── manifest.json ├── pages.json ├── pages │ ├── common │ │ ├── 404 │ │ │ └── index.vue │ │ ├── login │ │ │ └── index.vue │ │ └── webview │ │ │ └── index.vue │ └── tab │ │ ├── home │ │ └── index.vue │ │ ├── list │ │ └── index.vue │ │ └── user │ │ └── index.vue ├── plugins │ ├── index.ts │ ├── permission.ts │ └── ui.ts ├── router │ └── index.ts ├── static │ ├── images │ │ ├── 404.png │ │ ├── logo.png │ │ ├── pay.png │ │ └── tabbar │ │ │ ├── icon_home.png │ │ │ ├── icon_home_selected.png │ │ │ ├── icon_list.png │ │ │ ├── icon_list_selected.png │ │ │ ├── icon_me.png │ │ │ └── icon_me_selected.png │ └── styles │ │ └── common.scss ├── store │ ├── index.ts │ └── modules │ │ ├── app │ │ ├── index.ts │ │ └── types.ts │ │ └── user │ │ ├── index.ts │ │ └── types.ts ├── uni.scss └── utils │ ├── auth │ └── index.ts │ ├── common │ └── index.ts │ ├── index.ts │ ├── modals │ ├── index.ts │ └── types.ts │ ├── request │ ├── index.ts │ ├── interceptors.ts │ ├── status.ts │ └── types.ts │ └── storage │ └── index.ts ├── stylelint.config.js ├── tsconfig.json ├── types ├── auto-imports.d.ts ├── components.d.ts ├── env.d.ts ├── global.d.ts ├── i18n.d.ts └── module.d.ts ├── uno.config.ts └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # 告诉EditorConfig插件,这是根文件,不用继续往上查找 2 | root = true 3 | 4 | # 匹配全部文件 5 | [*] 6 | # 设置字符集 7 | charset = utf-8 8 | # 缩进风格,可选space、tab 9 | indent_style = space 10 | # 缩进的空格数 11 | indent_size = 2 12 | # 结尾换行符,可选lf、cr、crlf 13 | end_of_line = lf 14 | # 在文件结尾插入新行 15 | insert_final_newline = true 16 | # 删除一行中的前后空格 17 | trim_trailing_whitespace = true 18 | 19 | # 匹配md结尾的文件 20 | [*.md] 21 | insert_final_newline = false 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: [main] # master 分支有 push 时触发 6 | paths-ignore: # 下列文件的变更不触发部署,可以自行添加 7 | - README.md 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: 检出代码 14 | uses: actions/checkout@v4 15 | 16 | - name: 安装pnpm 17 | uses: pnpm/action-setup@v4 18 | with: 19 | version: 8 20 | 21 | - name: Node环境 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: pnpm 26 | 27 | - name: 安装构建 28 | run: | 29 | pnpm install 30 | pnpm build:h5 31 | 32 | # 部署到 GitHub pages 33 | - name: 部署 34 | uses: peaceiris/actions-gh-pages@v4 # 使用部署到 GitHub pages 的 action 35 | with: 36 | github_token: ${{ secrets.GITHUB_TOKEN }} # secret 名 37 | commit_message: 自动部署 # 部署时的 git 提交信息,自由填写 38 | publish_dir: ./dist/build/h5 # 部署打包后的 dist 目录 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | #user 24 | .hbuilderx 25 | unpackage 26 | /stats.html 27 | # pnpm-lock.yaml 28 | yarn.lock 29 | package-lock.json 30 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # 设置npm包的下载源为国内镜像,加速包下载 2 | registry=https://registry.npmmirror.com/ 3 | # 将依赖包提升到node_modules根目录,减少嵌套层级 4 | shamefully-hoist=true 5 | # 关闭严格的对等依赖检查,避免因对等依赖版本不匹配而安装失败 6 | strict-peer-dependencies=false 7 | # 自动安装对等依赖,无需手动安装 8 | auto-install-peers=true 9 | # 对等依赖去重,减少重复安装 10 | dedupe-peer-dependents=true 11 | # 使用提升模式链接依赖,与npm兼容性更好 12 | node-linker=hoisted 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | /* 3 | 推荐扩展。 4 | 一键安装方式: 5 | 1.点击左侧的扩展图标 6 | 2. 点击筛选扩展器图标 7 | 3.点击工作区推荐右侧的下载图标一键安装 8 | */ 9 | "recommendations": [ 10 | // "antfu.vite", // 在编辑器内预览/调试您的应用程序 11 | // "antfu.iconify", // hover 内联显示相应的图标 12 | "antfu.unocss", // 一款零配置的 CSS 框架 13 | "vue.volar", // Vue 3 的开发必备扩展 14 | "dbaeumer.vscode-eslint", // ESLint 支持 15 | // "editorConfig.editorConfig", // EditorConfig 支持 16 | // "uni-helper.uni-highlight-vscode", // 对条件编译的代码注释部分提供了语法提示、高亮、折叠 17 | // "uni-helper.uni-app-snippets-vscode" // uni-app 基本能力代码片段。 18 | // "uni-helper.uni-ui-snippets-vscode" // uni-ui 基本能力代码片段 19 | // "uni-helper.uni-helper-vscode" // Uni Helper 扩展包集合 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // 禁用 prettier,使用 eslint 的代码格式化 3 | "prettier.enable": false, 4 | // 保存时自动格式化 5 | "editor.formatOnSave": false, 6 | // 保存时自动修复 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | "eslint.validate": [ 12 | "javascript", 13 | "javascriptreact", 14 | "typescript", 15 | "typescriptreact", 16 | "vue", 17 | "html", 18 | "markdown", 19 | "json", 20 | "jsonc", 21 | "yaml" 22 | ], 23 | // 消除json文件中的注释警告 24 | "files.associations": { 25 | "manifest.json": "jsonc", 26 | "pages.json": "jsonc" 27 | }, 28 | // 消除Unknown at rule @apply警告 29 | "css.customData": [ 30 | ".vscode/unocss.json" 31 | ], 32 | "i18n-ally.localesPaths": [ 33 | "src/locales", 34 | "src/locales/langs" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/unocss.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.1, 3 | "atDirectives": [ 4 | { 5 | "name": "@apply" 6 | }, 7 | { 8 | "name": "@screen" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/vue3.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | "Print Vue3 SFC": { 19 | "scope": "vue", 20 | "prefix": "v3", 21 | "body": [ 22 | "\n", 25 | "\n", 28 | "\n", 31 | ], 32 | }, 33 | "Print style": { 34 | "scope": "vue", 35 | "prefix": "st", 36 | "body": ["\n"], 37 | }, 38 | "Print script": { 39 | "scope": "vue", 40 | "prefix": "sc", 41 | "body": ["\n"], 42 | }, 43 | "Print template": { 44 | "scope": "vue", 45 | "prefix": "te", 46 | "body": ["\n"], 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | 感谢你对本项目的兴趣!我们欢迎任何形式的贡献,无论是报告 bug、提交功能请求、改进文档,还是直接提交代码。为了使贡献过程更加顺利,请按照以下指南进行操作。 4 | 5 | ## 如何开始 6 | 7 | 1. **Fork 项目**:首先将项目 `Fork` 到你的 `GitHub` 账户。 8 | 2. **克隆到本地**:将你 `Fork` 后的仓库克隆到本地。 9 | ```bash 10 | git clone https://github.com/你的用户名/uniapp-vue3-template.git 11 | ``` 12 | 3. **创建分支**:在进行任何修改之前,先创建一个新的分支。 13 | ```bash 14 | git checkout -b feature/your-feature-name 15 | ``` 16 | 4. **进行修改**:根据你的需求进行代码修改或文档编辑。 17 | 5. **提交更改**:完成修改后,使用以下命令提交更改: 18 | ```bash 19 | pnpm cz 20 | ``` 21 | 6. **推送分支**:将你的分支推送到 GitHub。 22 | ```bash 23 | git push origin feature/your-feature-name 24 | ``` 25 | 7. **创建 Pull Request**:在 `GitHub` 上,提交你的 `Pull Request`,描述清楚你所做的更改。 26 | 27 | ## 提交代码规范 28 | 29 | - **代码格式**:只要你安装了依赖项,就不用担心代码风格。`Git` 钩子会在提交时为你格式化和修复它们。 30 | - **简洁明了的提交信息**:提交信息应简明扼要,说明改动的目的和内容。建议使用以下格式: 31 | ``` 32 | [类型]:简要说明 33 | ``` 34 | 例如: 35 | ``` 36 | feat: 添加新功能 37 | fix: 修复 bug 38 | docs: 更新文档 39 | style: 格式调整 40 | refactor: 代码重构 41 | test: 添加/修改测试 42 | ``` 43 | 44 | ## 贡献前请注意 45 | 46 | - **讨论功能请求**:在提交功能请求之前,确保这个功能是项目当前的方向。 47 | - **修复 bug**:在修复 `bug` 之前,先检查现有的 `issue` 列表,看看是否有人已经报告了相同的问题。 48 | - **文档改进**:欢迎任何形式的文档改进。文档有时会被遗漏,但它对于项目的可用性至关重要。 49 | 50 | ## 测试 51 | 52 | 在提交 `Pull Request` 之前,请确保测试成功通过。如果有修改涉及到核心功能,请确保你的改动不会破坏现有的功能。 53 | 54 | ## 行为规范 55 | 56 | 希望在这个项目中创建一个友好和包容的社区。请遵循以下行为准则: 57 | 58 | - 尊重他人,避免人身攻击、歧视或骚扰。 59 | - 提供有建设性的反馈,不论是代码还是讨论。 60 | - 对不同的观点保持开放的态度,愿意倾听他人的建议。 61 | 62 | ## 相关文档 63 | 64 | - [项目主页](https://github.com/oyjt/uniapp-vue3-template) 65 | - [问题追踪](https://github.com/oyjt/uniapp-vue3-template/issues) 66 | 67 | 感谢你的贡献!期待与你的合作! 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 江阳小道 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uniapp 团队协作开发实践模板(Vue3) 2 | 3 | [![GitHub Repo stars](https://img.shields.io/github/stars/oyjt/uniapp-vue3-template?style=flat&logo=github)](https://github.com/oyjt/uniapp-vue3-template) 4 | [![GitHub forks](https://img.shields.io/github/forks/oyjt/uniapp-vue3-template?style=flat&logo=github)](https://github.com/oyjt/uniapp-vue3-template) 5 | [![node version](https://img.shields.io/badge/node-%3E%3D18-green)](https://github.com/oyjt/uniapp-vue3-template) 6 | [![pnpm version](https://img.shields.io/badge/pnpm-%3E%3D8-green)](https://github.com/oyjt/uniapp-vue3-template) 7 | [![GitHub package.json version (subfolder of monorepo)](https://img.shields.io/github/package-json/v/oyjt/uniapp-vue3-template)](https://github.com/oyjt/uniapp-vue3-template) 8 | [![GitHub License](https://img.shields.io/github/license/oyjt/uniapp-vue3-template)](https://github.com/oyjt/uniapp-vue3-template) 9 | 10 | 11 | 使用uniapp+vite+vue3+typescript+uview-plus+unocss 搭建的适合团队协作的快速开发模版 12 | 13 | [uview-plus官方文档](https://uiadmin.net/uview-plus/) 14 | 15 | 本项目集众多项目的优点,打造最适合团队协作开发的项目模板。 16 | 17 | 国内仓库地址:[https://gitee.com/ouyang/uniapp-vue3-template](https://gitee.com/ouyang/uniapp-vue3-template) 18 | 19 | 在线预览地址:[https://oyjt.github.io/uniapp-vue3-template/](https://oyjt.github.io/uniapp-vue3-template/) 20 | 21 | ### 特性 22 | 23 | - [x] 集成`uview-plus3.0 ui`库 24 | - [x] 支持多环境打包构建 25 | - [x] 使用`pinia`状态管理 26 | - [x] 封装网络请求,并支持`Typescript` 27 | - [x] 支持路径别名 28 | - [x] 支持自动加载组件和`API` 29 | - [x] 自动校验`git`提交代码格式 30 | - [x] 集成`ESLint`、`StyleLint`、`EditorConfig`代码格式规范 31 | - [x] `Typescript`支持 32 | - [x] 集成`UnoCSS` 33 | - [x] 集成`iconify`图标库 34 | - [x] 集成`z-paging`下拉刷新功能 35 | - [x] 添加页面跳转拦截,登录权限校验 36 | - [x] 支持`token`无感刷新 37 | - [x] 项目分包 38 | - [x] 集成小程序隐私协议授权组件 39 | - [x] 项目构建自动删除本地图片并替换本地图片路径为线上图片 40 | - [x] 集成包体积视图分析插件 41 | - [x] 支持国际化 42 | - [x] 集成`alova`网络请求(具体使用请切换到 [feature/alova](https://github.com/oyjt/uniapp-vue3-template/tree/feature/alova) 分支) 43 | - [x] 集成`axios`网络请求(具体使用请切换到 [feature/axios](https://github.com/oyjt/uniapp-vue3-template/tree/feature/axios) 分支) 44 | - [x] 支持新的`wot-design-uni`库(具体使用请切换到[feature/wot-design-uni](https://github.com/oyjt/uniapp-vue3-template/tree/feature/wot-design-uni)分支),[wot-design-uni官方文档](https://wot-design-uni.cn/) 45 | - [x] 支持新的`shadcn-ui`库(具体使用请切换到[feature/shadcn-ui](https://github.com/oyjt/uniapp-vue3-template/tree/feature/shadcn-ui)分支),[shadcn-ui官方文档](https://ui.shadcn.com/) 46 | 47 | ### uniapp插件推荐 48 | - [uniapp 插件精选(https://github.com/oyjt/awesome-uniapp)](https://github.com/oyjt/awesome-uniapp) 49 | 50 | ### 目录结构 51 | 项目中采用目前最新的技术方案来实现,目录结构清晰。 52 | ``` 53 | uniapp-vue3-project 54 | ├ build vite配置统一管理 55 | │ ├ config 56 | │ └ plugins 57 | ├ env 环境变量 58 | ├ scripts 一些脚本 59 | │ ├ post-upgrade.js 依赖库清理 60 | │ └ verify-commit.js git提交检验 61 | ├ src 62 | │ ├ api 接口管理 63 | │ ├ components 公共组件 64 | │ ├ hooks 常用hooks封装 65 | │ ├ locale 国际化语言管理 66 | │ ├ pages 页面管理 67 | │ ├ plugins 插件管理 68 | │ ├ router 路由管理 69 | │ ├ static 静态资源 70 | │ ├ store 状态管理 71 | │ ├ utils 一些工具 72 | │ ├ App.vue 73 | │ ├ main.ts 74 | │ ├ manifest.json 项目配置 75 | │ ├ pages.json 页面配置 76 | │ └ uni.scss 全局scss变量 77 | ├ types 全局typescript类型文件 78 | │ ├ auto-imports.d.ts 79 | │ ├ components.d.ts 80 | │ ├ global.d.ts 81 | │ └ module.d.ts 82 | ├ LICENSE 83 | ├ README.md 84 | ├ cz.config.js cz-git配置 85 | ├ eslint.config.js eslint配置 86 | ├ index.html 87 | ├ package.json 88 | ├ pnpm-lock.yaml 89 | ├ stylelint.config.js stylelint配置 90 | ├ tsconfig.json 91 | ├ uno.config.ts unocss配置 92 | └ vite.config.ts vite配置 93 | ``` 94 | 95 | #### vite插件管理 96 | ``` 97 | build 98 | ├ config vite配置 99 | │ ├ index.ts 入口文件 100 | │ └ proxy.ts 跨域代理配置 101 | └ plugins vite插件 102 | ├ autoImport.ts 自动导入api 103 | ├ cleanImage.ts 自动清理图片文件 104 | ├ component.ts 自动导入组件 105 | ├ index.ts 入口文件 106 | ├ replaceUrl.ts 自动替换图片地址为CDN地址 107 | ├ unocss.ts unocss配置 108 | └ visualizer.ts 包体积视图分析 109 | 110 | ``` 111 | 112 | #### 接口管理 113 | ``` 114 | api 115 | ├ common 通用api 116 | │ ├ index.ts 117 | │ └ types.ts 118 | ├ user 用户相关api 119 | │ ├ index.ts 120 | │ └ types.ts 121 | └ index.ts 入口文件 122 | ``` 123 | 124 | #### hooks管理 125 | ``` 126 | hooks 127 | ├ use-clipboard 剪切板 128 | │ └ index.ts 129 | ├ use-loading loading 130 | │ └ index.ts 131 | ├ use-modal 模态框 132 | │ └ index.ts 133 | ├ use-permission 校验权限 134 | │ └ index.ts 135 | ├ use-share 分享 136 | │ └ index.ts 137 | └ index.ts 入口文件 138 | ``` 139 | 140 | ### 页面管理 141 | ``` 142 | pages 143 | ├ common 公共页面(分包common) 144 | │ ├ login 145 | │ │ └ index.vue 146 | │ └ webview 147 | │ └ index.vue 148 | └ tab 主页面(主包) 149 | ├ home 150 | │ └ index.vue 151 | ├ list 152 | │ └ index.vue 153 | └ user 154 | └ index.vue 155 | ``` 156 | 157 | #### 状态管理 158 | ``` 159 | store 160 | ├ modules 161 | │ ├ app app状态 162 | │ │ ├ index.ts 163 | │ │ └ types.ts 164 | │ └ user 用户状态 165 | │ ├ index.ts 166 | │ └ types.ts 167 | └ index.ts 入口文件 168 | ``` 169 | 170 | ### 工具方法 171 | ``` 172 | utils 173 | ├ auth token相关方法 174 | │ └ index.ts 175 | ├ common 通用方法 176 | │ └ index.ts 177 | ├ modals 弹窗相关方法 178 | │ └ index.ts 179 | ├ request 网络请求相关方法 180 | │ ├ index.ts 181 | │ ├ interceptors.ts 182 | │ ├ status.ts 183 | │ └ types.ts 184 | └ index.ts 入口文件 185 | ``` 186 | 187 | ### 使用方法 188 | 189 | ```bash 190 | # 安装依赖 191 | pnpm install 192 | 193 | # 启动H5 194 | pnpm dev:h5 195 | 196 | # 启动微信小程序 197 | pnpm dev:mp-weixin 198 | ``` 199 | 200 | ### 发布 201 | 202 | ```bash 203 | # 构建开发环境 204 | pnpm build:h5 205 | pnpm build:mp-weixin 206 | 207 | # 构建测试环境 208 | pnpm build:h5-test 209 | pnpm build:mp-weixin-test 210 | 211 | # 构建生产环境 212 | pnpm build:h5-prod 213 | pnpm build:mp-weixin-prod 214 | ``` 215 | 216 | ### 代码提交 217 | ```bash 218 | pnpm cz 219 | ``` 220 | 221 | ### 更新uniapp版本 222 | 223 | 更新uniapp相关依赖到最新正式版 224 | ```bash 225 | npx @dcloudio/uvm@latest 226 | ``` 227 | 或者执行下面的命令 228 | ```bash 229 | pnpm uvm 230 | ``` 231 | 232 | 在升级完后,会自动添加很多无用依赖,执行下面的代码减小保体积 233 | ``` 234 | pnpm uvm-rm 235 | ``` 236 | 237 | ### `v3` 代码块 238 | 在 `vue` 文件中,输入 `v3` 按 `tab` 即可快速生成页面模板,可以大大加快页面生成。 239 | > 原理:基于 VSCode 代码块生成。 240 | 241 | ### 登录鉴权 242 | 1. 页面如果需要登录才能访问,只需在 `pages.json` 文件中需要鉴权的页面下设置 `needLogin` 属性设置为 `true` 即可,比如 243 | ``` 244 | { 245 | "pages": [ 246 | { 247 | "path": "pages/test/test", 248 | "needLogin": true, 249 | "style": { 250 | "navigationBarTitleText": "", 251 | }, 252 | } 253 | ] 254 | } 255 | ``` 256 | 257 | 2. 如果有`tab`页面需要登录才能访问,上面的设置在小程序中点击`tabbar`时无效,因为在小程序中点击tabbar不会触发`uni.switchTab`方法,下面是官方给出的回复及解决方案。 258 | 259 | > 拦截uni.switchTab本身没有问题。但是在微信小程序端点击tabbar的底层逻辑并不是触发uni.switchTab。所以误认为拦截无效,此类场景的解决方案是在tabbar页面的页面生命周期onShow中处理。 260 | 261 | 可参考`pages/tab/user/index.vue`中的代码,核心代码如下: 262 | ``` 263 | 273 | ``` 274 | 275 | ### 注意事项 276 | 1. 微信小程序开发者工具中内置的打包分析不准确,本项目使用了`rollup-plugin-visualizer`来分析小程序包体积,默认不开启,有需要的移除相关注释即可 277 | 2. 自动构建处理本地图片资源,使用了`vite-plugin-clean-build`和`vite-plugin-replace-image-url`这两个插件,默认不开启相关功能,如果需要使用再`build/vite/plugins/index.ts`文件中移除相关注释即可 278 | 3. 使用`vite-plugin-replace-image-url`插件,想要图片自动替换生效,需要在项目中使用绝对路径引入图片资源,如下示例所示。 279 | 280 | 示例一:style中的图片使用 281 | ``` 282 | 287 | 290 | ``` 291 | 292 | 示例二:js中的图片使用 293 | 294 | ``` 295 | 306 | ``` 307 | 308 | 示例二:css中的图片使用 309 | ``` 310 | 315 | ``` 316 | 317 | 4. 部分用户构建微信小程序如下错误,原因是微信开发者工具缺失了对应的依赖。 318 | ``` 319 | This @babel/plugin-proposal-private-property-in-object version is not meant to 320 | be imported. 321 | ``` 322 | 此时升级微信开发者工具,或者安装`@babel/plugin-proposal-private-property-in-object`依赖即可解决问题。 323 | 324 | 5. `shadcn-ui` 分支采用最新的 `tailwindcss v4.1` 版本,因为现阶段的 `unocss` 对于最新版 `tailwindcss` 支持还不够完善。 325 | `shadcn-ui`并不太适合移动端使用,如果不喜欢可以移除,只保留纯净的框架。 326 | 327 | ### 捐赠 328 | 329 | 如果你觉得这个项目对你有帮助,你可以请作者喝饮料🍹 330 | 331 |

332 | 微信收款码 333 |

334 | -------------------------------------------------------------------------------- /build/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './proxy'; 2 | -------------------------------------------------------------------------------- /build/config/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyOptions } from 'vite'; 2 | 3 | type ProxyTargetList = Record; 4 | 5 | export const createViteProxy = (env: Record) => { 6 | const { VITE_APP_PROXY, VITE_API_PREFIX, VITE_API_BASE_URL } = env; 7 | // 不使用代理直接返回 8 | if (!JSON.parse(VITE_APP_PROXY)) return undefined; 9 | const proxy: ProxyTargetList = { 10 | [VITE_API_PREFIX]: { 11 | target: VITE_API_BASE_URL, 12 | changeOrigin: true, 13 | rewrite: path => path.replace(new RegExp(`^${VITE_API_PREFIX}`), ''), 14 | }, 15 | }; 16 | return proxy; 17 | }; 18 | -------------------------------------------------------------------------------- /build/plugins/autoImport.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name AutoImportDeps 3 | * @description 按需加载,自动引入 4 | */ 5 | import AutoImport from 'unplugin-auto-import/vite'; 6 | 7 | export const AutoImportDeps = () => { 8 | return AutoImport({ 9 | imports: ['vue', 'uni-app', 'pinia'], 10 | dts: 'types/auto-imports.d.ts', 11 | vueTemplate: true, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /build/plugins/cleanImage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name cleanImagePlugin 3 | * @description 清除构建后的图片资源 4 | */ 5 | import CleanBuild from 'vite-plugin-clean-build'; 6 | 7 | export const CleanImagePlugin = () => { 8 | return CleanBuild({ 9 | outputDir: 'dist/build/mp-weixin', 10 | patterns: ['static/images/**', '!static/images/logo.png', '!static/images/tabbar/**'], 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /build/plugins/component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name AutoRegistryComponents 3 | * @description 按需加载,自动引入 4 | */ 5 | import Components from 'unplugin-vue-components/vite'; 6 | 7 | export const AutoRegistryComponents = () => { 8 | return Components({ 9 | dts: 'types/components.d.ts', 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /build/plugins/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name createVitePlugins 3 | * @description 封装plugins数组统一调用 4 | */ 5 | import type { PluginOption } from 'vite'; 6 | import uniPlugin from '@dcloudio/vite-plugin-uni'; 7 | import ViteRestart from 'vite-plugin-restart'; 8 | import { AutoImportDeps } from './autoImport'; 9 | // import { ConfigImageminPlugin } from './imagemin'; 10 | // import { ReplaceUrlPlugin } from './replaceUrl'; 11 | import { AutoRegistryComponents } from './component'; 12 | import { ConfigUnoCSSPlugin } from './unocss'; 13 | 14 | export default function createVitePlugins(isBuild: boolean) { 15 | const vitePlugins: (PluginOption | PluginOption[])[] = [ 16 | // UnoCSS配置 17 | ConfigUnoCSSPlugin(), 18 | // 自动按需引入依赖 19 | AutoImportDeps(), 20 | // 自动按需引入组件(注意:需注册至 uni 之前,否则不会生效) 21 | AutoRegistryComponents(), 22 | // uni支持(兼容性写法,当type为module时,必须要这样写) 23 | (uniPlugin as any).default(), 24 | ViteRestart({ 25 | // 通过这个插件,在修改vite.config.js文件则不需要重新运行也生效配置 26 | restart: ['vite.config.ts'], 27 | }), 28 | ]; 29 | 30 | if (isBuild) { 31 | const buildPlugins: (PluginOption | PluginOption[])[] = [ 32 | // 图片资源自动转换为网络资源 33 | // ReplaceUrlPlugin(), 34 | // 自动清除本地图片 35 | // CleanImagePlugin(), 36 | // 打包视图分析 37 | // VisualizerPlugin(), 38 | ]; 39 | vitePlugins.push(...buildPlugins); 40 | } 41 | 42 | return vitePlugins; 43 | } 44 | -------------------------------------------------------------------------------- /build/plugins/replaceUrl.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name ReplaceImageUrl 3 | * @description 替换图片地址 4 | */ 5 | import replaceImageUrl from 'vite-plugin-replace-image-url'; 6 | 7 | export const ReplaceUrlPlugin = () => { 8 | return replaceImageUrl({ 9 | publicPath: 'https://photo.example.com/miniprogram', 10 | sourceDir: 'src/static', 11 | verbose: true, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /build/plugins/unocss.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name ConfigUnoCSSPlugin 3 | * @description UnoCSS相关配置 4 | */ 5 | import UnoCSS from 'unocss/vite'; 6 | 7 | export const ConfigUnoCSSPlugin = () => { 8 | return UnoCSS(); 9 | }; 10 | -------------------------------------------------------------------------------- /build/plugins/visualizer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name VisualizerPlugin 3 | * @description 打包视图分析 4 | */ 5 | import { visualizer } from 'rollup-plugin-visualizer'; 6 | 7 | export const VisualizerPlugin = () => { 8 | return visualizer({ 9 | emitFile: false, 10 | filename: 'stats.html', // 分析图生成的文件名 11 | open: true, // 如果存在本地服务端口,将在打包后自动展示 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /cz.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('cz-git').CommitizenGitOptions} */ 2 | export default { 3 | alias: { fd: 'docs: fix typos' }, 4 | messages: { 5 | type: '选择你要提交的类型 :', 6 | scope: '选择一个提交范围(可选):', 7 | customScope: '请输入自定义的提交范围 :', 8 | subject: '填写简短精炼的变更描述 :\n', 9 | body: '填写更加详细的变更描述(可选)。使用 \'|\' 换行 :\n', 10 | breaking: '列举非兼容性重大的变更(可选)。使用 \'|\' 换行 :\n', 11 | footerPrefixesSelect: '选择关联issue前缀(可选):', 12 | customFooterPrefix: '输入自定义issue前缀 :', 13 | footer: '列举关联issue (可选) 例如: #31, #I3244 :\n', 14 | confirmCommit: '是否提交或修改commit ?', 15 | }, 16 | types: [ 17 | { value: 'feat', name: 'feat: 新增功能 | A new feature', emoji: ':sparkles:' }, 18 | { value: 'fix', name: 'fix: 修复缺陷 | A bug fix', emoji: ':bug:' }, 19 | { value: 'docs', name: 'docs: 文档更新 | Documentation only changes', emoji: ':memo:' }, 20 | { value: 'style', name: 'style: 代码格式 | Changes that do not affect the meaning of the code', emoji: ':lipstick:' }, 21 | { value: 'refactor', name: 'refactor: 代码重构 | A code change that neither fixes a bug nor adds a feature', emoji: ':recycle:' }, 22 | { value: 'perf', name: 'perf: 性能提升 | A code change that improves performance', emoji: ':zap:' }, 23 | { value: 'test', name: 'test: 测试相关 | Adding missing tests or correcting existing tests', emoji: ':white_check_mark:' }, 24 | { value: 'build', name: 'build: 构建相关 | Changes that affect the build system or external dependencies', emoji: ':package:' }, 25 | { value: 'ci', name: 'ci: 持续集成 | Changes to our CI configuration files and scripts', emoji: ':ferris_wheel:' }, 26 | { value: 'chore', name: 'chore: 其他修改 | Other changes that don\'t modify src or test files', emoji: ':hammer:' }, 27 | { value: 'revert', name: 'revert: 回退代码 | Reverts a previous commit', emoji: ':rewind:' }, 28 | ], 29 | useEmoji: false, 30 | emojiAlign: 'center', 31 | useAI: false, 32 | aiNumber: 1, 33 | themeColorCode: '', 34 | scopes: [], 35 | allowCustomScopes: true, 36 | allowEmptyScopes: true, 37 | customScopesAlign: 'bottom', 38 | customScopesAlias: 'custom', 39 | emptyScopesAlias: 'empty', 40 | upperCaseSubject: false, 41 | markBreakingChangeMode: false, 42 | allowBreakingChanges: ['feat', 'fix'], 43 | breaklineNumber: 100, 44 | breaklineChar: '|', 45 | skipQuestions: [], 46 | issuePrefixes: [{ value: 'closed', name: 'closed: 标记 ISSUES 已完成' }], 47 | customIssuePrefixAlign: 'top', 48 | emptyIssuePrefixAlias: 'skip', 49 | customIssuePrefixAlias: 'custom', 50 | allowCustomIssuePrefix: true, 51 | allowEmptyIssuePrefix: true, 52 | confirmColorize: true, 53 | minSubjectLength: 0, 54 | defaultBody: '', 55 | defaultIssues: '', 56 | defaultScope: '', 57 | defaultSubject: '', 58 | }; 59 | -------------------------------------------------------------------------------- /env/.env: -------------------------------------------------------------------------------- 1 | # 页面标题 2 | VITE_APP_TITLE=uniapp-vue3模板项目 3 | 4 | # 开发环境配置 5 | VITE_APP_ENV=development 6 | 7 | # 接口地址 8 | VITE_API_BASE_URL=http://localhost:8080 9 | 10 | # 端口号 11 | VITE_APP_PORT=9527 12 | 13 | # h5是否需要配置代理 14 | VITE_APP_PROXY=true 15 | 16 | # API代理前缀 17 | VITE_API_PREFIX=/api 18 | 19 | # 删除console 20 | VITE_DROP_CONSOLE=false 21 | -------------------------------------------------------------------------------- /env/.env.development: -------------------------------------------------------------------------------- 1 | # 开发环境配置 2 | VITE_APP_ENV=development 3 | 4 | # 接口地址 5 | VITE_API_BASE_URL=http://localhost:8080 6 | 7 | # 删除console 8 | VITE_DROP_CONSOLE=false 9 | -------------------------------------------------------------------------------- /env/.env.production: -------------------------------------------------------------------------------- 1 | # 生产环境配置 2 | VITE_APP_ENV=production 3 | 4 | # 接口地址 5 | VITE_API_BASE_URL=http://localhost:8080/prod 6 | 7 | # 删除console 8 | VITE_DROP_CONSOLE=true 9 | -------------------------------------------------------------------------------- /env/.env.test: -------------------------------------------------------------------------------- 1 | # 预发布环境配置 2 | VITE_APP_ENV=staging 3 | 4 | # 接口地址 5 | VITE_API_BASE_URL=http://localhost:8080/staging 6 | 7 | # 删除console 8 | VITE_DROP_CONSOLE=true 9 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config'; 2 | 3 | export default antfu( 4 | { 5 | unocss: true, 6 | ignores: [ 7 | 'dist/**', 8 | '.vscode/**', 9 | '.idea/**', 10 | 'node_modules/**', 11 | 'src/uni_modules/**', 12 | 'src/manifest.json', 13 | 'src/pages.json', 14 | 'README.md', 15 | ], 16 | }, 17 | { 18 | rules: { 19 | // vue顶级标签的顺序 20 | 'vue/block-order': ['error', { 21 | order: ['template', 'script', 'style'], 22 | }], 23 | // 需要尾随逗号 24 | 'comma-dangle': ['error', 'only-multiline'], 25 | // 允许console 26 | 'no-console': 'off', 27 | // 需要分号 28 | 'style/semi': ['error', 'always'], 29 | // 块内的空行 30 | 'padded-blocks': ['error', 'never'], 31 | // 顶级函数应使用 function 关键字声明 32 | 'antfu/top-level-function': 'off', 33 | // 全局的 process 不能用 34 | 'node/prefer-global/process': 'off', 35 | // 禁止未使用的捕获组 36 | 'regexp/no-unused-capturing-group': 'off', 37 | // 允许接口和类型别名中的成员之间使用三个分隔符 38 | 'style/member-delimiter-style': ['error', { 39 | multiline: { 40 | delimiter: 'semi', 41 | requireLast: true, 42 | }, 43 | singleline: { 44 | delimiter: 'semi', 45 | requireLast: false, 46 | }, 47 | multilineDetection: 'brackets', 48 | }], 49 | // if 语句后需要换行 50 | 'antfu/if-newline': 'off', 51 | }, 52 | }, 53 | ); 54 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uniapp-vue3-project", 3 | "type": "module", 4 | "version": "1.4.0", 5 | "description": "uniapp 团队协作开发实践模板(Vue3)", 6 | "author": { 7 | "name": "江阳小道", 8 | "email": "oyjt001@gmail.com", 9 | "github": "https://github.com/oyjt" 10 | }, 11 | "license": "MIT", 12 | "homepage": "https://github.com/oyjt/uniapp-vue3-template", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/oyjt/uniapp-vue3-template.git" 16 | }, 17 | "keywords": [ 18 | "Vue3", 19 | "uniapp", 20 | "uniapp-vue3-template", 21 | "Vite5", 22 | "TypeScript", 23 | "uview-plus", 24 | "uniapp template", 25 | "UnoCSS" 26 | ], 27 | "engines": { 28 | "node": ">=18", 29 | "pnpm": ">=8" 30 | }, 31 | "scripts": { 32 | "preinstall": "npx only-allow pnpm", 33 | "uvm": "npx @dcloudio/uvm@latest", 34 | "uvm-rm": "node ./scripts/post-upgrade.js", 35 | "dev:h5": "uni", 36 | "dev:h5:ssr": "uni --ssr", 37 | "dev:h5-test": "uni --mode test", 38 | "dev:h5-pro": "uni --mode production", 39 | "dev:mp-weixin": "uni -p mp-weixin", 40 | "dev:mp-weixin-test": "uni -p mp-weixin --mode test", 41 | "dev:mp-weixin-prod": "uni -p mp-weixin --mode production", 42 | "dev:app": "uni -p app", 43 | "dev:app-android": "uni -p app-android", 44 | "dev:app-ios": "uni -p app-ios", 45 | "build:h5": "uni build", 46 | "build:h5:ssr": "uni build --ssr", 47 | "build:h5-test": "uni build --mode test", 48 | "build:h5-prod": "uni build --mode production", 49 | "build:mp-weixin": "uni build -p mp-weixin", 50 | "build:mp-weixin-test": "uni build -p mp-weixin --mode test", 51 | "build:mp-weixin-prod": "uni build -p mp-weixin --mode production", 52 | "build:app": "uni build -p app", 53 | "build:app-android": "uni build -p app-android", 54 | "build:app-ios": "uni build -p app-ios", 55 | "type-check": "vue-tsc --noEmit", 56 | "eslint": "eslint \"src/**/*.{js,jsx,ts,tsx,vue}\"", 57 | "eslint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx,vue}\" --fix", 58 | "stylelint": "stylelint \"src/**/*.{vue,scss,css,sass,less}\"", 59 | "stylelint:fix": "stylelint \"src/**/*.{vue,scss,css,sass,less}\" --fix", 60 | "cz": "git add . && npx czg", 61 | "postinstall": "simple-git-hooks", 62 | "clean": "npx rimraf node_modules", 63 | "clean:cache": "npx rimraf node_modules/.cache" 64 | }, 65 | "dependencies": { 66 | "@dcloudio/uni-app": "3.0.0-4060420250429001", 67 | "@dcloudio/uni-app-plus": "3.0.0-4060420250429001", 68 | "@dcloudio/uni-components": "3.0.0-4060420250429001", 69 | "@dcloudio/uni-h5": "3.0.0-4060420250429001", 70 | "@dcloudio/uni-mp-weixin": "3.0.0-4060420250429001", 71 | "dayjs": "^1.11.13", 72 | "pinia": "2.2.4", 73 | "pinia-plugin-persistedstate": "4.1.3", 74 | "uview-plus": "^3.4.28", 75 | "vue": "3.4.21", 76 | "vue-i18n": "9.1.9", 77 | "z-paging": "^2.8.4" 78 | }, 79 | "devDependencies": { 80 | "@antfu/eslint-config": "4.13.0", 81 | "@dcloudio/types": "^3.4.8", 82 | "@dcloudio/uni-automator": "3.0.0-4060420250429001", 83 | "@dcloudio/uni-cli-shared": "3.0.0-4060420250429001", 84 | "@dcloudio/uni-stacktracey": "3.0.0-4060420250429001", 85 | "@dcloudio/vite-plugin-uni": "3.0.0-4060420250429001", 86 | "@esbuild/darwin-arm64": "0.25.1", 87 | "@esbuild/darwin-x64": "0.25.1", 88 | "@iconify-json/mdi": "^1.2.3", 89 | "@rollup/rollup-darwin-arm64": "4.38.0", 90 | "@rollup/rollup-darwin-x64": "4.38.0", 91 | "@types/node": "^22.15.17", 92 | "@uni-helper/uni-app-types": "1.0.0-alpha.6", 93 | "@unocss/eslint-plugin": "^0.63.6", 94 | "@unocss/preset-icons": "^0.63.6", 95 | "czg": "^1.11.0", 96 | "eslint": "^9.26.0", 97 | "lint-staged": "^16.0.0", 98 | "miniprogram-api-typings": "^4.0.7", 99 | "picocolors": "^1.1.1", 100 | "rimraf": "^6.0.1", 101 | "rollup-plugin-visualizer": "^5.14.0", 102 | "sass": "1.79.6", 103 | "sass-loader": "^16.0.4", 104 | "simple-git-hooks": "^2.13.0", 105 | "stylelint": "^16.19.1", 106 | "stylelint-config-recess-order": "^6.0.0", 107 | "stylelint-config-standard": "^38.0.0", 108 | "stylelint-config-standard-vue": "^1.0.0", 109 | "typescript": "^5.8.3", 110 | "unocss": "0.63.6", 111 | "unocss-preset-weapp": "^66.0.1", 112 | "unplugin-auto-import": "0.19.0", 113 | "unplugin-vue-components": "^28.5.0", 114 | "vite": "5.2.8", 115 | "vite-plugin-clean-build": "^1.4.1", 116 | "vite-plugin-replace-image-url": "^1.4.1", 117 | "vite-plugin-restart": "^0.4.2", 118 | "vue-tsc": "^2.2.10" 119 | }, 120 | "simple-git-hooks": { 121 | "pre-commit": "npx lint-staged", 122 | "commit-msg": "node ./scripts/verify-commit.js" 123 | }, 124 | "lint-staged": { 125 | "src/**/*.{js,jsx,ts,tsx}": "eslint --fix", 126 | "*.{scss,css,style,html}": "stylelint --fix", 127 | "*.vue": [ 128 | "eslint --fix", 129 | "stylelint --fix" 130 | ] 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /scripts/post-upgrade.js: -------------------------------------------------------------------------------- 1 | // # 执行 `pnpm upgrade` 后会升级 `uniapp` 相关依赖 2 | // # 在升级完后,会自动添加很多无用依赖,这需要删除以减小依赖包体积 3 | // # 只需要执行下面的命令即可 4 | 5 | import { exec } from 'node:child_process'; 6 | 7 | // 定义要执行的命令 8 | const dependencies = [ 9 | '@dcloudio/uni-app-harmony', 10 | // TODO: 如果需要某个平台的小程序,请手动删除或注释掉 11 | '@dcloudio/uni-mp-alipay', 12 | '@dcloudio/uni-mp-baidu', 13 | '@dcloudio/uni-mp-jd', 14 | '@dcloudio/uni-mp-kuaishou', 15 | '@dcloudio/uni-mp-lark', 16 | '@dcloudio/uni-mp-qq', 17 | '@dcloudio/uni-mp-toutiao', 18 | '@dcloudio/uni-mp-xhs', 19 | '@dcloudio/uni-quickapp-webview', 20 | '@dcloudio/uni-mp-harmony', 21 | // vue 已经内置了 @vue/runtime-core,这里移除掉 22 | '@vue/runtime-core', 23 | ]; 24 | 25 | // 使用exec执行命令 26 | exec(`pnpm remove ${dependencies.join(' ')}`, (error, stdout, stderr) => { 27 | if (error) { 28 | // 如果有错误,打印错误信息 29 | console.error(`执行出错: ${error}`); 30 | return; 31 | } 32 | // 打印正常输出 33 | console.log(`stdout: ${stdout}`); 34 | // 如果有错误输出,也打印出来 35 | console.error(`stderr: ${stderr}`); 36 | }); 37 | -------------------------------------------------------------------------------- /scripts/verify-commit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 提交信息校验 3 | * @link https://github.com/toplenboren/simple-git-hooks 4 | * @see 参考:https://github.com/vuejs/vue-next/blob/master/.github/commit-convention.md 5 | */ 6 | import { readFileSync } from 'node:fs'; 7 | import path from 'node:path'; 8 | import process from 'node:process'; 9 | import pico from 'picocolors'; 10 | 11 | const msgPath = path.resolve('.git/COMMIT_EDITMSG'); 12 | const msg = readFileSync(msgPath, 'utf-8').trim(); 13 | 14 | const commitRE 15 | = /^(?:revert: )?(?:feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|mod|release|strengthen)(?:\(.+\))?: .{1,50}/; 16 | 17 | if (!commitRE.test(msg)) { 18 | console.log(pico.yellow(`\n提交的信息: ${msg}\n`)); 19 | console.error( 20 | ` ${pico.white(pico.bgRed(' 格式错误 '))} ${pico.red( 21 | '无效的提交信息格式.', 22 | )}\n\n${ 23 | pico.red(' 正确的提交消息格式. 例如:\n\n') 24 | } ${pico.green('feat: add a new feature')}\n` 25 | + ` ${pico.green('fix: fixed an bug')}`, 26 | ); 27 | process.exit(1); 28 | } 29 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /src/api/common/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 通用接口 3 | */ 4 | import type { SendCodeReq, SendCodeRes, UploadRes } from './types'; 5 | import { post, upload } from '@/utils/request'; 6 | 7 | // 文件上传 8 | export const uploadFile = (filePath: string) => 9 | upload('/common/upload', { filePath, name: 'file' }); 10 | 11 | // 发送验证码 12 | export const sendCode = (data: SendCodeReq) => post('/sendCode', { data }); 13 | -------------------------------------------------------------------------------- /src/api/common/types.ts: -------------------------------------------------------------------------------- 1 | export interface CommonReq { 2 | [key: string]: any; 3 | } 4 | 5 | export interface CommonRes { 6 | [key: string]: any; 7 | } 8 | 9 | export interface UploadRes { 10 | file: string; 11 | url: string; 12 | } 13 | 14 | export interface SendCodeReq { 15 | phone: number; 16 | code: number; 17 | } 18 | 19 | export interface SendCodeRes { 20 | code: number; 21 | } 22 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import * as CommonApi from './common'; 2 | import * as UserApi from './user'; 3 | 4 | export { CommonApi, UserApi }; 5 | -------------------------------------------------------------------------------- /src/api/user/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 用户信息相关接口 3 | */ 4 | import type { CommonRes } from '@/api/common/types'; 5 | import type { LoginByCodeReq, LoginByCodeRes, LoginReq, LoginRes, ProfileReq, ProfileRes } from './types'; 6 | import { get, post } from '@/utils/request'; 7 | 8 | /** 获取用户信息 */ 9 | export const profile = (params?: ProfileReq) => get('/user/profile', { params }); 10 | 11 | /** 登录 */ 12 | export const login = (data: LoginReq) => post('/user/login', { data, custom: { auth: false } }); 13 | 14 | /** 验证码登录 */ 15 | export const loginByCode = (data: LoginByCodeReq) => post('/user/loginByCode', { data }); 16 | 17 | /** 退出登录 */ 18 | export const logout = () => post('/user/logout'); 19 | -------------------------------------------------------------------------------- /src/api/user/types.ts: -------------------------------------------------------------------------------- 1 | export interface ProfileReq { 2 | user_id?: string; 3 | } 4 | 5 | export interface ProfileRes { 6 | user_id?: string; 7 | user_name?: string; 8 | avatar?: string; 9 | token?: string; 10 | } 11 | 12 | export interface LoginReq { 13 | phone: string; 14 | code: string; 15 | } 16 | 17 | export interface LoginRes { 18 | token: string; 19 | user_id: number; 20 | user_name: string; 21 | avatar: string; 22 | } 23 | 24 | export interface LoginByCodeReq { 25 | code: string; 26 | } 27 | 28 | export interface LoginByCodeRes { 29 | [key: string]: any; 30 | } 31 | -------------------------------------------------------------------------------- /src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyjt/uniapp-vue3-template/9dd7ce3b935161f0e1e189784d9237d946d4497c/src/components/.gitkeep -------------------------------------------------------------------------------- /src/components/agree-privacy/index.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 145 | 146 | 215 | -------------------------------------------------------------------------------- /src/components/lang-select/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import useClipboard from './use-clipboard'; 2 | import useLoading from './use-loading'; 3 | import useLocation from './use-location'; 4 | import useModal from './use-modal'; 5 | import usePermission from './use-permission'; 6 | import useShare from './use-share'; 7 | 8 | export { useClipboard, useLoading, useLocation, useModal, usePermission, useShare }; 9 | -------------------------------------------------------------------------------- /src/hooks/use-clipboard/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 剪切板 3 | * @example 4 | * const {setClipboardData, getClipboardData} = useClipboard() 5 | * // 设置剪切板 6 | * setClipboardData({data: '1234567890'}) 7 | * // 获取剪切板 8 | * const data = await getClipboardData() 9 | */ 10 | export default function useClipboard() { 11 | const setClipboardData = ({ data, showToast = true }: UniApp.SetClipboardDataOptions) => { 12 | return new Promise((resolve, reject) => { 13 | uni.setClipboardData({ 14 | data, 15 | showToast, 16 | success: ({ data }) => resolve(data), 17 | fail: error => reject(error), 18 | }); 19 | }); 20 | }; 21 | const getClipboardData = () => { 22 | return new Promise((resolve, reject) => { 23 | uni.getClipboardData({ 24 | success: ({ data }) => resolve(data), 25 | fail: error => reject(error), 26 | }); 27 | }); 28 | }; 29 | return { 30 | setClipboardData, 31 | getClipboardData, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/hooks/use-loading/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * loading 提示框 3 | * @example 4 | * const {showLoading, hideLoading} = useLoading() 5 | * // 显示loading 6 | * showLoading() 7 | * // 隐藏loading 8 | * hideLoading() 9 | */ 10 | export default function useLoading() { 11 | const showLoading = (content = '加载中') => { 12 | uni.showLoading({ 13 | title: content, 14 | mask: true, 15 | }); 16 | }; 17 | const hideLoading = () => { 18 | uni.hideLoading(); 19 | }; 20 | return { 21 | showLoading, 22 | hideLoading, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/use-location/index.ts: -------------------------------------------------------------------------------- 1 | import type { AddressInfo, LocationInfo, LocationOptions } from './types'; 2 | 3 | /** 4 | * 定位hooks,提供定位相关功能 5 | * - 获取位置 6 | * - 位置监听 7 | * - 地址解析 8 | * - 距离计算 9 | */ 10 | export default function useLocation() { 11 | // 当前位置信息 12 | const location = ref(null); 13 | 14 | // 定位状态 15 | const isLocating = ref(false); 16 | 17 | // 是否正在监听位置 18 | const isWatching = ref(false); 19 | 20 | // 定位错误信息 21 | const error = ref(null); 22 | 23 | // 历史位置 24 | const historyLocations = ref([]); 25 | 26 | // 监听位置的定时器ID 27 | let watchId: number | null = null; 28 | 29 | /** 30 | * 获取当前位置 31 | * @param options 定位选项 32 | */ 33 | const getLocation = (options: LocationOptions = {}) => { 34 | isLocating.value = true; 35 | error.value = null; 36 | 37 | const defaultOptions: LocationOptions = { 38 | type: 'gcj02', 39 | altitude: false, 40 | isHighAccuracy: false, 41 | }; 42 | 43 | const finalOptions = { ...defaultOptions, ...options }; 44 | 45 | return new Promise((resolve, reject) => { 46 | uni.getLocation({ 47 | type: finalOptions.type, 48 | altitude: finalOptions.altitude, 49 | isHighAccuracy: finalOptions.isHighAccuracy, 50 | highAccuracyExpireTime: finalOptions.highAccuracyExpireTime, 51 | success: (res) => { 52 | // 更新当前位置 53 | const locationData: LocationInfo = { 54 | ...res, 55 | timestamp: Date.now(), 56 | }; 57 | 58 | location.value = locationData; 59 | 60 | // 添加到历史记录 61 | historyLocations.value.push(locationData); 62 | 63 | // 只保留最近的20条记录 64 | if (historyLocations.value.length > 20) { 65 | historyLocations.value.shift(); 66 | } 67 | 68 | finalOptions.success && finalOptions.success(res); 69 | resolve(locationData); 70 | }, 71 | fail: (err) => { 72 | error.value = err; 73 | finalOptions.fail && finalOptions.fail(err); 74 | reject(err); 75 | }, 76 | complete: () => { 77 | isLocating.value = false; 78 | finalOptions.complete && finalOptions.complete(); 79 | }, 80 | }); 81 | }); 82 | }; 83 | 84 | /** 85 | * 使用地理编码获取地址信息 86 | * @param latitude 纬度 87 | * @param longitude 经度 88 | */ 89 | const getAddress = (latitude: number, longitude: number) => { 90 | return new Promise((resolve, reject) => { 91 | // #ifdef APP-PLUS 92 | uni.request({ 93 | url: `https://apis.map.qq.com/ws/geocoder/v1/?location=${latitude},${longitude}&key=YOUR_KEY`, 94 | success: (res: any) => { 95 | if (res.data && res.data.status === 0) { 96 | const addressComponent = res.data.result.address_component; 97 | const formattedAddress = res.data.result.formatted_addresses.recommend; 98 | 99 | const addressInfo: AddressInfo = { 100 | nation: addressComponent.nation, 101 | province: addressComponent.province, 102 | city: addressComponent.city, 103 | district: addressComponent.district, 104 | street: addressComponent.street, 105 | streetNum: addressComponent.street_number, 106 | poiName: res.data.result.poi_count > 0 ? res.data.result.pois[0].title : '', 107 | cityCode: res.data.result.ad_info.city_code, 108 | }; 109 | 110 | if (location.value) { 111 | location.value.address = addressInfo; 112 | location.value.formatted = formattedAddress; 113 | } 114 | 115 | resolve(addressInfo); 116 | } 117 | else { 118 | reject(new Error('获取地址信息失败')); 119 | } 120 | }, 121 | fail: (err) => { 122 | reject(err); 123 | }, 124 | }); 125 | // #endif 126 | 127 | // #ifndef APP-PLUS 128 | // 其他平台可以使用uni.getLocation的geocode参数获取(仅App和微信小程序支持) 129 | // 或者使用其他地图服务的API 130 | reject(new Error('当前平台不支持地址解析')); 131 | // #endif 132 | }); 133 | }; 134 | 135 | /** 136 | * 停止监听位置 137 | */ 138 | const stopWatchLocation = () => { 139 | if (watchId !== null) { 140 | clearInterval(watchId); 141 | watchId = null; 142 | } 143 | 144 | isWatching.value = false; 145 | }; 146 | 147 | /** 148 | * 开始监听位置变化 149 | * @param options 定位选项 150 | * @param interval 监听间隔,单位毫秒 151 | */ 152 | const watchLocation = (options: LocationOptions = {}, interval: number = 5000) => { 153 | // 已经在监听,先停止 154 | if (isWatching.value) { 155 | stopWatchLocation(); 156 | } 157 | 158 | isWatching.value = true; 159 | 160 | // 首次定位 161 | getLocation(options).catch((err) => { 162 | console.error('监听位置首次定位失败', err); 163 | }); 164 | 165 | // 定时获取位置 166 | watchId = window.setInterval(() => { 167 | if (isWatching.value) { 168 | getLocation(options).catch((err) => { 169 | console.error('监听位置更新失败', err); 170 | }); 171 | } 172 | }, interval); 173 | 174 | return watchId; 175 | }; 176 | 177 | /** 178 | * 计算两点间距离(米) 179 | * @param lat1 第一个点的纬度 180 | * @param lon1 第一个点的经度 181 | * @param lat2 第二个点的纬度 182 | * @param lon2 第二个点的经度 183 | * @returns 距离,单位:米 184 | */ 185 | const calculateDistance = (lat1: number, lon1: number, lat2: number, lon2: number): number => { 186 | const R = 6371000; // 地球半径,单位米 187 | const dLat = ((lat2 - lat1) * Math.PI) / 180; 188 | const dLon = ((lon2 - lon1) * Math.PI) / 180; 189 | 190 | const a 191 | = Math.sin(dLat / 2) * Math.sin(dLat / 2) 192 | + Math.cos((lat1 * Math.PI) / 180) 193 | * Math.cos((lat2 * Math.PI) / 180) 194 | * Math.sin(dLon / 2) 195 | * Math.sin(dLon / 2); 196 | 197 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 198 | const distance = R * c; 199 | 200 | return distance; 201 | }; 202 | 203 | /** 204 | * 获取当前位置到目标位置的距离 205 | * @param targetLat 目标位置纬度 206 | * @param targetLon 目标位置经度 207 | * @returns 距离,单位:米,如果当前没有位置信息则返回-1 208 | */ 209 | const getDistanceFromCurrent = (targetLat: number, targetLon: number): number => { 210 | if (!location.value) { 211 | return -1; 212 | } 213 | 214 | return calculateDistance( 215 | location.value.latitude, 216 | location.value.longitude, 217 | targetLat, 218 | targetLon, 219 | ); 220 | }; 221 | 222 | /** 223 | * 格式化距离显示 224 | * @param distance 距离,单位:米 225 | * @returns 格式化后的距离字符串 226 | */ 227 | const formatDistance = (distance: number): string => { 228 | if (distance < 0) { 229 | return '未知距离'; 230 | } 231 | else if (distance < 1000) { 232 | return `${Math.round(distance)}米`; 233 | } 234 | else { 235 | return `${(distance / 1000).toFixed(1)}公里`; 236 | } 237 | }; 238 | 239 | /** 240 | * 打开导航 241 | * @param latitude 目标纬度 242 | * @param longitude 目标经度 243 | * @param name 目标名称 244 | * @param address 目标地址 245 | */ 246 | const openLocation = ( 247 | latitude: number, 248 | longitude: number, 249 | name: string = '', 250 | address: string = '', 251 | ) => { 252 | return new Promise((resolve, reject) => { 253 | uni.openLocation({ 254 | latitude, 255 | longitude, 256 | name, 257 | address, 258 | success: () => resolve(), 259 | fail: err => reject(err), 260 | }); 261 | }); 262 | }; 263 | 264 | /** 265 | * 选择位置 266 | */ 267 | const chooseLocation = () => { 268 | return new Promise((resolve, reject) => { 269 | uni.chooseLocation({ 270 | success: (res) => { 271 | // 更新当前位置 272 | if (res.latitude && res.longitude) { 273 | const locationData: LocationInfo = { 274 | latitude: res.latitude, 275 | longitude: res.longitude, 276 | accuracy: 0, 277 | verticalAccuracy: 0, 278 | horizontalAccuracy: 0, 279 | altitude: 0, 280 | speed: 0, 281 | timestamp: Date.now(), 282 | address: { 283 | province: '', 284 | city: '', 285 | district: '', 286 | street: '', 287 | poiName: res.name, 288 | }, 289 | formatted: res.address, 290 | }; 291 | 292 | location.value = locationData; 293 | } 294 | 295 | resolve(res); 296 | }, 297 | fail: err => reject(err), 298 | }); 299 | }); 300 | }; 301 | 302 | // 自动清理 303 | onUnmounted(() => { 304 | stopWatchLocation(); 305 | }); 306 | 307 | return { 308 | // 状态 309 | location, 310 | isLocating, 311 | isWatching, 312 | error, 313 | historyLocations, 314 | 315 | // 方法 316 | getLocation, 317 | getAddress, 318 | watchLocation, 319 | stopWatchLocation, 320 | calculateDistance, 321 | getDistanceFromCurrent, 322 | formatDistance, 323 | openLocation, 324 | chooseLocation, 325 | }; 326 | } 327 | -------------------------------------------------------------------------------- /src/hooks/use-location/types.ts: -------------------------------------------------------------------------------- 1 | // 定位选项 2 | export interface LocationOptions { 3 | type?: 'wgs84' | 'gcj02'; 4 | altitude?: boolean; 5 | isHighAccuracy?: boolean; 6 | highAccuracyExpireTime?: number; 7 | success?: (res: UniApp.GetLocationSuccess) => void; 8 | fail?: (err: any) => void; 9 | complete?: () => void; 10 | } 11 | 12 | // 地址信息 13 | export interface AddressInfo { 14 | nation?: string; 15 | province?: string; 16 | city?: string; 17 | district?: string; 18 | street?: string; 19 | streetNum?: string; 20 | poiName?: string; 21 | postalCode?: string; 22 | cityCode?: string; 23 | } 24 | 25 | // 位置信息 26 | export interface LocationInfo extends UniApp.GetLocationSuccess { 27 | address?: AddressInfo; 28 | formatted?: string; 29 | timestamp?: number; 30 | } 31 | -------------------------------------------------------------------------------- /src/hooks/use-modal/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Dialog 提示框 3 | * @example 4 | * const {showModal} = useModal() 5 | * showModal('提示内容') 6 | */ 7 | export default function useModal() { 8 | const showModal = (content: string, options: UniApp.ShowModalOptions) => { 9 | return new Promise((resolve, reject) => { 10 | uni.showModal({ 11 | title: '温馨提示', 12 | content, 13 | showCancel: false, 14 | confirmColor: '#1677FF', 15 | success: res => resolve(res), 16 | fail: () => reject(new Error('Alert 调用失败 !')), 17 | ...options, 18 | }); 19 | }); 20 | }; 21 | return { 22 | showModal, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/use-permission/index.ts: -------------------------------------------------------------------------------- 1 | import { hasPerm } from '@/plugins/permission'; 2 | import { currentRoute } from '@/router'; 3 | 4 | // 对某些特殊场景需要在页面onShow生命周期中校验权限: 5 | // 1.微信小程序端点击tabbar的底层逻辑不触发uni.switchTab 6 | // 2.h5在浏览器地址栏输入url后跳转不触发uni的路由api 7 | // 3.首次启动加载的页面不触发uni的路由api 8 | export default async function usePermission() { 9 | return hasPerm(currentRoute()); 10 | } 11 | -------------------------------------------------------------------------------- /src/hooks/use-share/index.ts: -------------------------------------------------------------------------------- 1 | import type { ShareOptions } from './types'; 2 | 3 | /** 4 | * 小程序分享 5 | * @param {object} options 6 | * @example 7 | * // 必须要调用onShareAppMessage,onShareTimeline才能正常分享 8 | * // 因为小程序平台,必须在注册页面时,主动配置onShareAppMessage, onShareTimeline才可以 9 | * // 组合式API是运行时才能注册,框架不可能默认给每个页面都开启这两个分享,所以必须在页面代码里包含这两个API的字符串,才会主动去注册。 10 | * // 相关说明链接:https://ask.dcloud.net.cn/question/150353 11 | * const {onShareAppMessage, onShareTimeline} = useShare({title: '分享标题', path: 'pages/index/index', query: 'id=1', imageUrl: 'https://xxx.png'}) 12 | * onShareAppMessage() 13 | * onShareTimeline() 14 | */ 15 | export default function useShare(options?: ShareOptions) { 16 | // #ifdef MP-WEIXIN 17 | const title = options?.title ?? ''; 18 | const path = options?.path ?? ''; 19 | const query = options?.query ?? ''; 20 | const imageUrl = options?.imageUrl ?? ''; 21 | 22 | const shareApp = (params: ShareOptions = {}) => { 23 | onShareAppMessage(() => { 24 | return { 25 | title, 26 | path: path ? `${path}${query ? `?${query}` : ''}` : '', 27 | imageUrl, 28 | ...params, 29 | }; 30 | }); 31 | }; 32 | 33 | const shareTime = (params: ShareOptions = {}) => { 34 | onShareTimeline(() => { 35 | return { 36 | title, 37 | query: options?.query ?? '', 38 | imageUrl, 39 | ...params, 40 | }; 41 | }); 42 | }; 43 | return { 44 | onShareAppMessage: shareApp, 45 | onShareTimeline: shareTime, 46 | }; 47 | // #endif 48 | } 49 | -------------------------------------------------------------------------------- /src/hooks/use-share/types.ts: -------------------------------------------------------------------------------- 1 | export interface ShareOptions { 2 | title?: string; 3 | path?: string; 4 | query?: string; 5 | imageUrl?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { createI18n } from 'vue-i18n'; 3 | import en from './langs/en'; 4 | import zhHans from './langs/zh-Hans'; 5 | 6 | const i18n = createI18n({ 7 | legacy: false, // 必须设置false才能使用Composition API 8 | globalInjection: true, // 为每个组件注入$为前缀的全局属性和函数 9 | locale: uni.getLocale(), 10 | messages: { 11 | en, 12 | 'zh-Hans': zhHans, 13 | }, 14 | }); 15 | 16 | function setupI18n(app: App) { 17 | app.use(i18n); 18 | } 19 | 20 | export { i18n }; 21 | export default setupI18n; 22 | -------------------------------------------------------------------------------- /src/locales/langs/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | locale: { 3 | 'auto': 'System', 4 | 'en': 'English', 5 | 'zh-hans': 'Chinese', 6 | }, 7 | home: { 8 | 'intro': 'Welcome to uni-app demo', 9 | 'toggle-langs': 'Change languages', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/locales/langs/zh-Hans.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | locale: { 3 | 'auto': '系统', 4 | 'en': '英语', 5 | 'zh-hans': '中文', 6 | }, 7 | home: { 8 | 'intro': '欢迎来到uni-app演示', 9 | 'toggle-langs': '切换语言', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import App from '@/App.vue'; 2 | import setupPlugins from '@/plugins'; 3 | import { createSSRApp } from 'vue'; 4 | // 引入UnoCSS 5 | import 'virtual:uno.css'; 6 | 7 | export function createApp() { 8 | const app = createSSRApp(App); 9 | app.use(setupPlugins); 10 | 11 | return { 12 | app, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "appid": "", 4 | "description": "", 5 | "versionName": "1.0.0", 6 | "versionCode": "100", 7 | "transformPx": false, 8 | /* 5+App特有相关 */ 9 | "app-plus": 10 | { 11 | "usingComponents": true, 12 | "nvueStyleCompiler": "uni-app", 13 | "compilerVersion": 3, 14 | "splashscreen": 15 | { 16 | "alwaysShowBeforeRender": true, 17 | "waiting": true, 18 | "autoclose": true, 19 | "delay": 0 20 | }, 21 | /* 模块配置 */ 22 | "modules": {}, 23 | /* 应用发布信息 */ 24 | "distribute": 25 | { 26 | /* android打包配置 */ 27 | "android": 28 | { 29 | "permissions": [ 30 | "", 31 | "", 32 | "", 33 | "", 34 | "", 35 | "", 36 | "", 37 | "", 38 | "", 39 | "", 40 | "", 41 | "", 42 | "", 43 | "", 44 | "" 45 | ] 46 | }, 47 | /* ios打包配置 */ 48 | "ios": {}, 49 | /* SDK配置 */ 50 | "sdkConfigs": {} 51 | } 52 | }, 53 | /* 快应用特有相关 */ 54 | "quickapp": {}, 55 | /* 小程序特有相关 */ 56 | "mp-weixin": 57 | { 58 | "appid": "", 59 | "setting": 60 | { 61 | "urlCheck": false 62 | }, 63 | "usingComponents": true 64 | }, 65 | "mp-alipay": 66 | { 67 | "usingComponents": true 68 | }, 69 | "mp-baidu": 70 | { 71 | "usingComponents": true 72 | }, 73 | "mp-toutiao": 74 | { 75 | "usingComponents": true 76 | }, 77 | "uniStatistics": 78 | { 79 | "enable": false 80 | }, 81 | "vueVersion": "3", 82 | "h5": 83 | { 84 | "router": 85 | { 86 | "mode": "hash", 87 | "base": "/uniapp-vue3-template/" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "easycom": { 3 | "custom": { 4 | "^u--(.*)": "uview-plus/components/u-$1/u-$1.vue", 5 | "^up-(.*)": "uview-plus/components/u-$1/u-$1.vue", 6 | "^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue", 7 | "^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue" 8 | } 9 | }, 10 | "pages": [ 11 | { 12 | "path": "pages/tab/home/index", 13 | "style": { 14 | "navigationBarTitleText": "首页" 15 | } 16 | }, 17 | { 18 | "path": "pages/tab/list/index", 19 | "style": { 20 | "navigationBarTitleText": "列表" 21 | } 22 | }, 23 | { 24 | "path": "pages/tab/user/index", 25 | "style": { 26 | "navigationStyle": "custom" 27 | }, 28 | "needLogin": true 29 | } 30 | ], 31 | "subPackages": [ 32 | { 33 | "root": "pages/common", 34 | "pages": [ 35 | { 36 | "path": "login/index", 37 | "style": { 38 | "navigationBarTitleText": "登录", 39 | "navigationStyle": "custom" 40 | } 41 | }, 42 | { 43 | "path": "webview/index", 44 | "style": { 45 | "navigationBarTitleText": "网页" 46 | } 47 | }, 48 | { 49 | "path": "404/index", 50 | "style": { 51 | "navigationBarTitleText": "404", 52 | "navigationStyle": "custom" 53 | } 54 | } 55 | ] 56 | } 57 | ], 58 | "preloadRule": { 59 | "pages/tab/home/index": { 60 | "network": "all", 61 | "packages": ["pages/common"] 62 | } 63 | }, 64 | "tabBar": { 65 | "color": "#1b233b", 66 | "selectedColor": "#21d59d", 67 | "borderStyle": "black", 68 | "backgroundColor": "#ffffff", 69 | "list": [{ 70 | "iconPath": "static/images/tabbar/icon_home.png", 71 | "selectedIconPath": "static/images/tabbar/icon_home_selected.png", 72 | "pagePath": "pages/tab/home/index", 73 | "text": "首页" 74 | }, { 75 | "iconPath": "static/images/tabbar/icon_list.png", 76 | "selectedIconPath": "static/images/tabbar/icon_list_selected.png", 77 | "pagePath": "pages/tab/list/index", 78 | "text": "列表" 79 | }, { 80 | "iconPath": "static/images/tabbar/icon_me.png", 81 | "selectedIconPath": "static/images/tabbar/icon_me_selected.png", 82 | "pagePath": "pages/tab/user/index", 83 | "text": "我的" 84 | }] 85 | }, 86 | "globalStyle": { 87 | "navigationBarTextStyle": "black", 88 | "navigationBarTitleText": "uni-app", 89 | "navigationBarBackgroundColor": "#F8F8F8", 90 | "backgroundColor": "#F8F8F8" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/pages/common/404/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | 36 | -------------------------------------------------------------------------------- /src/pages/common/login/index.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 125 | 126 | 181 | -------------------------------------------------------------------------------- /src/pages/common/webview/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/pages/tab/home/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 67 | -------------------------------------------------------------------------------- /src/pages/tab/list/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 63 | -------------------------------------------------------------------------------- /src/pages/tab/user/index.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 65 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import setupI18n from '@/locales'; 3 | import setupStore from '@/store'; 4 | import setupRequest from '@/utils/request'; 5 | import setupPermission from './permission'; 6 | import setupUI from './ui'; 7 | 8 | export default { 9 | install(app: App) { 10 | // UI扩展配置 11 | setupUI(app); 12 | // 状态管理 13 | setupStore(app); 14 | // 国际化 15 | setupI18n(app); 16 | // 路由拦截 17 | setupPermission(); 18 | // 网络请求 19 | setupRequest(); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/plugins/permission.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR404_PATH, 3 | isPathExists, 4 | LOGIN_PATH, 5 | removeQueryString, 6 | routes, 7 | } from '@/router'; 8 | import { isLogin } from '@/utils/auth'; 9 | 10 | // 白名单路由 11 | const whiteList = ['/']; 12 | routes.forEach((item) => { 13 | if (item.needLogin !== true) { 14 | whiteList.push(item.path); 15 | } 16 | }); 17 | 18 | /** 19 | * 权限校验 20 | * @param {string} path 21 | * @returns {boolean} 是否有权限 22 | */ 23 | export function hasPerm(path = '') { 24 | if (!isPathExists(path) && path !== '/') { 25 | uni.redirectTo({ 26 | url: ERROR404_PATH, 27 | }); 28 | return false; 29 | } 30 | // 在白名单中或有token,直接放行 31 | const hasPermission 32 | = whiteList.includes(removeQueryString(path)) || isLogin(); 33 | if (!hasPermission) { 34 | // 将用户的目标路径传递过去,这样可以实现用户登录之后,直接跳转到目标页面 35 | uni.redirectTo({ 36 | url: `${LOGIN_PATH}?redirect=${encodeURIComponent(path)}`, 37 | }); 38 | } 39 | return hasPermission; 40 | } 41 | 42 | function setupPermission() { 43 | // 注意:拦截uni.switchTab本身没有问题。但是在微信小程序端点击tabbar的底层逻辑并不是触发uni.switchTab。 44 | // 所以误认为拦截无效,此类场景的解决方案是在tabbar页面的页面生命周期onShow中处理。 45 | ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'].forEach((item) => { 46 | // https://uniapp.dcloud.net.cn/api/interceptor.html 47 | uni.addInterceptor(item, { 48 | // 页面跳转前进行拦截, invoke根据返回值进行判断是否继续执行跳转 49 | invoke(args) { 50 | // args为所拦截api中的参数,比如拦截的是uni.redirectTo(OBJECT),则args对应的是OBJECT参数 51 | return hasPerm(args.url); 52 | }, 53 | }); 54 | }); 55 | } 56 | 57 | export default setupPermission; 58 | -------------------------------------------------------------------------------- /src/plugins/ui.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import uviewPlus, { setConfig } from 'uview-plus'; 3 | 4 | function setupUI(app: App) { 5 | // 下面的在特殊场景下才需要配置,通常不用配置即可直接使用uview-plus框架。 6 | // 调用setConfig方法,方法内部会进行对象属性深度合并,可以放心嵌套配置 7 | // 需要在app.use(uview-plus)之后执行 8 | setConfig({ 9 | // 修改$u.config对象的属性 10 | config: { 11 | // 修改默认单位为rpx,相当于执行 uni.$u.config.unit = 'rpx' 12 | unit: 'px', 13 | }, 14 | // 修改$u.props对象的属性 15 | props: { 16 | // 修改radio组件的size参数的默认值,相当于执行 uni.$u.props.radio.size = 30 17 | radio: { 18 | // size: 20 19 | }, 20 | // 其他组件属性配置 21 | // ...... 22 | }, 23 | }); 24 | 25 | app.use(uviewPlus); 26 | } 27 | 28 | export default setupUI; 29 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import pagesJson from '@/pages.json'; 2 | 3 | // 路径常量 4 | export const HOME_PATH = '/pages/tab/home/index'; 5 | export const LOGIN_PATH = '/pages/common/login/index'; 6 | export const ERROR404_PATH = '/pages/common/404/index'; 7 | 8 | /** 9 | * 解析路由地址 10 | * @param {object} pagesJson 11 | * @returns [{"path": "/pages/tab/home/index","needLogin": false},...] 12 | */ 13 | function parseRoutes(pagesJson = {} as any) { 14 | if (!pagesJson.pages) { 15 | pagesJson.pages = []; 16 | } 17 | if (!pagesJson.subPackages) { 18 | pagesJson.subPackages = []; 19 | } 20 | 21 | function parsePages(pages = [] as any, rootPath = '') { 22 | const routes = []; 23 | for (let i = 0; i < pages.length; i++) { 24 | routes.push({ 25 | path: rootPath ? `/${rootPath}/${pages[i].path}` : `/${pages[i].path}`, 26 | needLogin: pages[i].needLogin === true, 27 | }); 28 | } 29 | return routes; 30 | } 31 | 32 | function parseSubPackages(subPackages = [] as any) { 33 | const routes = []; 34 | for (let i = 0; i < subPackages.length; i++) { 35 | routes.push(...parsePages(subPackages[i].pages, subPackages[i].root)); 36 | } 37 | return routes; 38 | } 39 | 40 | return [ 41 | ...parsePages(pagesJson.pages), 42 | ...parseSubPackages(pagesJson.subPackages), 43 | ]; 44 | } 45 | export const routes = parseRoutes(pagesJson); 46 | 47 | /** 48 | * 当前路由 49 | * @returns {string} 当前路由 50 | */ 51 | export function currentRoute() { 52 | // getCurrentPages() 至少有1个元素,所以不再额外判断 53 | const pages = getCurrentPages(); 54 | const currentPage = pages[pages.length - 1] as any; 55 | return currentPage?.$page?.fullPath || currentPage.route; 56 | } 57 | 58 | /** 59 | * 去除查询字符串 60 | * @param {string} path 61 | * @returns {string} 去除查询字符串后的路径 62 | */ 63 | export function removeQueryString(path = '') { 64 | return path.split('?')[0]; 65 | } 66 | 67 | /** 68 | * 路径是否存在 69 | * @param {string} path 70 | * @returns {boolean} 路径是否存在 71 | */ 72 | export function isPathExists(path = '') { 73 | const cleanPath = removeQueryString(path); 74 | return routes.some(item => item.path === cleanPath); 75 | } 76 | 77 | /** 78 | * 是否是tabbar页面路径 79 | * @param {string} path 80 | * @returns {boolean} 是否是tabbar页面 81 | */ 82 | export function isTabBarPath(path = '') { 83 | const cleanPath = removeQueryString(path); 84 | return ( 85 | pagesJson.tabBar?.list?.some( 86 | item => `/${item.pagePath}` === cleanPath, 87 | ) === true 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/static/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyjt/uniapp-vue3-template/9dd7ce3b935161f0e1e189784d9237d946d4497c/src/static/images/404.png -------------------------------------------------------------------------------- /src/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyjt/uniapp-vue3-template/9dd7ce3b935161f0e1e189784d9237d946d4497c/src/static/images/logo.png -------------------------------------------------------------------------------- /src/static/images/pay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyjt/uniapp-vue3-template/9dd7ce3b935161f0e1e189784d9237d946d4497c/src/static/images/pay.png -------------------------------------------------------------------------------- /src/static/images/tabbar/icon_home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyjt/uniapp-vue3-template/9dd7ce3b935161f0e1e189784d9237d946d4497c/src/static/images/tabbar/icon_home.png -------------------------------------------------------------------------------- /src/static/images/tabbar/icon_home_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyjt/uniapp-vue3-template/9dd7ce3b935161f0e1e189784d9237d946d4497c/src/static/images/tabbar/icon_home_selected.png -------------------------------------------------------------------------------- /src/static/images/tabbar/icon_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyjt/uniapp-vue3-template/9dd7ce3b935161f0e1e189784d9237d946d4497c/src/static/images/tabbar/icon_list.png -------------------------------------------------------------------------------- /src/static/images/tabbar/icon_list_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyjt/uniapp-vue3-template/9dd7ce3b935161f0e1e189784d9237d946d4497c/src/static/images/tabbar/icon_list_selected.png -------------------------------------------------------------------------------- /src/static/images/tabbar/icon_me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyjt/uniapp-vue3-template/9dd7ce3b935161f0e1e189784d9237d946d4497c/src/static/images/tabbar/icon_me.png -------------------------------------------------------------------------------- /src/static/images/tabbar/icon_me_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyjt/uniapp-vue3-template/9dd7ce3b935161f0e1e189784d9237d946d4497c/src/static/images/tabbar/icon_me_selected.png -------------------------------------------------------------------------------- /src/static/styles/common.scss: -------------------------------------------------------------------------------- 1 | page { 2 | font-size: $u-font-base; 3 | color: $u-main-color; 4 | background-color: $u-bg-color; 5 | } 6 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | // 数据持久化 4 | import { createPersistedState } from 'pinia-plugin-persistedstate'; 5 | 6 | // 导入子模块 7 | import useAppStore from './modules/app'; 8 | import useUserStore from './modules/user'; 9 | 10 | // 安装pinia状态管理插件 11 | function setupStore(app: App) { 12 | const store = createPinia(); 13 | 14 | const piniaPersist = createPersistedState({ 15 | storage: { 16 | getItem: uni.getStorageSync, 17 | setItem: uni.setStorageSync, 18 | }, 19 | }); 20 | store.use(piniaPersist); 21 | 22 | app.use(store); 23 | } 24 | 25 | // 导出模块 26 | export { useAppStore, useUserStore }; 27 | export default setupStore; 28 | -------------------------------------------------------------------------------- /src/store/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import type { AppState } from './types'; 2 | import { defineStore } from 'pinia'; 3 | 4 | const useAppStore = defineStore('app', { 5 | state: (): AppState => ({ 6 | systemInfo: {} as UniApp.GetSystemInfoResult, 7 | }), 8 | getters: { 9 | getSystemInfo(): UniApp.GetSystemInfoResult { 10 | return this.systemInfo; 11 | }, 12 | }, 13 | actions: { 14 | setSystemInfo(info: UniApp.GetSystemInfoResult) { 15 | this.systemInfo = info; 16 | }, 17 | initSystemInfo() { 18 | uni.getSystemInfo({ 19 | success: (res: UniApp.GetSystemInfoResult) => { 20 | this.setSystemInfo(res); 21 | }, 22 | fail: (err: any) => { 23 | console.error(err); 24 | }, 25 | }); 26 | }, 27 | checkUpdate() { 28 | const updateManager = uni.getUpdateManager(); 29 | updateManager.onCheckForUpdate((res: UniApp.OnCheckForUpdateResult) => { 30 | // 请求完新版本信息的回调 31 | 32 | console.log(res.hasUpdate); 33 | }); 34 | updateManager.onUpdateReady(() => { 35 | uni.showModal({ 36 | title: '更新提示', 37 | content: '新版本已经准备好,是否重启应用?', 38 | success(res) { 39 | if (res.confirm) { 40 | // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 41 | updateManager.applyUpdate(); 42 | } 43 | }, 44 | }); 45 | }); 46 | updateManager.onUpdateFailed((res: any) => { 47 | console.error(res); 48 | // 新的版本下载失败 49 | uni.showToast({ 50 | title: '更新失败', 51 | icon: 'error', 52 | }); 53 | }); 54 | }, 55 | }, 56 | }); 57 | 58 | export default useAppStore; 59 | -------------------------------------------------------------------------------- /src/store/modules/app/types.ts: -------------------------------------------------------------------------------- 1 | export interface AppState { 2 | systemInfo: UniApp.GetSystemInfoResult; 3 | } 4 | -------------------------------------------------------------------------------- /src/store/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import type { LoginReq } from '@/api/user/types'; 2 | import type { providerType, UserState } from './types'; 3 | import { UserApi } from '@/api'; 4 | import { clearToken, setToken } from '@/utils/auth'; 5 | 6 | import { defineStore } from 'pinia'; 7 | 8 | const useUserStore = defineStore('user', { 9 | state: (): UserState => ({ 10 | user_id: '', 11 | user_name: '江阳小道', 12 | avatar: '', 13 | token: '', 14 | }), 15 | getters: { 16 | userInfo(state: UserState): UserState { 17 | return { ...state }; 18 | }, 19 | }, 20 | actions: { 21 | // 设置用户的信息 22 | setInfo(partial: Partial) { 23 | this.$patch(partial); 24 | }, 25 | // 重置用户信息 26 | resetInfo() { 27 | this.$reset(); 28 | }, 29 | // 获取用户信息 30 | async info() { 31 | const result = await UserApi.profile(); 32 | this.setInfo(result); 33 | }, 34 | // 异步登录并存储token 35 | login(loginForm: LoginReq) { 36 | return new Promise((resolve, reject) => { 37 | UserApi.login(loginForm).then((res) => { 38 | const token = res.token; 39 | if (token) { 40 | setToken(token); 41 | } 42 | resolve(res); 43 | }).catch((error) => { 44 | reject(error); 45 | }); 46 | }); 47 | }, 48 | // Logout 49 | async logout() { 50 | await UserApi.logout(); 51 | this.resetInfo(); 52 | clearToken(); 53 | }, 54 | // 小程序授权登录 55 | authLogin(provider: providerType = 'weixin') { 56 | return new Promise((resolve, reject) => { 57 | uni.login({ 58 | provider, 59 | success: async (result: UniApp.LoginRes) => { 60 | if (result.code) { 61 | const res = await UserApi.loginByCode({ code: result.code }); 62 | resolve(res); 63 | } 64 | else { 65 | reject(new Error(result.errMsg)); 66 | } 67 | }, 68 | fail: (err: any) => { 69 | console.error(`login error: ${err}`); 70 | reject(err); 71 | }, 72 | }); 73 | }); 74 | }, 75 | }, 76 | persist: true, 77 | }); 78 | 79 | export default useUserStore; 80 | -------------------------------------------------------------------------------- /src/store/modules/user/types.ts: -------------------------------------------------------------------------------- 1 | export type RoleType = '' | '*' | 'user'; 2 | export interface UserState { 3 | user_id?: string; 4 | user_name?: string; 5 | avatar?: string; 6 | token?: string; 7 | } 8 | 9 | export type providerType = 10 | | 'weixin' 11 | | 'qq' 12 | | 'sinaweibo' 13 | | 'xiaomi' 14 | | 'apple' 15 | | 'univerify' 16 | | undefined; 17 | -------------------------------------------------------------------------------- /src/uni.scss: -------------------------------------------------------------------------------- 1 | @import 'uview-plus/theme.scss'; 2 | 3 | /* 颜色变量 */ 4 | 5 | /* 行为相关颜色 */ 6 | $u-primary: #21d59d; 7 | $u-primary-dark: #76a3fd; 8 | $u-success: #3ed268; 9 | $u-warning: #fe9831; 10 | $u-error: #fa4e62; 11 | 12 | /* 文字基本颜色 */ 13 | $u-main-color: #1b233b; 14 | $u-content-color: #60687e; 15 | $u-tips-color: #7e869a; 16 | $u-light-color: #bdc3d2; 17 | $u-disabled-color: #dce0eb; 18 | 19 | /* 背景颜色 */ 20 | $u-bg-color: #f1f7f7; 21 | 22 | /* 边框颜色 */ 23 | $u-border-color: #f2f7f7; 24 | 25 | /* 尺寸变量 */ 26 | 27 | /* 文字尺寸 */ 28 | $u-font-sm: 24rpx; 29 | $u-font-base: 28rpx; 30 | $u-font-lg: 32rpx; 31 | 32 | /* 图片尺寸 */ 33 | $u-img-sm: 40rpx; 34 | $u-img-base: 52rpx; 35 | $u-img-lg: 80rpx; 36 | 37 | /* Border Radius */ 38 | $u-border-radius-sm: 4rpx; 39 | $u-border-radius-base: 6rpx; 40 | $u-border-radius-lg: 12rpx; 41 | $u-border-radius-circle: 50%; 42 | 43 | /* 水平间距 */ 44 | $u-spacing-row-sm: 10rpx; 45 | $u-spacing-row-base: 20rpx; 46 | $u-spacing-row-lg: 30rpx; 47 | 48 | /* 垂直间距 */ 49 | $u-spacing-col-sm: 8rpx; 50 | $u-spacing-col-base: 16rpx; 51 | $u-spacing-col-lg: 24px; 52 | 53 | /* 透明度 */ 54 | $u-opacity-disabled: 0.3; 55 | -------------------------------------------------------------------------------- /src/utils/auth/index.ts: -------------------------------------------------------------------------------- 1 | const TokenKey = 'admin-token'; 2 | const TokenPrefix = 'Bearer '; 3 | function isLogin() { 4 | return !!uni.getStorageSync(TokenKey); 5 | } 6 | function getToken() { 7 | return uni.getStorageSync(TokenKey); 8 | } 9 | function setToken(token: string) { 10 | uni.setStorageSync(TokenKey, token); 11 | } 12 | function clearToken() { 13 | uni.removeStorageSync(TokenKey); 14 | } 15 | export { clearToken, getToken, isLogin, setToken, TokenPrefix }; 16 | -------------------------------------------------------------------------------- /src/utils/common/index.ts: -------------------------------------------------------------------------------- 1 | // 小程序更新检测 2 | export function mpUpdate() { 3 | const updateManager = uni.getUpdateManager(); 4 | updateManager.onCheckForUpdate((res) => { 5 | // 请求完新版本信息的回调 6 | console.log(res.hasUpdate); 7 | }); 8 | updateManager.onUpdateReady(() => { 9 | uni.showModal({ 10 | title: '更新提示', 11 | content: '检测到新版本,是否下载新版本并重启小程序?', 12 | success(res) { 13 | if (res.confirm) { 14 | // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 15 | updateManager.applyUpdate(); 16 | } 17 | }, 18 | }); 19 | }); 20 | updateManager.onUpdateFailed(() => { 21 | // 新的版本下载失败 22 | uni.showModal({ 23 | title: '已经有新版本了哟~', 24 | content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~', 25 | showCancel: false, 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth'; 2 | export * from './common'; 3 | export * from './modals'; 4 | export * from './request'; 5 | export * from './storage'; 6 | -------------------------------------------------------------------------------- /src/utils/modals/index.ts: -------------------------------------------------------------------------------- 1 | import type { ILoadingOptions, IShowModalOptions, IShowToastOptions } from './types'; 2 | 3 | /** 4 | * 轻提示 5 | * @param {string} content 提示内容 6 | * @param {object} option 配置 7 | */ 8 | export function Toast(content: string, option: IShowToastOptions = {}) { 9 | uni.showToast({ 10 | title: content, 11 | icon: 'none', 12 | mask: true, 13 | duration: 1500, 14 | ...option, 15 | }); 16 | } 17 | 18 | /** 19 | * Loading 提示框 20 | * @param {string} content 提示内容 21 | */ 22 | export const Loading: ILoadingOptions = { 23 | show: (content = '加载中') => { 24 | uni.showLoading({ 25 | title: content, 26 | mask: true, 27 | }); 28 | }, 29 | hide: () => { 30 | uni.hideLoading(); 31 | }, 32 | }; 33 | 34 | /** 35 | * Dialog 提示框 36 | * @param {string} content 提示内容 37 | * @param {object} option 配置 38 | */ 39 | export function Dialog(content: string, option: IShowModalOptions = {}) { 40 | option.showCancel = false; 41 | return new Promise((resolve, reject) => { 42 | uni.showModal({ 43 | title: '温馨提示', 44 | content, 45 | showCancel: false, 46 | confirmColor: '#1677FF', 47 | success(res) { 48 | if (res.confirm) 49 | resolve(res); 50 | }, 51 | fail() { 52 | reject(new Error('Alert 调用失败 !')); 53 | }, 54 | ...option, 55 | }); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/modals/types.ts: -------------------------------------------------------------------------------- 1 | export interface IShowToastOptions { 2 | title?: string; 3 | icon?: 'success' | 'loading' | 'error' | 'none'; 4 | image?: string; 5 | duration?: number; 6 | position?: 'top' | 'center' | 'bottom'; 7 | mask?: boolean; 8 | } 9 | 10 | export interface ILoadingOptions { 11 | show?: (content?: string) => void; 12 | hide?: () => void; 13 | } 14 | 15 | export interface IShowModalOptions { 16 | title?: string; 17 | content?: string; 18 | showCancel?: boolean; 19 | cancelText?: string; 20 | cancelColor?: string; 21 | confirmText?: string; 22 | confirmColor?: string; 23 | editable?: boolean; 24 | placeholderText?: string; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/request/index.ts: -------------------------------------------------------------------------------- 1 | // 引入配置 2 | import type { HttpRequestConfig, HttpResponse } from 'uview-plus/libs/luch-request/index'; 3 | import type { IResponse } from './types'; 4 | import Request from 'uview-plus/libs/luch-request/index'; 5 | import { requestInterceptors, responseInterceptors } from './interceptors'; 6 | 7 | const http = new Request(); 8 | 9 | // 引入拦截器配置 10 | export function setupRequest() { 11 | http.setConfig((defaultConfig: HttpRequestConfig) => { 12 | /* defaultConfig 为默认全局配置 */ 13 | defaultConfig.baseURL = import.meta.env.VITE_API_BASE_URL; 14 | // #ifdef H5 15 | if (import.meta.env.VITE_APP_PROXY === 'true') { 16 | defaultConfig.baseURL = import.meta.env.VITE_API_PREFIX; 17 | } 18 | // #endif 19 | return defaultConfig; 20 | }); 21 | requestInterceptors(http); 22 | responseInterceptors(http); 23 | } 24 | 25 | export function request(config: HttpRequestConfig): Promise { 26 | return new Promise((resolve, reject) => { 27 | http.request(config).then((res: HttpResponse>) => { 28 | console.log('[ res ] >', res); 29 | const { result } = res.data; 30 | resolve(result as T); 31 | }).catch((err: any) => { 32 | console.error('[ err ] >', err); 33 | reject(err); 34 | }); 35 | }); 36 | } 37 | 38 | export function get(url: string, config?: HttpRequestConfig): Promise { 39 | return request({ ...config, url, method: 'GET' }); 40 | } 41 | 42 | export function post(url: string, config?: HttpRequestConfig): Promise { 43 | return request({ ...config, url, method: 'POST' }); 44 | } 45 | 46 | export function upload(url: string, config?: HttpRequestConfig): Promise { 47 | return request({ ...config, url, method: 'UPLOAD' }); 48 | } 49 | 50 | export function download(url: string, config?: HttpRequestConfig): Promise { 51 | return request({ ...config, url, method: 'DOWNLOAD' }); 52 | } 53 | 54 | export default setupRequest; 55 | -------------------------------------------------------------------------------- /src/utils/request/interceptors.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | HttpError, 3 | HttpRequestAbstract, 4 | HttpRequestConfig, 5 | HttpResponse, 6 | } from 'uview-plus/libs/luch-request/index'; 7 | import { useUserStore } from '@/store'; 8 | import { getToken } from '@/utils/auth'; 9 | import storage from '@/utils/storage'; 10 | import { showMessage } from './status'; 11 | 12 | // 重试队列,每一项将是一个待执行的函数形式 13 | let requestQueue: (() => void)[] = []; 14 | 15 | // 防止重复提交 16 | const repeatSubmit = (config: HttpRequestConfig) => { 17 | const requestObj = { 18 | url: config.url, 19 | data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data, 20 | time: new Date().getTime(), 21 | }; 22 | const sessionObj = storage.getJSON('sessionObj'); 23 | if (!sessionObj) { 24 | storage.setJSON('sessionObj', requestObj); 25 | } 26 | else { 27 | const s_url = sessionObj.url; // 请求地址 28 | const s_data = sessionObj.data; // 请求数据 29 | const s_time = sessionObj.time; // 请求时间 30 | const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交 31 | if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { 32 | const message = '数据正在处理,请勿重复提交'; 33 | console.warn(`[${s_url}]: ${message}`); 34 | return Promise.reject(new Error(message)); 35 | } 36 | else { 37 | storage.setJSON('sessionObj', requestObj); 38 | } 39 | } 40 | }; 41 | 42 | // 是否正在刷新token的标记 43 | let isRefreshing: boolean = false; 44 | 45 | // 刷新token 46 | const refreshToken = async (http: HttpRequestAbstract, config: HttpRequestConfig) => { 47 | // 是否在获取token中,防止重复获取 48 | if (!isRefreshing) { 49 | // 修改登录状态为true 50 | isRefreshing = true; 51 | // 等待登录完成 52 | await useUserStore().authLogin(); 53 | // 登录完成之后,开始执行队列请求 54 | requestQueue.forEach(cb => cb()); 55 | // 重试完了清空这个队列 56 | requestQueue = []; 57 | isRefreshing = false; 58 | // 重新执行本次请求 59 | return http.request(config); 60 | } 61 | 62 | return new Promise>((resolve) => { 63 | // 将resolve放进队列,用一个函数形式来保存,等登录后直接执行 64 | requestQueue.push(() => { 65 | resolve(http.request(config)); 66 | }); 67 | }); 68 | }; 69 | 70 | function requestInterceptors(http: HttpRequestAbstract) { 71 | /** 72 | * 请求拦截 73 | * @param {object} http 74 | */ 75 | http.interceptors.request.use( 76 | (config: HttpRequestConfig) => { 77 | // 可使用async await 做异步操作 78 | // 初始化请求拦截器时,会执行此方法,此时data为undefined,赋予默认{} 79 | config.data = config.data || {}; 80 | // 自定义参数 81 | const custom = config?.custom; 82 | 83 | // 是否需要设置 token 84 | const isToken = custom?.auth === false; 85 | if (getToken() && !isToken && config.header) { 86 | // token设置 87 | config.header.token = getToken(); 88 | } 89 | 90 | // 是否显示 loading 91 | if (custom?.loading) { 92 | uni.showLoading({ 93 | title: '加载中', 94 | mask: true, 95 | }); 96 | } 97 | 98 | // 是否需要防止数据重复提交 99 | const isRepeatSubmit = custom?.repeatSubmit === false; 100 | if (!isRepeatSubmit && (config.method === 'POST' || config.method === 'UPLOAD')) { 101 | repeatSubmit(config); 102 | } 103 | return config; 104 | }, 105 | (config: any) => // 可使用async await 做异步操作 106 | Promise.reject(config), 107 | ); 108 | } 109 | function responseInterceptors(http: HttpRequestAbstract) { 110 | /** 111 | * 响应拦截 112 | * @param {object} http 113 | */ 114 | http.interceptors.response.use((response: HttpResponse) => { 115 | /* 对响应成功做点什么 可使用async await 做异步操作 */ 116 | const data = response.data; 117 | // 配置参数 118 | const config = response.config; 119 | // 自定义参数 120 | const custom = config?.custom; 121 | 122 | // 登录状态失效,重新登录 123 | if (data.code === 401) { 124 | return refreshToken(http, config); 125 | } 126 | 127 | // 隐藏loading 128 | if (custom?.loading) { 129 | uni.hideLoading(); 130 | } 131 | 132 | // 请求成功则返回结果 133 | if (data.code === 200) { 134 | return response || {}; 135 | } 136 | 137 | // 如果没有显式定义custom的toast参数为false的话,默认对报错进行toast弹出提示 138 | if (custom?.toast !== false) { 139 | uni.$u.toast(data.message); 140 | } 141 | 142 | // 请求失败则抛出错误 143 | return Promise.reject(data); 144 | }, (response: HttpError) => { 145 | // 自定义参数 146 | const custom = response.config?.custom; 147 | 148 | // 隐藏loading 149 | if (custom?.loading !== false) { 150 | uni.hideLoading(); 151 | } 152 | 153 | // 如果没有显式定义custom的toast参数为false的话,默认对报错进行toast弹出提示 154 | if (custom?.toast !== false) { 155 | const message = response.statusCode ? showMessage(response.statusCode) : '网络连接异常,请稍后再试!'; 156 | uni.$u.toast(message); 157 | } 158 | 159 | return Promise.reject(response); 160 | }); 161 | } 162 | 163 | export { requestInterceptors, responseInterceptors }; 164 | -------------------------------------------------------------------------------- /src/utils/request/status.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 根据状态码,生成对应的错误信息 3 | * @param {number|string} status 状态码 4 | * @returns {string} 错误信息 5 | */ 6 | export const showMessage = (status: number | string): string => { 7 | let message = ''; 8 | switch (status) { 9 | case 400: 10 | message = '请求错误(400)'; 11 | break; 12 | case 401: 13 | message = '未授权,请重新登录(401)'; 14 | break; 15 | case 403: 16 | message = '拒绝访问(403)'; 17 | break; 18 | case 404: 19 | message = '请求出错(404)'; 20 | break; 21 | case 408: 22 | message = '请求超时(408)'; 23 | break; 24 | case 500: 25 | message = '服务器错误(500)'; 26 | break; 27 | case 501: 28 | message = '服务未实现(501)'; 29 | break; 30 | case 502: 31 | message = '网络错误(502)'; 32 | break; 33 | case 503: 34 | message = '服务不可用(503)'; 35 | break; 36 | case 504: 37 | message = '网络超时(504)'; 38 | break; 39 | case 505: 40 | message = 'HTTP版本不受支持(505)'; 41 | break; 42 | default: 43 | message = `连接出错(${status})!`; 44 | } 45 | return `${message},请检查网络或联系管理员!`; 46 | }; 47 | -------------------------------------------------------------------------------- /src/utils/request/types.ts: -------------------------------------------------------------------------------- 1 | // 返回res.data的interface 2 | export interface IResponse { 3 | code: number | string; 4 | result: T; 5 | message: string; 6 | status: string | number; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | const storage = { 2 | set(key: string | null, value: string | null) { 3 | if (key !== null && value !== null) 4 | uni.setStorageSync(key, value); 5 | }, 6 | get(key: string | null) { 7 | if (key === null) 8 | return null; 9 | 10 | return uni.getStorageSync(key); 11 | }, 12 | setJSON(key: any, jsonValue: any) { 13 | if (jsonValue !== null) 14 | this.set(key, JSON.stringify(jsonValue)); 15 | }, 16 | getJSON(key: any) { 17 | const value = this.get(key); 18 | if (value) return JSON.parse(value); 19 | }, 20 | remove(key: string) { 21 | uni.removeStorageSync(key); 22 | }, 23 | }; 24 | 25 | export default storage; 26 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: [ 3 | 'stylelint-config-standard', 4 | 'stylelint-config-standard-vue', 5 | 'stylelint-config-recess-order', 6 | ], 7 | ignoreFiles: [ 8 | 'dist/**', 9 | 'src/uni_modules/**', 10 | 'node_modules/**', 11 | ], 12 | rules: { 13 | // 禁止空代码 14 | 'no-empty-source': null, 15 | // 禁止在覆盖高特异性选择器之后出现低特异性选择器 16 | 'no-descending-specificity': null, 17 | // 不允许未知单位 18 | 'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }], 19 | // 禁止空注释 20 | 'comment-no-empty': true, 21 | // @import 规则必须始终使用字符串表示法。 22 | 'import-notation': 'string', 23 | // 未知的 @ 规则 24 | 'at-rule-no-unknown': [ 25 | true, 26 | { 27 | ignoreAtRules: [ 28 | 'plugin', 29 | 'apply', 30 | 'screen', 31 | 'function', 32 | 'if', 33 | 'each', 34 | 'include', 35 | 'mixin', 36 | 'extend', 37 | 'content', 38 | 'use', 39 | ], 40 | }, 41 | ], 42 | 'selector-pseudo-element-no-unknown': [ 43 | true, 44 | { 45 | ignorePseudoElements: ['v-deep'], 46 | }, 47 | ], 48 | 'selector-pseudo-class-no-unknown': [ 49 | true, 50 | { 51 | ignorePseudoClasses: ['deep'], 52 | }, 53 | ], 54 | 'selector-type-no-unknown': [true, { ignoreTypes: ['page', 'radio', 'checkbox', 'scroll-view'] }], 55 | 'at-rule-no-deprecated': null, 56 | 'declaration-property-value-no-unknown': null, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "preserve", 5 | "lib": ["DOM", "ESNext"], 6 | "baseUrl": ".", 7 | "module": "ESNext", 8 | "moduleResolution": "bundler", 9 | "paths": { 10 | "@/*": ["src/*"] 11 | }, 12 | "resolveJsonModule": true, 13 | "types": ["@dcloudio/types", "@uni-helper/uni-app-types", "miniprogram-api-typings", "uview-plus/types", "z-paging/types"], 14 | "allowJs": true, 15 | "strict": true, 16 | "strictNullChecks": true, 17 | "noUnusedLocals": true, 18 | "sourceMap": true, 19 | "esModuleInterop": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "skipLibCheck": true 22 | }, 23 | "vueCompilerOptions": { 24 | "plugins": ["@uni-helper/uni-app-types/volar-plugin"] 25 | }, 26 | "include": [ 27 | "src/**/*.ts", 28 | "src/**/*.d.ts", 29 | "src/**/*.tsx", 30 | "src/**/*.vue", 31 | "types/**/*.d.ts", 32 | "types/**/*.ts" 33 | ], 34 | "exclude": ["dist", "node_modules", "uni_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /types/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const EffectScope: typeof import('vue')['EffectScope'] 10 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 11 | const computed: typeof import('vue')['computed'] 12 | const createApp: typeof import('vue')['createApp'] 13 | const createPinia: typeof import('pinia')['createPinia'] 14 | const customRef: typeof import('vue')['customRef'] 15 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 16 | const defineComponent: typeof import('vue')['defineComponent'] 17 | const defineStore: typeof import('pinia')['defineStore'] 18 | const effectScope: typeof import('vue')['effectScope'] 19 | const getActivePinia: typeof import('pinia')['getActivePinia'] 20 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 21 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 22 | const h: typeof import('vue')['h'] 23 | const inject: typeof import('vue')['inject'] 24 | const isProxy: typeof import('vue')['isProxy'] 25 | const isReactive: typeof import('vue')['isReactive'] 26 | const isReadonly: typeof import('vue')['isReadonly'] 27 | const isRef: typeof import('vue')['isRef'] 28 | const mapActions: typeof import('pinia')['mapActions'] 29 | const mapGetters: typeof import('pinia')['mapGetters'] 30 | const mapState: typeof import('pinia')['mapState'] 31 | const mapStores: typeof import('pinia')['mapStores'] 32 | const mapWritableState: typeof import('pinia')['mapWritableState'] 33 | const markRaw: typeof import('vue')['markRaw'] 34 | const nextTick: typeof import('vue')['nextTick'] 35 | const onActivated: typeof import('vue')['onActivated'] 36 | const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites'] 37 | const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress'] 38 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 39 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 40 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 41 | const onDeactivated: typeof import('vue')['onDeactivated'] 42 | const onError: typeof import('@dcloudio/uni-app')['onError'] 43 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 44 | const onHide: typeof import('@dcloudio/uni-app')['onHide'] 45 | const onLaunch: typeof import('@dcloudio/uni-app')['onLaunch'] 46 | const onLoad: typeof import('@dcloudio/uni-app')['onLoad'] 47 | const onMounted: typeof import('vue')['onMounted'] 48 | const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap'] 49 | const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged'] 50 | const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked'] 51 | const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed'] 52 | const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged'] 53 | const onPageNotFound: typeof import('@dcloudio/uni-app')['onPageNotFound'] 54 | const onPageScroll: typeof import('@dcloudio/uni-app')['onPageScroll'] 55 | const onPullDownRefresh: typeof import('@dcloudio/uni-app')['onPullDownRefresh'] 56 | const onReachBottom: typeof import('@dcloudio/uni-app')['onReachBottom'] 57 | const onReady: typeof import('@dcloudio/uni-app')['onReady'] 58 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 59 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 60 | const onResize: typeof import('@dcloudio/uni-app')['onResize'] 61 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 62 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 63 | const onShareAppMessage: typeof import('@dcloudio/uni-app')['onShareAppMessage'] 64 | const onShareTimeline: typeof import('@dcloudio/uni-app')['onShareTimeline'] 65 | const onShow: typeof import('@dcloudio/uni-app')['onShow'] 66 | const onTabItemTap: typeof import('@dcloudio/uni-app')['onTabItemTap'] 67 | const onThemeChange: typeof import('@dcloudio/uni-app')['onThemeChange'] 68 | const onUnhandledRejection: typeof import('@dcloudio/uni-app')['onUnhandledRejection'] 69 | const onUnload: typeof import('@dcloudio/uni-app')['onUnload'] 70 | const onUnmounted: typeof import('vue')['onUnmounted'] 71 | const onUpdated: typeof import('vue')['onUpdated'] 72 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] 73 | const provide: typeof import('vue')['provide'] 74 | const reactive: typeof import('vue')['reactive'] 75 | const readonly: typeof import('vue')['readonly'] 76 | const ref: typeof import('vue')['ref'] 77 | const resolveComponent: typeof import('vue')['resolveComponent'] 78 | const setActivePinia: typeof import('pinia')['setActivePinia'] 79 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 80 | const shallowReactive: typeof import('vue')['shallowReactive'] 81 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 82 | const shallowRef: typeof import('vue')['shallowRef'] 83 | const storeToRefs: typeof import('pinia')['storeToRefs'] 84 | const toRaw: typeof import('vue')['toRaw'] 85 | const toRef: typeof import('vue')['toRef'] 86 | const toRefs: typeof import('vue')['toRefs'] 87 | const toValue: typeof import('vue')['toValue'] 88 | const triggerRef: typeof import('vue')['triggerRef'] 89 | const unref: typeof import('vue')['unref'] 90 | const useAttrs: typeof import('vue')['useAttrs'] 91 | const useCssModule: typeof import('vue')['useCssModule'] 92 | const useCssVars: typeof import('vue')['useCssVars'] 93 | const useId: typeof import('vue')['useId'] 94 | const useModel: typeof import('vue')['useModel'] 95 | const useSlots: typeof import('vue')['useSlots'] 96 | const useTemplateRef: typeof import('vue')['useTemplateRef'] 97 | const watch: typeof import('vue')['watch'] 98 | const watchEffect: typeof import('vue')['watchEffect'] 99 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 100 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 101 | } 102 | // for type re-export 103 | declare global { 104 | // @ts-ignore 105 | export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' 106 | import('vue') 107 | } 108 | 109 | // for vue template auto import 110 | import { UnwrapRef } from 'vue' 111 | declare module 'vue' { 112 | interface GlobalComponents {} 113 | interface ComponentCustomProperties { 114 | readonly EffectScope: UnwrapRef 115 | readonly acceptHMRUpdate: UnwrapRef 116 | readonly computed: UnwrapRef 117 | readonly createApp: UnwrapRef 118 | readonly createPinia: UnwrapRef 119 | readonly customRef: UnwrapRef 120 | readonly defineAsyncComponent: UnwrapRef 121 | readonly defineComponent: UnwrapRef 122 | readonly defineStore: UnwrapRef 123 | readonly effectScope: UnwrapRef 124 | readonly getActivePinia: UnwrapRef 125 | readonly getCurrentInstance: UnwrapRef 126 | readonly getCurrentScope: UnwrapRef 127 | readonly h: UnwrapRef 128 | readonly inject: UnwrapRef 129 | readonly isProxy: UnwrapRef 130 | readonly isReactive: UnwrapRef 131 | readonly isReadonly: UnwrapRef 132 | readonly isRef: UnwrapRef 133 | readonly mapActions: UnwrapRef 134 | readonly mapGetters: UnwrapRef 135 | readonly mapState: UnwrapRef 136 | readonly mapStores: UnwrapRef 137 | readonly mapWritableState: UnwrapRef 138 | readonly markRaw: UnwrapRef 139 | readonly nextTick: UnwrapRef 140 | readonly onActivated: UnwrapRef 141 | readonly onAddToFavorites: UnwrapRef 142 | readonly onBackPress: UnwrapRef 143 | readonly onBeforeMount: UnwrapRef 144 | readonly onBeforeUnmount: UnwrapRef 145 | readonly onBeforeUpdate: UnwrapRef 146 | readonly onDeactivated: UnwrapRef 147 | readonly onError: UnwrapRef 148 | readonly onErrorCaptured: UnwrapRef 149 | readonly onHide: UnwrapRef 150 | readonly onLaunch: UnwrapRef 151 | readonly onLoad: UnwrapRef 152 | readonly onMounted: UnwrapRef 153 | readonly onNavigationBarButtonTap: UnwrapRef 154 | readonly onNavigationBarSearchInputChanged: UnwrapRef 155 | readonly onNavigationBarSearchInputClicked: UnwrapRef 156 | readonly onNavigationBarSearchInputConfirmed: UnwrapRef 157 | readonly onNavigationBarSearchInputFocusChanged: UnwrapRef 158 | readonly onPageNotFound: UnwrapRef 159 | readonly onPageScroll: UnwrapRef 160 | readonly onPullDownRefresh: UnwrapRef 161 | readonly onReachBottom: UnwrapRef 162 | readonly onReady: UnwrapRef 163 | readonly onRenderTracked: UnwrapRef 164 | readonly onRenderTriggered: UnwrapRef 165 | readonly onResize: UnwrapRef 166 | readonly onScopeDispose: UnwrapRef 167 | readonly onServerPrefetch: UnwrapRef 168 | readonly onShareAppMessage: UnwrapRef 169 | readonly onShareTimeline: UnwrapRef 170 | readonly onShow: UnwrapRef 171 | readonly onTabItemTap: UnwrapRef 172 | readonly onThemeChange: UnwrapRef 173 | readonly onUnhandledRejection: UnwrapRef 174 | readonly onUnload: UnwrapRef 175 | readonly onUnmounted: UnwrapRef 176 | readonly onUpdated: UnwrapRef 177 | readonly onWatcherCleanup: UnwrapRef 178 | readonly provide: UnwrapRef 179 | readonly reactive: UnwrapRef 180 | readonly readonly: UnwrapRef 181 | readonly ref: UnwrapRef 182 | readonly resolveComponent: UnwrapRef 183 | readonly setActivePinia: UnwrapRef 184 | readonly setMapStoreSuffix: UnwrapRef 185 | readonly shallowReactive: UnwrapRef 186 | readonly shallowReadonly: UnwrapRef 187 | readonly shallowRef: UnwrapRef 188 | readonly storeToRefs: UnwrapRef 189 | readonly toRaw: UnwrapRef 190 | readonly toRef: UnwrapRef 191 | readonly toRefs: UnwrapRef 192 | readonly toValue: UnwrapRef 193 | readonly triggerRef: UnwrapRef 194 | readonly unref: UnwrapRef 195 | readonly useAttrs: UnwrapRef 196 | readonly useCssModule: UnwrapRef 197 | readonly useCssVars: UnwrapRef 198 | readonly useId: UnwrapRef 199 | readonly useModel: UnwrapRef 200 | readonly useSlots: UnwrapRef 201 | readonly useTemplateRef: UnwrapRef 202 | readonly watch: UnwrapRef 203 | readonly watchEffect: UnwrapRef 204 | readonly watchPostEffect: UnwrapRef 205 | readonly watchSyncEffect: UnwrapRef 206 | } 207 | } -------------------------------------------------------------------------------- /types/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | // biome-ignore lint: disable 6 | export {} 7 | 8 | /* prettier-ignore */ 9 | declare module 'vue' { 10 | export interface GlobalComponents { 11 | AgreePrivacy: typeof import('./../src/components/agree-privacy/index.vue')['default'] 12 | LangSelect: typeof import('./../src/components/lang-select/index.vue')['default'] 13 | RouterLink: typeof import('vue-router')['RouterLink'] 14 | RouterView: typeof import('vue-router')['RouterView'] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /types/env.d.ts: -------------------------------------------------------------------------------- 1 | interface ImportMetaEnv { 2 | /** 页面标题 */ 3 | VITE_APP_TITLE: string; 4 | /** 开发环境配置 */ 5 | VITE_APP_ENV: string; 6 | /** 接口地址 */ 7 | VITE_API_BASE_URL: string; 8 | /** 端口号 */ 9 | VITE_APP_PORT: string; 10 | /** h5是否需要配置代理 */ 11 | VITE_APP_PROXY: string; 12 | /** API代理前缀 */ 13 | VITE_API_PREFIX: string; 14 | /** 删除console */ 15 | VITE_DROP_CONSOLE: string; 16 | } 17 | 18 | interface ImportMeta { 19 | readonly env: ImportMetaEnv; 20 | } 21 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { VNodeChild } from 'vue'; 2 | 3 | declare global { 4 | // vue 5 | declare type VueNode = VNodeChild | JSX.Element; 6 | 7 | declare type TimeoutHandle = ReturnType; 8 | declare type IntervalHandle = ReturnType; 9 | 10 | interface ImportMetaEnv extends ViteEnv { 11 | __: unknown; 12 | } 13 | 14 | declare interface ViteEnv { 15 | VITE_APP_TITLE?: string; 16 | VITE_APP_BASE_API: string; 17 | VITE_APP_PORT: number; 18 | VITE_APP_PROXY: boolean; 19 | VITE_API_PREFIX: string; 20 | VITE_DROP_CONSOLE: boolean; 21 | } 22 | 23 | declare function parseInt(s: string | number, radix?: number): number; 24 | 25 | declare function parseFloat(string: string | number): number; 26 | 27 | declare interface Uni { 28 | $u: any; 29 | } 30 | 31 | namespace JSX { 32 | interface IntrinsicElements { 33 | view: _View; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /types/i18n.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare module 'vue' { 4 | interface ComponentCustomProperties { 5 | $t: (key: string, opt?: Record) => string; 6 | $tm: (key: string, opt?: Record) => [] | { [p: string]: any }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /types/module.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | 6 | // eslint-disable-next-line ts/no-empty-object-type 7 | const component: DefineComponent<{}, {}, any>; 8 | export default component; 9 | } 10 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetIcons, 4 | transformerDirectives, 5 | transformerVariantGroup, 6 | } from 'unocss'; 7 | import { presetWeapp } from 'unocss-preset-weapp'; 8 | import { extractorAttributify, transformerClass } from 'unocss-preset-weapp/transformer'; 9 | 10 | const { presetWeappAttributify, transformerAttributify } = extractorAttributify(); 11 | 12 | export default defineConfig({ 13 | presets: [ 14 | // https://github.com/MellowCo/unocss-preset-weapp 15 | presetWeapp(), 16 | // attributify autocomplete 17 | presetWeappAttributify() as any, 18 | // https://unocss.dev/presets/icons 19 | presetIcons({ 20 | scale: 1.2, 21 | warn: true, 22 | extraProperties: { 23 | 'display': 'inline-block', 24 | 'vertical-align': 'middle', 25 | }, 26 | }), 27 | ], 28 | /** 29 | * 自定义快捷语句 30 | * @see https://github.com/unocss/unocss#shortcuts 31 | */ 32 | shortcuts: { 33 | 'border-base': 'border border-gray-500_10', 34 | 'center': 'flex justify-center items-center', 35 | }, 36 | transformers: [ 37 | // 启用 @apply 功能 38 | transformerDirectives({ 39 | enforce: 'pre', 40 | }), 41 | // https://unocss.dev/transformers/variant-group 42 | // 启用 () 分组功能 43 | transformerVariantGroup(), 44 | // https://github.com/MellowCo/unocss-preset-weapp/tree/main/src/transformer/transformerAttributify 45 | transformerAttributify() as any, 46 | // https://github.com/MellowCo/unocss-preset-weapp/tree/main/src/transformer/transformerClass 47 | transformerClass(), 48 | ], 49 | }); 50 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from 'vite'; 2 | import process from 'node:process'; 3 | import { fileURLToPath, URL } from 'node:url'; 4 | import { defineConfig, loadEnv } from 'vite'; 5 | import { createViteProxy } from './build/config/index'; 6 | import createVitePlugins from './build/plugins/index'; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(({ command, mode }): UserConfig => { 10 | // mode: 区分生产环境还是开发环境 11 | console.log('command, mode -> ', command, mode); 12 | 13 | const { UNI_PLATFORM } = process.env; 14 | console.log('UNI_PLATFORM -> ', UNI_PLATFORM); // 得到 mp-weixin, h5, app 等 15 | 16 | const env = loadEnv(mode, fileURLToPath(new URL('./env', import.meta.url))); 17 | console.log('环境变量 env -> ', env); 18 | 19 | const isBuild = process.env.NODE_ENV === 'production'; 20 | return { 21 | // 自定义env目录 22 | envDir: './env', 23 | resolve: { 24 | // https://cn.vitejs.dev/config/#resolve-alias 25 | alias: { 26 | // 设置别名 27 | '@': fileURLToPath(new URL('./src', import.meta.url)), 28 | }, 29 | }, 30 | // vite 相关配置 31 | server: { 32 | port: Number.parseInt(env.VITE_APP_PORT, 10), 33 | hmr: true, 34 | host: true, 35 | open: true, 36 | proxy: createViteProxy(env), 37 | }, 38 | // 设置scss的api类型为modern-compiler 39 | css: { 40 | preprocessorOptions: { 41 | scss: { 42 | api: 'modern-compiler', 43 | // 消除一些不必要的警告 44 | silenceDeprecations: ['legacy-js-api'], 45 | }, 46 | }, 47 | }, 48 | plugins: createVitePlugins(isBuild), 49 | esbuild: { 50 | drop: JSON.parse(env.VITE_DROP_CONSOLE) ? ['console', 'debugger'] : [], 51 | }, 52 | }; 53 | }); 54 | --------------------------------------------------------------------------------