├── .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 |
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 |
16 |
17 |
18 |
19 |
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 | | [
](http://godban.github.io/browsers-support-badges/)IE | [
](http://godban.github.io/browsers-support-badges/)Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](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 |
4 |
5 |
6 |
7 | lemon-template-vue
8 |
9 |
10 | [English](./README.md) / 简体中文
11 |
12 | 一个基于 Vue 3 生态系统的移动 web 应用模板。
13 |
14 |
15 |
16 |
17 |
18 |
19 |
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 | | [
](http://godban.github.io/browsers-support-badges/)IE | [
](http://godban.github.io/browsers-support-badges/)Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](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 |
5 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
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 |
2 |
3 |
4 |
5 |
32 |
33 |
42 |
--------------------------------------------------------------------------------
/src/components/SvgIcon/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
40 |
41 |
50 |
--------------------------------------------------------------------------------
/src/components/SwitchDark/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {{ $t(`route.${item.meta?.i18n}`) }}
24 |
25 |
26 |
27 |
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 |
2 |
3 |
4 |

5 |
6 |
7 |
{{ $t('errorPages.404') }}
8 |
{{ $t('errorPages.back') }}
9 |
10 |
11 |
12 |
13 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/views/Example/echartsDemo.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/src/views/Example/iconDemo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/views/Example/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
30 |
31 |
32 |
33 |
34 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/src/views/Example/keepAliveDemo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ $t('example.keepAliveTips') }}
10 |
11 |
12 |
13 |
14 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/views/Example/mockDemo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ $t('example.mockTips') }}
5 |
6 |
7 |
10 |
11 | {{ message }}
12 |
13 |
14 |
15 |
16 |
24 | {{ $t('example.request') }}
25 |
26 |
27 |
28 |
29 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/views/Home/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
34 |
35 |
36 |
85 |
86 |
114 |
--------------------------------------------------------------------------------
/src/views/Login/components/PasswordInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/views/Login/forgotPassword.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
6 |
7 |
8 |
11 |
18 |
19 |
22 |
28 |
29 | {{ $t('login.code') }}
30 |
31 |
32 |
33 |
43 |
53 |
54 | {{ $t('login.confirmReset') }}
55 |
56 |
57 |
58 |
59 |
60 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/src/views/Login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
6 |
7 |
8 |
11 |
17 |
18 |
28 |
29 | {{ $t('login.login') }}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
{{ $t('login.forgotPassword') }}
41 |
|
42 |
{{ $t('login.registerAccount') }}
43 |
44 |
45 |
46 |
47 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/src/views/Login/register.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
6 |
7 |
8 |
11 |
17 |
18 |
28 |
41 |
42 | {{ $t('login.register') }}
43 |
44 |
45 |
46 |
47 |
48 |
49 | {{ $t('login.readAgreement') }}
50 | 《{{ $t('login.privacyPolicy') }}》
51 | {{ $t('login.and') }}
52 | 《{{ $t('login.userAgreement') }}》
53 |
54 |
55 |
56 |
57 |
58 |
59 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/views/Mine/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
14 |
15 |
{{ $t('login.login') }}/{{ $t('login.register') }}
16 |
17 |
18 |
{{ userInfo?.nickname }}
19 |
{{ userInfo?.sign }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/src/views/ThemeSetting/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ $t('themeSetting.themeMode') }}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
{{ $t('themeSetting.systemTheme') }}
13 |
27 |
28 |
{{ $t('themeSetting.pageAnimation') }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
48 |
49 |
50 |
51 |
52 |
58 |
59 |
60 |
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 |
--------------------------------------------------------------------------------