├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── README.zh-CN.md ├── build ├── config │ ├── index.ts │ ├── proxy.ts │ └── utils.ts └── plugins │ ├── compress.ts │ ├── i18nPlugin.ts │ ├── index.ts │ └── svgPlugin.ts ├── commitlint.config.ts ├── eslint.config.js ├── index.html ├── mock ├── modules │ └── user.mock.ts └── util.ts ├── package.json ├── pnpm-lock.yaml ├── public ├── logo.png └── logo.svg ├── src ├── App.vue ├── api │ └── System │ │ └── user.ts ├── assets │ ├── icons │ │ ├── Moon.svg │ │ ├── Sunny.svg │ │ └── logo.svg │ └── images │ │ ├── 404.png │ │ └── logo.png ├── components │ ├── IconifyIcon │ │ └── index.vue │ ├── SvgIcon │ │ └── index.vue │ └── SwitchDark │ │ └── index.vue ├── config │ └── index.ts ├── hooks │ └── useECharts.ts ├── layout │ └── index.vue ├── locales │ ├── index.ts │ └── modules │ │ ├── en-US.json │ │ └── zh-CN.json ├── main.ts ├── plugins │ ├── ECharts.ts │ └── SvgIcons.ts ├── router │ ├── guards.ts │ ├── index.ts │ └── util.ts ├── store │ ├── index.ts │ └── modules │ │ ├── route.ts │ │ ├── setting.ts │ │ └── user.ts ├── styles │ ├── index.scss │ ├── theme.scss │ ├── transition.scss │ └── variables.scss ├── types │ ├── global.d.ts │ └── vite-env.d.ts ├── utils │ ├── color.ts │ ├── is.ts │ ├── request │ │ ├── CheckStatus.ts │ │ ├── index.ts │ │ └── request.ts │ └── validate.ts └── views │ ├── ErrorPages │ └── 404.vue │ ├── Example │ ├── echartsDemo.vue │ ├── iconDemo.vue │ ├── index.vue │ ├── keepAliveDemo.vue │ └── mockDemo.vue │ ├── Home │ └── index.vue │ ├── Login │ ├── components │ │ └── PasswordInput.vue │ ├── forgotPassword.vue │ ├── index.vue │ └── register.vue │ ├── Mine │ └── index.vue │ └── ThemeSetting │ └── index.vue ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── uno.config.ts ├── vercel.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | # 表示是最顶层的 EditorConfig 配置文件 4 | root = true 5 | 6 | [*] # 表示所有文件适用 7 | charset = utf-8 # 设置文件字符集为 utf-8 8 | indent_style = space # 缩进风格(tab | space) 9 | indent_size = 2 # 缩进大小 10 | end_of_line = lf # 控制换行类型(lf | cr | crlf) 11 | trim_trailing_whitespace = true # 去除行首的任意空白字符 12 | insert_final_newline = true # 始终在文件末尾插入一个新行 13 | 14 | [*.md] # 表示仅 md 文件适用以下规则 15 | max_line_length = off 16 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # port 2 | VITE_PORT = 5173 3 | 4 | # 标题 5 | VITE_APP_TITLE = 'Lemon-Template-Vue' 6 | 7 | # open 运行 npm run dev 时自动打开浏览器 8 | VITE_OPEN = true 9 | 10 | # 是否生成包预览文件 11 | VITE_REPORT = false 12 | 13 | # 是否删除生产环境 console 14 | VITE_DROP_CONSOLE = true 15 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 本地环境 2 | NODE_ENV = 'development' 3 | 4 | # 是否删除console 5 | VITE_DROP_CONSOLE = false 6 | 7 | # 是否开启mock 8 | VITE_USE_MOCK = true 9 | 10 | # 开发环境跨域代理,可以配置多个,请注意不要换行 11 | # VITE_PROXY = [["/api","http://localhost:5173"]] 12 | # VITE_PROXY = [["/appApi","http://localhost:3000"],["/upload","http://localhost:3000/upload"]] 13 | 14 | # 本地环境接口地址 15 | # 如果没有跨域问题,直接在这里配置即可 16 | VITE_APP_BASE_API = '' 17 | 18 | # 图片上传地址 19 | VITE_APP_UPLOAD_URL = 20 | 21 | # 图片前缀地址 22 | VITE_APP_IMG_URL = 23 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 线上环境 2 | NODE_ENV = "production" 3 | 4 | # 是否开启mock 5 | VITE_USE_MOCK = false 6 | 7 | # 线上环境接口地址 8 | VITE_APP_BASE_API = '/api' 9 | 10 | # 是否启用 gzip 或 brotli 压缩打包,如果需要多个压缩规则,可以使用 “,” 分隔 11 | # Optional: gzip | brotli | none 12 | VITE_BUILD_COMPRESS = none 13 | 14 | # 打包压缩后是否删除源文件 15 | VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: lts/* 21 | 22 | - run: npx changelogithub 23 | env: 24 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | !.vscode/settings.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | /src/types/components.d.ts 28 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "sankeyangshu.vscode-javascript-typescript-snippets", 4 | "sankeyangshu.vscode-vue-collection-snippets", 5 | "dbaeumer.vscode-eslint", 6 | "antfu.unocss", 7 | "mikestead.dotenv", 8 | "editorconfig.editorconfig", 9 | "usernamehw.errorlens", 10 | "lokalise.i18n-ally" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit", 4 | "source.organizeImports": "never" 5 | }, 6 | "eslint.useFlatConfig": true, 7 | "editor.formatOnSave": false, 8 | "eslint.validate": [ 9 | "vue", 10 | "html", 11 | "css", 12 | "scss", 13 | "json", 14 | "jsonc", 15 | "yaml", 16 | "yml", 17 | "markdown" 18 | ], 19 | "prettier.enable": false, 20 | 21 | "i18n-ally.displayLanguage": "zh-CN", 22 | "i18n-ally.localesPaths": [ 23 | "src/locales/modules" 24 | ], 25 | "i18n-ally.keystyle": "nested" 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 三棵杨树 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Lemon-Template-Vue 4 | 5 | 6 |

7 | lemon-template-vue 8 |

9 | 10 | English / [简体中文](./README.zh-CN.md) 11 | 12 | A mobile web apps template based on the Vue 3 ecosystem. 13 | 14 |

15 | license 16 | version 17 | languages 18 | repo-size 19 | issues 20 |

