123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550 |
- <template>
- <view class="overlay" :style="modelStyle" @touchmove.stop.prevent v-if="showGuide">
- <view class="overlay-part" ref="overlayPartTop" :animation="overlayPartTop"
- :style="{ height: `${hole.top}px`,transform:`translateY(${-hole.top}px)`}"></view>
- <view class="middle-row">
- <view class="overlay-part" ref="overlayPartLeft" :animation="overlayPartLeft"
- :style="{ width: `${hole.left}px`, height: `${hole.height}px`,transform:`translateX(${-hole.left}px)`}">
- </view>
- <view class="hole" :style="{ width: `${hole.width}px`, height: `${hole.height}px` }" @click="handler">
- </view>
- <view class="overlay-part" ref="overlayPartRight" :animation="overlayPartRight"
- :style="{ width: `${screenWidth - hole.left - hole.width}px`, height: `${hole.height}px`,transform:`translateX(${screenWidth - hole.left - hole.width}px)`}">
- </view>
- </view>
- <view class="overlay-part" ref="overlayPartBottom" :animation="overlayPartBottom"
- :style="{ height: `${screenHeight - hole.top - hole.height}px`,transform:`translateY(${screenHeight - hole.top - hole.height}px)`}">
- </view>
- <!-- tips提示框 -->
- <view class="tips" ref="tips" :style="{opacity:isTips?1:0,...tipPosition}">
- <text class="text">{{ guideInfo.tips }}</text>
- <view class="tool-btn">
- <text @click="skip" class="text">跳过</text>
- <view class="next" style="" @click="next">
- <text class="text">{{ guideInfo.next }}</text>
- </view>
- </view>
- <view class="arrow" :style="arrowPosition"></view>
- </view>
- </view>
- </template>
- <script>
- // #ifdef APP-NVUE
- const dom = weex.requireModule("dom");
- const animationModule = weex.requireModule('animation')
- // #endif
- export default {
- data() {
- return {
- stepName: 'step', //该提示步骤的名称,用于不在重复展示
- isTips: false,
- isStop: false,
- guideList: [{
- el: '',
- tips: '',
- next: '',
- }],
- overlayPartTop: {},
- overlayPartLeft: {},
- overlayPartRight: {},
- overlayPartBottom: {},
- tipsAction: {},
- index: 0, // 当前展示的索引
- showGuide: false, // 是否显示引导
- tipPosition: {},
- arrowPosition: '',
- // 屏幕宽高
- screenWidth: 0,
- screenHeight: 0,
- // 洞的动态位置和尺寸
- hole: {
- top: 0, // 距顶部距离
- left: 0, // 距左侧距离
- width: 0, // 洞的宽度
- height: 0, // 洞的高度
- }
- };
- },
- computed: {
- guideInfo() {
- return this.guideList[this.index] || {};
- },
- modelStyle() {
- const style = {
- width: this.screenWidth + 'px',
- height: this.screenHeight + 'px',
- }
- return style;
- },
- },
- mounted() {
- const systemInfo = uni.getSystemInfoSync();
- const guide = uni.getStorageSync(this.stepName);
- this.$nextTick(() => {
- this.screenWidth = systemInfo.windowWidth;
- this.screenHeight = systemInfo.windowHeight;
- })
- },
- methods: {
- handler() {
- this.$emit('clickChunk', this.index)
- },
- async skip() {
- this.isTips = false;
- await this.closeActives()
- this.showGuide = false;
- },
- async next() {
- if (!this.guideList[this.index] || this.isStop) return;
- this.isTips = false;
- this.index++
- this.getDomInfo()
- },
- start(step) {
- this.guideList = step.guideList;
- this.stepName = step.name;
- this.showGuide = true;
- this.getDomInfo();
- },
- checkEl(el) {
- return new Promise((resolve) => {
- this.getComponentRect(el)
- .then(rect => {
- if ((rect.top + rect.height + 100) > this.screenHeight) {
- this.scrollToElement(el, rect.top).then(() => {
- resolve()
- })
- } else {
- resolve()
- }
- })
- .catch(() => {
- resolve()
- })
- })
- },
- getDomInfo() {
- this.$nextTick(async () => {
- const {
- el
- } = this.guideInfo;
- this.isStop = true;
- await this.closeActives()
- await this.checkEl(el)
- const rect = await this.getComponentRect(el)
- if (!rect) return;
- for (let key in rect) {
- this.hole[key] = rect[key] = Math.round(rect[key]);
- }
- await this.openActives()
- this.isTips = true;
- this.$nextTick(() => {
- this.viewTips()
- })
- })
- },
- animationActive(actives) {
- // console.log(actives);
- return new Promise((resolve) => {
- let count = 0;
- // #ifdef APP-NVUE
- for (let active of actives) {
- animationModule.transition(active.dom, {
- styles: active.styles || {},
- duration: active.duration,
- timingFunction: active.timingFunction,
- delay: active.delay || 0,
- transformOrigin: active.transformOrigin,
- }, () => {
- count++
- if (count >= actives.length) {
- resolve()
- }
- })
- }
- // #endif
- // #ifndef APP-NVUE
- for (let active of actives) {
- let animation = uni.createAnimation({
- duration: active.duration,
- delay: active.delay || 0,
- timingFunction: active.timingFunction,
- transformOrigin: active.transformOrigin,
- })
- let res = this.extractTransformValue(active.styles.transform)
- let args = res.value.split(',')
- animation[res.key](...args).step()
- this[active.dom] = animation.export()
- }
- setTimeout(() => {
- resolve()
- }, 500)
- // #endif
- })
- },
- extractTransformValue(transformString) {
- // 正则表达式:匹配 translateY(或 translateX( 等形式
- const regex = /([a-zA-Z]+)\(([^)]+)\)/;
- const match = transformString.match(regex);
- if (match) {
- // match[1] 是 key(如 translateY),match[2] 是括号内的内容(如 1000px)
- let value = match[2];
- return {
- key: match[1], // 提取关键字(如 translateY)
- value: value // 转换为数字(例如 1000)
- };
- }
- },
- async closeActives() {
- const duration = 500;
- const actives = [{
- // #ifdef APP-NVUE
- dom: this.$refs.overlayPartTop,
- // #endif
- // #ifndef APP-NVUE
- dom: 'overlayPartTop',
- // #endif
- styles: {
- transform: `translateY(-1000px)`,
- },
- transformOrigin: 'center center',
- timingFunction: 'ease',
- duration,
- },
- {
- // #ifdef APP-NVUE
- dom: this.$refs.overlayPartBottom,
- // #endif
- // #ifndef APP-NVUE
- dom: 'overlayPartBottom',
- // #endif
- styles: {
- transform: `translateY(1000px)`,
- },
- transformOrigin: 'center center',
- timingFunction: 'ease',
- duration,
- },
- {
- // #ifdef APP-NVUE
- dom: this.$refs.overlayPartLeft,
- // #endif
- // #ifndef APP-NVUE
- dom: 'overlayPartLeft',
- // #endif
- styles: {
- transform: `translateX(-1000px)`,
- },
- transformOrigin: 'center center',
- timingFunction: 'ease',
- duration,
- },
- {
- // #ifdef APP-NVUE
- dom: this.$refs.overlayPartRight,
- // #endif
- // #ifndef APP-NVUE
- dom: 'overlayPartRight',
- // #endif
- styles: {
- transform: `translateX(1000px)`,
- },
- transformOrigin: 'center center',
- timingFunction: 'ease',
- duration,
- }
- ]
- await this.animationActive(actives)
- },
- async openActives() {
- const duration = 800;
- const actives = [{
- // #ifdef APP-NVUE
- dom: this.$refs.overlayPartTop,
- // #endif
- // #ifndef APP-NVUE
- dom: 'overlayPartTop',
- // #endif
- styles: {
- transform: `translateY(0px)`,
- },
- transformOrigin: 'center center',
- timingFunction: 'ease',
- duration,
- },
- {
- // #ifdef APP-NVUE
- dom: this.$refs.overlayPartBottom,
- // #endif
- // #ifndef APP-NVUE
- dom: 'overlayPartBottom',
- // #endif
- styles: {
- transform: `translateY(0px)`,
- },
- transformOrigin: 'center center',
- timingFunction: 'ease',
- duration,
- },
- {
- // #ifdef APP-NVUE
- dom: this.$refs.overlayPartLeft,
- // #endif
- // #ifndef APP-NVUE
- dom: 'overlayPartLeft',
- // #endif
- styles: {
- transform: `translateX(0px)`,
- },
- transformOrigin: 'center center',
- timingFunction: 'ease',
- duration,
- },
- {
- // #ifdef APP-NVUE
- dom: this.$refs.overlayPartRight,
- // #endif
- // #ifndef APP-NVUE
- dom: 'overlayPartRight',
- // #endif
- styles: {
- transform: `translateX(0px)`,
- },
- transformOrigin: 'center center',
- timingFunction: 'ease',
- duration,
- }
- ]
- await this.animationActive(actives)
- },
- async viewTips() {
- if (this.hole) {
- // 如果dom宽度大于或者等于窗口宽度,需要重新调整dom展示宽度
- const index = this.index;
- // 步骤条展示的高度需要加上屏幕滚动的高度
- // 设置提示框高度
- let tipTop = this.hole.top + this.hole.height + 5; // 调小高度,避免位置太低
- // 如果dom在屏幕底部的话,重新调整提示框和三角形的定位
- let newHeight = this.screenHeight - this.hole.bottom;
- let arrowTop = -5;
- let arrowLeft = 5;
- // #ifdef APP-NVUE
- const rect = await this.getComponentRect(this.$refs.tips)
- // #endif
- // #ifndef APP-NVUE
- const rect = await this.getComponentRect('.tips')
- // #endif
- if (!rect) return;
- for (let key in rect) {
- rect[key] = rect[key] = Math.round(rect[key])
- }
- arrowLeft = (rect.width - 10) / 2;
- let holeLeft = this.hole.width / 2 + this.hole.left
- let tipLeft = holeLeft - rect.width / 2;
- // 计算 下面高度小于tips的高度 tips应该显示在上面(默认显示在下面)
- if ((this.hole.top + this.hole.height + rect.height) > this.screenHeight) {
- tipTop = this.hole.top - (rect.height + 15);
- arrowTop = rect.height - 5;
- }
- // 计算tips在遮罩层里的左右位置
- if (tipLeft < 0) {
- tipLeft = this.hole.left;
- // 计算三角形在tips里的左右位置
- arrowLeft = (this.hole.width - 10) / 2
- } else if ((tipLeft + rect.width) > this.screenWidth) {
- tipLeft = this.hole.left - (rect.width - this.hole.width);
- // 计算三角形在tips里的左右位置
- let s = (rect.width - this.hole.width);
- arrowLeft = ((rect.width - s - 10) / 2) + s;
- }
- // 设置提示框定位
- this.arrowPosition = `top:${arrowTop}px;left:${arrowLeft}px;`;
- // #ifndef APP-NVUE
- this.tipPosition = {
- top: `${tipTop + 5}px`,
- left: `${tipLeft}px`,
- };
- // #endif
- // #ifdef APP-NVUE
- await this.animationActive([{
- dom: this.$refs.tips,
- styles: {
- transform: `translate(${tipLeft}px,${tipTop + 5}px)`,
- opacity: 1
- },
- duration: 500,
- timingFunction: 'ease',
- transformOrigin: 'center center',
- needLayout: true
- }])
- // #endif
- this.isStop = false;
- }
- },
- scrollToElement(el, top) {
- return new Promise((resolve, reject) => {
- if (!el) {
- reject('el is not defind')
- return;
- }
- // #ifdef APP-NVUE
- if (dom) {
- dom.scrollToElement(el, {
- offset: this.screenHeight / 2,
- animated: true
- })
- setTimeout(() => {
- resolve()
- }, 500)
- } else {
- reject('dom module is not defind')
- }
- // #endif
- // #ifndef APP-NVUE
- const fn = () => {
- setTimeout(() => {
- resolve()
- }, 500)
- }
- this.$emit('scrollToElement', {
- top,
- fn
- })
- // #endif
- })
- },
- getComponentRect(el) {
- return new Promise((resolve, reject) => {
- if (!el) {
- reject('el is not defind')
- return;
- }
- // #ifdef APP-NVUE
- if (dom) {
- dom.getComponentRect(el, (option) => {
- if (option.result) {
- resolve(option.size)
- } else {
- console.log('获取元素Rect失败', option);
- reject(option.size)
- }
- });
- } else {
- reject('dom module is not defind')
- }
- // #endif
- // #ifndef APP-NVUE
- try {
- console.log('元神启动3', el);
- uni.createSelectorQuery().in(this.$root)
- .select(el).boundingClientRect(option => {
- console.log('元神启动4', option);
- resolve(option)
- }).exec();
- } catch (e) {
- //TODO handle the exception
- reject(e)
- }
- // #endif
- })
- },
- }
- };
- </script>
- <style lang="scss" scoped>
- .overlay {
- position: fixed;
- top: 0;
- left: 0;
- z-index: 1000;
- .overlay-part {
- background-color: rgba(33, 33, 33, 0.5);
- }
- .middle-row {
- display: flex;
- flex-direction: row;
- }
- .hole {
- background-color: transparent;
- }
- .tips {
- width: 400rpx;
- box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.1);
- position: absolute;
- top: 0rpx;
- left: 0rpx;
- padding: 15rpx 20rpx;
- border-radius: 12rpx;
- background: linear-gradient(180deg, #1cbbb4, #0081ff);
- /* #ifndef APP-NVUE */
- transition: all ease 0.2s;
- /* #endif */
- z-index: 1000;
- .text {
- font-size: 28rpx;
- color: #fff;
- }
- .tool-btn {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- padding-right: 0rpx;
- margin-top: 20rpx;
- .text {
- color: #666;
- line-height: 48rpx;
- font-size: 24rpx;
- text-align: center;
- }
- .next {
- background: #fff;
- height: 48rpx;
- width: 100rpx;
- border-radius: 8rpx;
- .text {
- color: #666;
- line-height: 48rpx;
- font-size: 24rpx;
- text-align: center;
- }
- }
- }
- .arrow {
- height: 20rpx;
- width: 20rpx;
- background: #1cbbb4;
- position: absolute;
- top: -10rpx;
- transform: rotate(45deg);
- }
- }
- }
- </style>
|