巨详细 vue3-ts-pinia-vue-router-eslint-elementPlus-i18n-webpack后台管理平台框架从头搭建(包含自动引入api,批量注册组件,路由模块化等)

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 &rbrace;?

在这里插入图片描述

对着报错信息一顿百度,装了一些eslint相关的依赖,比如:

(错的!)下载 babel-eslint 插件,在package.json中配置 eslintConfig 属性

cnpm install babel-eslint --save

结果都不生效,还出现了更离谱的错误,最后我把我定义的数据重新敲一遍发现是因为多了逗号,但是eslint的提醒却在末尾!把逗号删了就好了。

附源代码

gitee源代码

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值