记vue分步引导的坑 driver.js 与 vue-tour html2canvas 与 dom-to-image

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

部分dom高亮时只有一个白色的框框覆
整了两天发现搞不定于是准备换插件。

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)

在这里插入图片描述
延迟0.3s:

	/* 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,每个步骤必有,干脆写出来,不然重复创建与移除,浪费资源

<!-- 生成的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);
	});
},
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值