Vue3快速搭建后台管理系统

前言

视频讲解为 B 站UP主:混吃等死的咸鱼仔
视频链接

一、使用 vue3

1.创建项目

1.创建空白目录
2.cmd 输入 vue ui
3.在界面中创建项目
创建-输入目录地址-再次创建-起一个名字-手动配置-勾选 Router、VueX、CSS Pre-processors、使用配置文件 - 使用vue 3x、Sass/SCSS (with dart-sass)、ESLint + Standard config - 再次创建
-不保存预设
4.安装依赖 axions
5.回到项目当中-终端输入- npm i vue@3.2.8 vue-router@4.0.11 vuex@4.0.2
6.重新 npm i
7.启动 npm run serve
项目创建完成

2.代码格式化

1.在vscode 中安装 Prettier - Code formatter
2.设置-搜索 save - 勾选 Edlitor:Format On Save
3.在 Home.vue 界面中右击 - 使用…格式化文档-配置默认格式化程序-选择 Prettier - Code formatter
4.新建 .prettierrc (跟 package.json 同级)
{
“semi”: false, //是否使用分号来结尾
“singleQuote”: true, //是否使用单引号
“trailingComma”: “none” //是否最后一句话使用逗哈来结尾
}
5.在 .eslintrc.js 中写入
rules: {
‘indent’: 0,
‘space-before-function-paren’: 0
}
6.在home.vue中输入一个分号测试是否自动删除

3.commit规范

1.安装 commitizen和cz-customizable
npm install -g commitizen@4.2.4
npm i cz-customizable@6.3.0 --save-dev
2.在 package.json 里最下方写入
“config”: {
“commitizen”: {
“path”: “node_modules/cz-customizable”
}
}
3.将.cz-config.js 拖入到根目录中
资源链接
4.终端输入 git add .
git cz (替代 git commit)
feat
可修改范围不选
commit 提交了那些东西
git push

4.强制 commit

1.使用husky进行强制git代码提交规范
npm install --save-dev @commitlint/config-conventional@12.1.4 @commitlint/cli@12.1.4
npm install husky@7.0.1 --save-dev
npx husky install
根目录多出一个 .husky文件
2.将 commitlint.config.js 文件拖入根目录
3.在 package.json 中 加入
“scripts”: {
“prepare”: “husky install”
}
4.执行 npm run prepare
5.新增husky配置文件 并往里面写入
终端输入:npx husky add .husky/commit-msg
.husky 目录下多出一个 commit-msg 配置文件
用house key 和 commit list进行一个关联
在commit-msg中(把undefined覆盖)写入 npx --no-install commitlint --edit
6.终端输入一个 不符合规矩的
git add .
git commit -m ‘新功能’
7.输入一个符合规矩的
git commit -m ‘feat: 新功能’

5.强制代码规范

1.使用husky强制代码格式化 创建配置文件
终端执行:npx husky add .husky/pre-commit
2.在 .husky 目录下 .pre-commit 中输入
npx lint-staged
3.在 package.json中配置
最下方加入
“lint-staged”: {
“src/**/*.{js,vue}”: [ //src目录下所有的js和vue文件
“eslint --fix”, // 自动修复
“git add” // 自动提交时修复
]
}

6.按需引入 elementplus

elementplus官网
1.安装: npm install element-plus --save
2.按需导入:npm install -D unplugin-vue-components unplugin-auto-import
3.我们使用的是 Webpack,在根目录下创建 vue.config.js
将一下代码写入

// webpack.config.js
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()]
    }))
  }
})

7.vue3.2新特性

1.不用再使用根标签包括
2.自己看,css变量可以用在script

<template>
  <div class="box"></div>
  <router-view />
</template>

<script setup>
const boxWidth = '100px'
</script>
<style lang="scss">
.box {
  width: v-bind(boxWidth);
  height: 100px;
  background: red;
}
</style>

8.初始化项目

1.App.vue清空

<template>
  <router-view></router-view>
</template>

<style lang="sass"></style>

2.将 styles文件夹 复制到 src 目录下
3.在 main.js 导入 scss文件 import '@/styles/index.scss
4.创建登录页面,在views目录下创建 login 目录,目录下创建 index.vue
5.运行 访问 login

