当前位置:   article > 正文

Vue3.2后台管理系统(Element-plus)学习笔记_vue3.2+elementplus管理系统怎么运行

vue3.2+elementplus管理系统怎么运行

1 前言

项目所需要的 资源——点击下载 

Vue3.2版实现的基于element-plus的表单增删改查和部分内容

主要实现的功能有:

    中英文切换

    全屏

    引导页

    表单的CRUD

软件安装:

  安装chrome

  安装vscode

  下载安装 nodejs

  打开命令行

  npm install -g @vue/cli

  vue --version

2 创建项目

  vue ui

  创建

  在此创建新项目

  填写项目名称

  下一步

  手动

  下一步

  [Choose Vue version]

  Babel

  Router

  Vuex

  Css Pre-processors

  使用配置文件

  下一步

  选择vue版本: 3.x

  Pick a CSS pre-process: 选择 Sass/SCSS

  Pick a linter/formatter config: 选择 ESLint + Standard config

  创建项目

  输入预设名,保存预设并创建项目

  安装依赖

    运行依赖 axios,

  打开Vscode,打开文件夹,选择刚刚创建的项目文件夹 vue_3.2。

  打开 package.json,查看vue的版本,如果不是 3.2+的话,需要重新安装一下。

  npm I vue@3.2.8 vue-router@4.0.11 vuex@4.0.2

  npm i

  运行项目:

npm run serve

  能正常在浏览器中打开项目,说明项目创建完成

3 代码格式化

vscode 打开文件夹

安装 prettier Code Formatter

设置 搜索save ,选中 Editor:Format On Save

打开 App.vue,右键,使用...格式化文档,

  上面选择“配置默认格式化程序”,选择 Prettier - Code formatter

新建文件 .prettierrc

内容为:

{

"semi": false,

"singleQuote": true,

"trailingComma": "none"

}

双击打开 .eslintrc.js,修改,以避免冲突:

  rules: {

'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',

'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',

'indent': 0,

'space-before-function-paren': 0

  }

4 commit规范

5 强制commit

6 强制代码规范

7 按需导入 elementplus

安装 elementui库

npm install element-plus --save

安装出错时执行:

npm install element-plus --save --legacy-peer-deps

设置按需自动导入:

npm install -D unplugin-vue-components unplugin-auto-import --legacy-peer-deps

新建vue.config.js并将以下内容整合到 vue.config.js

//webpack.config.js

const AutoImport = require('unplugin-auto-import/webpack')

const Components = require('unplugin-vue-components/webpack')

const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')

module.exports = {

  //...

  plugins: [

    AutoImport({

      resolvers: [ElementPlusResolver()],

    }),

    Components({

      resolvers: [ElementPlusResolver()],

    })

  ]

}

结果为:

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')

module.exports = defineConfig({

  transpileDependencies: true,

  configureWebpack: config =>{

    config.plugins.push(

      AutoImport({

        resolvers: [ElementPlusResolver()],

      })

    )

    config.plugins.push(

      Components({

        resolvers: [ElementPlusResolver()],

      })

    )

  }

})

浏览器中打开element-plus网站,组件:

Button 按钮 | Element Plus

将其中的一组按钮代码复制粘贴到 App.vue 中:

    <el-button>Default</el-button>

    <el-button type="primary">Primary</el-button>

    <el-button type="success">Success</el-button>

    <el-button type="info">Info</el-button>

    <el-button type="warning">Warning</el-button>

<el-button type="danger">Danger</el-button>

App.vue修改后:

<template>

  <div>

    <el-button>Default</el-button>

    <el-button type="primary">Primary</el-button>

    <el-button type="success">Success</el-button>

    <el-button type="info">Info</el-button>

    <el-button type="warning">Warning</el-button>

    <el-button type="danger">Danger</el-button>

  </div>

  <!-- <router-view /> -->

</template>

<script setup></script>

<style lang="scss"></style>

浏览器中刷新项目页面,如果有element-plus 相关的按钮出现,则说明配置成功

8 vue3.2新特性

  • template 里可以有多个根元素了
  • script要使用 <script setup></script>

vue3.2的模板为:

<template>

</template>

<script setup>

</script>

<style lang=”scss” scoped>

</style>

  • 参数可绑定到css里:

<template>

<div class="box"></div>

<router-view />

</template>

<script setup>

const width = '100px'

</script>

<style lang="scss">

.box {

  width: v-bind(width);

  height: 100px;

  background-color: red;

}

</style>

9 初始化项目

src下放置 styles 文件夹,其中包含index.scss,variables.scss等

main.js 中引入 index.scss

import '@/styles/index.scss';

views 下新建文件夹 login,其中新建文件 index.vue

<template>

<div>login</div>

</template>

<script setup></script>

<style lang="scss" scoped></style>

修改路由router下的 index.js,加入login

const routes = [

  {

    path: '/login',

    name: 'Login',

    component: () => import('../views/login')

  }

]

保存,提示 error Component name “Login“ should always be multi-word

在 vue.config.js 的 export 中增加:

lintOnSave:false,

  备选:.eslintrc.js 中增加:

'vue/multi-word-component-names': 0

重启项目

10 登录页面静态

注意其中的双向绑定 :model=”form”

在script setup中要进行声明

import {ref} from ‘vue

const form = ref({

  username: '',

  password: ''

})

<template>

  <div class="login-container">

    <el-form ref="formRef" :model="form" class="login-form" :rules="rules">

      <div class="title-container">

        <h3 class="title">用户登录</h3>

      </div>

      <el-form-item prop="username">

        <!-- <el-icon :size="20" class="svg-container">

          <edit />

        </el-icon> -->

        <svg-icon icon="user" class="svg-container"></svg-icon>

        <el-input v-model="form.username"></el-input>

      </el-form-item>

      <el-form-item prop="password">

        <svg-icon icon="password" class="svg-container"></svg-icon>

        <el-input :type="passwordType" v-model="form.password"></el-input>

        <svg-icon

          :icon="passwordType == 'password' ? 'eye' : 'eye-open'"

          class="svg-container"

          style="cursor: pointer"

          @click="changeType"

        ></svg-icon>

      </el-form-item>

      <el-button type="primary" class="login-button" @click="handleLogin"

        >登录</el-button

      >

    </el-form>

  </div>

</template>

<script setup>

import { ref } from 'vue'

//import { Edit, Search, User, Lock } from '@element-plus/icons-vue'

//import { login } from '@/api/login'

import { useStore } from 'vuex'

import { ElMessage } from 'element-plus'

const store = useStore()

const form = ref({

  username: '',

  password: ''

})

const rules = ref({

  username: [

    { required: true, message: 'Please input Activity name', trigger: 'blur' },

    { min: 3, max: 25, message: 'Length should be 3 to 25', trigger: 'blur' }

  ],

  password: [

    { min: 3, max: 25, message: 'Length should be 3 to 5', trigger: 'blur' }

  ]

})

const passwordType = ref('password')

const formRef = ref(null)

const handleLogin = async () => {

  await formRef.value.validate(async (valid, fields) => {

    if (valid) {

      store.dispatch('app/login', form.value)

    } else {

      console.log('error submit!', fields)

    }

  })

}

const changeType = () => {

  if (passwordType.value == 'password') {

    passwordType.value = 'text'

  } else {

    passwordType.value = 'password'

  }

}

</script>

<style lang="scss" scoped>

$bg: #2d3a4b;

$dark_gray: #889aa4;

$light_gray: #eee;

$cursor: #fff;

.login-container {

  min-height: 100%;

  width: 100%;

  background-color: $bg;

  overflow: hidden;

  .login-form {

    position: relative;

    width: 520px;

    max-width: 100%;

    padding: 160px 35px 0;

    margin: 0 auto;

    overflow: hidden;

    :deep(.el-form-item) {

      border: 1px solid rgba(255, 255, 255, 0.1);

      background: rgba(0, 0, 0, 0.1);

      border-radius: 5px;

      color: #454545;

    }

    :deep(.el-input) {

      display: inline-block;

      height: 47px;

      width: 85%;

      .el-input__wrapper {

        background: transparent;

        box-shadow: 0 0 0 0;

        border: 0px;

        -webkit-appearance: none;

        border-radius: 0px;

        padding: 12px 5px 12px 15px;

        color: $light_gray;

        height: 47px;

        caret-color: $cursor;

      }

    }

    .login-button {

      width: 100%;

      box-sizing: border-box;

    }

  }

  .tips {

    font-size: 16px;

    line-height: 28px;

    color: #fff;

    margin-bottom: 10px;

    span {

      &:first-of-type {

        margin-right: 16px;

      }

    }

  }

  .svg-container {

    padding: 6px 5px 6px 15px;

    color: $dark_gray;

    vertical-align: middle;

    display: inline-block;

  }

  .title-container {

    position: relative;

    .title {

      font-size: 26px;

      color: $light_gray;

      margin: 0px auto 40px auto;

      text-align: center;

      font-weight: bold;

    }

    ::v-deep .lang-select {

      position: absolute;

      top: 4px;

      right: 0;

      background-color: white;

      font-size: 22px;

      padding: 4px;

      border-radius: 4px;

      cursor: pointer;

    }

  }

  .show-pwd {

    // position: absolute;

    // right: 10px;

    // top: 7px;

    font-size: 16px;

    color: $dark_gray;

    cursor: pointer;

    user-select: none;

  }

}

