赞
踩
项目所需要的 资源——点击下载
Vue3.2版实现的基于element-plus的表单增删改查和部分内容
主要实现的功能有:
中英文切换
全屏
引导页
表单的CRUD
软件安装:
安装chrome
下载安装 nodejs
打开命令行
npm install -g @vue/cli
vue --version
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
能正常在浏览器中打开项目,说明项目创建完成
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
}
安装 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网站,组件:
将其中的一组按钮代码复制粘贴到 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 相关的按钮出现,则说明配置成功
vue3.2的模板为:
<template>
</template>
<script setup>
</script>
<style lang=”scss” scoped>
</style>
<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>
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
重启项目
注意其中的双向绑定 :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>
资源里的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>
重启项目
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。
创建目录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
登录页面,密码框输入的明文,暗文切换:
<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
})
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)
}
})
}
对每个请求都在头部增加token
app/request.js
service.interceptors.request.use(
(config) => {
config.headers.Authorization = localStorage.getItem('token')
return config
},
(error) => {
return Promise.reject(new Error(error))
}
)
用户必须在登录之后才能转到首页,在未登录时,任何路由都转向登录页面
创建文件 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
})
新建 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')
},
重新启动
创建文件、文件夹 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>
没有成功请求的一段时间后,自动退出
逻辑是设置两个常量值,一个保存最近请求时间的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)
})
})
},
头部实现:
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);
}
}
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里使用时提示找不到相应的方法。
在头部右侧的头像上单击,出现下拉菜单,点其中的退出菜单项,退出登录。
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')
}
安装
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>
从资源中复制国际化配置中的两个文件到 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>
在头部使用下拉菜单进行语言切换。
创建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>
使用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')
})
就是一步步引导用户操作。安装
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)
views/users/index.vue
表格的头部搜索区域
<template>
<el-card>
<el-row :gutter="20"> <!—20表示项之间的间距,默认有24份col-->
<el-col :span="7"> <!—设置搜索框占24个中的7个col-->
<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>
创建时间格式化,使用统一插件 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>
复制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()
}
修改用户状态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: '长度要为3到25',
trigger: 'blur'
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: 'blur'
},
{
min: 3,
max: 25,
message: '长度要为3到25',
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>
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;
}
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'
})
})
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。