写在前面
本人从事前端4个月以来参与了一个PC端vue项目和JS定制器练习,本博文是对PC端项目经验的总结,方便后续查找和翻阅。同时也希望自己的项目经验能够帮助到大家,不足之处,烦请大家批评指正,谢谢大家的支持。
PC端项目经验总结(排名不分先后,随心随性)
1.尽量少使用绝对定位,多使用万能的flex布局
如果一个页面使用很多绝对定位来布局,后期维护网页需要增添和删除内容修改样式时,会极为不便。使用父盒子包裹需要布局的内容,并给父盒子一个flex布局,可以很轻松地将不同的div和span放在一行显示。
2.网页请求优化
- 对同步模块框中不经常变化的接口,使用axios的config参数进行缓存。
/**
* 获取产品目录列表
* @param {object} catalog_id 请求参数
* @returns {Promise<Object|AxiosError>} 请求结果
*/
getProductsCatalogs(catalog_id) {
return this._get(`${catalog_id}/products/catalogs`, {
cache: true,
cache_seconds: 600
})
}
- 请求接口时去掉多余的参数。
- 可以使用web.dev对网站的性能进行测评。
3. 不能连写if条件中的判断条件
- 错误示范:
/**
* 获取分销商申请信息
*/
getDropshipApplyInfo() {
this.$storeDispatch('me/getDropshipperInfo', { skip_cache: true })
.onSuccess(({data}) => {
if(data.dropshipper.status === 'success' || 'pending') {
this.setMessagePromptBox(this.success_message, 'success')
this.$router.push(this.localePath({ name: 'dashboard' }))
} else {
this.setMessagePromptBox(this.$t('b_register.account_review'), 'error')
}
})
},
这个条件判断语句if(data.dropshipper.status === 'success' || 'pending')的做法是先判断data.dropshipper.status === 'success' 的值。如果值为真,就不会执行后面的判断条件,结果就为真;如果值为假,就会和 'pending'做或运算,得到 'pending'的结果,最后if会将这个字符串强制转换为布尔值true,所以最终结果也是真。即不管data.dropshipper.status === 'success' 的值是什么,最后的结果都为真,都会执行if条件语句中的内容。因此,不能连写if条件中的判断条件,不然会产生不同的判断语义。
-
正确示范:
if(['success', 'pending'].includes(data.dropshipper.status))
或者if (['success', 'pending'].indexOf(data.dropshipper.status) !== -1) -
特别注意:
此处请求不常用的接口(设置的缓存时间为120s )获取分销商申请信息, 使用到
{ skip_cache: true }。因为这个接口在C端用户进入B端发起申请分销商的请求后,如果返回的状态为成功,就会继续请求获取分销商申请信息的接口。如果此时不清除缓存,则不会发起获取分销商申请信息的请求,也就无法获取接口返回的状态了,因此需要清除缓存。所以区别于2.网页请求优化的第一点,给接口加缓存和清除缓存是要看情况的。
4 . 可以使用富文本编辑器(如vue-quill-editor)解析后端传入的html代码
5. 使用overflow-x:hidden来隐藏网页底部出现的滚动条
6.自定义组件的点击问题
如果在自定义组件中定义了点击事件,那么在父组件中调用自定义组件,点击会触发自定义组件中定义的点击事件。但是如果在自定义组件中没有定义点击事件,那么在父组件调用自定义组件时,给自定义组件增加点击事件,并在父组件中定义自定义组件的点击事件,此时点击将不会产生任何效果。这是因为在自定义组件中找不到定义的点击事件,如果需要触发点击事件,需要在父组件调用的自定义组件中增加.native修饰符或者在自定义组件的点击事件中传递一个click事件。
7.字符串模板``中使用${变量名}来获取变量
字符串模板只能在JS中引用,不能在html中使用。
8.当产品路由id发生变化时,轮播的当前索引并未重置
watch: {
'$route.params.id'() {
this.current_index = 0
}
}
9.根据路由地址判断是否为shopify页面
/**
* @returns {boolean} 是否是shopify页面
*/
is_shopify_page() {
return this.$route.path.indexOf('/shopify') === 0
}
shopify-app项目是基于B端代码进行兼容开发,网页大部分逻辑和样式都与B端相同。因此,对不同于B端的逻辑和样式需要使用is_shopify_page()方法进行判断,从而达到兼容开发互不影响的效果。
10.使用git和arcanist工具进行开发
arc branch fix //新建fix分支进行开发,需要安装和配置arcanist
git checkout develop //切换到develop分支
git pull //拉取最新代码
git add . //暂存代码
git commit -m 'fix(登录模块): 修正...的问题' //添加评论
arc 4d //提交代码审核申请
arc 4l //代码审核通过,拉取合并代码,并解决冲突后,将代码提交到仓库
11.使用vue 内置组件component切换不同模态框的显示与隐藏
<template>
<el-dialog
class="shopify-modal-wrapper"
:close-on-click-modal="false"
center
:visible.sync="is_show_sync_product_modal"
width="980px"
@close="closeModal"
>
<div slot="title">
<p>{{ current_title }}</p>
<p
v-if="product.shopify_product"
class="red synced-message"
>
{{ $t('shopify_app.the_product_has_been_synced') }}
</p>
</div>
<component
:is="current_component"
v-loading.fullscreen.lock="is_sync_product_modal_loading"
:product="product"
@close-modal="closeModal"
/>
</el-dialog>
</template>
给整个页面加loading可以结合v-loading.fullscreen.lock和vuex数据控制页面的加载状态。当点击Next按钮,则会切换为step2组件;当点击Back按钮,则会切换为step1组件;当关闭模态框时,会切换为默认的step1组件。
12.模态框的数据流问题(出错的原因纯属自己猜想,如有错误之处,麻烦评论区纠正,感谢)
- 公共产品模块:
- step1模态框:
- step2模态框:
-
问题1: 在第一个模态框修改数据后,点击进入第二个模态框。当切回第一个模态框时,发现第一个模态框修改的数据并未更新。
原因: 使用vue内置组件component控制的模态框相互独立,模态框切换的过程中,其余的模态框组件被销毁,而里面的数据并没有更新并保存下来,数据自然会丢失。如果要使模态框的切换成为一个连续的过程,可以在第一个模态框下一步按钮触发的next方法中,控制模态框的显示与隐藏。
做法:在step1组件下一步按钮触发的next方法中,使用vueX保存第一个模态框需要保存的数据。并在step1组件的created生命周期函数(只有在第一次点击同步按钮,显示第一个模态框和来回切换step1和step2组件的时候才触发生命周期函数)中,触发created生命周期函数中的第二个赋值函数,先判断vueX中存储的数据是否为空。
如果为空,就直接return。同时会触发created生命周期函数中的第一个赋值函数,获取产品的默认信息,将接口返回的产品信息展示到模态框上;如果不为空,就将vueX中存储的数据赋值给step1模态框需要使用的属性。总结:所以step1组件中的created生命周期函数存在
两个赋值函数。第一个赋值函数(用于每次重新打开step1模态框,初始化产品数据):当vueX数据为空(vueX的数据不存在或者每次在关闭模态框后被清空,问题2中会解释),说明是第一次打开step1组件或者关闭了模态框。此时step1模态框处于打开状态或者在打开step1模态框的状态,会将默认的产品信息展示到模态框上。这个赋值函数不论vueX数据是否为空都会被调用。
第二个赋值函数(用于从step2组件切换到step1组件,不会丢失修改的产品数据信息): 当vueX数据为空时,直接return,触发第一个赋值函数;当vueX中存在数据,说明已经进入过step2组件,此时切换到第一个模态框,会将vueX的数据赋值到step1组件中。此处需要特别注意,因为切换组件的过程会触发created生命周期函数,会同时调用两个赋值方法,此时将第二个赋值函数放在第二个,覆盖掉第一个的赋值即可。
vueX数据为空,则只会触发第一个赋值函数;vueX数据不为空,则会同时触发两个赋值函数,且第二个赋值函数由于在后面,会覆盖掉第一个赋值函数的值。 -
问题2:如果修改step1组件中的数据,切换到step2组件。然后关闭step1模态框,数据发生更新。此时再点击其他产品进行同步,step1组件默认都会展示相同的修改后的数据;如果修改step1组件中的数据,但不切换到step2组件,然后关闭step1模态框,数据不会发生更新;
原因:出现问题的原因在于step1组件中的数据是由vueX来控制,而vueX数据的保存是在点击Next按钮进入step2组件之后才触发。触发之后,会同时调用生命周期函数的两个赋值函数,此时模态框的初始值都被第二个赋值函数给覆盖掉了,所以每次打开模态框都会默认展示修改后的数据信息。因此需要在每次关闭模态框时,清空vueX中保存的step1组件的数据。
做法:监听
is_show_sync_product_modal(用于控制模态框是否显示的变量,在10中可以看到)的值。如果值为假,说明模态框处于关闭状态,就将保存在vueX中的step1数据清空。这样每次在step1组件被初始化时,就会调用created生命周期函数中的第一个赋值函数,展示默认的产品信息;如果值为真,说明模态框处于打开状态,则会同时调用created生命周期函数的两个赋值方法(这里也需要同时调用两个赋值方法的原因在于,created只有在第一次打开step1组件和切换组件的时候才会触发,is_show_sync_product_modal的值只要在第一个或者第二个模态框处于打开的状态就为真。
13.关于map()、foreach()、filter()、find()、sort()、some()和every()函数
这块内容有参考:掘金好文
对象.属性等效于对象[属性],对象.属性其实是简写。map()方法用于遍历数组,可以对对象数组返回的字段进行重构,不会改变原数组,返回值为一个新数组。但可以将返回的新数组赋值给原数组,达到改变原数组的目的,必须要加上return。map()方法不支持过滤处理,如下所示:
let newArr = [1,2,3,4,5].map(item => { if(item > 3) return item })
// => [undefined, undefined, undefined, 4, 5]
- 使用
map()方法获取对象数组某一项属性的值:
await getList(data).then((res) => {
if (!res.detail) return
this.currency = res.detail.map((item) => item.currency)
})
- 使用
map()方法可以给后端返回的接口增添字段:
//这个函数是initData混入里的,可以拿到后端返回的数据
//用到initData混入,就需要配合使用url
initCallBack() {
this.data.map((item) => {
//为后端返回的数据增添currentImageIndex和currentStyleIndex字段
item.currentImageIndex = 0
item.currentStyleIndex = 0
})
}
- 使用
map()方法可以重构自定义接口参数名称:
//() => ({}),使用()返回数据
this.selectCountry = this.selectCountry.map((item) => ({
id: item.id,
name: item.countryCnName
}))
- 使用
map()方法可以添加和修改属性(切记一定要rertun修改后的每一项):
this.gearsList = this.gearsList.map((item) => {
item.maxCount = Number(item.maxCount);
return item;
});
var kvArray = [{key: 1, value: 10},
{key: 2, value: 20},
{key: 3, value: 30}]
//使用箭头函数
var reformattedArray = kvArray.map(function(obj,index) {
console.log(index)
var rObj = {};
rObj.id=index;//添加id属性
rObj[obj.key] = obj.value;//修改属性
return rObj;
});
console.log(reformattedArray);
//使用箭头函数
var reformattedArray2 = kvArray.map(function(obj,index) {
obj.id=index;//添加id属性
return obj;//如果不返回则输出: Array [undefined, undefined, undefined]
});
console.log(reformattedArray2);
- 使用
map()方法配合find方法可以找到指定数据:
//使用filter(Boolean)可以过滤那些找不到为undefined的数据:
this.checkedCountry = this.checkedCountry
.map((item) => {
//find里面一般是() => {}
//如果需要返回数据,直接使用() => 判断条件
//或者使用() => ({})
//或者使用() => {return 判断条件}
return this.disabledList.find((item2) => item2 === item)
})
.filter(Boolean)
forEach()方法不会返回执行结果,而是undefined。也就是说,当数组中元素是值类型,forEach绝对不会改变数组;当是引用类型,则会改变数组,而map()方法会得到一个新的数组并返回。forEach()方法前支持链式调用,方法后不支持链式调用。
//使用foreach:
1. 数组元素为值类型(不会改变原数组):
let arr = [1, 2, 3, 4, 5]
let doubled = arr.forEach((num, index) => {
return num * 2
})
console.log('原数组', arr) //原数组 (5) [1, 2, 3, 4, 5]
console.log('返回值', doubled) // 返回值 undefined
2. 数组元素为值类型(可以通过在foreach循环中将值赋给原数组达到改变原数组的目的):
let arr = [1, 2, 3, 4, 5];
let doubled = arr.forEach((num, index) => {
return arr[index] = num * 2;}
);
console.log(arr) // [2, 4, 6, 8, 10] 原数组改变
console.log(doubled) // undefined 没有返回结果
3. 当数组元素为引用类型,会改变原数组的值
let arr = [
{ name: '鸣人', age: 16 },
{ name: '佐助', age: 17 }
]
let ret = arr.forEach((item) => {
item.age = item.age + 1
})
console.log('原数组', arr) // 原数组 [{name:'鸣人',age:17},{name:'佐助',age:18}]
console.log('返回值', ret) // 返回值 undefined
//使用map:
let arr = [1, 2, 3, 4, 5];
let doubled = arr.map(num => {
return num * 2;
});
console.log(doubled) // [2, 4, 6, 8, 10]
console.log(arr) // [1, 2, 3, 4, 5] 原数组不变
filter()和find()方法都需要return,而且都不会改变原数组。不同之处在于,filter()返回的是数组,会找出所有符合条件的数组,找不到会返回空数组。而find()返回的是对象,找到一条符合条件的对象后就不会继续往下查找了,找不到会返回undefined。
let copy = deepClone(this.fileList)
copy.map((item) => {
//将表格数据中的所属洲转换为接口需要的id
if (item.continentDictCode) {
item.continentDictCode = this.continentDictCode.find((code) => {
//注意一定要return
return code.itemName === item.continentDictCode
}).id
}
//将表格数据中的报关金额币种转换为接口需要的id
if (item.currency) {
const res = this.currency.find((type) => {
//注意一定要return
return type.currency === item.currency
})
if (!res) {
this.$message.error('请输入正确的报关金额币种')
return
}
item.currencyId = res.id
}
//将表格中的是否需要填写税号转换为接口需要的数字类型
if (item.prepaymentType) {
if (item.prepaymentType === 'IOSS税号') {
item.prepaymentType = 0
} else if (item.prepaymentType === '英国税号') {
item.prepaymentType = 1
} else {
item.prepaymentType = 2
}
}
})
- 使用数组方法必须要特别注意处理边界值(比如后端返回的数据为空的情况),此时可以使用
sizes.map || [].map来处理异常特殊情况; - 使用
JSON.parse(JSON.stringify(superRouter))可以深拷贝对象superRouter,将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象,它们老死不相往来,谁也不会影响谁。 slice()方法返回一个新的数组对象,这一对象是一个由begin和end决定的原数组的浅拷贝(包括begin,不包括end,从begin对应的索引号开始拷贝,到end对应的索引号结束拷贝)。原始数组不会被改变;如果end被省略,则slice会一直提取到原数组末尾;如果end大于数组的长度,slice也会一直提取到原数组末尾。结论:slice只是对数组的第一层进行深拷贝。
const animals = ['ant', 'bison', 'camel', 'duck', 'elephant'];
console.log(animals.slice(2));
// expected output: Array ["camel", "duck", "elephant"]
console.log(animals.slice(1, 5));
// expected output: Array ["bison", "camel", "duck", "elephant"]
//从数组倒数第二位开始截取,省略end意味着一直取到原数组末尾的数据
console.log(animals.slice(-2));
// expected output: Array ["duck", "elephant"]
console.log(animals.slice(2, -1));
// expected output: Array ["camel", "duck"]
//对只有一层的数组进行slice拷贝
//互不影响,老死不相往来
const originArray = [1,2,3,4,5];
const cloneArray = originArray.slice();
console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];
//对具有多层的数组进行slice拷贝
//对克隆数据的改变会影响到原数据
const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.slice();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2;
console.log(originArray); // [1,[1,2,3,4],{a:2}]
sort方法默认按字母升序排列(更准确一些是根据字符串Unicode码点),它直接改变原始数组;
[3,4,2,1,5].sort()
// => [1,2,3,4,5]
['Javascript','Vue','React','Node','Webpack'].sort();
// => ["Javascript", "Node", "React", "Vue", "Webpack"]
- 如果想按照其他标准进行排序,就需提供比较函数compareFunction(a,b),数组会按照调用该函数的返回值排序,即a和b是两个将要比较的元素:
1. 如果compareFunction(a,b)小于0,则a排列到b之前;
2. 如果 compareFunction(a, b)等于0,a和b的相对位置不变(并不保证);
3. 如果 compareFunction(a, b)大于0,b排列到a之前;
直接上例子:
let Users = [
{name:'鸣人',age:16},
{name:'卡卡西',age:28},
{name:'自来也',age:50},
{name:'佐助',age:17}
];
Users.sort((a,b) => {
return a.age - b.age
})
// => 鸣人、佐助、卡卡西、自来也的对象数组
-
findIndex()帮我们获取到所需元素在数组中的索引。 -
some和every用于检查数组中是否符合某些条件,返回值为Boolean值。不同之处在于some只要有一个满足条件就返回true,every检测数组中的每一项是否都满足条件,只有都满足了才会返回true,否则返回false。 -
使用
数组.join(' ')可以将数组转换为指定格式的字符串,''中可以自定义字符串之间的间隔格式。 -
使用
a.concat(b)可以将数组a和数组b拼接起来。
var alpha = ['a', 'b', 'c'];
var numeric = [1, 2, 3];
alpha.concat(numeric);
// result in ['a', 'b', 'c', 1, 2, 3]
- ...扩展运算符可以用于合并两个对象。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);
14.箭头函数里面的this指向
箭头函数中的this指向不为windows,如果需要在箭头函数中调用同一页面的其它方法。可以先在箭头函数外部使用var that = this,然后在箭头函数里面使用that.方法调用即可。
15.splice()函数
splice()函数用于删除数组中的元素,返回值为删除的元素。因此,如果要获取删除后的数组,直接使用cartList.splice(index, 1)就能自动更新cartList的值。
巧用splice()函数可以获取后端返回图片的名称:
原图为https://www.w3h5.com/zb_users/upload/2019/04/202107111554286068121005.png,通过截取图片名称会得到202107111554286068121005.png。
const name = data.clear_path && data.clear_path.split('/')[data.clear_path.split('/').length - 1]
15.reduce()函数
举个栗子就好:
//reduce()函数中的第一个参数是累加器,第二个参数是数组遍历的当前项
const totalPrice = selectedCartItem.reduce((total, item) => {
return item.variant.price * item.quantity + total
}, 0)
16.谷歌浏览器默认读取保存的用户名和密码
- first blood: 谷歌浏览器中的表单默认读取并填充浏览器已保存的用户名和密码。
解决方法: 将
el-input的autocomplete属性设置为new-password,就能解决谷歌浏览器默认读取保存的用户名和密码的问题。 - double kill: 谷歌浏览器中
type为password类型的el-input输入框在输入时,会自动拉取浏览器自动保存的账号数据。 解决方法:
<el-form ref="form" :model="form" :rules="rules" size="small" label-width="88px">
<el-form-item label="旧密码" prop="old_password">
<el-input
v-model="form.old_password"
placeholder="请输入旧密码"
type="password"
style="width: 370px"
autocomplete="new-password"
id="oldPassword"
readonly
@focus="removeAttr('oldPassword')"
@blur="setAttr('oldPassword')"
/>
</el-form-item>
<el-form-item label="新密码" prop="new_password1">
<el-input
v-model="form.new_password1"
placeholder="请输入新密码"
type="password"
style="width: 370px"
autocomplete="new-password"
id="newPassword1"
readonly
@focus="removeAttr('newPassword1')"
@blur="setAttr('newPassword1')"
/>
</el-form-item>
<el-form-item label="确认密码" prop="new_password2">
<el-input
id="newPassword2"
v-model="form.new_password2"
type="password"
placeholder="请输入确认密码"
style="width: 370px"
autocomplete="new-password"
readonly
@focus="removeAttr('newPassword2')"
@blur="setAttr('newPassword2')"
/>
</el-form-item>
</el-form>
removeAttr(idName) {
document.getElementById(idName).addEventListener('click', this.handleClick)
document.getElementById(idName).addEventListener('keydown', this.handleKeydown)
document.getElementById(idName).addEventListener('mousedown', this.handleMousedown)
//使用setTimeout,告诉JS是异步执行,这样子,就可以阻止第一次点击获取焦点时,下拉用户密码清
//单的框的出现
setTimeout(() => {
//获取焦点时 同时去除只读,这样可以获取光标,进行输入
document.getElementById(idName).removeAttribute('readonly')
}, 300)
},
setAttr(idName) {
//失去焦点立马更新为只读
document.getElementById(idName).setAttribute('readonly', 'true')
},
handleClick(e) {
//为什么先失去焦点,在获取焦点,这样子可以避免第二次或更多次连续点击输入框时,出现的用户密
// 码清单的框可以快速去除
if (e.type === 'click') {
document.getElementById(e.target.id).blur()
document.getElementById(e.target.id).focus()
}
},
// 监听键盘输入事件
// 当keyCode=8(backspace键) 和 keyCode=46(delete键)按下的时候,判断只剩下最后一个字符的时
// 候阻止按键默认事件,自己清空输入框
// 当keyCode=8(backspace键) 和 keyCode=46(delete键)按下的时候,判断如果处于全选状态,就阻
// 止按键默认事件,自己清空输入框
handleKeydown(e) {
if (e.type === 'keydown') {
const keyCode = e.keyCode
const passwordText = document.getElementById(e.target.id)
if (keyCode === 8 || keyCode === 46) {
//backspace 和delete
const len = passwordText.value.length
if (len === 1) {
passwordText.value = ''
return false
}
if (e.target.selectionStart === 0 && e.target.selectionEnd === len) {
passwordText.value = ''
return false
}
}
return true
}
},
//用来阻止第二次或更多次点击密码输入框时下拉用户密码清单的框一闪而过的问题
handleMousedown(e) {
if (e.type === 'mousedown') {
document.getElementById(e.target.id).blur()
document.getElementById(e.target.id).focus()
}
}
17.多个label标签和input输入框造成页面布局混乱
这种情况可以用el-form布局。使用:inline="true"将多个元素放在一行显示,使用label-width属性布局,调整两个label + input之间的距离,同时还可以做自定义校验。编辑价格对话框在出现滚动条时,样式会乱掉。这是因为出现的滚动条占据了一定的宽度,对话框的宽度不够,可以适当减小label-width的值,缩短两个label之间的距离,间接提升对话框的宽度。
<BaseDialog
style="text-align: left"
@closeHandle="handleClosed"
:dialogVisible.sync="showEditPriceDialog"
:append-to-body="false"
:modal-append-to-body="false"
width="840px"
height="562px"
title="设置价格"
>
<div class="base-dialog-wrapper">
<div class="product-wrapper flex">
<div class="product-name">产品名称: {{ product.name }}</div>
<div>产品分类: {{ product.category_name }}</div>
</div>
<div class="price-wrapper">
<el-form :inline="true" :model="ruleForm" :rules="rules" ref="ruleForm">
<el-form-item label="尺码价格的公差(d):" prop="size">
<el-input size="small" placeholder="请输入尺码价格的公差(d)" v-model="ruleForm.size"></el-input>
</el-form-item>
<el-form-item class="size-diff" label-width="158px" label="档位价格的公差(d):" prop="gear">
<el-input size="small" placeholder="请输入档位价格的公差(d)" v-model="ruleForm.gear"></el-input>
</el-form-item>
<div class="init-price">
<el-form-item label="初始价格:" label-width="130px" prop="init_price">
<el-input size="small" placeholder="请输入初始价格" v-model="ruleForm.init_price"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" size="mini" class="generate-button" @click="handleButtonClicked">
生成价格
</el-button>
</el-form-item>
</div>
</el-form>
</div>
<div class="setting-price-wrapper">
<div>价格设置:</div>
<setPriceTable
@takeRef="takeRefs"
:priceData="priceData"
:priceColData="priceColData"
:isSet="false"
:isInputVisible="true"
></setPriceTable>
</div>
</div>
<template #footer>
<LoadingBtn type="primary" @click="handleConfirmed"> 确认 </LoadingBtn>
<LoadingBtn @click="handleCanceled"> 取消 </LoadingBtn>
</template>
</BaseDialog>
18.待上架产品、已上架产品、已下架产品初始化页面报错
原因:图片数据在点击时才被赋值,页面初始时还未触发点击事件,没拿到对应的产品信息,也就没拿到图片数据。
解决方法:将图片数据放到计算属性中,并使用try catch包裹。如果找不到图片,则使用默认图片进行渲染。
computed: {
cover() {
try {
//点击查看价格时才给product赋值
if (!this.product.prim_struct[0].figures.length) {
return require("@/assets/images/default.png");
}
return this.product.prim_struct[0].figures[0].path;
} catch (err) {
return require("@/assets/images/default.png");
}
}
}
19.表单校验成功时,未调用生成数据的方法
最后要在表单校验器中添加回调函数callback(),自定义校验callback()必须被调用。
如果要校验表格中的文字是否为正数,需要使用表单嵌套表格的方法进行判断。
export default {
data() {
//自定义校验
var timeValidate = (rule, value, callback) => {
//失败时返回错误信息
if (
this.freightSettingForm.startReferTimeLimitation >=
this.freightSettingForm.endReferTimeLimitation
) {
callback(new Error("终止快递时效天数必须大于起始快递时效天数"));
} else {
//成功时必须调用callback()方法才能继续
callback()
}
};
//...
endReferTimeLimitation: [
{ required: true, message: "输入不可以为空", trigger: "blur" },
{ validator: isIntegerAndZero, trigger: "blur" },
//引入自定义校验规则,在失去焦点时触发
{ validator: timeValidate, trigger: "blur" },
],
}
}
正数正则:
export function isMoreZero(rule, value, callback) {
if (isEmpty(value)) {
return callback()
}
const re = /^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$/
const rsCheck = re.test(value)
if (!rsCheck) {
callback(new Error('请输入大于0的数'))
} else {
callback()
}
}
20.无法调用表格公共组件中的表单校验方法
在公共组件的mounted生命周期函数中,将表单的ref通过自定义组件传递给父组件。父组件通过自定义事件接收ref引用,并将接收的引用赋值给data中的变量。最后在需要调用表单验证方法的函数中,使用ref调用表单校验方法即可。
21.vue中的number修饰符
如果想自动将用户的输入值转为数值类型,可以给v-model添加number修饰符,但是使用这种方法无法输入小数。
<input v-model.number="age" type="number">
22.el-image大图浏览
<template v-slot:productShowCoverSlot="{ scoped }">
<div class="img-wrapper">
<el-image
:src="scoped.productShowCover"
:z-index="9999"
:preview-src-list="[`${scoped.productShowCover}`]"
/>
<span>
{{ scoped.productCnName || '暂无' }}
</span>
</div>
</template>
23.基于vue-element-admin框架的后台系统点击菜单栏重载页面
解决思路:vue-element-admin文档。先跳转到一个空白页面,再判断当前激活菜单栏的路由是否和当前的路由地址一致。如果一致,则从空白页面跳转到目标路由所在的地址;如果不一致,则跳转到目标路由所在的地址。
src/views/layout/components/Sidebar/Link.vue:
<template>
<!-- eslint-disable vue/require-component-is-->
<component v-bind="linkProps(to)" @click="click(to)">
<slot />
</component>
</template>
<script>
import { isExternal } from '@/utils'
export default {
props: {
to: {
type: String,
required: true
}
},
methods: {
isExternalLink(routePath) {
return isExternal(routePath)
},
click(url) {
// 通过重定向空白路由页面实现当前菜单刷新
if (JSON.parse(sessionStorage.getItem('defaultActive')) === url) {
// 点击的是当前路由 手动重定向页面到 '/redirect' 页面
sessionStorage.setItem('defaultActive', JSON.stringify(url))
const fullPath = encodeURI(url)
this.$router.replace({
//如果网址带有query,则使用 path: '/redirect' + encodeURI(fullPath)
//如果网址没有query, 则使用 path: '/redirect' + url
path: '/redirect' + encodeURI(fullPath)
})
} else {
sessionStorage.setItem('defaultActive', JSON.stringify(url))
// 正常跳转
this.$router.push({
path: url
})
}
},
linkProps(url) {
return {
//将当前组件的标签转换为div标签
is: 'div'
}
}
}
}
</script>
src/views/layout/components/Sidebar/redirect.vue:
<script>
export default {
created() {
const { params, query } = this.$route
const { path } = params
this.$router.replace({ path: '/' + path, query })
},
mounted() {},
render: function (h) {
return h() // avoid warning message
}
}
</script>
src/router/index.js:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import Layout from '@/views/layout/Layout'
/* Layout */
// import clientRouter from "./clientRouter";
/**
* hidden: true if `hidden:true` will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu, whatever its child routes length
* if not set alwaysShow, only more than one route under the children
* it will becomes nested mode, otherwise not show the root menu
* redirect: noredirect if `redirect:noredirect` will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
title: 'title' the name show in submenu and breadcrumb (recommend set)
icon: 'svg-name' the icon show in the sidebar,
}
**/
export const constantRouterMap = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/errorPage/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/errorPage/401'),
hidden: true
},
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
// path: '/redirect/:path(.*)' 约等于 path: '/redirect/*'
// path: '/redirect/:path'只能匹配 path: '/redirect/123'
// 而不能匹配 path: '/redirect/123/456'
path: '/redirect/:path(.*)',
component: () => import('@/views/layout/components/Sidebar/redirect')
}
]
}
]
//注册路由
export default new Router({
mode: 'hash',
scrollBehavior: () => ({ y: 0 }),
routes: [...constantRouterMap]
})
23.基于Prettier的eslint配置
在保存代码之后,遵循一定的格式,自动使用Prettier插件格式化代码。在VS Code编辑器中,先安装ESLint和Prettier插件。然后点开ESLint,点击卸载功能右边的齿轮图标进行扩展设置,选择在setting中编辑:
{
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"editor.formatOnSave": true,
"open-in-browser.default": "google",
"workbench.editor.enablePreview": false,
"less.compile": {
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.codeAction.showDocumentation": {
"enable": true
},
"prettier.semi": false,
"prettier.singleQuote": true,
"prettier.trailingComma": "none"
}
24.git指令
- 开发新功能或者修改
bug,新建本地分支,最终合并到dev分支上(推荐) 1. 第一种方式
git checkout dev-v1.5
git checkout -b dev-v1.5-fix-bug(新建并切换到dev-v1.5分支)
修改代码后
git add .
git commit -m '输入提交的标题'
git checkout dev-v1.5
git pull
git checkout dev-v1.5-fix-bug
git merge dev-v1.5(合并,有冲突解决冲突,并git add . git commit -m '合并')
git push origin HEAD:dev-v1.5(推送到指定分支)
2. 第二种方式
git checkout dev-v1.5
git branch dev-v1.5-fix-bug
git checkout dev-v1.5-fix-bug
//设置dev-v1.5-fix-bug的上游分支为dev-v1.5(也可以不和dev-v1.5分支建立关联)
git branch --set-upstream-to=origin/dev-v1.5 dev-v1.5-fix-bug
git add .
git commit -m '输入提交的标题'
//不管有没有关联上游分支,此处都不需要git pull。
//如果没有建立上游分支,git pull就不会生效;
//如果有建立上游分支再git pull,此时dev-v1.5上的某个文件如果在dev-v1.5-fix-bug分支上有修改,则会产生冲突,并以dev-v1.5的源文件为准
git checkout dev-v1.5
git pull
//此时git merge会以merge的对象为准,同一文件有冲突,也会以merge对象的文件为基准
git merge dev-v1.5-fix-bug(有冲突解决冲突,并git add . git commit -m '合并')
git push
- 直接在
dev分支开发新功能或者修改bug(忘记新建分支后迫不得已的操作) 1.采用git merge方式
git branch fix
git checkout fix
git branch --set-upstream-to=origin/dev-v1.5 fix
//或者直接git checkout -b fix,这条命令就相当于上面两条
git add .
git commit -m '输入提交的标题'
git checkout dev-v1.5
git pull
git merge fix(有冲突解决冲突,并git add . git commit -m '合并')
git push
2.采用git rebase方式
git checkout -b fix
git add .
git commit -m '输入提交的标题'
git rebase dev-v1.5
(将提交内容放在git提交记录的最后,这个分支保有dev分支的信息,还需要在dev分支上合并fix分支)
(有冲突解决冲突,并git add . git commit -m '合并',git rebase --continue,到完全解决冲突为止)
git checkout dev-v1.5
git pull
git merge fix(有冲突解决冲突,并git add . git commit -m '合并')
git push
小结: git merge和git rebase都是合并操作,但两者之间又有所区别。在git管理工具中,每次commit之后都会记录当前提交的内容和时间,这样git管理工具才能记录提交的先后顺序。
区别:rebase是在新建的本地分支fix上进行操作,并将本地分支的修改的内容置于当前dev分支提交记录的最后。而rebase之后还需要再merge和push,这样git管理工具记录的提交时间就从第一次commit的时间变为push的时间。而merge是在dev或者fix分支上进行操作,用merge提交的记录时间为第一次commit的时间。
举个栗子,比如A在昨天10:00am修改了代码,但是他这边的功能还没做完就请假了,而昨天B、C、D之后分别在2:00pm、3:00pm和4:00pm合并了代码。第二天A上班开发完成剩下的功能,B、C、D也没有commit,如果他使用merge的方法,他的提交记录就会在B、C、D之前,而如果使用rebase的方法push,他的提交记录就会放在B、C、D之后,这样做的好处是B、C、D在查看git提交记录时不会觉得突兀。
- git别名:采用
git config --global alias.简写名称 需要简写的全称
//配置常用的git指令别名
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status
- 使用tab一键补全git命令: window 可以使用windows git bash 敲TAB键有代码补全功能
Linux\Mac 推荐 oh-my-zsh
- 一些实用的git命令:
1. 如果想要放弃合并的内容,可以使用
git merge --abort和git rebase --abort
2. 使用git log可以查看在git上提交的内容
3. 将develop分支上的代码合并到master分支上
git checkout develop
git pull origin develop
git checkout master
git fetch origin master
git reset --hard origin/master
git merge develop
git push origin master
git checkout develop
4. 如果需要回滚到某个commit:
方法1
git reset --hard dbf5efdb3cd8ea5d576f2e29fe0db1951d0e3e3b
# 强制推送到远程分支, 会抹去远程库的提交信息, 不要这么干
# git push -f origin master
方法2
# 回退到指定版本, 需要解决冲突
git revert e7c8599d29b61579ef31789309b4e691d6d3a83f
# 放弃回退(加--hard会重置已commit和工作区的内容)
# git reset --hard origin/master
5. 输入git blame (文件完整路径)可以查看每次修改文件的记录。
25.找到当前对象在对象数组中的索引
//对象数组.indexof(对象数组)
//gear为当前对象数组,gearsList为整个对象数组,currentIndex为当前对象在对象数组中的索引
const currentIndex = this.gearsList.indexOf(gear)
26. 在插槽中重构数据
- 使用计算属性 + 闭包的思想
<template #priceSlot="{ scoped }">
<div class="flex-column">
//将插槽的数据传给计算属性
<div v-if="scoped.gear_category">¥{{ minPrice(scoped) }} - ¥{{ maxPrice(scoped) }}</div>
<div v-else>暂无数据</div>
<el-button
:loading="scoped.loading"
type="text"
size="mini"
class="filter-item"
@click="showPrice(scoped, scoped.index)"
v-if="scoped.gear_category"
>查看价格</el-button
>
</div>
</template>
computed: {
//切记在计算属性中是不能直接添加形参的
minPrice() {
//这里需要return一个函数,将需要取的数据data作为函数参数进行传参
return (data) => {
const { sizes } = data
let priceArr = []
sizes.map((size) => {
const { price_confs } = size
price_confs.map((item) => {
priceArr.push(Number(item.price))
})
})
let min = priceArr[0]
for (let i = 1; i < priceArr.length; i++) {
if (priceArr[i] < min) {
min = priceArr[i]
}
}
return min
}
},
maxPrice() {
return (data) => {
const { sizes } = data
let priceArr = []
sizes.map((size) => {
const { price_confs } = size
price_confs.map((item) => {
priceArr.push(Number(item.price))
})
})
let max = priceArr[0]
for (let i = 1; i < priceArr.length; i++) {
if (priceArr[i] > max) {
max = priceArr[i]
}
}
return max
}
}
}
2.直接使用函数的方法
<template #orderItemDTOListSlot="{ scoped }">
<span>{{ count(scoped) }}</span>
</template>
methods: {
count(data) {
const { orderItemDTOList } = data
let sum = 0
if (orderItemDTOList) {
orderItemDTOList.map((item) => {
sum += item.productCount
})
}
return sum
}
}
27.使用js模拟电商网站放大镜的效果
注意:
-
可以使用
vueX拿取sidebar.opened的值,判断当前是否处于折叠状态。需要根据当前状态,动态赋予偏移量。 -
记住一个原则: 放大显示的大小 / 遮罩图的大小 = 大图的大小 / 小图的大小,这个比例一定要相同。
-
基于
vue-element-admin的后台管理系统的路由可以使用redirect: 'noredirect'配置该路由为不可点击状态。
话不多说,上效果图:
<template>
<div>
<h3 style="margin-left: 100px">电商放大镜页面</h3>
<div class="body">
<div class="small" @mousemove="onMouseMove" @mouseenter="onMouseEnter" @mouseleave="onMouseOut">
<el-image :src="url" class="pic"></el-image>
<div class="mask"></div>
</div>
<div class="big">
<el-image :src="url" class="big-image"></el-image>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
url: 'http://osstest.zdwholesale.com/media/show_image/CS-20210527143114_2003.png'
}
},
computed: {
//判断当前是否处于折叠状态
sidebarState() {
return this.$store.state.app.sidebar.opened
}
},
methods: {
getEle(cla) {
return document.getElementsByClassName(cla)
},
onMouseEnter() {
//使用getElementsByClassName返回的为数组,需要使用索引
this.getEle('mask')[0].style.display = 'block'
this.getEle('big')[0].style.display = 'block'
},
onMouseOut() {
this.getEle('mask')[0].style.display = 'none'
this.getEle('big')[0].style.display = 'none'
},
onMouseMove(event) {
let small = this.getEle('small')[0]
let mask = this.getEle('mask')[0]
let bigImage = this.getEle('big-image')[0]
//计算鼠标距离盒子左边和上边的距离
//不论是否处于折叠状态,网页布局占据的高度都为50
//处于折叠状态,网页布局占据的宽度为37;未处于折叠状态,网页布局占据的宽度为220.21
const w = this.sidebarState ? 220.21 : 37
let x = event.clientX - w - small.offsetParent.offsetLeft - 0.5 * mask.offsetWidth
let y = event.clientY - 50 - small.offsetParent.offsetTop - 0.5 * mask.offsetHeight
//判断x、y的边界值
if (x < 0) {
x = 0
} else if (x > small.offsetWidth - mask.offsetWidth) {
x = small.offsetWidth - mask.offsetWidth
}
if (y < 0) {
y = 0
} else if (y > small.offsetHeight - mask.offsetHeight) {
y = small.offsetHeight - mask.offsetHeight
}
//根据x、y的值设置mask距离盒子上方和左方的距离
mask.style.left = x + 'px'
mask.style.top = y + 'px'
//根据x、y的值设置大图距离盒子上方和左方的距离
bigImage.style.left = -x * (bigImage.offsetWidth / small.offsetWidth) + 'px'
bigImage.style.top = -y * (bigImage.offsetHeight / small.offsetHeight) + 'px'
}
}
}
</script>
<style lang="scss" scoped>
.body {
margin-left: 100px;
width: 400px;
height: 500px;
position: relative;
.small {
width: 400px;
height: 400px;
.pic {
position: relative;
}
.mask {
width: 200px;
height: 200px;
position: absolute;
top: 0;
left: 0;
cursor: move;
display: none;
background: rgba(0, 0, 0, 0.1);
}
}
.big {
position: absolute;
left: 450px;
top: 0;
width: 500px;
height: 500px;
display: none;
overflow: hidden;
::v-deep {
.el-image,
.el-image__inner {
width: 1000px;
height: 1000px;
position: absolute;
top: 0;
left: 0;
}
}
}
}
</style>
28.谷歌浏览器无法右键-在新标签页打开链接
谷歌浏览器自带的在新标签页中打开链接功能,是借助a标签,通过给定跳转地址来打开新网页的。所以,直接给a标签或者在vue中使用router-link标签(会被渲染为a标签)即可。
<router-link
:to="`/design/detail?id=${data.id}`"
:class="['protoCardComponent', 'hover']"
@click="linkToDetail"
>
</router-link>
29.el-image使用本地图片
- 使用
CommonJS中的require方法:
<el-image fit="contain" class="logo" :src="require('../images/colorful-logo.png')"></el-image>
- 如果要修改
el-image的图片样式,可以不用样式穿透。使用行内样式,或者直接为当前图片添加一个类,然后再修改类里面的图片样式即可。
.logo {
width: 124px;
height: 27px;
.el-image {
height: 100%;
width: 100%;
}
}
- 使用
fit="contain"这个属性可以较好地保证图片的长宽比,使其不会被拉伸:
<el-image
fit="contain"
style="width: 100px; height: 100px"
:src="url"
>
</el-image>
- 图片路径:
./表示当前路径,../表示上级路径。
30.网页头部响应式布局
蓝湖拿到的设计稿宽度是1980px,需要给一个版心1280px。然后在版心中再布局,这样才能达到响应式布局的效果。
<template>
<div class="header-warpper">
<div class="content">
<div class="left">
<el-image fit="contain" class="logo" :src="logo"></el-image>
<div class="navs">
<router-link to="/" class="nav">首页</router-link>
<router-link to="/design/index" class="nav">在线定制</router-link>
</div>
</div>
<div class="right">
<el-button @click="$router.push('/login')">登录</el-button>
<el-button type="primary">注册</el-button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
logo: require('../images/colorful-logo.png')
}
},
components: {}
}
</script>
<style lang="scss" scoped>
.header-warpper {
background: #fff;
.content {
width: 1200px;
height: 64px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
.left {
display: flex;
align-items: center;
.logo {
width: 124px;
height: 27px;
.img {
height: 100%;
width: 100%;
}
}
.navs {
margin-left: 100px;
.nav {
font-size: 14px;
color: #324966;
&:not(:first-child) {
margin-left: 50px;
}
}
}
}
}
}
</style>
31.表单嵌套表格的校验
上需求图:
现在有一种需求,需要在切换档位的时候,不需要做表单校验,而在输入的时候需要做表单校验。使用的方法是监听表格的值。如果表格的值发生变化,则对表单进行自定义校验。
两种方法:
- 为表单添加表单属性
validate-on-rule-change,将该表单属性的值设置为false
<el-form :model="priceFormData" :rules="priceFormRules" ref="setPriceTable" :validate-on-rule-change="false">
</el-form>
- 调用表单的
clearValidate方法
watch: {
priceData: {
handler(n) {
let { priceData, priceColData, priceFormRules } = this
priceFormRules = {}
priceData.forEach((row, rowIndex) => {
const { ids, size, size_id, ...rest } = row
const formKeys = Object.keys(rest).sort((a, b) => {
return +a.replace('price', '') - +b.replace('price', '')
})
formKeys.map((key) => {
priceFormRules[`${key}${rowIndex}`] = [{ required: true, validator: validatePrice, trigger: 'blur' }]
this.$set(this.priceFormData, key + '' + rowIndex, row[key])
})
})
this.priceFormRules = priceFormRules
// this.$nextTick(function () {
// this.$refs.setPriceTable.clearValidate()
// })
},
immediate: true,
deep: true
}
}
32.网页布局相关
一行数据使用align-items,多行数据使用align-content。
记一次网页填充背景图的经历:
- 提供的背景图分辨率宽度为
1920px(电脑屏幕宽度>1920px):
height: 560px;
width: 100%;
min-width: 1200px;
background-image: url('../images/banner-bg.jpg');
background-size: 100%;
全屏时:
缩小屏幕宽度后:
- 提供的背景图分辨率宽度为
1920px(电脑屏幕宽度>1920px):
width: 100%;
height: 560px;
min-width: 1200px;
background-image: url('../images/banner.jpg');
background-position: center center;
全屏时:
- 提供的背景图分辨率宽度为
2560px(电脑屏幕宽度>1920px):
width: 100%;
height: 560px;
min-width: 1200px;
background-image: url('../images/banner.jpg');
background-position: center center;
全屏时:
缩小屏幕宽度后:
33.element UI对话框嵌套导致的问题
在饿了么对话框上设置:append-to-body="true"与:modal-append-to-body="true"
34.element el-table相关
- 饿了么表格布局:
<el-table :data="tableData" border>
<el-table-column prop="date" label="日期"> </el-table-column>
<el-table-column prop="name" label="姓名"> </el-table-column>
<el-table-column prop="province" label="省份"> </el-table-column>
<el-table-column prop="zip" label="邮编"> </el-table-column>
<el-table-column label="操作" width="100">
<template slot-scope="scope">
<el-button
@click="handleClick(scope.row)"
type="text"
size="small"
>查看</el-button
>
<el-button type="text" size="small">编辑</el-button>
</template>
</el-table-column>
</el-table>
tableData: [
{
date: '2016-05-02',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1518 弄',
zip: 200333
},
{
date: '2016-05-04',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1517 弄',
zip: 200333
},
{
date: '2016-05-01',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1519 弄',
zip: 200333
},
{
date: '2016-05-03',
name: '王小虎',
province: '上海',
city: '普陀区',
address: '上海市普陀区金沙江路 1516 弄',
zip: 200333
}
]
效果:
可能是饿了么版本原因,使用饿了么带边框表格的代码并不能达到表格全部有边框的效果,但是可以通过饿了么表格自带的header-cell-style实现效果:
<el-table
:data="tableData"
border
style="border-left: 1px solid #ebeef5;border-top: 1px solid #ebeef5;
"
:header-cell-style="{ borderRight: '1px solid #ebeef5' }"
>
</el-table>
- 饿了么表格合并: 1. 静态数据(假数据)合并
官方说明: 通过给table传入span-method方法可以实现合并行或列,方法的参数是一个对象,里面包含当前行row、当前列column、当前行号rowIndex(表头不计入行号)、当前列号columnIndex四个属性。该函数可以返回一个包含两个元素的数组,第一个元素代表rowspan,第二个元素代表colspan。 也可以返回一个键名为rowspan和colspan的对象。
构建一个新表格:
<el-table
:data="tableData"
border
style="border-left: 1px solid #ebeef5; border-top: 1px solid #ebeef5"
:header-cell-style="{ borderRight: '1px solid #ebeef5' }"
:span-method="spanMethod"
>
<el-table-column prop="id" label="学员ID" align="center"> </el-table-column>
<el-table-column prop="name" label="学员名称" align="center"> </el-table-column>
<el-table-column prop="skill1" label="技能1" align="center"> </el-table-column>
<el-table-column prop="skill2" label="技能2" align="center"> </el-table-column>
<el-table-column prop="skill3" label="技能3" align="center"> </el-table-column>
<el-table-column prop="skill4" label="技能4" align="center"> </el-table-column>
</el-table>
浏览效果:
(1) 单行合并
假定合并九尾妖狐这一行的第3列到第6列的内容:
/**
* 表格合并
* row 表格每一行的数据
* column 表格每一列的数据
* rowIndex 表格的行索引,不包括表头,从0开始
* columnIndex 表格的列索引,从0开始
*/
spanMethod({ row, column, rowIndex, columnIndex }) {
//从第二行开始
if (rowIndex === 1) {
//从第三列开始
if (columnIndex === 2) {
//1.使用数组返回(合并的行数,合并的列数)
return [1, 4]
//2.使用对象返回
return {
rowspan: 1,
colspan: 4
}
}
}
}
浏览效果: 九尾妖狐这一列行高较大,这是因为没有给这一行第4列到第6列赋值
//为合并后的列赋值,消除未赋值带来的影响
spanMethod({ row, column, rowIndex, columnIndex }) {
//从第二行开始
if (rowIndex === 1) {
//从第三列开始
if (columnIndex === 2) {
//1.使用数组返回(合并的行数,合并的列数)
return [1, 4]
//2.使用对象返回
return {
rowspan: 1,
colspan: 4
}
} else if (columnIndex === 3 || columnIndex === 4 || columnIndex === 5) {
//清空第4列到第6列的值,以便与第3列的值合并
return [0, 0]
}
}
}
浏览效果:
(2) 多行合并
假定合并除表头外的每一行的第3列到第6列的内容:
spanMethod({ row, column, rowIndex, columnIndex }) {
//从第三列开始
if (columnIndex === 2) {
//1.使用数组返回(合并的行数,合并的列数)
// return [1, 4]
//2.使用对象返回
return {
rowspan: 1,
colspan: 4
}
} else if (columnIndex === 3 || columnIndex === 4 || columnIndex === 5) {
return [0, 0]
}
}
浏览效果:
2. 动态数据(后端返回)合并
上述的表格数据是前端使用假数据写死的,但现实中的真实需求往往是前端接收后端返回的数据,然后对这些数据进行重构,再进行表格合并。
假如使用以下数据模拟后端返回的结果:
// 这些数据(包括记录数组和后端返回的数据)都定义在data中
// 记录数组rowsarr(记录当前合并的起始行和需要合并的行数)
rowsArr: [],
tableData: [
{
id: '1',
region: '武汉',
type: '电商',
company: [
{ name: '武汉电商公司1' },
{ name: '武汉电商公司2' },
{ name: '武汉电商公司3' },
{ name: '武汉电商公司4' }
]
},
{
id: '2',
region: '泉州',
type: '外贸',
company: [{ name: '泉州外贸公司1' }, { name: '泉州外贸公司2' }, { name: '泉州外贸公司3' }]
},
{
id: '3',
region: '浙江',
type: '金融',
company: [{ name: '浙江金融公司1' }, { name: '浙江金融公司2' }]
}
]
首先需要对这些数据进行重组和构造记录数组:
computed: {
newTableData() {
let currentArr = []
this.tableData.map((item) => {
//计算并添加记录数组,构造记录数组
//如果记录数组不为空,则表示不是第一次合并
//此时当前所在的行索引位置为上次的行索引位置加上次合并的行数
//当前需要合并的行数为当前公司名称的条数
if (this.rowsArr.length) {
this.rowsArr.push({
currentIndex:
this.rowsArr[this.rowsArr.length - 1].currentIndex + this.rowsArr[this.rowsArr.length - 1].quantity,
quantity: item.company.length
})
} else {
//如果记录数组为空,则表示是第一次合并
//此时当前行所在的索引位置为0,需要合并的行数为当前公司名称的条数
this.rowsArr.push({
currentIndex: 0,
quantity: item.company.length
})
}
if (item.company && item.company.length) {
//循环嵌套循环,循环公司名称,将每条公司名称都抽出来,重构表格数据
item.company.map((company) => {
currentArr.push({
id: item.id,
region: item.region,
type: item.type,
company: company.name
})
})
}
})
return currentArr
}
}
然后再定义html结构:
<el-table
:data="newTableData"
border
:span-method="spanMethod"
style="border-left: 1px solid #ebeef5; border-top: 1px solid #ebeef5; width: 80%; margin: 0 auto"
:header-cell-style="{ borderRight: '1px solid #ebeef5' }"
>
<el-table-column prop="id" label="地区ID" align="center"> </el-table-column>
<el-table-column prop="region" label="地区名称" align="center"> </el-table-column>
<el-table-column prop="type" label="类型" align="center"> </el-table-column>
<el-table-column prop="company" label="公司" align="center"> </el-table-column>
</el-table>
最后添加表格合并方法:
spanMethod({ rowIndex, columnIndex }) {
//对每一列都根据当前所在行的索引和需要合并的行数以及列数进行合并
if (columnIndex === 0 || columnIndex === 1 || columnIndex === 2) {
//清空表格第1列到第3列的值
let obj = [0, 0]
//为表格第1列到第3列需要合并的地方重新赋值
this.rowsArr.map((item) => {
if (item.currentIndex === rowIndex) {
obj = [item.quantity, 1]
}
})
return obj
}
}
注意: 在表格合并方法中,没有用到的参数可以省略。
效果:
- 饿了么表格数据获取: 使用
scope.row获取当前点击行的表格数据,使用scope.$index获取当前点击的表格行数索引值。使用type="selection"属性后,表格成为可以多选的表格,使用this.$refs.tableRef.selection可以获取选中行的数据(以数组形式呈现)。 - 饿了么动态表头构建:
- 饿了么表单嵌套表格:
35.动画效果的启用与删除
使用transition: width 2s这条css可以给指定元素做动画效果,第一个参数用于监听需要改变的属性,第二个参数用于指定整个动画的完成时间。例如可以在点击事件中重置指定元素的宽度,这样就能触发动画效果了。而如果要清除动画效果,需要获取到对应的dom元素,然后再利用document.style.transition = 'none'这条js代码即可达到效果。
36.无法穿透el-popover的样式
el-popover生成的div不在当前组件之内,和App.vue组件的div平级,在当前组件内无法直接使用穿透修改样式。但是可以通过给el-popover标签设置:append-to-body="false"属性,为该标签添加一个类名popover,然后在当前组件内再使用样式穿透即可修改样式:
.popover {
::v-deep {
.el-popover {
padding: 0;
border: none;
}
}
}
37.记一次使用组件化开发的思想编写代码遇到的问题
- 问题1: 在对话框子组件删除和编辑国家/地区列表后,页面没有刷新获取最新的列表。 原因: 刷新方法不应该写在子组件中,而应该在父组件中调用。
解决方法: 在子组件中拿到父组件的引用,然后在子组件中调用接口刷新页面。
父组件中:
<countryDialog
:form="form"
:id="id"
:addDialog.sync="addDialog"
:type="type"
:radio.sync="radio"
:sup_this="sup_this"
/>
data() {
return {
sup_this: this
}
}
子组件中:
props: {
sup_this: {
type: Object,
required: true
}
},
updateCounrty(list).then(async (res) => {
this.$emit('update:addDialog', false)
this.query = {}
//1.拿到父组件,在父组件中调用方法刷新页面
//2.或者在子组件中直接传递事件,然后在父组件中接收事件,调用方法刷新页面
this.sup_this.searchChange()
this.$message.success('更新国家信息成功!')
})
-
问题2: 为按钮加上
:loading="loading",在请求前将loading的值置为true,在请求结束后(包括then和catch方法)将loading的值置为false,可以达到防抖的效果。 -
问题3: 错误:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "addDialog" (found in component )原因: 组件props中的数据只能单向流动,即只能从父组件通过组件的DOM属性attribute传递props给子组件,子组件只能被动接收父组件传递过来的数据,并且在子组件中,不能修改由父组件传来的props数据。
解决方法:
(1)在子组件中需要修改由父组件传来的props数据时,一般推荐使用事件传递的方法。在子组件中将事件传递给父组件,然后在父组件中接收事件,在事件中修改需要更新的props值。
(2)使用事件传递的语法糖(相当于事件传递,适用于比较简单的场景)
在子组件中:
//比如在模态框的关闭回调中,需要修改```props: addDialog```的值
//可以使用this.$emit('update:props名称',props更改后的值)
handleClose() {
this.$refs.form.resetFields()
this.$emit('update:addDialog', false)
}
在父组件中: 使用:props名称.sync=绑定父组件中的变量。相比于传统的props传参多了一个sync修饰符,相当于接收update:addDialog事件,改变addDialog的值。
<countryDialog
:form="form"
:id="id"
:addDialog.sync="addDialog"
:type="type"
:radio.sync="radio"
:sup_this="sup_this"
/>
(3)子组件中:
props: {
radio: {
type: Number,
default: 1
}
},
watch: {
radio: {
handler(val) {
this.radio1 = val
},
immediate: true
},
radio1(val) {
//使用语法糖在子组件中改变props的值
this.$emit('update:radio', val)
}
},
data() {
return {
//使用中间变量接收props的值,此时可以直接在子组件中修改中间变量的值
//并通过watch将props和中间变量双向绑定
//在子组件中绑定中间变量
//在父组件里绑定props
radio1: this.radio,
}
}
<el-radio-group v-if="type === 'add'" v-model="radio1" @change="onRadioChange">
<el-form-item label="设置计算方式:" prop="way">
<el-radio :label="item.id" v-for="(item, index) in list" :key="index">{{ item.type }}</el-radio>
</el-form-item>
</el-radio-group>
父组件中:
<addDialog
:type.sync="type"
:form="form"
:radio.sync="radio"
@refresh="refresh"
@resetFields="resetFields"
ref="dialog"
@handleCopied="handleCopied"
/>
- 小技巧:
1. 在父组件中,使用
v-if销毁对话框子组件可以解决很多问题。包含对话框子组件的父组件在初始化时,只会走一次生命周期,调用父子组件中的生命周期函数。对话框子组件可能还没拿到数据就已经销毁,不会再触发子组件的生命周期函数,这时可以通过使用v-if控制子组件对话框的显示与隐藏,使对话框子组件强制执行生命周期函数或者销毁生命周期函数。
<countryDialog
v-if="addDialog"
:form="form"
:id="id"
:addDialog.sync="addDialog"
:type="type"
:radio.sync="radio"
:sup_this="sup_this"
/>
2. 在父组件中,给父组件添加ref,可以通过引用改变子组件的值。
<addDialog :type="type" :form="form" :radio.sync="radio" @refresh="refresh" ref="dialog" />
onEdit(data) {
this.type = 'edit'
this.$refs.dialog.visible = true
}
3. 如果子组件是一个带有表单验证的对话框,且表单的数据是由父组件传递而来,此时如果要清空表单,需要在父组件中进行。
4. 如果一个带有表单验证的对话框具有添加、编辑和查看的复用功能,建议为取消按钮的点击方法中加入el-form的clearValidate()方法。不然如果添加数据时,存在某项表单未填写内容,则会触发表单校验请填写XXX。此时如果取消关闭对话框,再次点击编辑窗口,会触发之前触发的表单校验,但此时的编辑对话框其实是有数据的。
5. 在dialog中,使用top属性可以指定表格与网页顶部的距离。在dialog的主体区域中,增添一个div包裹dialog的主体内容,并添加class,可以固定对话框主体区域的高度,当高度超出后,会自动为对话框添上滚动条。
<el-dialog title="查看" :visible.sync="visible" width="50%" :before-close="handleClose" top="10vh">
<div class="content-warpper">
主体内容
</div>
</el-dialog>
.content-warpper {
padding: 22px 0 18px 24px;
height: 700px;
overflow: auto;
}
38.浅谈get和post方法
它们是HTTP协议中两种发送请求的方法。使用get方法会把参数包含在url中,而post方法通过request body传递参数。get方法比post方法更不安全,因为参数直接暴露在url上,所以不能用来传递敏感信息。get方法产生一个TCP数据包,post方法产生两个TCP数据包。在接口中,如果为参数传入undefined,其实相当于没有传入该参数,这是一个小技巧。
具体详见: get方法和post方法的区别
39.后端接口返回空数据的处理
后端返回的数据为空时,可能存在data为null的情况,这个时候如果再使用数组方法会出现undefined的错误,需要对接口返回的数据进行处理,取data或者[]。
search: debounce(function () {
getCounrty({
isDeleted: 0,
twoCharCode: this.content,
continentDictCode: this.cdc || undefined
}).then((res) => {
//如果接口返回数据为空,可能会返回data为null,需要特别注意为空时的返回值
//否则会报undefined的错误
this.countryList = res.detail || []
this.initCheck()
this.test()
})
})
40.JS小数点精度问题
具体详见:JS小数点精度问题
JS中变量的定义:
<script>
import { deleteCounrty, batchCreate } from '@/api/country'
//templatx.xlsx放在public更目录下,在高版本的Vue中,public文件夹的文件会被自动识别为静态文件
const temUrl = require('template.xlsx')
export default {
data() {
return {
temUrl
}
}
}
JS定制器练习经验总结(电商私人定制)
1. 给canvas上的图片增加翻转功能,需要点击翻转按钮和画布才能完成图片翻转
需要使用canvas.renderAll()更新画布,此时点击翻转按钮就能完成图片的翻转功能。
2. svg转换为base64,可以解决跨域问题
3. 网页效果图请求优化
- 个人JS定制器demo展示(界面很丑,但不要在意这些细节):
上传对应刀版的定制图片:
效果图预览:
- 当前canvas上的定制图未发生变化,不发起请求
- 对应刀版未上传定制图,不发起请求
4. 颜色选择器的颜色发生变更时,需要点击颜色选择器外部才能更新颜色
<!--错误的写法-->
<input type="color" id="color" onchange="changeFontColor()" />
<!--正确的写法,将onchange事件改为oninput事件,此时点击不同的颜色就能更新-->
<input type="color" id="color" oninput="changeFontColor()" />
5. 原型挂载
// 避免图片上的操作图标被overlayImage覆盖
fabric.Canvas.prototype.controlsAboveOverlay = true;
//canvas的scaleX可能会出问题,需要修改精确度,确保canvas能够正常导出为json数据
fabric.Object.NUM_FRACTION_DIGITS = 20
6. 使用变换矩阵调整缩放比例的值
//加载背景图
function setBackgroundAttribute(file) {
fabric.util.loadImage(file, (img) => {
var rect = this.__rect = new fabric.Rect({
hasControls: false,
fill: new fabric.Pattern({
source: img,
//使用相同图案进行填充
repeat: 'repeat',
//变换矩阵(第1个和第4个是缩放比例)
patternTransform: [1, 0, 0, 1, 0, 0]
}),
left: 0,
top: 0,
type: 'Background',
width: canvas.width,
height: canvas.height,
// 不缓存,避免图片发生变化时,因为已经缓存导致图片无法更新
objectCaching: false,
selectable: false
})
canvas.add(rect);
canvas.moveTo(rect, 0);
_getElement('BackgroundResize').value = 100;
canvas.renderAll();
backgroundObject = rect;
}, null, 'anonymous')
}
//获得变换矩阵
function _getBackgroundImagePatternTransform() {
return this.__rect.fill.patternTransform;
}
//处理背景图片的转换方法
function _handleBackgroundImageTransformAction(type, transform, value = null) {
const actionMap = {
toggleFlipX: (_transform) => {使用```event.target.va"```,可以在上传多张图片的业务情形时,上传相同图片
_transform[0] = _transform[0] * (-1);
},
toggleFlipY: (_transform) => {
_transform[3] = _transform[3] * (-1);
},
scale: (_transform) => {
_transform[0] = value;
_transform[3] = value;
}
}
if (!Object.keys(actionMap).includes(type)) return;
return actionMap[type](transform, value);
}
// 缩放/方法
function resizeBackgroundImage(value) {
if (!this.__rect) return;
const factor = value / 100;
_handleBackgroundImageTransformAction('scale', _getBackgroundImagePatternTransform(), factor);
_getElement('BackgroundResize').title = `${value}%`;
canvas.requestRenderAll();
}
7. 上传本地图片
- 使用
label for和input type = "file"组合形成上传组件,将label for改造成按钮的样式,同时隐藏input type = "file"的样式;
<label class="bold btn add-background" for="background">
Add Background
</label>
<input type="file" onchange="addBackground(event)" id="background" accept="image/*" class="file-ipt">
- 使用
event.target.value="",可以应用在上传多张图片的业务情景时,上传相同图片; - 定义全局变量
imageObject。在每次调用上传本地图片的方法时,先canvas.remove(imageObject)清除图片,再将上传的图片赋值给全局变量imageObject,这种情况适合只能上传一张图片的业务情景。
8.使用固定定位做遮罩层
- loading效果:
<!--span的类我没加上-->
<div class="loading" id= "loading">
<img src="images/loading.gif" alt="">
<span>loading...</span>
</div>
.loading {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
background-color: white;
opacity: 0.8;
z-index: 2021;
display: flex;
justify-content: center;
align-items: center;
}
9.fabric.js功能属性
//禁止缩放时翻转
lockScalingFlip: true,
//绕图片中心点缩放
centeredScaling: true,
//绕图片中心点旋转
centeredRotation: true,
//设置坐标的中心点
OriginX: 'center', //X轴方向,有'left','center','right'这三个属性可供选择
OriginY: 'center',//Y轴方向,有'top','center','bottom'这三个属性可供选择
//fabric.js中的left和top
//left是中心点到画布左侧的距离
//top是中心点到画布上方的距离
9.两个数组不能直接使用===进行比较
在JS中,两个数组不能直接使用===进行比较,返回值恒为false。但是可以先将数组转换为字符串,再进行比较。
[1,2,3].toString() === [1,2,3].toString() //返回true,如果数组的先后顺序不一致,会返回false
[1,2,3].sort().toString() === [1,3,2].sort().toString() //返回true
10.对象转换为数组直接使用[]包裹对象即可
写在最后
还是很小白,还有很多知识需要学习和总结,专心学习技术。
总结:有点小绕,但只要自己看得懂就行了。