</style>

11 svg-icon

资源里的icons拖放到src下

components下创建文件夹SvgIcon, 其下创建文件 index.vue

<template>

  <svg class="svg-icon" aria-hidden="true">

    <use :xlink:href="iconName"></use>

  </svg>

</template>

<script setup>

import { defineProps, computed } from 'vue'

const props = defineProps({

  icon: { //给父组件使用,传入值

    type: String,

    required: true

  }

})

const iconName = computed(() => {

  return `#icon-${props.icon}`

})

</script>

<style lang="scss">

.svg-icon {

  width: 1em;

  height: 1em;

  vertical-align: -0.15em;

  fill: currentColor;

  overflow: hidden;

}

</style>

icons 下创建index.js,将导入的svg文件,并注册组件

import SvgIcon from '@/components/SvgIcon'

const svgRequired = require.context('./svg', false, /\.svg$/)

svgRequired.keys().forEach((item) => svgRequired(item))

export default (app) => {

  app.component('svg-icon', SvgIcon)

}

main.js中导入创建的SvgIcon:

import SvgIcon from ‘@/icons’

const app = createApp(App)

SvgIcon(app)

app.use(store).use(router).mount(‘#app’)

安装svg-loader

npm I –save-dev svg-sprite-loader@6.0.9

WebPack配置 vue.config.js:

module.exports 中增加:

chainWebpack(config) {

    // 设置 svg-sprite-loader

    // config webpack 配置对象

    // config.module 表示创建一个具名规则,以后用来修改规则

    config.module

      // 规则

      .rule('svg')

      // 忽略

      .exclude.add(resolve('src/icons'))

      // 结束

      .end()

    // config.module 表示创建一个具名规则,以后用来修改规则

    config.module

      // 规则

      .rule('icons')

      // 正则,解析 .svg 格式文件

      .test(/\.svg$/)

      // 解析的文件

      .include.add(resolve('src/icons'))

      // 结束

      .end()

      // 新增了一个解析的loader

      .use('svg-sprite-loader')

      // 具体的loader

      .loader('svg-sprite-loader')

      // loader 的配置

      .options({

        symbolId: 'icon-[name]'

      })

      // 结束

      .end()

    config

      .plugin('ignore')

      .use(new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn$/))

    config.module

      .rule('icons')

      .test(/\.svg$/)

      .include.add(resolve('src/icons'))

      .end()

      .use('svg-sprite-loader')

      .loader('svg-sprite-loader')

      .options({

        symbolId: 'icon-[name]'

      })

      .end()

  },

module.export 上面,外面,增加一些引入:

const path = require('path')

function resolve(dir) {

  return path.join(__dirname, dir)

}

const webpack = require('webpack')

使用,login\index.vue中:

      <el-form-item prop="username">

        <!-- <el-icon :size="20" class="svg-container">

          <edit />

        </el-icon> -->

        <svg-icon icon="user" class="svg-container"></svg-icon>

        <el-input v-model="form.username"></el-input>

      </el-form-item>

      <el-form-item prop="password">

        <svg-icon icon="password" class="svg-container"></svg-icon>

        <el-input :type="passwordType" v-model="form.password"></el-input>

        <svg-icon

          :icon="passwordType == 'password' ? 'eye' : 'eye-open'"

          class="svg-container"

          style="cursor: pointer"

          @click="changeType"

        ></svg-icon>

      </el-form-item>

重启项目

12 表单验证

login/index.vue的script setup中增加:

const rules = ref({

  username: [

    { required: true, message: 'Please input Activity name', trigger: 'blur' },

    { min: 3, max: 25, message: 'Length should be 3 to 25', trigger: 'blur' }

  ],

  password: [

    { min: 3, max: 25, message: 'Length should be 3 to 5', trigger: 'blur' }

  ]

})

然后对template中el-form,增加:

:rules="rules"

对 el-form-item 增加prop:

      <el-form-item prop="username">

      <el-form-item prop="password">

对登录按钮增加单击后统一校验:

对登录按钮增加单击事件:

@click="handleLogin"

script setup中,增加handleLogin方法:

const formRef = ref(null)

const handleLogin = async () => {

  await formRef.value.validate(async (valid, fields) => {

    if (valid) {

      store.dispatch('app/login', form.value)

    } else {

      console.log('error submit!', fields)

    }

  })

}

因为要使用验证,就要用到表单,这里使用在el-form上定义的ref值formRef。

13 发起登录请求

创建目录api,其中创建文件 request.js:

import axios from 'axios'

import { ElMessage } from 'element-plus'

import { setTokenTime, diffTokenTime } from '@/utils/auth'

import router from '@/router'

//import store from '@/store'

const service = axios.create({

  baseURL: process.env.VUE_APP_BASE_API,

  timeout: 5000

})

service.interceptors.response.use(

  (response) => {

    //console.log(response)

    const { data, meta } = response.data

    if (meta.status === 200 || meta.status === 201) {

      data.status = 0

      return data

    } else {

      ElMessage.error(meta.msg)

      data.status = -1

      return Promise.reject(new Error(meta.msg))

      //return data

    }

  },

  (error) => {

    console.log('error.response=', error.response)

    if (error.response == undefined) {

      return //Promise.reject(new Error('error...'))

    }

    error.response && ElMessage.error(error.response.data)

    return Promise.reject(new Error(error.response.data))

  }

)

export default service

根目录下创建 .env.development

ENV = 'development'

VUE_APP_BASE_API = '/api'

根目录下创建 .env.production

ENV = 'production'

VUE_APP_BASE_API = '/prod-api'

跨域问题,vue.config.js

module.exportes中增加:

  devServer: {

    https: false,

    //hotOnly: false, //如果运行报错 注释这一行

    proxy: {

      '/api': {

        target: 'http://localhost:5000', //'https://lianghj.top:8888/api/private/v1/',

        changeOrigin: true,

        pathRewrite: {

          '^/api': ''

        }

      }

    }

  },

这里的使用的5000是在本地架设的json-server,具体操作如下:

1.全局安装json-server

D盘下新建文件夹 json-server

打开命令行

npm install -g json-server

2.创建文件夹 __json_server_mock__/db.json

3.在该目录下创建middleware.js,也就是我们需要的登录中间件,复制以下内容

module.exports = (req, res, next) => {

  if (req.method === 'POST' && req.path === '/login') {

    if (req.body.username === 'admin' && req.body.password === '123456') {

      return res.status(200).json({

        meta: {

          msg: '登陆成功',

          status: 200

        },

        user: {

          token: '123'

        },

        data: {

          id: 1

        }

      })

    } else {

      return res.status(400).json({ message: '用户名或者密码错误' })

    }

  }

  next()

}

 后面根据情况可以增加其他接口,如 menus, user, roles, rights 等

4. Vue项目的vue.config.js文件中把代理的target修改为http://localhost:5000 (也就是接口地址改为本地)

5. D:\json-server下,新建package.json

{

  "name": "vue3.2",

  "version": "0.1.0",

  "private": true,

  "scripts": {

    "json-api": "json-server __json_server_mock__/db.json --watch --port 5000 --middlewares ./__json_server_mock__/middleware.js"

  }

}

6. npm run json-api

7.打开login页面,尝试登陆

api文件夹下创建login.js文件:

import request from './request'

export const login = (data) => {

  return request({

    url: '/login',

    method: 'POST',

    data

  })

}

views/login/index.vue中增加引用,并在登录按钮的单击事件方法中调用:

import { login } from '@/api/login'

const handleLogin = async () => {

  await formRef.value.validate(async (valid, fields) => {

    if (valid) {

      const res= await login(form.value)

      console.log(‘res=’,res)

    } else {

      console.log('error submit!', fields)

    }

  })

}

重启,登录 admin/123456

14 响应拦截器

