Browse Source

feat: tour小程序适配

yangxiaolu3 3 years ago
parent
commit
ac81c587b3

+ 1 - 1
src/config.json

@@ -1197,7 +1197,7 @@
           "version": "3.0.0",
           "name": "Tour",
           "cType": "展示组件",
-          "cName": "引导组件",
+          "cName": "引导",
           "desc": "漫游式引导组件",
           "show": true,
           "tarodoc": false,

+ 82 - 0
src/packages/__VUE/popover/doc.en-US.md

@@ -284,6 +284,85 @@ export default {
 :::
 
 
+### custom target 
+
+Popover 提供了 `targetId` 属性,用于匹配目标元素,在目标元素上添加对应的 id 值即可
+
+:::demo
+```html
+<template>
+  <nut-button type="primary" shape="square" id="popid" @click="clickCustomHandle">custom target</nut-button>
+    <nut-popover v-model:visible="customTarget" targetId="popid" :list="itemList" location="top-start"></nut-popover>
+</template>
+
+<script>
+import { reactive, ref } from 'vue';
+export default {
+  setup() {
+    const visible = ref({
+      customTarget:false
+    });
+
+    const itemList = reactive([
+      {name: 'option1'},
+      {name: 'option2'},
+      {name: 'option3'}
+    ]);
+
+    const clickCustomHandle = () => {
+      visible.customTarget = !visible.customTarget;
+    };
+
+    return {
+        itemList,
+        visible,
+        clickCustomHandle,
+      };
+    }
+}
+</script>
+
+
+```
+:::
+
+### Custom Color
+
+:::demo
+```html
+<template>
+  <nut-popover v-model:visible="customColor" :list="itemList" location="right-start" bgColor="#f00" theme="dark">
+      <template #reference>
+        <nut-button type="primary" shape="square" >Custom Color</nut-button>
+      </template>
+    </nut-popover>
+</template>
+
+<script>
+import { reactive, ref } from 'vue';
+export default {
+  setup() {
+    const visible = ref({
+      customColor:false
+    });
+
+    const itemList = reactive([
+      {name: 'option1'},
+      {name: 'option2'},
+      {name: 'option3'}
+    ]);
+
+    return {
+        itemList,
+        visible
+      };
+    }
+}
+</script>
+
+```
+:::
+
 ## API
 ### Props  
 
@@ -304,6 +383,9 @@ export default {
 | close-on-click-overlay `v3.2.8`       | Whether to close when clicking overlay  | boolean  | true  |
 | close-on-click-action `v3.2.8`       | Whether to close when clicking action  | boolean  | true |
 | close-on-click-outside `v3.2.8`       | Whether to close when clicking outside | boolean  | true  |
+| bg-color `v3.3.1`       | Custom color | String  | -  |
+| target-id `v3.3.1`       | Custom target id | String  | -  |
+| arrow-offset `v3.3.1`       | the offset of the arrow | Number  | 0  |
 
 ### List data structure  
 

+ 83 - 0
src/packages/__VUE/popover/doc.md

@@ -282,6 +282,86 @@ export default {
 ```
 :::
 
+### 自定义目标元素
+
+Popover 提供了 `targetId` 属性,用于匹配目标元素,在目标元素上添加对应的 id 值即可
+
+:::demo
+```html
+<template>
+  <nut-button type="primary" shape="square" id="popid" @click="clickCustomHandle">自定义目标元素</nut-button>
+    <nut-popover v-model:visible="customTarget" targetId="popid" :list="itemList" location="top-start"></nut-popover>
+</template>
+
+<script>
+import { reactive, ref } from 'vue';
+export default {
+  setup() {
+    const visible = ref({
+      customTarget:false
+    });
+
+    const itemList = reactive([
+      {name: 'option1'},
+      {name: 'option2'},
+      {name: 'option3'}
+    ]);
+
+    const clickCustomHandle = () => {
+      visible.customTarget = !visible.customTarget;
+    };
+
+    return {
+        itemList,
+        visible,
+        clickCustomHandle,
+      };
+    }
+}
+</script>
+
+
+```
+:::
+
+### 自定义颜色
+
+Popopver 提供了 2 种主题色,同样可以通过 `bgColor` 属性改变背景色
+
+:::demo
+```html
+<template>
+  <nut-popover v-model:visible="customColor" :list="itemList" location="right-start" bgColor="#f00" theme="dark">
+      <template #reference>
+        <nut-button type="primary" shape="square" >自定义颜色</nut-button>
+      </template>
+    </nut-popover>
+</template>
+
+<script>
+import { reactive, ref } from 'vue';
+export default {
+  setup() {
+    const visible = ref({
+      customColor:false
+    });
+
+    const itemList = reactive([
+      {name: 'option1'},
+      {name: 'option2'},
+      {name: 'option3'}
+    ]);
+
+    return {
+        itemList,
+        visible
+      };
+    }
+}
+</script>
+
+```
+:::
 
 ## API
 ### Props  
@@ -303,6 +383,9 @@ export default {
 | close-on-click-overlay `v3.2.8`       | 是否在点击遮罩层后关闭菜单  | boolean  | true  |
 | close-on-click-action `v3.2.8`       | 是否在点击选项后关闭  | boolean  | true |
 | close-on-click-outside `v3.2.8`       | 是否在点击外部元素后关闭菜单 | boolean  | true  |
+| bg-color `v3.3.1`       | 自定义背景色 | String  | -  |
+| target-id `v3.3.1`       | 自定义目标元素 id | String  | -  |
+| arrow-offset `v3.3.1`       | 小箭头的偏移量 | Number  | 0  |
 
 ### List 数据结构  
 

+ 6 - 0
src/packages/__VUE/popover/index.scss

@@ -252,3 +252,9 @@
 .nut-popover-wrapper {
   display: inline-block;
 }
+
+// Taro
+.nut-popover-content-copy {
+  position: absolute;
+  top: 99999px;
+}

+ 177 - 46
src/packages/__VUE/popover/index.taro.vue

@@ -1,12 +1,16 @@
 <template>
-  <view :class="['nut-popover', `nut-popover--${theme}`, `${customClass}`]">
-    <view class="nut-popover-wrapper" @click="openPopover" ref="popoverRef" :id="'popoverRef' + refRandomId"
-      ><slot name="reference"></slot
-    ></view>
-
+  <view
+    class="nut-popover-wrapper"
+    @click="openPopover"
+    v-if="!targetId"
+    ref="popoverRef"
+    :id="'popoverRef' + refRandomId"
+    ><slot name="reference"></slot
+  ></view>
+  <view :class="['nut-popover', `nut-popover--${theme}`, `${customClass}`]" :style="getRootPosition">
     <nut-popup
       :popClass="`nut-popover-content nut-popover-content--${location}`"
-      :style="getStyles"
+      :style="customStyle"
       v-model:visible="showPopup"
       position=""
       transition="nut-popover"
@@ -17,7 +21,7 @@
       :closeOnClickOverlay="closeOnClickOverlay"
     >
       <view ref="popoverContentRef" :id="'popoverContentRef' + refRandomId" class="nut-popover-content-group">
-        <view :class="popoverArrow" v-if="showArrow"> </view>
+        <view :class="popoverArrow" v-if="showArrow" :style="popoverArrowStyle"> </view>
         <slot name="content"></slot>
         <view
           v-for="(item, index) in list"
@@ -45,27 +49,47 @@
 
     <view class="nut-popover-content-bg" v-if="showPopup" @touchmove="clickAway" @click="clickAway"></view>
   </view>
+
+  <div :class="`nut-popover-content nut-popover-content-copy`">
+    <view ref="popoverContentRefCopy" :id="'popoverContentRefCopy' + refRandomId" class="nut-popover-content-group">
+      <slot name="content"></slot>
+      <view
+        v-for="(item, index) in list"
+        :key="index"
+        :class="[
+          item.className,
+          item.disabled && 'nut-popover-menu-disabled',
+          'nut-popover-menu-item',
+          'nut-popover-menu-taroitem'
+        ]"
+      >
+        <slot v-if="item.icon">
+          <nut-icon v-bind="$attrs" class="nut-popover-item-img" :classPrefix="iconPrefix" :name="item.icon"></nut-icon
+        ></slot>
+        <view class="nut-popover-menu-item-name">{{ item.name }}</view>
+      </view>
+    </view>
+  </div>
 </template>
 <script lang="ts">
 import { onMounted, computed, watch, ref, PropType, toRefs, reactive, CSSProperties } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 const { componentName, create } = createComponent('popover');
 import { useTaroRect } from '@/packages/utils/useTaroRect';
+import { useRect, rect } from '@/packages/utils/useRect';
 import { isArray } from '@/packages/utils/util';
-import Popup from '../popup/index.vue';
 import Taro from '@tarojs/taro';
 
 export default create({
   inheritAttrs: false,
-  components: {
-    [Popup.name]: Popup
-  },
+  components: {},
   props: {
     visible: { type: Boolean, default: false },
-    list: { type: Array as PropType<import('./type').PopoverList[]>, default: [] },
+    list: { type: Array, default: [] },
     theme: { type: String as PropType<import('./type').PopoverTheme>, default: 'light' },
     location: { type: String as PropType<import('./type').PopoverLocation>, default: 'bottom' },
     offset: { type: Array, default: [0, 12] },
+    arrowOffset: { type: Number, default: 0 },
     customClass: { type: String, default: '' },
     showArrow: { type: Boolean, default: true },
     iconPrefix: { type: String, default: 'nut-icon' },
@@ -75,17 +99,23 @@ export default create({
     overlayStyle: { type: Object as PropType<CSSProperties> },
     closeOnClickOverlay: { type: Boolean, default: true },
     closeOnClickAction: { type: Boolean, default: true },
-    closeOnClickOutside: { type: Boolean, default: true }
+    closeOnClickOutside: { type: Boolean, default: true },
+    targetId: { type: String, default: '' },
+    bgColor: { type: String, default: '' }
   },
   emits: ['update', 'update:visible', 'close', 'choose', 'open'],
   setup(props, { emit }) {
     const popoverRef = ref();
     const popoverContentRef = ref();
+    const popoverContentRefCopy = ref();
     const showPopup = ref(props.visible);
-    const state = reactive({
-      rootWidth: 0,
-      rootHeight: 0
-    });
+
+    let rootRect = ref<rect>();
+
+    let conentRootRect = ref<{
+      height: number;
+      width: number;
+    }>();
 
     const popoverArrow = computed(() => {
       const prefixCls = 'nut-popover-arrow';
@@ -93,37 +123,130 @@ export default create({
       const direction = loca.split('-')[0];
       return `${prefixCls} ${prefixCls}-${direction} ${prefixCls}--${loca}`;
     });
-    const getStyles = computed(() => {
-      let cross = +state.rootHeight;
-      let lengthways = +state.rootWidth;
-      let { offset, location } = props;
+    const popoverArrowStyle = computed(() => {
+      const styles: CSSProperties = {};
+      const { bgColor, arrowOffset, location } = props;
+      const direction = location.split('-')[0];
+      const skew = location.split('-')[1];
+      const base = 16;
+
+      if (bgColor) {
+        styles[`border${upperCaseFirst(direction)}Color`] = bgColor;
+      }
+
+      if (props.arrowOffset != 0) {
+        if (['bottom', 'top'].includes(direction)) {
+          if (!skew) {
+            styles.left = `calc(50% + ${arrowOffset}px)`;
+          }
+          if (skew == 'start') {
+            styles.left = `${base + arrowOffset}px`;
+          }
+          if (skew == 'end') {
+            styles.right = `${base - arrowOffset}px`;
+          }
+        }
+
+        if (['left', 'right'].includes(direction)) {
+          if (!skew) {
+            styles.top = `calc(50% - ${arrowOffset}px)`;
+          }
+          if (skew == 'start') {
+            styles.top = `${base - arrowOffset}px`;
+          }
+          if (skew == 'end') {
+            styles.bottom = `${base + arrowOffset}px`;
+          }
+        }
+      }
+      return styles;
+    });
+
+    const upperCaseFirst = (str: string) => {
+      var str = str.toLowerCase();
+      str = str.replace(/\b\w+\b/g, (word) => word.substring(0, 1).toUpperCase() + word.substring(1));
+      return str;
+    };
+
+    const getRootPosition = computed(() => {
+      let styles: CSSProperties = {};
+
+      if (!rootRect.value || !conentRootRect.value) return {};
+
+      const conentWidth = conentRootRect.value.width;
+      const conentHeight = conentRootRect.value.height;
+
+      const { width, height, left, top } = rootRect.value;
+
+      const { location, offset } = props;
+      const direction = location.split('-')[0];
+      const skew = location.split('-')[1];
+      let cross = 0;
+      let parallel = 0;
       if (isArray(offset) && offset.length == 2) {
         cross += +offset[1];
-        lengthways += +offset[1];
+        parallel += +offset[0];
       }
-      const direction = location.split('-')[0];
-      const style: CSSProperties = {};
-      const mapd: any = {
-        top: 'bottom',
-        bottom: 'top',
-        left: 'right',
-        right: 'left'
-      };
-      if (['top', 'bottom'].includes(direction)) {
-        style[mapd[direction]] = `${cross}px`;
-        style.marginLeft = `${offset[0]}px`;
-      } else {
-        style[mapd[direction]] = `${lengthways}px`;
-        style.marginTop = `${offset[0]}px`;
+      if (width) {
+        if (['bottom', 'top'].includes(direction)) {
+          const h = direction == 'bottom' ? height + cross : -(conentHeight + cross);
+
+          styles.top = `${top + h}px`;
+
+          if (!skew) {
+            styles.left = `${-(conentWidth - width) / 2 + left + parallel}px`;
+          }
+          if (skew == 'start') {
+            styles.left = `${left + parallel}px`;
+          }
+          if (skew == 'end') {
+            styles.left = `${rootRect.value.right + parallel}px`;
+          }
+        }
+        if (['left', 'right'].includes(direction)) {
+          const contentW = direction == 'left' ? -(conentWidth + cross) : width + cross;
+          styles.left = `${left + contentW}px`;
+          if (!skew) {
+            styles.top = `${top - conentHeight / 2 + height / 2 - 4 + parallel}px`;
+          }
+          if (skew == 'start') {
+            styles.top = `${top + parallel}px`;
+          }
+          if (skew == 'end') {
+            styles.top = `${top + height + parallel}px`;
+          }
+        }
       }
-      return style;
+
+      return styles;
+    });
+
+    const customStyle = computed(() => {
+      const styles: CSSProperties = {};
+      if (props.bgColor) {
+        styles.background = props.bgColor;
+      }
+
+      return styles;
     });
     // 获取宽度
     const getContentWidth = async () => {
-      const refe = await useTaroRect(popoverRef, Taro);
-      const { height, width } = refe;
-      state.rootHeight = height;
-      state.rootWidth = width;
+      let rect;
+      if (props.targetId) {
+        rect = await useTaroRect(props.targetId, Taro);
+      } else {
+        rect = await useTaroRect(popoverRef, Taro);
+      }
+      rootRect.value = rect;
+    };
+
+    const getPopoverContentW = async (type: number = 1) => {
+      const el = type == 1 ? popoverContentRef : popoverContentRefCopy;
+      let rectContent = await useTaroRect(el, Taro);
+      conentRootRect.value = {
+        height: rectContent.height,
+        width: rectContent.width
+      };
     };
     watch(
       () => props.visible,
@@ -132,7 +255,11 @@ export default create({
         if (value) {
           setTimeout(() => {
             getContentWidth();
-          }, 200);
+          }, 100);
+
+          setTimeout(() => {
+            getPopoverContentW();
+          }, 300);
         }
       }
     );
@@ -154,13 +281,14 @@ export default create({
         closePopover();
       }
     };
-    const clickAway = (event: Event) => {
-      closePopover();
+    const clickAway = () => {
+      props.closeOnClickOutside && closePopover();
     };
 
     onMounted(() => {
       setTimeout(() => {
         getContentWidth();
+        getPopoverContentW(0);
       }, 200);
     });
 
@@ -173,10 +301,13 @@ export default create({
       closePopover,
       chooseItem,
       popoverRef,
-      getStyles,
       popoverContentRef,
+      popoverContentRefCopy,
       refRandomId,
-      clickAway
+      clickAway,
+      popoverArrowStyle,
+      customStyle,
+      getRootPosition
     };
   }
 });

+ 28 - 34
src/packages/__VUE/tour/demo.vue

@@ -2,7 +2,7 @@
   <div class="demo">
     <h2>{{ translate('title') }}</h2>
 
-    <nut-cell title="点击试试" @click="showTour3 = true">
+    <nut-cell :title="translate('tryClick')" @click="showTour3 = true">
       <template v-slot:link>
         <nut-switch id="target7" />
       </template>
@@ -16,9 +16,9 @@
       location="bottom-end"
     ></nut-tour>
 
-    <h2>自定义样式</h2>
+    <h2>{{ translate('title1') }}</h2>
 
-    <nut-cell title="点击试试" @click="showTourHandle">
+    <nut-cell :title="translate('tryClick')" @click="showTourHandle">
       <template v-slot:link>
         <nut-switch id="target5" />
       </template>
@@ -37,9 +37,9 @@
       maskHeight="50"
     ></nut-tour>
 
-    <h2>设置偏移量</h2>
+    <h2>{{ translate('title2') }}</h2>
 
-    <nut-cell title="点击试试" @click="showTour2 = true">
+    <nut-cell :title="translate('tryClick')" @click="showTour2 = true">
       <template v-slot:link>
         <div class="tour-demo-img">
           <img
@@ -67,9 +67,9 @@
       :offset="[8, 8]"
     ></nut-tour>
 
-    <h2>自定义内容</h2>
+    <h2>{{ translate('title3') }}</h2>
 
-    <nut-cell title="点击试试" @click="showTour4 = true">
+    <nut-cell :title="translate('tryClick')" @click="showTour4 = true">
       <template v-slot:link>
         <nut-switch id="target8" />
       </template>
@@ -92,6 +92,10 @@
       </div>
     </nut-tour>
 
+    <h2>{{ translate('title4') }}</h2>
+
+    <nut-cell :title="translate('tryClick')" @click="showTour = true"></nut-cell>
+
     <nut-tabbar :bottom="true">
       <nut-tabbar-item
         id="target1"
@@ -140,25 +144,19 @@ const initTranslate = () =>
   useTranslate({
     'zh-CN': {
       title: '基础用法',
-      title1: '选项配置',
-      title2: '自定义内容',
-      title3: '位置自定义',
-      light: '明朗风格',
-      dark: '暗黑风格',
-      showIcon: '展示图标',
-      disableAction: '禁用选项',
-      content: '自定义内容'
+      title1: '自定义样式',
+      title2: '设置偏移量',
+      title3: '自定义内容',
+      title4: '步骤引导',
+      tryClick: '点击试试'
     },
     'en-US': {
       title: 'Basic Usage',
-      title1: 'Option Configuration',
-      title2: 'Custom Content',
-      title3: 'Custom Location',
-      light: 'light',
-      dark: 'dark',
-      showIcon: 'show icon',
-      disableAction: 'disabled',
-      content: 'custom content'
+      title1: 'Custom Style',
+      title2: 'Custom Offset',
+      title3: 'Custom Content',
+      title4: 'Steps',
+      tryClick: 'Try Click'
     }
   });
 export default createDemo({
@@ -174,20 +172,20 @@ export default createDemo({
       offset: [-3, -8],
       steps: [
         {
-          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          content: '70+ 高质量组件,覆盖移动端主流场景',
           target: 'target1'
         },
         {
-          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          content: '支持一套代码同时开发多端小程序+H5',
           target: 'target2'
         },
         {
-          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          content: '基于京东APP 10.0 视觉规范',
           target: 'target3',
           location: 'top-end'
         },
         {
-          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          content: '支持定制主题,内置 700+ 个主题变量',
           target: 'target4',
           location: 'top-end'
         }
@@ -195,14 +193,14 @@ export default createDemo({
 
       steps1: [
         {
-          content: '邀请有礼,全新改版,奖励多多哦',
+          content: '70+ 高质量组件,覆盖移动端主流场景',
           target: 'target5'
         }
       ],
 
       steps2: [
         {
-          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          content: '支持一套代码同时开发多端小程序+H5',
           target: 'target6',
           popoverOffset: [40, 12],
           arrowOffset: -36
@@ -211,7 +209,7 @@ export default createDemo({
 
       steps3: [
         {
-          content: '邀请有礼,全新改版,奖励多多哦',
+          content: '70+ 高质量组件,覆盖移动端主流场景',
           target: 'target7'
         }
       ],
@@ -242,10 +240,6 @@ export default createDemo({
 });
 </script>
 <style lang="scss">
-.music {
-  margin-bottom: 30px;
-}
-
 .nut-custom-tour {
   .nut-popover-content {
     width: auto !important;

+ 192 - 211
src/packages/__VUE/tour/doc.en-US.md

@@ -1,8 +1,8 @@
-# Popover
+# Tour
 
 ### Intro
 
-Click or hover over the element to pop up the bubble card overlay.
+A bubble component used to guide the user through the product's capabilities. This component has been available since version 4.0.
 
 ### Install
 
@@ -10,54 +10,50 @@ Click or hover over the element to pop up the bubble card overlay.
 
 import { createApp } from 'vue';
 // vue
-import { Popover, Popup } from '@nutui/nutui';
+import { Tour, Popover, Popup } from '@nutui/nutui';
 // taro
-import { Popover, Popup } from '@nutui/nutui-taro';
+import { Tour, Popover, Popup } from '@nutui/nutui-taro';
 
 const app = createApp();
 
 app.use(Popup);
 app.use(Popover);
+app.use(Tour);
 
 ```
 
 
 ### Basic Usage
 
-Popover supports both light and dark styles. The default is light style. Set the theme property to `dark` to switch to dark style.
+At each step, set the id of the target element, and the Tour component looks up the set id value
 
 :::demo
 ```html
 <template>
-  <nut-popover v-model:visible="visible.lightTheme" :list="iconItemList">
-    <template #reference>
-      <nut-button type="primary" shape="square">Light</nut-button>
+  <nut-cell title="try click" @click="showTour3 = true">
+    <template v-slot:link>
+      <nut-switch id="target7" />
     </template>
-  </nut-popover>
-
-  <nut-popover v-model:visible="visible.darkTheme" theme="dark" :list="iconItemList">
-    <template #reference>
-      <nut-button type="primary" shape="square">Dark</nut-button>
-    </template>
-  </nut-popover>
+  </nut-cell>
+  <nut-tour
+    v-model:visible="showTour3"
+    :steps="steps3"
+    type="tile"
+    location="bottom-end"
+  ></nut-tour>
 </template>
 <script>
 import { reactive, ref } from 'vue';
 export default {
   setup() {
-    const visible = ref({
-      darkTheme: false,
-      lightTheme: false,
+    const state = reactive({
+      showTour3: false,
+      steps3: [{
+          content: '70+ 高质量组件,覆盖移动端主流场景',
+          target: 'target7'
+        }]
     });
-    const iconItemList = reactive([
-      { name: 'option1' },
-      { name: 'option2' },
-      { name: 'option3' }
-    ]);
-    return {
-        visible,
-        iconItemList,
-      };
+    return {...toRefs(state)};
     }
 }
 </script>
@@ -65,95 +61,80 @@ export default {
 ```
 :::
 
-### Option Configuration
+### Custom Style
 
-In the list array, an option can be disabled via the `disabled` field.
+Through 'maskWidth', 'maskHeight', 'bgColor' can be configured hollow mask size, bubble shell layer background color
 
 :::demo
 ```html
 <template>
-  <nut-popover v-model:visible="visible.showIcon" theme="dark" :list="itemList">
-    <template #reference>
-      <nut-button type="primary" shape="square">Show Icon</nut-button>
-    </template>
-  </nut-popover>
-
-  <nut-popover v-model:visible="visible.disableAction" :list="itemListDisabled">
-    <template #reference>
-      <nut-button type="primary" shape="square">Disabled</nut-button>
+  <nut-cell title="try click" @click="showTour1 = true">
+    <template v-slot:link>
+      <nut-switch id="target5" />
     </template>
-  </nut-popover>
+  </nut-cell>
+
+  <nut-tour
+    class="nut-custom-tour nut-customword-tour nut-customstyle-tour"
+    v-model:visible="showTour1"
+    :steps="steps1"
+    location="bottom-end"
+    type="tile"
+    bgColor="#f00"
+    theme="dark"
+    :offset="[0, 0]"
+    maskWidth="50"
+    maskHeight="50"
+  ></nut-tour>
 </template>
 
 <script>
 import { reactive, ref } from 'vue';
 export default {
-  setup() {
-    const visible = ref({
-      showIcon: false,
-      disableAction: false,
+   setup() {
+    const state = reactive({
+      showTour1: false,
+      steps1: [{
+          content: '70+ 高质量组件,覆盖移动端主流场景',
+          target: 'target5'
+        }]
     });
-
-    const itemList = reactive([
-      {
-        name: 'option1',
-        icon: 'my2'
-      },{
-        name: 'option2',
-        icon: 'cart2'
-      },{
-        name: 'option3',
-        icon: 'location2'
-      }
-    ]);
-
-    const itemListDisabled = reactive([
-      {
-        name: 'option1',
-        disabled: true
-      },{
-        name: 'option2',
-        disabled: true
-      },{
-        name: 'option3'
-      }
-    ]);
-
-    return {
-        itemList,
-        visible,
-        itemListDisabled,
-      };
+    return {...toRefs(state)};
     }
 }
 </script>
 
-
 ```
 :::
 
 
-### Custom Content
+### Custom Offset
 
-Customize the content in the slot named content.
+'offset' sets the distance of the hollow mask relative to the target element, 'popoverOffset' sets the offset of the bubble layer
 
 :::demo
 ```html
 <template>
-  <nut-popover v-model:visible="visible.Customized">
-    <template #reference>
-      <nut-button type="primary" shape="square">custom content</nut-button>
-    </template>
-
-    <template #content>
-      <div class="self-content">
-        <div class="self-content-item" v-for="(item, index) in selfContent" :key="index">
-          <nut-icon :name="item.name" size="15"></nut-icon>
-        <div class="self-content-desc">{{ item.desc }}</div>
-        </div>
+  <nut-cell title="try click" @click="showTour2 = true">
+    <template v-slot:link>
+      <div class="tour-demo-img">
+        <img
+          id="target6"
+          src="https://img14.360buyimg.com/imagetools/jfs/t1/167902/2/8762/791358/603742d7E9b4275e3/e09d8f9a8bf4c0ef.png"
+        />
       </div>
     </template>
-  </nut-popover>
+  </nut-cell>
+
+  <nut-tour
+    v-model:visible="showTour2"
+    :steps="steps2"
+    type="tile"
+    bgColor="#f00"
+    theme="dark"
+    location="bottom-end"
+    :offset="[8, 8]"
+  ></nut-tour>
 </template>
 
 
@@ -161,122 +142,124 @@ Customize the content in the slot named content.
 import { reactive, ref } from 'vue';
 export default {
   setup() {
-    const visible = ref({
-      Customized: false,
+    const state = reactive({
+      showTour2: false,
+      steps2: [
+        {
+          content: '70+ 高质量组件,覆盖移动端主流场景',
+          target: 'target6',
+          popoverOffset: [40, 12],
+          arrowOffset: -36
+        }
+      ]
     });
-    const selfContent = reactive([
-      {
-        name: 'service',
-        desc: 'option1'
-      },
-      {
-        name: 'notice',
-        desc: 'option2'
-      },
-      {
-        name: 'location',
-        desc: 'option3'
-      },
-      {
-        name: 'category',
-        desc: 'option4'
-      },
-      {
-        name: 'scan2',
-        desc: 'option5'
-      },
-      {
-        name: 'message',
-        desc: 'option6'
-      }
-    ]);
-
-    return {
-      visible,
-      selfContent,
-    };
-  }
+    return {...toRefs(state)};
+    }
 }
 </script>
 
-<style lang="scss">
-.self-content {
-  width: 195px;
-  display: flex;
-  flex-wrap: wrap;
-  &-item {
-    margin-top: 10px;
-    margin-bottom: 10px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    flex-direction: column;
-  }
-  &-desc {
-    margin-top: 5px;
-    width: 60px;
-    font-size: 10px;
-    text-align: center;
-  }
-}
-</style>
-
 
 ```
 :::
 
-### Placement
+### Custom Content
 
-Use the location property to control where the bubble pops up. optional value
+Can customize the bubble layer through the slot slot
+
+:::demo
+```html
+<template>
+  <nut-cell title="try click" @click="showTour4 = true">
+    <template v-slot:link>
+      <nut-switch id="target8" />
+    </template>
+  </nut-cell>
+
+  <nut-tour
+    v-model:visible="showTour4"
+    :steps="steps4"
+    type="tile"
+    theme="dark"
+    location="bottom-end"
+    :offset="[8, 8]"
+    :closeOnClickOverlay="false"
+  >
+    <div class="tour-demo-custom-content">
+      <div>nutui 4.x 即将发布,敬请期待</div>
+      <nut-divider direction="vertical" />
+      <div @click="showTour4 = false">知道了</div>
+    </div>
+  </nut-tour>
+</template>
+
+<script lang="ts">
+import { reactive, ref } from 'vue';
+export default {
+  setup() {
+    const state = reactive({
+      showTour4: false,
+      steps4: [
+        {
+          target: 'target8'
+        }
+      ]
+    });
+    return {...toRefs(state)};
+  }
+};
+</script>
 
 ```
-top           # Top middle 
-left          # Left middle 
-right         # Right middle 
-bottom        # Bottom middle 
-```
-New since `v3.1.21`
-```
-top-start     # Top left
-top-end       # Top right 
-left-start    # Left top
-left-end      # Left bottom
-right-start   # Right top
-right-end     # Right bottom
-bottom-start  # Bottom left
-bottom-end    # Bottom right
-```
+:::
+
+### Steps
 
 
 :::demo
 ```html
 <template>
-  <nut-popover v-model:visible="visible" location="top" theme="dark" :list="iconItemList">
-    <template #reference>
-      <div class="brick"></div>
-    </template>
-  </nut-popover>
+  <nut-cell title="try click" @click="showTour = true"></nut-cell>
+
+  <nut-tour
+    class="nut-custom-tour"
+    v-model:visible="showTour"
+    :steps="steps"
+    location="top-start"
+    :offset="[0, 0]"
+    maskWidth="50"
+    maskHeight="50"
+  ></nut-tour>
 </template>
 
 <script lang="ts">
 import { reactive, ref } from 'vue';
 export default {
   setup() {
-    const visible = ref(false);
-
-    const iconItemList = reactive([
+    const state = reactive({
+      showTour: false,
+      steps: [
         {
-          name: 'option1'
+          content: '70+ 高质量组件,覆盖移动端主流场景',
+          target: 'target1'
         },
         {
-          name: 'option2'
-        }]);
-
-      return {
-        iconItemList,
-        visible,
-      };
-    }
+          content: '支持一套代码同时开发多端小程序+H5',
+          target: 'target2'
+        },
+        {
+          content: '基于京东APP 10.0 视觉规范',
+          target: 'target3',
+          location: 'top-end'
+        },
+        {
+          content: '支持定制主题,内置 700+ 个主题变量',
+          target: 'target4',
+          location: 'top-end'
+        }
+      ],
+    });
+    return {...toRefs(state)};
+  }
 };
 </script>
 
@@ -289,45 +272,43 @@ export default {
 
 | Attribute | Description | Type | Default |
 |----------------|---------------------------------|---------|------------|
-| list          | list of options                          | List[]   | []        |
-| visible      | whether to show                 | boolean  | false     |
-| theme          | Theme style, can be set to `dark` `light`          | string   | `light`   |
-| location       | pop-up location  | string   | `bottom`  |
-| offset `v3.1.21`       | the offset of the occurrence position  | [number, number]   | [0, 12]  |
-| show-arrow `v3.1.21`       | whether to show small arrows  | boolean  | true  |
-| custom-class `v3.1.21`       | custom class   | string  | ''  |
-| duration `v3.1.21`       | Transition duration  |  [number, number]  | 0.3  |
-| iconPrefix `v3.1.21`       | Icon className prefix | string  | 'nut-icon''  |
-| overlay `v3.2.8`       | Whether to show overlay  | Boolean  | false  |
-| overlay-class `v3.2.8`       | Custom overlay class | string  | ''  |
-| overlay-style `v3.2.8`       | Custom overlay style  | string  | ''  |
-| close-on-click-overlay `v3.2.8`       | Whether to close when clicking overlay  | boolean  | true  |
-| close-on-click-action `v3.2.8`       | Whether to close when clicking action  | boolean  | true |
-| close-on-click-outside `v3.2.8`       | Whether to close when clicking outside | boolean  | true  |
-
-### List data structure  
-
-The List property is an array of objects, each object in the array is configured with a column, and the object can contain the following values:
-
-| Key            | Description                 | Type      | Default  |
+| visible      | Whether to display the boot eject layer                 | boolean  | false     |
+| type          | Tour type            | string   | `step`   |
+| steps       | Boot Step Content  | StepOptions[]   | []  |
+| offset       | The offset of the hollow mask relative to the target element  | [number, number]   | [8, 10]  |
+| current      | When the type is' step ', the number of steps is displayed by default  | number  | 0  |
+| custom-class  | Custom class  | string  | ''  |
+| location      | Location of popover,[location](https://nutui.jd.com/#/zh-CN/component/popover)  |  String  | 'bottom'  |
+| next-step-Txt       | Next step text | string   | 'next'  |
+| prev-step-Txt        | Prev step text  | string   | 'prev'  |
+| complete-txt       | Complete text  | string   | 'complete'  |
+| mask       | Whether to display cutout mask  | Boolean  | true |
+| bg-color       | Custom background color  | Boolean  | '' |
+| theme   | Theme style, can be set to dark light,[theme](https://nutui.jd.com/#/zh-CN/component/popover)  | String  | 'light' |
+| mask-width       | Width of hollow mask  | Number、String  | '' |
+| mask-height       | Hollow mask height  | Number、String  | '' |
+| close-on-click-overlay      | Whether to close when clicking overlay,[closeOnClickOverlay](https://nutui.jd.com/#/zh-CN/component/popover)  | Boolean  | true |
+
+### StepOptions  
+
+| Attribute | Description | Type | Default |
 |----------------|----------------------|----------|--------|
-| name           | option text               | string   | -      |
-| icon           | `nut-icon` name      | string   | -      |
-| disabled       | whether to disable          | boolean  | false  | 
-| className `v3.1.21`      | Add extra class names for corresponding options          | string/Array/object  | -  | 
+| target           | target dom               | Element   | -      |
+| content           | popover content     | String   | ''     |
+| location       | Location of popover,[location](https://nutui.jd.com/#/zh-CN/component/popover)           | String  | 'bottom'  |
+| popover-offset | Offset of popopver [offset](https://nutui.jd.com/#/zh-CN/component/popover)     | [number, number]   | [0, 12]  | 
+| arrow-offset      | Offset of arrow [arrowOffset](https://nutui.jd.com/#/zh-CN/component/popover)           | number  | 0  | 
 
 
 ### Slots
 
 | Name   | Description           |
 |---------|--------------|
-| content | Customize Bubble Component Menu Content |
-| reference | The content of the element that triggers the Popover to display |
+| default | Custom popover content |
 
 ### Events
 
-| Event     | Description    | 
+| Name   | Description           |
 |---------|--------------|
-| choose | Triggered when an option is clicked |
-| open   | Triggered when the menu is opened |
-| close  | Fired when the menu is closed |
+| change | Emit when step change |
+| close   | Emit when popover close |

+ 191 - 208
src/packages/__VUE/tour/doc.md

@@ -1,8 +1,8 @@
-# Popover 气泡弹出框
+# Tour 引导
 
 ### 介绍
 
-点击或在元素上悬停鼠标,弹出气泡卡片浮层
+用于引导用户了解产品功能的气泡组件。自 4.0 版本开始提供该组件
 
 ### 安装
 
@@ -10,54 +10,50 @@
 
 import { createApp } from 'vue';
 // vue
-import { Popover, Popup } from '@nutui/nutui';
+import { Tour, Popover, Popup } from '@nutui/nutui';
 // taro
-import { Popover, Popup } from '@nutui/nutui-taro';
+import { Tour, Popover, Popup } from '@nutui/nutui-taro';
 
 const app = createApp();
 
 app.use(Popup);
 app.use(Popover);
+app.use(Tour);
 
 ```
 
 
 ### 基础用法
 
-Popover 支持明朗和暗黑两种风格,默认为明朗风格,将 theme 属性设置为 dark 可切换为暗黑风格。
+在每一步中设置 `target` 目标元素的 id ,Tour 组件则会根据设置的 id 值进行查找
 
 :::demo
 ```html
 <template>
-  <nut-popover v-model:visible="visible.lightTheme" :list="iconItemList">
-    <template #reference>
-      <nut-button type="primary" shape="square">明朗风格</nut-button>
+  <nut-cell title="点击试试" @click="showTour3 = true">
+    <template v-slot:link>
+      <nut-switch id="target7" />
     </template>
-  </nut-popover>
-
-  <nut-popover v-model:visible="visible.darkTheme" theme="dark" :list="iconItemList">
-    <template #reference>
-      <nut-button type="primary" shape="square">暗黑风格</nut-button>
-    </template>
-  </nut-popover>
+  </nut-cell>
+  <nut-tour
+    v-model:visible="showTour3"
+    :steps="steps3"
+    type="tile"
+    location="bottom-end"
+  ></nut-tour>
 </template>
 <script>
 import { reactive, ref } from 'vue';
 export default {
   setup() {
-    const visible = ref({
-      darkTheme: false,
-      lightTheme: false,
+    const state = reactive({
+      showTour3: false,
+      steps3: [{
+          content: '邀请有礼,全新改版,奖励多多哦',
+          target: 'target7'
+        }]
     });
-    const iconItemList = reactive([
-      { name: '选项一' },
-      { name: '选项二' },
-      { name: '选项三' }
-    ]);
-    return {
-        visible,
-        iconItemList,
-      };
+    return {...toRefs(state)};
     }
 }
 </script>
@@ -65,95 +61,80 @@ export default {
 ```
 :::
 
-### 选项配置
+### 自定义样式
 
-在 list 数组中,可以通过 disabled 字段来禁用某个选项。
+通过 `maskWidth`、`maskHeight`、`bgColor` 可配置镂空遮罩的大小、气泡弹层背景色
 
 :::demo
 ```html
 <template>
-  <nut-popover v-model:visible="visible.showIcon" theme="dark" :list="itemList">
-    <template #reference>
-      <nut-button type="primary" shape="square">展示图标</nut-button>
-    </template>
-  </nut-popover>
-
-  <nut-popover v-model:visible="visible.disableAction" :list="itemListDisabled">
-    <template #reference>
-      <nut-button type="primary" shape="square">禁用选项</nut-button>
+  <nut-cell title="点击试试" @click="showTour1 = true">
+    <template v-slot:link>
+      <nut-switch id="target5" />
     </template>
-  </nut-popover>
+  </nut-cell>
+
+  <nut-tour
+    class="nut-custom-tour nut-customword-tour nut-customstyle-tour"
+    v-model:visible="showTour1"
+    :steps="steps1"
+    location="bottom-end"
+    type="tile"
+    bgColor="#f00"
+    theme="dark"
+    :offset="[0, 0]"
+    maskWidth="50"
+    maskHeight="50"
+  ></nut-tour>
 </template>
 
 <script>
 import { reactive, ref } from 'vue';
 export default {
-  setup() {
-    const visible = ref({
-      showIcon: false,
-      disableAction: false,
+   setup() {
+    const state = reactive({
+      showTour1: false,
+      steps1: [{
+          content: '邀请有礼,全新改版,奖励多多哦',
+          target: 'target5'
+        }]
     });
-
-    const itemList = reactive([
-      {
-        name: '选项一',
-        icon: 'my2'
-      },{
-        name: '选项二',
-        icon: 'cart2'
-      },{
-        name: '选项三',
-        icon: 'location2'
-      }
-    ]);
-
-    const itemListDisabled = reactive([
-      {
-        name: '选项一',
-        disabled: true
-      },{
-        name: '选项二',
-        disabled: true
-      },{
-        name: '选项三'
-      }
-    ]);
-
-    return {
-        itemList,
-        visible,
-        itemListDisabled,
-      };
+    return {...toRefs(state)};
     }
 }
 </script>
 
-
 ```
 :::
 
 
-### 自定义内容
+### 设置偏移量
 
-在名为 content 插槽中自定义内容。
+`offset` 可设置镂空遮罩层相对于目标元素的距离,`popoverOffset` 可设置气泡层的偏移量
 
 :::demo
 ```html
 <template>
-  <nut-popover v-model:visible="visible.Customized">
-    <template #reference>
-      <nut-button type="primary" shape="square">自定义内容</nut-button>
-    </template>
-
-    <template #content>
-      <div class="self-content">
-        <div class="self-content-item" v-for="(item, index) in selfContent" :key="index">
-          <nut-icon :name="item.name" size="15"></nut-icon>
-        <div class="self-content-desc">{{ item.desc }}</div>
-        </div>
+  <nut-cell title="点击试试" @click="showTour2 = true">
+    <template v-slot:link>
+      <div class="tour-demo-img">
+        <img
+          id="target6"
+          src="https://img14.360buyimg.com/imagetools/jfs/t1/167902/2/8762/791358/603742d7E9b4275e3/e09d8f9a8bf4c0ef.png"
+        />
       </div>
     </template>
-  </nut-popover>
+  </nut-cell>
+
+  <nut-tour
+    v-model:visible="showTour2"
+    :steps="steps2"
+    type="tile"
+    bgColor="#f00"
+    theme="dark"
+    location="bottom-end"
+    :offset="[8, 8]"
+  ></nut-tour>
 </template>
 
 
@@ -161,121 +142,125 @@ export default {
 import { reactive, ref } from 'vue';
 export default {
   setup() {
-    const visible = ref({
-      Customized: false,
+    const state = reactive({
+      showTour2: false,
+      steps2: [
+        {
+          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          target: 'target6',
+          popoverOffset: [40, 12],
+          arrowOffset: -36
+        }
+      ]
     });
-    const selfContent = reactive([
-      {
-        name: 'service',
-        desc: '选项一'
-      },
-      {
-        name: 'notice',
-        desc: '选项二'
-      },
-      {
-        name: 'location',
-        desc: '选项三'
-      },
-      {
-        name: 'category',
-        desc: '选项四'
-      },
-      {
-        name: 'scan2',
-        desc: '选项五'
-      },
-      {
-        name: 'message',
-        desc: '选项六'
-      }
-    ]);
-
-    return {
-      visible,
-      selfContent,
-    };
-  }
+    return {...toRefs(state)};
+    }
 }
 </script>
 
-<style lang="scss">
-.self-content {
-  width: 195px;
-  display: flex;
-  flex-wrap: wrap;
-  &-item {
-    margin-top: 10px;
-    margin-bottom: 10px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    flex-direction: column;
-  }
-  &-desc {
-    margin-top: 5px;
-    width: 60px;
-    font-size: 10px;
-    text-align: center;
-  }
-}
-</style>
-
 
 ```
 :::
 
-### 位置自定义
-
-通过 location 属性来控制气泡的弹出位置。可选值
-```
-top           # 顶部中间位置
-left          # 左侧中间位置
-right         # 右侧中间位置
-bottom        # 底部中间位置
-```
-自 `v3.1.21` 起新增
-```
-top-start     # 顶部左侧位置
-top-end       # 顶部右侧位置 
-left-start    # 左侧上方位置
-left-end      # 左侧下方位置
-right-start   # 右侧上方位置
-right-end     # 右侧下方位置
-bottom-start  # 底部左侧位置
-bottom-end    # 底部右侧位置
-```
+### 自定义内容
 
+通过 slot 插槽可自定义气泡层内容
 
 :::demo
 ```html
 <template>
-  <nut-popover v-model:visible="visible" location="top" theme="dark" :list="iconItemList">
-    <template #reference>
-      <div class="brick"></div>
+  <nut-cell title="点击试试" @click="showTour4 = true">
+    <template v-slot:link>
+      <nut-switch id="target8" />
     </template>
-  </nut-popover>
+  </nut-cell>
+
+  <nut-tour
+    v-model:visible="showTour4"
+    :steps="steps4"
+    type="tile"
+    theme="dark"
+    location="bottom-end"
+    :offset="[8, 8]"
+    :closeOnClickOverlay="false"
+  >
+    <div class="tour-demo-custom-content">
+      <div>nutui 4.x 即将发布,敬请期待</div>
+      <nut-divider direction="vertical" />
+      <div @click="showTour4 = false">知道了</div>
+    </div>
+  </nut-tour>
 </template>
 
 <script lang="ts">
 import { reactive, ref } from 'vue';
 export default {
   setup() {
-    const visible = ref(false);
+    const state = reactive({
+      showTour4: false,
+      steps4: [
+        {
+          target: 'target8'
+        }
+      ]
+    });
+    return {...toRefs(state)};
+  }
+};
+</script>
+
+```
+:::
+
+### 步骤引导
+
+属性 `type` 设置为 `step` 可以进行步骤操作。属性 `Steps` 数组中的每一项代表每步的内容
+
+:::demo
+```html
+<template>
+  <nut-cell title="点击试试" @click="showTour = true"></nut-cell>
+
+  <nut-tour
+    class="nut-custom-tour"
+    v-model:visible="showTour"
+    :steps="steps"
+    location="top-start"
+    :offset="[0, 0]"
+    maskWidth="50"
+    maskHeight="50"
+  ></nut-tour>
+</template>
 
-    const iconItemList = reactive([
+<script lang="ts">
+import { reactive, ref } from 'vue';
+export default {
+  setup() {
+    const state = reactive({
+      showTour: false,
+      steps: [
         {
-          name: '选项一'
+          content: '70+ 高质量组件,覆盖移动端主流场景',
+          target: 'target1'
         },
         {
-          name: '选项二'
-        }]);
-
-      return {
-        iconItemList,
-        visible,
-      };
-    }
+          content: '支持一套代码同时开发多端小程序+H5',
+          target: 'target2'
+        },
+        {
+          content: '基于京东APP 10.0 视觉规范',
+          target: 'target3',
+          location: 'top-end'
+        },
+        {
+          content: '支持定制主题,内置 700+ 个主题变量',
+          target: 'target4',
+          location: 'top-end'
+        }
+      ],
+    });
+    return {...toRefs(state)};
+  }
 };
 </script>
 
@@ -288,45 +273,43 @@ export default {
 
 | 字段            | 说明                            | 类型     | 默认值      |
 |----------------|---------------------------------|---------|------------|
-| list          | 选项列表                          | List[]   | []        |
-| visible      | 是否展示气泡弹出层                 | boolean  | false     |
-| theme          | 主题风格,可选值为 dark            | string   | `light`   |
-| location       | 弹出位置  | string   | `bottom`  |
-| offset `v3.1.21`       | 出现位置的偏移量  | [number, number]   | [0, 12]  |
-| show-arrow `v3.1.21`       | 是否显示小箭头  | boolean  | true  |
-| custom-class `v3.1.21`       | 自定义 class 值  | string  | ''  |
-| duration `v3.1.21`       | 动画时长  |  [number, number]  | 0.3  |
-| iconPrefix `v3.1.21`       | 图标自定义类值,等同于 Icon 组件的[ class-prefix 属性](https://nutui.jd.com/#/zh-CN/component/icon)  | string  | 'nut-icon''  |
-| overlay `v3.2.8`       | 是否显示遮罩层  | Boolean  | false  |
-| overlay-class `v3.2.8`       | 自定义遮罩层类名 | string  | ''  |
-| overlay-style `v3.2.8`       | 自定义遮罩层样式  | string  | ''  |
-| close-on-click-overlay `v3.2.8`       | 是否在点击遮罩层后关闭菜单  | boolean  | true  |
-| close-on-click-action `v3.2.8`       | 是否在点击选项后关闭  | boolean  | true |
-| close-on-click-outside `v3.2.8`       | 是否在点击外部元素后关闭菜单 | boolean  | true  |
-
-### List 数据结构  
-
-List 属性是一个由对象构成的数组,数组中的每个对象配置一列,对象可以包含以下值:
+| visible      | 是否展示引导弹出层                 | boolean  | false     |
+| type          | 引导类型            | string   | `step`   |
+| steps       | 引导步骤内容  | StepOptions[]   | []  |
+| offset       | 镂空遮罩相对于目标元素的偏移量  | [number, number]   | [8, 10]  |
+| current      | 类型为 `step` 时,默认展示第几步  | number  | 0  |
+| custom-class       | 自定义 class 值  | string  | ''  |
+| location      | 弹出层位置,同 Popopver 的[location 属性](https://nutui.jd.com/#/zh-CN/component/popover)  |  String  | 'bottom'  |
+| next-step-Txt       | 下一步按钮文案  | string   | '下一步'  |
+| prev-step-Txt        | 上一步按钮文案  | string   | '上一步'  |
+| complete-txt       | 完成按钮文案  | string   | '完成'  |
+| mask       | 是否显示镂空遮罩  | Boolean  | true |
+| bg-color       | 自定义背景色  | Boolean  | '' |
+| theme       | 气泡遮罩层主题,同 Popopver 的[theme 属性](https://nutui.jd.com/#/zh-CN/component/popover)  | String  | 'light' |
+| mask-width       | 镂空遮罩层宽度  | Number、String  | '' |
+| mask-height       | 镂空遮罩层高度  | Number、String  | '' |
+| close-on-click-overlay      | 是否在点击镂空遮罩层后关闭,同 Popopver 的[closeOnClickOverlay 属性](https://nutui.jd.com/#/zh-CN/component/popover)  | Boolean  | true |
+
+### StepOptions  
 
 | 键名            | 说明                 | 类型      | 默认值  |
 |----------------|----------------------|----------|--------|
-| name           | 选项文字               | string   | -      |
-| icon           | nut-icon 图标名称      | string   | -      |
-| disabled       | 是否为禁用状态          | boolean  | false  | 
-| className `v3.1.21`      | 为对应选项添加额外的类名          | string/Array/object  | -  | 
+| target           | 目标对象               | Element   | -      |
+| content           | 气泡层内容     | String   | ''     |
+| location       | 弹出层位置,同 Popopver 的[location 属性](https://nutui.jd.com/#/zh-CN/component/popover)           | String  | 'bottom'  |
+| popover-offset      | 气泡层偏移量,同 Popopver 的[offset 属性](https://nutui.jd.com/#/zh-CN/component/popover)             | string/Array/object  | -  | 
+| arrow-offset      | 小箭头的偏移量,同 Popopver 的[arrowOffset 属性](https://nutui.jd.com/#/zh-CN/component/popover)           | number  | 0  | 
 
 
 ### Slots
 
 | 名称    | 说明         |
 |---------|--------------|
-| content | 自定义气泡组件菜单内容 |
-| reference | 触发 Popover 显示的元素内容 |
+| default | 自定义气泡层内容 |
 
 ### Events
 
 | 名称    | 说明         |
 |---------|--------------|
-| choose | 点击选项时触发 |
-| open   | 打开菜单时触发 |
-| close  | 关闭菜单时触发 |
+| change | 切换步骤时触发 |
+| close   | 气泡层关闭时触发 |

+ 2 - 2
src/packages/__VUE/tour/index.scss

@@ -3,7 +3,7 @@
     position: fixed;
     width: 100px;
     height: 50px;
-    box-shadow: 0px 0px 0px 100vh rgba(0, 0, 0, 0.5);
+    box-shadow: 0px 0px 0px 150vh rgba(0, 0, 0, 0.5);
     border-radius: 10px;
     z-index: 1002;
     &-none {
@@ -66,7 +66,7 @@
     position: fixed;
     width: 100vh;
     height: 100vh;
-    z-index: 999;
+    z-index: 2000;
     top: 0;
     left: 0;
     background: transparent;

+ 201 - 162
src/packages/__VUE/tour/index.taro.vue

@@ -1,201 +1,240 @@
 <template>
-  <view :class="['nut-popover', `nut-popover--${theme}`, `${customClass}`]">
-    <view class="nut-popover-wrapper" @click="openPopover" ref="popoverRef" :id="'popoverRef' + refRandomId"
-      ><slot name="reference"></slot
-    ></view>
-
-    <nut-popup
-      :popClass="`nut-popover-content nut-popover-content--${location}`"
-      :style="getStyles"
-      v-model:visible="showPopup"
-      position=""
-      transition="nut-popover"
-      :overlay="overlay"
-      :duration="duration"
-      :overlayStyle="overlayStyle"
-      :overlayClass="overlayClass"
-      :closeOnClickOverlay="closeOnClickOverlay"
-    >
-      <view ref="popoverContentRef" :id="'popoverContentRef' + refRandomId" class="nut-popover-content-group">
-        <view :class="popoverArrow" v-if="showArrow"> </view>
-        <slot name="content"></slot>
+  <view :class="classes">
+    <view class="nut-tour-masked" v-show="showTour" @click="handleClickMask"></view>
+
+    <view v-for="(step, i) in steps" :key="i" style="height: 0">
+      <template v-if="i == active">
         <view
-          v-for="(item, index) in list"
-          :key="index"
-          :class="[
-            item.className,
-            item.disabled && 'nut-popover-menu-disabled',
-            'nut-popover-menu-item',
-            'nut-popover-menu-taroitem'
-          ]"
-          @click.stop="chooseItem(item, index)"
+          class="nut-tour-mask"
+          :class="[mask ? '' : 'nut-tour-mask-none']"
+          :style="maskStyle"
+          v-if="showTour"
+          id="nut-tour-popid"
+        ></view>
+        <nut-popover
+          v-model:visible="showPopup"
+          :location="step.location || location"
+          targetId="nut-tour-popid"
+          :bgColor="bgColor"
+          :theme="theme"
+          :close-on-click-outside="false"
+          :offset="step.popoverOffset || [0, 12]"
+          :arrowOffset="step.arrowOffset || 0"
         >
-          <slot v-if="item.icon">
-            <nut-icon
-              v-bind="$attrs"
-              class="nut-popover-item-img"
-              :classPrefix="iconPrefix"
-              :name="item.icon"
-            ></nut-icon
-          ></slot>
-          <view class="nut-popover-menu-item-name">{{ item.name }}</view>
-        </view>
-      </view>
-    </nut-popup>
-
-    <view class="nut-popover-content-bg" v-if="showPopup" @touchmove="clickAway" @click="clickAway"></view>
+          <template #content>
+            <slot>
+              <view class="nut-tour-content" v-if="type == 'step'">
+                <view class="nut-tour-content-top">
+                  <view @click="close">
+                    <Close class="nut-tour-content-top-close" />
+                  </view>
+                </view>
+                <view class="nut-tour-content-inner">
+                  {{ step.content }}
+                </view>
+                <view class="nut-tour-content-bottom">
+                  <view class="nut-tour-content-bottom-init">{{ active + 1 }}/{{ steps.length }}</view>
+                  <view class="nut-tour-content-bottom-operate">
+                    <view class="nut-tour-content-bottom-operate-btn" @click="changeStep('prev')" v-if="active != 0">{{
+                      prevStepTxt
+                    }}</view>
+                    <view
+                      class="nut-tour-content-bottom-operate-btn active"
+                      @click="close"
+                      v-if="steps.length - 1 == active"
+                      >{{ completeTxt }}</view
+                    >
+                    <view class="nut-tour-content-bottom-operate-btn active" @click="changeStep('next')" v-else>{{
+                      nextStepTxt
+                    }}</view>
+                  </view>
+                </view>
+              </view>
+
+              <view class="nut-tour-content nut-tour-content-tile" v-if="type == 'tile'">
+                <view class="nut-tour-content-inner">
+                  {{ step.content }}
+                </view>
+              </view>
+            </slot>
+          </template>
+        </nut-popover>
+      </template>
+    </view>
   </view>
 </template>
 <script lang="ts">
-import { onMounted, computed, watch, ref, PropType, toRefs, reactive, CSSProperties } from 'vue';
+import { computed, watch, ref, reactive, toRefs, PropType, nextTick, onMounted } from 'vue';
+import { PopoverLocation } from '../popover/type';
 import { createComponent } from '@/packages/utils/create';
-const { componentName, create } = createComponent('popover');
 import { useTaroRect } from '@/packages/utils/useTaroRect';
-import { isArray } from '@/packages/utils/util';
+import { Close } from '@nutui/icons-vue';
 import Taro from '@tarojs/taro';
 
+interface StepOptions {
+  target: Element;
+  content: String;
+  location?: PopoverLocation;
+  popoverOffset?: number[];
+  arrowOffset?: number;
+}
+const { create } = createComponent('tour');
 export default create({
-  inheritAttrs: false,
-  components: {},
+  components: {
+    Close
+  },
   props: {
     visible: { type: Boolean, default: false },
-    list: { type: Array, default: [] },
-    theme: { type: String as PropType<import('./type').PopoverTheme>, default: 'light' },
-    location: { type: String as PropType<import('./type').PopoverLocation>, default: 'bottom' },
-    offset: { type: Array, default: [0, 12] },
-    customClass: { type: String, default: '' },
-    showArrow: { type: Boolean, default: true },
-    iconPrefix: { type: String, default: 'nut-icon' },
-    duration: { type: [Number, String], default: 0.3 },
-    overlay: { type: Boolean, default: false },
-    overlayClass: { type: String, default: '' },
-    overlayStyle: { type: Object as PropType<CSSProperties> },
-    closeOnClickOverlay: { type: Boolean, default: true },
-    closeOnClickAction: { type: Boolean, default: true },
-    closeOnClickOutside: { type: Boolean, default: true }
+    type: {
+      type: String,
+      default: 'step' // tile
+    },
+    steps: {
+      type: Array as PropType<StepOptions[]>,
+      default: () => []
+    },
+    location: {
+      type: String as PropType<PopoverLocation>,
+      default: 'bottom'
+    },
+    current: {
+      type: Number,
+      default: 0
+    },
+    nextStepTxt: {
+      type: String,
+      default: '下一步'
+    },
+    prevStepTxt: {
+      type: String,
+      default: '上一步'
+    },
+    completeTxt: {
+      type: String,
+      default: '完成'
+    },
+    mask: {
+      type: Boolean,
+      default: true
+    },
+    offset: {
+      type: Array as PropType<Number[]>,
+      default: [8, 10]
+    },
+    bgColor: {
+      type: String,
+      default: ''
+    },
+    theme: {
+      type: String,
+      default: 'light'
+    },
+    maskWidth: {
+      type: [Number, String],
+      default: ''
+    },
+    maskHeight: {
+      type: [Number, String],
+      default: ''
+    },
+    closeOnClickOverlay: {
+      type: Boolean,
+      default: true
+    }
   },
-  emits: ['update', 'update:visible', 'close', 'choose', 'open'],
+  emits: ['update:visible', 'change', 'close'],
   setup(props, { emit }) {
-    const popoverRef = ref();
-    const popoverContentRef = ref();
-    const showPopup = ref(props.visible);
     const state = reactive({
-      rootWidth: 0,
-      rootHeight: 0
+      showTour: props.visible,
+      showPopup: false,
+      active: 0
     });
 
-    const popoverArrow = computed(() => {
-      const prefixCls = 'nut-popover-arrow';
-      const loca = props.location;
-      const direction = loca.split('-')[0];
-      return `${prefixCls} ${prefixCls}-${direction} ${prefixCls}--${loca}`;
+    const maskRect = ref<{
+      [props: string]: number;
+    }>({});
+
+    const classes = computed(() => {
+      const prefixCls = 'nut-tour';
+      return `${prefixCls}`;
     });
-    const getStyles = computed(() => {
-      let cross = +state.rootHeight;
-      let lengthways = +state.rootWidth;
-      let { offset, location } = props;
-      if (isArray(offset) && offset.length == 2) {
-        cross += +offset[1];
-        lengthways += +offset[1];
-      }
-      const direction = location.split('-')[0];
-      const style: CSSProperties = {};
-      const mapd: any = {
-        top: 'bottom',
-        bottom: 'top',
-        left: 'right',
-        right: 'left'
+
+    const maskStyle = computed(() => {
+      const { offset, maskWidth, maskHeight } = props;
+      const { width, height, left, top } = maskRect.value;
+
+      const center = [left + width / 2, top + height / 2]; // 中心点 【横,纵】
+      const w: number = Number(maskWidth ? maskWidth : width);
+      const h: number = Number(maskHeight ? maskHeight : height);
+
+      const styles = {
+        width: `${w + +offset[1] * 2}px`,
+        height: `${h + +offset[0] * 2}px`,
+        top: `${center[1] - h / 2 - +offset[0]}px`,
+        left: `${center[0] - w / 2 - +offset[1]}px`
       };
-      if (['top', 'bottom'].includes(direction)) {
-        style[mapd[direction]] = `${cross}px`;
-        style.marginLeft = `${offset[0]}px`;
-      } else {
-        style[mapd[direction]] = `${lengthways}px`;
-        style.marginTop = `${offset[0]}px`;
-      }
-      return style;
+      return styles;
     });
-    // 获取宽度
-    const getContentWidth = async () => {
-      const refe = await useTaroRect(popoverRef, Taro);
-      const { height, width } = refe;
-      state.rootHeight = height;
-      state.rootWidth = width;
-    };
-    watch(
-      () => props.visible,
-      (value) => {
-        showPopup.value = value;
-        if (value) {
-          setTimeout(() => {
-            getContentWidth();
-          }, 200);
-        }
+
+    const changeStep = (type: string) => {
+      if (type == 'next') {
+        state.active = state.active + 1;
+      } else {
+        state.active = state.active - 1;
       }
-    );
-    const update = (val: boolean) => {
-      emit('update', val);
-      emit('update:visible', val);
+
+      state.showPopup = false;
+      nextTick(() => {
+        state.showPopup = true;
+        getRootPosition();
+      });
+
+      emit('change', state.active);
     };
-    const openPopover = () => {
-      update(!props.visible);
-      emit('open');
+
+    const getRootPosition = async () => {
+      const rect = await useTaroRect(props.steps[state.active].target, Taro);
+      maskRect.value = rect;
     };
-    const closePopover = () => {
+
+    const close = () => {
+      state.showTour = false;
+      state.showPopup = false;
+      emit('close', state.active);
       emit('update:visible', false);
-      emit('close');
     };
-    const chooseItem = (item: any, index: number) => {
-      emit('choose', item, index);
-      if (props.closeOnClickAction) {
-        closePopover();
-      }
-    };
-    const clickAway = (event: Event) => {
-      closePopover();
+
+    const handleClickMask = () => {
+      props.closeOnClickOverlay && close();
     };
 
     onMounted(() => {
       setTimeout(() => {
-        getContentWidth();
+        getRootPosition();
       }, 200);
     });
 
-    const refRandomId = Math.random().toString(36).slice(-8);
+    watch(
+      () => props.visible,
+      (val) => {
+        if (val) {
+          state.active = 0;
+          getRootPosition();
+        }
+
+        state.showTour = val;
+        state.showPopup = val;
+      }
+    );
 
     return {
-      showPopup,
-      openPopover,
-      popoverArrow,
-      closePopover,
-      chooseItem,
-      popoverRef,
-      getStyles,
-      popoverContentRef,
-      refRandomId,
-      clickAway
+      ...toRefs(state),
+      classes,
+      maskStyle,
+      changeStep,
+      close,
+      handleClickMask
     };
   }
 });
 </script>
-<style lang="scss">
-.self-content {
-  width: 195px;
-  display: flex;
-  flex-wrap: wrap;
-  &-item {
-    margin-top: 10px;
-    margin-bottom: 10px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    flex-direction: column;
-  }
-  &-desc {
-    margin-top: 5px;
-    width: 60px;
-    font-size: 10px;
-    text-align: center;
-  }
-}
-</style>

+ 3 - 2
src/packages/__VUE/tour/index.vue

@@ -211,11 +211,12 @@ export default create({
     watch(
       () => props.visible,
       (val) => {
-        state.showTour = val;
-        state.showPopup = val;
         if (val) {
           getRootPosition();
         }
+        state.active = 0;
+        state.showTour = val;
+        state.showPopup = val;
       }
     );
 

+ 2 - 1
src/packages/utils/useTaroRect/index.ts

@@ -47,7 +47,8 @@ export const useTaroRect = (elementRef: (Element | Window | any) | Ref<Element |
       });
     } else {
       const query = Taro.createSelectorQuery();
-      query.select(`#${(element as any).id}`) && query.select(`#${(element as any).id}`).boundingClientRect();
+      let el = (element as any).id ? (element as any).id : (element as any);
+      query.select(`#${el}`) && query.select(`#${el}`).boundingClientRect();
       query.exec(function (res: any) {
         resolve(res[0]);
       });

+ 5 - 5
src/sites/mobile-taro/vue/project.private.config.json

@@ -33,15 +33,15 @@
           "scene": null
         },
         {
-          "name": "exhibition/pages/ellipsis/index",
-          "pathName": "exhibition/pages/ellipsis/index",
+          "name": "exhibition/pages/popover/index",
+          "pathName": "exhibition/pages/popover/index",
           "query": "",
           "launchMode": "default",
           "scene": null
         },
         {
-          "name": "exhibition/pages/collapse/index",
-          "pathName": "exhibition/pages/collapse/index",
+          "name": "exhibition/pages/tour/index",
+          "pathName": "exhibition/pages/tour/index",
           "query": "",
           "launchMode": "default",
           "scene": null
@@ -63,6 +63,6 @@
       ]
     }
   },
-  "projectname": "vue",
+  "projectname": "vue4.x",
   "libVersion": "2.27.1"
 }

+ 39 - 10
src/sites/mobile-taro/vue/src/exhibition/pages/popover/index.vue

@@ -28,7 +28,7 @@
         </nut-popover>
       </nut-col>
       <nut-col :span="8">
-        <nut-popover v-model:visible="disableAction" :list="itemListDisabled" location="bottom-end">
+        <nut-popover v-model:visible="disableAction" :list="itemListDisabled" location="right-start">
           <template #reference>
             <nut-button type="primary" shape="square">禁用选项</nut-button>
           </template>
@@ -57,14 +57,36 @@
     <nut-picker v-model:visible="showPicker" :columns="columns" title="" @change="change" :swipe-duration="500">
       <template #top>
         <div class="brickBox">
-          <nut-popover v-model:visible="customPositon" :location="curPostion" theme="dark" :list="positionList">
-            <template #reference>
-              <div class="brick"></div>
-            </template>
-          </nut-popover>
+          <div class="brick" id="pickerTarget"></div>
         </div>
       </template>
     </nut-picker>
+
+    <nut-popover
+      v-model:visible="customPositon"
+      targetId="pickerTarget"
+      :location="curPostion"
+      theme="dark"
+      :list="positionList"
+    >
+    </nut-popover>
+
+    <h2>自定义对象</h2>
+    <nut-button type="primary" shape="square" id="popid" @click="clickCustomHandle"> 自定义对象 </nut-button>
+    <nut-popover
+      v-model:visible="customTarget"
+      targetId="popid"
+      :list="iconItemList"
+      location="top-start"
+    ></nut-popover>
+
+    <h2>自定义颜色</h2>
+
+    <nut-popover v-model:visible="customColor" :list="iconItemList" location="right-start" bgColor="#f00" theme="dark">
+      <template #reference>
+        <nut-button type="primary" shape="square">自定义颜色</nut-button>
+      </template>
+    </nut-popover>
   </div>
 </template>
 <script lang="ts">
@@ -84,7 +106,9 @@ export default {
       leftLocation: false, //向左弹出
       customPositon: false,
 
-      showPicker: false
+      showPicker: false,
+      customTarget: false,
+      customColor: false
     });
     const curPostion = ref('top');
 
@@ -188,12 +212,16 @@ export default {
       state.showPicker = true;
       setTimeout(() => {
         state.customPositon = true;
-      });
+      }, 1000);
     };
 
     const change = ({ selectedValue }) => {
       curPostion.value = selectedValue[0];
-      state.customPositon = true;
+      if (state.showPicker) state.customPositon = true;
+    };
+
+    const clickCustomHandle = () => {
+      state.customTarget = !state.customTarget;
     };
 
     return {
@@ -207,7 +235,8 @@ export default {
       positionList,
       columns,
       change,
-      handlePicker
+      handlePicker,
+      clickCustomHandle
     };
   }
 };

+ 222 - 248
src/sites/mobile-taro/vue/src/exhibition/pages/tour/index.vue

@@ -1,92 +1,135 @@
 <template>
   <div class="demo">
     <h2>基础用法</h2>
-    <nut-row type="flex">
-      <nut-col :span="8">
-        <nut-popover v-model:visible="lightTheme" :list="iconItemList" location="bottom-start" @choose="chooseItem">
-          <template #reference>
-            <nut-button type="primary" shape="square">明朗风格</nut-button>
-          </template>
-        </nut-popover>
-      </nut-col>
-      <nut-col :span="8">
-        <nut-popover v-model:visible="darkTheme" theme="dark" :list="iconItemList">
-          <template #reference>
-            <nut-button type="primary" shape="square">暗黑风格</nut-button>
-          </template>
-        </nut-popover>
-      </nut-col>
-    </nut-row>
+    <nut-cell title="点击试试" @click="showTour3 = true">
+      <template v-slot:link>
+        <nut-switch id="target7" />
+      </template>
+    </nut-cell>
 
-    <h2>选项配置</h2>
-    <nut-row type="flex">
-      <nut-col :span="8">
-        <nut-popover v-model:visible="showIcon" theme="dark" :list="itemList">
-          <template #reference>
-            <nut-button type="primary" shape="square">展示图标</nut-button>
-          </template>
-        </nut-popover>
-      </nut-col>
-      <nut-col :span="8">
-        <nut-popover v-model:visible="disableAction" :list="itemListDisabled" location="bottom-end">
-          <template #reference>
-            <nut-button type="primary" shape="square">禁用选项</nut-button>
-          </template>
-        </nut-popover>
-      </nut-col>
-    </nut-row>
+    <nut-tour
+      class="nut-custom-tour nut-customword-tour"
+      v-model:visible="showTour3"
+      :steps="steps3"
+      type="tile"
+      location="bottom-end"
+    ></nut-tour>
 
-    <h2>自定义内容</h2>
-    <nut-popover v-model:visible="Customized" location="top-start" custom-class="customClass">
-      <template #reference>
-        <nut-button type="primary" shape="square">自定义内容</nut-button>
+    <h2>自定义样式</h2>
+
+    <nut-cell title="点击试试" @click="showTourHandle">
+      <template v-slot:link>
+        <nut-switch id="target5" />
       </template>
+    </nut-cell>
+
+    <nut-tour
+      class="nut-custom-tour nut-customword-tour nut-customstyle-tour"
+      v-model:visible="showTour1"
+      :steps="steps1"
+      location="bottom-end"
+      type="tile"
+      bgColor="#f00"
+      theme="dark"
+      :offset="[0, 0]"
+      maskWidth="50"
+      maskHeight="50"
+    ></nut-tour>
 
-      <template #content>
-        <div class="self-content">
-          <div class="self-content-item" v-for="(item, index) in selfContent" :key="index">
-            <nut-icon :name="item.name" size="15"></nut-icon>
-            <div class="self-content-desc">{{ item.desc }}</div>
-          </div>
+    <h2>设置偏移量</h2>
+
+    <nut-cell title="点击试试" @click="showTour2 = true">
+      <template v-slot:link>
+        <div class="tour-demo-img">
+          <img
+            id="target6"
+            src="https://img14.360buyimg.com/imagetools/jfs/t1/167902/2/8762/791358/603742d7E9b4275e3/e09d8f9a8bf4c0ef.png"
+            alt=""
+          />
+          <img
+            src="https://img10.360buyimg.com/imagetools/jfs/t1/31842/40/20385/1762/63998e3eE594254bb/98ff51da635ead4a.png"
+            alt=""
+          />
+          <img src="https://storage.jd.com/oss-dev/test/3.2.6/Jdweapp.png" alt="" />
         </div>
       </template>
-    </nut-popover>
+    </nut-cell>
 
-    <h2>位置自定义</h2>
-    <nut-cell title="点击查看更多方向" @click="handlePicker"></nut-cell>
-    <nut-picker v-model:visible="showPicker" :columns="columns" title="" @change="change" :swipe-duration="500">
-      <template #top>
-        <div class="brickBox">
-          <div class="brick" id="pickerTarget"></div>
-        </div>
+    <nut-tour
+      class="nut-custom-tour nut-customword-tour"
+      v-model:visible="showTour2"
+      :steps="steps2"
+      type="tile"
+      bgColor="#f00"
+      theme="dark"
+      location="bottom-end"
+      :offset="[8, 8]"
+    ></nut-tour>
+
+    <h2>自定义内容</h2>
+
+    <nut-cell title="点击试试" @click="showTour4 = true">
+      <template v-slot:link>
+        <nut-switch id="target8" />
       </template>
-    </nut-picker>
+    </nut-cell>
 
-    <nut-popover
-      v-model:visible="customPositon"
-      targetId="pickerTarget"
-      :location="curPostion"
+    <nut-tour
+      class="nut-custom-tour nut-customword-tour"
+      v-model:visible="showTour4"
+      :steps="steps4"
+      type="tile"
       theme="dark"
-      :list="positionList"
+      location="bottom-end"
+      :offset="[8, 8]"
     >
-    </nut-popover>
+      <view class="tour-demo-custom-content">
+        <view>nutui 4.x 即将发布,敬请期待</view>
+        <nut-divider direction="vertical" />
+        <view @click="showTour4 = false">知道了</view>
+      </view>
+    </nut-tour>
 
-    <h2>自定义对象</h2>
-    <nut-button type="primary" shape="square" id="popid" @click="clickCustomHandle"> 自定义对象 </nut-button>
-    <nut-popover
-      v-model:visible="customTarget"
-      targetId="popid"
-      :list="iconItemList"
-      location="top-start"
-    ></nut-popover>
+    <!-- <h2>步骤</h2> -->
 
-    <h2>自定义颜色</h2>
+    <!-- <nut-cell title="点击试试" @click="showTour = true"></nut-cell>
 
-    <nut-popover v-model:visible="customColor" :list="iconItemList" location="right-start" bgColor="#f00" theme="dark">
-      <template #reference>
-        <nut-button type="primary" shape="square">自定义颜色</nut-button>
-      </template>
-    </nut-popover>
+    <nut-tabbar>
+      <nut-tabbar-item
+        id="target1"
+        tab-title="首页"
+        img="https://img13.360buyimg.com/imagetools/jfs/t1/23319/19/18329/3084/62e7c346E957c54ef/6c3e8a49e52b76f2.png"
+        activeImg="https://img11.360buyimg.com/imagetools/jfs/t1/70423/4/20553/3652/62e74629E23ba550e/aeeed0e3b9f43ae6.png"
+      ></nut-tabbar-item>
+      <nut-tabbar-item
+        id="target2"
+        tab-title="首页"
+        img="https://img13.360buyimg.com/imagetools/jfs/t1/202062/32/25149/5246/62e7c353E5a51db17/b82b940e6eb22ec3.png"
+        activeImg="https://img11.360buyimg.com/imagetools/jfs/t1/162634/35/26732/5502/62e747a8E5330f029/3ea00a0c140beb38.png"
+      ></nut-tabbar-item>
+      <nut-tabbar-item
+        id="target3"
+        tab-title="首页"
+        img="https://img12.360buyimg.com/imagetools/jfs/t1/60552/28/20576/5585/62e7c2ddE2e0b48a7/70eefb366b85f3e4.png"
+        activeImg="https://img11.360buyimg.com/imagetools/jfs/t1/138362/15/28011/5802/62e747a4E4139ef2f/9a79a1c0f6a273b4.png"
+      ></nut-tabbar-item>
+      <nut-tabbar-item
+        id="target4"
+        tab-title="首页"
+        img="https://img14.360buyimg.com/imagetools/jfs/t1/156023/30/29042/4257/62e7c34aE71f32967/690e2db242e2a97f.png"
+        activeImg="https://img13.360buyimg.com/imagetools/jfs/t1/144283/8/28420/4851/62e74784Eaa8549fe/80535de2961b812e.png"
+      ></nut-tabbar-item>
+    </nut-tabbar> -->
+
+    <!-- <nut-tour
+      class="nut-customword-tour"
+      v-model:visible="showTour"
+      :steps="steps"
+      location="bottom-start"
+      :offset="[0, 0]"
+      maskWidth="50"
+      maskHeight="50"
+    ></nut-tour> -->
   </div>
 </template>
 <script lang="ts">
@@ -95,210 +138,141 @@ import { reactive, ref, toRefs } from 'vue';
 export default {
   setup() {
     const state = reactive({
-      showIcon: false,
-      placement: false,
-      darkTheme: false,
-      lightTheme: false,
-      Customized: false,
-      disableAction: false,
-      topLocation: false, //向上弹出
-      rightLocation: false, //向右弹出
-      leftLocation: false, //向左弹出
-      customPositon: false,
-
-      showPicker: false,
-      customTarget: false,
-      customColor: false
-    });
-    const curPostion = ref('top');
+      showTour: false,
+      showTour1: false,
+      showTour2: false,
+      showTour3: false,
+      showTour4: false,
+      offset: [-3, -8],
+      steps: [
+        {
+          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          target: 'target1'
+        },
+        {
+          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          target: 'target2'
+        },
+        {
+          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          target: 'target3',
+          location: 'top-end'
+        },
+        {
+          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          target: 'target4',
+          location: 'top-end'
+        }
+      ],
 
-    const columns = ref([
-      { text: 'top', value: 'top' },
-      { text: 'top-start', value: 'top-start' },
-      { text: 'top-end', value: 'top-end' },
-      { text: 'right', value: 'right' },
-      { text: 'right-start', value: 'right-start' },
-      { text: 'right-end', value: 'right-end' },
-      { text: 'bottom', value: 'bottom' },
-      { text: 'bottom-start', value: 'bottom-start' },
-      { text: 'bottom-end', value: 'bottom-end' },
-      { text: 'left', value: 'left' },
-      { text: 'left-start', value: 'left-start' },
-      { text: 'left-end', value: 'left-end' }
-    ]);
+      steps1: [
+        {
+          content: '邀请有礼,全新改版,奖励多多哦',
+          target: 'target5'
+        }
+      ],
 
-    const iconItemList = reactive([
-      {
-        name: 'option1'
-      },
-      {
-        name: 'option2'
-      },
-      {
-        name: 'option3'
-      }
-    ]);
+      steps2: [
+        {
+          content: '这里换成关注和粉丝啦,听歌时长点击头像可见',
+          target: 'target6',
+          popoverOffset: [40, 12],
+          arrowOffset: -36
+        }
+      ],
 
-    const positionList = reactive([
-      {
-        name: 'option1'
-      },
-      {
-        name: 'option2'
-      }
-    ]);
+      steps3: [
+        {
+          content: '邀请有礼,全新改版,奖励多多哦',
+          target: 'target7'
+        }
+      ],
 
-    const itemList = reactive([
-      {
-        name: 'option1',
-        icon: 'my2'
-      },
-      {
-        name: 'option2',
-        icon: 'cart2'
-      },
-      {
-        name: 'option3',
-        icon: 'location2'
-      }
-    ]);
+      steps4: [
+        {
+          target: 'target8'
+        }
+      ],
 
-    const itemListDisabled = reactive([
-      {
-        name: 'option1',
-        disabled: true
-      },
-      {
-        name: 'option2',
-        disabled: true
-      },
-      {
-        name: 'option3'
-      }
-    ]);
-
-    const selfContent = reactive([
-      {
-        name: 'service',
-        desc: 'option1'
-      },
-      {
-        name: 'notice',
-        desc: 'option2'
-      },
-      {
-        name: 'location',
-        desc: 'option3'
-      },
-      {
-        name: 'category',
-        desc: 'option4'
-      },
-      {
-        name: 'scan2',
-        desc: 'option5'
-      },
-      {
-        name: 'message',
-        desc: 'option6'
-      }
-    ]);
-
-    const chooseItem = (item: unknown, index: number) => {
-      console.log(item, index);
-    };
-
-    const handlePicker = () => {
-      state.showPicker = true;
-      setTimeout(() => {
-        state.customPositon = true;
-      }, 500);
-    };
+      type: 'normal'
+    });
 
-    const change = ({ selectedValue }) => {
-      curPostion.value = selectedValue[0];
-      if (state.showPicker) state.customPositon = true;
-    };
+    // setTimeout(() => {
+    //   state.showTour = true;
+    // }, 1000);
 
-    const clickCustomHandle = () => {
-      state.customTarget = !state.customTarget;
+    const showTourHandle = () => {
+      state.showTour1 = true;
     };
 
     return {
-      iconItemList,
-      itemList,
       ...toRefs(state),
-      itemListDisabled,
-      selfContent,
-      chooseItem,
-      curPostion,
-      positionList,
-      columns,
-      change,
-      handlePicker,
-      clickCustomHandle
+      showTourHandle
     };
   }
 };
 </script>
 <style lang="scss">
-.demo > h2 {
-  padding: 0;
+.nut-custom-tour {
+  .nut-popover-content {
+    width: auto !important;
+  }
 }
-.brickBox {
-  margin: 80px 0;
-  display: flex;
-  justify-content: center;
-  .brick {
-    width: 60px;
-    height: 60px;
-    background: linear-gradient(135deg, #fa2c19 0%, #fa6419 100%);
-    border-radius: 10px;
+
+.nut-customword-tour {
+  .nut-tour-content-inner {
+    width: max-content;
   }
 }
 
-.radiogroup {
+.index-header {
   display: flex;
-  flex-wrap: wrap;
-  background: #fff;
-  padding: 10px 6px;
-
-  > .nut-radio {
-    width: 110px;
-
-    > .nut-radio__button {
-      padding: 5px 12px;
-      border: 1px solid #f6f7f9;
+  align-items: center;
+  height: 117px;
+  > img {
+    width: 67px;
+    height: 67px;
+    margin-right: 18px;
+    flex-shrink: 0;
+  }
+  .info {
+    display: flex;
+    flex-direction: column;
+    h1 {
+      margin: 0;
+      height: 48px;
+      font-size: 34px;
+      color: rgba(51, 51, 51, 1);
+    }
+    p {
+      height: 18px;
+      font-size: 12px;
+      color: rgba(154, 155, 157, 1);
     }
   }
 }
 
-.nut-popover-content {
-  width: 120px;
+.nut-customstyle-tour {
+  .nut-tour-mask {
+    border-radius: 50%;
+  }
 }
 
-.customClass {
-  .nut-popover-content {
-    width: auto;
+.tour-demo-img {
+  img {
+    width: 20px;
+    height: 20px;
+    margin-right: 10px;
   }
-  .self-content {
-    width: 195px;
-    display: flex;
-    flex-wrap: wrap;
-    &-item {
-      margin-top: 10px;
-      margin-bottom: 10px;
-      display: flex;
-      justify-content: center;
-      align-items: center;
-      flex-direction: column;
-    }
-    &-desc {
-      margin-top: 5px;
-      width: 60px;
-      font-size: 10px;
-      text-align: center;
-    }
+}
+.tour-demo-custom-content {
+  padding: 8px;
+  display: flex;
+  width: max-content;
+  align-items: center;
+
+  .nut-divider {
+    border-color: #fff;
   }
 }
 </style>