9.登录页面静态

<template>
  <div class="login-container">
    <el-form ref="formRef" :model="form" class="login-form">
      <div class="title-container">
        <h3 class="title">用户登录</h3>
      </div>
      <el-form-item>
        <!-- <el-icon :size="20" class="svg-container">
          <edit />
        </el-icon> -->
        <svg-icon icon="user" class="svg-container"></svg-icon>
        <el-input v-model="form.name"></el-input>
      </el-form-item>
      <el-form-item>
        <svg-icon icon="password" class="svg-container"></svg-icon>
        <el-input v-model="form.password"></el-input>
      </el-form-item>
      <el-button type="primary" class="login-button">登录</el-button>
    </el-form>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { Edit } from '@element-plus/icons-vue'
const form = ref({
  name: '',
  password: ''
})
</script>
<style lang="scss" scoped>
$bg: #2d3a4b;
$dark_gray: #889aa4;
$light_gray: #eee;
$cursor: #fff;

:deep(.el-input) {
  input {
    box-shadow: none; //去除白边
    &:focus {
      box-shadow: none; //输入内容原本样式
    }
  }
}

.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;

    ::v-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;
    }

    ::v-deep .el-input {
      display: inline-block;
      height: 47px;
      width: 85%;

      input {
        background: transparent;
        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>

10.svg-icon

记住先安装 npm install @element-plus/icons-vue

1.将 icons 复制到 src 目录下
2.在 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" scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>

3.在 icons 目录下 创建 index.js

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

4.在 main.js 中导入创建好的 index.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/styles/index.scss'
import SvgIcon from '@/icons'

const app = createApp(App)
SvgIcon(app)
app.use(store).use(router).mount('#app')

5.安装 svg loader,终端输入:npm i --save-dev svg-sprite-loader@6.0.9
6.在 vue.config.js 中配置

7.使用 svg-cion
上方:

const path = require('path')
function resolve(dir) {
  return path.join(__dirname, dir)
}
const webpack = require('webpack')

下方:

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()
  }

11.表单校验

1.el-form 标签上加上 :rules=“rules”
2.script上定义校验规则

const rules = ref({
  username: [
    {
      required: true,
      message: '请输入用户名',
      trigger: 'blur'
    }
  ],
  password: [
    {
      required: true,
      message: '请输入密码',
      trigger: 'blur'
    }
  ]
})

3.el-form-item 上绑定校验规则 prop=" "
4.统一校验,在 el-button 定义一个点击事件 @click=“handleLogin”
5.script 定义函数

const formRef = ref(null)
const handleLogin = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      alert('submit!')
    } else {
      console.log('error submit!!')
      return false
    }
  })
}

12.发送登录请求

1.在 src 目录下新建 api 文件夹 - 新建 request.js

import axios from "axios";
const service = axios.create({
    baseURL: process.env.VUE_APP_BASE_API,
    timeout: 5000
})

2.根目录新建 开发环境 .env.development

ENV = 'development'

VUE_APP_BASE_API = '/api'

3.再创建生产环境 .env.production

ENV = 'production'

VUE_APP_BASE_API = '/prod-api'

4.解决跨域问题,在 vue.config 中配置代理

