diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..932460e --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# 忽略目录 +/dist +/node_modules +.hbuilderx/* +.idea/* + +# 忽略文件 +*.log +**/*.tsbuildinfo +.eslintcache +pnpm-lock.yaml +/.eslintrc-auto-import.json +/typings/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..152f324 --- /dev/null +++ b/.npmrc @@ -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 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ad1ba07 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,12 @@ +////////////////////////////////// +// 向此工作区的用户推荐的扩展列表 // +///////////////////////////////// + +{ + "recommendations": [ + "Vue.volar", // Vue语言支持扩展 + "Vue.vscode-typescript-vue-plugin", // 一个TS服务器插件,使TS服务器知道*.vue文件。 + "esbenp.prettier-vscode", // 代码格式化 + "dbaeumer.vscode-eslint" // 代码质量检查 + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..57425c6 --- /dev/null +++ b/.vscode/settings.json @@ -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" + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..38525e3 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad94989 --- /dev/null +++ b/README.md @@ -0,0 +1,235 @@ +# 🌈 uni-app Vue3 Vite4 pinia2 TypeScript 基础框架 + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ +## 简介 + +- **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 ` + + + + + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..14568e8 --- /dev/null +++ b/package.json @@ -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'" + } +} diff --git a/scripts/verify-commit.ts b/scripts/verify-commit.ts new file mode 100644 index 0000000..96cdb28 --- /dev/null +++ b/scripts/verify-commit.ts @@ -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); +} diff --git a/shims-uni.d.ts b/shims-uni.d.ts new file mode 100644 index 0000000..b1e946a --- /dev/null +++ b/shims-uni.d.ts @@ -0,0 +1,10 @@ +/// +import 'vue'; + +declare module '@vue/runtime-core' { + type Hooks = App.AppInstance & Page.PageInstance; + + interface ComponentCustomOptions extends Hooks { + + } +} diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..735c9d7 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/androidPrivacy.json b/src/androidPrivacy.json new file mode 100644 index 0000000..5ec20fc --- /dev/null +++ b/src/androidPrivacy.json @@ -0,0 +1,29 @@ +{ + "version": "1", + "prompt": "template", + "title": "服务协议和隐私政策", + "message": "  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。
  你可阅读《服务协议》《隐私政策》了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。", + "buttonAccept": "同意并接受", + "buttonRefuse": "暂不同意", + // HX 3.4.13之后版本新增,system 使用系统webview 打开隐私协议链接,默认使用uni-app内置web组件 + "hrefLoader": "default", + "second": { + "title": "确认提示", + "message": "  进入应用前,你需先同意《服务协议》《隐私政策》,否则将退出应用。", + "buttonAccept": "同意并继续", + "buttonRefuse": "退出应用" + }, + "styles": { + "backgroundColor": "#00FF00", + "borderRadius": "5px", + "title": { + "color": "#ff00ff" + }, + "buttonAccept": { + "color": "#ffff00" + }, + "buttonRefuse": { + "color": "#00ffff" + } + } +} diff --git a/src/components/AppProvider/index.vue b/src/components/AppProvider/index.vue new file mode 100644 index 0000000..6b8a64b --- /dev/null +++ b/src/components/AppProvider/index.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/src/components/BasicButton/index.vue b/src/components/BasicButton/index.vue new file mode 100644 index 0000000..4be5b8c --- /dev/null +++ b/src/components/BasicButton/index.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/src/components/BasicButton/prpos.ts b/src/components/BasicButton/prpos.ts new file mode 100644 index 0000000..d9b5d9e --- /dev/null +++ b/src/components/BasicButton/prpos.ts @@ -0,0 +1,4 @@ +export const buttonProps = { + disabled: { type: Boolean, default: false }, + click: { type: Function }, +}; diff --git a/src/components/BasicInput/index.vue b/src/components/BasicInput/index.vue new file mode 100644 index 0000000..f316c9b --- /dev/null +++ b/src/components/BasicInput/index.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/src/components/Iconify/index.vue b/src/components/Iconify/index.vue new file mode 100644 index 0000000..eec1940 --- /dev/null +++ b/src/components/Iconify/index.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/src/components/Test/index.vue b/src/components/Test/index.vue new file mode 100644 index 0000000..f8fd3b3 --- /dev/null +++ b/src/components/Test/index.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/src/enums/appEnum.ts b/src/enums/appEnum.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/enums/cacheEnum.ts b/src/enums/cacheEnum.ts new file mode 100644 index 0000000..d5e744a --- /dev/null +++ b/src/enums/cacheEnum.ts @@ -0,0 +1,5 @@ +// token key +export const TOKEN_KEY = 'TOKEN__'; + +// user info key +export const USER_INFO_KEY = 'USER__INFO__'; diff --git a/src/enums/httpEnum.ts b/src/enums/httpEnum.ts new file mode 100644 index 0000000..0532e9d --- /dev/null +++ b/src/enums/httpEnum.ts @@ -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', +} diff --git a/src/hooks/app/useGlobalStyle.ts b/src/hooks/app/useGlobalStyle.ts new file mode 100644 index 0000000..f4bb04b --- /dev/null +++ b/src/hooks/app/useGlobalStyle.ts @@ -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, + }; +} diff --git a/src/hooks/app/useSystem.ts b/src/hooks/app/useSystem.ts new file mode 100644 index 0000000..e6df73f --- /dev/null +++ b/src/hooks/app/useSystem.ts @@ -0,0 +1,11 @@ +/** + * 系统信息 + * @description 除去uni官方不推荐使用的返回参数 + * @link https://uniapp.dcloud.net.cn/api/system/info.html + */ +export function useSystem() { + const systemInfo = reactive(uni.getSystemInfoSync()); + return { + systemInfo, + }; +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..c4c1a43 --- /dev/null +++ b/src/main.ts @@ -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, + }; +} diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..cc7cede --- /dev/null +++ b/src/manifest.json @@ -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": [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ], + "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 + } + } + } +} diff --git a/src/mock/index.ts b/src/mock/index.ts new file mode 100644 index 0000000..be0ea68 --- /dev/null +++ b/src/mock/index.ts @@ -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, +}); diff --git a/src/mock/utils.ts b/src/mock/utils.ts new file mode 100644 index 0000000..04c2eaa --- /dev/null +++ b/src/mock/utils.ts @@ -0,0 +1,22 @@ +import { ResultEnum } from '@/enums/httpEnum'; +import type { API } from '@/services/model/baseModel'; + +interface MockResponseOptions { + status: number + statusText: string + responseHeaders: Record + body: API +} + +export function createMock(options: Partial): MockResponseOptions { + return { + status: 200, + statusText: 'OK', + responseHeaders: {}, + body: { + code: ResultEnum.SUCCESS, + message: 'succeed', + ...options, + }, + }; +} diff --git a/src/mock/v1/index.ts b/src/mock/v1/index.ts new file mode 100644 index 0000000..97f528e --- /dev/null +++ b/src/mock/v1/index.ts @@ -0,0 +1,3 @@ +import { authMocks } from '@/mock/v1/modules/auth'; + +export const mockGroupV1 = [authMocks]; diff --git a/src/mock/v1/modules/auth.ts b/src/mock/v1/modules/auth.ts new file mode 100644 index 0000000..26e5ca5 --- /dev/null +++ b/src/mock/v1/modules/auth.ts @@ -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', + }, + }); + }, +}); diff --git a/src/pages.json b/src/pages.json new file mode 100644 index 0000000..ccb88e3 --- /dev/null +++ b/src/pages.json @@ -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" + } + ] + } +} diff --git a/src/pages/about/index.vue b/src/pages/about/index.vue new file mode 100644 index 0000000..dedd434 --- /dev/null +++ b/src/pages/about/index.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/pages/demo/index.vue b/src/pages/demo/index.vue new file mode 100644 index 0000000..c64a43d --- /dev/null +++ b/src/pages/demo/index.vue @@ -0,0 +1,12 @@ + + + + + diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue new file mode 100644 index 0000000..9d46c09 --- /dev/null +++ b/src/pages/index/index.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/src/pages/log/index.vue b/src/pages/log/index.vue new file mode 100644 index 0000000..34eff32 --- /dev/null +++ b/src/pages/log/index.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue new file mode 100644 index 0000000..54e48f8 --- /dev/null +++ b/src/pages/login/index.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/src/pages/notFound/404.vue b/src/pages/notFound/404.vue new file mode 100644 index 0000000..b20a7b2 --- /dev/null +++ b/src/pages/notFound/404.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/src/pages/template/index.vue b/src/pages/template/index.vue new file mode 100644 index 0000000..61e6091 --- /dev/null +++ b/src/pages/template/index.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/src/pagesA/list/test1/index.vue b/src/pagesA/list/test1/index.vue new file mode 100644 index 0000000..a5bb892 --- /dev/null +++ b/src/pagesA/list/test1/index.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/src/pagesA/list/test2/index.vue b/src/pagesA/list/test2/index.vue new file mode 100644 index 0000000..74e08c5 --- /dev/null +++ b/src/pagesA/list/test2/index.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/src/pagesB/detail/index.vue b/src/pagesB/detail/index.vue new file mode 100644 index 0000000..bb3d124 --- /dev/null +++ b/src/pagesB/detail/index.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/src/router/guard.ts b/src/router/guard.ts new file mode 100644 index 0000000..fc82141 --- /dev/null +++ b/src/router/guard.ts @@ -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); + }); +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..55a004a --- /dev/null +++ b/src/router/index.ts @@ -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) { + // Configure router guard + createRouterGuard(router); + app.use(router); +} + +export { router }; diff --git a/src/services/api/auth.ts b/src/services/api/auth.ts new file mode 100644 index 0000000..3de0828 --- /dev/null +++ b/src/services/api/auth.ts @@ -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(LOGIN, params, { + meta: { + ignoreAuth: true, + }, + }); +} + +/** + * 登出 + */ +export function logout() { + return request.Post(LOGIN_OUT, {}); +} + +/** + * 刷新token + */ +export function refreshToken() { + return request.Post(REFRESH_TOKEN, {}); +} diff --git a/src/services/api/user.ts b/src/services/api/user.ts new file mode 100644 index 0000000..c71ea43 --- /dev/null +++ b/src/services/api/user.ts @@ -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(Api.GET_USER_INFO); +} diff --git a/src/services/model/authModel.d.ts b/src/services/model/authModel.d.ts new file mode 100644 index 0000000..3ec2fc2 --- /dev/null +++ b/src/services/model/authModel.d.ts @@ -0,0 +1,7 @@ +declare interface LoginParams { + email: string + password: string +} +declare interface LoginModel { + token: string +} diff --git a/src/services/model/baseModel.d.ts b/src/services/model/baseModel.d.ts new file mode 100644 index 0000000..5e70f0c --- /dev/null +++ b/src/services/model/baseModel.d.ts @@ -0,0 +1,7 @@ +import type { ResultEnum } from '@/enums/httpEnum'; + +declare interface API { + code: ResultEnum + data?: T + message: string +} diff --git a/src/services/model/userModel.d.ts b/src/services/model/userModel.d.ts new file mode 100644 index 0000000..50956d1 --- /dev/null +++ b/src/services/model/userModel.d.ts @@ -0,0 +1,18 @@ +export interface UserInfoModel { + /** + * 用户id + */ + id?: number + /** + * 昵称 + */ + nickname?: string + /** + * 头像 + */ + avatar?: string + /** + * 邮箱 + */ + email?: string +} diff --git a/src/settings/encryptionSetting.ts b/src/settings/encryptionSetting.ts new file mode 100644 index 0000000..66c6fbb --- /dev/null +++ b/src/settings/encryptionSetting.ts @@ -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('VITE_APP_CACHE_PREFIX') || getEnvValue('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(); diff --git a/src/static/images/avatar.png b/src/static/images/avatar.png new file mode 100644 index 0000000..c46906d Binary files /dev/null and b/src/static/images/avatar.png differ diff --git a/src/static/images/h5Qcode.png b/src/static/images/h5Qcode.png new file mode 100644 index 0000000..8b140b8 Binary files /dev/null and b/src/static/images/h5Qcode.png differ diff --git a/src/static/images/tabBar/about.png b/src/static/images/tabBar/about.png new file mode 100644 index 0000000..1d9e2e8 Binary files /dev/null and b/src/static/images/tabBar/about.png differ diff --git a/src/static/images/tabBar/category.png b/src/static/images/tabBar/category.png new file mode 100644 index 0000000..ed2d66b Binary files /dev/null and b/src/static/images/tabBar/category.png differ diff --git a/src/static/images/tabBar/demo.png b/src/static/images/tabBar/demo.png new file mode 100644 index 0000000..b7a1d39 Binary files /dev/null and b/src/static/images/tabBar/demo.png differ diff --git a/src/static/images/tabBar/home.png b/src/static/images/tabBar/home.png new file mode 100644 index 0000000..ec7d0c7 Binary files /dev/null and b/src/static/images/tabBar/home.png differ diff --git a/src/static/images/tabBar/selectedAbout.png b/src/static/images/tabBar/selectedAbout.png new file mode 100644 index 0000000..3566103 Binary files /dev/null and b/src/static/images/tabBar/selectedAbout.png differ diff --git a/src/static/images/tabBar/selectedCategory.png b/src/static/images/tabBar/selectedCategory.png new file mode 100644 index 0000000..c42ad6c Binary files /dev/null and b/src/static/images/tabBar/selectedCategory.png differ diff --git a/src/static/images/tabBar/selectedDemo.png b/src/static/images/tabBar/selectedDemo.png new file mode 100644 index 0000000..1773840 Binary files /dev/null and b/src/static/images/tabBar/selectedDemo.png differ diff --git a/src/static/images/tabBar/selectedHome.png b/src/static/images/tabBar/selectedHome.png new file mode 100644 index 0000000..969aae2 Binary files /dev/null and b/src/static/images/tabBar/selectedHome.png differ diff --git a/src/static/images/wexinQcode.jpg b/src/static/images/wexinQcode.jpg new file mode 100644 index 0000000..4803a95 Binary files /dev/null and b/src/static/images/wexinQcode.jpg differ diff --git a/src/static/logo.png b/src/static/logo.png new file mode 100644 index 0000000..b5771e2 Binary files /dev/null and b/src/static/logo.png differ diff --git a/src/static/svg/404.svg b/src/static/svg/404.svg new file mode 100644 index 0000000..212f549 --- /dev/null +++ b/src/static/svg/404.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/svg/LOGO.svg b/src/static/svg/LOGO.svg new file mode 100644 index 0000000..6e8bed6 --- /dev/null +++ b/src/static/svg/LOGO.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/svg/weep.svg b/src/static/svg/weep.svg new file mode 100644 index 0000000..0b7fd49 --- /dev/null +++ b/src/static/svg/weep.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/stores/app.ts b/src/stores/app.ts new file mode 100644 index 0000000..87a6977 --- /dev/null +++ b/src/stores/app.ts @@ -0,0 +1,12 @@ +import { defineStore } from 'pinia'; + +interface AppState { + sys?: string | number +} + +export const useAppStore = defineStore({ + id: 'app-store', + state: (): AppState => ({}), + getters: {}, + actions: {}, +}); diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..efaf6c9 --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1,10 @@ +import type { App } from 'vue'; +import { createPinia } from 'pinia'; + +const store = createPinia(); + +export function setupStore(app: App) { + app.use(store); +} + +export { store }; diff --git a/src/stores/modules/user.ts b/src/stores/modules/user.ts new file mode 100644 index 0000000..cf78510 --- /dev/null +++ b/src/stores/modules/user.ts @@ -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(null); + const userInfo = ref(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, + }; +}); diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000..e69de29 diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..c8458b7 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,3 @@ +// type.d.ts +declare const ROUTES: []; +declare const PLATFORM: string; diff --git a/src/types/env.d.ts b/src/types/env.d.ts new file mode 100644 index 0000000..de89413 --- /dev/null +++ b/src/types/env.d.ts @@ -0,0 +1,24 @@ +// / + +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + + const component: DefineComponent, NonNullable, 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 +} diff --git a/src/types/uni-mini-router.d.ts b/src/types/uni-mini-router.d.ts new file mode 100644 index 0000000..ce72f87 --- /dev/null +++ b/src/types/uni-mini-router.d.ts @@ -0,0 +1,11 @@ +import 'uni-mini-router'; + +declare module 'uni-mini-router' { + + interface Route { + meta?: { + ignoreAuth?: boolean + tabBar?: boolean + } + } +} diff --git a/src/types/unplugin-transform-class-vite.d.ts b/src/types/unplugin-transform-class-vite.d.ts new file mode 100644 index 0000000..28d3383 --- /dev/null +++ b/src/types/unplugin-transform-class-vite.d.ts @@ -0,0 +1,4 @@ +declare module 'unplugin-transform-class/vite' { + const transformClassVitePlugin: any; + export default transformClassVitePlugin; +} diff --git a/src/uni.scss b/src/uni.scss new file mode 100644 index 0000000..ddcd3f8 --- /dev/null +++ b/src/uni.scss @@ -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; +} diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 0000000..552f2a0 --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,26 @@ +import { getCache, setCache } from '@/utils/cache'; +import { TOKEN_KEY } from '@/enums/cacheEnum'; + +const authenticationScheme = 'Bearer'; + +export function getToken() { + return getCache(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(); +} diff --git a/src/utils/cache/index.ts b/src/utils/cache/index.ts new file mode 100644 index 0000000..6b23b3d --- /dev/null +++ b/src/utils/cache/index.ts @@ -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 = { + 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(key: string): T { + return storage.get(key); +} + +export function removeCache(key: string): void { + return storage.remove(key); +} + +export function clearCache(): void { + return storage.clear(); +} diff --git a/src/utils/cache/storageCache.ts b/src/utils/cache/storageCache.ts new file mode 100644 index 0000000..4515208 --- /dev/null +++ b/src/utils/cache/storageCache.ts @@ -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 = {}) { + 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(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(); +} diff --git a/src/utils/character.ts b/src/utils/character.ts new file mode 100644 index 0000000..70a4d82 --- /dev/null +++ b/src/utils/character.ts @@ -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, + )}`; +} diff --git a/src/utils/cipher.ts b/src/utils/cipher.ts new file mode 100644 index 0000000..3ba8f44 --- /dev/null +++ b/src/utils/cipher.ts @@ -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 = {}) { + 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(); +} diff --git a/src/utils/env.ts b/src/utils/env.ts new file mode 100644 index 0000000..792d798 --- /dev/null +++ b/src/utils/env.ts @@ -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(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('VITE_PROXY_PREFIX') : getEnvValue('VITE_BASE_URL'); +} + +/** + * @description: Get environment VITE_UPLOAD_URL value + * @returns: + * @example: + */ +export function getUploadUrl(): string { + return (isH5() && isDevMode()) ? getEnvValue('VITE_UPLOAD_PROXY_PREFIX') : getEnvValue('VITE_UPLOAD_URL'); +} diff --git a/src/utils/http/checkStatus.ts b/src/utils/http/checkStatus.ts new file mode 100644 index 0000000..2a0aa65 --- /dev/null +++ b/src/utils/http/checkStatus.ts @@ -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); + } +} diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts new file mode 100644 index 0000000..5ec2d96 --- /dev/null +++ b/src/utils/http/index.ts @@ -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; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..297c424 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,14 @@ +import { isObject } from '@/utils/is'; + +/** + * 深度合并 + * @param src + * @param target + */ +export function deepMerge(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; +} diff --git a/src/utils/interceptors/index.ts b/src/utils/interceptors/index.ts new file mode 100644 index 0000000..1f3d627 --- /dev/null +++ b/src/utils/interceptors/index.ts @@ -0,0 +1,7 @@ +export function setupInterceptors() { + +} + +export function removeInterceptor() { + +} diff --git a/src/utils/is.ts b/src/utils/is.ts new file mode 100644 index 0000000..824494c --- /dev/null +++ b/src/utils/is.ts @@ -0,0 +1,100 @@ +const { toString } = Object.prototype; + +export function is(val: unknown, type: string) { + return toString.call(val) === `[object ${type}]`; +} + +export function isDef(val?: T): val is T { + return typeof val !== 'undefined'; +} + +export function isUnDef(val?: T): val is T { + return !isDef(val); +} + +export function isObject(val: any): val is Record { + return val !== null && is(val, 'Object'); +} + +export function isEmpty(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(val: unknown): val is Promise { + 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 { + 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 { + 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); +} diff --git a/src/utils/log.ts b/src/utils/log.ts new file mode 100644 index 0000000..944d9b0 --- /dev/null +++ b/src/utils/log.ts @@ -0,0 +1,11 @@ +import { getEnvValue } from '@/utils/env'; + +const projectName = getEnvValue('VITE_APP_TITLE'); + +export function warn(message: string) { + console.warn(`[${projectName} warn]:${message}`); +} + +export function error(message: string) { + throw new Error(`[${projectName} error]:${message}`); +} diff --git a/src/utils/platform.ts b/src/utils/platform.ts new file mode 100644 index 0000000..d29584e --- /dev/null +++ b/src/utils/platform.ts @@ -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-'); +} diff --git a/src/utils/uniapi/index.ts b/src/utils/uniapi/index.ts new file mode 100644 index 0000000..9983155 --- /dev/null +++ b/src/utils/uniapi/index.ts @@ -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); +} diff --git a/src/utils/uniapi/prompt.ts b/src/utils/uniapi/prompt.ts new file mode 100644 index 0000000..fadbaff --- /dev/null +++ b/src/utils/uniapi/prompt.ts @@ -0,0 +1,86 @@ +/** + * 交互反馈 + * https://uniapp.dcloud.io/api/ui/prompt.html + */ + +/** + * 显示消息提示框 + * @param title + * @param options + * @constructor + */ +export function Toast(title: string, options?: Partial) { + 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) { + 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); + }, + }); + }); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2ae6126 --- /dev/null +++ b/tsconfig.json @@ -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": [] +} diff --git a/unocss.config.js b/unocss.config.js new file mode 100644 index 0000000..f3c0fee --- /dev/null +++ b/unocss.config.js @@ -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, + }), + ], +}); diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..9ca06fe --- /dev/null +++ b/vite.config.ts @@ -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; +});