登录页面,密码框输入的明文,暗文切换:

      <el-form-item prop="password">

        <svg-icon icon="password" class="svg-container"></svg-icon>

        <el-input :type="passwordType" v-model="form.password"></el-input>

        <svg-icon

          :icon="passwordType == 'password' ? 'eye' : 'eye-open'"

          class="svg-container"

          style="cursor: pointer"

          @click="changeType"

        ></svg-icon>

      </el-form-item>

passwordType声明:

const passwordType = ref('password')

响应拦截api/request.js:

简化从res.data再取data

import { ElMessage } from 'element-plus'

service.interceptors.response.use(

  (response) => {

    //console.log(response)

    const { data, meta } = response.data

    if (meta.status === 200 || meta.status === 201) {

      data.status = 0

      return data

    } else {

      ElMessage.error(meta.msg)

      data.status = -1

      return Promise.reject(new Error(meta.msg))

      //return data

    }

  },

  (error) => {

    console.log('error.response=', error.response)

    if (error.response == undefined) {

      return //Promise.reject(new Error('error...'))

    }

    error.response && ElMessage.error(error.response.data)

    return Promise.reject(new Error(error.response.data))

  }

)

token的获取与保存:

stroe/index.js,删除export default 中的除 modules以外的内容

import { createStore } from 'vuex'

export default createStore({

  modules: {

  },

})

创建 store/modules文件夹,其中创建app.js:

import { login as loginApi } from '@/api/login'

import router from '@/router'

import { setTokenTime } from '@/utils/auth'

export default {

  namespaced: true,

  state: () => ({

    token: localStorage.getItem('token')

  }),

  mutations: {

    setToken(state, token) {

      state.token = token

      localStorage.setItem('token', token)

    }

  },

  actions: {

    login({ commit }, userInfo) {

      return new Promise((resolve, reject) => {

        loginApi(userInfo)

          .then((res) => {

            setTokenTime()

            commit('setToken', res.token)

            router.replace('/')

            resolve()

          })

          .catch((err) => {

            reject(err)

          })

      })

}

  }

}

store/index.js中导入app.js

import { createStore } from 'vuex'

import app from './modules/app'

import getters from './getters'

export default createStore({

  modules: {

    app

  },

  getters

})

15 登录

views/login/index.vue

import { useStore } from 'vuex'

import { ElMessage } from 'element-plus'

……

const store = useStore()

const handleLogin = async () => {

  await formRef.value.validate(async (valid, fields) => {

    if (valid) {

      store.dispatch('app/login', form.value)

    } else {

      console.log('error submit!', fields)

    }

  })

}

16 请求拦截器

对每个请求都在头部增加token

app/request.js

service.interceptors.request.use(

  (config) => {

    config.headers.Authorization = localStorage.getItem('token')

    return config

  },

  (error) => {

    return Promise.reject(new Error(error))

  }

)

17 路由守卫

用户必须在登录之后才能转到首页,在未登录时,任何路由都转向登录页面

创建文件 router/promssion.js

import router from './index'

import store from '@/store'

const whiteList = ['/login']

router.beforeEach((to, from, next) => {

  if (store.getters.token != undefined && store.getters.token != '' && store.getters.token != ‘undefined’) {

    console.log('token=', store.getters.token)

    if (to.path === '/login') {

      next('/')

    } else {

      next()

    }

  } else {

    if (whiteList.includes(to.path)) {

      next()

    } else {

      next('/login')

    }

  }

})

在store/index.js增加导入,并使用getters:

import { createStore } from 'vuex'

import app from './modules/app'

import getters from './getters'

export default createStore({

  modules: {

    app

  },

  getters

})

18 layout

新建 src/layout/index.vue

复制来自 element-plus官网组件页面的布局代码

  <el-container class="app-container">

    <el-aside width="200px" class="sidebar-container">

      <Menu />

    </el-aside>

    <el-container class="container">

      <el-header>Header</el-header>

      <el-main>

        <router-view />

      </el-main>

    </el-container>

  </el-container>

增加样式,全局导入,vue.config.js的modules.export中增加:

  css: {

    loaderOptions: {

      sass: {

        //additionalData prependData:   // scss loader 8版本及以下用prependData:

        additionalData: `

          @import "@/styles/variables.scss";  // scss文件地址

          @import "@/styles/mixin.scss";     // scss文件地址

        `

      }

    }

  }

在src/routers/index.js中进行修改:

      {

        path: '/',

        name: '/',

        component: () => import('@/views/layout')

      },

重新启动

19 menus菜单

创建文件、文件夹 src/layout/Menu/index.vue

<template>

  <el-menu

    active-text-color="#ffd04b"

    background-color="#545c64"

    class="el-menu-vertical-demo"

    text-color="#fff"

    :default-active="defaultActive"

    router

    unique-opened

  >

    <el-sub-menu

      :index="item.id + ''"

      v-for="(item, index) of menusList"

      :key="item.id"

    >

      <template #title>

        <el-icon>

          <component :is="iconList[index]"></component>

        </el-icon>

        <span>{{ item.authName }}</span>

      </template>

      <el-menu-item

        :index="'/' + it.path"

        v-for="it of item.children"

        :key="it.id"

        @click="savePath(it.path)"

      >

        <template #title>

          <el-icon>

            <component :is="icon"></component>

          </el-icon>

          <span>{{ it.authName }}</span>

        </template>

      </el-menu-item>

    </el-sub-menu>

  </el-menu>

</template>

<script setup>

import { ref } from 'vue'

import { menuList } from '@/api/menu'

// 默认选中 /users,选中其他的,再刷新,则选中刷新前的路径

const defaultActive = ref(sessionStorage.getItem('path') || '/users')

const savePath = (path) => {

  sessionStorage.setItem('path', `/${path}`)

}

const iconList = ref(['user', 'setting', 'shop', 'tickets', 'pie-chart'])

const icon = ref('menu')

const menusList = ref([])

const initMenusList = async () => {

  const res = await menuList()

  menusList.value = res

  console.log('res= ', res)

}

initMenusList()

</script>

<style lang="scss" scoped></style>

发起请求,拿到数据,新建src/api/menu.js:

import request from './request'

export const menuList = () => {

  return request({ //因为是get请求,所以不用特别写method

    url: 'menus'

  })

}

menu数据:

{

  "data": [

    {

      "id": 125,

      "authName": "用户管理",

      "path": "users",

      "children": [

        {

          "id": 110,

          "authName": "用户列表",

          "path": "users",

          "children": [

           

          ],

          "order": null

        }

      ],

      "order": 1

    },

    {

      "id": 103,

      "authName": "权限管理",

      "path": "rights",

      "children": [

        {

          "id": 111,

          "authName": "角色列表",

          "path": "roles",

          "children": [

           

          ],

          "order": null

        },

        {

          "id": 112,

          "authName": "权限列表",

          "path": "rights",

          "children": [

           

          ],

          "order": null

        }

      ],

      "order": 2

    },

    {

      "id": 101,

      "authName": "商品管理",

      "path": "goods",

      "children": [

        {

          "id": 104,

          "authName": "商品列表",

          "path": "goods",

          "children": [

           

          ],

          "order": 1

        },

        {

          "id": 115,

          "authName": "分类参数",

          "path": "params",

          "children": [

           

          ],

          "order": 2

        },

        {

          "id": 121,

          "authName": "商品分类",

          "path": "categories",

          "children": [

           

          ],

          "order": 3

        }

      ],

      "order": 3

    },

    {

      "id": 102,

      "authName": "订单管理",

      "path": "orders",

      "children": [

        {

          "id": 107,

          "authName": "订单列表",

          "path": "orders",

          "children": [

           

          ],

          "order": null

        }

      ],

      "order": 4

    },

    {

      "id": 145,

      "authName": "数据统计",

      "path": "reports",

      "children": [

        {

          "id": 146,

          "authName": "数据报表",

          "path": "reports",

          "children": [

           

          ],

          "order": null

        }

      ],

      "order": 5

    }

  ],

  "meta": {

    "msg": "获取菜单列表成功",

    "status": 200

  }

}

使用全局定义的菜单底色,需要修改layout/Menu/index.vue:

<template>

  <el-menu

    active-text-color="#ffd04b"

    :background-color="variables.menuBg"

    class="el-menu-vertical-demo"

    text-color="#fff"

    :default-active="defaultActive"

    router

    unique-opened

  >

然后在 script setup里导入:

import variables from '@/styles/variables.scss'

如果报SassError: This file is already being loaded. 错误,则要修改 vue.config.js

      sass: {

        //additionalData prependData:   // scss loader 8版本及以下用prependData:

        /* additionalData: `

          @import "@/styles/variables.scss";  // scss文件地址

          @import "@/styles/mixin.scss";     // scss文件地址

        ` */

        additionalData: (content, loaderContext) => {

          const { resourcePath } = loaderContext

          if (resourcePath.endsWith('variables.scss')) return content

          return `@import "@/styles/variables.scss";

          ${content}`

        }

      }

