|
|
@@ -0,0 +1,526 @@
|
|
|
+<template>
|
|
|
+ <view v-if="show" @click="focusTap" @touchmove.stop.prevent :style="{ '--path': path, '--duration': duration + 'ms' }"
|
|
|
+ class="clip-container">
|
|
|
+ <view @click.stop="maskTap" class="clip-box"></view>
|
|
|
+ <view @click.stop class="guide-box animate__animated" :class="[contentVisible ? 'opacity-100' : 'opacity-0']" :style="[msgStyles]">
|
|
|
+ <view v-if="$slots.message" class="">
|
|
|
+ <slot name="message" :msg="list[index].msg"></slot>
|
|
|
+ </view>
|
|
|
+ <view v-else class="msg-label">
|
|
|
+ {{ list[index].msg }}
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="step-box">
|
|
|
+ <view v-if="index > 0" @click="lastStep" class="step-btn">
|
|
|
+ 上一步
|
|
|
+ </view>
|
|
|
+ <view @click="nextStep" class="step-btn">
|
|
|
+ 下一步
|
|
|
+ </view>
|
|
|
+ <view @click="skipStep" class="step-btn">
|
|
|
+ 跳过
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+<script>
|
|
|
+const path_default = `
|
|
|
+ polygon(
|
|
|
+ 0% 0%,
|
|
|
+ 0% 0%,
|
|
|
+ 100% 0%,
|
|
|
+ 100% 100%,
|
|
|
+ 0% 100%,
|
|
|
+ 0% 0%,
|
|
|
+ 0% 0%,
|
|
|
+ 0% 100%,
|
|
|
+ 100% 100%,
|
|
|
+ 100% 0%
|
|
|
+ )
|
|
|
+ `
|
|
|
+
|
|
|
+/**
|
|
|
+ * guide 引导弹窗
|
|
|
+ * @description 引导组件,用于教学提示、用户操作引导等内容,支持自定义组件节点自动聚焦和手动设置相对位置聚焦。组件只提供容器,内部内容由用户自定义
|
|
|
+ * @tutorial //git地址
|
|
|
+ * @property {Boolean} show 是否展示弹窗 (默认 false )
|
|
|
+ * @property {Number} index 当前步骤索引(默认 0 )
|
|
|
+ * @property {String | Number} duration 动画时长
|
|
|
+ * @property {String} unit 换算单位
|
|
|
+ * @property {Array} list 步骤列表
|
|
|
+ * @property {Object} {
|
|
|
+ * @property{String} ref 要聚焦的子组件的ref
|
|
|
+ * @property{String} target 当前组件/子组件中的选择器标识
|
|
|
+ * @property{String} position 提示框展示位置'top'/'bottom'
|
|
|
+ * @property{String} msg 提示框文字
|
|
|
+ * @property{String} msgStyles 提示框自定义样式
|
|
|
+ * @property{String} width 自定义聚焦范围的宽度
|
|
|
+ * @property{String} height 自定义聚焦范围的高度
|
|
|
+ * @property{String} left 自定义聚焦范围的左侧偏移量 left/right仅生效一个 使用自定义聚焦后ref、target属性将失效
|
|
|
+ * @property{String} right 自定义聚焦范围的右侧偏移量 left/right仅生效一个 使用自定义聚焦后ref、target属性将失效
|
|
|
+ * @property{String} top 自定义聚焦范围的上侧偏移量 top/bottom仅生效一个 使用自定义聚焦后ref、target属性将失效
|
|
|
+ * @property{String} bottom 自定义聚焦范围的下侧偏移量 top/bottom仅生效一个 使用自定义聚焦后ref、target属性将失效
|
|
|
+ } list步骤列表内部参数说明
|
|
|
+ * @event {Function} open 弹出层打开
|
|
|
+ * @event {Function} close 弹出层收起
|
|
|
+ * @event {Function} focus 聚焦区域点击事件
|
|
|
+ * @event {Function} mask 遮罩区域点击事件
|
|
|
+ * @event {Function} next 执行下一步
|
|
|
+ * @event {Function} last 执行上一步
|
|
|
+ * @event {Function} skip 跳过所有步骤
|
|
|
+ * @event {Function} finish 结束引导
|
|
|
+ * @event {Function} getQuery 获取兄弟组件布局查询对象
|
|
|
+ * @example <popup-guide :show="guideShow" :list="guideList" :index="guideIndex" @next="guideNext" @last="guideLast" @skip="guideSkip" @finish="guideFinish" ></popup-guide>
|
|
|
+ */
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ path: path_default,
|
|
|
+ msgStyles: {},
|
|
|
+ contentVisible: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ props: {
|
|
|
+ // 是否展示
|
|
|
+ show: {
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ // 步骤列表
|
|
|
+ list: {
|
|
|
+ default: []
|
|
|
+ },
|
|
|
+ // 当前步骤索引
|
|
|
+ index: {
|
|
|
+ default: 0
|
|
|
+ },
|
|
|
+ // 动画时长
|
|
|
+ duration: {
|
|
|
+ default: 350
|
|
|
+ },
|
|
|
+ // 换算单位
|
|
|
+ unit: {
|
|
|
+ default: 'rpx'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ emits: ['open', 'close', 'focus', 'mask', 'next', 'last', 'skip', 'finish', 'getQuery'],
|
|
|
+ watch: {
|
|
|
+ index: {
|
|
|
+ handler(newVal) {
|
|
|
+ if (newVal != undefined) {
|
|
|
+ this.getCurrentPath()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ show: {
|
|
|
+ handler(newVal) {
|
|
|
+ if (newVal == true) {
|
|
|
+ this.getCurrentPath()
|
|
|
+ this.$emit('open', this.callBackData())
|
|
|
+ }
|
|
|
+ if (newVal == false) {
|
|
|
+ this.$emit('close', this.callBackData())
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ pathInit() {
|
|
|
+ this.path = path_default
|
|
|
+ },
|
|
|
+ testNumber(value) {
|
|
|
+ return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value)
|
|
|
+ },
|
|
|
+ addUnit(value = '', unit = this.unit) {
|
|
|
+ value = String(value)
|
|
|
+ return this.testNumber(value) ? `${value}${unit}` : value
|
|
|
+ },
|
|
|
+ isEmpty(val) {
|
|
|
+ return val === null || val === undefined || val === ''
|
|
|
+ },
|
|
|
+ async getCurrentPath() {
|
|
|
+ await this.$nextTick(() => { })
|
|
|
+
|
|
|
+ let {
|
|
|
+ ref,
|
|
|
+ target,
|
|
|
+
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+
|
|
|
+ top,
|
|
|
+ left,
|
|
|
+ right,
|
|
|
+ bottom,
|
|
|
+
|
|
|
+ gap,
|
|
|
+ position,
|
|
|
+ msgStyles
|
|
|
+
|
|
|
+ } = this.list[this.index]
|
|
|
+ let x1, x2, y1, y2
|
|
|
+
|
|
|
+ if (target) {
|
|
|
+ let maxWidth
|
|
|
+ uni.getSystemInfo({
|
|
|
+ success(res) {
|
|
|
+ // #ifndef H5
|
|
|
+ maxWidth = res.screenWidth
|
|
|
+ // #endif
|
|
|
+ // #ifdef H5
|
|
|
+ maxWidth = res.windowWidth
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // console.log(maxWidth,'maxWidth')
|
|
|
+
|
|
|
+ const targetDom = await new Promise((resolve, reject) => {
|
|
|
+ if (ref) {
|
|
|
+ this.$emit('getQuery', function () {
|
|
|
+ uni.createSelectorQuery()
|
|
|
+ .in(this.$refs[ref])
|
|
|
+ .select(target)
|
|
|
+ .boundingClientRect(data => {
|
|
|
+ resolve(data)
|
|
|
+ }).exec()
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ uni.createSelectorQuery()
|
|
|
+ .select(target)
|
|
|
+ .boundingClientRect(data => {
|
|
|
+ resolve(data)
|
|
|
+ }).exec()
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // console.log(targetDom,'targetDom')
|
|
|
+ width = targetDom.width
|
|
|
+ height = targetDom.height
|
|
|
+ top = targetDom.top
|
|
|
+ left = targetDom.left
|
|
|
+ right = maxWidth - targetDom.right
|
|
|
+ //+1像素修正元素处于屏幕正中间时的msg相对位置
|
|
|
+ if (left > right + 1) {
|
|
|
+ left = ''
|
|
|
+ } else {
|
|
|
+ right = ''
|
|
|
+ }
|
|
|
+ bottom = targetDom.bottom
|
|
|
+ // console.log(left,'left',right,'right')
|
|
|
+
|
|
|
+
|
|
|
+ x1 = !this.isEmpty(left) ? `calc(${this.addUnit(left, 'px')} + ${this.addUnit(width, 'px')})` : `calc(100% - ${this.addUnit(right, 'px')})`
|
|
|
+ x2 = !this.isEmpty(left) ? `${this.addUnit(left, 'px')}` : `calc(100% - ${this.addUnit(right, 'px')} - ${this.addUnit(width, 'px')})`
|
|
|
+
|
|
|
+ y1 = !this.isEmpty(top) ? `${this.addUnit(top, 'px')}` : `calc(100% - ${this.addUnit(bottom, 'px')} - ${this.addUnit(height, 'px')})`
|
|
|
+ y2 = !this.isEmpty(top) ? `calc(${this.addUnit(top, 'px')} + ${this.addUnit(height, 'px')})` : `calc(100% - ${this.addUnit(bottom, 'px')})`
|
|
|
+
|
|
|
+ } else {
|
|
|
+ x1 = !this.isEmpty(left) ? `calc(${this.addUnit(left)} + ${this.addUnit(width)})` : `calc(100% - ${this.addUnit(right)})`
|
|
|
+ x2 = !this.isEmpty(left) ? `${this.addUnit(left)}` : `calc(100% - ${this.addUnit(right)} - ${this.addUnit(width)})`
|
|
|
+
|
|
|
+ y1 = !this.isEmpty(top) ? `${this.addUnit(top)}` : `calc(100% - ${this.addUnit(bottom)} - ${this.addUnit(height)})`
|
|
|
+ y2 = !this.isEmpty(top) ? `calc(${this.addUnit(top)} + ${this.addUnit(height)})` : `calc(100% - ${this.addUnit(bottom)})`
|
|
|
+ }
|
|
|
+ // 延时,解决动画丢失的问题
|
|
|
+ await new Promise((resolve) => {
|
|
|
+ setTimeout(() => {
|
|
|
+ resolve()
|
|
|
+ }, 50)
|
|
|
+ })
|
|
|
+ this.path = `
|
|
|
+ polygon(
|
|
|
+ 0% 0%,
|
|
|
+ 0% ${y1},
|
|
|
+ ${x1} ${y1},
|
|
|
+ ${x1} ${y2},
|
|
|
+ ${x2} ${y2},
|
|
|
+ ${x2} ${y1},
|
|
|
+ 0% ${y1},
|
|
|
+ 0% 100%,
|
|
|
+ 100% 100%,
|
|
|
+ 100% 0%
|
|
|
+ )
|
|
|
+ `
|
|
|
+ // console.log(this.path,'path.value')
|
|
|
+
|
|
|
+
|
|
|
+ //设置msg样式
|
|
|
+ const msgDom = await new Promise((resolve, reject) => {
|
|
|
+ uni.createSelectorQuery().in(this).select('.guide-box').boundingClientRect(data => {
|
|
|
+ resolve(data)
|
|
|
+ }).exec()
|
|
|
+ })
|
|
|
+
|
|
|
+ let animationName
|
|
|
+ gap = gap || 20
|
|
|
+ position = position || 'top'
|
|
|
+
|
|
|
+ if (target) {
|
|
|
+ switch (position) {
|
|
|
+ case 'top':
|
|
|
+ if (!this.isEmpty(top)) {
|
|
|
+ top = `calc(${this.addUnit(top, 'px')} - ${this.addUnit(gap)} - ${this.addUnit(msgDom.height, 'px')})`
|
|
|
+ } else {
|
|
|
+ top = `calc(100% - ${this.addUnit(bottom, 'px')} - ${this.addUnit(height, 'px')} - ${this.addUnit(gap)} - ${this.addUnit(msgDom.height, 'px')})`
|
|
|
+ }
|
|
|
+ animationName = 'backInDown'
|
|
|
+ break;
|
|
|
+ case 'bottom':
|
|
|
+ if (!this.isEmpty(top)) {
|
|
|
+ top = `calc(${this.addUnit(top, 'px')} + ${this.addUnit(height, 'px')} + ${this.addUnit(gap)})`
|
|
|
+ } else {
|
|
|
+ top = `calc(100% - ${this.addUnit(bottom, 'px')} + ${this.addUnit(gap)} + ${this.addUnit(msgDom.height, 'px')})`
|
|
|
+ }
|
|
|
+ animationName = 'backInUp'
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ switch (position) {
|
|
|
+ case 'top':
|
|
|
+ if (top) {
|
|
|
+ top = `calc(${this.addUnit(top)} - ${this.addUnit(gap)} - ${this.addUnit(msgDom.height, 'px')})`
|
|
|
+ } else {
|
|
|
+ top = `calc(100% - ${this.addUnit(bottom)} - ${this.addUnit(height)} - ${this.addUnit(gap)} - ${this.addUnit(msgDom.height, 'px')})`
|
|
|
+ }
|
|
|
+ animationName = 'backInDown'
|
|
|
+ break;
|
|
|
+ case 'bottom':
|
|
|
+ if (top) {
|
|
|
+ top = `calc(${this.addUnit(top)} + ${this.addUnit(height)} + ${this.addUnit(gap)})`
|
|
|
+ } else {
|
|
|
+ top = `calc(100% - ${this.addUnit(bottom)} + ${this.addUnit(gap)} + ${this.addUnit(msgDom.height, 'px')})`
|
|
|
+ }
|
|
|
+ animationName = 'backInUp'
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ left = target ? this.addUnit(left, 'px') : this.addUnit(left)
|
|
|
+ right = target ? this.addUnit(right, 'px') : this.addUnit(right)
|
|
|
+
|
|
|
+ this.msgStyles = {
|
|
|
+ 'top': top,
|
|
|
+ 'left': left,
|
|
|
+ 'right': right,
|
|
|
+ 'transition': `all ${this.duration}ms`,
|
|
|
+ 'animation-name': animationName,
|
|
|
+ '-webkit-animation-name': animationName,
|
|
|
+
|
|
|
+ ...msgStyles
|
|
|
+ }
|
|
|
+ setTimeout(() => {
|
|
|
+ this.contentVisible = true;
|
|
|
+ }, 300);
|
|
|
+ },
|
|
|
+ nextStep() {
|
|
|
+ if (this.list[this.index + 1]) {
|
|
|
+ this.$emit('next', this.callBackData())
|
|
|
+ this.$emit('update:index', this.index + 1)
|
|
|
+ } else {
|
|
|
+ this.contentVisible = false;
|
|
|
+ this.pathInit()
|
|
|
+ setTimeout(() => {
|
|
|
+ this.$emit('finish', this.callBackData())
|
|
|
+ this.$emit('update:show', false)
|
|
|
+ }, this.duration)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ lastStep() {
|
|
|
+ this.$emit('last', this.callBackData())
|
|
|
+ },
|
|
|
+ skipStep() {
|
|
|
+ this.pathInit()
|
|
|
+ setTimeout(() => {
|
|
|
+ this.$emit('skip', this.callBackData())
|
|
|
+ }, this.duration)
|
|
|
+ },
|
|
|
+ focusTap() {
|
|
|
+ this.$emit('focus', this.callBackData())
|
|
|
+ },
|
|
|
+ maskTap() {
|
|
|
+ this.$emit('mask', this.callBackData())
|
|
|
+ },
|
|
|
+ callBackData() {
|
|
|
+ return {
|
|
|
+ index: this.index,
|
|
|
+ value: this.list[this.index]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+.shadow {
|
|
|
+ box-shadow: 2rpx 2rpx 12rpx var(--shadow);
|
|
|
+}
|
|
|
+
|
|
|
+.clip-container {
|
|
|
+ --path: '';
|
|
|
+ --duration: '';
|
|
|
+ --color: #1676ff;
|
|
|
+
|
|
|
+ position: absolute;
|
|
|
+ /* #ifdef H5 */
|
|
|
+ width: 750rpx;
|
|
|
+ /* #endif */
|
|
|
+ /* #ifndef H5 */
|
|
|
+ width: 100%;
|
|
|
+ /* #endif */
|
|
|
+ height: 100vh;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ z-index: 9996;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+.clip-box {
|
|
|
+
|
|
|
+ box-sizing: border-box;
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh;
|
|
|
+ transition: all var(--duration);
|
|
|
+
|
|
|
+ clip-path: var(--path);
|
|
|
+
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+.guide-box {
|
|
|
+ width: 50%;
|
|
|
+ transition: all var(--duration);
|
|
|
+ position: absolute;
|
|
|
+ color: var(--color);
|
|
|
+ background-color: #fff;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ box-shadow: 2rpx 2rpx 2rpx #fff;
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding: 20rpx;
|
|
|
+
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.msg-label {
|
|
|
+ font-size: 28rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.step-box {
|
|
|
+ margin-top: auto;
|
|
|
+ width: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ padding: 20rpx 0 0 0;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.step-btn {
|
|
|
+ margin-left: 20rpx;
|
|
|
+ box-shadow: 2rpx 2rpx 12rpx #ddd;
|
|
|
+ padding: 6rpx 16rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+@-webkit-keyframes backInDown {
|
|
|
+ 0% {
|
|
|
+ -webkit-transform: translateY(-1200px);
|
|
|
+ transform: translateY(-1200px);
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
+
|
|
|
+ 80% {
|
|
|
+ -webkit-transform: translateY(0px);
|
|
|
+ transform: translateY(0px);
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ -webkit-transform: scale(1);
|
|
|
+ transform: scale(1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+@keyframes backInDown {
|
|
|
+ 0% {
|
|
|
+ -webkit-transform: translateY(-1200px);
|
|
|
+ transform: translateY(-1200px);
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
+
|
|
|
+ 80% {
|
|
|
+ -webkit-transform: translateY(0px);
|
|
|
+ transform: translateY(0px);
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ -webkit-transform: scale(1);
|
|
|
+ transform: scale(1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@-webkit-keyframes backInUp {
|
|
|
+ 0% {
|
|
|
+ -webkit-transform: translateY(1200px);
|
|
|
+ transform: translateY(1200px);
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
+
|
|
|
+ 80% {
|
|
|
+ -webkit-transform: translateY(0px);
|
|
|
+ transform: translateY(0px);
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ -webkit-transform: scale(1);
|
|
|
+ transform: scale(1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes backInUp {
|
|
|
+ 0% {
|
|
|
+ -webkit-transform: translateY(1200px);
|
|
|
+ transform: translateY(1200px);
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
+
|
|
|
+ 80% {
|
|
|
+ -webkit-transform: translateY(0px);
|
|
|
+ transform: translateY(0px);
|
|
|
+ opacity: 0.7;
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ -webkit-transform: scale(1);
|
|
|
+ transform: scale(1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.animate__animated {
|
|
|
+ -webkit-animation-duration: var(--duration);
|
|
|
+ animation-duration: var(--duration);
|
|
|
+ -webkit-animation-duration: var(--duration);
|
|
|
+ animation-duration: var(--duration);
|
|
|
+ -webkit-animation-fill-mode: both;
|
|
|
+ animation-fill-mode: both;
|
|
|
+}
|
|
|
+</style>
|