devServer: {
    https: false,
    hotOnly: false,
    proxy: {
      '/api': {
        target: 'https://lianghj.top:8888/api/private/vi/',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }

5.修改 request.js baseURL: process.env.VUE_APP_BASE_API(记得去掉引号)
6.api 目录下新建 login.js

import request from './request'

export const login = (data) => {
    return request({
        url: '/login',
        method: 'POST',
        data
    })
}

7.重启项目
8.在 login.vue 导入import { login } from '@/api/login'
9.发送请求

const handleLogin = () => {
  formRef.value.validate(async (valid) => {
    if (valid) {
      // alert('submit!')
      await login(form.value)
    } else {
      console.log('error submit!!')
      return false
    }
  })
}

这里出现了一个 BUG
Uncaught (in promise) Error: Request failed with status code 404
解决办法:在 babel.config.js 加入以下下配置

module.exports = {
  "env": {
    "development": {
      "sourceMaps": true,
      "retainLines": true,
    }
  },
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
}

响应拦截器

1.将密码框 改为暗文,在 input 上加上一个 type值为 password,input 标签后加上一个小图标,<svg-cion icon="eye" @click="changeType"></svg-icon> ,定义方法,完整代码如下

<el-input v-model="form.password" :type="passwordType"></el-input>
<svg-icon
	:icon="passwordType === ‘password’ ? 'eye' : 'eye-open'"
	@click="changeType"
</svg-icon>

<script setup>
	const passwordType = ref('password')
	const changeType = () => {
		if (passwordType.value === 'password') {
			passwordType.value = 'text'
		} else {
			passwordType.value = 'password'
		}
	}
</script>

2.需要知道请求回来的数据

const res = await login(form.value)

excess响应请求拦截器

  1. 在 request.js 中配置
import { ElMessage } from 'element-plus'

service.interceptors.response.use(
	(response) => {
		const { data, meta } = response.data
		if (meta.status === 200 || meta.status === 201) {
			return data
		} else {
			ElMessage.error(meta.msg)
			return Promise.reject(new Error(meta.msg))
		}
	},
	(error) => {
		error.response && ElMessage.error(error.response.data)
		return Promise.reject(new Error(error.response.data))
	}
}
  1. 在 main.js 中 引入 import 'element-plus/dist/index.css'

登录

存储 Token VueX

1.将 store 目录下是 index.js 修改

import default createStore({
	modules: {
	}
})

2.在 store 目录下建一个文件夹 modules下建一个 app.js
3.login 包的 index.vue 找到 login(form.value),注释掉登录,剪切 import { login } from ‘@/api/login’,粘贴到 app.js,修改 login 名称 import { login as loginApi } from '@/api/login'在 app.js 进行登录操作

import { login as loginApi } from '@/api/login'
import router from '@/router'
import { setTokenTime } from '@/utils/auth'
export default {
    namespaced: true,
    state: () => ({
        // 取出 token
        token: localStorage.getItem('token') || '',
        siderType: true,
        lang: localStorage.getItem('lang') || 'zh'
    }),
    mutations: {
       // state, 外界传入的 token
       // token,从哪里来,从登陆获取
       setToken(state, token) {
           state.token = token
           localStorage.setItem('token', token)
       },
       changeSiderType(state) {
           state.siderType = !state.siderType
       },
       changLang(state, lang) {
           state.lang = lang
       }
    },
    actions: {
        // 执行登录操作
        login({commit}, userInfo) {
            // resolve 正常调用该方法
            // reject 异常调用
            return new Promise((resolve, reject) => {
                // userInfo 将用户信息传入进去
                loginApi(userInfo).then(res => {
                    commit('setToken', res.token)
                    setTokenTime()
                    // 成功之后跳转
                    router.replace('/')
                    resolve()
                }).catch(err => {
                    reject(err)
                })
            })
        },
        // 退出 
        logout ({commit}) {
            commit('setToken', '')
            localStorage.clear()
            router.replace('/login')
        }
    }
}

4.在index.js 中导入 app.js

import app from './modules/app'
export default createStore({
	modules: {
		app
	}
}

5.返回 login包下 index.vue,触发,使用 Vuex,引入 vuex

import { useStore } from 'vuex'
const store = useStore()

store.dispathch('app/login', from.value)

请求拦截器

.在 request.js 中设置 service 请求拦截器

service.interceptors.request.use(
    (config) => {
        config.headers.Authorization = localStorage.getItem('token')
        return config
    },
    (error) => {
        return Promise.reject(new Error(error))
    }
)

路由守卫

1.在 router 目录下 新建一个 permission.js 文件

import router from './index'
import store from '@/store'

// 定义一些白名单,让一些不用登录也能进去的页面
const whiteList = ['/login']
router.beforeEach((to, from, next) => {
    console.log(store.getters.token)
    if (store.getters.token) {
        if (to.path === '/login') {
            next('/')
        } else {
            next()
        }
    } else {
        // 如果有数组里有就跳转
        if (whiteList.includes(to.path)) {
            next()
        } else {
            next('/login')
        }
    }
})

2.为了方便拿到 token,在 store 目录下创建一个 getters.js 文件

export default {
    token: (state) => state.app.token,
    siderType: (state) => state.app.siderType,
    lang: (state) => state.app.lang
}

3.在 store 目录下 index.js 中使用 getter.js

import { createStore } from 'vuex'
import app from './modules/app'
import getters from './getters'

export default createStore({
  modules: {
    app
  },
  getters
})

4.使用,在 main.js 中导入 import '@/router/permission'

Layout布局

1.src 目录下新建 layout 目录 - index.vue

<template>
  <el-container class="app-wrapper">
    <el-aside width="200px" class="sidebar-container">Aside</el-aside>
    <el-container class="container">
      <el-header>Header</el-header>
      <el-main>Main</el-main>
    </el-container>
  </el-container>
</template>

<script setup></script>

<style lang="scss" scoped>
.app-container {
  position: relative;
  width: 100%;
  height: 100%;
}
.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);
  }
}
::v-deep .el-header {
  padding: 0;
}
</style>