重启。

如果没有看到左侧菜单栏底色改变,则表明引入失败,需要将 styles/variables.scss重命名为 variables.module.scss,并且所有引用、使用到它的地方,都要做相应的修改。

再重启。

菜单路由设置:

从资料中复制路由页面文件夹中除login外的其他文件夹,粘贴到views下

修改router/index.js,增加相应的路由

layout/index.vue修改,增加router-view:

      <el-main>

        <router-view />

      </el-main>

左侧菜单,一次只展开一项:unique-opened

router/index.js里设置了重定向redirect:

const routes = [

  {

    path: '/',

    name: 'home',

    component: layout,

    redirect: '/users',

    children: [

因此要在layout/Menu/index.vue里,设置菜单的默认选中default-active:

<template>

  <el-menu

    active-text-color="#ffd04b"

    :background-color="variables.menuBg"

    class="el-menu-vertical-demo"

    text-color="#fff"

    :default-active="defaultActive"

    router

    unique-opened

  >

并在script setup中声明变量:

const defaultActive = ref(‘/users’)

现在布局中主体显示的跟左侧树中选择的菜单不一致,即defaultActive不能被写死,而要根据菜单切换进行设置:

要先存起来,在子菜单项上增加click事件方法savePath:

      <el-menu-item

        :index="'/' + it.path"

        v-for="it of item.children"

        :key="it.id"

        @click="savePath(it.path)"

      >

在script setup中定义,并对defaultActive定义进行修改:

const defaultActive = ref(sessionStorage.getItem('path') || '/users')

const savePath = (path) => {

  sessionStorage.setItem('path', `/${path}`)

}

左侧菜单中菜单项的左侧增加小图标:

可行方法,不建议:

main.js中,注册图标,以图标名为组件名

import * as ELIcons from '@element-plus/icons-vue'

const app = createApp(App)

for (const iconName in ELIcons) {

  app.component(iconName, ELIcons[iconName])

}

在layout/Menu/index.vue中声明数组,并在菜单项,子菜单项里使用:

const iconList = ref(['user', 'setting', 'shop', 'tickets', 'pie-chart'])

const icon = ref('menu')

<el-sub-menu

      :index="item.id + ''"

      v-for="(item, index) of menusList"

      :key="item.id"

    >

      <template #title>

        <el-icon>

          <component :is="iconList[index]"></component>

        </el-icon>

        <span>{{ item.authName }}</span>

      </template>

      <el-menu-item

        :index="'/' + it.path"

        v-for="it of item.children"

        :key="it.id"

        @click="savePath(it.path)"

      >

        <template #title>

          <el-icon>

            <component :is="icon"></component>

          </el-icon>

          <span>{{ it.authName }}</span>

        </template>

      </el-menu-item>

    </el-sub-menu>

20 被动退出

没有成功请求的一段时间后,自动退出

逻辑是设置两个常量值,一个保存最近请求时间的key名,一个保存闲置时长,以毫秒为单位的两小时。每次请求时,用当前时间减最近请求时间比较,是不是大于闲置时间,如果大于了,则强制退出。否则,更新最近请求时间为当前时间。

创建utils,创建文件constant.js

export const TOKEN_TIME = 'tokenTime'

export const TOKEN_TIME_VALUE = 2 * 60 * 60 * 1000 //两小时过期

创建文件utils/auth.js,定义设置和获取最近请求时间的方法,及过期时间比较

import { TOKEN_TIME, TOKEN_TIME_VALUE } from './constant'

// 登录时设置时间

export const setTokenTime = () => {

  localStorage.setItem(TOKEN_TIME, Date.now())

}

// 获取

export const getTokenTime = () => {

  return localStorage.getItem(TOKEN_TIME)

}

// 是否已经过期

export const diffTokenTime = () => {

  const currentTime = Date.now()

  const tokenTime = getTokenTime()

  return currentTime - tokenTime > TOKEN_TIME_VALUE

}

在请求拦截器中进行调用 api/request.js

增加引用

import { setTokenTime, diffTokenTime } from '@/utils/auth'

import router from '@/router'

import store from '@/store'

请求拦截器:

service.interceptors.request.use(

  (config) => {

    let token = localStorage.getItem('token')

    console.log('token=', token)

    if (token) {

      if (diffTokenTime() == true) {

        console.log('before logout')

        store.dispatch('app/logout')

        return Promise.reject(new Error('token 失效了'))

      } else {

        setTokenTime()

      }

    } else {

      router.replace('/login')

    }

    config.headers.Authorization = localStorage.getItem('token')

    return config

  },

  (error) => {

    return Promise.reject(new Error(error))

  }

)

store/modules/app.js里定义logout方法:

    // 退出

    logout({ commit }) {

      console.log('logout...')

      commit('setToken', '') //清空停牌值

      localStorage.clear() //清空本地存储

      router.replace('/login') //转向登录页面

    }

登录时设置登录时间:

    login({ commit }, userInfo) {

      return new Promise((resolve, reject) => {

        loginApi(userInfo)

          .then((res) => {

            setTokenTime() //设置登录时间

            commit('setToken', res.token)

            router.replace('/')

            resolve()

          })

          .catch((err) => {

            reject(err)

          })

      })

    },

21 汉堡按钮伸缩项

头部实现:

layout中创建headers/index.vue

<template>

  <div calss="navbar"></div>

</template>

<script setup></script>

<style lang="scss" scoped>

.navbar {

  width: 100%;

  height: 60px;

  overflow: hidden;

  background-color: #fff;

  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);

  padding: 0 16px;

  display: flex;

  align-items: center;

  box-sizing: border-box;

  position: relative;

  .navbar-right {

    flex: 1;

    display: flex;

    align-items: center;

    justify-content: flex-end;

    :v-deep(.navbar-item) {

      display: inline-block;

      margin-right: 18px;

      font-size: 22px;

      color: #5a5e66;

      box-sizing: border-box;

      cursor: pointer;

    }

  }

}

</style>

在layout/index.vue中使用:

      <el-header>

        <Headers />

      </el-header>

引入声明:

import Headers from './headers'

让左侧菜单宽度使用variables.scss(或variables.module.scss)中定义的值:

    <el-aside :width="variables.sideBarWidth" class="sidebar-container">

      <Menu />

    </el-aside>

引入

import { ref } from 'vue'

import Menu from './Menu'

import Headers from './headers'

import variables from '@/styles/variables.module.scss'

汉堡按钮实现:

汉堡按钮是在头部左侧一个按钮,单击后控制左侧菜单栏的显示与隐藏

layout/headers中创建文件夹components,创建文件 hamburger.vue:

<template>

  <div class="hamburger-container">

    <svg-icon icon="hamburger-opened"></svg-icon>

  </div>

</template>

<script setup></script>

<style lang="scss" scoped>

.hamburger-container {

  margin-right: 16px;

  box-sizing: border-box;

  cursor: pointer;

}

</style>

在layout/headers/index.vue中使用:

<template>

  <div calss="navbar">

    <hamburager></hamburager>

  </div>

导入:

import hamburager from './components/hamburger.vue'

单击汉堡按钮时,要使得左侧菜单栏收起或展开,需要在vuex里定义这个状态

store/modules/app.js增加:

  state: () => ({

    token: localStorage.getItem('token'),

    siderType: true

  }),

  mutations: {

    setToken(state, token) {

      state.token = token

      localStorage.setItem('token', token)

    },

    changeSiderType(state){

      state.siderType = !state.siderType

    }

  },

汉堡按钮增加单击事件:

<template>

  <div class="hamburger-container">

    <svg-icon icon="hamburger-opened" @click="" toggleClick></svg-icon>

  </div>

</template>

<script setup>

import { useStore } from 'vuex'

const store = useStore()

const toggleClick = () => {

  store.commit('app/changeSiderType')

}

</script>

布局里需要拿到siderType值,需要在 store/getters.js中定义一下:

export default {

  token: (state) => state.app.token,

  siderType: (state) => state.app.siderType

}

汉堡按钮的图标需要根据siderType的值进行修改:layout/headers/components/hamburger.vue

<template>

  <div class="hamburger-container">

    <svg-icon :icon="icon" @click="toggleClick" ></svg-icon>

  </div>

</template>

<script setup>

import { useStore } from 'vuex'

import { computed } from 'vue'

const store = useStore()

const toggleClick = () => {

  store.commit('app/changeSiderType')

}

const icon = computed(()=>{

    return store.getters.siderType?'hamburger-opened':'hamburger-closed'

})

菜单栏切换卷起和展开状态layout/Menu/index.vue:

<template>

  <el-menu

    active-text-color="#ffd04b"

    :background-color="variables.menuBg"

    class="el-menu-vertical-demo"

    text-color="#fff"

    :default-active="defaultActive"

    router

    unique-opened

    :collapse="!$store.getters.siderType"

菜单栏卷起状态时,菜单栏的宽度并没有缩小,需要对layout/index.vue中的菜单栏宽度进行动态赋值:

layout/index.vue

<template>

  <el-container class="app-container">

    <el-aside :width="asideWidth" class="sidebar-container">

      <Menu />

    </el-aside>

import { computed } from 'vue'

import store from '@/store'

//const asideWidth = ref(variables.sideBarWidth)

const asideWidth = computed(() => {

  if (store.getters.siderType == true) {

    return variables.sideBarWidth

  } else {

    return variables.hideSideBarWidth

  }

})

在左侧菜单栏卷起后,宽度也缩小了,但右侧区域并没有扩展到左侧,需要对右侧宽度进行调整,动态设置class:

    <el-container

      class="container"

      :class="{ hidderContainer: !$store.getters.siderType }"

    >

      <el-header>

        <Headers />

      </el-header>

      <el-main>

        <router-view />

      </el-main>

    </el-container>

.container {

  width: calc(100% - $sideBarWidth);

  height: 100%;

  position: fixed;

  top: 0;

  right: 0;

  z-index: 9;

  transition: all 0.28s;

  &.hidderContainer {

    width: calc(100% - $hideSideBarWidth);

  }

}

22 动态面包屑导航

layout/headers/components下新建 breadcrumb.vue

从element-plus网站的组件页面中复制面包屑代码,粘贴到breadcrumb.vue中:

<template>

  <el-breadcrumb separator="/">

    <el-breadcrumb-item>promotion detail</el-breadcrumb-item>

  </el-breadcrumb>

</template>

<script setup></script>

<style lang="scss" scoped></style>

在layout/Menu/index.vue中引入并使用:

<template>

  <div class="navbar">

    <Hamburager />

    <Breadcrumb />

  </div>

</template>

import Breadcrumb from './components/breadcrumb.vue'

在面包屑中使用路由路径,layout/headers/components/breadcrumb.vue中,监听路由变化,并赋值,渲染:

<template>

  <el-breadcrumb separator="/">

    <el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index">{{

      item.name

    }}</el-breadcrumb-item>

  </el-breadcrumb>

