├── .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 | "",
23 | " $1",
24 | "\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": ["", " $1", "\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 | [](https://github.com/oyjt/uniapp-vue3-template)
4 | [](https://github.com/oyjt/uniapp-vue3-template)
5 | [](https://github.com/oyjt/uniapp-vue3-template)
6 | [](https://github.com/oyjt/uniapp-vue3-template)
7 | [](https://github.com/oyjt/uniapp-vue3-template)
8 | [](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 |
283 |
284 | ...
285 |
286 |
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 |
2 |
3 |
4 |
5 | {{ initTitle }}
6 |
7 |
8 |
9 | {{ initSubTitle }}
10 | 1.为向您提供基本的服务,我们会遵循正当、合法、必要的原则收集和使用必要的信息。
11 | 2.基于您的授权我们可能会收集和使用您的相关信息,您有权拒绝或取消授权。
12 | 3.未经您的授权同意,我们不会将您的信息共享给第三方或用于您未授权的其他用途。
13 | 4.详细信息请您完整阅读{{
14 | initPrivacyContractName
15 | }}
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
145 |
146 |
215 |
--------------------------------------------------------------------------------
/src/components/lang-select/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
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 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
25 |
26 |
36 |
--------------------------------------------------------------------------------
/src/pages/common/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 欢迎登录
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 | 密码登录
22 |
23 |
24 | 遇到问题
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 微信
34 |
35 |
36 |
37 |
38 |
39 | QQ
40 |
41 |
42 |
43 | 登录代表同意
44 |
45 | 用户协议、隐私政策,
46 |
47 | 并授权使用您的账号信息(如昵称、头像、收获地址)以便您统一管理
48 |
49 |
50 |
51 |
52 |
125 |
126 |
181 |
--------------------------------------------------------------------------------
/src/pages/common/webview/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
--------------------------------------------------------------------------------
/src/pages/tab/home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 | {{ $t('home.intro') }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
67 |
--------------------------------------------------------------------------------
/src/pages/tab/list/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
63 |
--------------------------------------------------------------------------------
/src/pages/tab/user/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | uni-app
11 |
12 |
13 | 微信号:uni-app
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
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 |
--------------------------------------------------------------------------------