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( + +
+
nutui 4.x 即将发布,敬请期待
+
+
+ ) + 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 && ( +
+
maskClose(e)}> + +
+
+ )} +
+ {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' && ( +
+
+ {item.content} +
+
+ )} + + )} + +
+ + )} +
+ ) + })} +
+ ) +} + +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 && ( +
+
maskClose(e)}> + +
+
+ )} +
+ {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' && ( +
+
+ {item.content} +
+
+ )} + + )} + +
+ + )} +
+ ) + })} +
+ ) +} + +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() + } + }) + } + }) +}