2.router 目录下新建路由
3.导入 sass 样式,在 vue.config.js 中配置

css: {
    loaderOptions: {
      sass: {
        // 8版本用prependData:
        additionalData: `
          @import "@/styles/variables.scss";  // scss文件地址
          @import "@/styles/mixin.scss";     // scss文件地址
        `
      }
    }
  }

menus菜单

  1. layout目录下新建一个Menu - index.vue,官网找到 menu 菜单,复制
<template>
  <el-menu
    active-text-color="#ffd04b"
    background-color="#545c64"
    class="el-menu-vertical-demo"
    default-active="2"
    text-color="#fff"
    @open="handleOpen"
    @close="handleClose"
  >
    <el-sub-menu index="1">
      <template #title>
        <el-icon><location /></el-icon>
        <span>Navigator One</span>
      </template>
      <el-menu-item-group title="Group One">
        <el-menu-item index="1-1">item one</el-menu-item>
        <el-menu-item index="1-2">item one</el-menu-item>
      </el-menu-item-group>
      <el-menu-item-group title="Group Two">
        <el-menu-item index="1-3">item three</el-menu-item>
      </el-menu-item-group>
      <el-sub-menu index="1-4">
        <template #title>item four</template>
        <el-menu-item index="1-4-1">item one</el-menu-item>
      </el-sub-menu>
    </el-sub-menu>
    <el-menu-item index="2">
      <el-icon><icon-menu /></el-icon>
      <span>Navigator Two</span>
    </el-menu-item>
    <el-menu-item index="3" disabled>
      <el-icon><document /></el-icon>
      <span>Navigator Three</span>
    </el-menu-item>
    <el-menu-item index="4">
      <el-icon><setting /></el-icon>
      <span>Navigator Four</span>
    </el-menu-item>
  </el-menu>
</template>

<script setup></script>

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

  1. layout - index.vue,导入 Menu使用
<template>
  <el-container class="app-wrapper">
    <el-aside width="200px" class="sidebar-container">
      <Menu />
    </el-aside>
    <el-container class="container">
      <el-header>Header</el-header>
      <el-main>Main</el-main>
    </el-container>
  </el-container>
</template>

<script setup>
import Menu from './Menu'
</script>
  1. 发送请求拿到数据,src - api - 新建 menu.js
import request from './request'

export const menuList = () => {
    return request({
        url: '/menu'
    })
}
  1. 回到 layout - Menu - index.vue中,导入
<template>
  <el-menu
    active-text-color="#ffd04b"
    background-color="#545c64"
    class="el-menu-vertical-demo"
    default-active="2"
    text-color="#fff"
    router
  >
    <el-sub-menu :index="item.id" v-for="item in menusList" :key="item.id">
      <template #title>
        <el-icon><location /></el-icon>
        <span>{{ item.authName }}</span>
      </template>
      <el-menu-item
        :index="'/' + it.path"
        v-for="it in item.children"
        :key="it.id"
      >
        <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 { menuList } from '@/api/menu'
import { ref } from 'vue'

