共计 23833 个字符,预计需要花费 60 分钟才能阅读完成。
1. 初始化项目
vue3 推荐使用 vite 进行项目构建,vite 官方中文文档参考:cn.vitejs.dev/guide/
另外包管理工具推荐 pnmp
,其号称高性能的 npm
。pnmp
由 npm/yarn
衍生而来,解决了 npm/yarn 内部潜在的 bug,极大的优化了性能,扩展了使用场景。
pnpm 安装:
npm i -g pnpm
项目初始化:
pnpm create vite@latest
过程如下:
>pnpm create vite@latest
✔ Project name: … web
✔ Select a framework: › Vue
✔ Select a variant: › TypeScript
此时可以执行如下命令安装依赖:
pnpm i
执行如下命令可以启动项目:
pnpm run dev
2. 项目配置
2.1 配置 eslint
ESLint 是一个用于识别和修复 JavaScript 代码问题的静态代码分析工具。参考了:ESLint 9.0版本踩坑记录
生成配置文件 eslint.config.js
:
npm init @eslint/config@latest
过程如下:
>npx eslint --init
You can also run this command directly using 'npm init @eslint/config@latest'.
Need to install the following packages:
@eslint/create-config@1.3.1
Ok to proceed? (y) y
@eslint/create-config: v1.3.1
✔ How would you like to use ESLint? · problems
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · vue
✔ Does your project use TypeScript? · typescript
✔ Where does your code run? · browser
The config that you've selected requires the following dependencies:
eslint, globals, @eslint/js, typescript-eslint, eslint-plugin-vue
✔ Would you like to install them now? · No / Yes
✔ Which package manager do you want to use? · pnpm
☕️Installing...
如果上面选择立即安装,会安装
@eslint/js
、eslint
、eslint-plugin-vue
、globals
、typescript-eslint
几个库。
eslint.config.js
初始内容为:
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";
export default [
{
files: ["**/*.{js,mjs,cjs,ts,vue}"]
},
{
languageOptions: {
globals: globals.browser
}
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs["flat/essential"],
{
files: ["**/*.vue"],
languageOptions: {
parserOptions: {
parser: tseslint.parser
}
}
},
];
简单测试一下 ESLint 是否能够正常使用:
// eslint.config.js
export default [
{
files: ["**/*.{js,mjs,cjs,ts,vue}"],
rules: {
semi: 2, //`semi` 规则用于检查是否缺少分号
},
},
//...
];
执行如下命令:
npx eslint src/main.ts
控制台提示缺少分号的错误,说明 rules 配置生效。
然后需要修改 eslint.config.js
配置文件:
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
export default [
{
ignores: [
'node_modules',
'dist',
'public',
],
},
{
files: ['**/*.{js,mjs,cjs,ts,vue}'],
},
{
languageOptions: {
globals: globals.browser,
},
},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
...pluginVue.configs['flat/essential'],
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: {
parser: tseslint.parser,
ecmaVersion: 'latest',
/** 允许在 .vue 文件中使用 JSX */
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
// 在这里追加 Vue 规则
'vue/no-mutating-props': [
'error',
{
shallowOnly: true,
},
],
// 关闭组件命名规则
"vue/multi-word-component-names": "off"
},
},
/**
* JavaScript 规则
*/
{
files: ['**/*.{js,mjs,cjs,vue}'],
rules: {
'no-console': 'warn',
},
},
/**
* TypeScript 规则
*/
{
files: ['**/*.{ts,tsx,vue}'],
rules: {
// 这里可以添加 TypeScript 相关的规则
},
},
];
package.json
新增两个运行脚本:
"scripts": {
"lint": "eslint src",
"fix": "eslint src --fix"
}
不确定是否需要安装 vite-plugin-eslint...
2.2 配置 prettier
上面配置的 eslint 主要检查语法问题以及少部分格式,而 prettier 则只规范格式,比如强制使用双引号,以及空格的数量,这样可以做到风格统一。
安装依赖包:
pnpm install -D prettier eslint-plugin-prettier
eslint
在 8.53.0 之后就不再支持格式化规则,因此eslint-config-prettier
作为eslint
和prettier
的规则冲突解决插件,已经没用了。
创建 prettier.config.js
配置文件:
// prettier.config.js
/**
* @type {import('prettier').Config}
* @see https://www.prettier.cn/docs/options.html
*/
export default {
trailingComma: 'all',
singleQuote: true,
semi: false,
printWidth: 80,
arrowParens: 'always',
proseWrap: 'always',
endOfLine: 'auto',
experimentalTernaries: false,
tabWidth: 2,
useTabs: false,
quoteProps: 'consistent',
jsxSingleQuote: false,
bracketSpacing: true,
bracketSameLine: false,
jsxBracketSameLine: false,
vueIndentScriptAndStyle: false,
singleAttributePerLine: false,
}
eslint.config.js
文件新增内容:
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
export default [
// ...
/**
* prettier 配置
* 会合并根目录下的 prettier.config.js 文件
* @see https://prettier.io/docs/en/options
*/
eslintPluginPrettierRecommended,
];
2.3 配置 stylelint
暂略...
2.4 配置 husky
暂略...
2.5 强制使用 pnpm
可以暂略...
注意,目前似乎 npm 有 bug,导致 preinstall 脚本 在执行
npm install
后执行。
团队开发项目的时候,需要统一包管理器工具,因为不同包管理器工具下载同一个依赖,可能版本不一样,导致项目出现 bug 问题。
需要在项目根目录下创建 scritps/preinstall.js
文件:
if (!/pnpm/.test(process.env.npm_execpath || '')) {
console.warn(
`\u001b[33mThis repository must using pnpm as the package manager ` +
` for scripts to work properly.\u001b[39m\n`,
)
process.exit(1)
}
然后配置 package.json
文件:
"scripts": {
"preinstall": "node ./scripts/preinstall.js"
}
此时使用 npm
或者 yarn
安装就会报错,原理就是在 install 的时候会触发 preinstall(npm 提供的生命周期钩子)这个文件里面的代码。
3. 项目集成
3.1 集成 element-plus
当然了,这里根据自己需要进行 UI 组件库的集成。
官网地址:https://element-plus.gitee.io/zh-CN/
安装:
pnpm install element-plus @element-plus/icons-vue
main.ts
进行全局安装 element-plus,并配置中文:
// ...
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
const app = createApp(App)
app.use(ElementPlus, {
locale: zhCn
})
app.mount('#app')
然后在 src/vite-env.d.ts
文件新增一行:
declare module 'element-plus/dist/locale/zh-cn.mjs';
element-plus 全局组件类型声明:
似乎可以忽略
// tsconfig.json
{
"compilerOptions": {
// ...
"types": ["element-plus/global"]
}
}
配置完毕可以测试 element-plus 组件与图标的使用.
3.2 其他配置
- src 别名配置
开发项目的时候文件与文件关系可能很复杂,因此我们需要给 src 文件夹配置一个别名。
配置 vite.config.ts
:
import path from 'path'
export default defineConfig({
resolve: {
alias: {
"@": path.resolve("./src") // 相对路径别名配置,使用 @ 代替 src
}
}
})
TypeScript 编译配置,在 tsconfig.json
文件中新增内容:
{
"compilerOptions": {
"baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录
"paths": { //路径映射,相对于 baseUrl
"@/*": ["src/*"]
},
},
// 不加这个可能在 vue 中引入组件有红色波浪线
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
}
- setup 插件
vite-plugin-vue-setup-extend
的作用是为了方便定义组件名。
安装:
pnpm i vite-plugin-vue-setup-extend -D
在 vite 配置文件中引入:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export default defineConfig({
plugins: [
vue(),
VueSetupExtend(),
],
})
比如原来需要两个 script
标签:
<script lang="ts">
export default {
name: 'Login'
}
</script>
<script setup lang="ts">
</script>
现在可以写出这样:
<script setup lang="ts" name="Login">
</script>
3.3 配置环境变量
项目开发过程中,至少会经历开发环境、测试环境和生产环境(即正式环境)三个阶段。不同阶段请求的状态(如接口地址等)不尽相同,若手动切换接口地址是相当繁琐且易出错的,于是环境变量配置的需求就应运而生。
项目根目录分别添加开发、生产和测试环境的文件:
#开发环境
.env.development
#生产环境
.env.production
#测试环境
.env.test
文件内容:
NODE_ENV = 'development'
VITE_APP_TITLE = 'APP'
VITE_APP_BASE_API = '/api'
NODE_ENV = 'production'
VITE_APP_TITLE = 'APP'
VITE_APP_BASE_API = '/api'
NODE_ENV = 'test'
VITE_APP_TITLE = 'APP'
VITE_APP_BASE_API = '/api'
变量必须以 VITE_ 为前缀才能暴露给外部读取,通过 import.meta.env 获取环境变量。
console.log(import.meta.env.VITE_APP_TITLE);
配置运行命令:
"scripts": {
"dev": "vite --host 0.0.0.0 --open",
"build:test": "vue-tsc && vite build --mode test",
"build:pro": "vue-tsc && vite build --mode production"
},
3.4 svg 图标配置
svg 即矢量图标,文件比 img 要小的很多,且放大不会失真。
安装插件:
pnpm install vite-plugin-svg-icons -D
配置 vite.config.ts
:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default defineConfig({
plugins: [
vue(),
createSvgIconsPlugin({
// Specify the icon folder to be cached
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// Specify symbolId format
symbolId: 'icon-[dir]-[name]',
}),
],
resolve: {
alias: {
'@': path.resolve('./src'), // 相对路径别名配置,使用 @ 代替 src
},
},
})
这里需要注意的是需要创建 src/assets/icons
文件夹,并将需要使用的 svg 文件放在该目录下。
main.ts
配置:
import 'virtual:svg-icons-register'
然后可以将 svg 图标放在该文件下下,使用方式如下:
<template>
<!-- svg:图标外层容器节点,内部需要与 use 标签结合使用 -->
<svg>
<!-- 指定 href 属性,属性值务必 icon-图标名字 -->
<!-- 会引用 icon 文件夹下的 vue.svg 图标 -->
<use href="#icon-vue"></use>
</svg>
</template>
因为项目很多模块需要使用图标,因此可以将其封装为全局组件,新建 src/components/SvgIcon/index.vue
文件:
<template>
<div>
<svg :style="{ width: width, height: height }">
<use :href="prefix + name" :fill="color"></use>
</svg>
</div>
</template>
<script setup lang="ts">
defineProps({
//xlink:href 属性值的前缀
prefix: {
type: String,
default: '#icon-'
},
//svg 矢量图的名字
name: String,
//svg 图标的颜色
color: {
type: String,
default: ""
},
//svg 宽度
width: {
type: String,
default: '16px'
},
//svg 高度
height: {
type: String,
default: '16px'
}
})
</script>
<style scoped></style>
新建 src/components/index.ts
文件,用于注册 components
文件夹内部全部全局组件。
import SvgIcon from './SvgIcon/index.vue';
import type { App, Component } from 'vue';
const components: { [name: string]: Component } = { SvgIcon };
export default {
install(app: App) {
Object.keys(components).forEach((key: string) => {
app.component(key, components[key]);
})
}
}
main.ts
安装自定义插件:
import gloablComponent from '@/components/index';
app.use(gloablComponent)
使用:
<script setup lang="ts">
import SvgIcon from '@/components/SvgIcon/index.vue'
</script>
<template>
<svg-icon name="vue"></svg-icon>
</template>
3.5 集成 less
安装:
pnpm install less less-loader -D
在组件内可以使用 scss 语法,加上 lang="less"
即可。
<style scoped lang="less"></style>
新建 src/styles/reset.less
用于重置浏览器默认样式。
/* https://github.com/ixyzorg/ixyz-style-reset.git */
/* General reset */
* {
margin: 0;
background: none;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
/* line-height is set by :root */
line-height: inherit;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
a {
color: inherit;
text-decoration: none;
}
s,
u {
text-decoration: none;
}
select {
appearance: none;
-webkit-appearance: none;
}
input[type="submit"],
button {
width: auto;
overflow: visible;
cursor: pointer;
line-height: inherit;
color: inherit;
/* Corrects inability to style clickable `input` types in iOS */
appearance: none;
}
button::-moz-focus-inner {
/* Remove excess padding and border in Firefox 4+ */
border: 0;
padding: 0;
}
/* safari requires some special resets for input type="search" */
input[type="search"] {
-webkit-appearance: textfield;
}
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/* ie 11 has it's own magic font-size rules for sub and sup */
@media all and (-ms-high-contrast: none),
(-ms-high-contrast: active) {
sub,
sup {
font-size: 120%;
}
}
/* some sensible global styles */
:root {
/* prevents mobile browsers from sometimes scaling text */
text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
/* sets box-sizing back to the sane border-box for all elements */
*,
*::before,
*::after {
box-sizing: border-box;
}
新建 src/styles/index.less
文件,项目中需要用到清除默认样式,因此需要引入 reset.less
。
@import './reset.less';
然后在 main.ts
引入 index.less
。
import '@/styles/index.less'
但是在 index.less
全局样式文件中没有办法使用变量,因此需要给项目中引入全局变量。
新建 src/styles/variable.less
文件:
@color: red;
然后配置 vite.config.ts
文件:
export default defineConfig({
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true,
additionalData: '@import "./src/styles/variable.less";',
},
},
},
})
variable.less 后面的分号不能忽略,否则会报错。
在 vue 里面使用全局变量:
<style scoped lang="less">
h1 {
color: @color;
}
</style>
不加 lang="less"
也会导致无法使用变量。
3.6 配置 mock
mock 可以模拟数据来代替真实数据,在后端接口没有开发完成的情况下非常有用。
安装依赖:
pnpm install -D vite-plugin-mock mockjs
在 vite.config.ts
配置文件启用插件。
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig({
plugins: [
viteMockServe({
enable: true,
}),
],
})
这里也可以使用 export default ({ command }) => { return {...}} 方式
在根目录下创建 mock
文件夹,再创建一个 user.ts
文件:
注意不是 src
目录下创建!!!否则请求报 404 错误
//用户信息数据
function createUserList() {
return [
{
userId: 1,
avatar:
'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
username: 'admin',
password: '111111',
desc: '平台管理员',
roles: ['平台管理员'],
buttons: ['cuser.detail'],
routes: ['home'],
token: 'Admin Token',
},
{
userId: 2,
avatar:
'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
username: 'system',
password: '111111',
desc: '系统管理员',
roles: ['系统管理员'],
buttons: ['cuser.detail', 'cuser.user'],
routes: ['home'],
token: 'System Token',
},
]
}
export default [
// 用户登录接口
{
url: '/api/user/login', //请求地址
method: 'post', //请求方式
response: ({ body }) => {
//获取请求体携带过来的用户名与密码
const { username, password } = body
//调用获取用户信息函数,用于判断是否有此用户
const checkUser = createUserList().find(
(item) => item.username === username && item.password === password,
)
//没有用户返回失败信息
if (!checkUser) {
return { code: 201, msg: '账号或者密码不正确' }
}
//如果有返回成功信息
const { token } = checkUser
return { code: 200, msg: '登录成功', data: { token } }
},
},
// 获取用户信息
{
url: '/api/user/info',
method: 'get',
response: (request) => {
//获取请求头携带token
const token = request.headers.token
//查看用户信息是否包含有次token用户
const checkUser = createUserList().find((item) => item.token === token)
//没有返回失败的信息
if (!checkUser) {
return { code: 201, msg: '获取用户信息失败' }
}
//如果有返回成功信息
return { code: 200, msg: '获取用户信息成功', data: { checkUser } }
},
},
]
3.7 配置 axios
axios 用于发送网络请求。
安装:
pnpm install axios
然后可以在 main.ts
编写测试代码:
// main.ts
import axios from 'axios';
axios({
url: '/api/user/login',
method: 'post',
data: {
username: 'admin',
password: '111111'
}
})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.error('Error:', error);
});
项目上使用 axios 一般都会进行二次封装,目的是为了使用拦截器做一些特殊的处理。
-
请求拦截器,可以在请求拦截器中处理一些业务(开始进度条、请求头携带公共参数)
-
响应拦截器,可以在响应拦截器中处理一些业务(进度条结束、简化服务器返回的数据、处理http网络错误)
首先创建 src/utils
文件夹,再创建 request.ts
文件:
import axios from 'axios'
import { ElMessage } from 'element-plus'
//创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 5000,
})
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 可以在此处添加一些公共的请求头,比如token
// const token = localStorage.getItem('token');
// if (token) {
// config.headers.Authorization = `Bearer ${token}`;
// }
return config
},
(error) => {
// 请求错误时的处理
ElMessage({
type: 'error',
message: '请求发送失败,请重试',
})
return Promise.reject(error)
},
)
// 响应拦截器
service.interceptors.response.use(
(response) => {
// 检查 response 是否存在,防止空响应引发的错误
if (!response || !response.data) {
ElMessage({
type: 'error',
message: '服务器返回异常',
})
return Promise.reject('服务器返回异常')
}
const res = response.data
let msg = ''
if (res.code === 200) {
return res
} else {
switch (res.code) {
// 这里是后端自定义的 code
case 401:
msg = '未授权,请登录'
// 可以在这里触发登出逻辑,或跳转到登录页面
break
case 403:
msg = '服务器拒绝访问'
break
case 500:
msg = '服务器内部错误'
break
default:
msg = res.msg || '请求失败'
}
ElMessage({
type: 'error',
message: msg,
})
return Promise.reject(msg)
}
},
(error) => {
let errorMsg = '请求失败,请检查网络连接'
if (error.response) {
switch (error.response.status) {
case 404:
errorMsg = '请求地址不正确'
break
default:
errorMsg = `请求失败,状态码:${error.response.status}`
}
} else if (error.message.includes('timeout')) {
errorMsg = '请求超时,请重试'
}
ElMessage({
type: 'error',
message: errorMsg,
})
return Promise.reject(error)
},
)
// 公共参数
interface CommonRequestOption {
url: string
}
// get 请求参数
export interface GetRequestOption extends CommonRequestOption {
method: 'get' | 'GET'
params?: object
}
// post 请求参数
export interface PoseRequestOption extends CommonRequestOption {
method: 'post' | 'POST'
data?: object
}
function request<T>(options: GetRequestOption): Promise<ApiResponseData<T>>
function request<T>(options: PoseRequestOption): Promise<ApiResponseData<T>>
function request<T>(options: GetRequestOption | PoseRequestOption) {
return service(options.url, options)
.then((res) => res)
.catch((err) => {
return Promise.reject(err)
}) as Promise<ApiResponseData<T>>
}
export default request
此时发送数据变成了:
// main.ts
import request from '@/utils/request'
request({
url: '/user/login',
method: 'post',
data: {
username: 'admin',
password: '111111'
}
})
.then(response => {
console.log(response.data)
})
.catch(error => {
console.error('Error:', error)
});
3.8 配置 api 目录
一般项目中使用到的网络请求地址都会统一放在 src/api
目录。
新建 src/api/login/index.ts
文件:
import { request } from "@/utils/request"
/** 登录并返回 Token */
export function loginApi(data) {
return request({
url: "/user/login",
method: "post",
data
})
}
由于使用的是 typescript,所以一般还会新建 src/api/login/types
文件夹,并在该文件夹下创建 login.ts
类型声明文件。
export interface LoginRequestData {
/** admin 或 editor */
username: string
/** 密码 */
password: string
// 如果需要验证码可以新增字段
}
export type LoginResponseData = { token: string }
创建 src/types
文件夹,创建 api.d.ts
类型声明文件。
/** 所有 api 接口的响应数据都应该准守该格式 */
interface ApiResponseData<T> {
code: number
msg: string
data: T
}
此时 src/api/login/index.ts
文件可修改为:
import request from '@/utils/request'
import type { LoginRequestData, LoginResponseData } from './types/login.ts'
/** 登录并返回 Token */
export function loginApi(data: LoginRequestData) {
return request<LoginResponseData>({
url: '/user/login',
method: 'post',
data,
})
}
此时可以这样使用:
//main.ts
import { loginApi } from '@/api/login'
loginApi({
username: 'admin',
password: '111111',
})
.then((response) => {
console.log(response.data)
})
.catch((error) => {
console.error('Error:', error)
})
3.9 配置 pinia
pinia 是为 Vue 设计的状态管理库,类似于 Vuex。
安装:
pnpm install pinia
新建 src/store/index.ts
文件:
import { createPinia } from 'pinia'
const store = createPinia()
export default store
main.ts
进行引入:
import store from '@/store'
const app = createApp(App)
app.use(store)
app.mount('#app')
新建 src/store/modules/user.ts
文件:
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { loginApi } from '@/api/login'
import { type LoginRequestData } from '@/api/login/types/login'
export const useUserStore = defineStore('user', () => {
//const token = ref<string>(getToken() || "")
const token = ref<string>('')
const login = async ({ username, password }: LoginRequestData) => {
const { data } = await loginApi({ username, password })
token.value = data.token
console.log(data)
}
return { token, login }
})
然后可以进行使用:
// main.ts
import store from '@/store'
import { useUserStore } from '@/store/modules/user'
import { type LoginRequestData } from "@/api/login/types/login"
import { reactive } from "vue"
// 需要注册 pinia 后面再进行使用
//...
app.use(store)
app.mount('#app')
const loginFormData: LoginRequestData = reactive({
username: "admin",
password: "111111"
})
useUserStore()
.login(loginFormData)
.then(() => {
console.log('login success')
//router.push({ path: "/" })
})
.catch(() => {
console.log('login error')
loginFormData.password = ""
})
3.10 配置路由
这里使用的是 vue 官方推荐的 vue-router
。
安装:
pnpm install vue-router
然后创建 src/router/index.ts
文件:
import { createRouter, createWebHistory } from 'vue-router'
import constantRoutes from './constantRoutes'
// 创建路由器实例
const router = createRouter({
history: createWebHistory(),
routes: constantRoutes,
})
const whiteList:Array<string> = ['/login', '/404']
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token'); // 先获取 token
// 如果有 token,直接继续路由,且可以在此处加入权限控制
if (token) {
if (to.path === '/login') {
return next({ path: '/dashboard' });
}
return next(); // 有权限则放行
}
// 如果没有 token,则检查是否在白名单内
if (whiteList.includes(to.path)) {
return next();
}
// 重定向到登录页面,并附加重定向路径
next(`/login?redirect=${to.fullPath}`);
});
export default router
上面配置了一个路由器,以及设置了导航守卫。其中引入了静态路由文件 src/router/constantRoutes.ts
,内容如下:
const constantRoutes = [
{
path: '/',
component: () => import('@/components/Layout/index.vue'),
children: [
{
path: 'dashboard', // 默认子路由
name: 'dashboard',
component: () => import('@/views/Dashboard/index.vue'),
},
{
path: 'user', // 默认子路由
name: 'user',
component: () => import('@/views/User/index.vue'),
},
{
path: '',
redirect: { name: 'dashboard' }
}
],
},
{
path: '/login',
name: 'login',
component: () => import('@/views/Login/index.vue'),
},
// 通配符路由匹配所有不存在的路由
{
path: '/:pathMatch(.*)*', // 通配符
name: 'notFound',
component: () => import('@/views/NotFound/index.vue'),
},
]
export default constantRoutes
这里又引入了几个页面组件,如 login
、logout
、user
组件等,后面再进行展示内容。这里配置完以后,需要在 main.ts
中引入并使用:
import router from '@/router'
//...
app.use(router)
app.mount('#app')
以下是 constantRoutes.ts
中用到的组件:
src/components/Layout/index.vue
:
<script setup lang="ts" name="Layout">
import { Expand, Fold, User, HomeFilled, UserFilled } from '@element-plus/icons-vue'
import { ref, onMounted} from 'vue'
import router from '@/router';
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
// 激活的菜单项
const defaultActive = ref<string>(router.currentRoute.value.path)
const isCollapse = ref(false)
const handleShow = () => {
isCollapse.value = !isCollapse.value
}
const handleSelect = (index) => {
router.push(index)
}
const handleOpen = (key: string, keyPath: string[]) => {
//console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {
//console.log(key, keyPath)
}
const handleLogout = () => {
useUserStore().logout()
}
const handleModifyPassword = () => {
ElMessage({
message: '别急,功能还没开发呢',
type: 'warning',
})
}
</script>
<template>
<div class="layout">
<el-container>
<el-aside width="collapse">
<el-menu :default-active="defaultActive" :collapse="isCollapse" @open="handleOpen" @close="handleClose" @select="handleSelect">
<router-link to="/">
<div class="logo">
<div v-if="!isCollapse">Logo</div>
<div v-else>L</div>
</div>
</router-link>
<el-menu-item index="/dashboard">
<el-icon>
<home-filled />
</el-icon>
<template #title>主页</template>
</el-menu-item>
<el-menu-item index="/user">
<el-icon>
<user-filled />
</el-icon>
<template #title>用户管理</template>
</el-menu-item>
</el-menu>
</el-aside>
<el-container class="layout-right">
<el-header style="text-align: right; font-size: 12px">
<div class="hamburger" @click="handleShow">
<div v-if="!isCollapse">
<el-icon style="margin-top: 1px" :size="20">
<fold />
</el-icon>
</div>
<div v-else>
<el-icon style="margin-top: 1px" :size="20">
<expand />
</el-icon>
</div>
</div>
<div class="toolbar">
<el-dropdown>
<div class="profile">
<el-icon class="el-icon--left" :size="14" >
<user />
</el-icon>
<div>admin</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleModifyPassword">修改密码</el-dropdown-item>
<el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<div class="main-content">
<router-view></router-view>
</div>
</el-container>
</el-container>
</div>
</template>
<style scoped lang="less">
@aside-bgcolor: #304156;
@submenu-bgcolor: #1f2d3d;
.layout {
height: 100vh;
}
.el-container {
height: 100%;
}
.el-aside {
background-color: @aside-bgcolor;
color: var(--el-color-white);
.logo {
height: 60px;
font-size: 30px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.el-menu {
--el-menu-bg-color: @aside-bgcolor;
--el-menu-text-color: var(--el-color-white);
--el-menu-active-color: var(--el-color-primary);
border-right-width: 0;
}
.el-menu:not(.el-menu--collapse) {
width: 170px;
min-height: 400px;
}
.el-sub-menu li {
background-color: @submenu-bgcolor;
}
}
.el-header {
position: relative;
background-color: var(--el-color-white);
color: var(--el-text-color-primary);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
display: flex;
align-items: center;
justify-content: space-between;
.toolbar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100px;
height: 100%;
right: 20px;
.profile {
display: flex;
/** 去除悬浮黑框 */
outline: none;
}
}
}
.main-content {
height: 100%;
background-color: #f0f2f5;
}
</style>
src/views/Dashboard/index.vue
:
<script setup lang="ts" name="Dashboard"></script>
<template>
<div class="main-content">
<h1>我是首页</h1>
</div>
</template>
<style lang="less">
.main-content {
display: flex;
justify-content: center;
align-items: center;
h1 {
font-size: 60px;
}
}
</style>
src/views/Login/index.vue
:
<script setup lang="ts" name="Login">
import { User, Lock } from '@element-plus/icons-vue'
import { useUserStore } from '@/store/modules/user'
import { type LoginRequestData } from '@/api/login/types/login'
import { reactive } from 'vue'
import router from '@/router'
import { useRoute } from 'vue-router'
const loginFormData: LoginRequestData = reactive({
username: 'admin',
password: '111111',
})
const route = useRoute();
async function handleLogin() {
await useUserStore()
.login(loginFormData);
// 获取 URL 中的重定向参数(如果存在)
const redirectPath = route.query.redirect || '/dashboard';
// 重定向到指定路径
router.push(redirectPath as string);
}
</script>
<template>
<div class="login">
<el-row class="login-main">
<el-col :span="15" :xs="0" class="login-left">
<div class="login-left-text">
<div class="text-line1">Welcome to</div>
<div class="text-line2">Remind</div>
</div>
</el-col>
<el-col :span="9" :xs="24" class="login-right">
<el-form label-width="auto" class="login-form" :model="loginFormData">
<h1 class="login-form-title">登录</h1>
<el-input
:prefix-icon="User"
style="width: 100%"
placeholder="请输入账号"
v-model="loginFormData.username"
/>
<el-input
:prefix-icon="Lock"
style="width: 100%"
placeholder="请输入密码"
v-model="loginFormData.password"
type="password"
show-password
/>
<el-button class="login-form-button" @click.prevent="handleLogin">Login</el-button>
</el-form>
</el-col>
</el-row>
</div>
</template>
<style scoped lang="less">
@login-main-color: #07294c;
@shadow-color: rgba(100, 104, 104, 0.5);
.login {
height: 100vh;
width: 100%;
padding: 4% 8%;
}
.login-main {
height: 100%;
}
// 使用BEM命名法,将 .login-left 和 .text 提升
.login-left {
font-weight: 300;
color: var(--el-color-white);
background-color: @login-main-color;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
.login-left-text {
margin: 20px 0 0 40px;
font-size: 80px;
}
.login-right {
box-shadow: 5px 5px 15px @shadow-color;
}
.login-form {
height: 100%;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
gap: 20px;
padding: 0 50px;
}
.login-form-title {
font-size: 20px;
font-weight: bold;
}
.login-form-button {
color: var(--el-color-white);
background-color: @login-main-color;
font-weight: 400;
//align-self: flex-end;
height: 40px;
width: 100%;
padding: 10px 20px;
}
</style>
src/views/NotFound/index.vue
:
<script setup lang="ts" name="NotFound"></script>
<template>
<div class="not-found">
<h1>404</h1>
<p>请求的路径不存在.</p>
<router-link to="/">回到首页</router-link>
</div>
</template>
<style scoped lang="less">
.not-found {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
text-align: center;
}
h1 {
font-size: 3rem;
color: #ff6f61;
}
p {
font-size: 1.5rem;
}
a {
color: #409eff;
font-size: 1.2rem;
text-decoration: underline;
margin-top: 1rem;
}
</style>
src/views/User/index.vue
:
<script setup lang="ts" name="Dashboard"></script>
<template>
<div class="main-content">
<h1>我是用户管理</h1>
</div>
</template>
<style lang="less">
.main-content {
display: flex;
justify-content: center;
align-items: center;
h1 {
font-size: 60px;
}
}
</style>