</template>

<script setup>

import { ref, watch } from 'vue'

import { useRoute } from 'vue-router'

const route = useRoute()

const breadcrumbList = ref([])

const initBreacrumbList = () => {

  breadcrumbList.value = route.matched

  console.log(route.matched)

}

watch(

  route,

  () => {

    initBreacrumbList()

  },

  { deep: true, immediate: true }

)

</script>

<style lang="scss" scoped></style>

显示的路由中,最后一项为灰,单击不可跳转,而其他的可跳转:

<template>

  <el-breadcrumb separator="/">

    <el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index">

      <span class="no-redirect" v-if="index === breadcrumbList.length - 1">{{

        item.name

      }}</span>

      <span class="redirect" v-else @click="handleRedirect(item.path)">{{

        item.name

      }}</span>

    </el-breadcrumb-item>

  </el-breadcrumb>

</template>

<script setup>

import { ref, watch } from 'vue'

import { useRoute, useRouter } from 'vue-router'

const route = useRoute()

const router = useRouter()

const breadcrumbList = ref([])

const initBreacrumbList = () => {

  breadcrumbList.value = route.matched

  console.log(route.matched)

}

const handleRedirect = (path) => {

  router.push(path)

}

watch(

  route,

  () => {

    initBreacrumbList()

  },

  { deep: true, immediate: true }

)

</script>

<style lang="scss" scoped>

.no-redirect {

  color: #97a8be;

  cursor: text;

}

.redirect {

  color: #666;

  font-weight: 600;

  cursor: pointer;

  &:hover {

    color: $menuBg;

  }

}

</style>

要注意的是 initBreacrumbList 要放在watch的上面,以免在watch里使用时提示找不到相应的方法。

23 头像退出

在头部右侧的头像上单击,出现下拉菜单,点其中的退出菜单项,退出登录。

layout/headers/components下新建avator.vue,从element-plus的组件页面复制粘贴一个方形的头像代码:

<template>

  <el-avatar shape="square" :size="40" :src="homeIcon" />

</template>

<script setup>

import { ref } from 'vue'

import homeIcon from '@/assets/logo.png'

const squareUrl = ref('/logo.png')

</script>

<style lang="scss" scoped></style>

在layout/headers/index.vue中使用:

<template>

  <div class="navbar">

    <Hamburager />

    <Breadcrumb />

    <div class="navbar-right"><Avatar /></div>

  </div>

</template>

<script setup>

import Hamburager from './components/hamburger.vue'

import Breadcrumb from './components/breadcrumb.vue'

import Avatar from './components/avator.vue'

从element-plus的组件页面复制粘贴下拉菜单到avator.vue,并修改:

<template>

  <el-dropdown>

    <span class="el-dropdown-link">

      <el-avatar shape="square" :size="40" :src="homeIcon" />

      <!-- <el-icon class="el-icon--right">

        <arrow-down />

      </el-icon> -->

    </span>

    <template #dropdown>

      <el-dropdown-menu>

        <el-dropdown-item>退 </el-dropdown-item>

      </el-dropdown-menu>

    </template>

  </el-dropdown>

</template>

<script setup>

import { ref } from 'vue'

import homeIcon from '@/assets/logo.png'

const squareUrl = ref('/logo.png')

</script>

<style lang="scss" scoped>

::deep .el-dropdown-menu__item {

  white-space: nowrap;

}

</style>

退出菜单项单击事件绑定方法:

        <el-dropdown-item @click="logout">退 </el-dropdown-item>

import { useStore } from 'vuex'

const store = useStore()

const logout = () => {

  store.dispatch('app/logout')

}

24 i18初使用

安装

npm i vue-i18n@next –force

创建 src/i18n/index.js

import { createLoadingComponent } from 'element-plus/es/components/loading/src/loading'

import { createI18n } from 'vue-i18n'

const messages = {

  en: {

    msg: {

      title: 'user login'

    }

  },

  zh: {

    msg: {

      title: '用户登录'

    }

  }

}

const getCurrentLanguage = () => {

  const UAlang = navigator.language

  const langCode = UAlang.indexOf('zh' !== -1 ? 'zh' : 'en')

  localStorage.setItem('lang', langCode)

  return langCode

}

const i18n = createI18n({

  legacy: false,

  globalInjection: true,

  locale: getCurrentLanguage() || 'zh',

  messages: messages

})

export default i18n

main.js中导入

import i18n from '@/i18n'

app.use(store).use(router).use(i18n).mount('#app')

views/login/index.vue中使用:

        <h3 class="title">{{ $t('msg.title') }}</h3>

25 导入i18n中英资源

从资源中复制国际化配置中的两个文件到 i18n文件夹中,在i18n/index.js中引入:

import { createI18n } from 'vue-i18n'

import EN from './en'

import ZH from './zh'

const messages = {

  en: {

    ...EN

  },

  zh: {

    ...ZH

  }

}

views/login/index.vue中修改:

        <h3 class="title">{{ $t('login.title') }}</h3>

      <el-button type="primary" class="login-button" @click="handleLogin">{{

        $t('login.btnTitle')

      }}</el-button>

面包屑中修改layout/headers/components/breadcrumb.vue:

      <span class="no-redirect" v-if="index === breadcrumbList.length - 1">{{

        $t(`menus.${item.name}`)

      }}</span>

      <span class="redirect" v-else @click="handleRedirect(item.path)">{{

        $t(`menus.${item.name}`)

      }}</span>

左侧菜单栏中二级菜单显示的中英文切换 layout/Menu/index.vue:

        <template #title>

          <el-icon>

            <component :is="icon"></component>

          </el-icon>

          <span>{{ $t(`menus.${it.path}`) }}</span>

        </template>

26 中英切换

在头部使用下拉菜单进行语言切换。

创建layout/headers/components/lang.vue

<template>

  <el-dropdown>

    <span class="el-dropdown-link">

      Dropdown List

      <el-icon class="el-icon--right">

        <arrow-down />

      </el-icon>

    </span>

    <template #dropdown>

      <el-dropdown-menu>

        <el-dropdown-item>Action 1</el-dropdown-item>

        <el-dropdown-item>Action 2</el-dropdown-item>

      </el-dropdown-menu>

    </template>

  </el-dropdown>

