摩鸣
一、前端添加token
1、登陆后在localStorage中存储token值。login.vue中:
loginUser() {
this.$refs.loginForm.validate((valid) => {/* 表单检验 */
if (valid) {
login(this.loginForm).then(resp => {
localStorage.setItem('token', resp.data.token);//登陆之后在localStorage中存储token值
})
} else {
2、在axios配置文件utils/request.js里作如下配置:
// 请求拦截器,添加token
request.interceptors.request.use(config => {
// 在发送请求之前做些什么
if (localStorage.getItem('token')) {
/* 如果后台要token 这么写:
config.headers.token = localStorage.getItem('token')
如果后台要Authorization 这么写:*/
config.headers['Authorization'] = localStorage.getItem('token');
}
return config;
}, error => {
二、页面的跳转:window.location.href 与 router.push
window.location.href:浏览器会重新载入整个页面,从而刷新页面状态。会造成更多的网络流量和页面重新渲染,但它可以更彻底地重置页面状态和数据。
router.push:在Vue.js应用程序内部实现路由跳转,最终实现页面的切换。可以避免重复载入HTML、CSS和其他静态文件,从而实现更快的页面切换速度。但它也会随着用户在应用程序中内部跳转的增加而增加内存的开销和页面状态的管理难度。
// 响应拦截器
request.interceptors.response.use(resp => {
/* 对响应数据做点什么*/
if (resp.status && resp.status == 200) {
if (resp.data.code == 200) {
return resp.data;
} else if (resp.data.code == 300) {
MessageBox.confirm('登录已过期,请重新登录', '提示', {
confirmButtonText: '确定',
type: 'warning',
showClose: false,
showCancelButton: false,
closeOnClickModal: false,
closeOnPressEscape: false,
}).then(() => {
localStorage.removeItem('powerMenu');
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';/* 重定向当前页面到Login */
}).catch(() => {})
} else {
Message.error({message: resp.data.msg});
return;
}
} else {
三、登录界面
1、el-input中添加具有一个唯一值的key属性,帮助 Vue.js 识别唯一的元素,避免复用元素。
<el-input
:key="passwordType"
ref="password"
v-model="loginForm.password"
:type="passwordType"
placeholder="密码"
name="password"
tabindex="2"
auto-complete="on"
clearable
@keyup.enter.native="handleLogin"
>
2、用户名为空时,聚焦到用户名输入框;用户名填写完,聚焦到密码框上
mounted() {
if (this.loginForm.number === '') {
this.$refs.number.focus()
} else if (this.loginForm.password === '') {
this.$refs.password.focus()
}
},
3、点击显示密码的图标,切换密码显示状态后,自动聚焦 password 输入框
<svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
methods: {
showPwd() {
if (this.passwordType === "password") {//眼睛关闭,输入框为密码框
this.passwordType = "";//眼睛打开,输入框为普通框
} else {
this.passwordType = "password";
}
this.$nextTick(() => {
this.$refs.password.focus();
});
},
nextTick:将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。
四、动态路由
在login.vue中,获取powerMenu,默认登录成功后进入powerMenu[0].children[0].path页面。
this.$router.replace(firstRoutePath, () => {})将当前路由替换成一个新的路由,并触发相应的路由组件渲染和相关生命周期函数。当使用 replace 方法进行路由跳转时,浏览器的回退按钮将不会回到已经替换掉的路由页面,而是回到前一个非替换页面。用于实现登录后的页面重定向。如果需要保留路由历史记录,应该使用 push 方法来进行路由跳转。
loginUser() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
login(this.loginForm).then(resp => {
const powerMenu = resp.data.powerMenu;
const firstRoutePath = powerMenu[0].children[0].path;
localStorage.setItem('token', resp.data.token);//登陆之后在localStorage中存储token值。token值是一个字符串类型。
localStorage.setItem('powerMenu', JSON.stringify(resp.data.powerMenu));
/* resp.data.powerMenu是一个 JavaScript 对象或值,localStorage.setItem()方法只能将字符串值存储在本地存储中,
因此需要使用JSON.stringify()方法将JavaScript对象或值转换为字符串。后续使用localStorage.getItem()方法获取该值时,
可以使用JSON.parse()将JSON字符串转换为JavaScript对象或值进行使用 */
this.$router.replace(firstRoutePath, () => {})
})
在store/modules/menus.js中:
const menus = {
state: {
powerMenus: [],
},
mutations: {
SET_POWERMENUS: (state, powerMenus) => {
state.powerMenus = powerMenus;
}
},
actions: {
GenerateRouters({ commit }) {
return new Promise(resolve => {
const resp = localStorage.getItem('powerMenu');
// 获取用户的菜单数据(powerMenu),并将其格式化成合适的路由格式
if(resp && resp !== null && resp !== undefined) {
const userMenus = JSON.parse(resp);
const sidebarRoutes = formatMenus(userMenus);/* 将userMenus格式化为侧边栏路由权限 */
commit('SET_POWERMENUS', sidebarRoutes);
resolve(sidebarRoutes);/* 返回侧边栏路由 */
}
})
}
}
}
/* formatMenus函数的作用是将传入的数组进行格式化,返回一个新的数组rewritePowerMenus。*/
export const formatMenus = (powerMenus) => {
// 如果变量是不需要被重新赋值的,应该使用 const。而如果变量需要被重新赋值,又或者需要在块级作用域内定义,就应该使用 let
let rewritePowerMenus = [];
powerMenus.forEach(ele => {/* 函数内部使用了forEach循环遍历powerMenus数组中的每个元素*/
let { id, path, component, name, icon, children } = ele;
/* 然后将元素中的id、path、component、name、icon、children等属性解构出来*/
if(children && children instanceof Array) {
children = formatMenusChildren(children);
};
let rewritePowerMenu = {/* 进行一些处理后,将处理后的数据存入一个新的对象rewritePowerMenu中*/
path: path,
name: name,
icon: icon,
children: children,
component: Layout
};
rewritePowerMenus.push(rewritePowerMenu);/* 并将该对象push到rewritePowerMenus数组中。*/
});
return rewritePowerMenus;
}
export const formatMenusChildren = (children) => {
let childernMenus = [];
children.forEach(ele => {
let { id, path, component, name, icon } = ele;
let childernMenu = {
path: path,
name: name,
icon: icon,
component: () => import ('@/views' + component)
}; /* component属性使用了动态导入的方式,将组件路径拼接后作为参数传入import函数中。*/
childernMenus.push(childernMenu);/* 并将该对象push到childernMenus数组中。*/
});
return childernMenus;
}
在main.js中使用 Vuex 中的dispatch 方法触发一个名为 GenerateRouters 的 action,这个 action 会返回一个 Promise 。此 action 会根据用户的权限信息(不同的powerMenus)生成一份动态路由记录列表,然后调用 router.addRoutes() 方法将这些路由动态地添加到路由表中。这个过程是异步的,等路由表更新完成后使用next({ …to, replace: true}) 方法进行路由跳转,参数 replace: true 表示直接替换当前的路由记录。
// 限制用户访问某些页面
router.beforeEach((to, from, next) => {
if(to.fullPath === '/' || store.getters.powerMenus.length > 0) {// 表示前往登录页面或者用户已经登录成功
next();
}else {
// resolve 方法是用于返回成功结果的,调用 resolve 方法后,then 方法中的回调函数会被执行,并将 resolve 方法中的参数作为函数参数进行传递。
// reject 方法则用于返回失败的结果,调用 reject 方法后,catch 方法中的回调函数会被执行,并将 reject 方法中的参数作为函数参数进行传递。
/* 使用 Vuex 中的dispatch 方法触发一个名为 GenerateRouters 的 action,这个 action 会返回一个 Promise.
此 action 会根据用户的权限信息(不同的powerMenus)生成一份动态路由记录列表,然后调用 router.addRoutes() 方法
将这些路由动态地添加到路由表中。这个过程是异步的,等路由表更新完成后使用
next({ ...to, replace: true}) 方法进行路由跳转,参数 replace: true 表示直接替换当前的路由记录。*/
store.dispatch('GenerateRouters').then(accessRoutes => {
// router.addRoutes(accessRoutes);/* 动态添加可访问路由表 */
for (let i = 0, length = accessRoutes.length; i < length; i += 1) {
const ele = accessRoutes[i];
router.addRoute(ele);//addRoutes已经废弃,改用addRoute
}
next({...to, replace: true})
// ...to是ES6中的对象属性扩展语法。它表示将to对象中的所有可枚举属性进行解构,并基于这些属性创建一个新的对象
})
}
})
五、子组件向父组件传值:判断展开收起图标是否被点击
在子组件Hamburger中,可以通过this.$emit方法来触发一个自定义事件toggleClick,并传递需要传递的数据和参数(此例无)。
<div style="padding: 0 15px;" @click="toggleClick">
methods: {
toggleClick() {
this.$emit('toggleClick')/* 子组件向父组件传值。vue官方推荐始终使用 kebab-case格式的事件名*/
}
}
在父组件TopBar.vue中,可以通过 v-on(简写为@ )来监听子组件的自定义事件toggleClick,在父组件中定义一个接收监听的事件toggleSideBar,并在回调函数中处理子组件传递过来的数据和参数。
<Hamburger :is-active="sidebar.opened" @toggleClick="toggleSideBar"></Hamburger>
methods: {
// 点击图标控制store/modules/app.js中的sidebar的opend的值为true/false
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
}
store/modules/app.js中:
const state = {
sidebar: {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : false,
/* 从Cookies中获取侧边栏状态,如果存在则将其转换为布尔值并赋值给opened变量,否则将opened变量赋值为false。
!!+ 能将字符串先转为number,再转换为布尔值类型 */
withoutAnimation: false
},
}
const mutations = {
TOGGLE_SIDEBAR: state => {/* 切换侧边导航栏的开关 */
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
}
}
const actions = {
toggleSideBar({ commit }) {
commit('TOGGLE_SIDEBAR')
}
}
点击展开/折叠的传值顺序:Hamburger/index.vue的toggleClick() --> TopBar.vue的toggleSideBar() --> store/modules/app.js的先actions再mutations再state --> Layout.vue的isCollapse() --> SideBar.vue的isCollapse()
六、父组件向子组件传值:变换图标
子组件Hamburger/index.vue中
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64">
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
/* :class="{'is-active':isActive}":这是一个动态 class 绑定,用于在 isActive 变量为 true 时给 SVG 标签添加一个名为 is-active 的 class。
通过 is-active 类,可以为汉堡菜单图标添加一个特效,使其在点击后变图标,以此表示菜单已经展开或关闭。
viewBox="0 0 1024 1024":这是 SVG 标签的一个属性,用于定义 SVG 图像的坐标系,默认情况下,SVG 图像坐标系的左上角是 (0, 0) 点,
而 viewBox 属性可以将坐标系进行缩放以适应更大或更小的屏幕。
xmlns="http://www.w3.org/2000/svg":这是一个 SVG 标签的命名空间,它指定了要使用的 XML 命名空间。在 SVG 中,所有的元素都必须带有此属性,以指定它属于 SVG 命名空间。
width="64":这是 SVG 图像的宽度,设置为 64 像素。
height="64":这是 SVG 图像的高度,设置为 64 像素。
<path d="" />:这是 SVG 标签的一个子元素,用于定义一条路径。 */
props规范写法:(https://blog.csdn.net/monika_beiluqi/article/details/108630257)
如果有默认值,而且是数组或者对象类型,props需要用规范写法:既必须用对象形式且必须 用函数返回。
默认值如果不是数组或者对象,则可以直接用数组形式写。(此例为布尔值)
props: {
isActive: {
type: Boolean,
default: false
}
},
在父组件TopBar.vue中,先引入子组件,再components里添加Hamburger,最后页面使用。
<template>
<!-- 顶部 -->
<div class="top-bar-container">
<!-- 菜单栏收起/展开 图标 -->
<Hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar"></Hamburger>
<!-- 个人中心 -->
<UserCenter style="margin-right: 15px; display: flex; align-items: center;"></UserCenter>
</div>
</template>
<script>
import Hamburger from '@/components/Hamburger';
import UserCenter from '@/components/UserCenter/index.vue';
import { mapGetters } from 'vuex';
export default {
name: 'TopBar',
components: {
Hamburger,
UserCenter,
},
data () {
return {
七、侧边栏绑定路由表
default-active="$route.path"是当前激活菜单的 index;router启用会在激活导航时以 index 作为 path 进行路由跳转。
<el-menu
:default-active="$route.path"
:unique-opened="true"
class="el-menu-vertical-demo"
background-color="#545c64"
text-color="#fff"
active-text-color="#4EA8E8"
:collapse="isCollapse"
:collapse-transition="false"
router>
<el-submenu v-for="(item, index) in powerMenus" :index="index + ''" :key="index">
<template slot="title">
<i :class="item.icon"></i>
<span>{{item.name}}</span>
</template>
<el-menu-item :index="children.path" v-for="(children, indexj) in item.children" :key="indexj" :router="children.path">
<template slot="title">
<i :class="children.icon"></i>
<span>{{children.name}}</span>
</template>
</el-menu-item>
</el-submenu>
</el-menu>
router为VueRouter的实例,相当于一个全局的路由器对象,里面含有很多属性和子对象,例如history对象。经常用的跳转链接就可以用this.$router.push,和router-link跳转一样。
route相当于当前正在跳转的路由对象。每一个路由都会有一个router对象,可以从里面获取name,path,params等。
computed: {
...mapState({
powerMenus: state => state.menus.powerMenus,
}),
...mapGetters(['sidebar']),
isCollapse() {
return !this.sidebar.opened;
}
},
具体见store/modules/app.js
八、解决控制台路由相关的黄色警告
1、router/index,js:
const routes = [
{
path: '/',
name: 'Login',
component: Login
},
{
path: '/home',
// name: 'Layout',//样的命名路由链接去跳转到包含默认子路由的“Layout”路由,
// 那么默认子路由将不会被渲染并显示。这会导致链接跳转到“Layout”路由时只显示默认路由名,而不显示实际内容
component: Layout,
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/Index')
}
]
}
]
const createRouter = () => new VueRouter({
// mode: 'history',
routes
})
const router = createRouter()
export function resetRouter () {
const newRouter = createRouter();
router.matcher = newRouter.matcher;
}
export default router
2、main.js:
// 如果模块使用的是命名导出,则需要使用大括号 {} 将导入的模块括起来,并且使用导出时指定的名称
import { resetRouter } from '@/router/index'
store.dispatch('GenerateRouters').then(accessRoutes => {
resetRouter() // 重置路由
// router.addRoutes(accessRoutes);/* 动态添加可访问路由表 */
for (let i = 0, length = accessRoutes.length; i < length; i += 1) {
const ele = accessRoutes[i];
router.addRoute(ele);//addRoutes已经废弃,改用addRoute
}
next({...to, replace: true})
九、if语句判断输入框不为空的代码改进
if (resp && resp.code == 200) {
//if (resp.data !== null && resp.data !== '' && resp.data !== undefined) {
if ((resp.data??'') !== '') {//基本语法是:a ?? b,表示如果a是 null 或 undefined,则返回b,否则返回a的值。
// if(this.addForm.content !== null || this.addForm.content != undefined) {
if( !!this.addForm.content ) {//双重非空断言运算符 !!会将一个非空值转换为 true,一个空值转换为 false,从而可以用来简洁地进行空值判断
十、搭建主体框架时部分代码进行了注释
(因为没有一边敲代码,一边记录,就截几张图吧…)
1、关闭个人中心后,重置表单
添加:destroy-on-close="true"即可
<template>
<div>
<el-dropdown @command="handleCommand" trigger="click">
<span class="el-dropdown-link">
<i class="avatar">
<span v-if="(avatar ?? '') === '' ">
<img src="@/assets/images/header.svg" class="faceImageClass">
</span>
<span v-if="(avatar ?? '') !== '' ">
<img :src="avatar" class="faceImageClass">
</span>
</i>
<i class="el-icon-caret-bottom"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="UserCenter">个人中心</el-dropdown-item>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-drawer
title="个人中心"
:visible.sync="drawer"
:direction="direction"
:close-on-press-escape="false"
:wrapperClosable="false"
:destroy-on-close="true">
<UserInfo :user="user"></UserInfo>
</el-drawer>
</div>
</template>
2、slot插槽
<!-- 密码重置 -->
<div class="module margin-top">
<el-card class="card pwd-height">
<div class="title">密码重置</div>
<div class="content pwd">
<!-- el-form、el-form-item分别表示表单、表单项。:model="" 表示表单的数据模型,:rules="" 表示表单的验证规则,ref="" 用于在代码中引用这个表单组件 -->
<el-form :model="userInfoForm" :rules="userInfoRules" ref="userInfoForm">
<!-- prop="" 表示表单项验证的字段名。slot="label" 表示将包裹 slot 内容的标签作为表单项的 label,表示表单项的标题内容-->
<el-form-item prop="oldPassword" label-width="80px">
3、密码更改后退出登录、表单清空报错解决
4、vuex
// computed 返回的值会被缓存,只有在依赖的响应式数据发生变化时才重新计算,而 methods 每次被调用时都会执行相应的方法内容
computed: {
...mapState({ // 辅助函数,用于在 Vue 组件中将 Vuex 的 state 映射为组件的计算属性
initMenus: state => state.menus.initMenus,
}),
...mapGetters(['sidebar']), // 辅助函数,用于在 Vue 组件中将 Vuex 的 getters 映射为组件的计算属性
isCollapse() {
// console.log('6:isCollapse:SideBar', this.sidebar.opened)
return !this.sidebar.opened;
}
},
5、动态路由
6、JSON转form-data格式
当需要在一个简单的循环中对每个元素执行相同操作,并且不需要在迭代中中途进行任何跳过、中断或异常处理时,forEach 可以是更好的选择。在需要更多控制或灵活性时,或需要性能更好的迭代过程时,可能需要使用 for 循环。
①、forEach 无法控制 index 的值, 遍历数组同时删除或添加某项的操作应避免使用forEach 。
②、for循环中会用到一些中断行为,对于优化数组遍历查找是很好的,但由于forEach属于迭代器,只能按序依次遍历完成,所以不支持上述的中断行为。
③、性能比较 :for > forEach > map
// 将json转换成form_data
// export const json2FormData = (jsonParam) => {
// const formData = new FormData();
// Object.keys(jsonParam).forEach((key) => {
// formData.append(key, jsonParam[key]);
// });
// return formData;
// }
export const json2FormData = (jsonParam) => {
const formData = new FormData(); // 创建了一个新的FormData对象,用于存储表单数据
for (const key in jsonParam) { // 遍历jsonParam对象的所有属性
if (Object.prototype.hasOwnProperty.call(jsonParam, key)) { // 判断jsonParam对象是否具有该属性,并且不是继承而来的
formData.append(key, jsonParam[key]); // 将属性名和对应的属性值添加到formData中
}
}
return formData;
}
7、头像
APP.vue中:
<script>
/* 解决在页面刷新时,由于页面状态管理的重置导致用户头像信息丢失的问题。
问题描述:更新了头像,一刷新页面,右上角头像又变成UserCenter/index.vue里<img src="@/assets/images/header.svg" class="faceImageClass">默认头像了 */
export default {
mounted() {
if ((localStorage.getItem('user') ?? '') !== '') {
window.addEventListener('loadAvatar', this.getAvatar());
}
},
methods: {
getAvatar () {
const userInfo = JSON.parse(localStorage.getItem('user'));
// console.log('userInfo:', userInfo)
this.$store.dispatch('SetAvatar', userInfo.faceImage);
}
},
}
</script>
十一、轮播图管理
1、V-slot:
<el-table-column prop="photoType" label="轮播图类型" width="150" align="center">
<template #default="scope">
<el-tag v-if="scope.row.photoType === 0" effect="dark">课程</el-tag>
<el-tag v-if="scope.row.photoType === 3" effect="dark" type="success">微课</el-tag>
<el-tag v-if="scope.row.photoType === 2" effect="dark" type="warning">素材</el-tag>
</template>
</el-table-column>
<el-table-column prop="photoUrl" label="图片" width="100" align="center">
<template v-slot="{ row }">
<viewer>
<img :src="row.photoUrl" class="photo-url" />
</viewer>
</template>
</el-table-column>
2、分页
<div class="pagination">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:page-sizes="[5, 10, 15, 20, 30]"
:page-size="queryPage.limit"
layout="total, sizes, prev, pager, next, jumper"
:total="totalCount"
:current-page.sync="queryPage.page"
background></el-pagination>
</div>
<!-- .sync 后缀是 Vue.js 提供的语法糖,它会自动生成一个名为 update:current-page 的事件,其中 current-page 为绑定属性名。在分页组件中,
当用户在分页组件中切换页码时,<el-pagination> 组件会触发 update:current-page 事件,从而将用户所选择的页码值更新到当前组件使用的页码数据中。
这样,就能实现分页组件与父组件之间的双向数据绑定。如果不使用 .sync 后缀,那么在分页组件中切换页码时,只会更新父组件中的 currentPage 数据,
而不会将新值反向更新到 <el-pagination> 组件中,这就导致了表象上的数据不同步。使用 .sync 后缀可以解决这个问题,保证数据在父子组件之间的同步。 -->
3、使用 computed 属性将模板中的表达式进行简化
<el-form-item prop="aboutId">
<span slot="label" class="slot-form-label">{{ photoTypeName }}</span>
<el-select
v-model="addForm.aboutId"
:placeholder="photoTypeSelect"
filterable
clearable
class="item-content-class"
>
computed: {
photoTypeName() {
const typeMap = {
0: '课程名称',
3: '微课名称',
2: '素材名称',
};
return typeMap[this.addform.photoType];
},
photoTypeSelect() {
const typeSelectMap = {
0: '请选择课程',
3: '请选择微课',
2: '请选择素材',
};
return typeSelectMap[this.addForm.photoType];
},
},
新增、修改、搜索轮播图的计算属性名称不变,修改返回值
4、轮播图文件名去除时间戳的代码优化
(解决多个时间戳问题)
// 根据文件路径获取文件名
// export const getFileName = (photoUrl) => {
// var a = photoUrl.substring(photoUrl.lastIndexOf("/") + 1, photoUrl.length);
// var b = a.substring(0, a.lastIndexOf("_"));//截取-去掉名称里的时间戳
// var c = a.substring(a.lastIndexOf("."), a.length);//截取后缀字符串
// var fileName = b + c;//拼接图片名称
// return fileName;
// }
/* http://mmkj.wh-mm.com:55/demo/slideshow/2023-02-09/机械设计_195259628_103357970.jpg */
export const getFileName = ( photoUrl ) => {
const regex = /_\d+(_\d+)*\./; // 匹配字符串中所有以 _ 开头的数字字符串,并且保留文件后缀名
const fileNameWithSuffix = photoUrl.split('/').pop(); // 获取文件名(包括后缀)
const fileName = fileNameWithSuffix.replace(regex, ".");
return fileName;
}
5、删除最后一页的数据(该页仅一条数据时),需做处理
async deleteBtn (row) {
try {
const response = await deleteSlideshowInfo({ids: row.id});
if (response && response.code == 200) {
// if (row.photoUrl) {
// URL.revokeObjectURL(row.photoUrl);
// };
Message.success('操作成功');
// 判断当前分页查询的最后一页是否只有一条记录,以及当前页是否在最后一页
if(Math.floor((parseInt(this.totalCount)- 1) / this.queryPage.limit) == (parseInt(this.totalCount)- 1) / this.queryPage.limit &&
((Math.ceil((parseInt(this.totalCount)/ parseInt(this.queryPage.limit)))) - parseInt(this.queryPage.page)) < 1){
this.queryPage.page = this.queryPage.page - 1
}//Math.floor 方法用于向下取整,Math.ceil 方法用于向上取整, / 运算符用于除法运算,% 运算符用于取模运算
this.getTableData();
};
} catch (error) {
console.error(error);
}
},
十二、类型管理
1、动态class绑定
<button
:class="{'buttonSuccessClass': !foldUnfold, 'buttonInfoClass': foldUnfold}"
@click="changeExpand">
<i v-if="!foldUnfold" class="el-icon-bottom"></i>
<i v-if="foldUnfold" class="el-icon-top"></i>
{{ !foldUnfold ? '全部展开' : '全部折叠' }}
</button>
2、展开/折叠
// 全部展开/全部折叠:changeExpand(): 点击按钮时,切换折叠展开状态。将foldUnfold的值取反,并调用forArr方法对tableData进行处理。
changeExpand() {
this.foldUnfold = !this.foldUnfold;
this.forArr(this.tableData, this.foldUnfold);
},
// 展开/折叠:forArr(arr, isExpand): 遍历数组arr中的每个元素,根据isExpand的值来展开或折叠元素。如果元素存在子元素(children),则递归调用forArr方法。
forArr(arr, isExpand) {// arr 表示要遍历的数组,isExpand 表示是否展开数组元素
arr.forEach( element => {// 循环遍历一个树形的数组中的每一个元素
this.$refs.tableData.toggleRowExpansion(element, isExpand);// 控制表格的行(row)的展开或折叠状态
if (element.children) {// 使用递归来遍历每个元素的嵌套子元素,直到所有子元素都被处理
this.forArr(element.children, isExpand);
}
})
},
// 展开/折叠-02:expandUnExpand(data): 递归遍历data数组中的每个元素,将元素的id转为字符串并添加到expandArr数组中。如果元素存在子元素(children),则递归调用expandUnExpand方法。
expandUnExpand(data) {// 遍历树形结构的数组expandData
data.forEach(element => {
this.expandArr.push(element.id.toString());
if (element.children) {
this.expandUnExpand(element.children);
}
})
},
// 搜索
searchTableData() {
if ((this.queryPage.typeName ?? '') !== '') {
getAllTypes({ typeName: this.queryPage.typeName }).then(resp => {
if (resp && resp.code == 200) {
if (resp.data.length > 0) {//搜索到类型名称数据
this.expandArr = [];
this.tableData = resp.data;
this.foldUnfold = true;
this.expandUnExpand(this.tableData);
} else {//列表里没有所搜索的数据
this.foldUnfold = false;
this.tableData = [];
}
}
})
} else {//没有输入就点击搜索
this.expandArr = [];
this.getTableData();
this.foldUnfold = false;
this.forArr(this.tableData, false);
}
},
3、文件上传校验
// 文件上传校验
uploadChange(file, fileList) {
if (file.status !== 'ready') return;
let fileName = file.name.substring(file.name.lastIndexOf('.') + 1);
if (fileName !== "xls" && fileName !== "xlsx") {
this.$message({
type: "error",
message: "只能上传xls/xlsx文件,请重新上传!"
})
// 去除上传失败列表仍然显示文件
let uid = file.uid;
let idx = this.$refs.uploadFile.uploadFiles.findIndex(item => item.uid === uid);
this.$refs.uploadFile.uploadFiles.splice(idx, 1);
return false;
}
if (fileList.length > 1) {
fileList.splice(0, 1);
}
this.importFile.file = file.raw;
return true;
},
十三、按钮权限
<div class="button">
<el-button class="button-search" v-if="permission['查询']" type="primary" @click="querySearch">
<i class="el-icon-search button-search-icon"></i>
搜索</el-button>
// 对象的属性可以通过两种方式进行访问: 使用.运算符或使用[]运算符。
data() {
return {
path: this.$route.path,
permission: {},
}
},
computed: {
...mapState({ permissions: state => state.menus.permissions })
},
async mounted() {
this.getTableData();
this.getBtnPermission();
await this.getSchoolClassSearch();
},
methods: {
// 获取按钮权限
getBtnPermission() {
return this.permission = this.permissions[this.path] || {};
},
十四、角色管理
具体见element ui开发指南,部分代码如下:
<el-form-item prop="roleMenu">
<span slot="label" class="slot-form-label">菜单权限</span>
<div class="flex">
<el-checkbox v-model="expandFold" @change="handleMenuTreeExpand($event)">展开/折叠</el-checkbox>
<el-checkbox v-model="allNo" @change="handleMenuAll($event)">全选/全不选</el-checkbox>
</div>
<el-tree
:data="menuTree"
show-checkbox
ref="menuKeys"
class="border"
node-key="id"></el-tree>
</el-form-item>
data() {
const checkRoleMenu = (rule, value, callback) => {
let arr = this.$refs.menuKeys.getCheckedKeys(); // 在此获取选中的树形数据
if (arr.length == 0 || !arr) {
callback(new Error('请选择菜单权限'));
} else {
callback();
}
}
// 展开/折叠
handleMenuTreeExpand(value) {
let menuTreeList = this.menuTree;
for ( let i=0; i < menuTreeList.length; i++ ) { // 遍历数组中的每个节点
this.$refs.menuKeys.store.nodesMap[menuTreeList[i].id].expanded = value; // el-tree 展开/折叠指定节点
};
},
// 全选/全不选
handleMenuAll(value) {
this.$refs.menuKeys.setCheckedNodes(value ? this.menuTree : []);
},
// 获取菜单节点
getMenuCheckedKeys() {
let checkedKeys = this.$refs.menuKeys.getCheckedKeys();// 全选中节点
let halfCheckedKeys = this.$refs.menuKeys.getHalfCheckedKeys();// 半选中节点(部分选中)
checkedKeys.unshift.apply( checkedKeys, halfCheckedKeys);// 合并全选中和半选中节点
return checkedKeys;
},
十五、CSS
1、text-align-last:用于设置最后一行文本的对齐方式为两端对齐
// label两端对齐
/deep/ .el-form-item__label{
text-align: justify;
text-align-last: justify;
}
2、position
position: fixed;
固定定位的用途:返回顶部、楼层导航等
position: absolute;
绝对定位的用途:不规律的布局、子绝父相、浮层、弹框
绝对定位可以结合z-index属性来实现压盖、遮罩效果
position: relative;
相对定位的用途:微调位置
① 导航条 ② 搜索框:文字或图标与文本框对不齐 ③ 其它
3、侧边栏样式
①
<style lang="less" scoped>
// /deep/ .el-submenu .el-menu-item {
// min-width: 0px !important;
// }
②
// /deep/ .el-menu {
// border-right: none !important;
// }
4、顶部栏样式
.hamburger-container {
// 展开/折叠按钮悬浮变色
transition: background .3s; // 当元素的背景颜色发生改变时,会以 0.3 秒的时间渐变过渡到新的背景颜色
-webkit-tap-highlight-color:transparent; // 用于调整移动设备上触摸高亮效果
&:hover {
background: rgba(0, 0, 0, .025);
}
}
5、布局样式
<div class="right">
<TopBar/>
<MainContent/>
</div>
// .right {
// flex: 1;
// display: flex;
// flex-direction: column; // 指定子元素在主轴上的排列方向为垂直方向,即从上到下排列
// background-color: #ECF3F8;
// }
6、修改el-table宽度无限放大问题
.el-table{
width: 100%;
}
/* 将表格容器内的表头和表体的宽度都设置为 100% */
.el-table__header-wrapper table,.el-table__body-wrapper table{
width: 100% !important;
}
/* 将表格主体、页脚和表头的 table-layout 属性设置为 auto,以允许表格根据内容来调整列宽。 */
.el-table__body, .el-table__footer, .el-table__header{
table-layout: auto;
}
7、display
参考: css盒子模型
8、flex
参考: 弹性布局