const menusList = ref([])
const initMenusList = async () => {
  menusList.value = await menuList()
}
initMenusList()
</script>

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

  1. 将 所有路由页面粘贴到 views 目录下,router - index.js 路由配置,在 layout - index.vue中的 el-main 标签中写入 <router-view />,菜单点开了很多项,我们只展开一项,在el-menu 标签加入 unique-opened
  2. 因为 router - index.js 中 加入了 redirect,在 Menu-index.vue 中设置默认选中的内容
const defaultActive = ref(sessionStoreage.getItem('path') || '/users')

const savePath = (path) =>{
	sessionStoreage.setItem('path', `/${path}`)
	
}
<el-menu
	:default-active="defaultActive"
>
<el-sub-menu>
	<el-menu-item
		@click="savePath(it.path)"
	>
	</el-menu-item>
</el-sub-menu>
</el-menu>
  1. 使用小图标,安装 npm install @element-plus/icons-vue,main.js 配置 import * as ElIcons from '@element-plus/icons-vue',把所有的图片作为全局图片循环, Menu - index.vue 中定义小图标,删除原来的 ,,加入新的 <componenet :is="iconList[index]"></component>,复制,二级菜单显示
for(const iconName in ElIcons) {
	app.componnent(iconName, ElIcons[iconName])
}
// 一级图标
const iconList = ref(['user', 'setting', 'shop', 'tickets', 'pie-chart'])
// 二级图标
const icon = ref('menu')
	<template #title>
	   <el-icon>
	     <component :is="iconList[index]"></component>
	   </el-icon>
	   <span>{{ item.authName }}</span>
	 </template>
	 <template #title>
	     <el-icon>
	       <component :is="icon"></component>
	     </el-icon>
	     <span>{{ it.authName }}</span>
	   </template>
 </el-menu-item>

被动退出

1.src 目录下创建一个 tuils文件夹下创建一个 auth.js,再定义一个常量值的文件 constant.js
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
}

constant.js

export const TOKEN_TIME = 'tokenTime'

export const TOKEN_TIME_VALUE = 2 * 60 * 60 * 1000

2.在发起请求时比较 , request.js,在 vuex 中做退出的操作,在 sotre - modules - app.js 中定义 logout 方法,设置 一下登录时间

request.js

import { diffTokenTime } from '@/utils/auth'
import store from "@/store"