</template>

<script setup></script>

<style lang="scss" scoped></style>

在layout/headers/index.vue中使用

<template>

  <div class="navbar">

    <Hamburager />

    <Breadcrumb />

    <div class="navbar-right">

      <Lang class="navbar-item" />

      <Avatar class="navbar-item" />

    </div>

  </div>

</template>

<script setup>

import Hamburager from './components/hamburger.vue'

import Breadcrumb from './components/breadcrumb.vue'

import Avatar from './components/avator.vue'

import Lang from './components/lang.vue'

</script>

修改语言下拉菜单为图片lang.vue:

<template>

  <el-dropdown>

    <svg-icon icon="language"></svg-icon>

    <template #dropdown>

      <el-dropdown-menu>

        <el-dropdown-item>中文</el-dropdown-item>

        <el-dropdown-item>English</el-dropdown-item>

      </el-dropdown-menu>

    </template>

  </el-dropdown>

</template>

<script setup></script>

<style lang="scss" scoped></style>

完善:中文的状态下,中文的下拉菜单项就不能再被选中lang.vue。

<template>

  <el-dropdown @command="handleCommand">

    <svg-icon icon="language"></svg-icon>

    <template #dropdown>

      <el-dropdown-menu>

        <el-dropdown-item command="zh" :disabled="currentLanguage == 'zh'"

          >中文</el-dropdown-item

        >

        <el-dropdown-item command="en" :disabled="currentLanguage == 'en'"

          >English</el-dropdown-item

        >

      </el-dropdown-menu>

    </template>

  </el-dropdown>

</template>

<script setup>

import { useI18n } from 'vue-i18n'

import { computed } from 'vue'

const i18n = useI18n()

const currentLanguage = computed(() => {

  return i18n.locale.value

})

const handleCommand = (val) => {

  console.log(val)

}

</script>

<style lang="scss" scoped></style>

在切换后,最好存一下,就存在vuex里,store/modules/app.js:

