index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. <template>
  2. <view class="overlay" :style="modelStyle" @touchmove.stop.prevent v-if="showGuide">
  3. <view class="overlay-part" ref="overlayPartTop" :animation="overlayPartTop"
  4. :style="{ height: `${hole.top}px`,transform:`translateY(${-hole.top}px)`}"></view>
  5. <view class="middle-row">
  6. <view class="overlay-part" ref="overlayPartLeft" :animation="overlayPartLeft"
  7. :style="{ width: `${hole.left}px`, height: `${hole.height}px`,transform:`translateX(${-hole.left}px)`}">
  8. </view>
  9. <view class="hole" :style="{ width: `${hole.width}px`, height: `${hole.height}px` }" @click="handler">
  10. </view>
  11. <view class="overlay-part" ref="overlayPartRight" :animation="overlayPartRight"
  12. :style="{ width: `${screenWidth - hole.left - hole.width}px`, height: `${hole.height}px`,transform:`translateX(${screenWidth - hole.left - hole.width}px)`}">
  13. </view>
  14. </view>
  15. <view class="overlay-part" ref="overlayPartBottom" :animation="overlayPartBottom"
  16. :style="{ height: `${screenHeight - hole.top - hole.height}px`,transform:`translateY(${screenHeight - hole.top - hole.height}px)`}">
  17. </view>
  18. <!-- tips提示框 -->
  19. <view class="tips" ref="tips" :style="{opacity:isTips?1:0,...tipPosition}">
  20. <text class="text">{{ guideInfo.tips }}</text>
  21. <view class="tool-btn">
  22. <text @click="skip" class="text">跳过</text>
  23. <view class="next" style="" @click="next">
  24. <text class="text">{{ guideInfo.next }}</text>
  25. </view>
  26. </view>
  27. <view class="arrow" :style="arrowPosition"></view>
  28. </view>
  29. </view>
  30. </template>
  31. <script>
  32. // #ifdef APP-NVUE
  33. const dom = weex.requireModule("dom");
  34. const animationModule = weex.requireModule('animation')
  35. // #endif
  36. export default {
  37. data() {
  38. return {
  39. stepName: 'step', //该提示步骤的名称,用于不在重复展示
  40. isTips: false,
  41. isStop: false,
  42. guideList: [{
  43. el: '',
  44. tips: '',
  45. next: '',
  46. }],
  47. overlayPartTop: {},
  48. overlayPartLeft: {},
  49. overlayPartRight: {},
  50. overlayPartBottom: {},
  51. tipsAction: {},
  52. index: 0, // 当前展示的索引
  53. showGuide: false, // 是否显示引导
  54. tipPosition: {},
  55. arrowPosition: '',
  56. // 屏幕宽高
  57. screenWidth: 0,
  58. screenHeight: 0,
  59. // 洞的动态位置和尺寸
  60. hole: {
  61. top: 0, // 距顶部距离
  62. left: 0, // 距左侧距离
  63. width: 0, // 洞的宽度
  64. height: 0, // 洞的高度
  65. }
  66. };
  67. },
  68. computed: {
  69. guideInfo() {
  70. return this.guideList[this.index] || {};
  71. },
  72. modelStyle() {
  73. const style = {
  74. width: this.screenWidth + 'px',
  75. height: this.screenHeight + 'px',
  76. }
  77. return style;
  78. },
  79. },
  80. mounted() {
  81. const systemInfo = uni.getSystemInfoSync();
  82. const guide = uni.getStorageSync(this.stepName);
  83. this.$nextTick(() => {
  84. this.screenWidth = systemInfo.windowWidth;
  85. this.screenHeight = systemInfo.windowHeight;
  86. })
  87. },
  88. methods: {
  89. handler() {
  90. this.$emit('clickChunk', this.index)
  91. },
  92. async skip() {
  93. this.isTips = false;
  94. await this.closeActives()
  95. this.showGuide = false;
  96. },
  97. async next() {
  98. if (!this.guideList[this.index] || this.isStop) return;
  99. this.isTips = false;
  100. this.index++
  101. this.getDomInfo()
  102. },
  103. start(step) {
  104. this.guideList = step.guideList;
  105. this.stepName = step.name;
  106. this.showGuide = true;
  107. this.getDomInfo();
  108. },
  109. checkEl(el) {
  110. return new Promise((resolve) => {
  111. this.getComponentRect(el)
  112. .then(rect => {
  113. if ((rect.top + rect.height + 100) > this.screenHeight) {
  114. this.scrollToElement(el, rect.top).then(() => {
  115. resolve()
  116. })
  117. } else {
  118. resolve()
  119. }
  120. })
  121. .catch(() => {
  122. resolve()
  123. })
  124. })
  125. },
  126. getDomInfo() {
  127. this.$nextTick(async () => {
  128. const {
  129. el
  130. } = this.guideInfo;
  131. this.isStop = true;
  132. await this.closeActives()
  133. await this.checkEl(el)
  134. const rect = await this.getComponentRect(el)
  135. if (!rect) return;
  136. for (let key in rect) {
  137. this.hole[key] = rect[key] = Math.round(rect[key]);
  138. }
  139. await this.openActives()
  140. this.isTips = true;
  141. this.$nextTick(() => {
  142. this.viewTips()
  143. })
  144. })
  145. },
  146. animationActive(actives) {
  147. // console.log(actives);
  148. return new Promise((resolve) => {
  149. let count = 0;
  150. // #ifdef APP-NVUE
  151. for (let active of actives) {
  152. animationModule.transition(active.dom, {
  153. styles: active.styles || {},
  154. duration: active.duration,
  155. timingFunction: active.timingFunction,
  156. delay: active.delay || 0,
  157. transformOrigin: active.transformOrigin,
  158. }, () => {
  159. count++
  160. if (count >= actives.length) {
  161. resolve()
  162. }
  163. })
  164. }
  165. // #endif
  166. // #ifndef APP-NVUE
  167. for (let active of actives) {
  168. let animation = uni.createAnimation({
  169. duration: active.duration,
  170. delay: active.delay || 0,
  171. timingFunction: active.timingFunction,
  172. transformOrigin: active.transformOrigin,
  173. })
  174. let res = this.extractTransformValue(active.styles.transform)
  175. let args = res.value.split(',')
  176. animation[res.key](...args).step()
  177. this[active.dom] = animation.export()
  178. }
  179. setTimeout(() => {
  180. resolve()
  181. }, 500)
  182. // #endif
  183. })
  184. },
  185. extractTransformValue(transformString) {
  186. // 正则表达式:匹配 translateY(或 translateX( 等形式
  187. const regex = /([a-zA-Z]+)\(([^)]+)\)/;
  188. const match = transformString.match(regex);
  189. if (match) {
  190. // match[1] 是 key(如 translateY),match[2] 是括号内的内容(如 1000px)
  191. let value = match[2];
  192. return {
  193. key: match[1], // 提取关键字(如 translateY)
  194. value: value // 转换为数字(例如 1000)
  195. };
  196. }
  197. },
  198. async closeActives() {
  199. const duration = 500;
  200. const actives = [{
  201. // #ifdef APP-NVUE
  202. dom: this.$refs.overlayPartTop,
  203. // #endif
  204. // #ifndef APP-NVUE
  205. dom: 'overlayPartTop',
  206. // #endif
  207. styles: {
  208. transform: `translateY(-1000px)`,
  209. },
  210. transformOrigin: 'center center',
  211. timingFunction: 'ease',
  212. duration,
  213. },
  214. {
  215. // #ifdef APP-NVUE
  216. dom: this.$refs.overlayPartBottom,
  217. // #endif
  218. // #ifndef APP-NVUE
  219. dom: 'overlayPartBottom',
  220. // #endif
  221. styles: {
  222. transform: `translateY(1000px)`,
  223. },
  224. transformOrigin: 'center center',
  225. timingFunction: 'ease',
  226. duration,
  227. },
  228. {
  229. // #ifdef APP-NVUE
  230. dom: this.$refs.overlayPartLeft,
  231. // #endif
  232. // #ifndef APP-NVUE
  233. dom: 'overlayPartLeft',
  234. // #endif
  235. styles: {
  236. transform: `translateX(-1000px)`,
  237. },
  238. transformOrigin: 'center center',
  239. timingFunction: 'ease',
  240. duration,
  241. },
  242. {
  243. // #ifdef APP-NVUE
  244. dom: this.$refs.overlayPartRight,
  245. // #endif
  246. // #ifndef APP-NVUE
  247. dom: 'overlayPartRight',
  248. // #endif
  249. styles: {
  250. transform: `translateX(1000px)`,
  251. },
  252. transformOrigin: 'center center',
  253. timingFunction: 'ease',
  254. duration,
  255. }
  256. ]
  257. await this.animationActive(actives)
  258. },
  259. async openActives() {
  260. const duration = 800;
  261. const actives = [{
  262. // #ifdef APP-NVUE
  263. dom: this.$refs.overlayPartTop,
  264. // #endif
  265. // #ifndef APP-NVUE
  266. dom: 'overlayPartTop',
  267. // #endif
  268. styles: {
  269. transform: `translateY(0px)`,
  270. },
  271. transformOrigin: 'center center',
  272. timingFunction: 'ease',
  273. duration,
  274. },
  275. {
  276. // #ifdef APP-NVUE
  277. dom: this.$refs.overlayPartBottom,
  278. // #endif
  279. // #ifndef APP-NVUE
  280. dom: 'overlayPartBottom',
  281. // #endif
  282. styles: {
  283. transform: `translateY(0px)`,
  284. },
  285. transformOrigin: 'center center',
  286. timingFunction: 'ease',
  287. duration,
  288. },
  289. {
  290. // #ifdef APP-NVUE
  291. dom: this.$refs.overlayPartLeft,
  292. // #endif
  293. // #ifndef APP-NVUE
  294. dom: 'overlayPartLeft',
  295. // #endif
  296. styles: {
  297. transform: `translateX(0px)`,
  298. },
  299. transformOrigin: 'center center',
  300. timingFunction: 'ease',
  301. duration,
  302. },
  303. {
  304. // #ifdef APP-NVUE
  305. dom: this.$refs.overlayPartRight,
  306. // #endif
  307. // #ifndef APP-NVUE
  308. dom: 'overlayPartRight',
  309. // #endif
  310. styles: {
  311. transform: `translateX(0px)`,
  312. },
  313. transformOrigin: 'center center',
  314. timingFunction: 'ease',
  315. duration,
  316. }
  317. ]
  318. await this.animationActive(actives)
  319. },
  320. async viewTips() {
  321. if (this.hole) {
  322. // 如果dom宽度大于或者等于窗口宽度,需要重新调整dom展示宽度
  323. const index = this.index;
  324. // 步骤条展示的高度需要加上屏幕滚动的高度
  325. // 设置提示框高度
  326. let tipTop = this.hole.top + this.hole.height + 5; // 调小高度,避免位置太低
  327. // 如果dom在屏幕底部的话,重新调整提示框和三角形的定位
  328. let newHeight = this.screenHeight - this.hole.bottom;
  329. let arrowTop = -5;
  330. let arrowLeft = 5;
  331. // #ifdef APP-NVUE
  332. const rect = await this.getComponentRect(this.$refs.tips)
  333. // #endif
  334. // #ifndef APP-NVUE
  335. const rect = await this.getComponentRect('.tips')
  336. // #endif
  337. if (!rect) return;
  338. for (let key in rect) {
  339. rect[key] = rect[key] = Math.round(rect[key])
  340. }
  341. arrowLeft = (rect.width - 10) / 2;
  342. let holeLeft = this.hole.width / 2 + this.hole.left
  343. let tipLeft = holeLeft - rect.width / 2;
  344. // 计算 下面高度小于tips的高度 tips应该显示在上面(默认显示在下面)
  345. if ((this.hole.top + this.hole.height + rect.height) > this.screenHeight) {
  346. tipTop = this.hole.top - (rect.height + 15);
  347. arrowTop = rect.height - 5;
  348. }
  349. // 计算tips在遮罩层里的左右位置
  350. if (tipLeft < 0) {
  351. tipLeft = this.hole.left;
  352. // 计算三角形在tips里的左右位置
  353. arrowLeft = (this.hole.width - 10) / 2
  354. } else if ((tipLeft + rect.width) > this.screenWidth) {
  355. tipLeft = this.hole.left - (rect.width - this.hole.width);
  356. // 计算三角形在tips里的左右位置
  357. let s = (rect.width - this.hole.width);
  358. arrowLeft = ((rect.width - s - 10) / 2) + s;
  359. }
  360. // 设置提示框定位
  361. this.arrowPosition = `top:${arrowTop}px;left:${arrowLeft}px;`;
  362. // #ifndef APP-NVUE
  363. this.tipPosition = {
  364. top: `${tipTop + 5}px`,
  365. left: `${tipLeft}px`,
  366. };
  367. // #endif
  368. // #ifdef APP-NVUE
  369. await this.animationActive([{
  370. dom: this.$refs.tips,
  371. styles: {
  372. transform: `translate(${tipLeft}px,${tipTop + 5}px)`,
  373. opacity: 1
  374. },
  375. duration: 500,
  376. timingFunction: 'ease',
  377. transformOrigin: 'center center',
  378. needLayout: true
  379. }])
  380. // #endif
  381. this.isStop = false;
  382. }
  383. },
  384. scrollToElement(el, top) {
  385. return new Promise((resolve, reject) => {
  386. if (!el) {
  387. reject('el is not defind')
  388. return;
  389. }
  390. // #ifdef APP-NVUE
  391. if (dom) {
  392. dom.scrollToElement(el, {
  393. offset: this.screenHeight / 2,
  394. animated: true
  395. })
  396. setTimeout(() => {
  397. resolve()
  398. }, 500)
  399. } else {
  400. reject('dom module is not defind')
  401. }
  402. // #endif
  403. // #ifndef APP-NVUE
  404. const fn = () => {
  405. setTimeout(() => {
  406. resolve()
  407. }, 500)
  408. }
  409. this.$emit('scrollToElement', {
  410. top,
  411. fn
  412. })
  413. // #endif
  414. })
  415. },
  416. getComponentRect(el) {
  417. return new Promise((resolve, reject) => {
  418. if (!el) {
  419. reject('el is not defind')
  420. return;
  421. }
  422. // #ifdef APP-NVUE
  423. if (dom) {
  424. dom.getComponentRect(el, (option) => {
  425. if (option.result) {
  426. resolve(option.size)
  427. } else {
  428. console.log('获取元素Rect失败', option);
  429. reject(option.size)
  430. }
  431. });
  432. } else {
  433. reject('dom module is not defind')
  434. }
  435. // #endif
  436. // #ifndef APP-NVUE
  437. try {
  438. console.log('元神启动3', el);
  439. uni.createSelectorQuery().in(this.$root)
  440. .select(el).boundingClientRect(option => {
  441. console.log('元神启动4', option);
  442. resolve(option)
  443. }).exec();
  444. } catch (e) {
  445. //TODO handle the exception
  446. reject(e)
  447. }
  448. // #endif
  449. })
  450. },
  451. }
  452. };
  453. </script>
  454. <style lang="scss" scoped>
  455. .overlay {
  456. position: fixed;
  457. top: 0;
  458. left: 0;
  459. z-index: 1000;
  460. .overlay-part {
  461. background-color: rgba(33, 33, 33, 0.5);
  462. }
  463. .middle-row {
  464. display: flex;
  465. flex-direction: row;
  466. }
  467. .hole {
  468. background-color: transparent;
  469. }
  470. .tips {
  471. width: 400rpx;
  472. box-shadow: 0px 2px 9px 0px rgba(0, 0, 0, 0.1);
  473. position: absolute;
  474. top: 0rpx;
  475. left: 0rpx;
  476. padding: 15rpx 20rpx;
  477. border-radius: 12rpx;
  478. background: linear-gradient(180deg, #1cbbb4, #0081ff);
  479. /* #ifndef APP-NVUE */
  480. transition: all ease 0.2s;
  481. /* #endif */
  482. z-index: 1000;
  483. .text {
  484. font-size: 28rpx;
  485. color: #fff;
  486. }
  487. .tool-btn {
  488. display: flex;
  489. flex-direction: row;
  490. justify-content: space-between;
  491. align-items: center;
  492. padding-right: 0rpx;
  493. margin-top: 20rpx;
  494. .text {
  495. color: #666;
  496. line-height: 48rpx;
  497. font-size: 24rpx;
  498. text-align: center;
  499. }
  500. .next {
  501. background: #fff;
  502. height: 48rpx;
  503. width: 100rpx;
  504. border-radius: 8rpx;
  505. .text {
  506. color: #666;
  507. line-height: 48rpx;
  508. font-size: 24rpx;
  509. text-align: center;
  510. }
  511. }
  512. }
  513. .arrow {
  514. height: 20rpx;
  515. width: 20rpx;
  516. background: #1cbbb4;
  517. position: absolute;
  518. top: -10rpx;
  519. transform: rotate(45deg);
  520. }
  521. }
  522. }
  523. </style>