1、由于项目需要加入用户指引,于是我就找了下相关的插件。一开始使用driver.js做了个demo感觉还是不错的,于是就准备使用driver.js,修改下样式就行了。
2、但是真正用设计图来设置时却发现了问题,由于项目是用vue编写的,根据设计图拆分了很多可复用的组件。设计图中很多需要高亮的dom节点都是在好几个组件之下的,driver.js获取不到,而且部分dom高亮时只有一个白色的框框覆盖。
cnpm i driver.js -S
import Driver from 'driver.js';
import 'driver.js/dist/driver.min.css';
export default {
props: {
guideVisible: {
type: Boolean,
default: false
},
},
data() {
return {
driver: null, // 指引实例
searchDataStep: [
{
element: '#step-searchData',
popover: {
className: 'only-one-line', // 弹窗样式
title: '无', // 弹窗标题不能为空,否则弹窗不显示。display隐藏吧。
position: 'left', // left, left-center, left-bottom, top, top-center, top-right, right, right-center, right-bottom, bottom, bottom-center, bottom-right, mid-center
description: '点击数据查询' // 弹窗内容
},
onNext: (e) => {
console.log('转到第二步前要执行的操作')
},
},
{
element: '#step-searchParams', // 嵌套组件深处,获取不到
popover: {
className: 'multiple-lines',
title: '查询条件',
position: 'left',
description: '选择对应数据的搜索条件,进行搜索。'
}
]
}
},
mounted(){
this.driver = new Driver({
prevBtnText: "上一步",
nextBtnText: "下一步",
doneBtnText: "完成",
closeBtnText: "关闭",
allowClose: false,
animate: true, // 动画
opacity: 0.5, // 遮罩层不透明度(0表示仅弹出且不覆盖)
padding: 10, // 边距
keyboardControl: false, // 禁止键盘操作
onHighlightStarted: (e) => {
// 样式
let timer = setTimeout(()=>{ // this.$nextTick 无效
let el = document.querySelector('#driver-popover-item')
if(el && el.style.display != 'none'){
el.style.display = 'flex' // 便于整合样式
let footerEl = document.getElementsByClassName('driver-popover-footer')[0]
footerEl.style.display = 'inline-block' // 底部控制按钮为行内元素
let prevEl = document.getElementsByClassName('driver-prev-btn')[0]
prevEl.style.display = 'none' // 隐藏上一步按钮
}
clearTimeout(timer)
},100)
}, // 在元素即将突出显示时调用
});
},
methods: {
// 点击操作指引
guideClick(){
this.driver.defineSteps(this.searchDataStep);
this.driver.start();
},
}
}
// css弹窗样式修改
/* driver.js */
#driver-popover-item
&.only-one-line
display flex!important
align-items center
justify-content space-between
.driver-popover-title
display none!important
.driver-popover-description
display inline-block!important
.driver-popover-footer
display inline-block!important
margin-top 0
&.multiple-lines
.driver-popover-title
font-size .14rem
.driver-popover-footer
margin-top .2rem
/* 统一取消关闭按钮 及 关闭按钮样式 */
.driver-popover-description
font-size .14rem
.driver-popover-footer
.driver-close-btn
display none!important
.driver-btn-group, .driver-close-only-btn
display flex!important
button
width 1rem
height .3rem
color #137CFD
padding 0
border .02rem solid #137CFD
font-size .14rem
background-color #ffffff
border-radius .04rem
.driver-prev-btn
display none!important
div
&.driver-fix-stacking
z-index 99!important
3、在帖子上看到有人使用vue-tour插件,缺点是:如果加上遮罩层,他的高亮跟没高亮一样,完全看不出来,而且高亮的元素也不明显(如果数据查询框不是刚好被选中,你还真没法一眼发现它)。
cnpm i vue-tour -S
// main.js
import VueTour from 'vue-tour'
require('vue-tour/dist/vue-tour.css')
Vue.use(VueTour)
<v-tour name="searchData" :steps="searchDataStep" :options="driverOptions" :callbacks="driverCallbacks"></v-tour>
export default {
data() {
return {
driverOptions: {
useKeyboardNavigation: false, // 是否可用键盘来控制前进后退
labels: {
buttonSkip: '跳过',
buttonPrevious: '上一步',
buttonNext: '下一步',
buttonStop: '完成'
},
highlight: true // 高亮
},
driverCallbacks: {
onStart: this.driverStart, // 在您开始游览时调用
onFinish: this.driverStop, //停止游览时调用
},
searchDataStep: [
{
target: '#step-searchData',
params: {
placement: 'left'
},
content: '点击数据查询',
before: type => new Promise((resolve, reject) => {
console.log('每一步执行前',type)
this.stepBefore()
resolve('foo')
})
},
{
target: '#step-searchParams',
header: {
title: '查询条件',
},
params: {
placement: 'left'
},
content: '选择对应数据的查询条件,进行搜索。',
before: type => new Promise((resolve, reject) => {
this.stepBefore()
resolve('foo')
})
},
]
}
},
methods: {
guideClick(){
this.guideListShow = false; // 隐藏分布指引类别
this.$tours['searchData'].start()
},
stepBefore(){
// 获取dom id
let tours = this.$tours['searchData']
let currentStep = tours.steps[tours.currentStep + 1]
let id = currentStep.target
this.domId = id
},
driverStart () {
console.log('开始')
},
driverStop () {
console.log('结束')
this.guideListShow = true
},
}
}
无遮罩层高亮,还算是能看见:
加遮罩层高亮,几乎看不出来,高亮元素也不明显:
但是他能够准确的获取我需要的dom节点,这一点我很满意,于是就准备使用vue-tour,然后自己做个遮罩层和高亮dom设置。
3、至于高亮dom如何实现,一开始用的 html2canvas 截图,但是对一些伪类、阴影啥的css支持不好,
cnpm i html2canvas -S
import html2canvas from 'html2canvas'
getImgBase64() { // 取消,样式有些获取不到,比如伪类。导致图片与dom不太一致
let _this = this
_this.domImg = '' // 图片为空
let _canvas = document.createElement('canvas')
// 获取dom位置
let _el = document.querySelector(_this.domId)
let style = _el.getBoundingClientRect()
let w = parseInt(style.width)
this.width = w
let h = parseInt(style.height)
this.height = h
this.offsetTop = parseInt(style.top)
this.offsetLeft = parseInt(style.left)
//可以按照自己的需求,对context的参数修改,translate指的是偏移量
let context = _canvas.getContext('2d')
//以下代码是获取根据屏幕分辨率,来设置canvas的宽高以获得高清图片
let devicePixelRatio = window.devicePixelRatio || 2 // 屏幕的设备像素比
// 浏览器在渲染canvas之前存储画布信息的像素比
let backingStoreRatio = context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1
let ratio = devicePixelRatio / backingStoreRatio // canvas的实际渲染倍率
_canvas.width = w * ratio
_canvas.height = h * ratio
_canvas.style.width = w + 'px'
_canvas.style.height = h + 'px'
html2canvas(_el, {
canvas: _canvas
}).then(function (canvas) {
_this.domImg = canvas.toDataURL('image/png')
let imgEl = document.createElement('img')
imgEl.setAttribute('src', _this.domImg)
})
}
对伪类阴影支持不好:
4、改成dom-to-image,代码更少,截图也更清晰,重点是对css支持较好。
cnpm i dom-to-image -S
import domtoimage from 'dom-to-image';
shotPic() { // png是透明的,统一在图片的父节点上加白色背景与白色阴影做高亮显示。
let _this = this
_this.domImg = '' // 图片为空
let _el = document.querySelector(_this.domId)
domtoimage.toPng(_el).then((dataUrl) => {
_this.domImg = dataUrl;
})
.catch((error) => {
console.error('图片生成失败!', error);
});
},
// css
.target
position absolute
background #fff
box-shadow 0 0 0 .1rem #fff
img
display block
width 100%
height 100%
5、自定义内容
1)、默认勾选第一项,要延迟0.3秒再截图,否则截取不到选中状态
this.resetPos(this.domId)
this.domImg = '' // 图片为空
this.shotPic(this.domId)
/* isDelayPic 延迟0.3s截图 */
this.resetPos(this.domId)
this.domImg = '' // 图片为空
if(isDelayPic){
// 默认勾选第一项,要延迟0.3秒再截图,否则截取不到选中状态
let _timer = setTimeout(() => {
this.shotPic(this.domId)
clearTimeout(_timer)
},300)
}else{
this.shotPic(this.domId)
}
2)、路由切换
首先若有多个路由切换的,分步引导组件应放在设置< router-view/>的页面,例如App.vue。
该引导的步骤弹窗应在切换路由成功后再显示,否则获取不到dom会显示在左上角。
content: '核实数据数量、金额,填写联系信息,提交订单即可。',
before: type => new Promise((resolve, reject) => {
this.$store.$emit('DataList-userGuideToOrder') // 去确认订单页面,设置默认数据
this.stepBefore(true, true)
resolve('foo')
})
这里我在创建订单页挂载完成时设置store createOrderMounted为true,在step步骤里设置一个定时器,每一秒监听一次创建订单页是否挂载完成,当createOrderMounted为true时,再执行步骤弹窗显示等操作。
computed: {
...mapState('dataExpress',['createOrderMounted']),
},
[
{
target: '#step-toOrder',
header: {
title: '购物车勾选',
},
params: {
placement: 'top'
},
content: '勾选所需购买的数据,点击提交订单。',
extra: ['.stripTable-0'],
before: type => new Promise((resolve, reject) => {
this.dataListShow('cart') // 状态栏、数据列表 切换为购物车
resolve('foo')
}).then(()=>{
this.stepBefore()
})
},
{
target: '#step-createOrder',
header: {
title: '提交订单',
},
params: {
placement: 'top'
},
content: '核实数据数量、金额,填写联系信息,提交订单即可。',
before: type => new Promise((resolve, reject) => {
this.$store.$emit('DataList-userGuideToOrder') // 去确认订单页面,设置默认数据
if(!this.createOrderMounted){
let timer = setInterval(()=>{
console.log('循环')
if(this.createOrderMounted){
console.log('before 创建订单页挂载完成')
clearInterval(timer)
resolve('foo')
}
},1000)
}else{
resolve('foo')
}
}).then(()=>{
console.log('stepBefore开始')
this.stepBefore(true)
})
},
],
// 指引结束
driverStop () {
this.$store.commit('dataExpress/SET_CREATE_ORDER_MOUNTED', false)
this.domImg = ''
},
// createOrder.vue
mounted() {
...
this.$store.commit('dataExpress/SET_CREATE_ORDER_MOUNTED', true)
}
3)多dom高亮
由于我们的ui设计图在某一引导步骤上高亮了多个元素,于是我就自己整了一下。
主要高亮dom,每个步骤必有,干脆写出来,不然重复创建与移除,浪费资源
<div class="target" v-else-if="domImg" :style="posStyle">
<img :src="domImg" />
div>
<v-tour name="my-tour" :steps="tourSteps" :options="tourOptions" :callbacks="tourCallbacks">v-tour>
其余额外高亮dom,先在 data() 设置step数据时,在需要高亮多个dom的步骤上加上extra属性。
{
target: '#step-addCart',
header: {
title: '数据购买',
},
params: {
placement: 'top'
},
content: '查询结果中选择所需购买的数据,点击加入购物车。',
extra: ['.el-table__header','.stripTable-0'], // 其余高亮dom
before: type => new Promise((resolve, reject) => {
this.stepBefore(true)
resolve('foo')
})
},
每一步骤执行前先移除上一次添加的额外高亮dom,判断当前步骤有没有额外dom,有就创建容器存放截图,并设置其style位置。
/**
* @description: 每一个引导步骤执行前 必执行的函数
* @param {*} isDelayPic 是否延迟截图。有时设置完列表勾选,截图没截到,加个延时器。在step里加currentStep会自动+1
* @param {*} isDelayDom 是否延迟获取dom。有时页面跳转,dom没获取到,加个延时器。
*/
stepBefore(isDelayPic = false){
//移除上一步骤添加的额外高亮dom
let extraTargetList = document.getElementsByName('target-extra') // 上一步骤添加的额外高亮dom
let parentNode = document.querySelector('.user-guide-box')
let arr = Array.prototype.slice.call(extraTargetList); // 转成数组。非ie浏览器正常
arr.forEach(node => parentNode.removeChild(node) )
this.domImg = ''
/* 获取dom id */
let tours = this.$tours['my-tour']
let currentStep = tours.steps[tours.currentStep + 1]
// console.log('currentStep',tours.currentStep,tours.steps,currentStep)
console.log('id',currentStep.target)
let id = currentStep.target
this.domId = id
// console.log('额外dom',currentStep.extra)
this.resetPos(this.domId, isDelayPic) // 主dom高亮
if(currentStep.extra && currentStep.extra.length > 0){ // 额外dom高亮
currentStep.extra.forEach(v => this.resetPos(v, isDelayPic, 'extra') )
}
},
/**
* @description: 获取即将高亮的dom位置并截图
* @param {*} domId 要高亮的dom节点id
* @param {*} isDelay 是否需要延迟0.3秒截图
* @param {*} type 类型:extra 延迟,noExtra 非延迟。默认noExtra。extra需要向shotPic()传递style。
*/
resetPos(domId, isDelay = false, type = 'noExtra') {
let _el = document.querySelector(domId)
let style = _el.getBoundingClientRect()
// console.log('坐标',style)
let width, height, offsetTop, offsetLeft;
width = parseInt(style.width)
height = parseInt(style.height)
offsetTop = parseInt(style.top)
offsetLeft = parseInt(style.left)
let cssText = `position: absolute;left:${offsetLeft}px;top:${offsetTop}px;width:${width}px;height:${height}px;background:#fff;`; // 缺点是覆盖之前所有style
type != 'extra' && (this.posStyle = cssText) // 设置主dom样式
if(isDelay){
// 默认勾选第一项,要延迟0.3秒再截图,否则截取不到选中状态
let _timer = setTimeout(() => {
this.shotPic(domId, type, type == 'extra' ? cssText : '')
clearTimeout(_timer)
},300)
}else{
this.shotPic(domId, type, type == 'extra' ? cssText : '')
}
},
/**
* @description: dom截图
* @param {*} domId 要高亮的dom节点id
* @param {*} type 类型:extra 延迟,noExtra 非延迟。默认noExtra。extra需要向文档添加高亮节点。
* @param {*} cssText 高亮节点样式(非主节点)
*/
shotPic(domId, type = 'noExtra', cssText = '') { // toPng透明,统一在图片的父节点上加白色背景做高亮显示。
let _this = this
let _el = document.querySelector(domId)
domtoimage.toPng(_el).then((dataUrl) => {
if(type == 'extra'){
let divEl = document.createElement('div')
divEl.setAttribute("class", "target");
divEl.setAttribute("name", "target-extra"); // 便于移除
divEl.style.cssText = cssText; // 缺点是覆盖之前所有style
let imgEl = document.createElement('img')
imgEl.setAttribute('src', dataUrl)
divEl.appendChild(imgEl)
let box = document.querySelector('.user-guide-box')
box.appendChild(divEl);
}else{
_this.domImg = dataUrl
}
})
.catch((error) => {
console.error('图片生成失败!', error);
});
},