21 | 22 | [Docs](https://sankeyangshu.github.io/lemon-template-docs/vue/) / [Feedback](https://github.com/sankeyangshu/lemon-template-vue/issues) 23 | 24 |
25 | 26 | --- 27 | 28 | ## Introduction 29 | 30 | 🚀🚀🚀 **lemon-template-vue** is built using the latest technologies, including `Vue 3.5`, `Vite 6`, `Vant 4`, `Pinia`, `TypeScript`, and `UnoCSS`. It integrates features like `Dark Mode`, system theme colors, and Mock data. 31 | 32 | You can directly start developing your business logic on this template! Hope you enjoy it. 👋👋👋 33 | 34 | > [!NOTE] 35 | > If this project is helpful to you, please click the "Star" button in the top-right corner. Thank you! 36 | 37 | ## Preview 38 | 39 | 👓 [Click Demo](https://lemon-template-vue.vercel.app/) (Switch to mobile view on PC browsers) 40 | 41 | ## Other Templates 42 | 43 | - [lemon-template-react](https://github.com/sankeyangshu/lemon-template-react) - A mobile web apps template based on the React ecosystem 44 | - [lemon-template-uniapp](https://github.com/sankeyangshu/lemon-template-uniapp) - An mobile web apps template based on the UniApp ecosystem 45 | 46 | ## Features 47 | 48 | - ⚡️ Developed with Vue 3.5 + TypeScript using **<script setup>** single-file components 49 | - ✨ Uses Vite 6 as the development and build tool (includes Gzip packaging, TSX syntax, proxy support, etc.) 50 | - 🍕 Fully integrates TypeScript 51 | - 🍍 Replaces Vuex with Pinia, offering lightweight and easy-to-use state management, with Pinia persistence plugin integrated 52 | - 📦 Automatic component loading 53 | - 🎨 Vant 4 component library 54 | - 🌀 UnoCSS for instant atomic CSS 55 | - 👏 Integrates multiple icon solutions 56 | - 🌓 Supports Dark Mode 57 | - 🌍 Multi-language support with i18n 58 | - 🔥 ECharts for data visualization, with useECharts Hooks 59 | - ⚙️ Unit testing using Vitest 60 | - ☁️ Axios encapsulation 61 | - 💾 Local Mock data support 62 | - 📱 Browser compatibility with viewport vw/vh units for layouts 63 | - 📥 Gzip compression for packaged resources 64 | - 🛡️ Splash screen animation for first load 65 | - 💪 Eslint for code linting, with Prettier for formatting 66 | - 🌈 Uses simple-git-hooks, lint-staged, and commitlint for commit message standards 67 | 68 | ## Prerequisites 69 | 70 | Familiarity with the following concepts will help you use this template effectively: 71 | 72 | - [Vue 3](https://v3.vuejs.org/) - Basic syntax of `Vue 3` 73 | - [Vite](https://vitejs.dev/) - Features of `Vite` 74 | - [Pinia](https://pinia.vuejs.org/) - Features of `Pinia` 75 | - [TypeScript](https://www.typescriptlang.org/) - Basic syntax of `TypeScript` 76 | - [Vue Router](https://router.vuejs.org/) - Basic usage of `Vue Router` 77 | - [Icones](https://icones.js.org/) - Recommended icon library for this project 78 | - [UnoCSS](https://github.com/antfu/unocss) - High-performance, flexible atomic CSS engine 79 | - [Vant](https://github.com/youzan/vant) - Mobile Vue component library 80 | - [ECharts 5](https://echarts.apache.org/en/handbook/) - Basic usage of `ECharts` 81 | - [Mock.js](https://github.com/nuysoft/Mock) - Basic syntax of `Mock.js` 82 | - [ES6+](http://es6.ruanyifeng.com/) - Basic syntax of `ES6` 83 | 84 | ## Environment Setup 85 | 86 | Ensure the following tools are installed locally: [Pnpm](https://pnpm.io/), [Node.js](http://nodejs.org/), and [Git](https://git-scm.com/). 87 | 88 | - Use [pnpm >= 8.15.4](https://pnpm.io/) to avoid dependency installation and build errors. 89 | - [Node.js](http://nodejs.org/) version `18.x` or above is required. Recommended: `^18.18.0 || >=20.0.0`. 90 | 91 | ## VSCode Extensions 92 | 93 | If you use [VSCode](https://code.visualstudio.com/) (recommended), install the following extensions for improved efficiency and code formatting: 94 | 95 | - [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - Essential for Vue development 96 | - [UnoCSS](https://marketplace.visualstudio.com/items?itemName=antfu.unocss) - UnoCSS support 97 | - [DotENV](https://marketplace.visualstudio.com/items?itemName=mikestead.dotenv) - `.env` file highlighting 98 | - [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) - Better error visualization 99 | - [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) - Maintain consistent coding styles across IDEs 100 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Script linting 101 | - [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) - All-in-one i18n support 102 | - [JavaScript and TypeScript VSCode Snippets](https://marketplace.visualstudio.com/items?itemName=sankeyangshu.vscode-javascript-typescript-snippets) - JS and TS snippets 103 | - [Vue Collection VSCode Snippets](https://marketplace.visualstudio.com/items?itemName=sankeyangshu.vscode-vue-collection-snippets) - Vue 2/3 snippets 104 | 105 | ## Usage 106 | 107 | ### Use the Scaffold 108 | 109 | > In development 110 | 111 | ### GitHub Template 112 | 113 | [Use this template to create a repository](https://github.com/sankeyangshu/lemon-template-vue/generate) 114 | 115 | ### Clone 116 | 117 | ```bash 118 | # Clone the project 119 | git clone https://github.com/sankeyangshu/lemon-template-vue.git 120 | 121 | # Enter the project directory 122 | cd lemon-template-vue 123 | 124 | # Install dependencies (use pnpm) 125 | pnpm install 126 | 127 | # Start the development server 128 | pnpm dev 129 | 130 | # Build for production 131 | pnpm build 132 | ``` 133 | 134 | ## Git Commit Guidelines 135 | 136 | ### Commit Standards 137 | 138 | The project enforces Git commit messages using `simple-git-hooks` and `commitlint`, adhering to the widely adopted [Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular) guidelines. 139 | 140 | - `feat`: Add new features 141 | - `fix`: Fix bugs 142 | - `docs`: Documentation changes 143 | - `style`: Code formatting (does not affect functionality, e.g., spacing, semicolons, etc.) 144 | - `refactor`: Code refactoring (neither bug fixes nor new features) 145 | - `perf`: Performance optimizations 146 | - `test`: Add or update test cases 147 | - `build`: Changes to build process or external dependencies (e.g., updating npm packages, modifying webpack configuration) 148 | - `ci`: Changes to CI configuration or scripts 149 | - `chore`: Changes to build tools or auxiliary libraries (does not affect source files or tests) 150 | - `revert`: Revert a previous commit 151 | 152 | ## Community 153 | 154 | You can use [issues](https://github.com/sankeyangshu/lemon-template-vue/issues) to report problems or submit a Pull Request. 155 | 156 | ## Browser Support 157 | 158 | - For local development, we recommend using the latest version of Chrome. [Download](https://www.google.com/intl/en/chrome/). 159 | - The production environment supports modern browsers. IE is no longer supported. For more details on browser support, check [Can I Use ES Module](https://caniuse.com/?search=ESModule). 160 | 161 | | [ IE](http://godban.github.io/browsers-support-badges/)
IE | [ Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | 162 | | :----------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 163 | | not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | 164 | 165 | ## License 166 | 167 | [MIT](./LICENSE) License © 2023-PRESENT [sankeyangshu](https://github.com/sankeyangshu) 168 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Lemon-Template-Vue 4 | 5 | 6 |

7 | lemon-template-vue 8 |

9 | 10 | [English](./README.md) / 简体中文 11 | 12 | 一个基于 Vue 3 生态系统的移动 web 应用模板。 13 | 14 |

15 | license 16 | version 17 | languages 18 | repo-size 19 | issues 20 |

21 | 22 | [文档](https://sankeyangshu.github.io/lemon-template-docs/zh/vue/) / [反馈](https://github.com/sankeyangshu/lemon-template-vue/issues) 23 | 24 |
25 | 26 | --- 27 | 28 | ## 简介 29 | 30 | 🚀🚀🚀 **lemon-template-vue** 使用了最新的`Vue3.5`、`Vite6`、`Vant4`、`Pinia`、`Typescript`、`UnoCSS`等主流技术开发,集成 `Dark Mode`(暗黑)模式和系统主题色、Mock数据等功能。 31 | 32 | 你可以在此之上直接开发你的业务代码!希望你能喜欢。👋👋👋 33 | 34 | > [!NOTE] 35 | > 如果对您有帮助,您可以点右上角 "Star" 支持一下 谢谢! 36 | 37 | ## 在线预览 38 | 39 | 👓 [点击这里](https://lemon-template-vue.vercel.app/)(PC浏览器请切换手机端模式) 40 | 41 | ## 其他模版 42 | 43 | - [lemon-template-react](https://github.com/sankeyangshu/lemon-template-react) - 基于 React 生态系统的移动 web 应用模板 44 | - [lemon-template-uniapp](https://github.com/sankeyangshu/lemon-template-uniapp) - 基于 UniApp 生态系统的移动小程序应用模板 45 | 46 | ## 项目功能 47 | 48 | - ⚡️ 使用 Vue3.5 + TypeScript 开发,单文件组件**< script setup >** 49 | - ✨ 采用 Vite6 作为项目开发、打包工具(配置 Gzip 打包、TSX 语法、跨域代理…) 50 | - 🍕 整个项目集成了 TypeScript 51 | - 🍍 使用 Pinia 替代 Vuex,轻量、简单、易用,集成 Pinia 持久化插件 52 | - 📦 组件自动化加载 53 | - 🎨 Vant4 组件库 54 | - 🌀 UnoCSS 即时原子化 CSS 引擎 55 | - 👏 集成多种图标方案 56 | - 🌓 支持深色模式 57 | - 🌍 多语言国际化,支持 i18n国际化方案 58 | - 🔥 集成 Echarts 数据可视化图表,封装 useECharts Hooks 59 | - ⚙️ 使用 Vitest 进行单元测试 60 | - ☁️ Axios 封装 61 | - 💾 本地 Mock 数据模拟的支持 62 | - 📱 浏览器适配 - 使用 viewport vw/vh 单位布局 63 | - 📥 打包资源 gzip 压缩 64 | - 🛡️ 首屏加载动画 65 | - 💪 集成 Eslint 代码校验规范,并且该 Eslint 配置默认使用 Prettier 格式化代码, 66 | - 🌈 使用 simple-git-hooks、lint-staged、commitlint 规范提交信息 67 | 68 | ## 基础知识 69 | 70 | 提前了解和学习这些知识会对使用本项目有很大的帮助。 71 | 72 | - [Vue3](https://v3.vuejs.org/) - 熟悉 `Vue3` 基础语法 73 | - [Vite](https://cn.vitejs.dev/) - 熟悉 `Vite` 特性 74 | - [Pinia](https://pinia.vuejs.org/) - 熟悉 `Pinia` 特性 75 | - [TypeScript](https://www.typescriptlang.org/) - 熟悉 `TypeScript` 基本语法 76 | - [Vue-Router](https://router.vuejs.org/) - 熟悉 `Vue-Router`基本使用 77 | - [Icones](https://icones.js.org/) - 本项目推荐图标库,当然你也可以使用 `IconSVg` 78 | - [UnoCSS](https://github.com/antfu/unocss) - 高性能且极具灵活性的即时原子化 CSS 引擎 79 | - [Vant](https://github.com/youzan/vant) - 移动端 Vue 组件库 80 | - [ECharts5](https://echarts.apache.org/handbook/zh/get-started/) - 熟悉 `Echarts` 基本使用 81 | - [Mock.js](https://github.com/nuysoft/Mock) - 了解 `Mockjs` 基本语法 82 | - [Es6+](http://es6.ruanyifeng.com/) - 熟悉 `ES6` 基本语法 83 | 84 | ## 环境准备 85 | 86 | 本地环境需要安装 [Pnpm](https://www.pnpm.cn/)、[Node.js](http://nodejs.org/) 和 [Git](https://git-scm.com/) 87 | 88 | - 推荐使用 [pnpm>=8.15.4](https://www.pnpm.cn/),否则依赖可能安装不上,出现打包报错等问题。 89 | - [Node.js](http://nodejs.org/) 版本要求`18.x`以上,这里推荐 `^18.18.0 || >=20.0.0`。 90 | 91 | ## Vscode 配套插件 92 | 93 | 如果你使用的 IDE 是[vscode](https://code.visualstudio.com/)(推荐)的话,可以安装以下工具来提高开发效率及代码格式化 94 | 95 | - [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - vue 开发必备 96 | - [UnoCSS](https://marketplace.visualstudio.com/items?itemName=antfu.unocss) - UnoCSS 提示插件 97 | - [DotENV](https://marketplace.visualstudio.com/items?itemName=mikestead.dotenv) - `.env` 文件 高亮 98 | - [Error Lens](https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens) - 更好的错误定位 99 | - [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) - 不同 IDE 维护一致的编码样式 100 | - [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - 脚本代码检查 101 | - [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) - 多合一的 I18n 支持 102 | - [JavaScript and TypeScript Vscode Snippets](https://marketplace.visualstudio.com/items?itemName=sankeyangshu.vscode-javascript-typescript-snippets) - JavaScript 和 TypeScript 代码片段 103 | - [Vue Collection Vscode Snippets](https://marketplace.visualstudio.com/items?itemName=sankeyangshu.vscode-vue-collection-snippets) - 提供 Vue 2/3 代码片段 104 | 105 | ## 安装和使用 106 | 107 | ### 使用脚手架 108 | 109 | > 开发中 110 | 111 | ### GitHub 模板 112 | 113 | [使用这个模板创建仓库](https://github.com/sankeyangshu/lemon-template-vue/generate) 114 | 115 | ### 克隆使用 116 | 117 | ```bash 118 | # 克隆项目 119 | git clone https://github.com/sankeyangshu/lemon-template-vue.git 120 | 121 | # 进入项目目录 122 | cd lemon-template-vue 123 | 124 | # 安装依赖 - 推荐使用pnpm 125 | pnpm install 126 | 127 | # 启动服务 128 | pnpm dev 129 | 130 | # 打包发布 131 | pnpm build 132 | ``` 133 | 134 | ## Git 提交规范 135 | 136 | ### 提交规范 137 | 138 | 项目使用 `simple-git-hooks` 和 `commitlint` 规范 Git 提交信息,遵循社区主流的 [Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular) 规范。 139 | 140 | - `feat`: 新增功能 141 | - `fix`: 修复 bug 142 | - `docs`: 文档变更 143 | - `style`: 代码格式(不影响功能,例如空格、分号等格式修正) 144 | - `refactor`: 代码重构(不包括 bug 修复、功能新增) 145 | - `perf`: 性能优化 146 | - `test`: 添加、修改测试用例 147 | - `build`: 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等) 148 | - `ci`: 修改 CI 配置、脚本 149 | - `chore`: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例) 150 | - `revert`: 回滚 commit 151 | 152 | ## 社区 153 | 154 | 您可以使用 [issue](https://github.com/sankeyangshu/lemon-template-vue/issues) 来反馈问题,或者提交一个 Pull Request。 155 | 156 | ## 浏览器支持 157 | 158 | - 本地开发推荐使用 Chrome 最新版浏览器 [Download](https://www.google.com/intl/zh-CN/chrome/)。 159 | - 生产环境支持现代浏览器,不在支持 IE 浏览器,更多浏览器可以查看 [Can I Use Es Module](https://caniuse.com/?search=ESModule)。 160 | 161 | | [ IE](http://godban.github.io/browsers-support-badges/)
IE | [ Edge](http://godban.github.io/browsers-support-badges/)
Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | 162 | | :----------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 163 | | not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions | 164 | 165 | ## 许可证 166 | 167 | [MIT](./LICENSE) License © 2023-PRESENT [sankeyangshu](https://github.com/sankeyangshu) 168 | -------------------------------------------------------------------------------- /build/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './proxy'; 2 | export * from './utils'; 3 | -------------------------------------------------------------------------------- /build/config/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyOptions } from 'vite'; 2 | 3 | type ProxyItem = [string, string]; 4 | 5 | type ProxyList = ProxyItem[]; 6 | 7 | type ProxyTargetList = Record; 8 | 9 | const httpsRE = /^https:\/\//; 10 | 11 | /** 12 | * Generate proxy (创建代理,用于解析 .env.development 代理配置) 13 | * @param {ProxyList} list 代理地址列表 14 | */ 15 | export function createProxy(list: ProxyList = []) { 16 | const ret: ProxyTargetList = {}; 17 | for (const [prefix, target] of list) { 18 | const isHttps = httpsRE.test(target); 19 | // https://github.com/http-party/node-http-proxy#options 20 | ret[prefix] = { 21 | target, 22 | changeOrigin: true, 23 | rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''), 24 | // https is require secure=false 25 | // 如果您secure="true"只允许来自 HTTPS 的请求,则secure="false"意味着允许来自 HTTP 和 HTTPS 的请求。 26 | ...(isHttps ? { secure: false } : {}), 27 | }; 28 | } 29 | return ret; 30 | } 31 | -------------------------------------------------------------------------------- /build/config/utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import process from 'node:process'; 3 | 4 | export function isDevFn(mode: string): boolean { 5 | return mode === 'development'; 6 | } 7 | 8 | export function isProdFn(mode: string): boolean { 9 | return mode === 'production'; 10 | } 11 | 12 | export function isTestFn(mode: string): boolean { 13 | return mode === 'test'; 14 | } 15 | 16 | /** 17 | * Whether to generate package preview 18 | */ 19 | export function isReportMode(): boolean { 20 | return process.env.VITE_REPORT === 'true'; 21 | } 22 | 23 | /** 24 | * Read all environment variable configuration files to process.env (读取并处理所有环境变量配置文件 .env) 25 | * 26 | * @param envConf - A record of environment variables to be processed. 27 | * @returns An object containing the processed environment variables with appropriate types. 28 | */ 29 | export function wrapperEnv(envConf: Recordable): ViteEnv { 30 | const ret: any = {}; 31 | 32 | for (const envName of Object.keys(envConf)) { 33 | // 去除空格 34 | let realName = envConf[envName].replace(/\\n/g, '\n'); 35 | realName = realName === 'true' ? true : realName === 'false' ? false : realName; 36 | 37 | if (envName === 'VITE_PORT') { 38 | realName = Number(realName); 39 | } 40 | if (envName === 'VITE_PROXY') { 41 | try { 42 | realName = JSON.parse(realName); 43 | // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars 44 | } catch (error) {} 45 | } 46 | ret[envName] = realName; 47 | process.env[envName] = realName; 48 | } 49 | return ret; 50 | } 51 | 52 | /** 53 | * Get user root directory 54 | * @param dir file path 55 | */ 56 | export function getRootPath(...dir: string[]) { 57 | return path.resolve(process.cwd(), ...dir); 58 | } 59 | -------------------------------------------------------------------------------- /build/plugins/compress.ts: -------------------------------------------------------------------------------- 1 | import viteCompression from 'vite-plugin-compression'; 2 | import type { PluginOption } from 'vite'; 3 | 4 | /** 5 | * Configures the compression plugin for Vite build process. (根据 compress 配置,生成不同的压缩规则) 6 | * 7 | * @param viteEnv - The Vite environment configuration containing compression settings. 8 | * @returns An array of Vite compression plugins based on the configured compression algorithms. 9 | * @see https://github.com/anncwb/vite-plugin-compression 10 | */ 11 | export function configCompressPlugin(viteEnv: ViteEnv): PluginOption | PluginOption[] { 12 | const { VITE_BUILD_COMPRESS = 'none', VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false } = viteEnv; 13 | const compressList = VITE_BUILD_COMPRESS.split(','); 14 | const plugins: PluginOption[] = []; 15 | 16 | if (compressList.includes('gzip')) { 17 | plugins.push( 18 | viteCompression({ 19 | ext: '.gz', 20 | algorithm: 'gzip', 21 | deleteOriginFile: VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE, 22 | }) 23 | ); 24 | } 25 | 26 | if (compressList.includes('brotli')) { 27 | plugins.push( 28 | viteCompression({ 29 | ext: '.br', 30 | algorithm: 'brotliCompress', 31 | deleteOriginFile: VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE, 32 | }) 33 | ); 34 | } 35 | 36 | return plugins; 37 | } 38 | -------------------------------------------------------------------------------- /build/plugins/i18nPlugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import process from 'node:process'; 3 | import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'; 4 | 5 | /** 6 | * 配置i18n国际化 vite 插件 7 | */ 8 | export function configVueI18nPlugin() { 9 | // 配置i18n 10 | return VueI18nPlugin({ 11 | // locale messages resource pre-compile option (指定需要导入的语言包文件夹) 12 | include: path.resolve(process.cwd(), 'src/locales/modules/**'), 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /build/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue'; 2 | import UnoCSS from 'unocss/vite'; 3 | import { VantResolver } from 'unplugin-vue-components/resolvers'; 4 | import Components from 'unplugin-vue-components/vite'; 5 | import mockDevServerPlugin from 'vite-plugin-mock-dev-server'; 6 | import ViteRestart from 'vite-plugin-restart'; 7 | import { configCompressPlugin } from './compress'; 8 | import { configVueI18nPlugin } from './i18nPlugin'; 9 | import { configSvgIconsPlugin } from './svgPlugin'; 10 | import type { PluginOption } from 'vite'; 11 | 12 | /** 13 | * 配置 vite 插件 14 | * @param {ViteEnv} viteEnv vite 环境变量配置文件键值队 object 15 | * @param {boolean} isBuild 是否是打包模式 16 | * @returns vitePlugins[] 17 | */ 18 | export const createVitePlugins = (viteEnv: ViteEnv, isBuild: boolean) => { 19 | const { VITE_USE_MOCK } = viteEnv; 20 | 21 | const vitePlugins: (PluginOption | PluginOption[])[] = [ 22 | vue(), 23 | 24 | Components({ 25 | dts: 'src/types/components.d.ts', 26 | resolvers: [VantResolver()], 27 | types: [], 28 | }), 29 | 30 | // 配置i18n 31 | configVueI18nPlugin(), 32 | 33 | UnoCSS(), 34 | 35 | // 通过这个插件,在修改vite.config.ts文件则不需要重新运行也生效配置 36 | ViteRestart({ 37 | restart: ['vite.config.ts'], 38 | }), 39 | ]; 40 | 41 | // 是否开启 mock 服务 https://github.com/pengzhanbo/vite-plugin-mock-dev-server 42 | if (VITE_USE_MOCK) { 43 | vitePlugins.push(mockDevServerPlugin()); 44 | } 45 | 46 | vitePlugins.push(configSvgIconsPlugin(isBuild)); 47 | 48 | if (isBuild) { 49 | // 创建打包压缩配置 50 | vitePlugins.push(configCompressPlugin(viteEnv)); 51 | } 52 | 53 | return vitePlugins; 54 | }; 55 | -------------------------------------------------------------------------------- /build/plugins/svgPlugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import process from 'node:process'; 3 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; 4 | 5 | /** 6 | * Configures the SVG icons plugin for Vite. (配置svg vite 插件) 7 | * 8 | * @param isBuild - Indicates if the plugin is being configured for a build process. 9 | * @returns The configured SVG icons plugin. 10 | * @see https://github.com/anncwb/vite-plugin-svg-icons 11 | */ 12 | export function configSvgIconsPlugin(isBuild: boolean) { 13 | // 使用 svg 图标 14 | const svgIconsPlugin = createSvgIconsPlugin({ 15 | // 指定需要缓存的图标文件夹 16 | iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')], 17 | // 是否压缩 18 | svgoOptions: isBuild, 19 | // 指定symbolId格式 20 | symbolId: 'icon-[dir]-[name]', 21 | }); 22 | 23 | return svgIconsPlugin; 24 | } 25 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@sankeyangshu/eslint-config'; 2 | 3 | export default defineConfig( 4 | { 5 | vue: true, 6 | unocss: true, 7 | formatter: { 8 | markdown: true, 9 | }, 10 | }, 11 | { 12 | rules: { 13 | // '@typescript-eslint/ban-types': 'off', 14 | // '@typescript-eslint/no-explicit-any': 'off', 15 | // '@typescript-eslint/promise-function-async': 'off', 16 | 'vue/multi-word-component-names': [ 17 | 'warn', 18 | { 19 | ignores: ['index', 'App', 'Register', '[id]', '[url]'], 20 | }, 21 | ], 22 | 'vue/component-name-in-template-casing': [ 23 | 'warn', 24 | 'PascalCase', 25 | { 26 | registeredComponentsOnly: false, 27 | ignores: ['/^icon-/'], 28 | }, 29 | ], 30 | 'unocss/order-attributify': 'off', 31 | }, 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Lemon-Template-Vue 8 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /mock/modules/user.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineMock } from 'vite-plugin-mock-dev-server'; 2 | import { resultError, resultSuccess } from '../util'; 3 | 4 | interface userType { 5 | id: number; 6 | username: string; 7 | password: string; 8 | nickname: string; 9 | avatar: string; 10 | sign?: string; 11 | token: string; 12 | } 13 | 14 | const mockExample = [ 15 | '水光潋滟晴方好,山色空蒙雨亦奇。', 16 | '春江潮水连海平,海上明月共潮生。', 17 | '山回路转不见君,雪上空留马行处。', 18 | '春风又绿江南岸,明月何时照我还。', 19 | '人生自古谁无死,留取丹心照汗青。', 20 | '人生得意须尽欢,莫使金樽空对月。', 21 | '天生我材必有用,千金散尽还复来。', 22 | '日暮乡关何处是,烟波江上使人愁。', 23 | ]; 24 | 25 | const mockUsers: userType[] = [ 26 | { 27 | id: 1, 28 | username: 'admin', 29 | password: '123456', 30 | nickname: '三棵杨树', 31 | avatar: 'https://avatars.githubusercontent.com/u/64878070?v=4', 32 | sign: '从来没有真正的绝境,只有心灵的迷途', 33 | token: 'adminToken', 34 | }, 35 | { 36 | id: 2, 37 | username: 'test', 38 | password: '123456', 39 | nickname: '测试账号', 40 | avatar: 'https://avatars.githubusercontent.com/u/2?v=4', 41 | sign: '水光潋滟晴方好,山色空蒙雨亦奇', 42 | token: 'testToken', 43 | }, 44 | { 45 | id: 3, 46 | username: 'guest', 47 | password: '123456', 48 | nickname: '访客', 49 | avatar: 'https://avatars.githubusercontent.com/u/3?v=4', 50 | token: 'guestToken', 51 | }, 52 | ]; 53 | 54 | export default defineMock([ 55 | { 56 | url: '/api/example', 57 | method: 'GET', 58 | delay: 500, 59 | body: () => { 60 | const rand = Math.floor(Math.random() * mockExample.length); 61 | const mockData = mockExample[rand]; 62 | return resultSuccess({ content: mockData, date: new Date().getTime() }); 63 | }, 64 | }, 65 | { 66 | url: '/api/auth/login', 67 | method: 'POST', 68 | delay: 500, 69 | body: ({ body }) => { 70 | const { username, password } = body; 71 | const userData = mockUsers.find( 72 | (item) => item.username === username && item.password === password 73 | ); 74 | if (!userData) { 75 | return resultError('帐号或密码不正确'); 76 | } 77 | 78 | const { token, ...rest } = userData; 79 | return resultSuccess({ 80 | user: rest, 81 | token, 82 | }); 83 | }, 84 | }, 85 | ]); 86 | -------------------------------------------------------------------------------- /mock/util.ts: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | 3 | /** 4 | * 成功返回函数 5 | * @param result 返回结果 6 | * @param param1 message 消息 7 | * @returns result 8 | */ 9 | export function resultSuccess(result: T, { message = 'success' } = {}) { 10 | return Mock.mock({ 11 | code: 0, 12 | data: result, 13 | message, 14 | timestamp: new Date().getTime(), 15 | }); 16 | } 17 | 18 | export function resultError(message = 'Request failed', { code = 500, result = null } = {}) { 19 | return { 20 | code, 21 | data: result, 22 | message, 23 | type: 'error', 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lemon-template-vue", 3 | "version": "1.1.1", 4 | "type": "module", 5 | "private": true, 6 | "packageManager": "pnpm@10.6.5", 7 | "description": "An mobile web apps template based on the Vue 3 ecosystem", 8 | "author": { 9 | "name": "sankeyangshu", 10 | "email": "sankeyangshu@gmail.com", 11 | "url": "https://github.com/sankeyangshu" 12 | }, 13 | "license": "MIT", 14 | "homepage": "https://github.com/sankeyangshu/lemon-template-vue#readme", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/sankeyangshu/lemon-template-vue.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/sankeyangshu/lemon-template-vue/issues" 21 | }, 22 | "keywords": [ 23 | "vue", 24 | "templates", 25 | "echarts", 26 | "typescript", 27 | "vant", 28 | "vueuse", 29 | "pinia", 30 | "i18n", 31 | "unocss" 32 | ], 33 | "engines": { 34 | "node": "^18.18.0 || >=20.0.0", 35 | "pnpm": ">=8.15.4" 36 | }, 37 | "scripts": { 38 | "dev": "vite", 39 | "build:dev": "vite build --mode development", 40 | "build:prod": "vite build --mode production", 41 | "build:test": "vite build --mode test", 42 | "lint": "eslint .", 43 | "lint:fix": "eslint . --fix", 44 | "test": "vitest", 45 | "preview": "vite preview", 46 | "clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite", 47 | "clean:lib": "rimraf node_modules", 48 | "release": "bumpp", 49 | "preinstall": "npx only-allow pnpm", 50 | "prepare": "simple-git-hooks" 51 | }, 52 | "dependencies": { 53 | "@unocss/reset": "^66.0.0", 54 | "@vueuse/core": "^13.0.0", 55 | "axios": "^1.8.4", 56 | "dayjs": "^1.11.13", 57 | "echarts": "^5.6.0", 58 | "nprogress": "^0.2.0", 59 | "pinia": "^3.0.1", 60 | "pinia-plugin-persistedstate": "^4.2.0", 61 | "vant": "^4.9.18", 62 | "vue": "^3.5.13", 63 | "vue-i18n": "^11.1.2", 64 | "vue-router": "^4.5.0" 65 | }, 66 | "devDependencies": { 67 | "@commitlint/cli": "^19.8.0", 68 | "@commitlint/config-conventional": "^19.8.0", 69 | "@iconify/vue": "^4.3.0", 70 | "@intlify/unplugin-vue-i18n": "^6.0.5", 71 | "@sankeyangshu/eslint-config": "^1.0.0", 72 | "@types/mockjs": "^1.0.10", 73 | "@types/node": "^22.13.10", 74 | "@types/nprogress": "^0.2.3", 75 | "@unocss/eslint-plugin": "^66.0.0", 76 | "@unocss/preset-rem-to-px": "^66.0.0", 77 | "@vitejs/plugin-vue": "^5.2.3", 78 | "autoprefixer": "^10.4.21", 79 | "bumpp": "^10.1.0", 80 | "eslint": "^9.22.0", 81 | "eslint-plugin-vue": "^10.0.0", 82 | "lint-staged": "^15.5.0", 83 | "mockjs": "^1.1.0", 84 | "postcss": "^8.5.3", 85 | "postcss-mobile-forever": "^4.4.0", 86 | "rimraf": "^6.0.1", 87 | "sass": "^1.86.0", 88 | "simple-git-hooks": "^2.11.1", 89 | "typescript": "~5.8.2", 90 | "unocss": "^66.0.0", 91 | "unplugin-vue-components": "^28.4.1", 92 | "vite": "^6.2.2", 93 | "vite-plugin-compression": "^0.5.1", 94 | "vite-plugin-mock-dev-server": "^1.8.4", 95 | "vite-plugin-restart": "^0.4.2", 96 | "vite-plugin-svg-icons": "^2.0.1", 97 | "vitest": "^3.0.9", 98 | "vue-eslint-parser": "^10.1.1", 99 | "vue-tsc": "^2.2.8" 100 | }, 101 | "simple-git-hooks": { 102 | "pre-commit": "npx lint-staged", 103 | "commit-msg": "npx --no-install commitlint --edit $1" 104 | }, 105 | "lint-staged": { 106 | "*": "eslint --fix" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sankeyangshu/lemon-template-vue/5f5db5903b72370b8891cefaee27df6e781981fc/public/logo.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/api/System/user.ts: -------------------------------------------------------------------------------- 1 | import http from '@/utils/request'; 2 | 3 | /** 4 | * 登录请求参数类型 5 | */ 6 | export type loginDataType = { 7 | username: string; 8 | password: string; 9 | }; 10 | 11 | /** 12 | * 登录返回参数类型 13 | */ 14 | export interface userInfoRepType { 15 | user: userInfoType; 16 | token: string; 17 | } 18 | 19 | /** 20 | * 用户信息类型 21 | */ 22 | export interface userInfoType { 23 | id: number; 24 | username: string; 25 | phone: string; 26 | nickname: string; 27 | avatar: string; 28 | sign?: string; 29 | } 30 | 31 | // api接口 32 | const api = { 33 | example: '/example', // 示例接口 34 | login: '/auth/login', // 用户登录接口 35 | }; 36 | 37 | /** 38 | * 获取示例数据 39 | * @returns 示例数据 40 | */ 41 | export function getExampleAPI() { 42 | return http.get<{ content: string; date: number }>(api.example); 43 | } 44 | 45 | /** 46 | * 用户登录 47 | * @param data 登录请求参数 48 | * @returns 登录结果 49 | */ 50 | export function postLoginAPI(data: loginDataType) { 51 | return http.post(api.login, data); 52 | } 53 | -------------------------------------------------------------------------------- /src/assets/icons/Moon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/Sunny.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sankeyangshu/lemon-template-vue/5f5db5903b72370b8891cefaee27df6e781981fc/src/assets/images/404.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sankeyangshu/lemon-template-vue/5f5db5903b72370b8891cefaee27df6e781981fc/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/components/IconifyIcon/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | 33 | 42 | -------------------------------------------------------------------------------- /src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 40 | 41 | 50 | -------------------------------------------------------------------------------- /src/components/SwitchDark/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | // 全局默认配置项 2 | 3 | /** 4 | * 首页地址(默认) 5 | */ 6 | export const HOME_URL = '/home'; 7 | 8 | /** 9 | * 登录页地址(默认) 10 | */ 11 | export const LOGIN_URL = '/login'; 12 | 13 | /** 14 | * 默认主题颜色 15 | */ 16 | export const DEFAULT_THEMECOLOR = '#1a89fa'; 17 | -------------------------------------------------------------------------------- /src/hooks/useECharts.ts: -------------------------------------------------------------------------------- 1 | import { useDebounceFn, useResizeObserver, type UseResizeObserverReturn } from '@vueuse/core'; 2 | import { computed, onMounted, onUnmounted, ref, watch, type Ref } from 'vue'; 3 | import echarts from '@/plugins/ECharts'; 4 | import { useSettingStore } from '@/store/modules/setting'; 5 | import type { EChartsCoreOption, EChartsInitOpts, SetOptionOpts } from 'echarts'; 6 | 7 | interface ConfigPropsType { 8 | /** 9 | * init函数基本配置 10 | * @see https://echarts.apache.org/zh/api.html#echarts.init 11 | */ 12 | echartsInitOpts?: EChartsInitOpts; 13 | /** 14 | * 是否开启过渡动画 15 | * @default true 16 | */ 17 | animation?: boolean; 18 | /** 19 | * 过渡动画持续时间(ms) 20 | * @default 300 21 | */ 22 | animationDuration?: number; 23 | /** 24 | * 是否自动调整大小 25 | * @default true 26 | */ 27 | autoResize?: boolean; 28 | /** 29 | * 防抖时间(ms) 30 | * @default 300 31 | */ 32 | resizeDebounceWait?: number; 33 | /** 34 | * 最大防抖时间(ms) 35 | * @default 500 36 | */ 37 | maxResizeDebounceWait?: number; 38 | /** 39 | * 主题模式 40 | * @default 'default' 41 | */ 42 | themeMode?: 'dark' | 'light' | 'default'; 43 | } 44 | 45 | /** 46 | * 使用ECharts图表 47 | * @param {Ref} dom - 图表容器 48 | * @param {ConfigPropsType} config - 配置项 49 | * @returns 返回图表实例 50 | */ 51 | export const useECharts = ( 52 | dom: Ref, 53 | config: ConfigPropsType = {} 54 | ) => { 55 | const { 56 | echartsInitOpts, 57 | animation = true, 58 | animationDuration = 300, 59 | autoResize = true, 60 | resizeDebounceWait = 300, 61 | maxResizeDebounceWait = 500, 62 | themeMode = 'default', 63 | } = config; 64 | 65 | const settingStore = useSettingStore(); 66 | 67 | /** 当前主题 */ 68 | const currentTheme = computed(() => { 69 | return themeMode === 'default' ? settingStore.darkMode : themeMode; 70 | }); 71 | 72 | /** 图表实例 */ 73 | let chartInstance: echarts.ECharts | null = null; 74 | 75 | /** 图表尺寸变化监听 */ 76 | let resizeObserver: UseResizeObserverReturn | null = null; 77 | 78 | /** 图表配置项 */ 79 | const chartOptions = ref(null); 80 | 81 | /** Loading 状态控制 */ 82 | const toggleLoading = (show: boolean) => { 83 | if (!chartInstance) return; 84 | if (show) { 85 | chartInstance.showLoading('default'); 86 | } else { 87 | chartInstance.hideLoading(); 88 | } 89 | }; 90 | 91 | /** 图表初始化 */ 92 | const initChart = async () => { 93 | if (!dom.value || echarts.getInstanceByDom(dom.value)) return; 94 | chartInstance = echarts.init(dom.value, currentTheme.value, echartsInitOpts); 95 | toggleLoading(true); 96 | }; 97 | 98 | /** 图表销毁 */ 99 | const destroyChart = () => { 100 | if (autoResize && resizeObserver) { 101 | resizeObserver.stop(); 102 | resizeObserver = null; 103 | } 104 | 105 | if (chartInstance) { 106 | chartInstance.dispose(); 107 | chartInstance = null; 108 | } 109 | }; 110 | 111 | /** 112 | * 图表渲染 113 | * @param options 图表数据集 114 | * @param opts 图表配置项 115 | */ 116 | const renderChart = (options: EChartsCoreOption, opts: SetOptionOpts = { notMerge: true }) => { 117 | if (!chartInstance) return; 118 | const finalOptions = { ...options, backgroundColor: 'transparent' }; 119 | chartInstance.setOption(finalOptions, opts); 120 | chartOptions.value = finalOptions; 121 | toggleLoading(false); 122 | }; 123 | 124 | /** 调整图表尺寸 */ 125 | const resize = () => { 126 | if (!chartInstance) return; 127 | chartInstance.resize({ 128 | animation: { 129 | duration: animation ? animationDuration : 0, 130 | }, 131 | }); 132 | }; 133 | 134 | /** 防抖处理的resize */ 135 | const resizeDebounceHandler = useDebounceFn(resize, resizeDebounceWait, { 136 | maxWait: maxResizeDebounceWait, 137 | }); 138 | 139 | /** 重置图表 */ 140 | const resetChart = () => { 141 | if (!chartInstance) return; 142 | chartInstance.clear(); 143 | }; 144 | 145 | // 监听主题变化,自动重新初始化图表 146 | watch(currentTheme, async () => { 147 | if (!chartInstance) return; 148 | destroyChart(); 149 | await initChart(); 150 | 151 | if (chartOptions.value) { 152 | renderChart(chartOptions.value); 153 | } 154 | }); 155 | 156 | /** 获取图表实例 */ 157 | const getChartInstance = () => chartInstance; 158 | 159 | // 组件实例被挂载之后 160 | onMounted(() => { 161 | initChart(); 162 | if (autoResize) { 163 | resizeObserver = useResizeObserver(dom, resizeDebounceHandler); 164 | } 165 | }); 166 | 167 | // 组件实例被卸载之后 168 | onUnmounted(() => { 169 | destroyChart(); 170 | }); 171 | 172 | return { 173 | getChartInstance, 174 | renderChart, 175 | resetChart, 176 | toggleLoading, 177 | }; 178 | }; 179 | -------------------------------------------------------------------------------- /src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from 'vant'; 2 | import enUS from 'vant/es/locale/lang/en-US'; 3 | import zhCN from 'vant/es/locale/lang/zh-CN'; 4 | import { computed, type App } from 'vue'; 5 | import { createI18n } from 'vue-i18n'; 6 | 7 | // 默认使用的语言 8 | const defaultLanguage = 'zhCn'; 9 | 10 | const vantLocales = { 11 | 'zh-CN': zhCN, 12 | 'en-US': enUS, 13 | }; 14 | 15 | /** 16 | * 获取默认的本地语言 17 | * @returns 语言 18 | */ 19 | const getDefaultLanguage = () => { 20 | const locales = Object.keys(vantLocales); 21 | 22 | const localLanguage = localStorage.getItem('language') || navigator.language; 23 | 24 | // 存在当前语言的语言包 或 存在当前语言的任意地区的语言包 25 | if (locales.includes(localLanguage)) return localLanguage; 26 | 27 | // 若未找到,则使用 默认语言包 28 | return defaultLanguage; 29 | }; 30 | 31 | /** 32 | * 加载本地语言包 33 | * @param locale 语言 34 | * @param i18n 国际化配置 35 | */ 36 | const loadLocaleMsg = async (locale: string, i18n: I18n) => { 37 | const messages = await import(`./modules/${locale}.json`); 38 | i18n.global.setLocaleMessage(locale, messages.default); 39 | }; 40 | 41 | const setLang = async (lang: string, i18n: I18n) => { 42 | await loadLocaleMsg(lang, i18n); 43 | 44 | document.querySelector('html')!.setAttribute('lang', lang); 45 | localStorage.setItem('language', lang); 46 | i18n.global.locale.value = lang; 47 | 48 | // 设置 vant 组件语言包 49 | Locale.use(lang, vantLocales[lang as keyof typeof vantLocales]); 50 | }; 51 | 52 | /** 53 | * 初始化国际化 54 | */ 55 | const initI18n = () => { 56 | const lang = getDefaultLanguage(); 57 | const i18n = createI18n({ 58 | // 使用 Composition API 模式,则需要将其设置为false 59 | legacy: false, 60 | // 全局注入 $t 函数 61 | globalInjection: true, 62 | // 使用的语言 63 | locale: lang, 64 | // 当前语言翻译缺失时显示的语言 65 | fallbackLocale: lang, 66 | }); 67 | 68 | setLang(lang, i18n); 69 | 70 | return i18n; 71 | }; 72 | 73 | const i18n = initI18n(); 74 | type I18n = typeof i18n; 75 | 76 | export const language = computed({ 77 | get() { 78 | return i18n.global.locale.value; 79 | }, 80 | set(lang: string) { 81 | setLang(lang, i18n); 82 | }, 83 | }); 84 | 85 | /** 86 | * 配置i18n国际化 87 | * @param app vue实例 88 | */ 89 | export function setupI18n(app: App) { 90 | app.use(i18n); 91 | } 92 | 93 | export { i18n }; 94 | -------------------------------------------------------------------------------- /src/locales/modules/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "route": { 3 | "home": "Home", 4 | "example": "Example", 5 | "mine": "Mine", 6 | "themeSetting": "Theme Settings", 7 | "login": "Login", 8 | "register": "Register", 9 | "forgotPassword": "Forgot Password", 10 | "mock": "Mock Demo", 11 | "echarts": "Echarts Demo", 12 | "icon": "Icon Demo", 13 | "keepAlive": "KeepAlive Demo", 14 | "notFound": "404 Page Demo" 15 | }, 16 | "home": { 17 | "info": "Mobile Web Application Template based on Vue 3 Ecosystem", 18 | "vue": "Vue3 + Vite6", 19 | "typescript": "TypeScript", 20 | "vant": "Vant4 Component Library", 21 | "pinia": "Pinia State Management", 22 | "unocss": "Unocss Atomic CSS Framework", 23 | "router": "Vue-router 4", 24 | "syntax": "Vue 3.5+ Latest Syntax", 25 | "setup": "Latest script setup Syntax", 26 | "utils": "Built-in Echarts VueUse", 27 | "viewport": "vmin Viewport Adaptation", 28 | "axios": "Axios Encapsulation", 29 | "icons": "Multiple Icon Solutions", 30 | "eslint": "Zero-config ESLint with Prettier", 31 | "git": "Git Hook for Standardized Commits", 32 | "theme": "Theme Config with Dark Mode", 33 | "gzip": "Gzip Compression for Build", 34 | "loading": "First Screen Loading Animation", 35 | "auth": "Complete Login System" 36 | }, 37 | "errorPages": { 38 | "back": "Back to Home", 39 | "403": "Sorry, you do not have access to this page", 40 | "404": "Sorry, the page you are visiting does not exist" 41 | }, 42 | "example": { 43 | "exampleComponent": "Example Component", 44 | "language": "Language", 45 | "darkMode": "Dark Mode", 46 | "basicSetting": "Basic Settings", 47 | "keepAliveTips": "The current component will be cached, and the previous state will be retained when re-entering", 48 | "mockTips": "Data from Mock requests", 49 | "noData": "No Data", 50 | "request": "Request" 51 | }, 52 | "login": { 53 | "username": "Username", 54 | "password": "Password", 55 | "login": "Login", 56 | "register": "Register", 57 | "registerAccount": "Register Account", 58 | "forgotPassword": "Forgot Password", 59 | "usernameError": "Please enter username", 60 | "passwordError": "Please enter password", 61 | "againEnterPassword": "Please enter password again", 62 | "passwordInconsistent": "Passwords do not match", 63 | "privacyPolicy": "Privacy Policy", 64 | "userAgreement": "User Agreement", 65 | "readAgreement": "I have read and agree to", 66 | "and": "and", 67 | "pleaseEnterNewPasswordAgain": "Please enter new password again", 68 | "pleaseEnterNewPassword": "Please enter new password", 69 | "pleaseEnterVerificationCode": "Please enter verification code", 70 | "code": "Get Code", 71 | "pleaseEnterValidPhone": "Please enter valid phone number", 72 | "pleaseEnterPhone": "Please enter phone number", 73 | "confirmReset": "Confirm Reset" 74 | }, 75 | "mine": { 76 | "logoutTips": "Are you sure you want to log out?", 77 | "tips": "Tips", 78 | "logout": "Log Out", 79 | "systemVersion": "System Version", 80 | "projectDocs": "Project Documentation" 81 | }, 82 | "themeSetting": { 83 | "themeMode": "Theme Mode", 84 | "systemTheme": "System Theme Color", 85 | "pageAnimation": "Page Transition Animation", 86 | "enableAnimation": "Enable Animation", 87 | "animationType": "Animation Type", 88 | "themeGradient": "Gradient", 89 | "themeFlash": "Flash", 90 | "themeSlide": "Slide", 91 | "themeFade": "Fade", 92 | "themeBottom": "Bottom Fade", 93 | "themeScale": "Scale Fade" 94 | }, 95 | "api": { 96 | "errMsg400": "Request failed! Please try again later", 97 | "errMsg401": "Login failed! Please log in again", 98 | "errMsg403": "The current account does not have permission to access!", 99 | "errMsg404": "The resource you are accessing does not exist!", 100 | "errMsg405": "Request method error! Please try again later", 101 | "errMsg408": "Request timed out! Please try again later", 102 | "errMsg500": "Service exception!", 103 | "errMsg501": "Network not implemented!", 104 | "errMsg502": "Network error!", 105 | "errMsg503": "Service unavailable, server temporarily overloaded or under maintenance!", 106 | "errMsg504": "Network timeout!", 107 | "errMsgDefault": "Request failed!" 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/locales/modules/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "route": { 3 | "home": "首页", 4 | "example": "示例", 5 | "mine": "我的", 6 | "themeSetting": "主题设置", 7 | "login": "登录", 8 | "register": "注册", 9 | "forgotPassword": "忘记密码", 10 | "mock": "Mock 指南", 11 | "echarts": "Echarts 演示", 12 | "icon": "Icon 示例", 13 | "keepAlive": "KeepAlive 演示", 14 | "notFound": "404页 演示" 15 | }, 16 | "home": { 17 | "info": "基于 Vue 3 生态系统的移动 Web 应用模板", 18 | "vue": "Vue3 + Vite6", 19 | "typescript": "TypeScript", 20 | "vant": "Vant4 组件库", 21 | "pinia": "Pinia 状态管理", 22 | "unocss": "Unocss 原子类框架", 23 | "router": "Vue-router 4", 24 | "syntax": "Vue 3.5+ 最新语法", 25 | "setup": "使用最新的 script setup 语法", 26 | "utils": "内置 Echarts VueUse", 27 | "viewport": "vmin 视口适配", 28 | "axios": "Axios 封装", 29 | "icons": "集成多种图标方案", 30 | "eslint": "零配置 ESlint,集成Prettier", 31 | "git": "使用 Git Hook 进行规范化提交", 32 | "theme": "主题配置,支持深色模式", 33 | "gzip": "打包资源 gzip 压缩", 34 | "loading": "首屏加载动画", 35 | "auth": "完善的登录系统" 36 | }, 37 | "errorPages": { 38 | "back": "返回首页", 39 | "403": "抱歉,你无权访问该页面", 40 | "404": "抱歉,你访问的页面不存在" 41 | }, 42 | "example": { 43 | "exampleComponent": "示例组件", 44 | "language": "语言", 45 | "darkMode": "暗黑模式", 46 | "basicSetting": "基础设置", 47 | "keepAliveTips": "当前组件会被缓存,再次进入时会保留之前的状态", 48 | "mockTips": "来自Mock请求的数据", 49 | "noData": "暂无数据", 50 | "request": "请求" 51 | }, 52 | "login": { 53 | "username": "用户名", 54 | "password": "密码", 55 | "login": "登录", 56 | "register": "注册", 57 | "registerAccount": "注册账号", 58 | "forgotPassword": "忘记密码", 59 | "usernameError": "请输入用户名", 60 | "passwordError": "请输入密码", 61 | "againEnterPassword": "请再次输入密码", 62 | "passwordInconsistent": "两次输入密码不一致", 63 | "privacyPolicy": "隐私条款", 64 | "userAgreement": "用户协议", 65 | "readAgreement": "我已阅读并同意", 66 | "and": "及", 67 | "pleaseEnterNewPasswordAgain": "请再次输入新密码", 68 | "pleaseEnterNewPassword": "请填写新密码", 69 | "pleaseEnterVerificationCode": "请填写验证码", 70 | "code": "获取验证码", 71 | "pleaseEnterValidPhone": "请输入正确的手机号", 72 | "pleaseEnterPhone": "请填写手机号", 73 | "confirmReset": "确认重置" 74 | }, 75 | "mine": { 76 | "logoutTips": "确定要退出登录吗?", 77 | "tips": "温馨提示", 78 | "logout": "退出登录", 79 | "systemVersion": "系统版本", 80 | "projectDocs": "项目文档" 81 | }, 82 | "themeSetting": { 83 | "themeMode": "主题模式", 84 | "systemTheme": "系统主题色", 85 | "pageAnimation": "页面切换动画", 86 | "enableAnimation": "开启动画", 87 | "animationType": "动画类型", 88 | "themeGradient": "渐变", 89 | "themeFlash": "闪现", 90 | "themeSlide": "滑动", 91 | "themeFade": "消退", 92 | "themeBottom": "底部消退", 93 | "themeScale": "缩放消退" 94 | }, 95 | "api": { 96 | "errMsg400": "请求失败!请您稍后重试", 97 | "errMsg401": "登录失效!请您重新登录", 98 | "errMsg403": "当前账号无权限访问!", 99 | "errMsg404": "你所访问的资源不存在!", 100 | "errMsg405": "请求方式错误!请您稍后重试", 101 | "errMsg408": "请求超时!请您稍后重试", 102 | "errMsg500": "服务异常!", 103 | "errMsg501": "网络未实现!", 104 | "errMsg502": "网络错误!", 105 | "errMsg503": "服务不可用,服务器暂时过载或维护!", 106 | "errMsg504": "网络超时!", 107 | "errMsgDefault": "请求失败!" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import setupSvgIcons from '@/plugins/SvgIcons'; 3 | import App from './App.vue'; 4 | import { setupI18n } from './locales'; 5 | import { setupRouter } from './router'; 6 | import { setupStore } from './store'; 7 | import 'virtual:svg-icons-register'; 8 | import 'vant/es/toast/style'; 9 | import 'vant/es/dialog/style'; 10 | import 'vant/es/notify/style'; 11 | import 'vant/es/image-preview/style'; 12 | import 'virtual:uno.css'; 13 | import '@unocss/reset/normalize.css'; 14 | import './styles/index.scss'; 15 | 16 | function bootstrap() { 17 | const app = createApp(App); 18 | 19 | setupStore(app); 20 | 21 | setupRouter(app); 22 | 23 | setupSvgIcons(app); 24 | 25 | setupI18n(app); 26 | 27 | app.mount('#app'); 28 | } 29 | 30 | bootstrap(); 31 | -------------------------------------------------------------------------------- /src/plugins/ECharts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BarChart, 3 | GaugeChart, 4 | LineChart, 5 | MapChart, 6 | PictorialBarChart, 7 | PieChart, 8 | RadarChart, 9 | } from 'echarts/charts'; // 引入图表,图表后缀都为 Chart 10 | import { 11 | AriaComponent, 12 | CalendarComponent, 13 | DataZoomComponent, 14 | GridComponent, 15 | LegendComponent, 16 | MarkLineComponent, 17 | ParallelComponent, 18 | PolarComponent, 19 | RadarComponent, 20 | TimelineComponent, 21 | TitleComponent, 22 | ToolboxComponent, 23 | TooltipComponent, 24 | VisualMapComponent, 25 | } from 'echarts/components'; // 引入标题,提示框,直角坐标系,数据集,内置数据转换器等组件,组件后缀都为 Component 26 | import * as echarts from 'echarts/core'; // 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。 27 | import { LabelLayout, UniversalTransition } from 'echarts/features'; // 标签自动布局、全局过渡动画等特性 28 | import { CanvasRenderer } from 'echarts/renderers'; // 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步 29 | 30 | // 注册必须的组件 31 | echarts.use([ 32 | AriaComponent, 33 | CalendarComponent, 34 | DataZoomComponent, 35 | GridComponent, 36 | LegendComponent, 37 | MarkLineComponent, 38 | ParallelComponent, 39 | PolarComponent, 40 | RadarComponent, 41 | TimelineComponent, 42 | TitleComponent, 43 | ToolboxComponent, 44 | TooltipComponent, 45 | VisualMapComponent, 46 | BarChart, 47 | GaugeChart, 48 | LineChart, 49 | MapChart, 50 | PictorialBarChart, 51 | PieChart, 52 | RadarChart, 53 | LabelLayout, 54 | UniversalTransition, 55 | CanvasRenderer, 56 | ]); 57 | 58 | export default echarts; 59 | -------------------------------------------------------------------------------- /src/plugins/SvgIcons.ts: -------------------------------------------------------------------------------- 1 | import SvgIcon from '@/components/SvgIcon/index.vue'; // svg图标组件 2 | import type { App } from 'vue'; 3 | 4 | /** 5 | * setup svg icon (导入svg图标) 6 | */ 7 | export default (app: App) => { 8 | app.component('SvgIcon', SvgIcon); 9 | }; 10 | -------------------------------------------------------------------------------- /src/router/guards.ts: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress'; 2 | import { LOGIN_URL } from '@/config'; 3 | import { useRouteStore } from '@/store/modules/route'; 4 | import { useUserStore } from '@/store/modules/user'; 5 | import type { Router } from 'vue-router'; 6 | import 'nprogress/nprogress.css'; 7 | 8 | NProgress.configure({ showSpinner: false, parent: '#app' }); 9 | 10 | // 白名单 11 | const whiteList = ['/login', '/register', '/forgotPassword']; 12 | 13 | export function createRouterGuard(router: Router) { 14 | router.beforeEach(async (to, _from, next) => { 15 | NProgress.start(); 16 | 17 | // 设置标题 18 | if (typeof to.meta.title === 'string') { 19 | document.title = to.meta.title || import.meta.env.VITE_APP_TITLE; 20 | } 21 | 22 | // 获取用户信息 store 23 | const userStore = useUserStore(); 24 | // 获取用户是否登录状态,确定用户是否已登录过,存在Token 25 | const hasToken = userStore.token; 26 | 27 | if (hasToken) { 28 | // 用户登录 29 | if (to.path === LOGIN_URL) { 30 | // 如果已登录,重定向到主页 31 | next({ path: '/' }); 32 | } else { 33 | const routeStore = useRouteStore(); 34 | if (!routeStore.allRouters.length) { 35 | const accessRoutes = await routeStore.generateRoutes(); 36 | // 动态添加访问路由表 37 | accessRoutes.forEach((item) => router.addRoute(item)); 38 | 39 | // 这里相当于push到一个页面 不在进入路由拦截 40 | next({ ...to, replace: true }); 41 | } else { 42 | next(); 43 | } 44 | } 45 | } else { 46 | // 用户未登录 47 | if (whiteList.includes(to.path)) { 48 | // 在免登录白名单中,直接进入 49 | next(); 50 | } else { 51 | // 没有访问权限的其他页面将重定向到登录页面 52 | next(`/login?redirect=${to.path}`); 53 | } 54 | } 55 | }); 56 | 57 | router.afterEach(() => { 58 | NProgress.done(); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | import Layout from '@/layout/index.vue'; 3 | import { createRouterGuard } from './guards'; 4 | import type { App } from 'vue'; 5 | import type { RouteRecordRaw } from 'vue-router'; 6 | 7 | /** 8 | * 异步路由组件 9 | */ 10 | export const asyncRoutes: Array = [ 11 | { 12 | path: '/', 13 | name: 'Root', 14 | component: Layout, 15 | redirect: '/home', 16 | children: [ 17 | { 18 | path: '/login', 19 | name: 'Login', 20 | component: () => import('@/views/Login/index.vue'), 21 | meta: { title: '登录', i18n: 'login' }, 22 | }, 23 | { 24 | path: '/register', 25 | name: 'Register', 26 | component: () => import('@/views/Login/register.vue'), 27 | meta: { title: '注册', i18n: 'register' }, 28 | }, 29 | { 30 | path: '/forgotPassword', 31 | name: 'ForgotPassword', 32 | component: () => import('@/views/Login/forgotPassword.vue'), 33 | meta: { title: '忘记密码', i18n: 'forgotPassword' }, 34 | }, 35 | { 36 | path: '/home', 37 | name: 'Home', 38 | component: () => import('@/views/Home/index.vue'), 39 | meta: { title: '首页', icon: 'home-o', iconType: 'vant', tabBar: true, i18n: 'home' }, 40 | }, 41 | { 42 | path: '/example', 43 | name: 'Example', 44 | component: () => import('@/views/Example/index.vue'), 45 | meta: { title: '示例', icon: 'gem-o', iconType: 'vant', tabBar: true, i18n: 'example' }, 46 | }, 47 | { 48 | path: '/mine', 49 | name: 'Mine', 50 | component: () => import('@/views/Mine/index.vue'), 51 | meta: { 52 | title: '我的', 53 | icon: 'contact-o', 54 | iconType: 'vant', 55 | tabBar: true, 56 | hiddenNavBar: true, 57 | i18n: 'mine', 58 | }, 59 | }, 60 | { 61 | path: '/themeSetting', 62 | name: 'ThemeSetting', 63 | component: () => import('@/views/ThemeSetting/index.vue'), 64 | meta: { title: '主题设置', i18n: 'themeSetting' }, 65 | }, 66 | ], 67 | }, 68 | { 69 | path: '/demo', 70 | name: 'Demo', 71 | component: Layout, 72 | children: [ 73 | { 74 | path: '/mock', 75 | name: 'MockDemo', 76 | component: () => import('@/views/Example/mockDemo.vue'), 77 | meta: { title: 'Mock 指南', i18n: 'mock' }, 78 | }, 79 | { 80 | path: '/echarts', 81 | name: 'EchartsDemo', 82 | component: () => import('@/views/Example/echartsDemo.vue'), 83 | meta: { title: 'Echarts 演示', i18n: 'echarts' }, 84 | }, 85 | { 86 | path: '/icon', 87 | name: 'IconDemo', 88 | component: () => import('@/views/Example/iconDemo.vue'), 89 | meta: { title: 'Icon 示例', i18n: 'icon' }, 90 | }, 91 | { 92 | path: '/keepAlive', 93 | name: 'KeepAliveDemo', 94 | component: () => import('@/views/Example/keepAliveDemo.vue'), 95 | meta: { title: 'KeepAlive 演示', keepAlive: true, i18n: 'keepAlive' }, 96 | }, 97 | ], 98 | }, 99 | ]; 100 | 101 | /** 102 | * 公共路由 103 | */ 104 | export const constantRoutes: Array = [ 105 | { 106 | path: '/404', 107 | name: '404', 108 | component: () => import('@/views/ErrorPages/404.vue'), 109 | }, 110 | ]; 111 | 112 | /** 113 | * notFoundRouter(找不到路由) 114 | */ 115 | export const notFoundRouter = { 116 | path: '/:pathMatch(.*)*', 117 | name: 'notFound', 118 | redirect: '/404', 119 | }; 120 | 121 | // 创建一个可以被 Vue 应用程序使用的路由实例 122 | export const router = createRouter({ 123 | history: createWebHashHistory(), 124 | routes: [...constantRoutes, ...asyncRoutes], 125 | }); 126 | 127 | // 配置路由器 128 | export function setupRouter(app: App) { 129 | app.use(router); 130 | createRouterGuard(router); 131 | } 132 | -------------------------------------------------------------------------------- /src/router/util.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router'; 2 | 3 | /** 4 | * 过滤需要显示tabBar的路由 5 | * @param routers 异步路由表 6 | * @returns 需要显示tabBar的路由数组 7 | */ 8 | export const filterTabBar = (routers: RouteRecordRaw[]) => { 9 | const tabBarRouter: RouteRecordRaw[] = []; 10 | const deep = (routerList: RouteRecordRaw[]) => { 11 | routerList.forEach((item) => { 12 | if (item.meta?.tabBar && item.name) { 13 | tabBarRouter.push(item); 14 | } 15 | if (item.children && item.children.length) { 16 | deep(item.children); 17 | } 18 | }); 19 | }; 20 | deep(routers); 21 | return tabBarRouter; 22 | }; 23 | 24 | /** 25 | * 过滤需要缓存的路由 26 | * @param routers 异步路由表 27 | * @returns 需要缓存的路由数组 28 | */ 29 | export const filterKeepAlive = (routers: RouteRecordRaw[]) => { 30 | const cacheRouter: string[] = []; 31 | const deep = (routerList: RouteRecordRaw[]) => { 32 | routerList.forEach((item) => { 33 | if (item.meta?.keepAlive && item.name) { 34 | cacheRouter.push(item.name as string); 35 | } 36 | if (item.children && item.children.length) { 37 | deep(item.children); 38 | } 39 | }); 40 | }; 41 | deep(routers); 42 | return cacheRouter; 43 | }; 44 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; 3 | import type { App } from 'vue'; 4 | 5 | // 创建pinia实例 6 | const store = createPinia(); 7 | 8 | // 使用数据持久化插件 9 | store.use(piniaPluginPersistedstate); 10 | 11 | /** 12 | * 配置pinia 13 | * @param app vue实例 14 | */ 15 | export function setupStore(app: App) { 16 | app.use(store); 17 | } 18 | 19 | export { store }; 20 | -------------------------------------------------------------------------------- /src/store/modules/route.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { computed, ref } from 'vue'; 3 | import { asyncRoutes, constantRoutes, notFoundRouter } from '@/router'; 4 | import { filterKeepAlive } from '@/router/util'; 5 | import type { RouteRecordRaw } from 'vue-router'; 6 | 7 | export const useRouteStore = defineStore('routeState', () => { 8 | const allRouters = ref([]); 9 | const dynamicRoutes = ref([]); // 动态路由 10 | 11 | const keepAliveRoutes = computed(() => { 12 | return filterKeepAlive(dynamicRoutes.value); 13 | }); 14 | 15 | const generateRoutes = (): Promise => { 16 | return new Promise((resolve) => { 17 | const accessedRoutes = asyncRoutes.concat(notFoundRouter); 18 | 19 | allRouters.value = constantRoutes.concat(accessedRoutes); 20 | dynamicRoutes.value = accessedRoutes; 21 | resolve(accessedRoutes); 22 | }); 23 | }; 24 | 25 | return { 26 | allRouters, 27 | dynamicRoutes, 28 | keepAliveRoutes, 29 | generateRoutes, 30 | }; 31 | }); 32 | -------------------------------------------------------------------------------- /src/store/modules/setting.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { ref } from 'vue'; 3 | import { DEFAULT_THEMECOLOR } from '@/config'; 4 | import type { ConfigProviderTheme } from 'vant'; 5 | 6 | const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; 7 | 8 | export const useSettingStore = defineStore( 9 | 'settingState', 10 | () => { 11 | const darkMode = ref(prefersDark ? 'dark' : 'light'); 12 | 13 | const themeColor = ref(DEFAULT_THEMECOLOR); // 主题颜色 14 | 15 | const isPageAnimate = ref(true); // 是否开启路由动画 16 | 17 | const pageAnimateType = ref('zoom-fade'); // 路由动画类型 18 | 19 | const setThemeDark = (value: ConfigProviderTheme) => { 20 | darkMode.value = value; 21 | }; 22 | 23 | const setThemeColor = (value: string) => { 24 | themeColor.value = value; 25 | }; 26 | 27 | const setPageAnimate = (value: boolean) => { 28 | isPageAnimate.value = value; 29 | }; 30 | 31 | const setPageAnimateType = (value: string) => { 32 | pageAnimateType.value = value; 33 | }; 34 | 35 | return { 36 | darkMode, 37 | themeColor, 38 | isPageAnimate, 39 | pageAnimateType, 40 | setThemeDark, 41 | setThemeColor, 42 | setPageAnimate, 43 | setPageAnimateType, 44 | }; 45 | }, 46 | { 47 | persist: true, // 进行持久化存储 48 | } 49 | ); 50 | -------------------------------------------------------------------------------- /src/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { ref } from 'vue'; 3 | import { postLoginAPI, type loginDataType, type userInfoType } from '@/api/System/user'; 4 | import { router } from '@/router'; 5 | 6 | export const useUserStore = defineStore( 7 | 'userState', 8 | () => { 9 | const userInfo = ref(null); // 用户信息 10 | const token = ref(null); // 用户token 11 | 12 | const setUserInfo = (value: userInfoType) => { 13 | userInfo.value = value; 14 | }; 15 | 16 | const setToken = (value: string) => { 17 | token.value = value; 18 | }; 19 | 20 | const login = async (loginForm: loginDataType) => { 21 | const { username, password } = loginForm; 22 | 23 | return new Promise((resolve, reject) => { 24 | postLoginAPI({ username: username.trim(), password }) 25 | .then(({ data }) => { 26 | setToken(data.token); // 保存用户token 27 | setUserInfo(data.user); 28 | resolve(); 29 | }) 30 | .catch((error) => { 31 | reject(error); 32 | }); 33 | }); 34 | }; 35 | 36 | const logout = (goLogin = false) => { 37 | userInfo.value = null; 38 | token.value = null; 39 | if (goLogin) { 40 | router.push('/login'); 41 | } 42 | }; 43 | return { 44 | userInfo, 45 | token, 46 | setUserInfo, 47 | setToken, 48 | login, 49 | logout, 50 | }; 51 | }, 52 | { 53 | persist: true, // 进行持久化存储 54 | } 55 | ); 56 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use './variables' as *; 2 | @use './theme' as *; 3 | @use './transition' as *; 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | -moz-osx-font-smoothing: grayscale; 9 | -webkit-font-smoothing: antialiased; 10 | text-rendering: optimizeLegibility; 11 | font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 12 | Arial, sans-serif; 13 | color: var(--van-text-color); 14 | background-color: var(--color-background); 15 | } 16 | 17 | html { 18 | box-sizing: border-box; 19 | height: 100%; 20 | } 21 | 22 | #app { 23 | height: 100%; 24 | width: 100%; 25 | position: relative; 26 | } 27 | 28 | a, 29 | a:focus, 30 | a:hover { 31 | cursor: pointer; 32 | color: inherit; 33 | text-decoration: none; 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/theme.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-text: var(--van-text-color, #323233); 3 | --color-background: #f0f2f5; 4 | --color-background-2: var(--van-background-2, #ffffff); 5 | --color-block-background: #ffffff; 6 | --color-border: var(--van-border-color, #ebedf0); 7 | } 8 | 9 | html.dark { 10 | --color-text: var(--van-text-color, #f5f5f5); 11 | --color-background: #222222; 12 | --color-background-2: var(--van-background-2, #1c1c1e); 13 | --color-block-background: var(--van-border-color, #3a3a3c); 14 | --color-border: var(--van-border-color, #3a3a3c); 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // 渐变 2 | .zoom-fade-enter-active, 3 | .zoom-fade-leave-active { 4 | transition: 5 | transform 0.2s, 6 | opacity 0.3s ease-out; 7 | } 8 | 9 | .zoom-fade-enter-from { 10 | opacity: 0; 11 | transform: scale(0.92); 12 | } 13 | 14 | .zoom-fade-leave-to { 15 | opacity: 0; 16 | transform: scale(1.06); 17 | } 18 | 19 | // 闪现 20 | .zoom-out-enter-active, 21 | .zoom-out-leave-active { 22 | transition: 23 | opacity 0.1 ease-in-out, 24 | transform 0.15s ease-out; 25 | } 26 | 27 | .zoom-out-enter-from, 28 | .zoom-out-leave-to { 29 | opacity: 0; 30 | transform: scale(0); 31 | } 32 | 33 | // 滑动 34 | .fade-slide-leave-active, 35 | .fade-slide-enter-active { 36 | transition: all 0.3s; 37 | } 38 | 39 | .fade-slide-enter-from { 40 | opacity: 0; 41 | transform: translateX(-30px); 42 | } 43 | 44 | .fade-slide-leave-to { 45 | opacity: 0; 46 | transform: translateX(30px); 47 | } 48 | 49 | // 消退 50 | .fade-enter-active, 51 | .fade-leave-active { 52 | transition: opacity 0.2s ease-in-out; 53 | } 54 | 55 | .fade-enter-from, 56 | .fade-leave-to { 57 | opacity: 0; 58 | } 59 | 60 | // 底部消退 61 | .fade-bottom-enter-active, 62 | .fade-bottom-leave-active { 63 | transition: 64 | opacity 0.25s, 65 | transform 0.3s; 66 | } 67 | 68 | .fade-bottom-enter-from { 69 | opacity: 0; 70 | transform: translateY(-10%); 71 | } 72 | 73 | .fade-bottom-leave-to { 74 | opacity: 0; 75 | transform: translateY(10%); 76 | } 77 | 78 | // 缩放消退 79 | .fade-scale-leave-active, 80 | .fade-scale-enter-active { 81 | transition: all 0.28s; 82 | } 83 | 84 | .fade-scale-enter-from { 85 | opacity: 0; 86 | transform: scale(1.2); 87 | } 88 | 89 | .fade-scale-leave-to { 90 | opacity: 0; 91 | transform: scale(0.8); 92 | } 93 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // 全局 css 变量 2 | $primary-color: var(--van-primary-color); 3 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | const __APP_INFO__: { 5 | pkg: { 6 | name: string; 7 | version: string; 8 | dependencies: Recordable; 9 | devDependencies: Recordable; 10 | }; 11 | lastBuildTime: string; 12 | }; 13 | 14 | /* Vite */ 15 | type Recordable = Record; 16 | 17 | interface ImportMetaEnv extends ViteEnv { 18 | __: unknown; 19 | } 20 | 21 | interface ViteEnv { 22 | VITE_USER_NODE_ENV: 'development' | 'production' | 'test'; 23 | VITE_APP_TITLE: string; 24 | VITE_PORT: number; 25 | VITE_USE_MOCK: boolean; 26 | VITE_OPEN: boolean; 27 | VITE_REPORT: boolean; 28 | VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'gzip,brotli' | 'none'; 29 | VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean; 30 | VITE_DROP_CONSOLE: boolean; 31 | VITE_PUBLIC_PATH: string; 32 | VITE_API_URL: string; 33 | VITE_PROXY: [string, string][]; 34 | VITE_USE_IMAGEMIN: boolean; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line spaced-comment 2 | /// 3 | 4 | declare module '*.vue' { 5 | import type { DefineComponent } from 'vue'; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 8 | const component: DefineComponent<{}, {}, any>; 9 | export default component; 10 | } 11 | 12 | declare module 'virtual:svg-icons-register' { 13 | // eslint-disable-next-line 14 | const component: any 15 | export default component; 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/color.ts: -------------------------------------------------------------------------------- 1 | import { showFailToast } from 'vant'; 2 | import { isArray } from './is'; 3 | 4 | /** 5 | * hex颜色转rgb颜色 6 | * @param {string} str 颜色值字符串 7 | * @returns {number[]} 返回RGB颜色数组 8 | */ 9 | export function hexToRgb(str: string) { 10 | let hexs: number[] = []; 11 | const reg = /^#?[0-9A-Fa-f]{6}$/; 12 | if (!reg.test(str)) return showFailToast('请输入正确的hex颜色值'); 13 | 14 | str = str.replace('#', ''); 15 | hexs = str.match(/../g)?.map((val) => Number.parseInt(val, 16)) || []; 16 | return hexs; 17 | } 18 | 19 | /** 20 | * rgb颜色转Hex颜色 21 | * @param {number} r 代表红色 0-255 22 | * @param {number} g 代表绿色 0-255 23 | * @param {number} b 代表蓝色 0-255 24 | * @returns {string} 返回hex颜色值 25 | */ 26 | export function rgbToHex(r: number, g: number, b: number) { 27 | const reg = /^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/; 28 | if (!reg.test(String(r)) || !reg.test(String(g)) || !reg.test(String(b))) { 29 | return showFailToast('请输入正确的RGB颜色值(0-255)'); 30 | } 31 | const hexs = [r.toString(16), g.toString(16), b.toString(16)]; 32 | for (let i = 0; i < 3; i++) { 33 | if (hexs[i].length === 1) hexs[i] = `0${hexs[i]}`; 34 | } 35 | return `#${hexs.join('')}`; 36 | } 37 | 38 | /** 39 | * 加深颜色值 40 | * @param {string} color 颜色值字符串 41 | * @param {number} level 加深的程度,限0-1之间 42 | * @returns {string} 返回处理后的颜色值 43 | */ 44 | export function getDarkColor(color: string, level: number) { 45 | const reg = /^#?[0-9A-Fa-f]{6}$/; 46 | if (!reg.test(color)) return showFailToast('请输入正确的hex颜色值'); 47 | 48 | const rgb = hexToRgb(color); 49 | if (!rgb || !isArray(rgb)) return; 50 | 51 | for (let i = 0; i < 3; i++) { 52 | rgb[i] = Math.floor(rgb[i] * (1 - level)); 53 | } 54 | return rgbToHex(rgb[0], rgb[1], rgb[2]); 55 | } 56 | 57 | /** 58 | * 变浅颜色值 59 | * @param {string} color 颜色值字符串 60 | * @param {number} level 加深的程度,限0-1之间 61 | * @returns {string} 返回处理后的颜色值 62 | */ 63 | export function getLightColor(color: string, level: number) { 64 | const reg = /^#?[0-9A-Fa-f]{6}$/; 65 | if (!reg.test(color)) return showFailToast('请输入正确的hex颜色值'); 66 | 67 | const rgb = hexToRgb(color); 68 | if (!rgb || !isArray(rgb)) return; 69 | 70 | for (let i = 0; i < 3; i++) { 71 | rgb[i] = Math.floor(255 * level + rgb[i] * (1 - level)); 72 | } 73 | return rgbToHex(rgb[0], rgb[1], rgb[2]); 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString; 2 | 3 | /** 4 | * 数据类型 5 | */ 6 | export enum DataType { 7 | Object = 'Object', 8 | Array = 'Array', 9 | Date = 'Date', 10 | RegExp = 'RegExp', 11 | Function = 'Function', 12 | String = 'String', 13 | Number = 'Number', 14 | Boolean = 'Boolean', 15 | Undefined = 'Undefined', 16 | Null = 'Null', 17 | Symbol = 'Symbol', 18 | Set = 'Set', 19 | Map = 'Map', 20 | Promise = 'Promise', 21 | Window = 'Window', 22 | AsyncFunction = 'AsyncFunction', 23 | } 24 | 25 | export type _DataType = keyof typeof DataType; 26 | 27 | /** 28 | * 判断值是否为某个类型 29 | * @param {unknown} val 值 30 | * @param {_DataType} type 类型 31 | * @returns 判断结果 32 | */ 33 | export const is = (val: unknown, type: _DataType) => { 34 | return toString.call(val) === `[object ${type}]`; 35 | }; 36 | 37 | // 我们可以通过 is 关键字更为精准的控制类型,以下代码相当于告诉编译器,如果返回结果为 true,则代表 val 是 T 类型 38 | /** 39 | * 判断是否是函数 40 | * @param {unknown} val 值 41 | * @returns 如果 val 是 Function,则返回 true,否则返回 false 42 | */ 43 | export const isFunction = (val: unknown) => { 44 | return is(val, 'Function'); 45 | }; 46 | 47 | /** 48 | * 是否为对象 49 | * @param {any} val 值 50 | * @returns 如果 val 是 Object,则返回 true,否则返回 false 51 | */ 52 | export const isObject = (val: any): val is Record => { 53 | return val !== null && is(val, 'Object'); 54 | }; 55 | 56 | /** 57 | * 判断是否为null 58 | * @param {unknown} val 值 59 | * @returns 如果 val 是 null,则返回 true,否则返回 false 60 | */ 61 | export const isNull = (val: unknown): val is null => { 62 | return val === null; 63 | }; 64 | 65 | /** 66 | * 是否已定义 67 | * @param {T} val 值 68 | * @returns 判断结果 69 | */ 70 | export const isDef = (val?: T): val is T => { 71 | return typeof val !== 'undefined'; 72 | }; 73 | 74 | /** 75 | * 是否未定义 76 | * @param {T} val 值 77 | * @returns 判断结果 78 | */ 79 | export const isUnDef = (val?: T): val is T => { 80 | return !isDef(val); 81 | }; 82 | 83 | /** 84 | * 是否为时间 85 | * @param {unknown} val 值 86 | * @returns 如果 val 是 Date,则返回 true,否则返回 false 87 | */ 88 | export const isDate = (val: unknown): val is Date => { 89 | return is(val, 'Date'); 90 | }; 91 | 92 | /** 93 | * 是否为数值 94 | * @param {unknown} val 值 95 | * @returns 如果 val 是 Number,则返回 true,否则返回 false 96 | */ 97 | export const isNumber = (val: unknown): val is number => { 98 | return is(val, 'Number'); 99 | }; 100 | 101 | /** 102 | * 是否为AsyncFunction 103 | * @param {unknown} val 值 104 | * @returns 如果 val 是 AsyncFunction,则返回 true,否则返回 false 105 | */ 106 | export const isAsyncFunction = (val: unknown): val is Promise => { 107 | return is(val, 'AsyncFunction'); 108 | }; 109 | 110 | /** 111 | * 是否为promise 112 | * @param {unknown} val 值 113 | * @returns 如果 val 是 Promise,则返回 true,否则返回 false 114 | */ 115 | export const isPromise = (val: unknown): val is Promise => { 116 | return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch); 117 | }; 118 | 119 | /** 120 | * 是否为字符串 121 | * @param {unknown} val 值 122 | * @returns 如果 val 是 String,则返回 true,否则返回 false 123 | */ 124 | export const isString = (val: unknown): val is string => { 125 | return is(val, 'String'); 126 | }; 127 | 128 | /** 129 | * 是否为boolean类型 130 | * @param {unknown} val 值 131 | * @returns 如果 val 是 Boolean,则返回 true,否则返回 false 132 | */ 133 | export function isBoolean(val: unknown): val is boolean { 134 | return is(val, 'Boolean'); 135 | } 136 | 137 | /** 138 | * 是否为数组 139 | * @param {any} val 值 140 | * @returns 如果 val 是 Array,则返回 true,否则返回 false 141 | */ 142 | export function isArray(val: any): val is Array { 143 | return val && Array.isArray(val); 144 | } 145 | 146 | /** 147 | * 是否为浏览器 148 | * @param {any} val 值 149 | * @returns 如果 val 是 Window,则返回 true,否则返回 false 150 | */ 151 | export const isWindow = (val: any): val is Window => { 152 | return typeof window !== 'undefined' && is(val, 'Window'); 153 | }; 154 | 155 | /** 156 | * 是否是Element 157 | * @param {unknown} val 值 158 | * @returns 如果 val 是 Element,则返回 true,否则返回 false 159 | */ 160 | export const isElement = (val: unknown): val is Element => { 161 | return isObject(val) && !!val.tagName; 162 | }; 163 | 164 | /** 165 | * 是否客户端 166 | * @returns 如果是客户端,则返回 true,否则返回 false 167 | */ 168 | export const isClient = () => { 169 | return typeof window !== 'undefined'; 170 | }; 171 | 172 | export const isServer = typeof window === 'undefined'; 173 | 174 | /** 175 | * 是否为Map 176 | * @param {unknown} val 值 177 | * @returns 如果 val 是 Map,则返回 true,否则返回 false 178 | */ 179 | export const isMap = (val: unknown): val is Map => { 180 | return is(val, 'Map'); 181 | }; 182 | 183 | /** 184 | * 是否为正则 185 | * @param {unknown} val 值 186 | * @returns 如果 val 是 RegExp,则返回 true,否则返回 false 187 | */ 188 | export const isRegExp = (val: unknown): val is RegExp => { 189 | return is(val, 'RegExp'); 190 | }; 191 | 192 | /** 193 | * 是否为图片节点 194 | * @param {Element} o 节点 195 | * @returns 如果 o 是图片节点,则返回 true,否则返回 false 196 | */ 197 | export const isImageDom = (o: Element) => { 198 | return o && ['IMAGE', 'IMG'].includes(o.tagName); 199 | }; 200 | 201 | /** 202 | * 是否为空并且未定义 203 | * @param {unknown} val 值 204 | * @returns 如果 val 是 null 或 undefined,则返回 true,否则返回 false 205 | */ 206 | export const isNullAndUnDef = (val: unknown): val is null | undefined => { 207 | return isUnDef(val) && isNull(val); 208 | }; 209 | 210 | /** 211 | * 是否为空或未定义 212 | * @param {unknown} val 值 213 | * @returns 如果 val 是 null 或 undefined,则返回 true,否则返回 false 214 | */ 215 | export const isNullOrUnDef = (val: unknown): val is null | undefined => { 216 | return isUnDef(val) || isNull(val); 217 | }; 218 | 219 | /** 220 | * 是否是url 221 | * @param {string} path 路径 222 | * @returns 如果 path 是 url,则返回 true,否则返回 false 223 | */ 224 | export const isUrl = (path: string): boolean => { 225 | const reg = /^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- ./?%&=]*)?/; 226 | return reg.test(path); 227 | }; 228 | 229 | /** 230 | * 是否为空 231 | * @param {T} val 值 232 | * @returns 如果 val 是空,则返回 true,否则返回 false 233 | */ 234 | export const isEmpty = (val: T): val is T => { 235 | if (isArray(val) || isString(val)) { 236 | return val.length === 0; 237 | } 238 | 239 | if (val instanceof Map || val instanceof Set) { 240 | return val.size === 0; 241 | } 242 | 243 | if (isObject(val)) { 244 | return Object.keys(val).length === 0; 245 | } 246 | 247 | return false; 248 | }; 249 | -------------------------------------------------------------------------------- /src/utils/request/CheckStatus.ts: -------------------------------------------------------------------------------- 1 | import { showFailToast } from 'vant'; 2 | import { i18n } from '@/locales'; 3 | import { useUserStore } from '@/store/modules/user'; 4 | 5 | /** 6 | * 校验网络请求状态码 7 | * @param {number} status 状态码 8 | * @param {string | string[]} msg 错误提示信息 9 | */ 10 | export const checkStatus = (status: number, msg?: string | Array): void => { 11 | const userStore = useUserStore(); 12 | 13 | let errMsg = ''; // 错误提示信息 14 | if (msg) { 15 | errMsg = typeof msg === 'string' ? msg : msg[0]; 16 | } 17 | 18 | switch (status) { 19 | case 400: 20 | showFailToast(errMsg || i18n.global.t('api.errMsg400')); 21 | break; 22 | case 401: 23 | showFailToast(errMsg || i18n.global.t('api.errMsg401')); 24 | // 退出登录 25 | userStore.logout(true); 26 | break; 27 | case 403: 28 | showFailToast(errMsg || i18n.global.t('api.errMsg403')); 29 | break; 30 | case 404: 31 | showFailToast(errMsg || i18n.global.t('api.errMsg404')); 32 | break; 33 | case 405: 34 | showFailToast(errMsg || i18n.global.t('api.errMsg405')); 35 | break; 36 | case 408: 37 | showFailToast(errMsg || i18n.global.t('api.errMsg408')); 38 | break; 39 | case 500: 40 | showFailToast(errMsg || i18n.global.t('api.errMsg500')); 41 | break; 42 | case 502: 43 | showFailToast(errMsg || i18n.global.t('api.errMsg502')); 44 | break; 45 | case 503: 46 | showFailToast(errMsg || i18n.global.t('api.errMsg503')); 47 | break; 48 | case 504: 49 | showFailToast(errMsg || i18n.global.t('api.errMsg504')); 50 | break; 51 | default: 52 | showFailToast(errMsg || i18n.global.t('api.errMsgDefault')); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/utils/request/index.ts: -------------------------------------------------------------------------------- 1 | import request from './request'; 2 | import type { AxiosRequestConfig } from 'axios'; 3 | 4 | /** 5 | * 网络请求响应格式,T 是具体的接口返回类型数据 6 | */ 7 | interface CustomSuccessData { 8 | code?: number; 9 | msg?: string; 10 | message?: string; 11 | data: T; 12 | [keys: string]: any; 13 | } 14 | 15 | /** 16 | * 封装get请求方法 17 | * @param {string} url url 请求地址 18 | * @param {string | object} params 请求参数 19 | * @param {AxiosRequestConfig} config 请求配置 20 | * @returns {Promise>} 返回的接口数据 21 | */ 22 | const get = ( 23 | url: string, 24 | params?: string | object, 25 | config?: AxiosRequestConfig 26 | ): Promise> => { 27 | config = { 28 | method: 'get', // `method` 是创建请求时使用的方法 29 | url, // `url` 是用于请求的服务器 URL 30 | ...config, 31 | }; 32 | if (params) { 33 | config.params = params; 34 | } 35 | return request(config); 36 | }; 37 | 38 | /** 39 | * 封装post请求方法 40 | * @param {string} url url 请求地址 41 | * @param {string | object} data 请求参数 42 | * @param {AxiosRequestConfig} config 请求配置 43 | * @returns {Promise>} 返回的接口数据 44 | */ 45 | const post = ( 46 | url: string, 47 | data?: string | object, 48 | config?: AxiosRequestConfig 49 | ): Promise> => { 50 | config = { 51 | method: 'post', 52 | url, 53 | ...config, 54 | }; 55 | if (data) { 56 | config.data = data; 57 | } 58 | return request(config); 59 | }; 60 | 61 | /** 62 | * 封装patch请求方法 63 | * @param {string} url url 请求地址 64 | * @param {string | object} data 请求参数 65 | * @param {AxiosRequestConfig} config 请求配置 66 | * @returns {Promise>} 返回的接口数据 67 | */ 68 | const patch = ( 69 | url: string, 70 | data?: string | object, 71 | config?: AxiosRequestConfig 72 | ): Promise> => { 73 | config = { 74 | method: 'patch', 75 | url, 76 | ...config, 77 | }; 78 | if (data) { 79 | config.data = data; 80 | } 81 | return request(config); 82 | }; 83 | 84 | /** 85 | * 封装delete请求方法 86 | * @param {string} url url 请求地址 87 | * @param {string | object} params 请求参数 88 | * @param {AxiosRequestConfig} config 请求配置 89 | * @returns {Promise>} 返回的接口数据 90 | */ 91 | const remove = ( 92 | url: string, 93 | params?: string | object, 94 | config?: AxiosRequestConfig 95 | ): Promise> => { 96 | config = { 97 | method: 'delete', 98 | url, 99 | ...config, 100 | }; 101 | if (params) { 102 | config.params = params; 103 | } 104 | return request(config); 105 | }; 106 | 107 | // 包裹请求方法的容器,使用 http 统一调用 108 | const http = { 109 | get, 110 | post, 111 | patch, 112 | remove, 113 | }; 114 | 115 | export default http; 116 | -------------------------------------------------------------------------------- /src/utils/request/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { showToast } from 'vant'; 3 | import { useUserStore } from '@/store/modules/user'; 4 | import { checkStatus } from './CheckStatus'; 5 | import type { AxiosError, AxiosResponse } from 'axios'; 6 | 7 | // 创建新的axios实例 8 | const service = axios.create({ 9 | // 公共接口 10 | baseURL: import.meta.env.VITE_APP_BASE_API, 11 | // 超时时间 单位是ms,这里设置了5s的超时时间 12 | timeout: 5000, 13 | }); 14 | 15 | // 添加一个请求拦截器 16 | service.interceptors.request.use( 17 | (config) => { 18 | // 发请求前做的一些处理,数据转化,配置请求头,设置token,设置loading等 19 | // 每次发送请求之前判断pinia中是否存在token,如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况 20 | const userStore = useUserStore(); 21 | const token = userStore.token; 22 | 23 | if (token) { 24 | config.headers.Authorization = `Bearer ${token}`; 25 | } 26 | 27 | return config; 28 | }, 29 | (error: AxiosError) => { 30 | // 请求错误,这里可以用全局提示框进行提示 31 | showToast({ 32 | type: 'fail', 33 | message: '请求错误,请稍后再试', 34 | }); 35 | return Promise.reject(error); 36 | } 37 | ); 38 | 39 | // 添加一个响应拦截器 40 | service.interceptors.response.use( 41 | (response: AxiosResponse) => { 42 | const { status, data } = response; 43 | if (status === 200) { 44 | // 接口网络请求成功,关闭等待提示 45 | if (data.code === 0) { 46 | // 接口请求结果正确 47 | return data; 48 | } else { 49 | checkStatus(data.code, data.message); 50 | return Promise.reject(data); 51 | } 52 | } 53 | }, 54 | (error: AxiosError) => { 55 | const { response } = error; 56 | // 响应失败,关闭等待提示 57 | // 提示错误信息 58 | if (JSON.stringify(error).includes('Network Error')) { 59 | showToast({ 60 | message: '网络超时', 61 | type: 'fail', 62 | }); 63 | } 64 | // 根据响应的错误状态码,做不同的处理 65 | if (response) { 66 | checkStatus(response.status); 67 | } 68 | return Promise.reject(error); 69 | } 70 | ); 71 | 72 | export default service; 73 | -------------------------------------------------------------------------------- /src/utils/validate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 校验是否是合法邮箱 3 | * @param {string} email 邮箱 4 | * @returns 校验结果 5 | */ 6 | export function validEmail(email: string): boolean { 7 | const reg = /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/; 8 | return reg.test(email); 9 | } 10 | 11 | /** 12 | * 校验是否是合法Url 13 | * @param {string} url Url地址 14 | * @returns 校验结果 15 | */ 16 | export function validURL(url: string): boolean { 17 | const reg = 18 | /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/; 19 | return reg.test(url); 20 | } 21 | 22 | /** 23 | * 判断是否是合法手机号 24 | * @param {string} phone 手机号 25 | * @returns 校验结果 26 | */ 27 | export function validPhone(phone: string) { 28 | const reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/; 29 | return reg.test(phone); 30 | } 31 | -------------------------------------------------------------------------------- /src/views/ErrorPages/404.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/views/Example/echartsDemo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/views/Example/iconDemo.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/views/Example/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/views/Example/keepAliveDemo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/views/Example/mockDemo.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/views/Home/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 85 | 86 | 114 | -------------------------------------------------------------------------------- /src/views/Login/components/PasswordInput.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/views/Login/forgotPassword.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/views/Login/index.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/views/Login/register.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/views/Mine/index.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/views/ThemeSetting/index.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ESNext", 5 | "jsx": "preserve", 6 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 7 | "moduleDetection": "force", 8 | "useDefineForClassFields": true, 9 | "experimentalDecorators": true, 10 | "baseUrl": ".", 11 | "module": "ESNext", 12 | 13 | /* Bundler mode */ 14 | "moduleResolution": "bundler", 15 | "paths": { 16 | "@/*": ["src/*"], 17 | "~root/*": ["./*"] 18 | }, 19 | "resolveJsonModule": true, 20 | "types": ["node", "vite/client"], 21 | "allowImportingTsExtensions": true, 22 | 23 | /* Linting */ 24 | "strict": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "noUnusedLocals": true, 27 | "noUnusedParameters": true, 28 | "importHelpers": true, 29 | "noEmit": true, 30 | 31 | "sourceMap": true, 32 | "allowSyntheticDefaultImports": true, 33 | "esModuleInterop": true, 34 | "isolatedModules": true, 35 | "skipLibCheck": true, 36 | "noUncheckedSideEffectImports": true 37 | }, 38 | "include": [ 39 | "src/**/*.ts", 40 | "src/**/*.d.ts", 41 | "src/**/*.tsx", 42 | "src/**/*.vue", 43 | "tests/**/*.ts", 44 | "tests/**/*.tsx", 45 | "build/**/*.ts", 46 | "build/**/*.d.ts", 47 | "mock/**/*.ts", 48 | "vite.config.ts" 49 | ], 50 | "exclude": ["node_modules", "dist", "**/*.js"] 51 | } 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.app.json" }, 4 | { "path": "./tsconfig.node.json" } 5 | ], 6 | "files": [] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "moduleDetection": "force", 7 | "module": "ESNext", 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "types": ["node", "vite/client"], 12 | "allowImportingTsExtensions": true, 13 | 14 | /* Linting */ 15 | "strict": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noEmit": true, 20 | "isolatedModules": true, 21 | "skipLibCheck": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import presetRemToPx from '@unocss/preset-rem-to-px'; 2 | import { 3 | defineConfig, 4 | presetAttributify, 5 | presetIcons, 6 | presetTypography, 7 | presetWebFonts, 8 | presetWind3, 9 | transformerDirectives, 10 | transformerVariantGroup, 11 | } from 'unocss'; 12 | 13 | export default defineConfig({ 14 | presets: [ 15 | presetWind3(), 16 | presetAttributify(), 17 | presetIcons(), 18 | presetRemToPx({ 19 | baseFontSize: 4, 20 | }), 21 | presetTypography(), 22 | presetWebFonts({ 23 | provider: 'google', 24 | fonts: { 25 | mono: ['Fira Code'], 26 | }, 27 | }), 28 | ], 29 | 30 | shortcuts: { 31 | 'm-0-auto': 'm-0 ma', // margin: 0 auto 32 | 'wh-full': 'w-full h-full', // width: 100%, height: 100% 33 | 'flex-center': 'flex justify-center items-center', 34 | 'flex-x-center': 'flex justify-center', 35 | 'flex-y-center': 'flex items-center', 36 | 'text-overflow': 'overflow-hidden whitespace-nowrap text-ellipsis', 37 | 'text-break': 'whitespace-normal break-all break-words', 38 | }, 39 | 40 | transformers: [transformerDirectives(), transformerVariantGroup()], 41 | }); 42 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/(.*)", 5 | "destination": "/$1" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import { fileURLToPath, URL } from 'node:url'; 3 | import autoprefixer from 'autoprefixer'; 4 | import dayjs from 'dayjs'; 5 | import viewport from 'postcss-mobile-forever'; 6 | import { defineConfig, loadEnv } from 'vite'; 7 | import { createProxy, wrapperEnv } from './build/config'; 8 | import { createVitePlugins } from './build/plugins'; 9 | import pkg from './package.json'; 10 | import type { ConfigEnv, UserConfig } from 'vite'; 11 | 12 | const { dependencies, devDependencies, name, version } = pkg; 13 | const __APP_INFO__ = { 14 | pkg: { dependencies, devDependencies, name, version }, // APP 信息 15 | lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), // 最后编译时间 16 | }; 17 | 18 | // https://vite.dev/config/ 19 | export default defineConfig((config: ConfigEnv): UserConfig => { 20 | const root = process.cwd(); 21 | const { mode, command } = config; 22 | 23 | const env = loadEnv(mode, root); 24 | const viteEnv = wrapperEnv(env); 25 | 26 | const { VITE_PUBLIC_PATH, VITE_DROP_CONSOLE, VITE_PORT, VITE_PROXY, VITE_OPEN } = viteEnv; 27 | const isBuild = command === 'build'; 28 | 29 | return { 30 | base: VITE_PUBLIC_PATH, 31 | root, 32 | 33 | // 加载插件 34 | plugins: createVitePlugins(viteEnv, isBuild), 35 | 36 | // 配置别名 37 | resolve: { 38 | alias: { 39 | '@': fileURLToPath(new URL('./src', import.meta.url)), 40 | '~root': fileURLToPath(new URL('.', import.meta.url)), 41 | }, 42 | }, 43 | 44 | css: { 45 | preprocessorOptions: { 46 | scss: { 47 | api: 'modern-compiler', 48 | additionalData: `@use "@/styles/variables.scss" as *;`, 49 | }, 50 | }, 51 | postcss: { 52 | plugins: [ 53 | autoprefixer(), 54 | // https://github.com/wswmsword/postcss-mobile-forever 55 | viewport({ 56 | appSelector: '#app', 57 | viewportWidth: 375, 58 | maxDisplayWidth: 600, 59 | selectorBlackList: ['.ignore', 'keep-px'], 60 | rootContainingBlockSelectorList: ['van-tabbar', 'van-popup'], 61 | valueBlackList: ['1px solid'], 62 | }), 63 | ], 64 | }, 65 | }, 66 | 67 | // 跨域代理 68 | server: { 69 | host: true, 70 | open: VITE_OPEN, 71 | port: Number(VITE_PORT), 72 | proxy: createProxy(VITE_PROXY), 73 | }, 74 | 75 | // 定义全局常量替换方式 76 | define: { 77 | __APP_INFO__: JSON.stringify(__APP_INFO__), 78 | }, 79 | 80 | esbuild: { 81 | // 使用 esbuild 压缩 剔除 console.log 82 | pure: VITE_DROP_CONSOLE ? ['console'] : [], 83 | drop: VITE_DROP_CONSOLE ? ['debugger'] : [], 84 | }, 85 | 86 | build: { 87 | minify: 'esbuild', 88 | sourcemap: false, 89 | outDir: 'dist', 90 | reportCompressedSize: false, 91 | chunkSizeWarningLimit: 2000, 92 | rollupOptions: { 93 | output: { 94 | chunkFileNames: 'js/[name]-[hash].js', // 引入文件名的名称 95 | entryFileNames: 'js/[name]-[hash].js', // 包的入口文件名称 96 | assetFileNames: '[ext]/[name]-[hash].[ext]', // 资源文件像 字体,图片等 97 | }, 98 | }, 99 | }, 100 | }; 101 | }); 102 | --------------------------------------------------------------------------------