service.interceptors.request.use(
    (config) => {
        if (localStorage.getItem('token')) {
            if (diffTokenTime()) {
                store.dispatch('app/logout')
            }
        }

app.js

// 执行登录操作
        login({commit}, userInfo) {
            // resolve 正常调用该方法
            // reject 异常调用
            return new Promise((resolve, reject) => {
                // userInfo 将用户信息传入进去
                loginApi(userInfo).then(res => {
                    commit('setToken', res.token)
                    setTokenTime()
                    // 成功之后跳转
                    router.replace('/')
                    resolve()
                }).catch(err => {
                    reject(err)
                })
            })
        },
        // 退出 
        logout ({commit}) {
            commit('setToken', '')
            localStorage.clear()
            router.replace('/login')
        }

汉堡闹钟伸缩项

1.layout - headers - index.vue
2. 设置 layout -index.vue

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

import Headers from './headers'
import variables from '@/styles/variables.scss'
import { ref } from 'vue'
const asideWidth = ref(variables.sidBarWidth)
  1. headers - component是- hamburger.vue,headers - index.vue 导入 hamburger.vue
  2. store - modules - app.js ,定义小图标初始值,在 headers - components - hamburger.vue 定义事件,为了方便拿到 siderType 值,在 store - getters.js 中定义

app.js

state: () => ({
	// 初始值
	siderType: true
)}
mutations: {
	changeSiderType(state) {
		state.siderType = !state.siderType
	}
}

hamburger.vue

<div class="hamburger-container" @click="toggleClick">
<svg-icon :icon="icon"></svg-icon>

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

getters.js

export default {
	siderType: (state) => state.app.siderType
}
  1. 实现 menus 伸缩, 在 Menu - index.vue,在 layout - index.vue 将 ref 改成 computed

Menu - index.vue

<el-menu
	:collapse="!$store.getters.siderType"
></el-menu>

layout -index.vue
import { ref } from ‘vue’

<el-container
	:class="{ hidderContainer: !$store.getters.siderType }"
>


import { computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
// const asideWidth = ref(variables.sideBarWidth)
const asideWidth = computed(() => {
	return store.getters.siderType ? variables.sideBarWidth : variables.hideSideBarWidth
})

动态面板屑导航

  1. layout - headers - components - breadcrumb.vue
<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 { watch, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()

const breadcrumbList = ref([])

const initBreadcrumblist = () => {
  breadcrumbList.value = route.matched
}

const handleRedirect = (path) => {
  router.push(path)
}

watch(
  route,
  () => {
    initBreadcrumblist()
  },
  { 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>

  1. headers - index.vue 导入 breadcrumb.vue

头像退出

  1. layout - headers - components - avatar.vue,headers - index.vue 导入 avatar.vue,avatar.vue 实现下拉菜单,添加退出事件

avatar.vue

<template>
  <el-dropdown>
    <span class="el-dropdown-link">
      <el-avatar shape="square" :size="40" :src="squareUrl"></el-avatar>
    </span>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item @click="logout">退出</el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script setup>
import { ref } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const squareUrl = ref(
  'https://img0.baidu.com/it/u=1056811702,4111096278&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500'
)

const logout = () => {
  store.dispatch('app/logout')
}
</script>

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

headers - index.vue,import Avatar from './components/avatar.vue'

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

i18n 初使用

1.安装i18n,npm i vue-i18n@next
2.src - i18n - index.js

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 'en'
}

const i18n = createI18n({
    legacy: false,
    globalInjection: true,
    locale: getCurrentLanguage() || 'zh',
    messages: messages
})

export default i18n

3.main.js 中导入 import i18n from

import i18n from '@/i18n'
app.use(store).use(router).use(i18n).mount('#app')

4.修改 views - login - index.vue

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

5.导入资源,将 en.js 和 zh.js,修改 i18n - index.js

import EN from './en'
import ZH from './zh'
const messages = {
    en: {
        ...EN
    },
    zh: {
       ...ZH
    }
}

6.修改 login - index.vue

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

<el-button type="primary" class="login-button" @click="handleLogin">{{
  $t('login.btnTitle')
}}</el-button>

7.修改 layout - headers - components - breadcrumb.vue

<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index">
      <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>
    </el-breadcrumb-item>

8.修改 Menu - index.vue

<template #title>
          <el-icon>
            <component :is="icon"></component>
          </el-icon>
          <span>{{ $t(`menus.${it.path}`) }}</span>
        </template>

中英文切换

1.layout - headers - components - 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'
import { useStore } from 'vuex'
const i18n = useI18n()
const store = useStore()
const currentLanguage = computed(() => {
  return i18n.locale.value
})

const handleCommand = (val) => {
  i18n.locale.value = val
  store.commit('app/changLang', val)
  localStorage.setItem('lang', val)
}
</script>

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

2.headers - index.vue 引入 lang.vue

3.store - modules - app.js

state: () => ({
	lang: localStoreage.getItem('lang') || 'zh'
}),
mutations: {
	changLang (state, lang) {
		state.lang = lang
	}
}

全屏

1.安装 npm install screenfull@5.1.0
2.layout - headers - components - screenFull.vue, headers - index.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, onMounted, onBeforeMount } from 'vue'

const icon = ref(screenfull.isFullscreen)
const handleFullScreen = () => {
  if (screenfull.isEnabled) {
    screenfull.toggle()
  }
}

const changeIcon = () => {
  icon.value = screenfull.isFullscreen
}

onMounted(() => {
  screenfull.on('change', changeIcon)
})

onBeforeMount(() => {
  screenfull.off('change')
})
</script>

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

headers - index.vue

<div class="navbar-right">
  <screen-full class="navbar-item" />
  <Lang class="navbar-item" />
  <Avatar class="navbar-item" />
</div>

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

引导页

1.安装 npm install driver.js
2.layout - headers - components - driver - index.vue,headers - index.vue 引入
driver - index.vue

headers - index.vue

import Driver from './components/driver'
<div class="navbar-right">
  <Driver class="navbar-item" />
  <screen-full class="navbar-item" />
  <Lang class="navbar-item" />
  <Avatar class="navbar-item" />
</div>

3.driver - steps.js
4.监听语言的一个改变, i18n - watchlang.js
,store - getters.js定义 lang,方便我们拿到语言

export default {
	lang: state => state.app.lang
}

表格静态页

1.views - users - index.vue

<template>
  <el-card>
    <el-row :gutter="20" class="header">
      <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
        :width="item.width"
        :prop="item.prop"
        :label="$t(`table.${item.label}`)"
        v-for="(item, index) in options"
        :key="index"
      >
        <template v-slot="{ row }" v-if="item.prop === 'mg_state'">
          <el-switch v-model="row.mg_state" />
        </template>
        <template #default v-else-if="item.prop === 'action'">
          <el-button type="primary" size="small">Primary</el-button>
          <el-button type="warning" size="small">Warning</el-button>
          <el-button type="danger" size="small">Danger</el-button>
        </template>
      </el-table-column>
    </el-table>
  </el-card>
</template>

<script setup>
import { Search } from '@element-plus/icons-vue'
import { ref } from 'vue'
import { getUser } from '@/api/users'
import { options } from './options'
const queryForm = ref({
  query: '',
  pagenum: 1,
  pagesize: 2
})

const tableData = ref([])

const initGetUsersList = async () => {
  const res = await getUser(queryForm.value)
  console.log(res)
  tableData.value = res.users
}

initGetUsersList()
</script>

<style lang="scss" scoped>
.header {
  padding-bottom: 16px;
  box-sizing: border-box;
}
</style>

2.src - api -users.js

import request from './request'

export const getUser = (params) => {
    return request({
        url: '/users',
        params
    })
}

3.循环遍历 users - options.js

export const options = [
    {
        label: 'username',
        prop: 'username'
    },
    {
        label: 'email',
        prop: 'email',
        width: 200
    },
    {
        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: 300
    },
]

全局属性

1.处理时间, utils - filters.js,安装第三方库:npm install dayjs --save

const dayjs = require('dayjs')

const filetrTimes = (val, format = 'YYYY-MM-DD') => {
    if (!isNull(val)) {
        val = parseInt(val) * 1000
        return dayjs(val).format(format)
    } else {
        return '--'
    }
}

export const isNull = (date) => {
    if (!date) return true
    if (JSON.stringify(date) === '{}') return true
    if (JSON.stringify(date) === '[]') return true
}

// 使用 vue 全局属性
export default (app) => {
    app.config.globalProperties.$filters = {
        filetrTimes
    }
}

2.main.js 导入

import filters from './utils/filters'
filters(app)

分页器

1.编辑 users - index.vue,加上 @click,实现搜索功能,加上 el-pagination 放在 el-table 下方,实现分页

<el-button type="primary" :icon="Search" @click="initGetUsersList">{{
        $t('table.search')
      }}</el-button>
      <el-button type="primary">{{ $t('table.adduser') }}</el-button>

<el-pagination
      v-model:currentPage="queryForm.pagenum"
      v-model:page-size="pageSize4"
      :page-sizes="[2, 5, 10, 15]"
      :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 tableData = ref([])

const initGetUsersList = async () => {
  const res = await getUser(queryForm.value)
  total.value = res.total
  tableData.value = res.users
}

initGetUsersList()

const handleSizeChange = (pageSize) => {
  queryForm.value.pagenum = 1
  queryForm.value.pagesize = pageSize
  initGetUsersList()
}

const handleCurrentChange = (pagenum) => {
  queryForm.value.pagenum = pagenum
  initGetUsersList()
}

添加用户

1.src - api - user.js

export const changeUserState = (uid, type) => {
    return request({
        url: `users/${uid}/state/${type}`,
        method: 'put'
    })
}

2.users - index.vue,导入 changeUserState

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

<template v-slot="{ row }" v-if="item.prop === 'mg_state'">
  <el-switch v-model="row.mg_state" @change="changeState(row)" />
</template>



3.不是自己的,使用 i18n

import { useI18n } from 'vue-i18n'
const i18n = useI18n()
const changeState = async (info) => {
  await changeUserState(info.id, info.mg_state)
  ElMessage({
    message: i18n.t('message.updeteSuccess'),
    type: info.mg_state ? 'success' : 'error'
  })
}

4.users - components - dialog.vue,users - index.vue 导入 dialog.vue,dialog 父组件加上 v-mode, 子组件改为 :mode-value,给添加按钮加上点击事件,定义事件,定义数据

5.点击编辑时, users - index.vue,定义一个值

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

const dialogTitle = ref('')
const handieDialogValue = () => {
  dialogTitle.value = '添加用户'
  dialogVisible.value = true
}

6.users - components - dialog.vue 接受

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

defineProps({
  dialogTitle: {
    type: String,
    default: '',
    required: true
  }
})

7.src - api -user.js 创建接口

export const addUser = (data) => {
    return request({
        url: 'users',
        method: 'post',
        data
    })
}

export const deleteUser = (id) => {
    return request({
        url: `users/${id}`,
        method: 'delete'
    })
}

8.users - components - dialog.vue 导入

import { addUser } from '@/api/users'
const handleConfirm = async () => {
  await addUser(form.value)
  ElMessage({
    message: i18n.global.t('message.updeteSuccess'),
    type: 'success'
  })
  handleClose()
}

9.使用自己的 i18n

import i18n from '@/i18n'
import { ElMessage } from 'element-plus'
const handleConfirm = async () => {
  await addUser(form.value)
  ElMessage({
    message: i18n.global.t('message.updeteSuccess'),
    type: 'success'
  })
  handleClose()
}

10.清空上一次添加的值,users - index.vue,加上 v-if

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

编辑用户

1.统一校验,users - components - dialog.vue

const handleConfirm = () => {
  formRef.value.validate(async (valid) => {
    if (valid) {
      await addUser(form.value)
      ElMessage({
        message: i18n.global.t('message.updeteSuccess'),
        type: 'success'
      })
      handleClose()
    } else {
      console.log('error submit!!')
      return false
    }
  })
}

2.分发事件,users - components - dialog.vue,users.index.vue 绑定

const emits = defineEmits(['update:modelValue', 'initUserList'])
const handleConfirm = () => {
  formRef.value.validate(async (valid) => {
    if (valid) {
      await addUser(form.value)
      ElMessage({
        message: i18n.global.t('message.updeteSuccess'),
        type: 'success'
      })
      emits('initUserList')
      handleClose()
    } else {
      console.log('error submit!!')
      return false
    }
  })
}

3.users - index.vue,方法加一个()不带值,#default="{ row }" 解构出 row,

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

<el-button
  type="primary"
  size="small"
  :icon="Edit"
  @click="handleDialogValue(row)"
  
></el-button>
<Dialog
  v-model="dialogVisible"
  :dialogTitle="dialogTitle"
  v-if="dialogVisible"
  @initUserList="initGetUsersList"
  :dialogTableValue="dialogTableValue"
/>

import { isNull } from '@/utils/filters'
const dialogTableValue = ref({})
const handleDialogValue = () => {
  if (isNull(row)) {
    dialogTitle.value = '添加用户'
    dialogTableValue.value = {}
  } else {
    dialogTitle.value = '编辑用户'
    dialogTableValue.value = JSON.parse(JSON.stringify(row))
  }

  dialogVisible.value = true
}

4.users - components - dialog.vue,导入 watch ,监听值的改变

import { defineEmits, ref, defineProps, watch } from 'vue'
watch(
  () => props.dialogTableValue,
  () => {
    form.value = props.dialogTableValue
  },
  { deep: true, immediate: true }
)

<el-form-item label="密码" prop="password" v-if="dialogTitle === '添加用户'">

5.添加编辑接口, src - api - users.js

export const editUser = (data) => {
    return request({
        url: `users/${data.id}`,
        method: 'put',
        data
    })
}

6.users - components - dialog.vue,导入接口

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

props.dialogTitle === '添加用户' ? await addUser(form.value) : await editUser(form.value)

删除用户

1.users - index.vue

<el-button
  type="danger"
  size="small"
  :icon="Delete"
  @click="delUser(row)"
></el-button>
import { getUser, changeUserState, deleteUser } from '@/api/users'

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'
      })
    })
}
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值