export default {

  namespaced: true,

  state: () => ({

    token: localStorage.getItem('token'),

    siderType: true,

    lang: localStorage.getItem('lang') || 'zh'

  }),

  mutations: {

    setToken(state, token) {

      state.token = token

      localStorage.setItem('token', token)

    },

    changeSiderType(state) {

      state.siderType = !state.siderType

    },

    changeLang(state, lang) {

      state.lang = lang

    }

  },

lang.vue中使用vuex中的设置:

<script setup>

import { useI18n } from 'vue-i18n'

import { computed } from 'vue'

import { useStore } from 'vuex'

const i18n = useI18n()

const store = useStore()

const currentLanguage = computed(() => {

  return i18n.locale.value

})

const handleCommand = (val) => {

  console.log(val)

  i18n.locale.value = val

  store.commit('app/changLang', val)

  localStorage.setItem('lang', val)

}

</script>

27 全屏

使用screenfull插件,安装

npm i screenfull@5.1.0

layout/headers/components下新建文件screenFull.vue

<template>

  <div @click="handleFullScreen"><svg-icon icon="fullscreen"></svg-icon></div>

</template>

<script setup>

import screenfull from 'screenfull'

const handleFullScreen = () => {

  if (screenfull.isEnabled) {

    screenfull.toggle()

  }

}

</script>

<style lang="scss" scoped></style>

在layout/headers/index.vue中使用:

    <div class="navbar-right">

      <ScreenFull class="navbar-item" />

      <Lang class="navbar-item" />

      <Avatar class="navbar-item" />

    </div>

import ScreenFull from './components/screenFull.vue'

完善,全屏按钮切换时,显示的图片也要切换screenFull.vue

<template>

  <div @click="handleFullScreen">

    <svg-icon :icon="icon ? 'exit-fullscreen' : 'fullscreen'"></svg-icon>

  </div>

</template>

<script setup>

import screenfull from 'screenfull'

import { ref } from 'vue'

const icon = ref(screenfull.isFullscreen)

这时切换发现图标没有改变。

这是需要对screenfull 进行监听。

import { ref, onMounted, onBeforeMount } from 'vue'

const changeIcon = () => {

  icon.value = screenfull.isFullscreen

}

onMounted(() => {

  screenfull.on('change', changeIcon)

})

onBeforeMount(() => {

  screenfull.off('change')

})

28 引导页

就是一步步引导用户操作。安装

npm install driver.js –force

layout/headers/components下新建文件夹driver,新建文件index.vue

<template>

  <div>

    <svg-icon icon="guide"></svg-icon>

  </div>

</template>

<script setup>

import Driver from 'driver.js'

import 'driver.js/dist/driver.min.css'

import { onMounted } from 'vue'

let driver

onMounted(() => {

  driver = new Driver({

    animate: true,

    opacity: 0.75,

    padding: 10,

    allowClose: true,

    overlayClickNext: false,

    doneBtnText: 'Done',

    closeBtnText: 'Close',

    stageBackground: '#ffffff',

    nextBtnText: 'Next',

    prevBtnText: 'Previous'

  })

})

</script>

<style lang="scss" scoped></style>

新建文件steps.js

export const steps = [

  {

    element: '#guide',

    popover: {

      className: 'first-step-popover-class',

      title: 'Title on Popover',

      description: 'Body of the popover',

      position: 'left'

    }

  }

]

完善layout/headers/components/driver/index.vue

<template>

  <div id="guide" @click.prevent.stop="handleGuide">

    <svg-icon icon="guide"></svg-icon>

  </div>

</template>

<script setup>

import Driver from 'driver.js'

import 'driver.js/dist/driver.min.css'

import { onMounted } from 'vue'

import { steps } from '@/layout/headers/components/driver/steps.js'

let driver

onMounted(() => {

  driver = new Driver({

    animate: false, // Whether to animate or not

    opacity: 0.75, // Background opacity (0 means only popovers and without overlay)

    padding: 10, // Distance of element from around the edges

    allowClose: true, // Whether the click on overlay should close or not

    overlayClickNext: false, // Whether the click on overlay should move next

    doneBtnText: 'Done', // Text on the final button

    closeBtnText: 'Close', // Text on the close button for this step

    stageBackground: '#ffffff', // Background color for the staged behind highlighted element

    nextBtnText: 'Next', // Next button text for this step

    prevBtnText: 'Previous', // Previous button text for this step

    showButtons: false // Do not show control buttons in footer

  })

})

const t = 1

const handleGuide = () => {

  driver.defineSteps(steps)

  driver.start()

}

</script>

<style lang="scss" scoped></style>

使用自定义的i18n

import i18n from '@/i18n'

const t = i18n.global.t

let driver

onMounted(() => {

  driver = new Driver({

    animate: false, // Whether to animate or not

    opacity: 0.75, // Background opacity (0 means only popovers and without overlay)

    padding: 10, // Distance of element from around the edges

    allowClose: true, // Whether the click on overlay should close or not

    overlayClickNext: false, // Whether the click on overlay should move next

    doneBtnText: t('driver.doneBtnText'), //'Done', // Text on the final button

    closeBtnText: t('driver.closeBtnText'), //'Close', // Text on the close button for this step

    stageBackground: '#ffffff', // Background color for the staged behind highlighted element

    nextBtnText: t('driver.closeBtnText'), // Next button text for this step

    prevBtnText: t('driver.prevBtnText'), // Previous button text for this step

    showButtons: true // Do not show control buttons in footer

  })

})

将弹框中的标题和内容也换成i18n:

const handleGuide = () => {

  driver.defineSteps(steps(t))

  driver.start()

}

steps.js

export const steps = (i18n) => [

  {

    element: '#guide',

    popover: {

      title: i18n('driver.guideBtn'),

      description: 'Body of the popover',

      position: 'left' //left,bottom,..

    }

  },

  {

    element: '#hamburger',

    popover: {

      title: i18n('driver.hamburgerBtn'),

      description: 'Body of the popover',

      position: 'bottom' //left,bottom,..

    }

  },

  {

    element: '#screenful',

    popover: {

      title: i18n('driver.fullScreen'),

      description: 'Body of the popover',

      position: 'left' //left,bottom,..

    }

  }

]

在语言变化后,弹出框的标题有了变化,但按钮文字没有变。需要对语言变化进行监听。这里做一个封装。新建i18n/watchlang.js

store/getters.js里增加:

export default {

  token: (state) => state.app.token,

  siderType: (state) => state.app.siderType,

  lang: (state) => state.app.lang

}

watchlang.js

import { watch } from 'vue'

import store from '@/store'

export const watchLang = (...cbs) => {

  watch(

    () => store.getters.lang,

    () => {

      cbs.forEach((cb) => cb(store.getters.lang))

    },

    { deep: true }

  )

}

使用,修改layout/headers/components/driver/index.vue

import { watchLang } from '@/i18n/watchlang'

const t = i18n.global.t

let driver

onMounted(() => {

  initDriver()

})

const initDriver = () => {

  driver = new Driver({

    animate: false, // Whether to animate or not

    opacity: 0.75, // Background opacity (0 means only popovers and without overlay)

    padding: 10, // Distance of element from around the edges

    allowClose: true, // Whether the click on overlay should close or not

    overlayClickNext: false, // Whether the click on overlay should move next

    doneBtnText: t('driver.doneBtnText'), //'Done', // Text on the final button

    closeBtnText: t('driver.closeBtnText'), //'Close', // Text on the close button for this step

    stageBackground: '#ffffff', // Background color for the staged behind highlighted element

    nextBtnText: t('driver.nextBtnText'), // Next button text for this step

    prevBtnText: t('driver.prevBtnText'), // Previous button text for this step

    showButtons: true // Do not show control buttons in footer

  })

}

watchLang(initDriver)

29 表格静态

views/users/index.vue

表格的头部搜索区域

<template>

  <el-card>

    <el-row :gutter="20"> <!—20表示项之间的间距,默认有24col-->

      <el-col :span="7"> <!—设置搜索框占24个中的7col-->

        <el-input

          :placeholder="$t('table.placeholder')"

          clearable

          v-model="queryForm.query"

        ></el-input>

      </el-col>

      <el-button type="primary" :icon="Search">{{

        $t('table.search')

      }}</el-button>

      <el-button type="primary">{{ $t('table.adduser') }}</el-button>

    </el-row>

  </el-card>

</template>

<script setup>

import { ref } from 'vue'

const queryForm = ref({

  query: '',

  pagenum: 1,

  pagesize: 10

})

</script>

<style lang="scss" scoped></style>

表格,复制element-plus网站组件页面中的表格代码

<template>

  <el-card>

    <el-row :gutter="20" class="headers">

      <el-col :span="7">

        <el-input

          :placeholder="$t('table.placeholder')"

          clearable

          v-model="queryForm.query"

        ></el-input>

      </el-col>

      <el-button type="primary" :icon="Search">{{

        $t('table.search')

      }}</el-button>

      <el-button type="primary">{{ $t('table.adduser') }}</el-button>

    </el-row>

    <el-table :data="tableData" stripe style="width: 100%">

      <el-table-column prop="date" label="Date" width="180" />

      <el-table-column prop="name" label="Name" width="180" />

      <el-table-column prop="address" label="Address" />

    </el-table>

  </el-card>

</template>

<script setup>

import { ref } from 'vue'

const queryForm = ref({

  query: '',

  pagenum: 1,

  pagesize: 10

})

const tableData = [

  {

    date: '2016-05-03',

    name: 'Tom',

    address: 'No. 189, Grove St, Los Angeles'

  },

  {

    date: '2016-05-02',

    name: 'Tom',

    address: 'No. 189, Grove St, Los Angeles'

  },

  {

    date: '2016-05-04',

    name: 'Tom',

    address: 'No. 189, Grove St, Los Angeles'

  },

  {

    date: '2016-05-01',

    name: 'Tom',

    address: 'No. 189, Grove St, Los Angeles'

  }

]

</script>

<style lang="scss" scoped>

.headers {

  margin-bottom: 20px;

}

</style>

使用后台数据,api/users.js

import request from './request'

export const getUser = (params) => {

  return request({

    url: '/users',

    params

  })

}

为表格设置列,v-for,并对状态列和操作列进行显示设置

<template>

  <el-card>

    <el-row :gutter="20" class="headers">

      <el-col :span="7">

        <el-input

          :placeholder="$t('table.placeholder')"

          clearable

          v-model="queryForm.query"

        ></el-input>

      </el-col>

      <el-button type="primary" :icon="Search">{{

        $t('table.search')

      }}</el-button>

      <el-button type="primary">{{ $t('table.adduser') }}</el-button>

    </el-row>

    <el-table :data="tableData" stripe style="width: 100%" border>

      <el-table-column

        v-for="item in columns"

        :prop="item.prop"

        :label="$t(`table.${item.label}`)"

        :width="item.width"

        :align="item.align"

      >

        <template v-slot="{ row }" v-if="item.prop == 'mg_state'">

          <el-switch v-model="row.mg_state"></el-switch>

        </template>

        <template #default v-else-if="item.prop == 'action'">

          <el-button type="primary" size="small" icon="Edit"></el-button>

          <el-button type="warning" size="small" icon="Setting"></el-button>

          <el-button type="danger" size="small" icon="Delete"></el-button>

        </template>

      </el-table-column>

    </el-table>

  </el-card>

</template>

<script setup>

import { ref } from 'vue'

import { getUser } from '@/api/users'

const queryForm = ref({

  query: '',

  pagenum: 1,

  pagesize: 10

})

const tableData = ref([])

const columns = ref([

  {

    label: 'username',

    prop: 'username'

  },

  {

    label: 'email',

    prop: 'email'

  },

  {

    label: 'mobile',

    prop: 'mobile'

  },

  {

    label: 'role_name',

    prop: 'role_name'

  },

  {

    label: 'mg_state',

    prop: 'mg_state'

  },

  {

    label: 'create_time',

    prop: 'create_time'

  },

  {

    label: 'action',

    prop: 'action',

    width: 160,

    align: 'center'

  }

])

const initGetUsersList = async () => {

  const res = await getUser(queryForm.value)

  console.log('getUser res=', res)

  tableData.value = res.users

}

initGetUsersList()

</script>

<style lang="scss" scoped>

.headers {

  margin-bottom: 20px;

}

</style>

30 全局属性

创建时间格式化,使用统一插件 dayjs,安装

npm install dayjs -–save --legacy-peer-deps

utils下新建filters.js

const dayjs = require('dayjs')

const filterTimes = (val, format = 'YYYY-MM-DD') => {

  if (!isNull(val)) {

    val = parseInt(val) * 1000

    return dayjs(val).format(format)

  } else {

    return '--'

  }

}

export const isNull = (data) => {

  if (!data) return true

  if (JSON.stringify(data) == '{}') return true

  if (JSON.stringify(data) == '[]') return true

}

export default (app) => {

  app.config.globalProperties.$filters = {

    filterTimes

  }

}

main.js中导入:

import filters from './utils/filters'

const app = createApp(App)

for (const iconName in ELIcons) {

  app.component(iconName, ELIcons[iconName])

}

filters(app)

views/users/index.vue中增加

        <template v-slot="{ row }" v-else-if="item.prop == 'create_time'">

          {{ $filters.filterTimes(row.create_time) }}

        </template>

31 分页器

复制element-plus网站组件页面中的pagination组件中的代码到views/users/index.vue中table下,并修改:

    <el-pagination

      v-model:current-page="queryForm.pagenum"

      v-model:page-size="queryForm.pagesize"

      :page-sizes="[10, 20, 50, 100]"

      :small="small"

      :disabled="disabled"

      :background="background"

      layout="total, sizes, prev, pager, next, jumper"

      :total="total"

      @size-change="handleSizeChange"

      @current-change="handleCurrentChange"

    />

const total = ref(0)

const initGetUsersList = async () => {

  const res = await getUser(queryForm.value)

  tableData.value = res.users

  total.value = res.total

}

页码改变和每页条数改变事件方法:

const handleSizeChange = (pageSize) => {

  queryForm.value.pagenum = 1

  queryForm.value.pagesize = pageSize

  initGetUsersList()

}

const handleCurrentChange = (pageNum) => {

  queryForm.value.pagenum = pageNum

  initGetUsersList()

}

32 添加用户

修改用户状态api/users.js增加

export const changeUserState = (uid, type) => {

  return request({

    url: `/users/${uid}/state/${type}`,

    method: 'PUT',

    params

  })

}

views/users/index.vue

          <el-switch

            v-model="row.mg_state"

            @change="changeState(row)"

          ></el-switch>

import { getUser, changeUserState } from '@/api/users'

const changeState = async (info) => {

  await changeUserState(info.id, info.mg_state)

}

使用提示

import { useI18n } from 'vue-i18n'

const i18n = useI18n()

const changeState = async (info) => {

  await changeUserState(info.id, info.mg_state)

  ElMessage({

    message: i18n.t('message.updateSuccess'),

    type: 'success'

  })

}

添加用户,单击页面上添加用户按钮,要弹出一个对话框

新建 views/users/components/dialog.vue,从element-plus中复制dialog代码

<template>

  <el-dialog

    :model-value="dialogVisible"

    title="Tips"

    width="30%"

    @close="handleClose"

  >

    <span>This is a message</span>

    <template #footer>

      <span class="dialog-footer">

        <el-button @click="handleClose">Cancel</el-button>

        <el-button type="primary" @click="handleConfirm"> Confirm </el-button>

      </span>

    </template>

  </el-dialog>

</template>

<script setup>

import { defineEmits } from 'vue'

const emits = defineEmits(['update:modelValue'])

const handleClose = () => {

  emits('update:modelValue', false)

}

const handleConfirm = () => {

  handleClose()

}

</script>

<style lang="scss" scoped></style>

注意:这里有修改父级中变量值的方法

views/users/index.vue

      <el-button type="primary" :icon="Search" @click="initGetUsersList">{{

        $t('table.search')

      }}</el-button>

      <el-button type="primary" @click="handleDialogValue">{{

        $t('table.adduser')

      }}</el-button>

  </el-card>

  <Dialog v-model="dialogVisible" />

</template>

const dialogVisible = ref(false)

const handleDialogValue = () => {

  dialogVisible.value = true

}

在 dialog.vue中填入表单:

<template>

  <el-dialog

    :model-value="dialogVisible"

    title="添加用户"

    width="40%"

    @close="handleClose"

  >

    <el-form ref="formRef" :model="from" label-width="70px" :rules="rules">

      <el-form-item label="用户名" prop="username">

        <el-input v-model="form.username"></el-input>

      </el-form-item>

      <el-form-item label="密码" prop="password">

        <el-input v-model="form.password"></el-input>

      </el-form-item>

      <el-form-item label="手机号" prop="mobile">

        <el-input v-model="form.mobile"></el-input>

      </el-form-item>

      <el-form-item label="email" prop="email">

        <el-input v-model="form.email"></el-input>

      </el-form-item>

    </el-form>

    <template #footer>

      <span class="dialog-footer">

        <el-button @click="handleClose">取消</el-button>

        <el-button type="primary" @click="handleConfirm"> 确认 </el-button>

      </span>

    </template>

  </el-dialog>

</template>

<script setup>

import { ref, defineEmits } from 'vue'

const emits = defineEmits(['update:modelValue'])

const handleClose = () => {

  emits('update:modelValue', false)

}

const handleConfirm = () => {

  handleClose()

}

const form = ref({

  username: '',

  password: '',

  mobile: '',

  email: ''

})

const rules = ref({

  username: [

    {

      required: true,

      message: '请输入用户名',

      trigger: 'blur'

    },

    {

      min: 3,

      max: 25,

      message: '长度要为325',

      trigger: 'blur'

    }

  ],

  password: [

    {

      required: true,

      message: '请输入密码',

      trigger: 'blur'

    },

    {

      min: 3,

      max: 25,

      message: '长度要为325',

      trigger: 'blur'

    }

  ],

  email: [

    {

      required: true,

      message: '请输入邮箱',

      trigger: 'blur'

    },

    {

      type: 'email',

      message: '请输入正确邮箱',

      trigger: ['blur', 'change']

    }

  ],

  mobile: [

    {

      required: true,

      message: '请输入手机号码',

      trigger: 'blur'

    },

    {

      type: 'mobile',

      message: '请输入正确手机号码',

      trigger: ['blur', 'change']

    }

  ]

})

</script>

<style lang="scss" scoped></style>

对话框标题需要根据需要进行设置,添加用户按钮单击后,是添加用户标题,修改用户按钮单击后,是修改用户标题。使用从父组件传进来的变量。

views/users/index.vue

  <Dialog v-model="dialogVisible" :dialogTitle="dialogTitle" />

const dialogTitle = ref('')

const handleDialogValue = () => {

  dialogTitle.value = '添加用户'

  dialogVisible.value = true

}

dialog.vue里

<template>

  <el-dialog

    :model-value="dialogVisible"

    :title="dialogTitle"

    width="40%"

    @close="handleClose"

  >

import { ref, defineEmits,defineProps } from 'vue'

defineProps({

  dialogTitle: {

    type: String,

    default: '',

    required: true

  }

})

添加用户api/users.js

export const addUser = (data) => {

  return request({

    url: 'users',

    method: 'POST',

    data

  })

}

dialog.vue中使用:

import { addUser } from '@/api/users'

import i18n from '@/i18n'

import { ElMessage } from 'element-plus'

const handleConfirm = async () => {

  await addUser(form.value)

  ElMessage({

    message: i18n.global.t('message.updateSuccess'),

    type: 'success'

  })

  handleClose()

}

让对话框每次打开都是新的,即表单项里都是空值:

  <Dialog

    v-model="dialogVisible"

    :dialogTitle="dialogTitle"

    v-if="dialogVisible"

  />

统一校验

const formRef = ref(null)

const handleConfirm = () => {

  formRef.value.validate(async (valid) => {

    if (valid) {

      await addUser(form.value)

      ElMessage({

        message: i18n.global.t('message.updateSuccess'),

        type: 'success'

      })

      handleClose()

    } else {

      console.log('error submit')

      return false

    }

  })

}

分发事件,通知父组件,刷新表格数据

const emits = defineEmits(['update:modelValue','initUserList'])

const handleClose = () => {

  emits('update:modelValue', false)

}

const formRef = ref(null)

const handleConfirm = () => {

  formRef.value.validate(async (valid) => {

    if (valid) {

      await addUser(form.value)

      ElMessage({

        message: i18n.global.t('message.updateSuccess'),

        type: 'success'

      })

      emits('initUserList')

      handleClose()

    } else {

      console.log('error submit')

      return false

    }

  })

}

父组件views/users/index.vue中事件绑定:

  <Dialog

    v-model="dialogVisible"

    :dialogTitle="dialogTitle"

    v-if="dialogVisible"

    @initUserList="initUserList"

  />

添加用户时,不带值,方法后面加括号:

      <el-button type="primary" :icon="Search" @click="initGetUsersList()">{{

        $t('table.search')

      }}</el-button>

33 编辑用户

api/users.js

export const editUser = (data) => {

  return request({

    url: `users/${data.id}`,

    method: 'PUT',

    data

  })

}

views/users/index.vue

        <template #default="{ row }" v-else-if="item.prop == 'action'">

          <el-button

            type="primary"

            size="small"

            icon="Edit"

            @click="handleDialogValue(row)"

          ></el-button>

import { isNull } from '@/utils/filters'

const dialogTableValue = ref({})

const handleDialogValue = (row) => {

  if (isNull(row)) {

    dialogTitle.value = '添加用户'

    dialogTableValue.value = {}

  } else {

    dialogTitle.value = '编辑用户'

    dialogTableValue.value = JSON.parse(JSON.stringify(row))

  }

  dialogVisible.value = true

}

dialog.vue

import { addUser,editUser } from '@/api/users'

const handleConfirm = () => {

  formRef.value.validate(async (valid) => {

    if (valid) {

      props.dialogTitle === '添加用户'

        ? await addUser(form.value)

        : await editUser(form.value)

      ElMessage({

        message: i18n.global.t('message.updateSuccess'),

        type: 'success'

      })

      emits('initUserList')

      handleClose()

    } else {

      console.log('error submit')

      return false

    }

  })

分页器样式微调,跟上面有20px间距,靠右views/users/index.vue:

    <el-pagination

      v-model:current-page="queryForm.pagenum"

      v-model:page-size="queryForm.pagesize"

      :page-sizes="[10, 20, 50, 100]"

      :small="small"

      :disabled="disabled"

      :background="background"

      layout="->,total, sizes, prev, pager, next, jumper"

      :total="total"

      @size-change="handleSizeChange"

      @current-change="handleCurrentChange"

    />

:deep(.el-pagination) {

  margin-top: 20px;

  box-sizing: border-box;

  text-align: right;

}

34 删除用户

api/users.js

export const deleteUser = (id) => {

  return request({

    url: `users/${id}`,

    method: 'DELETE'

  })

}

views/users/index.vue

          <el-button

            type="danger"

            size="small"

            icon="Delete"

            @click="delUser(row)"

          ></el-button>

const delUser = (row) => {

  ElMessageBox.confirm(i18n.t('dialog.deleteTitle'), 'Warning', {

    confirmButtonText: 'OK',

    cancelButtonText: 'Cancel',

    type: 'warning'

  })

    .then(async () => {

      await deleteUser(row.id)

      ElMessage({

        type: 'success',

        message: 'Delete completed'

      })

      initGetUsersList()

    })

    .catch(() => {

      ElMessage({

        type: 'info',

        message: 'Delete canceled'

      })

    })

}

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/383451
推荐阅读