From d1fbd55a554f5ef97a4d5ca088fffc8bf531a164 Mon Sep 17 00:00:00 2001
From: junjun666 <35680677+junjun666@users.noreply.github.com>
Date: Tue, 8 Aug 2023 11:11:43 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20tour=20=E5=BC=95?=
=?UTF-8?q?=E5=AF=BC=E7=BB=84=E4=BB=B6=20(#1279)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/config.json | 11 +
src/locales/base.ts | 5 +
src/locales/en-US.ts | 5 +
src/locales/id-ID.ts | 5 +
src/locales/tr-TR.ts | 5 +
src/locales/zh-CN.ts | 5 +
src/locales/zh-TW.ts | 5 +
src/locales/zh-UG.ts | 5 +
src/packages/popover/doc.en-US.md | 1 +
src/packages/popover/doc.md | 1 +
src/packages/popover/popover.taro.tsx | 43 ++-
src/packages/popover/popover.tsx | 41 ++-
src/packages/switch/switch.taro.tsx | 8 +-
src/packages/switch/switch.tsx | 8 +-
src/packages/tabbaritem/tabbaritem.taro.tsx | 2 +
src/packages/tabbaritem/tabbaritem.tsx | 2 +
src/packages/tour/__test__/tour.spec.tsx | 154 ++++++++
src/packages/tour/demo.scss | 62 ++++
src/packages/tour/demo.taro.tsx | 308 ++++++++++++++++
src/packages/tour/demo.tsx | 304 +++++++++++++++
src/packages/tour/doc.en-US.md | 386 ++++++++++++++++++++
src/packages/tour/doc.md | 386 ++++++++++++++++++++
src/packages/tour/doc.taro.md | 386 ++++++++++++++++++++
src/packages/tour/index.taro.ts | 5 +
src/packages/tour/index.ts | 5 +
src/packages/tour/tour.scss | 78 ++++
src/packages/tour/tour.taro.tsx | 268 ++++++++++++++
src/packages/tour/tour.tsx | 262 +++++++++++++
src/sites/mobile-taro/src/app.config.ts | 1 +
src/styles/variables.scss | 33 ++
src/utils/typings.ts | 1 +
src/utils/use-taro-rect.ts | 25 ++
32 files changed, 2811 insertions(+), 5 deletions(-)
create mode 100644 src/packages/tour/__test__/tour.spec.tsx
create mode 100644 src/packages/tour/demo.scss
create mode 100644 src/packages/tour/demo.taro.tsx
create mode 100644 src/packages/tour/demo.tsx
create mode 100644 src/packages/tour/doc.en-US.md
create mode 100644 src/packages/tour/doc.md
create mode 100644 src/packages/tour/doc.taro.md
create mode 100644 src/packages/tour/index.taro.ts
create mode 100644 src/packages/tour/index.ts
create mode 100644 src/packages/tour/tour.scss
create mode 100644 src/packages/tour/tour.taro.tsx
create mode 100644 src/packages/tour/tour.tsx
create mode 100644 src/utils/use-taro-rect.ts
diff --git a/src/config.json b/src/config.json
index f95f306ffc..d46ff514e0 100644
--- a/src/config.json
+++ b/src/config.json
@@ -1058,6 +1058,17 @@
"taro": true,
"author": "lzz"
},
+ {
+ "version": "2.0.0",
+ "name": "Tour",
+ "type": "component",
+ "cName": "引导",
+ "desc": "用于引导用户了解产品功能的气泡组件。自 4.0 版本开始提供该组件。",
+ "sort": 24,
+ "show": true,
+ "taro": true,
+ "author": "swag~jun"
+ },
{
"version": "2.0.0",
"name": "Video",
diff --git a/src/locales/base.ts b/src/locales/base.ts
index d4dbb3b93e..928595adf0 100644
--- a/src/locales/base.ts
+++ b/src/locales/base.ts
@@ -126,6 +126,11 @@ export interface BaseLang {
refreshingText: string
completeText: string
}
+ tour: {
+ prevStepText: string
+ completeText: string
+ nextStepText: string
+ }
watermark: {
errorCanvasTips: string
}
diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts
index 1b922fc521..f07ffe0a15 100644
--- a/src/locales/en-US.ts
+++ b/src/locales/en-US.ts
@@ -125,6 +125,11 @@ const enUS: BaseLang = {
refreshingText: 'Loading...',
completeText: 'Refresh successful',
},
+ tour: {
+ prevStepText: 'Previous',
+ completeText: 'Finish',
+ nextStepText: 'Next step',
+ },
watermark: {
errorCanvasTips: 'Canvas is not supported in the current environment',
},
diff --git a/src/locales/id-ID.ts b/src/locales/id-ID.ts
index cdb2d9628c..9a629d9f8e 100644
--- a/src/locales/id-ID.ts
+++ b/src/locales/id-ID.ts
@@ -126,6 +126,11 @@ const idID: BaseLang = {
refreshingText: 'Memuat...',
completeText: 'Penyegaran berhasil',
},
+ tour: {
+ prevStepText: 'Sebelumnya',
+ completeText: 'Menyelesaikan',
+ nextStepText: 'Langkah berikutnya',
+ },
watermark: {
errorCanvasTips: 'Canvas is not supported in the current environment',
},
diff --git a/src/locales/tr-TR.ts b/src/locales/tr-TR.ts
index 1d475bb3b0..78f97d4428 100644
--- a/src/locales/tr-TR.ts
+++ b/src/locales/tr-TR.ts
@@ -134,6 +134,11 @@ const trTR: BaseLang = {
refreshingText: 'Yükleniyor...',
completeText: 'Yenileme başarılı',
},
+ tour: {
+ prevStepText: 'Sonraki adım',
+ completeText: 'Sona ermek',
+ nextStepText: 'Sonraki adım',
+ },
watermark: {
errorCanvasTips: 'Geçerli ortam Canvası desteklemiyor',
},
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
index 414a0a9309..1e6c45458f 100644
--- a/src/locales/zh-CN.ts
+++ b/src/locales/zh-CN.ts
@@ -126,6 +126,11 @@ const zhCN: BaseLang = {
refreshingText: '加载中...',
completeText: '刷新成功',
},
+ tour: {
+ prevStepText: '上一步',
+ completeText: '完成',
+ nextStepText: '下一步',
+ },
watermark: {
errorCanvasTips: '当前环境不支持Canvas',
},
diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts
index 647ac83ddf..083648a6d4 100644
--- a/src/locales/zh-TW.ts
+++ b/src/locales/zh-TW.ts
@@ -124,6 +124,11 @@ const zhTW: BaseLang = {
refreshingText: '加載中...',
completeText: '刷新成功',
},
+ tour: {
+ prevStepText: '上一步',
+ completeText: '完成',
+ nextStepText: '下一步',
+ },
watermark: {
errorCanvasTips: '當前環境不支持Canvas',
},
diff --git a/src/locales/zh-UG.ts b/src/locales/zh-UG.ts
index b1f51def6c..0e7cb7c54d 100644
--- a/src/locales/zh-UG.ts
+++ b/src/locales/zh-UG.ts
@@ -124,6 +124,11 @@ const zhUG: BaseLang = {
refreshingText: 'يېڭىلىنىۋاتىدۇ...',
completeText: 'تامام',
},
+ tour: {
+ prevStepText: 'ئالدىنقى',
+ completeText: 'تامام',
+ nextStepText: 'كېيىنكى قەدەم',
+ },
watermark: {
errorCanvasTips: 'Canvas نى قوللىمايدۇ',
},
diff --git a/src/packages/popover/doc.en-US.md b/src/packages/popover/doc.en-US.md
index 9e31f8f868..cbf318746e 100644
--- a/src/packages/popover/doc.en-US.md
+++ b/src/packages/popover/doc.en-US.md
@@ -498,6 +498,7 @@ export default App
| visible | whether to show | `boolean` | `false` |
| location | The pop-up position, the specific parameter values inside can refer to the above position customization example | `string` | `bottom` |
| offset | the offset of the occurrence position | `string[]` \| `number[]` | `[0, 12]` |
+| arrowOffset | the offset of the arrow | `number` | `0` |
| showArrow | whether to show small arrows | `boolean` | `true` |
| closeOnActionClick | Whether to close when clicking action | `boolean` | `true` |
| closeOnOutsideClick | Whether to close when clicking outside | `boolean` | `true` |
diff --git a/src/packages/popover/doc.md b/src/packages/popover/doc.md
index c089b84bac..6cb5d0f8fe 100644
--- a/src/packages/popover/doc.md
+++ b/src/packages/popover/doc.md
@@ -503,6 +503,7 @@ export default App
| visible | 是否展示气泡弹出层 | `boolean` | `false` |
| location | 弹出位置,里面具体的参数值可以参考上面的位置自定义例子 | `string` | `bottom` |
| offset | 出现位置的偏移量 | `string[]` \| `number[]` | `[0, 12]` |
+| arrowOffset | 小箭头的偏移量 | `number` | `0` |
| showArrow | 是否显示小箭头 | `boolean` | `true` |
| closeOnActionClick | 是否在点击选项后关闭 | `boolean` | `true` |
| closeOnOutsideClick | 是否在点击外部元素后关闭菜单 | `boolean` | `true` |
diff --git a/src/packages/popover/popover.taro.tsx b/src/packages/popover/popover.taro.tsx
index 259799f66d..620e711fac 100644
--- a/src/packages/popover/popover.taro.tsx
+++ b/src/packages/popover/popover.taro.tsx
@@ -39,6 +39,7 @@ export interface PopoverProps extends PopupProps {
location: PopoverLocation | string
visible: boolean
offset: string[] | number[]
+ arrowOffset: number
targetId: string
showArrow: boolean
closeOnOutsideClick: boolean
@@ -56,6 +57,7 @@ const defaultProps = {
location: 'bottom',
visible: false,
offset: [0, 12],
+ arrowOffset: 0,
targetId: '',
className: '',
showArrow: true,
@@ -83,6 +85,7 @@ export const Popover: FunctionComponent<
closeOnActionClick,
className,
showArrow,
+ arrowOffset,
style,
onClick,
onOpen,
@@ -252,6 +255,40 @@ export const Popover: FunctionComponent<
return styles
}
+ const popoverArrowStyle = () => {
+ const styles: CSSProperties = {}
+ const direction = location.split('-')[0]
+ const skew = location.split('-')[1]
+ const base = 16
+
+ 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 handleSelect = (item: List, index: number) => {
if (!item.disabled) {
onSelect && onSelect(item, index)
@@ -289,7 +326,9 @@ export const Popover: FunctionComponent<
{...rest}
>
- {showArrow &&
}
+ {showArrow && (
+
+ )}
{Array.isArray(children) ? children[1] : ''}
{list.map((item, index) => {
return (
@@ -311,7 +350,7 @@ export const Popover: FunctionComponent<
})}
- {showPopup && (
+ {showPopup && closeOnOutsideClick && (
{
+ const styles: CSSProperties = {}
+ const direction = location.split('-')[0]
+ const skew = location.split('-')[1]
+ const base = 16
+
+ 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 handleSelect = (item: List, index: number) => {
if (!item.disabled) {
onSelect && onSelect(item, index)
@@ -271,7 +308,9 @@ export const Popover: FunctionComponent<
{...rest}
>
- {showArrow &&
}
+ {showArrow && (
+
+ )}
{Array.isArray(children) ? children[1] : ''}
{list.map((item, index) => {
return (
diff --git a/src/packages/switch/switch.taro.tsx b/src/packages/switch/switch.taro.tsx
index 1634b499f3..042d746917 100644
--- a/src/packages/switch/switch.taro.tsx
+++ b/src/packages/switch/switch.taro.tsx
@@ -27,6 +27,7 @@ export const Switch: FunctionComponent
> = (props) => {
className,
style,
onChange,
+ ...rest
} = {
...defaultProps,
...props,
@@ -50,7 +51,12 @@ export const Switch: FunctionComponent> = (props) => {
setValue(!value)
}
return (
- onClick(e)} style={style}>
+
onClick(e)}
+ style={style}
+ {...rest}
+ >
{!value &&
}
{activeText && (
diff --git a/src/packages/switch/switch.tsx b/src/packages/switch/switch.tsx
index 1634b499f3..042d746917 100644
--- a/src/packages/switch/switch.tsx
+++ b/src/packages/switch/switch.tsx
@@ -27,6 +27,7 @@ export const Switch: FunctionComponent
> = (props) => {
className,
style,
onChange,
+ ...rest
} = {
...defaultProps,
...props,
@@ -50,7 +51,12 @@ export const Switch: FunctionComponent> = (props) => {
setValue(!value)
}
return (
- onClick(e)} style={style}>
+
onClick(e)}
+ style={style}
+ {...rest}
+ >
{!value &&
}
{activeText && (
diff --git a/src/packages/tabbaritem/tabbaritem.taro.tsx b/src/packages/tabbaritem/tabbaritem.taro.tsx
index 4a47b26569..31228a4a51 100644
--- a/src/packages/tabbaritem/tabbaritem.taro.tsx
+++ b/src/packages/tabbaritem/tabbaritem.taro.tsx
@@ -49,6 +49,7 @@ export const TabbarItem: FunctionComponent
> = (
top,
right,
handleClick,
+ ...rest
} = {
...defaultProps,
...props,
@@ -79,6 +80,7 @@ export const TabbarItem: FunctionComponent> = (
...style,
}}
onClick={() => handleClick(index)}
+ {...rest}
>
{icon ? (
<>
diff --git a/src/packages/tabbaritem/tabbaritem.tsx b/src/packages/tabbaritem/tabbaritem.tsx
index 503d091fa6..dc31026494 100644
--- a/src/packages/tabbaritem/tabbaritem.tsx
+++ b/src/packages/tabbaritem/tabbaritem.tsx
@@ -49,6 +49,7 @@ export const TabbarItem: FunctionComponent> = (
top,
right,
handleClick,
+ ...rest
} = {
...defaultProps,
...props,
@@ -79,6 +80,7 @@ export const TabbarItem: FunctionComponent> = (
...style,
}}
onClick={() => handleClick(index)}
+ {...rest}
>
{icon ? (
<>
diff --git a/src/packages/tour/__test__/tour.spec.tsx b/src/packages/tour/__test__/tour.spec.tsx
new file mode 100644
index 0000000000..210dbde5dd
--- /dev/null
+++ b/src/packages/tour/__test__/tour.spec.tsx
@@ -0,0 +1,154 @@
+import * as React from 'react'
+import { render, fireEvent, waitFor } from '@testing-library/react'
+import '@testing-library/jest-dom'
+
+import { Tour } from '../tour'
+
+const steps = [
+ {
+ content: '70+ 高质量组件,覆盖移动端主流场景',
+ target: 'target',
+ },
+]
+
+const steps2 = [
+ {
+ content: '支持一套代码同时开发多端小程序+H5',
+ target: 'target2',
+ popoverOffset: [40, 12],
+ arrowOffset: -36,
+ },
+]
+
+const steps3 = [
+ {
+ content: '70+ 高质量组件,覆盖移动端主流场景',
+ target: 'target3',
+ },
+]
+
+const steps4 = [
+ {
+ content: '70+ 高质量组件,覆盖移动端主流场景',
+ target: 'target4',
+ },
+ {
+ content: '支持一套代码同时开发多端小程序+H5',
+ target: 'target5',
+ },
+ {
+ content: '基于京东APP 10.0 视觉规范',
+ target: 'target6',
+ location: 'top-end',
+ },
+ {
+ content: '支持定制主题,内置 700+ 个主题变量',
+ target: 'target7',
+ location: 'top-end',
+ },
+]
+
+test('base render', () => {
+ const root = document.createElement('div')
+ root.id = 'target'
+ const { container } = render(
+
+ )
+ expect(container.querySelector('.nut-popover')).toBeTruthy()
+ expect(
+ container.querySelector('.nut-popover-content--bottom-end')
+ ).toBeTruthy()
+})
+
+test('custom style', () => {
+ const root = document.createElement('div')
+ root.id = 'target'
+ const { container } = render(
+
+ )
+ const tourComponent = container.querySelectorAll('.nut-tour')[0]
+ expect(tourComponent).toHaveStyle({
+ '--nutui-popover-content-background-color': 'rgb(255, 0, 0)',
+ })
+})
+
+test('custom offset', () => {
+ const root = document.createElement('div')
+ root.id = 'target2'
+ const { container } = render(
+
+ )
+ const tourArrow = container.querySelectorAll('.nut-popover-arrow')[0]
+ expect(tourArrow).toHaveStyle({ right: '52px' })
+})
+
+test('slot render', () => {
+ const root = document.createElement('div')
+ root.id = 'target3'
+ const { container } = render(
+
+
+
+ )
+ const tourArrow = container.querySelectorAll('.nut-popover-content-group')[0]
+ expect(tourArrow).toHaveTextContent('nutui 4.x 即将发布,敬请期待')
+})
+
+test('steps render', async () => {
+ const root = document.createElement('div')
+ root.id = 'target4'
+ const root1 = document.createElement('div')
+ root1.id = 'target5'
+ const { container } = render(
+
+ )
+ const btn = container.querySelectorAll(
+ '.nut-tour-content-bottom-operate-btn'
+ )[0]
+ expect(btn).toBeTruthy()
+ fireEvent.click(btn)
+
+ await waitFor(() => {
+ const btn2 = container.querySelectorAll(
+ '.nut-tour-content-bottom-operate-btn'
+ )
+ expect(btn2.length).toBe(2)
+ })
+})
diff --git a/src/packages/tour/demo.scss b/src/packages/tour/demo.scss
new file mode 100644
index 0000000000..f42e9e2cc0
--- /dev/null
+++ b/src/packages/tour/demo.scss
@@ -0,0 +1,62 @@
+.nut-custom-tour {
+ .nut-popover-content {
+ width: auto !important;
+ }
+}
+.nut-customword-tour {
+ .nut-tour-content-inner {
+ width: max-content;
+ }
+}
+.index-header {
+ display: flex;
+ 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-customstyle-tour {
+ .nut-tour-mask {
+ border-radius: 50%;
+ }
+}
+.tour-demo-img {
+ img {
+ width: 20px;
+ height: 20px;
+ margin-right: 10px;
+ }
+}
+.tour-demo-custom-content {
+ padding: 8px;
+ display: flex;
+ width: max-content;
+ align-items: center;
+ .nut-divider {
+ border-color: #fff;
+ }
+}
+.tour-demo {
+ .nut-tabbar-item_icon-box_nav-word {
+ font-size: 14px;
+ }
+}
diff --git a/src/packages/tour/demo.taro.tsx b/src/packages/tour/demo.taro.tsx
new file mode 100644
index 0000000000..424dab7c4a
--- /dev/null
+++ b/src/packages/tour/demo.taro.tsx
@@ -0,0 +1,308 @@
+import React, { useState } from 'react'
+import Taro from '@tarojs/taro'
+import { useTranslate } from '@/sites/assets/locale/taro'
+import {
+ Cell,
+ Switch,
+ Divider,
+ Tabbar,
+ Tour,
+} from '@/packages/nutui.react.taro'
+import Header from '@/sites/components/header'
+import '@/packages/tour/demo.scss'
+
+interface T {
+ title1: string
+ title2: string
+ title3: string
+ title4: string
+ title5: string
+ clickTry: string
+ stepContent1: string
+ stepContent2: string
+ stepContent3: string
+ stepContent4: string
+ customContent: string
+ btnContent: string
+ tabTitle1: string
+ tabTitle2: string
+ tabTitle3: string
+ tabTitle4: string
+}
+const TourDemo = () => {
+ const [translated] = useTranslate({
+ 'zh-CN': {
+ title1: '基础用法',
+ title2: '自定义样式',
+ title3: '设置偏移量',
+ title4: '自定义内容',
+ title5: '步骤引导',
+ clickTry: '点击试试',
+ stepContent1: '70+ 高质量组件,覆盖移动端主流场景',
+ stepContent2: '支持一套代码同时开发多端小程序+H5',
+ stepContent3: '基于京东APP 10.0 视觉规范',
+ stepContent4: '支持定制主题,内置 700+ 个主题变量',
+ customContent: 'nutui-react 2.x 已经发布',
+ btnContent: '知道了',
+ tabTitle1: '首页',
+ tabTitle2: '分类',
+ tabTitle3: '购物车',
+ tabTitle4: '我的',
+ },
+ 'en-US': {
+ title1: 'Basic Usage',
+ title2: 'Custom Style',
+ title3: 'Custom Offset',
+ title4: 'Custom Content',
+ title5: 'Steps',
+ clickTry: 'click to try',
+ stepContent1: '70+ high-quality components',
+ stepContent2: 'Support a set of codes to develop',
+ stepContent3: 'Based on JD APP 10.0',
+ stepContent4: 'Support custom theme, built-in 700+ theme variables',
+ customContent: 'nutui-react 2.x is already released',
+ btnContent: 'knew',
+ tabTitle1: 'page',
+ tabTitle2: 'sort',
+ tabTitle3: 'cart',
+ tabTitle4: 'mine',
+ },
+ })
+
+ const [showTour, setShowTour] = useState(false)
+ const [showTour1, setShowTour1] = useState(false)
+ const [showTour2, setShowTour2] = useState(false)
+ const [showTour3, setShowTour3] = useState(false)
+ const [showTour4, setShowTour4] = useState(false)
+ const steps = [
+ {
+ content: translated.stepContent1,
+ target: 'target',
+ },
+ ]
+
+ const steps1 = [
+ {
+ content: translated.stepContent1,
+ target: 'target1',
+ },
+ ]
+
+ const steps2 = [
+ {
+ content: translated.stepContent2,
+ target: 'target2',
+ popoverOffset: [40, 12],
+ arrowOffset: -36,
+ },
+ ]
+
+ const steps3 = [
+ {
+ target: 'target3',
+ },
+ ]
+
+ const steps4 = [
+ {
+ content: translated.stepContent1,
+ target: 'target4',
+ },
+ {
+ content: translated.stepContent2,
+ target: 'target5',
+ },
+ {
+ content: translated.stepContent3,
+ target: 'target6',
+ location: 'top-end',
+ },
+ {
+ content: translated.stepContent4,
+ target: 'target7',
+ location: 'top-end',
+ },
+ ]
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const closeTour1 = () => {
+ setShowTour1(false)
+ }
+
+ const closeTour2 = () => {
+ setShowTour2(false)
+ }
+
+ const closeTour3 = () => {
+ setShowTour3(false)
+ }
+
+ const closeTour4 = () => {
+ setShowTour4(false)
+ }
+
+ return (
+ <>
+
+
+
{translated.title1}
+
{
+ setShowTour(true)
+ }}
+ />
+ }
+ />
+
+
+ {translated.title2}
+ {
+ setShowTour1(true)
+ }}
+ />
+ }
+ />
+
+
+ {translated.title3}
+ {
+ setShowTour2(true)
+ }}
+ extra={
+
+ }
+ />
+
+
+ {translated.title4}
+ {
+ setShowTour3(true)
+ }}
+ />
+ }
+ />
+
+
+ {translated.customContent}
+
+ {
+ setShowTour3(false)
+ }}
+ >
+ {translated.btnContent}
+
+
+
+
+ {translated.title5}
+ {
+ setShowTour4(true)
+ }}
+ />
+
+
+
+
+
+
+
+ | | | | |
+ >
+ )
+}
+
+export default TourDemo
diff --git a/src/packages/tour/demo.tsx b/src/packages/tour/demo.tsx
new file mode 100644
index 0000000000..2c1c2ec8df
--- /dev/null
+++ b/src/packages/tour/demo.tsx
@@ -0,0 +1,304 @@
+import React, { useState } from 'react'
+import { useTranslate } from '../../sites/assets/locale'
+import Cell from '@/packages/cell'
+import Switch from '@/packages/switch'
+import Divider from '@/packages/divider'
+import Tabbar from '@/packages/tabbar'
+import { Tour } from './tour'
+
+import './demo.scss'
+
+interface T {
+ title1: string
+ title2: string
+ title3: string
+ title4: string
+ title5: string
+ clickTry: string
+ stepContent1: string
+ stepContent2: string
+ stepContent3: string
+ stepContent4: string
+ customContent: string
+ btnContent: string
+ tabTitle1: string
+ tabTitle2: string
+ tabTitle3: string
+ tabTitle4: string
+}
+const TourDemo = () => {
+ const [translated] = useTranslate({
+ 'zh-CN': {
+ title1: '基础用法',
+ title2: '自定义样式',
+ title3: '设置偏移量',
+ title4: '自定义内容',
+ title5: '步骤引导',
+ clickTry: '点击试试',
+ stepContent1: '70+ 高质量组件,覆盖移动端主流场景',
+ stepContent2: '支持一套代码同时开发多端小程序+H5',
+ stepContent3: '基于京东APP 10.0 视觉规范',
+ stepContent4: '支持定制主题,内置 700+ 个主题变量',
+ customContent: 'nutui-react 2.x 已经发布',
+ btnContent: '知道了',
+ tabTitle1: '首页',
+ tabTitle2: '分类',
+ tabTitle3: '购物车',
+ tabTitle4: '我的',
+ },
+ 'en-US': {
+ title1: 'Basic Usage',
+ title2: 'Custom Style',
+ title3: 'Custom Offset',
+ title4: 'Custom Content',
+ title5: 'Steps',
+ clickTry: 'click to try',
+ stepContent1: '70+ high-quality components',
+ stepContent2: 'Support a set of codes to develop',
+ stepContent3: 'Based on JD APP 10.0',
+ stepContent4: 'Support custom theme, built-in 700+ theme variables',
+ customContent: 'nutui-react 2.x is already released',
+ btnContent: 'knew',
+ tabTitle1: 'page',
+ tabTitle2: 'sort',
+ tabTitle3: 'cart',
+ tabTitle4: 'mine',
+ },
+ })
+
+ const [showTour, setShowTour] = useState(false)
+ const [showTour1, setShowTour1] = useState(false)
+ const [showTour2, setShowTour2] = useState(false)
+ const [showTour3, setShowTour3] = useState(false)
+ const [showTour4, setShowTour4] = useState(true)
+ const steps = [
+ {
+ content: translated.stepContent1,
+ target: 'target',
+ },
+ ]
+
+ const steps1 = [
+ {
+ content: translated.stepContent1,
+ target: 'target1',
+ },
+ ]
+
+ const steps2 = [
+ {
+ content: translated.stepContent2,
+ target: 'target2',
+ popoverOffset: [40, 12],
+ arrowOffset: -36,
+ },
+ ]
+
+ const steps3 = [
+ {
+ target: 'target3',
+ },
+ ]
+
+ const steps4 = [
+ {
+ content: translated.stepContent1,
+ target: 'target4',
+ },
+ {
+ content: translated.stepContent2,
+ target: 'target5',
+ },
+ {
+ content: translated.stepContent3,
+ target: 'target6',
+ location: 'top-end',
+ },
+ {
+ content: translated.stepContent4,
+ target: 'target7',
+ location: 'top-end',
+ },
+ ]
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const closeTour1 = () => {
+ setShowTour1(false)
+ }
+
+ const closeTour2 = () => {
+ setShowTour2(false)
+ }
+
+ const closeTour3 = () => {
+ setShowTour3(false)
+ }
+
+ const closeTour4 = () => {
+ setShowTour4(false)
+ }
+
+ return (
+ <>
+
+
{translated.title1}
+
{
+ setShowTour(true)
+ }}
+ />
+ }
+ />
+
+
+ {translated.title2}
+ {
+ setShowTour1(true)
+ }}
+ />
+ }
+ />
+
+
+ {translated.title3}
+ {
+ setShowTour2(true)
+ }}
+ extra={
+
+ }
+ />
+
+
+ {translated.title4}
+ {
+ setShowTour3(true)
+ }}
+ />
+ }
+ />
+
+
+ {translated.customContent}
+
+ {
+ setShowTour3(false)
+ }}
+ >
+ {translated.btnContent}
+
+
+
+
+ {translated.title5}
+ {
+ setShowTour4(true)
+ }}
+ />
+
+
+
+
+
+
+
+ | | | | |
+ >
+ )
+}
+
+export default TourDemo
diff --git a/src/packages/tour/doc.en-US.md b/src/packages/tour/doc.en-US.md
new file mode 100644
index 0000000000..43985ef45a
--- /dev/null
+++ b/src/packages/tour/doc.en-US.md
@@ -0,0 +1,386 @@
+# Tour
+
+### Intro
+
+A bubble component used to guide the user through the product's capabilities.
+
+## Install
+
+```tsx
+import { Tour } from '@nutui/nutui-react';
+```
+
+## Demo
+### Basic Usage
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour } from '@nutui/nutui-react';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '70+ high-quality components',
+ target: 'target',
+ },
+ ]
+
+ return (
+ <>
+ {
+ setShowTour(true)
+ }}
+ />
+ }
+ />
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### Custom Style
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour } from '@nutui/nutui-react';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '70+ high-quality components',
+ target: 'target',
+ },
+ ]
+
+ return (
+ <>
+ {
+ setShowTour(true)
+ }}
+ />
+ }
+ />
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### Custom Offset
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour } from '@nutui/nutui-react';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: 'Support a set of codes to develop',
+ target: 'target',
+ popoverOffset: [40, 12],
+ arrowOffset: -36,
+ },
+ ]
+
+ return (
+ <>
+
+
+
+
+ | | |
+ }
+ />
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### Custom Content
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour, Divider } from '@nutui/nutui-react';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ target: 'target',
+ },
+ ]
+
+ return (
+ <>
+
{
+ setShowTour3(true)
+ }}
+ />
+ }
+ />
+
+
+ nutui-react 2.x is already released
+
+ {
+ setShowTour(false)
+ }}
+ >
+ knew
+
+
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### Steps
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Tour, Tabbar } from '@nutui/nutui-react';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '70+ high-quality components',
+ target: 'target4',
+ },
+ {
+ content: 'Support a set of codes to develop',
+ target: 'target5',
+ },
+ {
+ content: 'Based on JD APP 10.0',
+ target: 'target6',
+ location: 'top-end',
+ },
+ {
+ content: 'Support custom theme, built-in 700+ theme variables',
+ target: 'target7',
+ location: 'top-end',
+ },
+ ]
+
+ return (
+ <>
+ {
+ setShowTour(true)
+ }}
+ />
+
+
+
+
+
+
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+
+## Tour
+
+### Props
+
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| visible | Whether to display the boot eject layer | `boolean` | `false` |
+| type | Tour type | `step` \| `tile` | `step` |
+| list | Boot Step Content | `ListOptions[]` | `-` |
+| offset | The offset of the hollow mask relative to the target element | `number[]` | `[8, 10]` |
+| location | Location of popover[location ](https://nutui.jd.com/h5/react/2x/#/zh-CN/component/popover) | `string` | `bottom` |
+| next | Next step text | `ReactNode` | `''` |
+| prev | Next step text | `ReactNode` | `''` |
+| complete | Complete text | `ReactNode` | `''` |
+| mask | Whether to display cutout mask | `boolean` | `true` |
+| maskWidth | Width of hollow mask | `number` \| `string` | `''` |
+| maskHeight | Hollow mask height | `number` \| `string` | `''` |
+| closeOnOverlayClick | Whether to close when clicking overlay,[closeOnClickOverlay](https://nutui.jd.com/h5/react/2x/#/zh-CN/component/popover) | `boolean` | `true` |
+| showPrev | Whether to show prev button | `boolean` | `true` |
+| title | Whether to show title bar | `ReactNode` | `''` |
+| onClose | Emit when popover close | `(e: MouseEvent) => void` | `-` |
+| onChange | Emit when step change | `(value: number) => void` | `-` |
+
+### ListOptions
+
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| target | target dom | `Element` \| `string` | `-` |
+| content | popover content | `string` | `-` |
+| location | Location of popover | `string` | `-` |
+| popoverOffset | Offset of popopver | `number[]` | `-` |
+| arrowOffset | Offset of arrow | `number` | `-` |
+
+## Theming
+
+### CSS Variables
+
+The component provides the following CSS variables, which can be used to customize styles. Please refer to [ConfigProvider component](#/en-US/component/configprovider).
+
+| Name | Description | Default |
+| --- | --- | --- |
+| \--nutui-tour-mask-border-radius | The border-radius value of the mask layer | `10px` |
+| \--nutui-tour-content-min-width | The min-width value of the content area | `200px` |
+| \--nutui-tour-content-padding | The padding value of the content area | `10px 12px` |
+| \--nutui-tour-content-inner-margin | The margin value inside the content area | `10px 0px` |
+| \--nutui-tour-content-inner-font-size | The font-size value inside the content area | `14px` |
+| \--nutui-tour-content-bottom-margin-top | margin-top value at the bottom of the content area | `10px` |
+| \--nutui-tour-content-bottom-btn-margin-left | The margin-left value of the button at the bottom of the content area | `4px` |
+| \--nutui-tour-content-bottom-btn-padding | The padding value of the button at the bottom of the content area | `2px 4px` |
+| \--nutui-tour-content-bottom-btn-font-size | The font-size value of the button at the bottom of the content area | `12px` |
+| \--nutui-tour-content-bottom-btn-border-radius | The border-radius value of the button at the bottom of the content area | `4px` |
+
+
+
+
+
diff --git a/src/packages/tour/doc.md b/src/packages/tour/doc.md
new file mode 100644
index 0000000000..a373f0ba91
--- /dev/null
+++ b/src/packages/tour/doc.md
@@ -0,0 +1,386 @@
+# Tour 引导
+
+### 介绍
+
+用于引导用户了解产品功能的气泡组件。
+
+## 安装
+
+```tsx
+import { Tour } from '@nutui/nutui-react';
+```
+
+## 代码演示
+### 基础用法
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour } from '@nutui/nutui-react';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '70+ 高质量组件,覆盖移动端主流场景',
+ target: 'target',
+ },
+ ]
+
+ return (
+ <>
+ {
+ setShowTour(true)
+ }}
+ />
+ }
+ />
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### 自定义样式
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour } from '@nutui/nutui-react';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '70+ 高质量组件,覆盖移动端主流场景',
+ target: 'target',
+ },
+ ]
+
+ return (
+ <>
+ {
+ setShowTour(true)
+ }}
+ />
+ }
+ />
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### 设置偏移量
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour } from '@nutui/nutui-react';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '支持一套代码同时开发多端小程序+H5',
+ target: 'target',
+ popoverOffset: [40, 12],
+ arrowOffset: -36,
+ },
+ ]
+
+ return (
+ <>
+
+
+
+
+ | | | | |
+ }
+ />
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### 自定义内容
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour, Divider } from '@nutui/nutui-react';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ target: 'target',
+ },
+ ]
+
+ return (
+ <>
+
{
+ setShowTour3(true)
+ }}
+ />
+ }
+ />
+
+
+ nutui-react 2.x 已经发布
+
+ {
+ setShowTour(false)
+ }}
+ >
+ 知道了
+
+
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### 步骤引导
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Tour, Tabbar } from '@nutui/nutui-react';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '70+ 高质量组件,覆盖移动端主流场景',
+ target: 'target4',
+ },
+ {
+ content: '支持一套代码同时开发多端小程序+H5',
+ target: 'target5',
+ },
+ {
+ content: '基于京东APP 10.0 视觉规范',
+ target: 'target6',
+ location: 'top-end',
+ },
+ {
+ content: '支持定制主题,内置 700+ 个主题变量',
+ target: 'target7',
+ location: 'top-end',
+ },
+ ]
+
+ return (
+ <>
+ {
+ setShowTour(true)
+ }}
+ />
+
+
+
+
+
+
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+
+## Tour
+
+### Props
+
+
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| visible | 是否展示引导弹出层 | `boolean` | `false` |
+| type | 引导类型 | `step` \| `tile` | `step` |
+| list | 引导步骤内容 | `ListOptions[]` | `-` |
+| offset | 镂空遮罩相对于目标元素的偏移量 | `number[]` | `[8, 10]` |
+| location | 弹出层位置,同 Popopver 的[location 属性](https://nutui.jd.com/h5/react/2x/#/zh-CN/component/popover) | `string` | `bottom` |
+| next | 下一步按钮文案 | `ReactNode` | `''` |
+| prev | 上一步按钮文案 | `ReactNode` | `''` |
+| complete | 完成按钮文案 | `ReactNode` | `''` |
+| mask | 是否显示镂空遮罩 | `boolean` | `true` |
+| maskWidth | 镂空遮罩层宽度 | `number` \| `string` | `''` |
+| maskHeight | 镂空遮罩层高度 | `number` \| `string` | `''` |
+| closeOnOverlayClick | 是否在点击镂空遮罩层后关闭,同 Popopver 的[closeOnClickOverlay 属性](https://nutui.jd.com/h5/react/2x/#/zh-CN/component/popover) | `boolean` | `true` |
+| showPrev | 是否展示上一步按钮 | `boolean` | `true` |
+| title | 是否展示标题栏 | `ReactNode` | `''` |
+| onClose | 气泡层关闭时触发 | `(e: MouseEvent) => void` | `-` |
+| onChange | 切换步骤时触发 | `(value: number) => void` | `-` |
+
+### ListOptions
+
+
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| target | 目标对象 | `Element` \| `string` | `-` |
+| content | 气泡层内容 | `string` | `-` |
+| location | 弹出层位置 | `string` | `-` |
+| popoverOffset | 气泡层偏移量 | `number[]` | `-` |
+| arrowOffset | 小箭头的偏移量 | `number` | `-` |
+
+## 主题定制
+
+### 样式变量
+
+组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/component/configprovider)。
+
+| 名称 | 说明 | 默认值 |
+| --- | --- | --- |
+| \--nutui-tour-mask-border-radius | 遮罩层的border-radius值 | `10px` |
+| \--nutui-tour-content-min-width | 内容区的min-width值 | `200px` |
+| \--nutui-tour-content-padding | 内容区的padding值 | `10px 12px` |
+| \--nutui-tour-content-inner-margin | 内容区内部的margin值 | `10px 0px` |
+| \--nutui-tour-content-inner-font-size | 内容区内部的font-size值 | `14px` |
+| \--nutui-tour-content-bottom-margin-top | 内容区底部的margin-top值 | `10px` |
+| \--nutui-tour-content-bottom-btn-margin-left | 内容区底部按钮的margin-left值 | `4px` |
+| \--nutui-tour-content-bottom-btn-padding | 内容区底部按钮的padding值 | `2px 4px` |
+| \--nutui-tour-content-bottom-btn-font-size | 内容区底部按钮的font-size值 | `12px` |
+| \--nutui-tour-content-bottom-btn-border-radius | 内容区底部按钮的border-radius值 | `4px` |
+
+
+
+
+
diff --git a/src/packages/tour/doc.taro.md b/src/packages/tour/doc.taro.md
new file mode 100644
index 0000000000..a421b10430
--- /dev/null
+++ b/src/packages/tour/doc.taro.md
@@ -0,0 +1,386 @@
+# Tour 引导
+
+### 介绍
+
+用于引导用户了解产品功能的气泡组件。
+
+## 安装
+
+```tsx
+import { Tour } from '@nutui/nutui-react-taro';
+```
+
+## 代码演示
+### 基础用法
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour } from '@nutui/nutui-react-taro';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '70+ 高质量组件,覆盖移动端主流场景',
+ target: 'target',
+ },
+ ]
+
+ return (
+ <>
+ {
+ setShowTour(true)
+ }}
+ />
+ }
+ />
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### 自定义样式
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour } from '@nutui/nutui-react-taro';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '70+ 高质量组件,覆盖移动端主流场景',
+ target: 'target',
+ },
+ ]
+
+ return (
+ <>
+ {
+ setShowTour(true)
+ }}
+ />
+ }
+ />
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### 设置偏移量
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour } from '@nutui/nutui-react-taro';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '支持一套代码同时开发多端小程序+H5',
+ target: 'target',
+ popoverOffset: [40, 12],
+ arrowOffset: -36,
+ },
+ ]
+
+ return (
+ <>
+
+
+
+
+ | | | | |
+ }
+ />
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### 自定义内容
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Switch, Tour, Divider } from '@nutui/nutui-react-taro';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ target: 'target',
+ },
+ ]
+
+ return (
+ <>
+ {
+ setShowTour3(true)
+ }}
+ />
+ }
+ />
+
+
+ nutui 4.x 即将发布,敬请期待
+
+ {
+ setShowTour(false)
+ }}
+ >
+ 知道了
+
+
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+### 步骤引导
+
+:::demo
+
+```tsx
+import React, { useState } from "react";
+import { Cell, Tour, Tabbar } from '@nutui/nutui-react-taro';
+
+const App = () => {
+ const [showTour, setShowTour] = useState(false)
+
+ const closeTour = () => {
+ setShowTour(false)
+ }
+
+ const steps = [
+ {
+ content: '70+ 高质量组件,覆盖移动端主流场景',
+ target: 'target4',
+ },
+ {
+ content: '支持一套代码同时开发多端小程序+H5',
+ target: 'target5',
+ },
+ {
+ content: '基于京东APP 10.0 视觉规范',
+ target: 'target6',
+ location: 'top-end',
+ },
+ {
+ content: '支持定制主题,内置 700+ 个主题变量',
+ target: 'target7',
+ location: 'top-end',
+ },
+ ]
+
+ return (
+ <>
+ {
+ setShowTour(true)
+ }}
+ />
+
+
+
+
+
+
+
+ >
+ )
+}
+export default App;
+```
+
+:::
+
+
+## Tour
+
+### Props
+
+
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| visible | 是否展示引导弹出层 | `boolean` | `false` |
+| type | 引导类型 | `step` \| `tile` | `step` |
+| list | 引导步骤内容 | `ListOptions[]` | `-` |
+| offset | 镂空遮罩相对于目标元素的偏移量 | `number[]` | `[8, 10]` |
+| location | 弹出层位置,同 Popopver 的[location 属性](https://nutui.jd.com/h5/react/2x/#/zh-CN/component/popover) | `string` | `bottom` |
+| next | 下一步按钮文案 | `ReactNode` | `''` |
+| prev | 上一步按钮文案 | `ReactNode` | `''` |
+| complete | 完成按钮文案 | `ReactNode` | `''` |
+| mask | 是否显示镂空遮罩 | `boolean` | `true` |
+| maskWidth | 镂空遮罩层宽度 | `number` \| `string` | `''` |
+| maskHeight | 镂空遮罩层高度 | `number` \| `string` | `''` |
+| closeOnOverlayClick | 是否在点击镂空遮罩层后关闭,同 Popopver 的[closeOnClickOverlay 属性](https://nutui.jd.com/h5/react/2x/#/zh-CN/component/popover) | `boolean` | `true` |
+| showPrev | 是否展示上一步按钮 | `boolean` | `true` |
+| title | 是否展示标题栏 | `ReactNode` | `''` |
+| onClose | 气泡层关闭时触发 | `(e: MouseEvent) => void` | `-` |
+| onChange | 切换步骤时触发 | `(value: number) => void` | `-` |
+
+### ListOptions
+
+
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| target | 目标对象 | `Element` \| `string` | `-` |
+| content | 气泡层内容 | `string` | `-` |
+| location | 弹出层位置 | `string` | `-` |
+| popoverOffset | 气泡层偏移量 | `number[]` | `-` |
+| arrowOffset | 小箭头的偏移量 | `number` | `-` |
+
+## 主题定制
+
+### 样式变量
+
+组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/component/configprovider)。
+
+| 名称 | 说明 | 默认值 |
+| --- | --- | --- |
+| \--nutui-tour-mask-border-radius | 遮罩层的border-radius值 | `10px` |
+| \--nutui-tour-content-min-width | 内容区的min-width值 | `200px` |
+| \--nutui-tour-content-padding | 内容区的padding值 | `10px 12px` |
+| \--nutui-tour-content-inner-margin | 内容区内部的margin值 | `10px 0px` |
+| \--nutui-tour-content-inner-font-size | 内容区内部的font-size值 | `14px` |
+| \--nutui-tour-content-bottom-margin-top | 内容区底部的margin-top值 | `10px` |
+| \--nutui-tour-content-bottom-btn-margin-left | 内容区底部按钮的margin-left值 | `4px` |
+| \--nutui-tour-content-bottom-btn-padding | 内容区底部按钮的padding值 | `2px 4px` |
+| \--nutui-tour-content-bottom-btn-font-size | 内容区底部按钮的font-size值 | `12px` |
+| \--nutui-tour-content-bottom-btn-border-radius | 内容区底部按钮的border-radius值 | `4px` |
+
+
+
+
+
diff --git a/src/packages/tour/index.taro.ts b/src/packages/tour/index.taro.ts
new file mode 100644
index 0000000000..23c4fccbe4
--- /dev/null
+++ b/src/packages/tour/index.taro.ts
@@ -0,0 +1,5 @@
+import { Tour } from './tour.taro'
+
+export type { ListOptions, TourType, TourProps } from './tour.taro'
+
+export default Tour
diff --git a/src/packages/tour/index.ts b/src/packages/tour/index.ts
new file mode 100644
index 0000000000..4927f6746a
--- /dev/null
+++ b/src/packages/tour/index.ts
@@ -0,0 +1,5 @@
+import { Tour } from './tour'
+
+export type { ListOptions, TourType, TourProps } from './tour'
+
+export default Tour
diff --git a/src/packages/tour/tour.scss b/src/packages/tour/tour.scss
new file mode 100644
index 0000000000..fdc83a5e80
--- /dev/null
+++ b/src/packages/tour/tour.scss
@@ -0,0 +1,78 @@
+.nut-tour {
+ &-mask {
+ position: fixed;
+ box-shadow: 0px 0px 0px 150vh rgba(0, 0, 0, 0.5);
+ border-radius: $tour-mask-border-radius;
+ z-index: 999;
+ &-none {
+ box-shadow: none;
+ }
+
+ &-hidden {
+ opacity: 0;
+ }
+ }
+
+ &-content {
+ display: block;
+ padding: $tour-content-padding;
+ min-width: $tour-content-min-width;
+ box-sizing: content-box;
+
+ &-top {
+ display: block;
+ text-align: right;
+
+ &-close {
+ --nut-icon-width: 10px;
+ --nut-icon-height: 10px;
+ }
+ }
+
+ &-inner {
+ margin: $tour-content-inner-margin;
+ font-size: $tour-content-inner-font-size;
+ white-space: nowrap;
+ }
+
+ &-bottom {
+ margin-top: $tour-content-bottom-margin-top;
+ display: flex;
+ justify-content: space-between;
+ &-operate {
+ display: flex;
+ justify-content: flex-end;
+ &-btn {
+ display: inline-block;
+ border: 1px solid $disable-color;
+ margin-left: $tour-content-bottom-btn-margin-left;
+ padding: $tour-content-bottom-btn-padding;
+ font-size: $tour-content-bottom-btn-font-size;
+ border-radius: $tour-content-bottom-btn-border-radius;
+ color: $text-color;
+ &.active {
+ color: #fff;
+ border: 0;
+ background: $primary-color;
+ }
+ }
+ }
+ }
+
+ &-tile {
+ .nut-tour-content-inner {
+ margin: 0;
+ }
+ }
+ }
+
+ &-masked {
+ position: fixed;
+ width: 100vh;
+ height: 100vh;
+ z-index: 1000;
+ top: 0;
+ left: 0;
+ background: transparent;
+ }
+}
diff --git a/src/packages/tour/tour.taro.tsx b/src/packages/tour/tour.taro.tsx
new file mode 100644
index 0000000000..8833617a98
--- /dev/null
+++ b/src/packages/tour/tour.taro.tsx
@@ -0,0 +1,268 @@
+import React, { useState, useEffect, ReactNode, FunctionComponent } from 'react'
+import type { MouseEvent } from 'react'
+import { Close } from '@nutui/icons-react-taro'
+import classNames from 'classnames'
+import Popover, { PopoverLocation } from '@/packages/popover/index.taro'
+import { getTaroRectById } from '@/utils/use-taro-rect'
+import { BasicComponent, ComponentDefaults } from '@/utils/typings'
+import { useConfig } from '@/packages/configprovider'
+
+export interface ListOptions {
+ target: Element | string
+ content?: string
+ location?: string
+ popoverOffset?: number[]
+ arrowOffset?: number
+}
+
+export type TourType = 'step' | 'tile'
+
+export interface TourProps extends BasicComponent {
+ visible: boolean
+ type: TourType
+ location: PopoverLocation | string
+ mask: boolean
+ maskWidth: number | string
+ maskHeight: number | string
+ offset: number[]
+ list: ListOptions[]
+ title: ReactNode
+ next: ReactNode
+ prev: ReactNode
+ complete: ReactNode
+ showPrev: boolean
+ closeOnOverlayClick: boolean
+ onClose: (e: MouseEvent) => void
+ onChange: (value: number) => void
+}
+const defaultProps = {
+ ...ComponentDefaults,
+ visible: false,
+ type: 'step',
+ location: 'bottom',
+ mask: true,
+ maskWidth: '',
+ maskHeight: '',
+ offset: [8, 10],
+ title: '',
+ next: '',
+ prev: '',
+ complete: '',
+ showPrev: true,
+ closeOnOverlayClick: true,
+} as TourProps
+
+const classPrefix = 'nut-tour'
+export const Tour: FunctionComponent<
+ Partial &
+ Omit, 'title' | 'onChange'>
+> = (props) => {
+ const { locale } = useConfig()
+ const {
+ children,
+ className,
+ title,
+ closeOnOverlayClick,
+ showPrev,
+ list,
+ type,
+ location,
+ visible,
+ mask,
+ maskWidth,
+ maskHeight,
+ offset,
+ next,
+ prev,
+ complete,
+ onClose,
+ onChange,
+ ...rest
+ } = {
+ ...defaultProps,
+ ...props,
+ }
+
+ const [showTour, setShowTour] = useState(false)
+ const [showPopup, setShowPopup] = useState(false)
+ const [active, setActive] = useState(0)
+
+ const [maskRect, setMaskRect] = useState({
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ width: 0,
+ height: 0,
+ })
+
+ const classes = classNames(className, classPrefix)
+
+ useEffect(() => {
+ if (visible) {
+ getRootPosition()
+ }
+ setActive(0)
+ setShowTour(visible)
+ setShowPopup(visible)
+ }, [visible])
+
+ useEffect(() => {
+ if (visible) {
+ setShowPopup(true)
+ getRootPosition()
+ }
+ }, [active])
+
+ const getRootPosition = () => {
+ getTaroRectById(list[active].target as string).then((rect: any) => {
+ console.log('rect', rect)
+ setMaskRect({
+ top: rect.top,
+ left: rect.left,
+ right: rect.right,
+ bottom: rect.bottom,
+ width: rect.width,
+ height: rect.height,
+ })
+ })
+ }
+
+ const maskStyle = () => {
+ const { width, height, left, top } = maskRect
+ const center = [left + width / 2, top + height / 2] // 中心点 【横,纵】
+ const w = Number(maskWidth || width)
+ const h = Number(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`,
+ }
+ return styles
+ }
+
+ const maskClose = (e: MouseEvent) => {
+ setShowTour(false)
+ setShowPopup(false)
+
+ onClose && onClose(e)
+ }
+
+ const handleClickMask = (e: MouseEvent) => {
+ closeOnOverlayClick && maskClose(e)
+ }
+
+ const changeStep = (type: string) => {
+ if (type === 'next') {
+ setActive(active + 1)
+ onChange && onChange(active + 1)
+ } else {
+ setActive(active - 1)
+ onChange && onChange(active - 1)
+ }
+ setShowPopup(false)
+ }
+
+ return (
+
+
+ {list.map((item, index) => {
+ return (
+
+ {index === active && (
+ <>
+ {showTour && (
+
+ )}
+
+ <>>
+ <>
+ {children || (
+ <>
+ {type === 'step' && (
+
+ {title && (
+
+ )}
+
+ {item.content}
+
+
+
+ {active + 1}/{list.length}
+
+
+ {active !== 0 && showPrev && (
+ changeStep('prev')}
+ >
+ {prev || locale.tour.prevStepText}
+
+ )}
+ {list.length - 1 === active && (
+ maskClose(e)}
+ >
+ {complete || locale.tour.completeText}
+
+ )}
+ {list.length - 1 !== active && (
+ changeStep('next')}
+ >
+ {next || locale.tour.nextStepText}
+
+ )}
+
+
+
+ )}
+ {type === 'tile' && (
+
+ )}
+ >
+ )}
+ >
+
+ >
+ )}
+
+ )
+ })}
+
+ )
+}
+
+Tour.defaultProps = defaultProps
+Tour.displayName = 'NutTour'
diff --git a/src/packages/tour/tour.tsx b/src/packages/tour/tour.tsx
new file mode 100644
index 0000000000..77a286d6b8
--- /dev/null
+++ b/src/packages/tour/tour.tsx
@@ -0,0 +1,262 @@
+import React, { useState, useEffect, ReactNode, FunctionComponent } from 'react'
+import type { MouseEvent } from 'react'
+import { Close } from '@nutui/icons-react'
+import classNames from 'classnames'
+import Popover from '@/packages/popover'
+import { PopoverLocation } from '@/packages/popover/popover'
+import { getRect } from '@/utils/use-client-rect'
+import { BasicComponent, ComponentDefaults } from '@/utils/typings'
+import { useConfig } from '@/packages/configprovider'
+
+export interface ListOptions {
+ target: Element | string
+ content?: string
+ location?: string
+ popoverOffset?: number[]
+ arrowOffset?: number
+}
+
+export type TourType = 'step' | 'tile'
+
+export interface TourProps extends BasicComponent {
+ visible: boolean
+ type: TourType
+ location: PopoverLocation | string
+ mask: boolean
+ maskWidth: number | string
+ maskHeight: number | string
+ offset: number[]
+ list: ListOptions[]
+ title: ReactNode
+ next: ReactNode
+ prev: ReactNode
+ complete: ReactNode
+ showPrev: boolean
+ closeOnOverlayClick: boolean
+ onClose: (e: MouseEvent) => void
+ onChange: (value: number) => void
+}
+const defaultProps = {
+ ...ComponentDefaults,
+ visible: false,
+ type: 'step',
+ location: 'bottom',
+ mask: true,
+ maskWidth: '',
+ maskHeight: '',
+ offset: [8, 10],
+ title: '',
+ next: '',
+ prev: '',
+ complete: '',
+ showPrev: true,
+ closeOnOverlayClick: true,
+} as TourProps
+
+const classPrefix = 'nut-tour'
+export const Tour: FunctionComponent<
+ Partial &
+ Omit, 'title' | 'onChange'>
+> = (props) => {
+ const { locale } = useConfig()
+ const {
+ children,
+ className,
+ title,
+ closeOnOverlayClick,
+ showPrev,
+ list,
+ type,
+ location,
+ visible,
+ mask,
+ maskWidth,
+ maskHeight,
+ offset,
+ next,
+ prev,
+ complete,
+ onClose,
+ onChange,
+ ...rest
+ } = {
+ ...defaultProps,
+ ...props,
+ }
+
+ const [showTour, setShowTour] = useState(false)
+ const [showPopup, setShowPopup] = useState(false)
+ const [active, setActive] = useState(0)
+
+ const [maskRect, setMaskRect] = useState({
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ width: 0,
+ height: 0,
+ })
+
+ const classes = classNames(className, classPrefix)
+
+ useEffect(() => {
+ if (visible) {
+ getRootPosition()
+ }
+ setActive(0)
+ setShowTour(visible)
+ setShowPopup(visible)
+ }, [visible])
+
+ useEffect(() => {
+ if (visible) {
+ setShowPopup(true)
+ getRootPosition()
+ }
+ }, [active])
+
+ const getRootPosition = () => {
+ const el: any = document.querySelector(`#${list[active].target}`)
+ const rect = getRect(el)
+ setMaskRect(rect)
+ }
+
+ const maskStyle = () => {
+ const { width, height, left, top } = maskRect
+ const center = [left + width / 2, top + height / 2] // 中心点 【横,纵】
+ const w = Number(maskWidth || width)
+ const h = Number(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`,
+ }
+ return styles
+ }
+
+ const maskClose = (e: MouseEvent) => {
+ setShowTour(false)
+ setShowPopup(false)
+
+ onClose && onClose(e)
+ }
+
+ const handleClickMask = (e: MouseEvent) => {
+ closeOnOverlayClick && maskClose(e)
+ }
+
+ const changeStep = (type: string) => {
+ if (type === 'next') {
+ setActive(active + 1)
+ onChange && onChange(active + 1)
+ } else {
+ setActive(active - 1)
+ onChange && onChange(active - 1)
+ }
+ setShowPopup(false)
+ }
+
+ return (
+
+
+ {list.map((item, index) => {
+ return (
+
+ {index === active && (
+ <>
+ {showTour && (
+
+ )}
+
+ {/* placeholder don't delete <>> */}
+ <>>
+ <>
+ {children || (
+ <>
+ {type === 'step' && (
+
+ {title && (
+
+ )}
+
+ {item.content}
+
+
+
+ {active + 1}/{list.length}
+
+
+ {active !== 0 && showPrev && (
+ changeStep('prev')}
+ >
+ {prev || locale.tour.prevStepText}
+
+ )}
+ {list.length - 1 === active && (
+ maskClose(e)}
+ >
+ {complete || locale.tour.completeText}
+
+ )}
+ {list.length - 1 !== active && (
+ changeStep('next')}
+ >
+ {next || locale.tour.nextStepText}
+
+ )}
+
+
+
+ )}
+ {type === 'tile' && (
+
+ )}
+ >
+ )}
+ >
+
+ >
+ )}
+
+ )
+ })}
+
+ )
+}
+
+Tour.defaultProps = defaultProps
+Tour.displayName = 'NutTour'
diff --git a/src/sites/mobile-taro/src/app.config.ts b/src/sites/mobile-taro/src/app.config.ts
index 653a1ad1f3..7aabe2ca33 100644
--- a/src/sites/mobile-taro/src/app.config.ts
+++ b/src/sites/mobile-taro/src/app.config.ts
@@ -97,6 +97,7 @@ const subPackages = [
"pages/swiper/index",
"pages/table/index",
"pages/tag/index",
+ "pages/tour/index",
"pages/video/index",
"pages/virtuallist/index"
]
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
index 4c5158b394..551e36ac56 100644
--- a/src/styles/variables.scss
+++ b/src/styles/variables.scss
@@ -740,6 +740,39 @@ $toast-cover-bg-color: var(--nutui-toast-cover-bg-color, $gray7) !default;
$toast-inner-top: var(--nutui-toast-inner-top, 50%) !default;
$toast-inner-text-align: var(--nutui-toast-inner-text-align, 'center') !default;
+//tour(✅)
+$tour-mask-border-radius: var(--nutui-tour-mask-border-radius, 10px) !default;
+$tour-content-min-width: var(--nutui-tour-content-min-width, 200px) !default;
+$tour-content-padding: var(--nutui-tour-content-padding, 10px 12px) !default;
+$tour-content-inner-margin: var(
+ --nutui-tour-content-inner-margin,
+ 10px 0px
+) !default;
+$tour-content-inner-font-size: var(
+ --nutui-tour-content-inner-font-size,
+ 14px
+) !default;
+$tour-content-bottom-margin-top: var(
+ --nutui-tour-content-bottom-margin-top,
+ 10px
+) !default;
+$tour-content-bottom-btn-margin-left: var(
+ --nutui-tour-content-bottom-btn-margin-left,
+ 4px
+) !default;
+$tour-content-bottom-btn-padding: var(
+ --nutui-tour-content-bottom-btn-padding,
+ 2px 4px
+) !default;
+$tour-content-bottom-btn-font-size: var(
+ --nutui-tour-content-bottom-btn-font-size,
+ 12px
+) !default;
+$tour-content-bottom-btn-border-radius: var(
+ --nutui-tour-content-bottom-btn-border-radius,
+ 4px
+) !default;
+
//backtop(✅)
$backtop-border-color: var(--nutui-backtop-border-color, #e0e0e0) !default;
diff --git a/src/utils/typings.ts b/src/utils/typings.ts
index 9eea7ac58d..a995f67e65 100644
--- a/src/utils/typings.ts
+++ b/src/utils/typings.ts
@@ -4,6 +4,7 @@ export interface BasicComponent {
className?: string
style?: CSSProperties
children?: ReactNode
+ id?: string
}
export const ComponentDefaults = {
diff --git a/src/utils/use-taro-rect.ts b/src/utils/use-taro-rect.ts
new file mode 100644
index 0000000000..2d2ccf7bc0
--- /dev/null
+++ b/src/utils/use-taro-rect.ts
@@ -0,0 +1,25 @@
+import Taro from '@tarojs/taro'
+
+export const getTaroRectById = (id: string) => {
+ return new Promise((resolve, reject) => {
+ if (Taro.getEnv() === Taro.ENV_TYPE.WEB) {
+ const t = document ? document.querySelector(`#${id}`) : ''
+ if (t) {
+ resolve(t?.getBoundingClientRect())
+ }
+ reject()
+ } else {
+ const query = Taro.createSelectorQuery()
+ query
+ .select(`#${id}`)
+ .boundingClientRect()
+ .exec(function (rect: any) {
+ if (rect[0]) {
+ resolve(rect[0])
+ } else {
+ reject()
+ }
+ })
+ }
+ })
+}
| |