orgin/master #1
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# 忽略目录
|
||||
/dist
|
||||
/node_modules
|
||||
.hbuilderx/*
|
||||
.idea/*
|
||||
|
||||
# 忽略文件
|
||||
*.log
|
||||
**/*.tsbuildinfo
|
||||
.eslintcache
|
||||
pnpm-lock.yaml
|
||||
/.eslintrc-auto-import.json
|
||||
/typings/
|
||||
6
.npmrc
Normal file
@ -0,0 +1,6 @@
|
||||
# 提示:如果你想自动安装对等依赖,在项目根目录下的.npmrc文件中添加"auto-install-peers=true"。
|
||||
# 提示:如果你不希望pnpm在对等依赖问题上失败,在项目根目录下的.npmrc文件中添加"strict-peer-dependencies=false"。
|
||||
# auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
# registry=https://registry.npmmirror.com
|
||||
registry=https://registry.npmjs.org
|
||||
12
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
//////////////////////////////////
|
||||
// 向此工作区的用户推荐的扩展列表 //
|
||||
/////////////////////////////////
|
||||
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar", // Vue语言支持扩展
|
||||
"Vue.vscode-typescript-vue-plugin", // 一个TS服务器插件,使TS服务器知道*.vue文件。
|
||||
"esbenp.prettier-vscode", // 代码格式化
|
||||
"dbaeumer.vscode-eslint" // 代码质量检查
|
||||
]
|
||||
}
|
||||
73
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// vscode 编辑器和插件的配置 ////////////////////////////////////////////////
|
||||
// 参考:https://blog.csdn.net/weixin_46238462/article/details/125867532 //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.detectIndentation": false,
|
||||
"editor.tabSize": 4,
|
||||
"files.eol": "auto",
|
||||
"eslint.enable": true,
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// 官网参考:https://prettier.io/docs/en/options.html#tab-width //
|
||||
////////////////////////////////////////////////////////////////
|
||||
"prettier.semi": true,
|
||||
"prettier.tabWidth": 4,
|
||||
"prettier.printWidth": 160,
|
||||
"prettier.endOfLine": "auto",
|
||||
"prettier.singleQuote": true,
|
||||
"prettier.ignorePath": ".prettierignore",
|
||||
// Enable the ESlint flat config support
|
||||
// (remove this if your ESLint extension above v3.0.5)
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
|
||||
// Disable the default formatter, use eslint instead
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
|
||||
// Auto fix
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
|
||||
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off" },
|
||||
{ "rule": "format/*", "severity": "off" },
|
||||
{ "rule": "*-indent", "severity": "off" },
|
||||
{ "rule": "*-spacing", "severity": "off" },
|
||||
{ "rule": "*-spaces", "severity": "off" },
|
||||
{ "rule": "*-order", "severity": "off" },
|
||||
{ "rule": "*-dangle", "severity": "off" },
|
||||
{ "rule": "*-newline", "severity": "off" },
|
||||
{ "rule": "*quotes", "severity": "off" },
|
||||
{ "rule": "*semi", "severity": "off" }
|
||||
],
|
||||
|
||||
// Enable eslint for all supported languages
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml",
|
||||
"toml",
|
||||
"xml",
|
||||
"gql",
|
||||
"graphql",
|
||||
"astro",
|
||||
"css",
|
||||
"less",
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
]
|
||||
}
|
||||
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 h_mo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
235
README.md
Normal file
@ -0,0 +1,235 @@
|
||||
# 🌈 uni-app Vue3 Vite4 pinia2 TypeScript 基础框架
|
||||
|
||||
<p align="center">
|
||||
<a href="https://cn.vuejs.org/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/-Vue3-34495e?logo=vue.js" />
|
||||
</a>
|
||||
<a href="https://pinia.vuejs.org/zh/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/-Vite4-646cff?logo=vite&logoColor=white" />
|
||||
</a>
|
||||
<a href="https://www.typescriptlang.org/zh/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/-TypeScript5-blue?logo=typescript&logoColor=white" />
|
||||
</a>
|
||||
<a href="https://eslint.org/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/-ESLint8-4b32c3?logo=eslint&logoColor=white" />
|
||||
</a>
|
||||
<a href="https://pnpm.io/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/-pnpm8-F69220?logo=pnpm&logoColor=white" />
|
||||
</a>
|
||||
<a href="https://unocss.dev/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/-UnoCss-4d4d4d?logo=unocss" />
|
||||
</a>
|
||||
<a href="https://iconify.design/" target="_blank">
|
||||
<img src="https://img.shields.io/badge/-Iconify-1769aa?logo=Iconify" />
|
||||
</a>
|
||||
<a href="https://gitee.com/h_mo/uniapp-vue3-vite-ts-template" target="_blank">
|
||||
<img src="https://svg.hamm.cn/gitee.svg?type=star&user=h_mo&project=uniapp-vue3-vite-ts-template"/>
|
||||
</a>
|
||||
<a href="https://gitee.com/h_mo/uniapp-vue3-vite-ts-template" target="_blank">
|
||||
<img src="https://svg.hamm.cn/gitee.svg?type=fork&user=h_mo&project=uniapp-vue3-vite-ts-template"/>
|
||||
</a>
|
||||
<a href="https://ext.dcloud.net.cn/plugin?id=8559" target="_blank">
|
||||
<img src="https://svg.hamm.cn/badge.svg?key=Platform&value=uni-app"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 简介
|
||||
|
||||
- **uni-app Vue3 Vite4 pinia2 TypeScript 基础框架**
|
||||
- cli 创建的 Vue3/Vite 项目 与 使用 HBuilderX 导入插件 的包有差异,请直接访问 [开源地址](https://gitee.com/h_mo/uniapp-vue3-vite-ts-template)
|
||||
- 访问[uniapp 插件](https://ext.dcloud.net.cn/plugin?id=8559)
|
||||
- 如有问题请加群【[872378674](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=T3PX4_sWeMXeFGWF6EZJLXABSNyStYR0&authKey=EYXATTrGpmyowFxk9xtX6T7FIRbOF7brLd9uODxl%2B6jIbGfWQGW869V1hkPSlGYT&noverify=0&group_code=872378674)】交流
|
||||
|
||||
### 说明
|
||||
|
||||
- 框架完全基于 Vue3 SFC `<script setup>` 写法,不支持 Vue2;
|
||||
- 可用于学习与交流;
|
||||
- 目前测试 H5、微信小程序,APP(Android),支付宝小程序通过;
|
||||
- 其他平台暂未测试,后续会增加;
|
||||
- 如发现问题或建议可在评论区留言, 或提[Issues](https://gitee.com/h_mo/uniapp-vue3-vite-ts-template/issues)及[PR](https://gitee.com/h_mo/uniapp-vue3-vite-ts-template/pulls),会及时处理;
|
||||
- 如有需求亦可在评论区留言,或在此项目基础上增加;
|
||||
- [代码规范 & 详细解释 husky、prettier、eslint、lint-staged 的作用和使用](https://blog.csdn.net/cookcyq__/article/details/125457031)
|
||||
|
||||
## 特性
|
||||
|
||||
- **最新技术栈**:使用 Vue3/Vite4/pinia ,TypeScript 等前端前沿技术开发;
|
||||
- **[Unocss](https://github.com/unocss/unocss)**: 原子化 CSS, [iconify](https://github.com/iconify/iconify)图标
|
||||
- **Eslint/Prettier**: 规范代码格式,统一编码;
|
||||
- **路由拦截**: [uni-mini-router](https://gitee.com/fant-mini/uni-mini-router),类似Vue Router的API和功能,在uni-app中进行路由跳转、传参、拦截等常用操作;
|
||||
- **请求拦截**: 使用[alova 请求](https://github.com/alovajs/alova),支持请求和响应拦截等;
|
||||
- **Mock 数据**: 配合 alova 请求的[@alova/mock](https://github.com/alovajs/mock),模拟 api 请求(App 不支持);
|
||||
- **缓存加密**: 支持 AES 加密缓存,可设置区分在开发或生成环境中是否加密;
|
||||
|
||||
## 目录结构
|
||||
|
||||
```shell
|
||||
.
|
||||
├─ src
|
||||
│ ├─assets # 静态资源目录
|
||||
│ │
|
||||
│ ├─components # 组件目录
|
||||
│ │ ├─ BasicButton
|
||||
│ │ │ ├─index.vue
|
||||
│ │ │ └─prpos.ts
|
||||
│ │ └─...
|
||||
│ │
|
||||
│ ├─enums # 枚举/常量
|
||||
│ │ ├─ cacheEnum.ts
|
||||
│ │ └─...
|
||||
│ │
|
||||
│ ├─pages # 页面
|
||||
│ │ ├─ index
|
||||
│ │ │ └─index.vue
|
||||
│ │ └─...
|
||||
│ │
|
||||
│ ├─services # 接口相关
|
||||
│ │ ├─ api # api
|
||||
│ │ │ ├─auth.ts
|
||||
│ │ │ └─...
|
||||
│ │ │
|
||||
│ │ └─ model # 数据模型
|
||||
│ │ ├─authModel.d.ts
|
||||
│ │ └─...
|
||||
│ │
|
||||
│ ├─settings # 设置
|
||||
│ │ └─ encryptionSetting # 加密设置
|
||||
│ │
|
||||
│ ├─state # 状态管理模式(pinia)
|
||||
│ │ ├─ modules # 数据模块
|
||||
│ │ │ ├─auth.ts
|
||||
│ │ │ └─...
|
||||
│ │ │
|
||||
│ │ └─ index.ts
|
||||
│ │
|
||||
│ ├─static # 静态公共文件
|
||||
│ │ ├─ images # 图片
|
||||
│ │ │ ├─avatar.png
|
||||
│ │ │ └─...
|
||||
│ │ │
|
||||
│ │ └─ ...
|
||||
│ │
|
||||
│ ├─types # 类型文件
|
||||
│ │ ├─ http.d.ts
|
||||
│ │ └─ ...
|
||||
│ │
|
||||
│ └─utils # 工具类
|
||||
│ ├─ cache # 缓存相关目录
|
||||
│ ├─ http # request相关目录
|
||||
│ ├─ interceptors # 拦截器相关目录
|
||||
│ └─ ...
|
||||
│
|
||||
├─ .env
|
||||
├─ .env.development
|
||||
├─ .env.production
|
||||
├─ .eslintignore
|
||||
├─ .eslintrc.js
|
||||
├─ .gitignore
|
||||
├─ .prettierignore
|
||||
├─ .prettierrc.js
|
||||
├─ favicon.ico
|
||||
├─ index.html
|
||||
├─ package.json
|
||||
├─ pnpm-lock.yaml
|
||||
├─ README.md
|
||||
├─ tree.txt
|
||||
├─ tsconfig.json
|
||||
└─ vite.config.ts
|
||||
|
||||
```
|
||||
|
||||
## 预览
|
||||
|
||||
- 域名到期,暂时已不能预览
|
||||
|
||||
[//]: # '- H5'
|
||||
[//]: # ' '
|
||||
[//]: # '- 小程序(体验版-需申请体验)'
|
||||
[//]: #
|
||||
[//]: # ' '
|
||||
|
||||
## 安装使用
|
||||
|
||||
- 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- 运行
|
||||
|
||||
```bash
|
||||
# 其他端请查看 package.json script
|
||||
pnpm dev:h5
|
||||
```
|
||||
|
||||
- 打包
|
||||
|
||||
```bash
|
||||
# 其他端请查看 package.json script
|
||||
pnpm build:h5
|
||||
```
|
||||
|
||||
- 更新依赖到最新(新手请忽略)
|
||||
|
||||
```bash
|
||||
pnpm up
|
||||
# 打开HBuilder X alpha桌面程序-->点击上面的帮助-->历次更新说明-->获取最新版本号(如:3.7.2.20230217-alpha)
|
||||
npx @dcloudio/uvm 3.7.2.20230217-alpha
|
||||
```
|
||||
|
||||
## Gitee 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 Feat_xxx 分支
|
||||
3. 提交代码
|
||||
4. 新建 Pull Request
|
||||
|
||||
## [Gitee 贡献提交规范](https://www.conventionalcommits.org/zh-hans/v1.0.0/)
|
||||
|
||||
### 提交类型
|
||||
|
||||
| 提交类型 | 标题 | 描述 |
|
||||
| ---------- | ------------------ | ------------------------------------------------------------------------------------- |
|
||||
| `feat` | 特征 | 新功能、新特性 |
|
||||
| `fix` | Bug 修复 | bug 修复 |
|
||||
| `docs` | 文档 | 仅文档更改 |
|
||||
| `style` | 风格 | 不影响代码含义的更改(空格、格式、缺少分号等) |
|
||||
| `refactor` | 代码重构 | 重构,在不影响代码内部行为,功能下的代码修改 |
|
||||
| `perf` | 性能改进 | 更改代码,以提高性能 |
|
||||
| `test` | 测试 | 添加缺失的测试或纠正现有的测试 |
|
||||
| `build` | 构建 | 影响构建系统或外部依赖项的更改(示例范围:gulp、broccoli、npm) |
|
||||
| `ci` | 持续集成 | 对我们的 CI 配置文件和脚本的更改(示例范围:Travis、Circle、BrowserStack、SauceLabs) |
|
||||
| `chore` | 其他文件修改 | 不修改 src 或测试文件的其他更改 |
|
||||
| `revert` | 还原 | 恢复之前的提交 |
|
||||
| `release` | 发布新版本 | \- |
|
||||
| `workflow` | 工作流相关文件修改 | \- |
|
||||
|
||||
### 提交别名
|
||||
|
||||
| 提交类型 | 映射到 | 标题 | 描述 |
|
||||
| ------------------ | ------- | -------- | -------------------------- |
|
||||
| `initial` | `feat` | 最初的 | 初始提交 |
|
||||
| `dependencies` | `fix` | 依赖项 | 更新依赖项 |
|
||||
| `peerDependencies` | `fix` | 对等依赖 | 更新对等依赖项 |
|
||||
| `devDependencies` | `chore` | 开发依赖 | 更新开发依赖 |
|
||||
| `metadata` | `fix` | 元数据 | 更新元数据(package.json) |
|
||||
|
||||
### 快捷别名提示
|
||||
|
||||
1. resolve a conflict:解决冲突
|
||||
2. merge branch:合并分支
|
||||
3. feat: [...] : 添加的新功能说明
|
||||
4. fix: [...] : 修复的 bug 说明
|
||||
5. initial project:初始化项目
|
||||
6. style: [...] : 修改的样式范围
|
||||
7. perf:[...] : 优化的范围
|
||||
8. release : 发布新版本
|
||||
9. docs: 文档修改
|
||||
10. refactor: 代码重构
|
||||
11. revert: 还原之前的版本
|
||||
12. dependencies: 依赖项修改
|
||||
13. devDependencies: 开发依赖修改
|
||||
14. review:复习,回顾
|
||||
15. strengthen: 加强,巩固
|
||||
|
||||
##
|
||||
2
env/.env
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# title
|
||||
VITE_APP_TITLE='Uni-app Vue3 Ts --Vite'
|
||||
17
env/.env.development
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
# 运行环境
|
||||
VITE_ENV=development
|
||||
|
||||
# 是否使用模拟数据
|
||||
VITE_USE_MOCK=true
|
||||
|
||||
VITE_PORT=3000
|
||||
|
||||
# BASE_URL
|
||||
VITE_BASE_URL==/api
|
||||
# 上传域名
|
||||
VITE_UPLOAD_URL=/upload
|
||||
|
||||
# 代理前缀,仅H5有效
|
||||
VITE_PROXY_PREFIX=/api
|
||||
VITE_UPLOAD_PROXY_PREFIX=/upload
|
||||
|
||||
12
env/.env.production
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# 运行环境
|
||||
VITE_ENV=production
|
||||
|
||||
# 是否使用模拟数据
|
||||
VITE_USE_MOCK=true
|
||||
|
||||
# api域名
|
||||
VITE_BASE_URL=/api/v1
|
||||
|
||||
|
||||
# 上传域名
|
||||
VITE_UPLOAD_URL=/upload
|
||||
45
eslint.config.js
Normal file
@ -0,0 +1,45 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
|
||||
import process from 'node:process';
|
||||
import antfu from '@antfu/eslint-config';
|
||||
|
||||
/**
|
||||
* @see https://github.com/antfu/eslint-config
|
||||
*/
|
||||
|
||||
export default antfu({
|
||||
formatters: true,
|
||||
vue: true,
|
||||
jsx: true,
|
||||
unocss: true,
|
||||
rules: {
|
||||
'style/indent': ['error', 2, { SwitchCase: 2 }],
|
||||
'style/quotes': ['error', 'single'],
|
||||
'style/semi': ['error', 'always'],
|
||||
'style/semi-style': ['error', 'last'],
|
||||
'style/max-len': ['error', {
|
||||
code: 160,
|
||||
tabWidth: 2,
|
||||
ignoreUrls: true,
|
||||
ignoreComments: true,
|
||||
ignoreStrings: true,
|
||||
ignoreTemplateLiterals: true,
|
||||
ignoreRegExpLiterals: true,
|
||||
}],
|
||||
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
||||
'vue/script-indent': ['error', 2, { baseIndent: 0 }],
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-useless-catch': 'off',
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
ignores: [
|
||||
'./dist/*',
|
||||
'./.vscode/*',
|
||||
'./.idea/*',
|
||||
'**/androidPrivacy.json',
|
||||
'README.md',
|
||||
],
|
||||
});
|
||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 698 B |
25
index.html
Normal file
@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport =
|
||||
'CSS' in window &&
|
||||
typeof CSS.supports === 'function' &&
|
||||
(CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') +
|
||||
'" />',
|
||||
)
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
100
package.json
Normal file
@ -0,0 +1,100 @@
|
||||
{
|
||||
"name": "uniapp_vue3_vite_ts",
|
||||
"type": "module",
|
||||
"version": "2.0.0-alpha.1",
|
||||
"scripts": {
|
||||
"dev:app": "uni -p app",
|
||||
"dev:custom": "uni -p",
|
||||
"dev:h5": "pnpm git:hooks && uni",
|
||||
"dev:h5:ssr": "uni --ssr",
|
||||
"dev:mp-alipay": "uni -p mp-alipay",
|
||||
"dev:mp-baidu": "uni -p mp-baidu",
|
||||
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||
"dev:mp-lark": "uni -p mp-lark",
|
||||
"dev:mp-qq": "uni -p mp-qq",
|
||||
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||
"dev:mp-weixin": "pnpm git:hooks && uni -p mp-weixin",
|
||||
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||
"build:app": "uni build -p app",
|
||||
"build:custom": "uni build -p",
|
||||
"build:h5": "uni build --minify",
|
||||
"build:h5:ssr": "uni build --ssr",
|
||||
"build:mp-alipay": "uni build -p mp-alipay",
|
||||
"build:mp-baidu": "uni build -p mp-baidu",
|
||||
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||
"build:mp-lark": "uni build -p mp-lark",
|
||||
"build:mp-qq": "uni build -p mp-qq",
|
||||
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||
"build:mp-weixin": "uni build -p mp-weixin --minify",
|
||||
"build:quickapp-webview": "uni build -p quickapp-webview",
|
||||
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||
"lint:fix": "eslint . --fix --ignore-pattern '*/androidPrivacy.json' --ignore-pattern 'README.md'",
|
||||
"git:hooks": "npx simple-git-hooks"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alova/adapter-uniapp": "^1.2.2",
|
||||
"@alova/mock": "^1.5.1",
|
||||
"@dcloudio/uni-app": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-app-plus": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-components": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-h5": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-i18n": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-mp-alipay": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-mp-baidu": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-mp-kuaishou": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-mp-lark": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-mp-qq": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-mp-toutiao": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-mp-weixin": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-alpha-4010920240607001",
|
||||
"@multiavatar/multiavatar": "^1.0.7",
|
||||
"alova": "^2.21.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^2.21.1",
|
||||
"@dcloudio/types": "^3.4.8",
|
||||
"@dcloudio/uni-automator": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-cli-shared": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-alpha-4010920240607001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-4010920240607001",
|
||||
"@iconify/vue": "^4.1.2",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.14.2",
|
||||
"@unocss/eslint-plugin": "^0.61.0",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vue/runtime-core": "^3.4.29",
|
||||
"eslint": "^9.4.0",
|
||||
"eslint-plugin-format": "^0.1.1",
|
||||
"globals": "^15.4.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"picocolors": "^1.0.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.77.5",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"tsx": "^4.15.4",
|
||||
"typescript": "^5.4.5",
|
||||
"uni-mini-router": "^0.1.6",
|
||||
"uni-read-pages-vite": "^0.0.6",
|
||||
"unocss": "^0.61.0",
|
||||
"unocss-preset-weapp": "^0.60.1",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-transform-class": "^0.5.3",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-restart": "^0.4.0"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npx lint-staged",
|
||||
"commit-msg": "npx tsx ./scripts/verify-commit.ts"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix --ignore-pattern '*/androidPrivacy.json' --ignore-pattern 'README.md'"
|
||||
}
|
||||
}
|
||||
28
scripts/verify-commit.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 提交信息校验
|
||||
* @link https://github.com/toplenboren/simple-git-hooks
|
||||
* @see 参考:https://github.com/vuejs/vue-next/blob/master/.github/commit-convention.md
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import pico from 'picocolors';
|
||||
|
||||
const msgPath = path.resolve('.git/COMMIT_EDITMSG');
|
||||
const msg = readFileSync(msgPath, 'utf-8').trim();
|
||||
|
||||
const commitRE
|
||||
= /^(?:revert: )?(?:feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|mod|release|strengthen)(?:\(.+\))?: .{1,50}/;
|
||||
|
||||
if (!commitRE.test(msg)) {
|
||||
console.log(pico.yellow(`\n提交的信息: ${msg}\n`));
|
||||
console.error(
|
||||
` ${pico.white(pico.bgRed(' 格式错误 '))} ${pico.red(
|
||||
'无效的提交信息格式.',
|
||||
)}\n\n${
|
||||
pico.red(' 正确的提交消息格式. 例如:\n\n')
|
||||
} ${pico.green('feat: add a new feature')}\n`
|
||||
+ ` ${pico.green('fix: fixed an bug')}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
10
shims-uni.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference types='@dcloudio/types' />
|
||||
import 'vue';
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
type Hooks = App.AppInstance & Page.PageInstance;
|
||||
|
||||
interface ComponentCustomOptions extends Hooks {
|
||||
|
||||
}
|
||||
}
|
||||
19
src/App.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app';
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
|
||||
onLaunch(() => {
|
||||
console.log('App Launch');
|
||||
});
|
||||
onShow(() => {
|
||||
const userStore = useUserStore();
|
||||
userStore.initUserInfo();
|
||||
console.log('App Show');
|
||||
});
|
||||
onHide(() => {
|
||||
console.log('App Hide');
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
29
src/androidPrivacy.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": "1",
|
||||
"prompt": "template",
|
||||
"title": "服务协议和隐私政策",
|
||||
"message": " 请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/> 你可阅读<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
|
||||
"buttonAccept": "同意并接受",
|
||||
"buttonRefuse": "暂不同意",
|
||||
// HX 3.4.13之后版本新增,system 使用系统webview 打开隐私协议链接,默认使用uni-app内置web组件
|
||||
"hrefLoader": "default",
|
||||
"second": {
|
||||
"title": "确认提示",
|
||||
"message": " 进入应用前,你需先同意<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>,否则将退出应用。",
|
||||
"buttonAccept": "同意并继续",
|
||||
"buttonRefuse": "退出应用"
|
||||
},
|
||||
"styles": {
|
||||
"backgroundColor": "#00FF00",
|
||||
"borderRadius": "5px",
|
||||
"title": {
|
||||
"color": "#ff00ff"
|
||||
},
|
||||
"buttonAccept": {
|
||||
"color": "#ffff00"
|
||||
},
|
||||
"buttonRefuse": {
|
||||
"color": "#00ffff"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/components/AppProvider/index.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup name="AppProvider"></script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
32
src/components/BasicButton/index.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang="ts" setup>
|
||||
import { buttonProps } from '@/components/BasicButton/prpos';
|
||||
|
||||
const props = defineProps(buttonProps);
|
||||
const emits = defineEmits(['click']);
|
||||
function click() {
|
||||
emits('click');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="default-btn" :disabled="props.disabled" @tap="click">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.default-btn {
|
||||
color: #fff;
|
||||
border-width: 4rpx;
|
||||
border-color: #bfdbfe;
|
||||
border-style: solid;
|
||||
border-radius: 6rpx;
|
||||
background-color: #60a5fa;
|
||||
padding: 12rpx 26rpx;
|
||||
display: inline-block;
|
||||
font-size: 24rpx;
|
||||
&:hover {
|
||||
background-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/components/BasicButton/prpos.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const buttonProps = {
|
||||
disabled: { type: Boolean, default: false },
|
||||
click: { type: Function },
|
||||
};
|
||||
31
src/components/BasicInput/index.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicInput',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(_props) {
|
||||
// const _props = props;
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input :type="type" :name="name" :value="value">
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
26
src/components/Iconify/index.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IconProps } from '@iconify/vue';
|
||||
/**
|
||||
* @name Iconify
|
||||
* @desc 图标
|
||||
* @docs https://iconify.design/docs/icon-components/vue/
|
||||
* @example <Iconify icon="ant-design:home-outlined" />
|
||||
*/
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
interface Props {
|
||||
icon: string
|
||||
size?: string | number
|
||||
width?: IconProps['width']
|
||||
height?: IconProps['height']
|
||||
color?: IconProps['color']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Icon v-bind="props" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
16
src/components/Test/index.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
});
|
||||
|
||||
const text = `TEXT: ${props.text}`;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>{{ text }}</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
0
src/enums/appEnum.ts
Normal file
5
src/enums/cacheEnum.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// token key
|
||||
export const TOKEN_KEY = 'TOKEN__';
|
||||
|
||||
// user info key
|
||||
export const USER_INFO_KEY = 'USER__INFO__';
|
||||
22
src/enums/httpEnum.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @description: 请求结果设置
|
||||
*/
|
||||
export enum ResultEnum {
|
||||
SUCCESS = 10000,
|
||||
FAIL = 10001,
|
||||
ERROR = 1,
|
||||
TIMEOUT = 401,
|
||||
TYPE = 'success',
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: contentType
|
||||
*/
|
||||
export enum ContentTypeEnum {
|
||||
// json
|
||||
JSON = 'application/json;charset=UTF-8',
|
||||
// form-data qs
|
||||
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
// form-data upload
|
||||
FORM_DATA = 'multipart/form-data;charset=UTF-8',
|
||||
}
|
||||
15
src/hooks/app/useGlobalStyle.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import pagesJson from '@/pages.json';
|
||||
|
||||
const { globalStyle } = pagesJson;
|
||||
/**
|
||||
* 全局样式
|
||||
*/
|
||||
export function useGlobalStyle() {
|
||||
const { navigationBarTextStyle, navigationBarTitleText, navigationBarBackgroundColor, backgroundColor } = globalStyle;
|
||||
return {
|
||||
navigationBarTextStyle,
|
||||
navigationBarTitleText,
|
||||
navigationBarBackgroundColor,
|
||||
backgroundColor,
|
||||
};
|
||||
}
|
||||
11
src/hooks/app/useSystem.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 系统信息
|
||||
* @description 除去uni官方不推荐使用的返回参数
|
||||
* @link https://uniapp.dcloud.net.cn/api/system/info.html
|
||||
*/
|
||||
export function useSystem() {
|
||||
const systemInfo = reactive<UniNamespace.GetSystemInfoResult>(uni.getSystemInfoSync());
|
||||
return {
|
||||
systemInfo,
|
||||
};
|
||||
}
|
||||
17
src/main.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { createSSRApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import 'virtual:uno.css';
|
||||
import { setupRouter } from './router';
|
||||
import { setupStore } from './stores';
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App);
|
||||
|
||||
setupStore(app);
|
||||
|
||||
setupRouter(app);
|
||||
|
||||
return {
|
||||
app,
|
||||
};
|
||||
}
|
||||
116
src/manifest.json
Normal file
@ -0,0 +1,116 @@
|
||||
{
|
||||
"name": "uniapp_vue3_vite_ts",
|
||||
"appid": "__UNI__4350F4E",
|
||||
"description": "",
|
||||
"versionName": "1.2.1",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"nvueStyleCompiler": "uni-app",
|
||||
"compilerVersion": 3,
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules": {},
|
||||
"optimization": {
|
||||
"subPackages": true
|
||||
},
|
||||
/* 应用发布信息 */
|
||||
"distribute": {
|
||||
/* android打包配置 */
|
||||
"android": {
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
],
|
||||
"minSdkVersion": 21
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios": {
|
||||
"dSYMs": false
|
||||
},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs": {
|
||||
"ad": {}
|
||||
},
|
||||
"splashscreen": {
|
||||
"useOriginalMsgbox": true
|
||||
}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp": {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin": {
|
||||
"appid": "wxc8e4923d551cd4a4",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true
|
||||
},
|
||||
"usingComponents": true,
|
||||
"permission": {},
|
||||
"optimization": {
|
||||
"subPackages": true
|
||||
},
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"style": "v2"
|
||||
},
|
||||
"mp-alipay": {
|
||||
"usingComponents": true,
|
||||
"optimization": {
|
||||
"subPackages": true
|
||||
}
|
||||
},
|
||||
"mp-baidu": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-toutiao": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"uniStatistics": {
|
||||
"enable": false
|
||||
},
|
||||
"vueVersion": "3",
|
||||
"h5": {
|
||||
"router": {
|
||||
"mode": "hash",
|
||||
"base": "./"
|
||||
},
|
||||
"devServer": {
|
||||
"https": false
|
||||
},
|
||||
"title": "uniapp_vue3_vite_ts",
|
||||
"unipush": {
|
||||
"enable": false
|
||||
},
|
||||
"sdkConfigs": {
|
||||
"maps": {}
|
||||
},
|
||||
"optimization": {
|
||||
"treeShaking": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/mock/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { createAlovaMockAdapter } from '@alova/mock';
|
||||
import { uniappMockResponse, uniappRequestAdapter } from '@alova/adapter-uniapp';
|
||||
import { mockGroupV1 } from '@/mock/v1';
|
||||
|
||||
/**
|
||||
* 模拟数据请求适配器
|
||||
* @link https://alova.js.org/zh-CN/extension/alova-mock
|
||||
*/
|
||||
export const mockAdapter = createAlovaMockAdapter(mockGroupV1, {
|
||||
// 指定uniapp请求适配器后,未匹配模拟接口的请求将使用这个适配器发送请求
|
||||
httpAdapter: uniappRequestAdapter,
|
||||
|
||||
// mock接口响应延迟,单位毫秒
|
||||
delay: 1500,
|
||||
|
||||
// 是否打印mock接口请求信息
|
||||
// mockRequestLogger: false,
|
||||
|
||||
// 模拟响应适配器,指定后响应数据将转换为uniapp兼容的数据格式
|
||||
onMockResponse: uniappMockResponse,
|
||||
});
|
||||
22
src/mock/utils.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { ResultEnum } from '@/enums/httpEnum';
|
||||
import type { API } from '@/services/model/baseModel';
|
||||
|
||||
interface MockResponseOptions<T = any> {
|
||||
status: number
|
||||
statusText: string
|
||||
responseHeaders: Record<string, any>
|
||||
body: API<T>
|
||||
}
|
||||
|
||||
export function createMock(options: Partial<API>): MockResponseOptions {
|
||||
return {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
responseHeaders: {},
|
||||
body: {
|
||||
code: ResultEnum.SUCCESS,
|
||||
message: 'succeed',
|
||||
...options,
|
||||
},
|
||||
};
|
||||
}
|
||||
3
src/mock/v1/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { authMocks } from '@/mock/v1/modules/auth';
|
||||
|
||||
export const mockGroupV1 = [authMocks];
|
||||
40
src/mock/v1/modules/auth.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { defineMock } from '@alova/mock';
|
||||
import { join, random, sampleSize } from 'lodash-es';
|
||||
import multiavatar from '@multiavatar/multiavatar';
|
||||
import { createMock } from '@/mock/utils';
|
||||
import { ResultEnum } from '@/enums/httpEnum';
|
||||
import { getRandomChsString } from '@/utils/character';
|
||||
|
||||
function createRandomToken(len = 36 * 6) {
|
||||
const token = join(sampleSize('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ._-', len), '');
|
||||
return `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.${token}`;
|
||||
}
|
||||
|
||||
export const authMocks = defineMock({
|
||||
// 登录
|
||||
'[POST]/api/login': (params) => {
|
||||
const { email, password } = params.data;
|
||||
if (email === 'uni-app@test.com' && (password === 'Vue3_Ts_Vite' || password === '123456')) {
|
||||
const token = createRandomToken();
|
||||
return createMock({ data: { token } });
|
||||
}
|
||||
return createMock({ data: [], code: ResultEnum.FAIL, message: '邮箱或密码错误' });
|
||||
},
|
||||
// 获取用户信息
|
||||
'[GET]/api/users': () => {
|
||||
const generateNicknames = getRandomChsString(random(2, 6));
|
||||
const svgCode = multiavatar(generateNicknames);
|
||||
const base64SVG = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
|
||||
svgCode,
|
||||
)}`;
|
||||
|
||||
return createMock({
|
||||
data: {
|
||||
id: 1,
|
||||
nickname: generateNicknames,
|
||||
avatar: base64SVG,
|
||||
email: 'uni-app@test.com',
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
89
src/pages.json
Normal file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"name": "Home",
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "Home"
|
||||
},
|
||||
"meta": {
|
||||
"tabBar": true,
|
||||
"ignoreAuth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Demo",
|
||||
"path": "pages/demo/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "Demo"
|
||||
},
|
||||
"meta": {
|
||||
"tabBar": true,
|
||||
"ignoreAuth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "About",
|
||||
"path": "pages/about/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "关于"
|
||||
},
|
||||
"meta": {
|
||||
"ignoreAuth": true,
|
||||
"tabBar": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Login",
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录"
|
||||
},
|
||||
"meta": {
|
||||
"ignoreAuth": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Log",
|
||||
"path": "pages/log/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "日志"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subPackages": [],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarBackgroundColor": "#F2F2F2",
|
||||
"backgroundColor": "#F2F2F2",
|
||||
"navigationStyle": "default",
|
||||
"renderingMode": "seperated",
|
||||
"pageOrientation": "portrait"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#474747",
|
||||
"selectedColor": "#9BC6FC",
|
||||
"backgroundColor": "#F2F2F2",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页",
|
||||
"iconPath": "static/images/tabBar/home.png",
|
||||
"selectedIconPath": "static/images/tabBar/selectedHome.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/demo/index",
|
||||
"text": "Demo",
|
||||
"iconPath": "static/images/tabBar/demo.png",
|
||||
"selectedIconPath": "static/images/tabBar/selectedDemo.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/about/index",
|
||||
"text": "关于",
|
||||
"iconPath": "static/images/tabBar/about.png",
|
||||
"selectedIconPath": "static/images/tabBar/selectedAbout.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
42
src/pages/about/index.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import BasicButton from '@/components/BasicButton/index.vue';
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const { loggedIn, userInfo } = storeToRefs(userStore);
|
||||
|
||||
function handleJump(url: string) {
|
||||
router.push(url);
|
||||
}
|
||||
|
||||
// 登出
|
||||
function handleLoginOut() {
|
||||
userStore.logout();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="text-md pt-36">
|
||||
<view v-if="loggedIn" class="text-center">
|
||||
<image class="h-56 w-56" :src="userInfo?.avatar" />
|
||||
<view class="mt-2">
|
||||
{{ userInfo?.nickname }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-6 flex flex-col gap-y-xl justify-center items-center">
|
||||
<BasicButton @click="handleJump('/pages/log/index?id=4345&title=log&word=关键词')">
|
||||
log
|
||||
</BasicButton>
|
||||
<BasicButton v-if="loggedIn" @click="handleLoginOut">
|
||||
登出
|
||||
</BasicButton>
|
||||
<BasicButton v-else @click="handleJump('/pages/login/index')">
|
||||
登入
|
||||
</BasicButton>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
12
src/pages/demo/index.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
const demo = ref('Demo');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="pt-36 text-lg font-medium flex justify-center items-center">
|
||||
{{ demo }}
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
20
src/pages/index/index.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { getRandomIcon } from '@/utils/character';
|
||||
import { platform } from '@/utils/platform';
|
||||
|
||||
const logo = getRandomIcon();
|
||||
const appTitle = 'uniapp-vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="pt-36 flex flex-col gap-y-2 items-center">
|
||||
<image :src="logo" class="h-56 w-56" alt="" mode="widthFix" />
|
||||
<view class="text-xl font-semibold">
|
||||
{{ appTitle }}
|
||||
</view>
|
||||
<view>当前平台:{{ platform }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
20
src/pages/log/index.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
// onLoad((query) => {
|
||||
// console.log('log onLoad query', query);
|
||||
// });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="text-center">
|
||||
<view class="mt-36 text-center">
|
||||
登录后访问log
|
||||
</view>
|
||||
<image class="my-4 h-48 w-48" :src="userStore.userInfo?.avatar" mode="aspectFit" lazy-load="false" binderror="" bindload="" />
|
||||
<view>{{ userStore.userInfo?.nickname }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
109
src/pages/login/index.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { omit } from 'lodash-es';
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
import { Toast } from '@/utils/uniapi/prompt';
|
||||
|
||||
const pageQuery = ref<Record<string, any> | undefined>(undefined);
|
||||
onLoad((query) => {
|
||||
pageQuery.value = query;
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const form = reactive({
|
||||
email: 'uni-app@test.com',
|
||||
password: 'Vue3_Ts_Vite',
|
||||
});
|
||||
const userStore = useUserStore();
|
||||
|
||||
function submit(e: any) {
|
||||
userStore.login(e.detail.value).then(() => {
|
||||
Toast('登录成功', { duration: 1500 });
|
||||
setTimeout(() => {
|
||||
if (unref(pageQuery)?.redirect) {
|
||||
// 如果有存在redirect(重定向)参数,登录成功后直接跳转
|
||||
const params = omit(unref(pageQuery), ['redirect', 'tabBar']);
|
||||
if (unref(pageQuery)) {
|
||||
// 这里replace方法无法跳转tabbar页面故改为replaceAll
|
||||
unref(pageQuery)?.tabBar === 'true'
|
||||
? router.replaceAll({ name: unref(pageQuery)?.redirect, params })
|
||||
: router.replace({ name: unref(pageQuery)?.redirect, params });
|
||||
}
|
||||
} else {
|
||||
// 不存在则返回上一页
|
||||
router.back();
|
||||
}
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="container">
|
||||
<view class="title">
|
||||
登录
|
||||
</view>
|
||||
<view class="form-wrap">
|
||||
<form class="form" @submit="submit">
|
||||
<label class="form-item">
|
||||
<view class="form-label">邮箱:</view>
|
||||
<view class="form-element"><input name="email" :value="form.email"></view>
|
||||
</label>
|
||||
<label class="form-item">
|
||||
<view class="form-label">密码:</view>
|
||||
<view class="form-element"><input type="password" name="password" :value="form.password"></view>
|
||||
</label>
|
||||
<button form-type="submit" class="submit-btn" hover-class="none">
|
||||
登录
|
||||
</button>
|
||||
</form>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
width: 80%;
|
||||
|
||||
.title {
|
||||
padding: 320rpx 0 32rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
padding: 20rpx 24rpx;
|
||||
box-shadow: 16rpx 16rpx 30rpx #e5e7eb;
|
||||
|
||||
.form {
|
||||
.form-item {
|
||||
display: flex;
|
||||
height: 88rpx;
|
||||
border-bottom: 2rpx solid #dbeafe;
|
||||
align-items: center;
|
||||
|
||||
.form-label {
|
||||
min-width: 96rpx;
|
||||
}
|
||||
|
||||
.form-element {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 44rpx;
|
||||
border: 4rpx solid #bfdbfe;
|
||||
background-color: #60a5fa;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
|
||||
:hover {
|
||||
background-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
33
src/pages/notFound/404.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import BasicButton from '@/components/BasicButton/index.vue';
|
||||
|
||||
const go = ref<string>('');
|
||||
const router = useRouter();
|
||||
const redirect = ref<string>('');
|
||||
onLoad((query) => {
|
||||
go.value = query?.go || '';
|
||||
redirect.value = query?.redirect || '';
|
||||
});
|
||||
|
||||
/**
|
||||
* 返回首页
|
||||
*/
|
||||
function backHome() {
|
||||
router.pushTab(redirect.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="pt-320rpx flex flex-col w-screen items-center">
|
||||
<image class="w-360rpx" mode="widthFix" src="/static/svg/weep.svg" />
|
||||
<view class="mb-40rpx">
|
||||
<text>{{ go }} 页面找不到了~</text>
|
||||
</view>
|
||||
<BasicButton @click="backHome">
|
||||
返回首页
|
||||
</BasicButton>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
7
src/pages/template/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<view> 页面模板,新建pages,将此页面内容复制粘贴到新建.vue文件 </view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
21
src/pagesA/list/test1/index.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import BasicButton from '@/components/BasicButton/index.vue';
|
||||
|
||||
const router = useRouter();
|
||||
function jumpTest2() {
|
||||
router.push('/pagesA/list/test2/index?id=256');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="center">
|
||||
Test1
|
||||
</view>
|
||||
<view class="center">
|
||||
<BasicButton @click="jumpTest2">
|
||||
Test2 →
|
||||
</BasicButton>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
19
src/pagesA/list/test2/index.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import BasicButton from '@/components/BasicButton/index.vue';
|
||||
|
||||
const router = useRouter();
|
||||
function jumpDetail() {
|
||||
router.push('/pagesB/detail/index?page=1&limit=20');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
<view> Test2 </view>
|
||||
<BasicButton @click="jumpDetail">
|
||||
Detail →
|
||||
</BasicButton>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
7
src/pagesB/detail/index.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
Detail
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
44
src/router/guard.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import type { Router } from 'uni-mini-router/lib/interfaces';
|
||||
import { isLogin } from '@/utils/auth';
|
||||
|
||||
export function createRouterGuard(router: Router) {
|
||||
createBeforeEachGuard(router);
|
||||
createAfterEachGuard(router);
|
||||
}
|
||||
|
||||
function createBeforeEachGuard(router: Router) {
|
||||
router.beforeEach((to, _, next) => {
|
||||
console.log('beforeEach', to);
|
||||
const _isLogin = isLogin();
|
||||
if (to && to?.meta?.ignoreAuth) {
|
||||
// 如果目标路由忽略验证直接跳转
|
||||
next();
|
||||
} else if (!_isLogin && to && to.name !== 'Login') {
|
||||
// 如果没有登录且目标路由不是登录页面则跳转到登录页面
|
||||
// 将目标路由和参数传入登录页面,登录成功后直接跳转到目标路由,优化体验
|
||||
next({ name: 'Login', params: { redirect: to.name!, ...to.query }, navType: 'push' });
|
||||
} else if (_isLogin && to && to.name === 'Login') {
|
||||
// 如果已经登录且目标页面是登录页面则跳转至首页
|
||||
next({ name: 'Home', navType: 'replaceAll' });
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
function createAfterEachGuard(router: Router) {
|
||||
router.afterEach((to) => {
|
||||
if (to && to?.meta?.ignoreAuth)
|
||||
return;
|
||||
const _isLogin = isLogin();
|
||||
if (!_isLogin && to && to.name !== 'Login') {
|
||||
// 如果没有登录且目标路由不是登录页面则跳转到登录页面
|
||||
router.push({ name: 'Login', params: { ...to.query } });
|
||||
} else if (_isLogin && to && to.name === 'Login') {
|
||||
// 如果已经登录且目标页面是登录页面则跳转至首页
|
||||
router.replaceAll({ name: 'Home' });
|
||||
}
|
||||
console.log('afterEach', to);
|
||||
});
|
||||
}
|
||||
19
src/router/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/**
|
||||
* router
|
||||
* @see https://gitee.com/fant-mini/uni-mini-router
|
||||
*/
|
||||
import { createRouter } from 'uni-mini-router';
|
||||
import type { App } from 'vue';
|
||||
import { createRouterGuard } from './guard';
|
||||
|
||||
const router = createRouter({
|
||||
routes: [...ROUTES], // 路由表信息
|
||||
});
|
||||
|
||||
export function setupRouter(app: App<Element>) {
|
||||
// Configure router guard
|
||||
createRouterGuard(router);
|
||||
app.use(router);
|
||||
}
|
||||
|
||||
export { router };
|
||||
31
src/services/api/auth.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { request } from '@/utils/http';
|
||||
|
||||
const LOGIN = '/login';
|
||||
const LOGIN_OUT = '/logout';
|
||||
const REFRESH_TOKEN = '/refresh/token';
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* @param params
|
||||
*/
|
||||
export function login(params: LoginParams) {
|
||||
return request.Post<LoginModel>(LOGIN, params, {
|
||||
meta: {
|
||||
ignoreAuth: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
export function logout() {
|
||||
return request.Post(LOGIN_OUT, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
*/
|
||||
export function refreshToken() {
|
||||
return request.Post<LoginModel>(REFRESH_TOKEN, {});
|
||||
}
|
||||
13
src/services/api/user.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { request } from '@/utils/http';
|
||||
import type { UserInfoModel } from '@/services/model/userModel';
|
||||
|
||||
enum Api {
|
||||
GET_USER_INFO = '/users',
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
export function getUserInfoApi() {
|
||||
return request.Get<UserInfoModel>(Api.GET_USER_INFO);
|
||||
}
|
||||
7
src/services/model/authModel.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
declare interface LoginParams {
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
declare interface LoginModel {
|
||||
token: string
|
||||
}
|
||||
7
src/services/model/baseModel.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
import type { ResultEnum } from '@/enums/httpEnum';
|
||||
|
||||
declare interface API<T = any> {
|
||||
code: ResultEnum
|
||||
data?: T
|
||||
message: string
|
||||
}
|
||||
18
src/services/model/userModel.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
export interface UserInfoModel {
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
id?: number
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
nickname?: string
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
avatar?: string
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
email?: string
|
||||
}
|
||||
15
src/settings/encryptionSetting.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { getEnvValue, getPkgVersion, isDevMode } from '@/utils/env';
|
||||
|
||||
// System default cache time, in seconds
|
||||
export const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;
|
||||
const PREFIX = getEnvValue<string>('VITE_APP_CACHE_PREFIX') || getEnvValue<string>('VITE_APP_TITLE') || 'UNI_APP_VUE3_TS';
|
||||
export const DEFAULT_PREFIX_KEY = `${PREFIX}${getPkgVersion()}`;
|
||||
|
||||
// aes encryption key
|
||||
export const cacheCipher = {
|
||||
key: 'aQ0{gD1@c_0@oH5:',
|
||||
iv: 'aF0#gC_$hE1$eA1!',
|
||||
};
|
||||
|
||||
// Whether the system cache is encrypted using aes
|
||||
export const enableStorageEncryption = !isDevMode();
|
||||
BIN
src/static/images/avatar.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/static/images/h5Qcode.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
src/static/images/tabBar/about.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/static/images/tabBar/category.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/static/images/tabBar/demo.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
src/static/images/tabBar/home.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/static/images/tabBar/selectedAbout.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/static/images/tabBar/selectedCategory.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/static/images/tabBar/selectedDemo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src/static/images/tabBar/selectedHome.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/static/images/wexinQcode.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
src/static/logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
1
src/static/svg/404.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1645602826416" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3227" data-spm-anchor-id="a313x.7781069.0.i21" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M260.266667 789.333333c-21.333333 0-38.4-17.066667-38.4-38.4v-59.733333H38.4c-12.8 0-29.866667-8.533333-34.133333-21.333333-4.266667-17.066667-4.266667-29.866667 4.266666-42.666667l221.866667-294.4c8.533333-12.8 25.6-17.066667 42.666667-12.8 17.066667 4.266667 25.6 21.333333 25.6 38.4v256h34.133333c21.333333 0 38.4 17.066667 38.4 38.4s-17.066667 38.4-38.4 38.4H298.666667v59.733333c0 21.333333-17.066667 38.4-38.4 38.4z m-145.066667-179.2h106.666667V469.333333l-106.666667 140.8zM913.066667 742.4c-21.333333 0-38.4-17.066667-38.4-38.4v-59.733333h-183.466667c-12.8 0-29.866667-8.533333-34.133333-21.333334-8.533333-12.8-4.266667-29.866667 4.266666-38.4l221.866667-294.4c8.533333-12.8 25.6-17.066667 42.666667-12.8 17.066667 4.266667 25.6 21.333333 25.6 38.4v256h34.133333c21.333333 0 38.4 17.066667 38.4 38.4s-17.066667 38.4-38.4 38.4h-34.133333v59.733334c0 17.066667-17.066667 34.133333-38.4 34.133333zM768 567.466667h106.666667V426.666667L768 567.466667zM533.333333 597.333333c-46.933333 0-85.333333-25.6-119.466666-68.266666-29.866667-38.4-42.666667-93.866667-42.666667-145.066667 0-55.466667 17.066667-106.666667 42.666667-145.066667 29.866667-42.666667 72.533333-68.266667 119.466666-68.266666 46.933333 0 85.333333 25.6 119.466667 68.266666 29.866667 38.4 42.666667 93.866667 42.666667 145.066667 0 55.466667-17.066667 106.666667-42.666667 145.066667-34.133333 46.933333-76.8 68.266667-119.466667 68.266666z m0-362.666666c-55.466667 0-98.133333 68.266667-98.133333 149.333333s46.933333 149.333333 98.133333 149.333333c55.466667 0 98.133333-68.266667 98.133334-149.333333s-46.933333-149.333333-98.133334-149.333333z" fill="#5e7987" p-id="3228" data-spm-anchor-id="a313x.7781069.0.i16" class=""></path><path d="M354.133333 691.2a162.133333 21.333333 0 1 0 324.266667 0 162.133333 21.333333 0 1 0-324.266667 0Z" fill="#d0dfe6" p-id="3229" data-spm-anchor-id="a313x.7781069.0.i20" class=""></path><path d="M8.533333 832a162.133333 21.333333 0 1 0 324.266667 0 162.133333 21.333333 0 1 0-324.266667 0Z" fill="#d0dfe6" p-id="3230" data-spm-anchor-id="a313x.7781069.0.i22" class=""></path><path d="M661.333333 797.866667a162.133333 21.333333 0 1 0 324.266667 0 162.133333 21.333333 0 1 0-324.266667 0Z" fill="#d0dfe6" p-id="3231" data-spm-anchor-id="a313x.7781069.0.i19" class=""></path></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
1
src/static/svg/LOGO.svg
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
1
src/static/svg/weep.svg
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
12
src/stores/app.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
interface AppState {
|
||||
sys?: string | number
|
||||
}
|
||||
|
||||
export const useAppStore = defineStore({
|
||||
id: 'app-store',
|
||||
state: (): AppState => ({}),
|
||||
getters: {},
|
||||
actions: {},
|
||||
});
|
||||
10
src/stores/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import type { App } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
export function setupStore(app: App<Element>) {
|
||||
app.use(store);
|
||||
}
|
||||
|
||||
export { store };
|
||||
69
src/stores/modules/user.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { useRequest } from 'alova';
|
||||
import { getUserInfoApi } from '@/services/api/user';
|
||||
import type { UserInfoModel } from '@/services/model/userModel';
|
||||
import { login as loginApi } from '@/services/api/auth';
|
||||
import { getToken, isLogin, setToken } from '@/utils/auth';
|
||||
import { removeCache } from '@/utils/cache';
|
||||
import { TOKEN_KEY } from '@/enums/cacheEnum';
|
||||
|
||||
export const useUserStore = defineStore('UserStore', () => {
|
||||
const token = ref<string | null>(null);
|
||||
const userInfo = ref<UserInfoModel | null>(null);
|
||||
|
||||
// 初始化
|
||||
function initUserInfo() {
|
||||
if (isLogin()) {
|
||||
token.value = getToken();
|
||||
getUserInfo();
|
||||
}
|
||||
}
|
||||
|
||||
// 是否登录
|
||||
const loggedIn = computed(() => !!token.value);
|
||||
|
||||
// 登录
|
||||
const { send: sendLogin } = useRequest(loginApi, { immediate: false });
|
||||
async function login(params: LoginParams) {
|
||||
try {
|
||||
const res = await sendLogin(params);
|
||||
token.value = res.token;
|
||||
setToken(res.token);
|
||||
await getUserInfo();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const { send: _getUserInfo } = useRequest(getUserInfoApi, { initialData: null, immediate: false });
|
||||
async function getUserInfo() {
|
||||
try {
|
||||
userInfo.value = await _getUserInfo();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 登出
|
||||
// const { send: sendLogout } = useRequest(logoutApi, { immediate: false });
|
||||
async function logout() {
|
||||
try {
|
||||
// await sendLogout();
|
||||
removeCache(TOKEN_KEY);
|
||||
userInfo.value = null;
|
||||
token.value = null;
|
||||
} catch (err: any) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
loggedIn,
|
||||
login,
|
||||
logout,
|
||||
getUserInfo,
|
||||
initUserInfo,
|
||||
};
|
||||
});
|
||||
0
src/styles/main.css
Normal file
3
src/types.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
// type.d.ts
|
||||
declare const ROUTES: [];
|
||||
declare const PLATFORM: string;
|
||||
24
src/types/env.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
// / <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
|
||||
const component: DefineComponent<NonNullable<unknown>, NonNullable<unknown>, any>;
|
||||
export default component;
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_ENV: string
|
||||
readonly VITE_APP_TITLE: string
|
||||
readonly VITE_BASE_URL: string
|
||||
readonly VITE_UPLOAD_URL: string
|
||||
readonly VITE_APP_CACHE_PREFIX: string
|
||||
readonly VITE_PORT: number
|
||||
readonly VITE_USE_MOCK: string | boolean
|
||||
readonly VITE_PROXY_PREFIX: string
|
||||
readonly VITE_UPLOAD_PROXY_PREFIX: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
11
src/types/uni-mini-router.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
import 'uni-mini-router';
|
||||
|
||||
declare module 'uni-mini-router' {
|
||||
|
||||
interface Route {
|
||||
meta?: {
|
||||
ignoreAuth?: boolean
|
||||
tabBar?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/types/unplugin-transform-class-vite.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
declare module 'unplugin-transform-class/vite' {
|
||||
const transformClassVitePlugin: any;
|
||||
export default transformClassVitePlugin;
|
||||
}
|
||||
47
src/uni.scss
Normal file
@ -0,0 +1,47 @@
|
||||
view,
|
||||
scroll-view,
|
||||
swiper,
|
||||
match-media,
|
||||
movable-area,
|
||||
movable-view,
|
||||
cover-view,
|
||||
cover-image,
|
||||
icon,
|
||||
text,
|
||||
rich-text,
|
||||
progress,
|
||||
button,
|
||||
checkboxe,
|
||||
ditor,
|
||||
form,
|
||||
input,
|
||||
label,
|
||||
picker,
|
||||
picker-view,
|
||||
radio,
|
||||
slider,
|
||||
switch,
|
||||
textarea,
|
||||
navigator,
|
||||
audio,
|
||||
camera,
|
||||
image,
|
||||
video,
|
||||
live-player,
|
||||
live-pusher,
|
||||
map,
|
||||
canvas,
|
||||
web-view,
|
||||
:before,
|
||||
:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 隐藏scroll-view的滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
26
src/utils/auth.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { getCache, setCache } from '@/utils/cache';
|
||||
import { TOKEN_KEY } from '@/enums/cacheEnum';
|
||||
|
||||
const authenticationScheme = 'Bearer';
|
||||
|
||||
export function getToken() {
|
||||
return getCache<string>(TOKEN_KEY) || null;
|
||||
}
|
||||
|
||||
export function getAuthorization() {
|
||||
const token = getToken();
|
||||
return token ? `${authenticationScheme} ${token}` : null;
|
||||
}
|
||||
|
||||
export function setToken(token: string) {
|
||||
return setCache(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
return setCache(TOKEN_KEY, null);
|
||||
}
|
||||
|
||||
// 是否登录
|
||||
export function isLogin() {
|
||||
return !!getToken();
|
||||
}
|
||||
29
src/utils/cache/index.ts
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
import type { CreateStorageParams } from './storageCache';
|
||||
import { createStorage } from './storageCache';
|
||||
import { DEFAULT_CACHE_TIME, DEFAULT_PREFIX_KEY, cacheCipher, enableStorageEncryption } from '@/settings/encryptionSetting';
|
||||
|
||||
const options: Partial<CreateStorageParams> = {
|
||||
prefixKey: DEFAULT_PREFIX_KEY,
|
||||
key: cacheCipher.key,
|
||||
iv: cacheCipher.iv,
|
||||
hasEncrypt: enableStorageEncryption,
|
||||
timeout: DEFAULT_CACHE_TIME,
|
||||
};
|
||||
|
||||
export const storage = createStorage(options);
|
||||
|
||||
export function setCache(key: string, value: any, expire?: number | null): void {
|
||||
storage.set(key, value, expire);
|
||||
}
|
||||
|
||||
export function getCache<T = any>(key: string): T {
|
||||
return storage.get<T>(key);
|
||||
}
|
||||
|
||||
export function removeCache(key: string): void {
|
||||
return storage.remove(key);
|
||||
}
|
||||
|
||||
export function clearCache(): void {
|
||||
return storage.clear();
|
||||
}
|
||||
110
src/utils/cache/storageCache.ts
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
import { cacheCipher } from '@/settings/encryptionSetting';
|
||||
import type { EncryptionParams } from '@/utils/cipher';
|
||||
import { AesEncryption } from '@/utils/cipher';
|
||||
import { isNullOrUnDef } from '@/utils/is';
|
||||
|
||||
export interface CreateStorageParams extends EncryptionParams {
|
||||
prefixKey: string
|
||||
hasEncrypt: boolean
|
||||
timeout?: number | null
|
||||
}
|
||||
export function createStorage({
|
||||
prefixKey = '',
|
||||
key = cacheCipher.key,
|
||||
iv = cacheCipher.iv,
|
||||
timeout = null,
|
||||
hasEncrypt = true,
|
||||
}: Partial<CreateStorageParams> = {}) {
|
||||
if (hasEncrypt && [key.length, iv.length].some(item => item !== 16)) {
|
||||
throw new Error('When hasEncrypt is true, the key or iv must be 16 bits!');
|
||||
}
|
||||
|
||||
const encryption = new AesEncryption({ key, iv });
|
||||
|
||||
/**
|
||||
* Cache class
|
||||
* Construction parameters can be passed into sessionStorage, localStorage,
|
||||
* @class Cache
|
||||
* @example
|
||||
*/
|
||||
class Storage {
|
||||
private prefixKey?: string;
|
||||
|
||||
private encryption: AesEncryption;
|
||||
|
||||
private hasEncrypt: boolean;
|
||||
|
||||
constructor() {
|
||||
this.prefixKey = prefixKey;
|
||||
this.encryption = encryption;
|
||||
this.hasEncrypt = hasEncrypt;
|
||||
}
|
||||
|
||||
private getKey(key: string) {
|
||||
return `${this.prefixKey}${key}`.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
* @param {*} expire Expiration time in seconds
|
||||
* @memberof Cache
|
||||
*/
|
||||
set(key: string, value: any, expire: number | null = timeout) {
|
||||
try {
|
||||
const stringData = JSON.stringify({
|
||||
value,
|
||||
time: Date.now(),
|
||||
expire: !isNullOrUnDef(expire) ? new Date().getTime() + expire * 1000 : null,
|
||||
});
|
||||
const stringifyValue = this.hasEncrypt ? this.encryption.encryptByAES(stringData) : stringData;
|
||||
uni.setStorageSync(this.getKey(key), stringifyValue);
|
||||
} catch (err) {
|
||||
throw new Error(`setStorageSync error: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read cache
|
||||
* @param {string} key
|
||||
* @param {*} def
|
||||
* @memberof Cache
|
||||
*/
|
||||
get<T = any>(key: string, def: any = null): T {
|
||||
const val = uni.getStorageSync(this.getKey(key));
|
||||
if (!val)
|
||||
return def;
|
||||
|
||||
try {
|
||||
const decVal = this.hasEncrypt ? this.encryption.decryptByAES(val) : val;
|
||||
const data = JSON.parse(decVal);
|
||||
const { value, expire } = data;
|
||||
if (isNullOrUnDef(expire) || expire < new Date().getTime()) {
|
||||
this.remove(key);
|
||||
return def;
|
||||
}
|
||||
return value;
|
||||
} catch (e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache based on key
|
||||
* @param {string} key
|
||||
* @memberof Cache
|
||||
*/
|
||||
remove(key: string) {
|
||||
uni.removeStorageSync(this.getKey(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all caches of this instance
|
||||
*/
|
||||
clear(): void {
|
||||
uni.clearStorageSync();
|
||||
}
|
||||
}
|
||||
return new Storage();
|
||||
}
|
||||
34
src/utils/character.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { random } from 'lodash-es';
|
||||
import multiavatar from '@multiavatar/multiavatar';
|
||||
|
||||
const CHS_RANGE_START = 0x4E00; // 简体中文编码范围开始
|
||||
const CHS_RANGE_END = 0x9FA5; // 简体中文编码范围结束
|
||||
|
||||
// 生成随机简体中文字符的函数
|
||||
function getRandomChsChar() {
|
||||
// 生成 CHS_RANGE_START 和 CHS_RANGE_END 之间的随机整数
|
||||
const randomCharCode
|
||||
= Math.floor(Math.random() * (CHS_RANGE_END - CHS_RANGE_START + 1)) + CHS_RANGE_START;
|
||||
|
||||
// 将随机整数转换为字符
|
||||
return String.fromCharCode(randomCharCode);
|
||||
}
|
||||
|
||||
// 生成指定长度的随机简体中文字符串
|
||||
export function getRandomChsString(length: number) {
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += getRandomChsChar();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机 svg 图标
|
||||
*/
|
||||
export function getRandomIcon() {
|
||||
const svgCode = multiavatar(getRandomChsString(random(16, 32)));
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(
|
||||
svgCode,
|
||||
)}`;
|
||||
}
|
||||
71
src/utils/cipher.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { decrypt, encrypt } from 'crypto-js/aes';
|
||||
import UTF8, { parse } from 'crypto-js/enc-utf8';
|
||||
import pkcs7 from 'crypto-js/pad-pkcs7';
|
||||
import ECB from 'crypto-js/mode-ecb';
|
||||
import md5 from 'crypto-js/md5';
|
||||
|
||||
import Base64 from 'crypto-js/enc-base64';
|
||||
|
||||
export interface EncryptionParams {
|
||||
key: string
|
||||
iv: string
|
||||
}
|
||||
|
||||
/**
|
||||
* AES 加密解密
|
||||
*/
|
||||
export class AesEncryption {
|
||||
private key;
|
||||
|
||||
private iv;
|
||||
|
||||
constructor(opt: Partial<EncryptionParams> = {}) {
|
||||
const { key, iv } = opt;
|
||||
if (key) {
|
||||
this.key = parse(key);
|
||||
}
|
||||
if (iv) {
|
||||
this.iv = parse(iv);
|
||||
}
|
||||
}
|
||||
|
||||
get getOptions() {
|
||||
return {
|
||||
mode: ECB,
|
||||
padding: pkcs7,
|
||||
iv: this.iv,
|
||||
};
|
||||
}
|
||||
|
||||
encryptByAES(cipherText: string) {
|
||||
return encrypt(cipherText, this.key!, this.getOptions).toString();
|
||||
}
|
||||
|
||||
decryptByAES(cipherText: string) {
|
||||
return decrypt(cipherText, this.key!, this.getOptions).toString(UTF8);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64加密
|
||||
* @param cipherText
|
||||
*/
|
||||
export function encryptByBase64(cipherText: string) {
|
||||
return UTF8.parse(cipherText).toString(Base64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64解密
|
||||
* @param cipherText
|
||||
*/
|
||||
export function decodeByBase64(cipherText: string) {
|
||||
return Base64.parse(cipherText).toString(UTF8);
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5加密
|
||||
* @param password
|
||||
*/
|
||||
export function encryptByMd5(password: string) {
|
||||
return md5(password).toString();
|
||||
}
|
||||
83
src/utils/env.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import pkg from '../../package.json';
|
||||
import { isH5 } from '@/utils/platform';
|
||||
|
||||
/**
|
||||
* @description: Generate cache key according to version
|
||||
*/
|
||||
export function getPkgVersion() {
|
||||
return `${`__${pkg.version}`}__`.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Development mode
|
||||
*/
|
||||
export const devMode = 'development';
|
||||
|
||||
/**
|
||||
* @description: Production mode
|
||||
*/
|
||||
export const prodMode = 'production';
|
||||
|
||||
/**
|
||||
* @description: Get environment mode
|
||||
* @returns:
|
||||
* @example:
|
||||
*/
|
||||
export function getEnvMode(): string {
|
||||
return getEnvValue('VITE_ENV');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Get environment variables
|
||||
* @returns:
|
||||
* @example:
|
||||
*/
|
||||
export function getEnvValue<T = string>(key: keyof ImportMetaEnv): T {
|
||||
const envValue = import.meta.env[key];
|
||||
return (envValue === 'true' ? true : envValue === 'false' ? false : envValue) as unknown as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Is it a development mode
|
||||
* @returns:
|
||||
* @example:
|
||||
*/
|
||||
export function isDevMode(): boolean {
|
||||
return getEnvMode() === devMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Is it a production mode
|
||||
* @returns:
|
||||
* @example:
|
||||
*/
|
||||
export function isProdMode(): boolean {
|
||||
return getEnvMode() === prodMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Whether to use mock data
|
||||
* @returns:
|
||||
* @example:
|
||||
*/
|
||||
export function isUseMock(): boolean {
|
||||
return getEnvValue('VITE_USE_MOCK');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Get environment VITE_BASE_URL value
|
||||
* @returns:
|
||||
* @example:
|
||||
*/
|
||||
export function getBaseUrl(): string {
|
||||
return (isH5() && isDevMode()) ? getEnvValue<string>('VITE_PROXY_PREFIX') : getEnvValue<string>('VITE_BASE_URL');
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: Get environment VITE_UPLOAD_URL value
|
||||
* @returns:
|
||||
* @example:
|
||||
*/
|
||||
export function getUploadUrl(): string {
|
||||
return (isH5() && isDevMode()) ? getEnvValue<string>('VITE_UPLOAD_PROXY_PREFIX') : getEnvValue<string>('VITE_UPLOAD_URL');
|
||||
}
|
||||
51
src/utils/http/checkStatus.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Toast } from '@/utils/uniapi/prompt';
|
||||
|
||||
export function checkStatus(status: number, msg: string): void {
|
||||
let errMessage = null;
|
||||
switch (status) {
|
||||
case 400:
|
||||
errMessage = `${msg}`;
|
||||
break;
|
||||
// 401: Not logged in
|
||||
// Jump to the login page if not logged in, and carry the path of the current page
|
||||
// Return to the current page after successful login. This step needs to be operated on the login page.
|
||||
case 401:
|
||||
errMessage = '用户没有权限(令牌、用户名、密码错误)!';
|
||||
break;
|
||||
case 403:
|
||||
errMessage = '用户得到授权,但是访问是被禁止的!';
|
||||
break;
|
||||
case 404:
|
||||
errMessage = '网络请求错误,未找到该资源!';
|
||||
break;
|
||||
case 405:
|
||||
errMessage = '网络请求错误,请求方法未允许!';
|
||||
break;
|
||||
case 408:
|
||||
errMessage = '网络请求超时!';
|
||||
break;
|
||||
case 500:
|
||||
errMessage = '服务器错误,请联系管理员!';
|
||||
break;
|
||||
case 501:
|
||||
errMessage = '网络未实现!';
|
||||
break;
|
||||
case 502:
|
||||
errMessage = '服务不可用,服务器暂时过载或维护!';
|
||||
break;
|
||||
case 503:
|
||||
errMessage = '服务不可用,服务器暂时过载或维护!';
|
||||
break;
|
||||
case 504:
|
||||
errMessage = '网络超时!';
|
||||
break;
|
||||
case 505:
|
||||
errMessage = 'http版本不支持该请求!';
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
if (errMessage) {
|
||||
Toast(errMessage);
|
||||
}
|
||||
}
|
||||
75
src/utils/http/index.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { createAlova } from 'alova';
|
||||
import AdapterUniapp from '@alova/adapter-uniapp';
|
||||
import { assign } from 'lodash-es';
|
||||
import { checkStatus } from './checkStatus';
|
||||
import { getBaseUrl, isUseMock } from '@/utils/env';
|
||||
import { mockAdapter } from '@/mock';
|
||||
import { ContentTypeEnum, ResultEnum } from '@/enums/httpEnum';
|
||||
import type { API } from '@/services/model/baseModel';
|
||||
import { getAuthorization } from '@/utils/auth';
|
||||
|
||||
const BASE_URL = getBaseUrl();
|
||||
|
||||
const ContentType = {
|
||||
'Content-Type': ContentTypeEnum.JSON,
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
};
|
||||
|
||||
/**
|
||||
* alova 请求实例
|
||||
* @link https://github.com/alovajs/alova
|
||||
*/
|
||||
const alovaInstance = createAlova({
|
||||
baseURL: BASE_URL,
|
||||
localCache: null, // 设置为null即可全局关闭全部请求缓存
|
||||
...AdapterUniapp({
|
||||
/* #ifndef APP-PLUS */
|
||||
mockRequest: isUseMock() ? mockAdapter : undefined, // APP 平台无法使用mock
|
||||
/* #endif */
|
||||
}),
|
||||
timeout: 5000,
|
||||
beforeRequest: (method) => {
|
||||
method.config.headers = assign(method.config.headers, ContentType);
|
||||
const { config } = method;
|
||||
const ignoreAuth = !config.meta?.ignoreAuth;
|
||||
const authorization = ignoreAuth ? getAuthorization() : null;
|
||||
if (ignoreAuth && !authorization) {
|
||||
throw new Error('[请求错误]:未登录');
|
||||
}
|
||||
method.config.headers.authorization = getAuthorization();
|
||||
},
|
||||
responded: {
|
||||
/**
|
||||
* 请求成功的拦截器
|
||||
* 第二个参数为当前请求的method实例,你可以用它同步请求前后的配置信息
|
||||
* @param response
|
||||
* @param method
|
||||
*/
|
||||
onSuccess: async (response, method) => {
|
||||
const { config } = method;
|
||||
const { requestType } = config;
|
||||
const { statusCode, data: rawData, errMsg } = response as UniNamespace.RequestSuccessCallbackResult;
|
||||
if (statusCode === 200) {
|
||||
if (requestType) {
|
||||
return response;
|
||||
}
|
||||
const { code, message, data } = rawData as API;
|
||||
if (code === ResultEnum.SUCCESS) {
|
||||
return data as any;
|
||||
}
|
||||
checkStatus(statusCode, message || '');
|
||||
throw new Error(`请求错误[${code}]:${message}`);
|
||||
}
|
||||
throw new Error(`请求错误[${statusCode}]:${errMsg}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 请求失败的拦截器,请求错误时将会进入该拦截器。
|
||||
*/
|
||||
onError: (err) => {
|
||||
throw new Error(`请求错误:${err}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const request = alovaInstance;
|
||||
14
src/utils/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { isObject } from '@/utils/is';
|
||||
|
||||
/**
|
||||
* 深度合并
|
||||
* @param src
|
||||
* @param target
|
||||
*/
|
||||
export function deepMerge<T = any>(src: any = {}, target: any = {}): T {
|
||||
let key: string;
|
||||
for (key in target) {
|
||||
src[key] = isObject(src[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key]);
|
||||
}
|
||||
return src;
|
||||
}
|
||||
7
src/utils/interceptors/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function setupInterceptors() {
|
||||
|
||||
}
|
||||
|
||||
export function removeInterceptor() {
|
||||
|
||||
}
|
||||
100
src/utils/is.ts
Normal file
@ -0,0 +1,100 @@
|
||||
const { toString } = Object.prototype;
|
||||
|
||||
export function is(val: unknown, type: string) {
|
||||
return toString.call(val) === `[object ${type}]`;
|
||||
}
|
||||
|
||||
export function isDef<T = unknown>(val?: T): val is T {
|
||||
return typeof val !== 'undefined';
|
||||
}
|
||||
|
||||
export function isUnDef<T = unknown>(val?: T): val is T {
|
||||
return !isDef(val);
|
||||
}
|
||||
|
||||
export function isObject(val: any): val is Record<any, any> {
|
||||
return val !== null && is(val, 'Object');
|
||||
}
|
||||
|
||||
export function isEmpty<T = unknown>(val: T): val is T {
|
||||
if (isArray(val) || isString(val)) {
|
||||
return val.length === 0;
|
||||
}
|
||||
|
||||
if (val instanceof Map || val instanceof Set) {
|
||||
return val.size === 0;
|
||||
}
|
||||
|
||||
if (isObject(val)) {
|
||||
return Object.keys(val).length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isDate(val: unknown): val is Date {
|
||||
return is(val, 'Date');
|
||||
}
|
||||
|
||||
export function isNull(val: unknown): val is null {
|
||||
return val === null;
|
||||
}
|
||||
|
||||
export function isNullAndUnDef(val: unknown): val is null | undefined {
|
||||
return isUnDef(val) && isNull(val);
|
||||
}
|
||||
|
||||
export function isNullOrUnDef(val: unknown): val is null | undefined {
|
||||
return isUnDef(val) || isNull(val);
|
||||
}
|
||||
|
||||
export function isNumber(val: unknown): val is number {
|
||||
return is(val, 'Number');
|
||||
}
|
||||
|
||||
export function isPromise<T = any>(val: unknown): val is Promise<T> {
|
||||
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch);
|
||||
}
|
||||
|
||||
export function isString(val: unknown): val is string {
|
||||
return is(val, 'String');
|
||||
}
|
||||
|
||||
export function isFunction(val: unknown): val is Function {
|
||||
return typeof val === 'function';
|
||||
}
|
||||
|
||||
export function isBoolean(val: unknown): val is boolean {
|
||||
return is(val, 'Boolean');
|
||||
}
|
||||
|
||||
export function isRegExp(val: unknown): val is RegExp {
|
||||
return is(val, 'RegExp');
|
||||
}
|
||||
|
||||
export function isArray(val: any): val is Array<any> {
|
||||
return val && Array.isArray(val);
|
||||
}
|
||||
|
||||
export function isWindow(val: any): val is Window {
|
||||
return typeof window !== 'undefined' && is(val, 'Window');
|
||||
}
|
||||
|
||||
export function isElement(val: unknown): val is Element {
|
||||
return isObject(val) && !!val.tagName;
|
||||
}
|
||||
|
||||
export function isMap(val: unknown): val is Map<any, any> {
|
||||
return is(val, 'Map');
|
||||
}
|
||||
|
||||
export const isServer = typeof window === 'undefined';
|
||||
|
||||
export const isClient = !isServer;
|
||||
|
||||
export function isUrl(path: string): boolean {
|
||||
const reg
|
||||
// eslint-disable-next-line regexp/no-super-linear-backtracking
|
||||
= /^(?:https|http|ftp|rtsp|mms):\/\/(?:(?:[\w!~*'().&=+$%-]+: )?[\w!~*'().&=+$%-]+@)?(?:(?:\d{1,3}.){3}\d{1,3}|(?:[\w!~*'()-]+.)*(?:[0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z].[a-zA-Z]{2,6})(?::\d{1,4})?(?:\/?|(?:\/[\w!~*'().;?:@&=+$,%#-]+)+\/?)$/;
|
||||
return reg.test(path);
|
||||
}
|
||||
11
src/utils/log.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { getEnvValue } from '@/utils/env';
|
||||
|
||||
const projectName = getEnvValue<string>('VITE_APP_TITLE');
|
||||
|
||||
export function warn(message: string) {
|
||||
console.warn(`[${projectName} warn]:${message}`);
|
||||
}
|
||||
|
||||
export function error(message: string) {
|
||||
throw new Error(`[${projectName} error]:${message}`);
|
||||
}
|
||||
17
src/utils/platform.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @description 获取当前平台
|
||||
*/
|
||||
|
||||
export const platform = PLATFORM;
|
||||
|
||||
export function isH5() {
|
||||
return platform === 'h5';
|
||||
}
|
||||
|
||||
export function isApp() {
|
||||
return platform === 'app';
|
||||
}
|
||||
|
||||
export function isMp() {
|
||||
return platform.startsWith('mp-');
|
||||
}
|
||||
53
src/utils/uniapi/index.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @description 设置系统剪贴板的内容
|
||||
* @param data 需要设置的内容
|
||||
* @param showToast 配置是否弹出提示,默认弹出提示
|
||||
* @constructor
|
||||
*/
|
||||
export function SetClipboardData(data: string, showToast = true) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.setClipboardData({
|
||||
data,
|
||||
showToast,
|
||||
success: (res) => {
|
||||
resolve(res);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取系统剪贴板内容
|
||||
* @constructor
|
||||
*/
|
||||
export function GetClipboardData() {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getClipboardData({
|
||||
success: (res) => {
|
||||
resolve(res);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* rpx 换算为 px
|
||||
* @param upx
|
||||
*/
|
||||
export function rpx2px(upx: number) {
|
||||
return uni.upx2px(upx);
|
||||
}
|
||||
|
||||
/**
|
||||
* px 换算为 rpx
|
||||
* @param px
|
||||
*/
|
||||
export function px2rpx(px: number) {
|
||||
return px / (uni.upx2px(100) / 100);
|
||||
}
|
||||
86
src/utils/uniapi/prompt.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 交互反馈
|
||||
* https://uniapp.dcloud.io/api/ui/prompt.html
|
||||
*/
|
||||
|
||||
/**
|
||||
* 显示消息提示框
|
||||
* @param title
|
||||
* @param options
|
||||
* @constructor
|
||||
*/
|
||||
export function Toast(title: string, options?: Partial<UniApp.ShowToastOptions>) {
|
||||
uni.showToast({
|
||||
title,
|
||||
duration: 1500,
|
||||
icon: 'none',
|
||||
mask: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏消息提示框
|
||||
*/
|
||||
export function HideToast() {
|
||||
uni.hideToast();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 loading 提示框
|
||||
* @param title
|
||||
* @param options
|
||||
* @constructor
|
||||
*/
|
||||
export function Loading(title: string, options?: Partial<UniApp.ShowLoadingOptions>) {
|
||||
uni.showLoading({
|
||||
title,
|
||||
mask: true,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏 loading 提示框
|
||||
*/
|
||||
export function HideLoading() {
|
||||
uni.hideLoading();
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示模态弹窗,可以只有一个确定按钮,也可以同时有确定和取消按钮
|
||||
* @param options
|
||||
* @constructor
|
||||
*/
|
||||
export function Modal(options: UniApp.ShowModalOptions) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
...options,
|
||||
success: (res) => {
|
||||
resolve(res);
|
||||
},
|
||||
fail: (res) => {
|
||||
reject(res);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从底部向上弹出操作菜单
|
||||
* @param options
|
||||
* @constructor
|
||||
*/
|
||||
export function ActionSheet(options: UniApp.ShowActionSheetOptions) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.showActionSheet({
|
||||
...options,
|
||||
success: (res) => {
|
||||
resolve(res);
|
||||
},
|
||||
fail: (res) => {
|
||||
reject(res);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
59
tsconfig.json
Normal file
@ -0,0 +1,59 @@
|
||||
{ // 指定要从编译中排除的文件列表
|
||||
"compilerOptions": { // 为所有发出的文件指定一个输出文件夹
|
||||
"target": "ESNext", // 允许export=导出,由import from 导入
|
||||
"lib": ["ESNext", "DOM", "ScriptHost"], // 是否启用对装饰器的实验性支持,装饰器是一种语言特性,还没有完全被 JavaScript 规范批准
|
||||
"emitDecoratorMetadata": true, // 是否通过" . “(obj.key) 语法访问字段和"索引”( obj[“key”]), 以及在类型中声明属性的方式之间的一致性
|
||||
|
||||
/* 实验选项 */
|
||||
"experimentalDecorators": true,
|
||||
"baseUrl": "./", // 指定基目录以解析非相对模块名
|
||||
"rootDir": "./src", // 编译出目标语言版本
|
||||
"module": "ESNext", // 生成代码的模板标准
|
||||
"moduleResolution": "node", // 删除注释
|
||||
"paths": { "@/*": ["./src/*"] }, // 有错误时不进行编译
|
||||
"resolveJsonModule": true, // TS需要引用的库
|
||||
"types": ["@types/node"], // 在源文件中指定根文件夹
|
||||
/* 严格检查选项 */
|
||||
"strict": true, // 设置发出文件的换行符
|
||||
// "sourceMap": true, //为发出的JavaScript文件创建源映射文件。
|
||||
// "declaration": true, // 从项目中的TypeScript和JavaScript文件生成.d.ts文件
|
||||
// "declarationMap": true, // 为d.ts文件创建源地图
|
||||
|
||||
"strictBindCallApply": true, // 检查bind、call和apply方法的参数是否与原始函数匹配。
|
||||
"strictFunctionTypes": true, // 不允许this有隐式的any类型
|
||||
"strictNullChecks": true, // 在给函数赋值时,要确保参数和返回值是子类型兼容的。
|
||||
"strictPropertyInitialization": true, // 开启所有严格的类型检查
|
||||
"alwaysStrict": true, // 是否检查子类继承自基类时,其重载的函数命名与基类的函数不同步问题
|
||||
"noFallthroughCasesInSwitch": true, // 在代码中注入'use strict'
|
||||
"noImplicitAny": true, // 检查函数是否不含有隐式返回值
|
||||
"noImplicitOverride": true, // 是否检查未使用的参数
|
||||
"noImplicitReturns": true, // 不允许隐式的any类型
|
||||
"noImplicitThis": true, // 是否通过索引签名来描述对象上有未知键但已知值的对象
|
||||
"noPropertyAccessFromIndexSignature": false, // 检查switch中是否含有case没有使用break跳出
|
||||
"noUncheckedIndexedAccess": true, // 类的实例属性必须初始化
|
||||
|
||||
/* 额外检查 */
|
||||
"noUnusedLocals": true, // 是否检查未使用的局部变量
|
||||
"noUnusedParameters": true, // 在输出中启用颜色和格式,使编译器错误更容易阅读
|
||||
"newLine": "crlf", // 是否查看 TS 在编译时花费的时间
|
||||
"noEmitOnError": true, // 在进行类型检查时,请考虑null和undefined。
|
||||
"outDir": "./dist", // 指定要包含的类型包名,而不需要在源文件中引用
|
||||
"removeComments": true, // 指定TypeScript如何从给定的模块说明符查找文件
|
||||
"esModuleInterop": true, // 指定一组条目,它们将导入重新映射到其他查找位置
|
||||
"pretty": true, // 为装饰器启用对发出类型元数据的实验性支持
|
||||
|
||||
/* 高级选项 */
|
||||
"forceConsistentCasingInFileNames": true, // 是否区分文件系统大小写规则
|
||||
"extendedDiagnostics": false // 是否解析 JSON 模块
|
||||
// "incremental": true // 增量编译
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"vite.config.*",
|
||||
"typings/*.d.ts"
|
||||
], // 指定要包含在编译中的文件匹配列表
|
||||
"exclude": []
|
||||
}
|
||||
73
unocss.config.js
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* unocss defineConfig
|
||||
* @link unocss: https://github.com/unocss/unocss
|
||||
* @type {import('unocss').UserConfig}
|
||||
*/
|
||||
|
||||
import { defineConfig, presetIcons } from 'unocss';
|
||||
import presetWeapp from 'unocss-preset-weapp';
|
||||
import { transformerAttributify, transformerClass } from 'unocss-preset-weapp/transformer';
|
||||
|
||||
const transformRules = {
|
||||
'.': '-d2e-',
|
||||
'/': '-s2f-',
|
||||
':': '-c3a-',
|
||||
'%': '-p25-',
|
||||
'!': '-e21-',
|
||||
'#': '-w23-',
|
||||
'(': '-b28-',
|
||||
')': '-b29-',
|
||||
'[': '-f4b-',
|
||||
']': '-f5d-',
|
||||
'$': '-r24-',
|
||||
',': '-r2c-',
|
||||
};
|
||||
|
||||
const prefix = '';
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
// https://github.com/MellowCo/unocss-preset-weapp
|
||||
presetWeapp({
|
||||
nonValuedAttribute: true,
|
||||
prefix,
|
||||
whRpx: true,
|
||||
transform: true,
|
||||
platform: 'uniapp',
|
||||
transformRules,
|
||||
}),
|
||||
presetIcons({
|
||||
scale: 1.2,
|
||||
warn: true,
|
||||
}),
|
||||
],
|
||||
shortcuts: [
|
||||
{
|
||||
center: 'flex justify-center items-center',
|
||||
},
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
primary: '#007AFF',
|
||||
secondary: '#4CD964',
|
||||
danger: '#FF3B30',
|
||||
warning: '#FF9500',
|
||||
info: '#5AC8FA',
|
||||
light: '#F0F0F0',
|
||||
dark: '#1A1A1A',
|
||||
},
|
||||
fontSize: {
|
||||
mini: ['20rpx', '26rpx'],
|
||||
},
|
||||
},
|
||||
transformers: [
|
||||
transformerAttributify({
|
||||
classPrefix: prefix,
|
||||
transformRules,
|
||||
nonValuedAttribute: true,
|
||||
}),
|
||||
transformerClass({
|
||||
transformRules,
|
||||
}),
|
||||
],
|
||||
});
|
||||
128
vite.config.ts
Normal file
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* vite 配置
|
||||
* @see https://cn.vitejs.dev/config/
|
||||
* @type {import('vite').UserConfig}
|
||||
*/
|
||||
import { resolve } from 'node:path';
|
||||
import process from 'node:process';
|
||||
import type { UserConfig } from 'vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import TransformPages from 'uni-read-pages-vite';
|
||||
import uni from '@dcloudio/vite-plugin-uni';
|
||||
import UnoCSS from 'unocss/vite';
|
||||
import transformClass from 'unplugin-transform-class/vite';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import ViteRestart from 'vite-plugin-restart';
|
||||
import AutoImport from 'unplugin-auto-import/vite';
|
||||
|
||||
export default defineConfig(async ({ mode }) => {
|
||||
const root = process.cwd();
|
||||
const env = loadEnv(mode, resolve(root, 'env'));
|
||||
const isProd = mode === 'production';
|
||||
const { UNI_PLATFORM } = process.env;
|
||||
const isH5 = UNI_PLATFORM === 'h5';
|
||||
const { VITE_PROXY_PREFIX, VITE_UPLOAD_PROXY_PREFIX, VITE_BASE_URL, VITE_UPLOAD_URL, VITE_PORT } = env;
|
||||
|
||||
return {
|
||||
base: './',
|
||||
envDir: './env', // 自定义env目录
|
||||
// 设置路径别名
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve('./src'),
|
||||
},
|
||||
extensions: ['.js', '.ts'], // 使用路径别名时想要省略的后缀名,可以自己 增减
|
||||
},
|
||||
// 自定义全局变量
|
||||
define: {
|
||||
'process.env': {},
|
||||
'PLATFORM': JSON.stringify(UNI_PLATFORM),
|
||||
'ROUTES': new TransformPages().routes,
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@import "./src/uni.scss";',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
// @ts-expect-error TODO uni() 会报错:uni is not a function,暂时使用此方式解决
|
||||
uni?.default(),
|
||||
AutoImport({
|
||||
include: [
|
||||
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
|
||||
/\.vue$/,
|
||||
/\.vue\?vue/, // .vue
|
||||
],
|
||||
imports: [
|
||||
'vue',
|
||||
'uni-app',
|
||||
'pinia',
|
||||
{
|
||||
'uni-mini-router': ['useRouter', 'useRoute'],
|
||||
},
|
||||
{
|
||||
alova: ['useRequest'],
|
||||
},
|
||||
],
|
||||
dts: 'typings/auto-imports.d.ts',
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
},
|
||||
}),
|
||||
UnoCSS(),
|
||||
transformClass(),
|
||||
ViteRestart({
|
||||
restart: ['vite.config.ts'],
|
||||
}),
|
||||
isH5 && isProd
|
||||
&& visualizer({
|
||||
filename: './node_modules/.cache/visualizer/stats.html',
|
||||
open: true,
|
||||
gzipSize: true,
|
||||
brotliSize: true,
|
||||
}),
|
||||
],
|
||||
// 开发服务器配置
|
||||
server: {
|
||||
host: true,
|
||||
// open: true,
|
||||
port: Number.parseInt(VITE_PORT!, 10),
|
||||
proxy: {
|
||||
[VITE_PROXY_PREFIX!]: {
|
||||
target: VITE_BASE_URL,
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(new RegExp(`^${VITE_PROXY_PREFIX}`), ''),
|
||||
},
|
||||
[VITE_UPLOAD_PROXY_PREFIX!]: {
|
||||
target: VITE_UPLOAD_URL,
|
||||
changeOrigin: true,
|
||||
rewrite: (path: string) => path.replace(new RegExp(`^${VITE_UPLOAD_PROXY_PREFIX}`), ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
// 构建配置
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
chunkSizeWarningLimit: 1500,
|
||||
sourcemap: !isProd,
|
||||
target: 'es6',
|
||||
minify: isProd ? 'terser' : false,
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: isProd,
|
||||
drop_debugger: true,
|
||||
},
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `assets/[name].${new Date().getTime()}.js`,
|
||||
chunkFileNames: `assets/[name].${new Date().getTime()}.js`,
|
||||
assetFileNames: `assets/[name].${new Date().getTime()}.[ext]`,
|
||||
compact: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as UserConfig;
|
||||
});
|
||||