webpack+vue3+ts+vue-router路由模块化+pinia+element-plus项目搭建
目录
一、创建项目
1.进入想要创建项目的文件夹内,点击路径的空白处输入cmd并回车(这样就不用一步步cd进文件夹了)
2.输入以下命令:
vue create my-vue3-project
my-vue3-project就是项目的名称,自己随意起
3.接下来会让我们选择各种配置:
(1)这里我选择最后一种根据自己的需求手动选择
(2)下面的多种选项可以通过上下键和空格进行选择:(这里不选择vuex是因为本项目将会使用pinia替代vuex)
(3)依次选择以下配置:
(4)然后选择npm/yarn的方式安装依赖,我选择npm
此时依赖已经安装完毕,可以在命令行界面根据指引直接运行,也可以用编辑器如vscode打开项目,在编辑器中运行
4.运行项目
vscode打开项目文件夹,新建终端:
输入命令运行项目:
npm run serve
运行成功后,浏览器打开这个地址
项目创建成功啦:
二、配置状态管理 Pinia(替代vuex)
新建一个终端:
1.安装pinia:
cnpm i -S pinia
在src下新建store文件夹
2.pinia数据持久化
相关链接:https://www.jb51.net/article/249783.htm
安装依赖
cnpm i -S pinia-plugin-persist
3.在store文件夹下创建index.ts
import piniaPluginPersist from 'pinia-plugin-persist'
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(piniaPluginPersist)
export default pinia
4.挂载到main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './store' // 引入
const app = createApp(App)
app.use(pinia) // 挂载
app.use(router)
app.mount('#app')
注:为了书写方便我将createApp(App).use(pinia).use(router).mount(‘#app’)拆分开写了
5.在store文件夹下新建modules文件夹,再随便建一个示例文件count.ts
代码如下:(该文件仅为示例,根据自己的功能需要在modules下新增文件)
// 示例中 'counter'为id需唯一,新建别的ts文件时记得修改这个名字和‘counterStore’,不能重复
import { defineStore } from 'pinia'
export const counterStore = defineStore('counter', {
state: () => {
return {
count: 25
}
},
getters: {
getCount: (state) => {
return (num: number) => state.count + num
},
getComputedCount (): number {
return this.count + this.getCount(this.count) // 调用其它getter
}
},
actions: {
saveCount (count: number) {
this.count = count
}
},
persist: {
enabled: true,
strategies: [{ storage: localStorage, paths: ['token', 'userInfo'] }]
}
})
pinia配置完成!
三、组件自动导入
1.自动引入ui库
使用unplugin-vue-components
插件自动解析ui组件来自动注册;就是说不需要再import { ... } from ..
了,该插件会自动帮助解析并注册成组件。
1.首先需要安装依赖
cnpm i -D unplugin-vue-components
2.然后在vue.config.js文件中引入该插件
// 组件自动加载
const Components = require('unplugin-vue-components/webpack')
一般支持unplugin-vue-components
按需加载的ui组件库,都会暴露一个配置给unplugin-vue-components
,
截止到当前2023/3/3支持以下ui库:https://gitcode.net/mirrors/antfu/unplugin-vue-components?utm_source=csdn_github_accelerator
可以去ui官网看提供的配置也可以去git仓库直接看文档。
3.这里我拿elementplus举例
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
在vue.config.js文件中配置unplugin-vue-components插件
module.exports = defineConfig({
transpileDependencies: true,
// 自动按需加载
configureWebpack: {
plugins: [
Components({
resolvers: [ElementPlusResolver()],
dirs: ['src/components', 'src/layout'],
dts: 'src/components.d.ts'
// 允许子目录作为组件的命名空间前缀。
// directoryAsNamespace: true
})
]
}
})
配置好后,可以直接使用主要按钮 等等elementplus的组件,而不需要import { Button } from ‘element-Plus’; 导入进来,刷新页面就可以看到组件能正确的显示在页面上。
更重要的是,你写在src/components文件夹以及src/layout文件夹里的公共组件,也不需要再import导入了,会自动按需注册。
那么问题来了,你怎么知道要使用什么组件名称呢?
答案就是在根目录的components.d.ts文件里(这个文件是自动生成的,每次需要解析注册组件该文件都会自动更新)
最后需要注意的是:在使用ui组件库提供的函数式组件时,需要再额外引入css样式。其他比如Vant的Toast,Dialog,Notify,ImagePreview,需要额外引入
2.自动引入ref,reavtive等
首先安装插件
cnpm i -D unplugin-auto-import
配置 vue.config.ts
//引入自动引入插件*
const AutoImport = require('unplugin-auto-import/webpack')
跟Components同级配置unplugin-auto-import插件
AutoImport({
dirs: ['src/utils'], // 这里面是想要被自动导入的文件夹
imports: ['vue', 'vue-router'],
dts: 'src/auto-imports.d.ts'
}),
然后就可以在文件里直接使用vue3语法(ref,computed等等)不用引入啦
添加进tsconfig.json
在tsconfig.json文件中的include属性中增加"src/**/*.d.ts" 如下:
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx",
"src/**/*.d.ts"
]
四、配置vue-router(模块化)
1.创建项目时脚手架为我们自动生成了router文件夹以及index.ts文件,根据示例添加路由即可。但当团队协作同时开发时,所有人在同一文件中新增路由,合并文件时难免出现代码冲突,因此可以配置路由模块化,开发人员各自新建自己的ts路由文件,配置文件会将其组合起来。
// router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const constantFiles = require.context('./modules', true, /\.ts$/)
let routes: Array<RouteRecordRaw> = []
// 根据路径拿到所有modules文件夹下的ts文件
constantFiles.keys().forEach((key) => {
routes = routes.concat(constantFiles(key).default)
})
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
2.src/router下创建modules目录,新建ts文件,文件中路由可以直接访问,不同模块的功能可以分别建不同的ts文件定义路由
由于目前还没有创建文件,我们待会再来创建具体的路由文件,可以先往下走。(八–>使用)
五、国际化
自己的内容国际化
1.安装依赖
cnpm install vue-i18n
2.src目录下新建utils文件夹,用来存放公共方法等文件;utils文件夹下新建 storage.ts 文件
// storage.ts 本地存储
const languageKey = 'language'
export const getLanguage = (): string => {
return localStorage.getItem(languageKey) as string // 获取语言
}
export const setLanguage = (language: string):void => {
localStorage.setItem(languageKey, language) // 存入语言
}
由于我们在3.2中已经设置src/utils目录下的文件自动导入,所以getLanguage和setLanguage方法可以在任何地方直接使用,而不需要手动引入,是不是超方便~
3.在src下新建locales文件夹,包含index.ts, locales目录下新建lang文件夹,包含tc.ts、sc.ts、en.ts
// index.ts
import { createI18n } from 'vue-i18n'
import enLocale from 'element-plus/es/locale/lang/en'
import zhLocale from 'element-plus/es/locale/lang/zh-cn'
import zhTwLocale from 'element-plus/es/locale/lang/zh-tw'
import tc from './lang/tc'
import sc from './lang/sc'
import en from './lang/en'
const messages = {
tc: {
...tc,
...zhTwLocale
},
sc: {
...sc,
...zhLocale
},
en: {
...en,
...enLocale
}
}
// 获取本地语言
export const getLocale = ():string => {
// 优先从local取语言
const localLanguage = getLanguage()
if (localLanguage !== null && localLanguage !== undefined && localLanguage !== '') {
document.documentElement.lang = localLanguage
return localLanguage
}
// 从浏览器对象取语言
const language = navigator.language.toLowerCase()
const locales = Object.keys(messages)
for (const locale of locales) {
if (language.includes(locale)) {
document.documentElement.lang = locale
return locale
}
}
// 默认中文 tc
return 'tc'
}
const i18n = createI18n({
legacy: false,
locale: getLocale(), // 首先从缓存里拿,没有的话就用浏览器语言,
fallbackLocale: 'tc', // 设置备用语言
messages
})
export default i18n
import tc from ‘./lang/tc’ 报错:文件“d:/Snowy/AAATest/my-vue3-project/src/locales/lang/tc.ts”不是模块
解决方法:一顿百度猛如虎,原来是我新建的tc.ts、sc.ts、en.ts文件内还没写东西,没有导出模块,写上就好了
// tc.ts
const tc = {
common: {
all: '全部'
}
}
export default tc
4.在main.ts引入并挂载
import i18n from './locales'
......
app.use(i18n)
elementPlus国际化
1.安装elementPlus
cnpm install element-plus --save
2.在main.ts中引入element-plus及其语言包
// main.ts
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import zhTw from 'element-plus/es/locale/lang/zh-tw'
import en from 'element-plus/es/locale/lang/en'
import 'element-plus/dist/index.css'
如果此时报错‘找不到模块“element-plus”或其相应的类型声明。’ 不要着急 他可能只是反应慢,耐心等下。
3.在main.ts中引入挂载element-plus并设置默认语言
app.use(ElementPlus, {
locale: getLanguage() === 'tc' ? zhTw : getLanguage() === 'sc' ? zhCn : en
})
至此国际化文件就配置好啦,如何切换语言,在我们搭建好框架,有了切换按钮后再写
使用
locales目录下新建tool.ts
// tool.ts
import i18n from './index'
// 导出全局t方法
export const t = (str: string): string => {
return i18n.global.t(str)
}
例子:在template中,使用
$t('common.view')
在ts中,先引入t方法再使用:
// 引入
import { t } from '@/locales/tool'
// 使用
t('common.view')
六、环境配置
开发过程、测试过程、生产过程使用的接口地址不同,还有执行的操作可能也不一样,也就需要配置好开发环境、测试环境、生产环境,需要什么环境下的配置直接使用即可。
1、在src同级目录也就是根目录下新建文件:.env.development(开发环境)、.env.test(测试环境)、.env.production文件(生产环境)
2、三个配置文件的配置内容如下:
// .env.development
# 模式
NODE_ENV = 'development'
# 通过"VUE_APP_MODE"变量来区分环境
VUE_APP_MODE = 'development'
# 基础路径
VUE_APP_API_URL = ''
// .env.test
# 模式
NODE_ENV = 'test'
# 通过"VUE_APP_MODE"变量来区分环境
VUE_APP_MODE = 'test'
# 基础路径
VUE_APP_API_URL = ''
// .env.production
# 模式
NODE_ENV = 'production'
# 通过"VUE_APP_MODE"变量来区分环境
VUE_APP_MODE = 'production'
# 基础路径
VUE_APP_API_URL = ''
七、接口请求封装
axios封装
1.安装axios
cnpm i axios
2.utils文件夹下新建https.ts
// https.ts
import axios, { type AxiosRequestConfig } from 'axios'
import { userStore } from '@/store/modules/user'
import { ElMessage } from 'element-plus'
import { getLocale } from '@/locales' // 获取当前语言的方法
const { NODE_ENV, VUE_APP_BASE_URL, VUE_APP_API } = process.env
const IS_PROD = ['production', 'prod'].includes(NODE_ENV) // 是否为生产环境
const baseURL = IS_PROD ? `${String(VUE_APP_BASE_URL)}${String(VUE_APP_API)}` : VUE_APP_API
const https = () => {
const user = userStore()
const config: AxiosRequestConfig = {
baseURL,
timeout: 20000,
headers: {
language: getLocale(),
Authorization: user.getToken
}
}
/* 初始化axioas */
const service = axios.create(config)
/* 请求拦截器 */
service.interceptors.request.use(
(config) => {
// do something
config.baseURL = baseURL
console.log('----------baseURL:', baseURL)
return config
},
(error) => {
// 拦截接口超时
const isTimeOut = error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1
if (isTimeOut) {
ElMessage('接口超时,请稍后重试')
}
// // 处理响应失败
return Promise.reject(error)
}
)
/* 响应拦截器 */
service.interceptors.response.use(
(response) => {
// do something
console.log('http请求返回值:', response.data)
return Promise.resolve(response.data)
},
(error) => {
// 拦截接口超时
const isTimeOut = error.code === 'ECONNABORTED' && error.message.indexOf('timeout') !== -1
if (isTimeOut) {
ElMessage('接口超时,请稍后重试')
}
// 处理响应失败
return Promise.reject(error)
}
)
return service
}
export enum Method {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
DELETE = 'DELETE'
}
export enum ContentType {
form = 'application/x-www-form-urlencoded',
json = 'application/json; charset=utf-8',
multipart = 'multipart/form-data'
}
/**
* 网络请求参数
*/
// export interface RequestParams {
// [key: string]: any
// }
export default https
3.配置代理
vue.config.js文件中,与configureWebpack同级,新增以下代码:
devServer: {
host: '0.0.0.0',
port: 8080,
// open: true, // 自动打开浏览器
proxy: {
'/api': {
target: VUE_APP_BASE_URL, // 目标代理接口地址
secure: false,
changeOrigin: true, // 开启代理,在本地创建一个虚拟服务端
// ws: true, // 是否启用websockets
logLevel: 'debug',
onProxyReq (proxyReq, req, res) {
console.log('[HPM] %s %s %s %s', req.method, req.originalUrl, '->', VUE_APP_BASE_URL)
console.log('[HPM] Rewriting path from "%s" to "%s"', req.originalUrl, req.url)
},
pathRewrite: {
'^/api': ''
}
}
}
}
api请求promise封装
1.src目录下新建apis文件夹,该文件夹下存放我们的接口,如新建一个test.ts文件
// test.ts
import https, { ContentType, Method } from '@/utils/https'
// 测试接口
export const getTestData = (params:any) => {
return https().request({
url: '/courseList ',
method: Method.POST,
headers: { 'Content-Type': ContentType.json },
data: params
})
}
使用方法:先引入接口名,然后直接使用
八、后台管理平台layout框架搭建
设置公共样式
1.assets目录下新建style文件夹,并新建以下文件
// _mixins.scss
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
//flex布局 参数为可用布局
@mixin flex($justify: space-between,$align: center) {
display: flex;
justify-content: $justify;
align-items: $align;
}
//字体样式
@mixin font($font_size,$line_height,$color, $fontWeight: 400) {
font-size: $font_size;
line-height: $line_height;
color: $color;
font-weight: $fontWeight;
}
//背景颜色和字体颜色
@mixin bgColor($color, $bgColor) {
color: $color;
background-color: $bgColor;
}
// 页面padding-top
@mixin pageTop($top){
padding-top: $top;
}
// 图片盒子
@mixin imgBox($width,$height){
display: inline-block;
width: $width;
height: $height;
img{
width: 100%;height: 100%;
}
}
// 单行省略号
@mixin ellipsis-single{
overflow: hidden; /*超出部分隐藏*/
white-space: nowrap; /*禁止换行*/
text-overflow: ellipsis;
}
// 多行省略号
@mixin ellipsis-multi-row($num: 2){
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: $num;
overflow: hidden;
}
// 隐藏滚动条
@mixin hideScrollbar(){
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar {
display: none;
}
}
// main.css
/* 清除内外边距 */
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote,
dl, dt, dd, ul, ol, li,legend,
pre,
fieldset, button, input, textarea,
th, td {
margin: 0;
padding: 0;
}
/* 设置默认字体 */
/* body,
button, input, select, textarea {
font: 12px/1.3 "Microsoft YaHei",Tahoma, Helvetica, Arial, "\5b8b\4f53", sans-serif;
color: #333;
} */
/* 重置列表元素 */
ul, ol { list-style: none; }
/* 重置文本格式元素 */
a { text-decoration: none;}
/* 重置表单元素 */
legend { color: #000; } /* for ie6 */
fieldset, img { border: none; }
button, input, select, textarea {
font-size: 100%; /* 使得表单元素在 ie 下能继承字体大小 */
}
/* 重置表格元素 */
table {
border-collapse: collapse;
border-spacing: 0;
}
/* 重置 hr */
hr {
border: none;
height: 1px;
}
.clearFix::after{
content:"";
display: block;
clear:both;
}
/* 让非ie浏览器默认也显示垂直滚动条,防止因滚动条引起的闪烁 */
/* html { overflow-y: scroll; } */
html,body{
width: 100%;
height: 100%;
overflow: hidden;
}
/* 清除浮动 */
.clearfix::after {
display: block;
height: 0;
content: "";
clear: both;
visibility: hidden;
}
// global.scss 公共样式变量
$color-red: red;
// ……
2.vue.config.js中与configureWebpack同级增加:
css: {
// 开启 CSS source maps?
sourceMap: false,
loaderOptions: {
less: {
charset: false,
}
scss: {
additionalData: `
@import "./src/assets/style/_mixins.scss";
`
}
},
extract: {
ignoreOrder: true
}
}
搭建
1.顶部组件
1.src下新建layout文件夹,该文件夹下新建commonHeader.vue,leftNav.vue,layoutIndex.vue
// commonHeader.vue 框架顶部,包含国际化语言切换
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { setLanguage } from '@/utils/storage'
const { locale } = useI18n()
const langList = ref([
{
code: 'tc',
name: '繁'
},
{
code: 'sc',
name: '简'
},
{
code: 'en',
name: '英'
}
])
const changeLanguage = (lang: string) => {
locale.value = lang
console.log(locale.value);
setLanguage(lang)
location.reload() // 切换语言后刷新下页面
}
</script>
<template>
<div class="headWrap">
<div class="headLeft"></div>
<div class="headRight">
<div class="langNav">
<div class="langItem" v-for="lang in langList" :key="lang.code" @click="changeLanguage(lang.code)"
:class="{ activeLang: locale === lang.code }">
{{ lang.name }}
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.headWrap {
@include flex(space-between, center);
width: 100%;
height: 100%;
.headRight {
.langNav {
@include flex(space-between, center);
.langItem {
@include font(18px, 18px, #6E42B1);
cursor: pointer;
margin-left: 5px;
}
.activeLang {
text-decoration: underline;
cursor: pointer;
margin-left: 5px;
}
}
}
}</style>
2.左侧目录组件
注意,url属性中放路由地址,即使不需要跳转的目录也需要给一个url,不可重复,因为菜单是通过url中的路由值跳转的
显示icon时可以直接使用img,不过我封装成了一个组件效果是一样的,感兴趣的可以看 十.1
// leftNav.vue 左侧菜单栏
<script lang="ts" setup>
import { useRoute } from 'vue-router'
const route = useRoute()
type grandVo = {
menuid: string,
icon: string,
menuname: string,
url: string,
}
type childVo = {
menuid: string,
icon: string,
menuname: string,
url: string,
childList?: grandVo[],
}
type menuVo = {
menuid: string,
icon: string,
menuname: string,
url: string,
childList?: childVo[],
}
const allmenu = ref<menuVo[]>([])
const getMenu = () => {
const res = {
success: true,
data: [
{
menuid: '0',
icon: 'home-active',
menuname: '首页',
url: '/index'
},
{
menuid: '1',
icon: 'home-active',
menuname: '会议管理',
url: '1',
childList: [
{
menuid: '1-1',
icon: '',
menuname: '会议查询',
url: '/searchMetting'
}
]
},
{
menuid: '2',
icon: '',
menuname: '订单管理',
url: '/url2'
},
{
menuid: '3',
icon: '',
menuname: '保单管理',
url: '3',
childList: [
{
menuid: '3-1',
icon: '',
menuname: '客户管理',
url: '/url3-1'
},
{
menuid: '3-2',
icon: '',
menuname: '客户管理',
url: '/url3-2',
childList: [
{
menuid: '3-2-1',
icon: '',
menuname: '客户管理',
url: '/url3-2-1'
},
{
menuid: '3-2-2',
icon: '',
menuname: '客户管理',
url: '/url3-2-2'
}
]
}
]
}
],
msg: 'success'
}
allmenu.value = res.data
}
onMounted(() => {
getMenu()
})
const iconSize = ref('22px') // menu icon宽高
</script>
<template>
<el-menu :default-active="route.path" router
class="el-menu-vertical-demo" background-color="#ffffff" text-color="#333333" active-text-color="#6E42B1">
<template v-for="menu in allmenu" :key="menu.menuid">
<!-- 一级菜单-可展开 -->
<el-sub-menu v-if="menu.childList && menu.childList.length > 0" :key="menu.menuid" :index="menu.url" >
<template #title>
<!-- icon -->
<!-- <AppIcon v-if="menu.icon" :name="menu.icon" :size="iconSize" /> -->
<span v-if="menu.menuname">{{ menu.menuname }}</span>
</template>
<!-- 二级菜单 -->
<el-menu-item v-for="subMenu in menu.childList" :key="subMenu.menuid" :index="subMenu.url"
:style="subMenu.childList ? 'display: none' : null">
<!-- <AppIcon v-if="subMenu.icon" :name="subMenu.icon" :size="iconSize" /> -->
<span>{{ subMenu.childList ? null : subMenu.menuname }}</span>
</el-menu-item>
<!-- 三级菜单 -->
<el-sub-menu v-for="subMenu in menu.childList.filter(x => x.childList && x.childList.length > 0)" :key="subMenu.menuid"
class="child-sub-menu" :index="subMenu.url">
<template #title>
<!-- icon -->
<!-- <AppIcon v-if="subMenu.icon" :name="subMenu.icon" :size="iconSize" /> -->
<span v-if="subMenu.menuname">{{ subMenu.menuname }}</span>
</template>
<el-menu-item v-for="thirdMenu in subMenu.childList" :key="thirdMenu.menuid"
:index="thirdMenu.url">
<!-- <AppIcon v-if="thirdMenu.icon" :name="thirdMenu.icon" :size="iconSize" /> -->
<span>{{ thirdMenu.menuname }}</span>
</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<!-- 一级单层菜单 -->
<el-menu-item v-else :index="menu.url">
<!-- <AppIcon v-if="menu.icon" :name="menu.icon" :size="iconSize" /> -->
<span>{{ menu.menuname }}</span>
</el-menu-item>
</template>
</el-menu>
</template>
<style lang="scss" scoped>
:deep(.el-sub-menu .el-menu-item){
min-width: auto !important;
}
</style>
3.整体layout布局
这里采用的是element-plus的布局
// layoutIndex.vue
<template>
<el-container>
<el-header>
<CommonHeader />
</el-header>
<el-container>
<el-aside>
<LeftNav />
</el-aside>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<style lang="scss" scoped>
.el-header {
background: #FFFFFF;
box-shadow: 0px 1px 5px 0px rgba(180, 204, 222, 0.5);
z-index: 1;
}
.el-aside {
width: 200px !important;
min-height: 100vh;
font-size: 16px;
background-color: #ffffff !important;
@include hideScrollbar();
}
.el-main {
padding: 20px;
}
.el-container {
width: 100%;
height: 100vh;
}
</style>
layout搭好之后,就可在路由文件中引入layoutIndex.vue作为父组件啦
使用
1.创建页面
src/views目录下存放我们的vue页面,以首页为例
新建src/views/home/HomeIndex.vue
// HomeIndex.vue
<script lang="ts" setup>
</script>
<template>
<div class="homeWrap">
首页
</div>
</template>
<style lang="scss" scoped>
</style>
2.为新增的页面添加路由
src/router/modules下新建home.ts
// home.ts
import type { RouteRecordRaw } from 'vue-router'
const homeRouter: RouteRecordRaw[] = [
{
path: '/home',
component: () => import('@/layout/layoutIndex.vue'),
children: [
{
path: '/index',
component: () => import('@/views/home/HomeIndex.vue'),
meta: {
title: '首页',
module: 'HomeIndex',
hidden: false
}
}
]
}
]
export default homeRouter
但是这个时候发现,明明自动引入了ref等api,但是eslint却报错
解决方法:
在刚才的 vue.config.js 文件中修改:
AutoImport({
dirs: ['src/utils'],
dts: 'src/auto-imports.d.ts',
imports: ['vue', 'vue-router'],
eslintrc: {
enabled: true, // 默认false, true启用。生成一次就可以,避免每次工程启动都生成
filepath: './.eslintrc-auto-import.json', // 生成json文件,eslintrc中引入
globalsPropValue: true
}
})
保存之后重启项目会随即在根目录下生成 .eslintrc-auto-import.json 文件
如果没有在项目目录里找到.eslintrc文件的话,那eslint的配置应该就在package.json文件中
然后将 .eslintrc-auto-import.json 文件引入package.json文件中
"./.eslintrc-auto-import.json"
位置如下:
重启项目,eslint报错消失
记得把App.vue文件中的官方自带的东西删掉,留下这些即可
浏览器输入http://localhost:8080/#/index
框架就搭完了~
九、路由守卫
需求是,未登录状态下,地址栏输入除登录以及白名单之外的路由无法进入,会被强制到登录页。一般登录后会有一个token,我们将token存入pinia,通过token是否有值判断是否拦截路由跳转。
1.store/modules目录下新建user.ts存放登录后获取到的用户信息以及token等
// user.ts
import { defineStore } from 'pinia'
interface UserInfoType {
userName?: string
}
export const userStore = defineStore('user', {
state () {
return {
token: '',
userInfo: {}
}
},
getters: {
getToken (state: { token: string }): string {
return state.token
},
getUserInfo (state: { userInfo: object }): UserInfoType {
return state.userInfo
}
},
actions: {
setToken (value: string) {
this.token = value
},
setUserInfo (value: object) {
this.userInfo = value
},
resetUserInfo () {
this.token = ''
this.userInfo = {}
}
},
persist: {
enabled: true,
strategies: [{ storage: sessionStorage, paths: ['token', 'userInfo'] }]
}
})
2.安装nprogress进度条
cnpm i --save nprogress
cnpm i --save-dev @types/nprogress
3.src/router目录下新建perssion.ts文件,nprogress报错找不到模块不要急,等他反应一会或者文件关了重新点开就好了。
import router from '@/router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import { userStore } from '@/store/modules/user'
NProgress.configure({
easing: 'ease', // 动画方式
showSpinner: true, // 是否显示右上角加载icon
trickleSpeed: 200, // 自动递增间隔
minimum: 0.4 // 更改启动时使用的最小百分比
})
// 白名单
const whiteList = ['/login', '/redirect']
router.beforeEach((to, form, next) => {
const userInfo = userStore() // 不要全局调用 因为在 main.ts文件中,是先引入permission.ts文件然后再将pinia挂载到app上的
NProgress.start()
console.log('--跳转至:', to, ' userInfo:', userInfo, ' token:', userInfo.getToken)
if (userInfo.getToken) {
// 已登录状态
next()
} else {
// Has no token 未登录
if (whiteList.includes(to.path)) {
// In the free login whitelist, go directly
next()
} else {
// Other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
}
}
})
router.afterEach(() => {
NProgress.done()
})
最后别忘了在main.ts中引入
import '@/router/permission'
前面第八步我们已经新建了一个首页,这个时候再进已经进不去了,因为token值为空
然后可以新建一个登录页(记得自己加路由):
<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus'
import { userStore } from '@/store/modules/user'
import router from '@/router'
// import { interfaceTest } from '@/apis/appBannerApi'
const ruleFormRef = ref<FormInstance>()
const ruleForm = reactive({
account: 'admin',
password: 'admin'
})
const rules = reactive<FormRules>({
account: [
{ required: true, message: 'Please input account', trigger: 'blur' },
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' }
],
password: [
{ required: true, message: 'Please input password', trigger: 'blur' },
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' }
]
})
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid:any, fields:any) => {
if (valid) {
const userInfo = userStore()
console.log('tokenValue')
userInfo.setToken('This is tokenValue') // 由于没有调接口,我们先随便给一个值
// interfaceTest({
// }).then()
router.push('/index') //此时就能跳转成功了
} else {
console.log('error submit!', fields)
}
})
}
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
ruleForm.account = ''
ruleForm.password = ''
}
</script>
<template>
<div class="page-box">
<div class="login-box">
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="rules"
label-width="60px"
class="demo-ruleForm"
status-icon>
<el-form-item label="账户" prop="account">
<el-input v-model="ruleForm.account" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="ruleForm.password" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(ruleFormRef)">
登入
</el-button>
<el-button @click="resetForm(ruleFormRef)">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<style lang="scss" scoped>
.page-box{
// background-image: url('../../assets/images/login/loginImg.png');
// /* 背景图垂直、水平均居中 */
// background-position: center center;
// /* 背景图不平铺 */
// background-repeat: no-repeat;
// /* 当内容高度大于图片高度时,背景图像的位置相对于viewport固定 */
// background-attachment: fixed;
// /* 让背景图基于容器大小伸缩 */
// background-size: cover;
/* 设置背景颜色,背景图加载过程中会显示背景色 */
background-color: #464646;
width: 100%;
height: 100vh;
@include flex(center,center);
.login-box{
width: 520px;
height: 420px;
background: #FFFFFF;
@include flex(center,center);
}
}
</style>
路由:
// src/router/modules/login.ts
import type { RouteRecordRaw } from 'vue-router'
const loginRouter: RouteRecordRaw[] = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/Login.vue'),
meta: {
title: 'login',
module: 'login',
hidden: false
}
}
]
export default loginRouter
登录后就能跳转成功了
十、组件封装
关于组件的封装根据项目不同,自己随意,我仅记录一下我封装的常用的一些组件,均放在components/common目录下,使用时就不用先引入了,可以直接用
1.Icon
<template>
<i class="s-icon">
<img :src="geturl" alt="" :style="styleObject" />
</i>
</template>
<script setup lang="ts">
const props = defineProps({
name: {
type: String,
require: true,
default: ''
},
size: {
type: String,
require: false,
default: '20px'
}
})
const geturl = computed(() => {
if (!props.name) return ''
else if (props.name.includes('/') || props.name.includes('base64')) {
return props.name
} else if (props.name.includes('svg')) {
return require(`@/assets/icons/svg/${props.name}`)
}
return require(`@/assets/icons/png/${props.name}.png`)
})
const styleObject = computed(() => {
return {
width: props.size,
height: props.size
}
})
</script>
<style lang="scss" scoped>
.s-icon {
display: inline-block;
line-height: 1;
overflow: hidden;
img {
float: left;
}
}
</style>
记得创建文件夹哦~
png文件夹内就放.png图片,假设png文件夹内有图片pic.png,使用时:
<AppIcon name="pic" size="24px" />
2.表格+分页
// tableList.vue
<script lang="ts" setup>
import { ElTable } from 'element-plus'
const tableRef = ref<InstanceType<typeof ElTable>>()
type columnVo = {
prop?: string,
label?: string,
width?: string | number,
type?: string
}
const props = defineProps({
tableColumnList: {
type: Array as () => Array<columnVo>,
required: true,
default: () => []
},
tableData: {
type: Array as () => Array<any>,
required: true,
default: () => []
},
pageNum: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
dataTotal: {
type: Number,
require: true,
default: 0
},
multiple: {
type: Boolean,
default: true
},
borderFlag: {
type: Boolean,
default: true
},
loading: Boolean,
isOperate: {
type: Boolean,
default: true
},
})
const currentPage = computed(() => props.pageNum)
const emit = defineEmits(['handleSelectionChange', 'getData', 'checkClick', 'editClick'])
const handleSelectionChange = (val: []) => {
emit('handleSelectionChange', val)
}
// 单行点击事件
const getData = (val: []) => {
emit('getData', val)
}
const handleCurrentChange = (val: number) => {
console.log(`current page: ${val}`)
}
</script>
<template>
<div class="tableBox">
<el-table ref="tableRef" :data="props.tableData" :border="props.borderFlag" stripe style="width: 100%" v-loading="loading" @selection-change="handleSelectionChange"
@row-click="getData"
>
<!-- 第一列选中框 -->
<el-table-column
v-if="props.multiple"
type="selection"
width="55"
/>
<el-table-column
v-for="(item, index) in props.tableColumnList"
:key="index"
:prop="item.prop"
:label="item.label"
:width="item.width"
:type="item.type"
>
<template #default="scope">
<!-- 该列是否展示为图片 -->
<div v-if="item.prop === 'img'">
<img
class="table_icon"
:src="scope.row.img"
alt=""
>
</div>
</template>
</el-table-column>
<el-table-column
fixed="right"
:label="$t('common.operate')"
width="200"
>
<template
#default="scope"
>
<div v-if="isOperate">
<el-button
link
type="primary"
size="small"
@click="$emit('checkClick', scope.row)"
>
{{ $t('common.view') }}
</el-button>
<el-button
link
type="primary"
size="small"
@click="$emit('editClick', scope.row)"
v-if="scope.row.entranceType !== null"
>
{{ $t('common.edit') }}
</el-button>
</div>
<slot
name="operate"
v-if="!isOperate"
:message="scope.row"
/>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination v-model:current-page="currentPage"
:page-size="props.pageSize" @current-change="handleCurrentChange" background layout="prev, pager, next, jumper, total, slot" :total="dataTotal">
<span class="pageSlot">显示{{ (currentPage - 1) * 10 + 1 }}-{{ Math.min(currentPage * 10, dataTotal) }}条</span>
</el-pagination>
</div>
</div>
</template>
<style lang="scss" scoped>
.pagination{
display: flex;
justify-content: end;
margin-top: 20px;
.pageSlot{
margin-left: 10px;
}
}
:deep(.el-table--striped .el-table__body tr.el-table__row--striped td) {
background: #EAF4FF; // 斑马纹底色
}
:deep(.el-pagination.is-background .el-pager li:not(.disabled).active) {
background-color: #D4E8FD; // 分页背景颜色
color: #fff;
}
:deep(.el-table__inner-wrapper){
width: 100%;
.el-table__header{
.el-table__cell{
background-color: #D4E8FD;
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #242424;
text-align: center;
}
}
.el-table__body{
.el-table__cell{
font-size: 14px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #737B8B;
text-align: center;
}
.el-button{
font-size: 12px;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
color: #216FED;
margin: 0 10px;
}
.table_icon{
width: 30px;
height: 30px;
}
}
}
</style>
完整vue.config.js文件
const path = require('path')
const { defineConfig } = require('@vue/cli-service')
// 组件自动加载
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
const { VUE_APP_BASE_URL } = process.env
module.exports = defineConfig({
transpileDependencies: true,
// 自动按需加载
configureWebpack: {
plugins: [
AutoImport({
dirs: ['src/utils'],
dts: 'src/auto-imports.d.ts',
imports: ['vue', 'vue-router'],
eslintrc: {
enabled: true, // 默认false, true启用。生成一次就可以,避免每次工程启动都生成
filepath: './.eslintrc-auto-import.json', // 生成json文件,eslintrc中引入
globalsPropValue: true
}
}),
Components({
resolvers: [ElementPlusResolver()],
dirs: ['src/components', 'src/layout'],
dts: 'src/components.d.ts'
// 允许子目录作为组件的命名空间前缀。
// directoryAsNamespace: true
})
],
resolve: {
extensions: ['.js', '.vue', '.json', '.ts'],
alias: {
'@': path.resolve(__dirname, 'src/')
}
}
},
css: {
// 开启 CSS source maps?
sourceMap: false,
loaderOptions: {
less: {
charset: false
},
scss: {
additionalData: `
@import "./src/assets/style/_mixins.scss";
`
}
},
extract: {
ignoreOrder: true
}
},
devServer: {
host: '0.0.0.0',
port: 8080,
// open: true, // 自动打开浏览器
proxy: {
'/api': {
target: VUE_APP_BASE_URL, // 目标代理接口地址
secure: false,
changeOrigin: true, // 开启代理,在本地创建一个虚拟服务端
// ws: true, // 是否启用websockets
logLevel: 'debug',
onProxyReq (proxyReq, req, res) {
console.log('[HPM] %s %s %s %s', req.method, req.originalUrl, '->', VUE_APP_BASE_URL)
console.log('[HPM] Rewriting path from "%s" to "%s"', req.originalUrl, req.url)
},
pathRewrite: {
'^/api': ''
}
}
}
}
})
完整main.ts文件
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import '@/router/permission'
import pinia from './store'
import i18n from './locales'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import zhTw from 'element-plus/es/locale/lang/zh-tw'
import en from 'element-plus/es/locale/lang/en'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(pinia)
app.use(router)
app.use(i18n)
app.use(ElementPlus, {
locale: getLanguage() === 'tc' ? zhTw : getLanguage() === 'sc' ? zhCn : en
})
app.mount('#app')
踩坑:
1.使用defineProps时,eslint报错:‘defineProps’ is not defined
原因分析:
Script setup标签,其中部分属性在高版本中,会默认导入,无需再手动import
解决方法:
在eslint的配置文件中(我的是在package.json中),新增以下代码,并重启项目
"vue/setup-compiler-macros": true
2.Component name “Login“ should always be multi-word.
我给登录页文件命名为Login.vue报了这个错,要求驼峰命名法,也就是说至少要两个单词,但登录页我就想叫login.vue
解决方法:
在eslint的配置文件中(我的是在package.json中),新增以下代码,并重启项目
"vue/multi-word-component-names":"off"
3.Parsing error: Unexpected token. Did you mean {'}'}
or }
?
对着报错信息一顿百度,装了一些eslint相关的依赖,比如:
(错的!)下载 babel-eslint 插件,在package.json中配置 eslintConfig 属性
cnpm install babel-eslint --save
结果都不生效,还出现了更离谱的错误,最后我把我定义的数据重新敲一遍发现是因为多了逗号,但是eslint的提醒却在末尾!把逗号删了就好了。