文章目录
0. 学习视频
1. 前期工作
- cmd
- vue ui
- 创建项目
- 这里没有选择使用代码严谨
- 安装axios依赖
- 前期依赖一览
- 这里没有选择使用代码严谨
- 代码格式化
- 如果没有生效 【右键 …配置格式化文档 】
- 如果没有生效 【右键 …配置格式化文档 】
2. 项目
1. element-plus
npm install element-plus --save
1. 按需导入element
npm install -D unplugin-vue-components unplugin-auto-import
这里出现 bug
node版本和其不匹配 需要node升级
所以这边配置使用的是全局引入
- 按需导入的Webpack设置
module.exports = {
configureWebpack: (config) => {
config.plugins.push(
AutoImport({
resolvers: [ElementPlusResolver()]
})
)
config.plugins.push(
Components({
resolvers: [ElementPlusResolver()]
})
)
},
}
- script中引入所需element
import { ElMessage, ElMessageBox } from "element-plus";
2. 全局导入
- 在main.js中引入
// elementplus全局配置
import 'element-plus/dist/index.css'
import ElementPlus from 'element-plus'
app.use(ElementPlus).mount('#app')
2. vue3新特性
- 异步写法
- 使用then
getAllNews().then((res) => {});
- 没有then这个操作 就要用async和await
- data
vue2中的data值 就是 vue3中的const常量
切记 vue3取值都是从value取的
- 不需要根标签包裹(尽量还是进行包裹)
- css可以直接绑定js变量(如下)
3. css初始化
- main.js中导入
// 全局配置css
import '@/styles/index.scss'
4. 初始化测试
- element-plus版本更新太快,修改:
npm i element-plus@1.3.0-beta.5
1. login页面 + login路由
{
path: '/login',
name: 'Login',
component: () => import('../views/login/index.vue')
},
- 双向数据绑定 :model=“form”
<el-form ref="formRef" :model="form" class="login-form" :rules="rules"></el-form>
- 从vue中导入ref【绑定静态数据】
import { ref } from 'vue'
- 获取form表单
const form = ref({
// 将数据固定 方便
username: 'admin',
password: '123456'
})
ref 后面在 vue3 的官方博客里有发布说大幅提升了性能,所以后面的无论什么数据类型都会使用 ref
2. SVG组件
封装成组件 提高利用率
- 安装插件
npm i --save-dev svg-sprite-loader@6.0.9
- 修改webpack配置vue.config.js
- 在main.js中引入
import SvgIcon from '@/icons'
SvgIcon(app)
- 获取方法【iconfont】
- 文件自命名 使用的时候用icon=“文件名”
- 文件自命名 使用的时候用icon=“文件名”
- 使用
<svg-icon icon="user" class="svg-container"></svg-icon>
<svg-icon icon="password"></svg-icon>
- 关于密码
ElementPlus 自带切换功能,<el-input type=“password” show - password />即可
3. 登录页面的基本设置【请求前】
- 定制rules校验规则 在form中绑定
- 点击登录按钮 实现校验事件
vue3.2的语法
const handleLogin = () => {
formRef.value.validate(async (valid) => {
if (valid) {}
else {}
})
}
- 发起请求操作
src\api\request.js
- 生产环境
.env.development
ENV = 'development'
VUE_APP_BASE_API = '/api'
- 开发环境
.env.production
ENV = 'production'
VUE_APP_BASE_API = '/prod-api'
- 跨域问题
vue.config.js
devServer: {
https: false,
// hotOnly改为hot
hot: false,
proxy: {
'/api': {
target: 'http://43.143.0.76:8889/api/private/v1/',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
},
- 设置login请求
4. 登录页面的基本设置【请求中】
- login页面 点击登录按钮 触发事件
- app是命名空间,因为分模块了,login在app命名空间下,所以使用app/login
const formRef = ref(null)
const handleLogin = () => {
formRef.value.validate(async (valid) => {
if (valid) {
// 这里没有进行保存个人信息的操作 只保存了token
// 利用 vuex 进行异步请求【action】 进行路由跳转
store.dispatch('app/login', form.value)
console.log(form.value);
// 直接进行异步请求 查看返回数据
const res = await login(form.value)
console.log(res);
} else {
console.log('error submit!!')
return false
}
})
}
5. 请求拦截器和响应拦截器
- 响应拦截器
解构数据 将返回的信息变成我们希望的格式
// 响应请求拦截器
// 例将res.data.data中的data直接取出
service.interceptors.response.use(
(response) => {
console.log(response);
// 解构 取出 data和meta
const { data, meta } = response.data
// 进行判断
if (meta.status === 200 || meta.status === 201) {
// 如果meta值正确返回data
return data
} else {
// 错误时消息提醒
ElMessage.error(meta.msg)
return Promise.reject(new Error(meta.msg))
}
},
// 如果没有响应
(error) => {
console.log(error.response)
error.response && ElMessage.error(error.response.data)
return Promise.reject(new Error(error.response.data))
}
)
- 通过vuex二次存储token
在登录操作的时候 已经获取了token 并在localStorage存储
state: () => ({
token: localStorage.getItem('token') || '', // 如果localStorage中有就取出 否则为空
}),
// 同步方法
mutations: {
setToken(state, token) {
state.token = token
localStorage.setItem('token', token)
},
},
action 中完成异步方法
// 接受一个用户信息
login({ commit }, userInfo) {
// 发起请求
return new Promise((resolve, reject) => {
loginApi(userInfo) //接口
.then((res) => {
console.log(res)
// 如果成功 调用mutations中的setToken 将res.token传值
commit('setToken', res.token)
setTokenTime()
// 成功之后路由跳转
router.replace('/')
resolve()
})
.catch((err) => {
reject(err)
})
})
},
index.js中导入app
import { createStore } from 'vuex'
import app from './modules/app'
import getters from './getters'
export default createStore({
modules: {
app
},
getters
})
- 请求拦截器
设置请求头
// 请求拦截器+路由守卫
service.interceptors.request.use(
(config) => {
// 为每一个接口加上独有的token
// 这里后面会有一个对token时间的判断
config.headers.Authorization = localStorage.getItem('token')
return config
},
(error) => {
return Promise.reject(new Error(error))
}
)
- 路由守卫
src\router\permission.js
此之前先设置vuex getters 方便获取数据
// 白名单 用户没有登录也可以访问的页面
const whiteList = ['/login']
router.beforeEach((to, from, next) => {
// token存在
if (store.getters.token) {
// 路径是否在login
if (to.path === '/login') {
// 登陆了就无需留在登录页面
next('/')
} else {
next()
}
} else {
// 没有token
if (whiteList.includes(to.path)) {
next()
} else {
next('/login')
}
}
})
在main.js中导入路由守卫
这是一份js文件,引入js文件的时候不用名字接收,就等于直接执行这个文件的内容
结合起来,就是建立一个全局路由守卫。
import '@/router/permission'
5. layout
- 组件
- 路由
- 在webpack配置css
css: {
loaderOptions: {
sass: {
// 8版本用prependData:
prependData: `
@import "@/styles/variables.scss"; // scss文件地址
@import "@/styles/mixin.scss"; // scss文件地址
`
}
}
}
layout中的el-main使用< router-view > < /router-view >标签
6. 导入菜单
const menusList = ref([])
const initMenusList = async () => {
console.log("=========================");
menusList.value = await menuList()
// const res = await menuList()
// console.log(res);
console.log(menusList);
}
initMenusList()
- 配置路由
- 定义一个默认的展开项
//这个值不能被写死 每次点击的时候会发生改变
const defaultActive = ref(sessionStorage.getItem('path') || '/users')
- 点击事件【对后台返回的菜单列表进行循环操作 取出每一个菜单】
- 每一次点击的时候 就会将这个路径存储
// 将path值存储
const savePath = (path) => {
sessionStorage.setItem('path', `/${path}`)
}
- 所以在存在路径的时候就选择已经存储的路径 否则默认/users
// 定义一个默认的展开项
const defaultActive = ref(sessionStorage.getItem('path') || '/users')
7. 被动退出 当token过期
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()
console.log("currentTime=",currentTime);
const tokenTime = getTokenTime()
console.log("tokenTime=",tokenTime);
return currentTime - tokenTime > TOKEN_TIME_VALUE
}
export const TOKEN_TIME = 'tokenTime'
export const TOKEN_TIME_VALUE = 2 * 60 * 60 * 1000
// 设置常量 设置有效期时间 6000 = 6s
在请求拦截器中查看是否过期 超过有效时间则自动退出
// 请求拦截器+路由守卫
service.interceptors.request.use(
(config) => {
if (localStorage.getItem('token')) {
if (diffTokenTime()) {
// 通过vuex实现一个退出
store.dispatch('app/logout')
return Promise.reject(new Error('token 失效了'))
}
}
// 为每一个接口加上独有的token
config.headers.Authorization = localStorage.getItem('token')
return config
},
(error) => {
return Promise.reject(new Error(error))
}
)
// 退出 将token值清空 存储清空 跳转到登录页面
logout({ commit }) {
commit('setToken', '')
localStorage.clear()
router.replace('/login')
}
登录的时候设置时间 setTokenTime()
login({ commit }, userInfo) {
// 发起请求
return new Promise((resolve, reject) => {
loginApi(userInfo) //接口
.then((res) => {
console.log(res)
// 如果成功 调用mutations中的setToken 将res.token传值
commit('setToken', res.token)
setTokenTime()
// 成功之后路由跳转
router.replace('/')
resolve()
})
.catch((err) => {
reject(err)
})
})
},
8. 汉堡按钮伸缩项
src\layout\headers\components\hamburger.vue
- 点击事件【一下为具体操作】
-
- 引入useStore 使用vuex
import { useStore } from 'vuex'
import { computed } from 'vue'
-
- 点击事件切换侧边栏【这里是使用vuex中的同步方法】
const store = useStore()
const toggleClick = () => {
store.commit('app/changeSiderType')
}
-
- 更换icon
// 根据值切换图标 动态 :icon
const icon = computed(() => {
return store.getters.siderType ? 'hamburger-opened' : 'hamburger-closed'
})
- vuex中siderType相关
state: () => ({
// 默认值是一个true值
siderType: true,
}),
mutations: {
// 取反
changeSiderType(state) {
state.siderType = !state.siderType
},
},
- 折叠菜单绑定【element中有这个属性】
:collapse="!$store.getters.siderType"
- 【只设置这个发现文字可以缩下去但是菜单栏的宽度没有改变】
- 所以el-aside也需要设置值,不止是menu
// 菜单的宽度应该是一个动态值 可以伸缩 所以用computed而不是ref
// const asideWidth = ref(variables.sideBarWidth)
const asideWidth = computed(() => {
return store.getters.siderType
? variables.sideBarWidth
: variables.hideSideBarWidth
})
- el-container 同步做出伸缩【利用css】
.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);
}
}
10. 动态面包屑
- 获取路由
- 更新操作 watch
// 每次点击不同的标题需要更新路由 使用watch
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
// 路由表
console.log(route.matched)
}
// 跳转页面
const handleRedirect = (path) => {
router.push(path)
}
watch(
route,
() => {
initBreadcrumbList()
},
{ deep: true, immediate: true }
)
<!-- 动态面包屑 上级可以点 最后一级不能点-->
<el-breadcrumb separator="/">
<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> //这里用了i18n
<!-- {{$item.name}}</span> -->
<!-- 否则都可以点击跳转 -->
<span class="redirect" v-else @click="handleRedirect(item.path)">
{{$t(`menus.${item.name}`)}}</span>
<!-- {{item.name}}</span> -->
</el-breadcrumb-item>
</el-breadcrumb>
不使用点击事件切换路径 也可以使用to属性
< el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="index" :to="item.path">
10. 头像退出
<el-dropdown>
<span class="el-dropdown-link">
<!-- 头像组成 -->
<el-avatar shape="square" :size="40" :src="squareUrl"></el-avatar>
</span>
<template #dropdown>
<el-dropdown-menu>
<!-- 退出事件 调用action vuex-->
<el-dropdown-item @click="logout">退出</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
- 退出点击事件@click=“logout”
- 使用vuex异步请求【action异步方法】
import { useStore } from 'vuex'
const store = useStore()
const logout = () => {
store.dispatch('app/logout')
}
11. i18n
引入到了main.js 所以在初始化就会实现
npm install vue-i18n@next
src\i18n\index.js
const getCurrentLanguage = () => {
const UAlang = navigator.language // 浏览器中文zh-CN
// 是否为中文
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).use(ElementPlus).mount('#app')
- 子菜单中的中英文更改【因为这里后台返回的路径名字,即是,我们设置的目录名字(因果关系可以也倒过来)】
- 切换语言事件【封装为组件】
src\layout\headers\components\lang.vue
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'
import { useStore } from 'vuex'
const i18n = useI18n()
const store = useStore()
const currentLanguage = computed(() => { // 绑定 i18n.locale.value
return i18n.locale.value
})
const handleCommand = (val) => {
console.log(val);
i18n.locale.value = val
store.commit('app/changLang', val)
localStorage.setItem('lang', val)
}
- vuex中二次存储语言【先在main.js中 引入的文件已经存储到内存中 所以是二次存储】
state: () => ({
// 已经存储了则直接从缓存中保存
lang: localStorage.getItem('lang') || 'zh'
}),
mutations: {
changLang (state, lang) {
state.lang = lang
}
},
getters中方便获取的方法
lang: (state) => state.app.lang
12. 全屏功能
下载插件 screenfull
降低版本
13. 引导页
下载插件 drive.js
降低版本
14. 表格内容
1. 初始化数据
const initGetUsersList = async () => {
// console.log(queryForm.value);
// 有搜索值传query值
const res = await getUser(queryForm.value);
console.log(res);
total.value = res.total;
tableData.value = res.users;
};
initGetUsersList();
- 利用for循环将多条el-table-column变成一条【设置了一个js】
- 新学的方法 思路扩展 可实践
- 插槽问题
2. dayjs
全局属性
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
}
// 进行全局定义
export default app => {
app.config.globalProperties.$filters = {
filetrTimes
}
}
- 导入在main.js中
import filters from './utils/filters'
filters(app)
3. 分页器
<el-pagination
v-model:currentPage="queryForm.pagenum"
v-model:page-size="queryForm.pagesize"
: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 queryForm = ref({
query: "",
pagenum: 1,
pagesize: 2,
});
const total = ref(0);
const handleSizeChange = (pageSize) => {
queryForm.value.pagenum = 1;
queryForm.value.pagesize = pageSize;
initGetUsersList();
};
const handleCurrentChange = (pageNum) => {
queryForm.value.pagenum = pageNum;
initGetUsersList();
};
4. 修改用户状态/添加按钮/编辑按钮
- 修改用户状态
export const changeUserState = (uid,type) => {
return request({
url: `/users/${uid}/state/${type}`,
method:'put'
})
}
import { changeUserState } from "@/api/users"; //导入api
import { useI18n } from "vue-i18n"; // 使用内部的i18n
const i18n = useI18n();
简化操作 用async await
const changeState = async (info) => {
await changeUserState(info.id, info.mg_state);
// 利用i18n
ElMessage({
message: i18n.t("message.updeteSuccess"),
type: "success",
});
};
之前使用的是内部i18n 这里使用自己的i18n
src\i18n\index.js
import i18n from '@/i18n'
const t = i18n.global.t
doneBtnText: t('driver.doneBtnText'), // Text on the final button
- 添加用户的对话框
import Dialog from "./components/dialog.vue";
const dialogVisible = ref(false);
const dialogTitle = ref("");
const dialogTableValue = ref({});
<Dialog
v-model="dialogVisible"
:dialogTitle="dialogTitle"
v-if="dialogVisible"
@initUserList="initGetUsersList()"
:dialogTableValue="dialogTableValue"
/>
- 当点击按钮事件
const handleDialogValue = (row) => {
dialogVisible.value = true;
if(isNull(row)){ // 判断是否为空
dialogTitle.value = "添加用户";
dialogTableValue.value = {}
}else{
dialogTitle.value = "编辑用户"
dialogTableValue.value = JSON.parse(JSON.stringify(row))
}
};
- 使用model-value
// 需要修改这里的值update
// modelValue
import { defineEmits } from "vue";
const emits = defineEmits(["update:modelValue", "initUserList"]);
const handleClose = () => {
emits("update:modelValue", false);
};
- 确认按钮
const handleConfirm = () => {
formRef.value.validate(async (valid) => {
if (valid) {
props.dialogTitle === "添加用户"
? await addUser(form.value)
: await editUser(form.value);
ElMessage({
message: i18n.t("message.updeteSuccess"),
type: "success",
});
emits("initUserList");
handleClose();
} else {
console.log("error submit!!");
return false;
}
});
};
从父组件传入到子组件
import { defineProps } from "vue";
const props = defineProps({
dialogTitle: {
type: String,
default: "",
required: true,
},
dialogTableValue: {
type: Object,
default: () => {},
},
});
里面 再加一个
dialogVisible: { type: String, default: “”, required: false }
这样控制台就没有报错了
- 整体校验
formRef.value.validate(async (valid) => {
if (valid) {
handleClose()
} else {
return false;
}
});
-
判断row是否为空栏区分增加和修改
-
监听值的改变
watch(
() => props.dialogTableValue,
() => {
console.log(props.dialogTableValue);
// 写的位置要注意 这里已经用到了form
form.value = props.dialogTableValue;
},
{ deep: true, immediate: true }
);