orgin/master #1

Merged
uniccoo merged 257 commits from orgin/master into master 2024-06-18 15:49:12 +00:00
95 changed files with 2780 additions and 0 deletions

13
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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'
[//]: # ' ![h5](https://api-catch.ranesuangyu.top/images/20220621/364f2b47d91ae5ae82a33d33854e2540.png)'
[//]: # '- 小程序(体验版-需申请体验)'
[//]: #
[//]: # ' ![小程序](http://api-catch.ranesuangyu.top/images/20220621/8d4388315ef5b8630d0c0b3963d1ba6b.jpg)'
## 安装使用
- 安装依赖
```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
View File

@ -0,0 +1,2 @@
# title
VITE_APP_TITLE='Uni-app Vue3 Ts --Vite'

17
env/.env.development vendored Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

25
index.html Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,29 @@
{
"version": "1",
"prompt": "template",
"title": "服务协议和隐私政策",
"message": "  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
"buttonAccept": "同意并接受",
"buttonRefuse": "暂不同意",
// HX 3.4.13system 使webview 使uni-appweb
"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"
}
}
}

View File

@ -0,0 +1,9 @@
<script lang="ts" setup name="AppProvider"></script>
<template>
<view>
<slot />
</view>
</template>
<style lang="scss" scoped></style>

View 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>

View File

@ -0,0 +1,4 @@
export const buttonProps = {
disabled: { type: Boolean, default: false },
click: { type: Function },
};

View 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>

View 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>

View 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
View File

5
src/enums/cacheEnum.ts Normal file
View 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
View 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',
}

View 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,
};
}

View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
import { authMocks } from '@/mock/v1/modules/auth';
export const mockGroupV1 = [authMocks];

View 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
View 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
View 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
View 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
View 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
View 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
View 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)) {
// replacetabbarreplaceAll
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>

View 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>

View File

@ -0,0 +1,7 @@
<script lang="ts" setup></script>
<template>
<view> 页面模板,新建pages,将此页面内容复制粘贴到新建.vue文件 </view>
</template>
<style lang="scss" scoped></style>

View 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>

View 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>

View File

@ -0,0 +1,7 @@
<script lang="ts" setup></script>
<template>
Detail
</template>
<style scoped></style>

44
src/router/guard.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,18 @@
export interface UserInfoModel {
/**
* id
*/
id?: number
/**
*
*/
nickname?: string
/**
*
*/
avatar?: string
/**
*
*/
email?: string
}

View 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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
src/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

1
src/static/svg/404.svg Normal file
View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.9 KiB

1
src/static/svg/weep.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

12
src/stores/app.ts Normal file
View 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
View 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 };

View 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
View File

3
src/types.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
// type.d.ts
declare const ROUTES: [];
declare const PLATFORM: string;

24
src/types/env.d.ts vendored Normal file
View 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
View File

@ -0,0 +1,11 @@
import 'uni-mini-router';
declare module 'uni-mini-router' {
interface Route {
meta?: {
ignoreAuth?: boolean
tabBar?: boolean
}
}
}

View File

@ -0,0 +1,4 @@
declare module 'unplugin-transform-class/vite' {
const transformClassVitePlugin: any;
export default transformClassVitePlugin;
}

47
src/uni.scss Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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');
}

View 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
View 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
View 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;
}

View File

@ -0,0 +1,7 @@
export function setupInterceptors() {
}
export function removeInterceptor() {
}

100
src/utils/is.ts Normal file
View 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
View 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
View 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
View 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);
}

View 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
View 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, // TypeScriptJavaScript.d.ts
// "declarationMap": true, // d.ts
"strictBindCallApply": true, // bindcallapply
"strictFunctionTypes": true, // thisany
"strictNullChecks": true, //
"strictPropertyInitialization": true, //
"alwaysStrict": true, //
"noFallthroughCasesInSwitch": true, // 'use strict'
"noImplicitAny": true, //
"noImplicitOverride": true, // 使
"noImplicitReturns": true, // any
"noImplicitThis": true, //
"noPropertyAccessFromIndexSignature": false, // switchcase使break
"noUncheckedIndexedAccess": true, //
/* */
"noUnusedLocals": true, // 使
"noUnusedParameters": true, // 使
"newLine": "crlf", // TS
"noEmitOnError": true, // nullundefined
"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
View 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
View 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;
});