diff --git a/site/site.config.mjs b/site/site.config.mjs index d5410b882b..6df0e9da8b 100644 --- a/site/site.config.mjs +++ b/site/site.config.mjs @@ -439,6 +439,12 @@ export default { path: '/vue-next/components/drawer', component: () => import('tdesign-vue-next/drawer/drawer.md'), }, + { + title: 'Guide 引导', + name: 'guide', + path: '/vue-next/components/guide', + component: () => import('tdesign-vue-next/guide/guide.md'), + }, { title: 'Message 全局提示', name: 'message', diff --git a/src/_common b/src/_common index 8e25593811..eaaaa97248 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 8e25593811a4c3265aeda7fa4e54801e5ba4311b +Subproject commit eaaaa972480457e72de12b124f950b1db0f3605a diff --git a/src/components.ts b/src/components.ts index 5918590a49..7bd8fb58ad 100644 --- a/src/components.ts +++ b/src/components.ts @@ -65,6 +65,7 @@ export * from './rate'; export * from './alert'; export * from './dialog'; export * from './drawer'; +export * from './guide'; export * from './loading'; export * from './message'; export * from './notification'; diff --git a/src/guide/__test__/index.test.jsx b/src/guide/__test__/index.test.jsx new file mode 100644 index 0000000000..89aa5d3316 --- /dev/null +++ b/src/guide/__test__/index.test.jsx @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils'; +import Guide from '@/src/guide/index.ts'; + +// every component needs four parts: props/events/slots/functions. +describe('Guide', () => { + // test props api + describe(':props', () => { + it('', () => { + const wrapper = mount({ + render() { + return ; + }, + }); + expect(wrapper.exists()).toBe(true); + }); + }); + + // test events + describe('@event', () => { + it('', () => {}); + }); + + // test slots + describe('', () => { + it('', () => {}); + }); + + // test exposure function + describe('function', () => { + it('', () => {}); + }); +}); diff --git a/src/guide/_example/base.vue b/src/guide/_example/base.vue new file mode 100644 index 0000000000..86a34bc959 --- /dev/null +++ b/src/guide/_example/base.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/src/guide/_example/custom-popup.vue b/src/guide/_example/custom-popup.vue new file mode 100644 index 0000000000..726607a923 --- /dev/null +++ b/src/guide/_example/custom-popup.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/guide/_example/dialog-body.vue b/src/guide/_example/dialog-body.vue new file mode 100644 index 0000000000..76f7bb7f6a --- /dev/null +++ b/src/guide/_example/dialog-body.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/src/guide/_example/dialog.vue b/src/guide/_example/dialog.vue new file mode 100644 index 0000000000..8d5746ea06 --- /dev/null +++ b/src/guide/_example/dialog.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/guide/_example/highlightContent.vue b/src/guide/_example/highlightContent.vue new file mode 100644 index 0000000000..a91c672d66 --- /dev/null +++ b/src/guide/_example/highlightContent.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/guide/_example/my-popup.vue b/src/guide/_example/my-popup.vue new file mode 100644 index 0000000000..b0ed60cbda --- /dev/null +++ b/src/guide/_example/my-popup.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/guide/_example/no-mask.vue b/src/guide/_example/no-mask.vue new file mode 100644 index 0000000000..5dcff967e8 --- /dev/null +++ b/src/guide/_example/no-mask.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/src/guide/_example/popup-dialog.vue b/src/guide/_example/popup-dialog.vue new file mode 100644 index 0000000000..f3783e0f72 --- /dev/null +++ b/src/guide/_example/popup-dialog.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/guide/guide-step-props.ts b/src/guide/guide-step-props.ts new file mode 100644 index 0000000000..6fd5149acc --- /dev/null +++ b/src/guide/guide-step-props.ts @@ -0,0 +1,80 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdGuideStepProps } from '../guide/type'; +import { PropType } from 'vue'; + +export default { + /** 当前步骤提示框的内容 */ + body: { + type: [String, Function] as PropType, + }, + /** 自定义内容,同 content */ + children: { + type: [String, Function] as PropType, + }, + /** 用户自定义引导弹框的内容,一旦存在,此时除 `placement`、`offset`和`element` 外,其它属性全部失效) */ + content: { + type: Function as PropType, + }, + /** 高亮的节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'#tdesign' 或 () => document.querySelector('#tdesign') */ + element: { + type: [String, Function] as PropType, + required: true, + }, + /** 用户自定义的高亮框 (仅当 `mode` 为 `popup` 时生效) */ + highlightContent: { + type: Function as PropType, + }, + /** 高亮框的内边距 */ + highlightPadding: { + type: Number, + }, + /** 引导框的类型 */ + mode: { + type: String as PropType, + validator(val: TdGuideStepProps['mode']): boolean { + if (!val) return true; + return ['popup', 'dialog'].includes(val); + }, + }, + /** 用于自定义当前引导框的下一步按钮的内容 */ + nextButtonProps: { + type: Object as PropType, + }, + /** 相对于 placement 的偏移量,示例:[-10, 20] 或 ['10px', '8px'] */ + offset: { + type: Array as PropType, + }, + /** 引导框相对于高亮元素出现的位置 */ + placement: { + type: String, + default: 'top', + }, + /** 用于自定义当前引导框的上一步按钮的内容 */ + prevButtonProps: { + type: Object as PropType, + }, + /** 是否出现遮罩层 */ + showOverlay: { + type: Boolean, + default: true, + }, + /** 用于自定义当前步骤引导框的跳过按钮的内容 */ + skipButtonProps: { + type: Object as PropType, + }, + /** 覆盖引导框的类名 */ + stepOverlayClass: { + type: String, + default: '', + }, + /** 当前步骤的标题内容 */ + title: { + type: String, + default: '', + }, +}; diff --git a/src/guide/guide.en-US.md b/src/guide/guide.en-US.md new file mode 100644 index 0000000000..b10ffb17bc --- /dev/null +++ b/src/guide/guide.en-US.md @@ -0,0 +1,57 @@ +:: BASE_DOC :: + +## API +### Guide Props + +name | type | default | description | required +-- | -- | -- | -- | -- +counter | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +current | Number | 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 | `v-model` and `v-model:current` is supported | N +defaultCurrent | Number | 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 | uncontrolled property | N +finishButtonProps | Object | { content: '完成', theme: 'primary' } | Typescript:`ButtonProps` | N +hideCounter | Boolean | false | \- | N +hidePrev | Boolean | false | \- | N +hideSkip | Boolean | false | \- | N +highlightPadding | Number | 8 | \- | N +mode | String | popup | options:popup/dialog | N +nextButtonProps | Object | { content: '下一步', theme: 'primary' } | Typescript:`ButtonProps`,[Button API Documents](./button?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/guide/type.ts) | N +prevButtonProps | Object | { content: '上一步', theme: 'primary' } | Typescript:`ButtonProps` | N +showOverlay | Boolean | true | \- | N +skipButtonProps | Object | { content: '跳过', theme: 'default' } | Typescript:`ButtonProps` | N +steps | Array | - | Typescript:`Array` | N +zIndex | Number | 999999 | \- | N +onChange | Function | | Typescript:`(current: number, context?: { e: MouseEvent, total: number }) => void`
| N +onFinish | Function | | Typescript:`(context: { e: MouseEvent, current: number, total: number }) => void`
| N +onNextStepClick | Function | | Typescript:`(context: { e: MouseEvent, next: number, current: number, total: number }) => void`
| N +onPrevStepClick | Function | | Typescript:`(context: { e: MouseEvent, prev: number, current: number, total: number }) => void`
| N +onSkip | Function | | Typescript:`(context: { e: MouseEvent, current: number, total: number }) => void`
| N + +### Guide Events + +name | params | description +-- | -- | -- +change | `(current: number, context?: { e: MouseEvent, total: number })` | \- +finish | `(context: { e: MouseEvent, current: number, total: number })` | \- +next-step-click | `(context: { e: MouseEvent, next: number, current: number, total: number })` | \- +prev-step-click | `(context: { e: MouseEvent, prev: number, current: number, total: number })` | \- +skip | `(context: { e: MouseEvent, current: number, total: number })` | \- + +### GuideStep Props + +name | type | default | description | required +-- | -- | -- | -- | -- +body | String / Slot / Function | - | Typescript:`string | TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +children | String / Slot / Function | - | Typescript:`string | TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +content | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +element | String / Function | - | required。Typescript:`AttachNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | Y +highlightContent | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +highlightPadding | Number | - | \- | N +mode | String | - | options:popup/dialog | N +nextButtonProps | Object | - | Typescript:`ButtonProps` | N +offset | Array | - | Typescript:`Array` | N +placement | String | 'top' | Typescript:`StepPopupPlacement | StepDialogPlacement` `type StepPopupPlacement = 'top'|'left'|'right'|'bottom'|'top-left'|'top-right'|'bottom-left'|'bottom-right'|'left-top'|'left-bottom'|'right-top'|'right-bottom'` `type StepDialogPlacement = 'top'|'center' `。[see more ts definition](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/guide/type.ts) | N +prevButtonProps | Object | - | Typescript:`ButtonProps` | N +showOverlay | Boolean | true | \- | N +skipButtonProps | Object | - | Typescript:`ButtonProps` | N +stepOverlayClass | String | - | \- | N +title | String | - | \- | N diff --git a/src/guide/guide.md b/src/guide/guide.md new file mode 100644 index 0000000000..2b2a8368d5 --- /dev/null +++ b/src/guide/guide.md @@ -0,0 +1,56 @@ +:: BASE_DOC :: +## API +### Guide Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +counter | Slot / Function | - | 用于自定义渲染计数部分。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +current | Number | 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 | 支持语法糖 `v-model` 或 `v-model:current` | N +defaultCurrent | Number | 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 | 非受控属性 | N +finishButtonProps | Object | `{ content: '完成', theme: 'primary' }` | 透传 完成 的全部属性。TS 类型:`ButtonProps` | N +hideCounter | Boolean | false | 是否隐藏计数 | N +hidePrev | Boolean | false | 是否隐藏上一步按钮 | N +hideSkip | Boolean | false | 是否隐藏跳过按钮 | N +highlightPadding | Number | 8 | 高亮框的内边距 | N +mode | String | popup | 引导框的类型。可选项:popup/dialog | N +nextButtonProps | Object | `{ content: '下一步', theme: 'primary' }` | 透传 下一步按钮 的全部属性。TS 类型:`ButtonProps`,[Button API Documents](./button?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/guide/type.ts) | N +prevButtonProps | Object | `{ content: '上一步', theme: 'primary' }` | 透传 上一步按钮 的全部属性。TS 类型:`ButtonProps` | N +showOverlay | Boolean | true | 是否出现遮罩层 | N +skipButtonProps | Object | `{ content: '跳过', theme: 'default' }` | 透传 跳过按钮 的全部属性。TS 类型:`ButtonProps` | N +steps | Array | - | 用于定义每个步骤的内容,包括高亮的节点、相对位置和具体的文案内容等。。TS 类型:`Array` | N +zIndex | Number | 999999 | 提示框的层级 | N +onChange | Function | | TS 类型:`(current: number, context?: { e: MouseEvent, total: number }) => void`
当前步骤发生变化时触发 | N +onFinish | Function | | TS 类型:`(context: { e: MouseEvent, current: number, total: number }) => void`
点击完成按钮时触发 | N +onNextStepClick | Function | | TS 类型:`(context: { e: MouseEvent, next: number, current: number, total: number }) => void`
点击下一步时触发 | N +onPrevStepClick | Function | | TS 类型:`(context: { e: MouseEvent, prev: number, current: number, total: number }) => void`
点击上一步时触发 | N +onSkip | Function | | TS 类型:`(context: { e: MouseEvent, current: number, total: number }) => void`
点击跳过按钮时触发 | N + +### Guide Events + +名称 | 参数 | 描述 +-- | -- | -- +change | `(current: number, context?: { e: MouseEvent, total: number })` | 当前步骤发生变化时触发 +finish | `(context: { e: MouseEvent, current: number, total: number })` | 点击完成按钮时触发 +next-step-click | `(context: { e: MouseEvent, next: number, current: number, total: number })` | 点击下一步时触发 +prev-step-click | `(context: { e: MouseEvent, prev: number, current: number, total: number })` | 点击上一步时触发 +skip | `(context: { e: MouseEvent, current: number, total: number })` | 点击跳过按钮时触发 + +### GuideStep Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +body | String / Slot / Function | - | 当前步骤提示框的内容。TS 类型:`string | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +children | String / Slot / Function | - | 自定义内容,同 content。TS 类型:`string | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +content | Slot / Function | - | 用户自定义引导弹框的内容,一旦存在,此时除 `placement`、`offset`和`element` 外,其它属性全部失效)。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +element | String / Function | - | 必需。高亮的节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'#tdesign' 或 () => document.querySelector('#tdesign')。TS 类型:`AttachNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | Y +highlightContent | Slot / Function | - | 用户自定义的高亮框 (仅当 `mode` 为 `popup` 时生效)。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue-next/blob/develop/src/common.ts) | N +highlightPadding | Number | - | 高亮框的内边距 | N +mode | String | - | 引导框的类型。可选项:popup/dialog | N +nextButtonProps | Object | - | 用于自定义当前引导框的下一步按钮的内容。TS 类型:`ButtonProps` | N +offset | Array | - | 相对于 placement 的偏移量,示例:[-10, 20] 或 ['10px', '8px']。TS 类型:`Array` | N +placement | String | 'top' | 引导框相对于高亮元素出现的位置。TS 类型:`StepPopupPlacement | StepDialogPlacement` `type StepPopupPlacement = 'top'|'left'|'right'|'bottom'|'top-left'|'top-right'|'bottom-left'|'bottom-right'|'left-top'|'left-bottom'|'right-top'|'right-bottom'` `type StepDialogPlacement = 'top'|'center' `。[详细类型定义](https://github.com/Tencent/tdesign-vue-next/tree/develop/src/guide/type.ts) | N +prevButtonProps | Object | - | 用于自定义当前引导框的上一步按钮的内容。TS 类型:`ButtonProps` | N +showOverlay | Boolean | true | 是否出现遮罩层 | N +skipButtonProps | Object | - | 用于自定义当前步骤引导框的跳过按钮的内容。TS 类型:`ButtonProps` | N +stepOverlayClass | String | - | 覆盖引导框的类名 | N +title | String | - | 当前步骤的标题内容 | N diff --git a/src/guide/guide.tsx b/src/guide/guide.tsx new file mode 100644 index 0000000000..356feb3499 --- /dev/null +++ b/src/guide/guide.tsx @@ -0,0 +1,423 @@ +import { defineComponent, computed, nextTick, onMounted, ref, toRefs, watch } from 'vue'; + +import props from './props'; + +import { TdGuideProps, GuideCrossProps } from './type'; + +import { scrollToParentVisibleArea, getRelativePosition, getTargetElm, scrollToElm } from './utils'; + +import setStyle from '../_common/js/utils/set-style'; + +import TransferDom from '../utils/transfer-dom'; +import { addClass, removeClass, isFixed, getWindowScroll } from '../utils/dom'; + +import useVModel from '../hooks/useVModel'; +import { useTNodeJSX } from '../hooks/tnode'; +import { usePrefixClass } from '../hooks/useConfig'; + +import Button from '../button'; +import Popup from '../popup'; + +export default defineComponent({ + name: 'TGuide', + directives: { TransferDom }, + props, + setup(props) { + const renderTNodeJSX = useTNodeJSX(); + const COMPONENT_NAME = usePrefixClass('guide'); + const LOCK_CLASS = usePrefixClass('guide--lock'); + + const { current, modelValue, hideCounter, hidePrev, hideSkip, steps, zIndex } = toRefs(props); + const [innerCurrent, setInnerCurrent] = useVModel( + current, + modelValue, + props.defaultCurrent, + props.onChange, + 'current', + ); + + // 覆盖层,用于覆盖所有元素 + const overlayLayerRef = ref(); + // 高亮层,用于高亮元素 + const highlightLayerRef = ref(); + // 提示层,用于高亮元素 + const referenceLayerRef = ref(); + // 当前高亮的元素 + const currentHighlightLayerElm = ref(); + // 下一个高亮的元素 + const nextHighlightLayerElm = ref(); + // dialog wrapper ref + const dialogWrapperRef = ref(); + // dialog ref + const dialogTooltipRef = ref(); + // 是否开始展示 + const actived = ref(false); + // 步骤总数 + const stepsTotal = computed(() => steps.value.length); + // 当前步骤的信息 + const currentStepInfo = computed(() => steps.value[innerCurrent.value]); + // 当前是否为 popup + const isPopup = computed(() => getCurrentCrossProps('mode') === 'popup'); + // 当前元素位置状态 + const currentElmIsFixed = computed(() => isFixed(currentHighlightLayerElm.value || document.body)); + // 获取当前步骤的所有属性 用户当前步骤设置 > 用户全局设置的 > 默认值 + const getCurrentCrossProps = (propsName: Key) => + currentStepInfo.value[propsName] ?? props[propsName]; + + // 设置高亮层的位置 + const setHighlightLayerPosition = (highlighLayer: HTMLElement) => { + let { top, left } = getRelativePosition(nextHighlightLayerElm.value, currentHighlightLayerElm.value); + let { width, height } = nextHighlightLayerElm.value.getBoundingClientRect(); + const highlightPadding = getCurrentCrossProps('highlightPadding'); + + if (isPopup.value) { + width += highlightPadding * 2; + height += highlightPadding * 2; + top -= highlightPadding; + left -= highlightPadding; + } else { + const { scrollTop, scrollLeft } = getWindowScroll(); + top += scrollTop; + left += scrollLeft; + } + + setStyle(highlighLayer, { + width: `${width}px`, + height: `${height}px`, + top: `${top}px`, + left: `${left}px`, + }); + }; + + const showPopupGuide = () => { + const currentElement = getTargetElm(currentStepInfo.value.element); + nextHighlightLayerElm.value = currentElement; + + nextTick(() => { + scrollToParentVisibleArea(nextHighlightLayerElm.value); + setHighlightLayerPosition(highlightLayerRef.value); + setHighlightLayerPosition(referenceLayerRef.value); + scrollToElm(nextHighlightLayerElm.value); + currentHighlightLayerElm.value = currentElement; + }); + }; + + const destroyTooltipElm = () => { + referenceLayerRef.value?.parentNode.removeChild(referenceLayerRef.value); + }; + + const showDialogGuide = () => { + nextTick(() => { + const currentElement = dialogTooltipRef.value; + nextHighlightLayerElm.value = currentElement; + scrollToParentVisibleArea(nextHighlightLayerElm.value); + setHighlightLayerPosition(highlightLayerRef.value); + scrollToElm(nextHighlightLayerElm.value); + currentHighlightLayerElm.value = currentElement; + }); + }; + + const destroyDialogTooltipElm = () => { + dialogTooltipRef.value?.parentNode.removeChild(dialogTooltipRef.value); + dialogWrapperRef.value?.parentNode.removeChild(dialogWrapperRef.value); + }; + + const showGuide = () => { + if (isPopup.value) { + destroyDialogTooltipElm(); + showPopupGuide(); + } else { + destroyTooltipElm(); + showDialogGuide(); + } + }; + + const destroyGuide = () => { + destroyTooltipElm(); + destroyDialogTooltipElm(); + highlightLayerRef.value?.parentNode.removeChild(highlightLayerRef.value); + overlayLayerRef.value?.parentNode.removeChild(overlayLayerRef.value); + removeClass(document.body, LOCK_CLASS.value); + }; + + const handleSkip = (e: MouseEvent) => { + const total = stepsTotal.value; + actived.value = false; + setInnerCurrent(-1, { e, total }); + props.onSkip?.({ e, current: -1, total }); + }; + + const handlePrev = (e: MouseEvent) => { + const total = stepsTotal.value; + setInnerCurrent(innerCurrent.value - 1, { e, total }); + props.onPrevStepClick?.({ + e, + prev: innerCurrent.value - 1, + current: innerCurrent.value, + total, + }); + }; + + const handleNext = (e: MouseEvent) => { + const total = stepsTotal.value; + setInnerCurrent(innerCurrent.value + 1, { e, total }); + props.onNextStepClick?.({ + e, + next: innerCurrent.value + 1, + current: innerCurrent.value, + total, + }); + }; + + const handleFinish = (e: MouseEvent) => { + const total = stepsTotal.value; + actived.value = false; + setInnerCurrent(-1, { e, total }); + props.onFinish?.({ e, current: -1, total }); + }; + + const initGuide = () => { + if (innerCurrent.value >= 0 && innerCurrent.value < steps.value.length) { + if (!actived.value) { + actived.value = true; + addClass(document.body, LOCK_CLASS.value); + } + showGuide(); + } + }; + + watch(innerCurrent, (val) => { + if (val >= 0 && val < steps.value.length) { + initGuide(); + } else { + actived.value = false; + destroyGuide(); + } + }); + + onMounted(() => { + initGuide(); + }); + + return () => { + const renderOverlayLayer = () => ( +
+ ); + + const renderHighlightLayer = () => { + const style = { zIndex: zIndex.value - 1 }; + const highlightClass = [ + `${COMPONENT_NAME.value}__highlight`, + `${COMPONENT_NAME.value}__highlight--${isPopup.value ? 'popup' : 'dialog'}`, + `${COMPONENT_NAME.value}--${currentElmIsFixed.value && isPopup.value ? 'fixed' : 'absolute'}`, + ]; + const showOverlay = getCurrentCrossProps('showOverlay'); + const maskClass = [`${COMPONENT_NAME.value}__highlight--${showOverlay ? 'mask' : 'nomask'}`]; + const { highlightContent } = currentStepInfo.value; + const showHighlightContent = highlightContent && isPopup.value; + + return ( +
+ {showHighlightContent && } +
+ ); + }; + + const renderCounter = () => { + const popupSlotCounter = renderTNodeJSX('counter', { + params: { total: stepsTotal.value, current: innerCurrent.value }, + }); + + const popupDefaultCounter = ( +
+ {popupSlotCounter || ( + + {innerCurrent.value + 1}/{stepsTotal.value} + + )} +
+ ); + return <>{!hideCounter.value && popupDefaultCounter}; + }; + + const renderAction = (mode: TdGuideProps['mode']) => { + const isLast = innerCurrent.value === stepsTotal.value - 1; + const isFirst = innerCurrent.value === 0; + const buttonSize = mode === 'popup' ? 'small' : 'medium'; + + return ( +
+ {!hideSkip.value && !isLast && ( +
+ ); + }; + + const renderTooltipBody = () => { + const title =
{currentStepInfo.value.title}
; + const { body: descBody } = currentStepInfo.value; + let renderDesc; + if (typeof descBody === 'string') { + renderDesc = descBody; + } else { + renderDesc = ; + } + const desc = ( +
{typeof descBody === 'string' ? descBody : }
+ ); + + return ( + <> + {title} + {desc} + + ); + }; + + const renderPopupContent = () => { + const footerClasses = [`${COMPONENT_NAME.value}__footer`, `${COMPONENT_NAME.value}__footer--popup`]; + const action = ( +
+ {renderCounter()} + {renderAction('popup')} +
+ ); + + return ( +
+ {renderTooltipBody()} + {action} +
+ ); + }; + + const renderPopupGuide = () => { + const { content } = currentStepInfo.value; + let renderBody; + if (content) { + const contentProps = { + handlePrev, + handleNext, + handleSkip, + handleFinish, + current: innerCurrent.value, + total: stepsTotal.value, + }; + renderBody = () => ; + } else { + renderBody = renderPopupContent; + } + + const classes = [ + `${COMPONENT_NAME.value}__reference`, + `${COMPONENT_NAME.value}--${currentElmIsFixed.value ? 'fixed' : 'absolute'}`, + ]; + + return ( + +
+ + ); + }; + + const renderDialogGuide = () => { + const style = { zIndex: zIndex.value }; + const wrapperClasses = [ + `${COMPONENT_NAME.value}__wrapper`, + { [`${COMPONENT_NAME.value}__wrapper--center`]: currentStepInfo.value.placement === 'center' }, + ]; + const dialogClasses = [ + `${COMPONENT_NAME.value}__reference`, + `${COMPONENT_NAME.value}--absolute`, + `${COMPONENT_NAME.value}__dialog`, + { + [`${COMPONENT_NAME.value}__dialog--nomask`]: !getCurrentCrossProps('showOverlay'), + [currentStepInfo.value.stepOverlayClass]: !!currentStepInfo.value.stepOverlayClass, + }, + ]; + const footerClasses = [`${COMPONENT_NAME.value}__footer`, `${COMPONENT_NAME.value}__footer--popup`]; + return ( + <> +
+
+ {renderTooltipBody()} +
+ {renderCounter()} + {renderAction('dialog')} +
+
+
+ + ); + }; + + const renderGuide = () => { + return ( + <> + {renderOverlayLayer()} + {renderHighlightLayer()} + {isPopup.value ? renderPopupGuide() : renderDialogGuide()} + + ); + }; + + return <>{actived.value && renderGuide()}; + }; + }, +}); diff --git a/src/guide/index.ts b/src/guide/index.ts new file mode 100644 index 0000000000..8ee32c75ab --- /dev/null +++ b/src/guide/index.ts @@ -0,0 +1,12 @@ +import _Guide from './guide'; +import withInstall from '../utils/withInstall'; +import { TdGuideProps } from './type'; + +import './style'; + +export * from './type'; +export type GuideProps = TdGuideProps; + +export const Guide = withInstall(_Guide); + +export default Guide; diff --git a/src/guide/props.ts b/src/guide/props.ts new file mode 100644 index 0000000000..ff74dee635 --- /dev/null +++ b/src/guide/props.ts @@ -0,0 +1,94 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdGuideProps } from './type'; +import { PropType } from 'vue'; + +import { ButtonProps } from '../button'; + +export default { + /** 用于自定义渲染计数部分 */ + counter: { + type: Function as PropType, + }, + /** null */ + current: { + type: Number, + default: undefined, + }, + modelValue: { + type: Number, + default: undefined, + }, + /** null,非受控属性 */ + defaultCurrent: { + type: Number, + }, + /** 透传 完成 的全部属性 */ + finishButtonProps: { + type: Object as PropType, + default: { content: '完成', theme: 'primary' } as ButtonProps, + }, + /** 是否隐藏计数 */ + hideCounter: Boolean, + /** 是否隐藏上一步按钮 */ + hidePrev: Boolean, + /** 是否隐藏跳过按钮 */ + hideSkip: Boolean, + /** 高亮框的内边距 */ + highlightPadding: { + type: Number, + default: 8, + }, + /** 引导框的类型 */ + mode: { + type: String as PropType, + default: 'popup' as TdGuideProps['mode'], + validator(val: TdGuideProps['mode']): boolean { + if (!val) return true; + return ['popup', 'dialog'].includes(val); + }, + }, + /** 透传 下一步按钮 的全部属性 */ + nextButtonProps: { + type: Object as PropType, + default: { content: '下一步', theme: 'primary' } as ButtonProps, + }, + /** 透传 上一步按钮 的全部属性 */ + prevButtonProps: { + type: Object as PropType, + default: { content: '上一步', theme: 'default' } as ButtonProps, + }, + /** 是否出现遮罩层 */ + showOverlay: { + type: Boolean, + default: true, + }, + /** 透传 跳过按钮 的全部属性 */ + skipButtonProps: { + type: Object as PropType, + default: { content: '跳过', theme: 'default' } as ButtonProps, + }, + /** 用于定义每个步骤的内容,包括高亮的节点、相对位置和具体的文案内容等。 */ + steps: { + type: Array as PropType, + }, + /** 提示框的层级 */ + zIndex: { + type: Number, + default: 999999, + }, + /** 当前步骤发生变化时触发 */ + onChange: Function as PropType, + /** 点击完成按钮时触发 */ + onFinish: Function as PropType, + /** 点击下一步时触发 */ + onNextStepClick: Function as PropType, + /** 点击上一步时触发 */ + onPrevStepClick: Function as PropType, + /** 点击跳过按钮时触发 */ + onSkip: Function as PropType, +}; diff --git a/src/guide/style/css.js b/src/guide/style/css.js new file mode 100644 index 0000000000..6a9a4b1328 --- /dev/null +++ b/src/guide/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/guide/style/index.js b/src/guide/style/index.js new file mode 100644 index 0000000000..6ff0f4f84c --- /dev/null +++ b/src/guide/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/web/components/guide/_index.less'; diff --git a/src/guide/type.ts b/src/guide/type.ts new file mode 100644 index 0000000000..2569d034da --- /dev/null +++ b/src/guide/type.ts @@ -0,0 +1,197 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { ButtonProps } from '../button'; +import { TNode, AttachNode } from '../common'; + +export interface TdGuideProps { + /** + * 用于自定义渲染计数部分 + */ + counter?: TNode; + /** + * null + * @default 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 + */ + current?: number; + /** + * null,非受控属性 + * @default 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 + */ + defaultCurrent?: number; + /** + * null + * @default 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 + */ + modelValue?: number; + /** + * 透传 完成 的全部属性 + * @default { content: '完成', theme: 'primary' } + */ + finishButtonProps?: ButtonProps; + /** + * 是否隐藏计数 + * @default false + */ + hideCounter?: boolean; + /** + * 是否隐藏上一步按钮 + * @default false + */ + hidePrev?: boolean; + /** + * 是否隐藏跳过按钮 + * @default false + */ + hideSkip?: boolean; + /** + * 高亮框的内边距 + * @default 8 + */ + highlightPadding?: number; + /** + * 引导框的类型 + * @default popup + */ + mode?: 'popup' | 'dialog'; + /** + * 透传 下一步按钮 的全部属性 + * @default { content: '下一步', theme: 'primary' } + */ + nextButtonProps?: ButtonProps; + /** + * 透传 上一步按钮 的全部属性 + * @default { content: '上一步', theme: 'primary' } + */ + prevButtonProps?: ButtonProps; + /** + * 是否出现遮罩层 + * @default true + */ + showOverlay?: boolean; + /** + * 透传 跳过按钮 的全部属性 + * @default { content: '跳过', theme: 'default' } + */ + skipButtonProps?: ButtonProps; + /** + * 用于定义每个步骤的内容,包括高亮的节点、相对位置和具体的文案内容等。 + */ + steps?: Array; + /** + * 提示框的层级 + * @default 999999 + */ + zIndex?: number; + /** + * 当前步骤发生变化时触发 + */ + onChange?: (current: number, context?: { e: MouseEvent; total: number }) => void; + /** + * 点击完成按钮时触发 + */ + onFinish?: (context: { e: MouseEvent; current: number; total: number }) => void; + /** + * 点击下一步时触发 + */ + onNextStepClick?: (context: { e: MouseEvent; next: number; current: number; total: number }) => void; + /** + * 点击上一步时触发 + */ + onPrevStepClick?: (context: { e: MouseEvent; prev: number; current: number; total: number }) => void; + /** + * 点击跳过按钮时触发 + */ + onSkip?: (context: { e: MouseEvent; current: number; total: number }) => void; +} + +export interface TdGuideStepProps { + /** + * 当前步骤提示框的内容 + */ + body?: string | TNode; + /** + * 自定义内容,同 content + */ + children?: string | TNode; + /** + * 用户自定义引导弹框的内容,一旦存在,此时除 `placement`、`offset`和`element` 外,其它属性全部失效) + */ + content?: TNode; + /** + * 高亮的节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'#tdesign' 或 () => document.querySelector('#tdesign') + */ + element: AttachNode; + /** + * 用户自定义的高亮框 (仅当 `mode` 为 `popup` 时生效) + */ + highlightContent?: TNode; + /** + * 高亮框的内边距 + */ + highlightPadding?: number; + /** + * 引导框的类型 + */ + mode?: 'popup' | 'dialog'; + /** + * 用于自定义当前引导框的下一步按钮的内容 + */ + nextButtonProps?: ButtonProps; + /** + * 相对于 placement 的偏移量,示例:[-10, 20] 或 ['10px', '8px'] + */ + offset?: Array; + /** + * 引导框相对于高亮元素出现的位置 + * @default 'top' + */ + placement?: StepPopupPlacement | StepDialogPlacement; + /** + * 用于自定义当前引导框的上一步按钮的内容 + */ + prevButtonProps?: ButtonProps; + /** + * 是否出现遮罩层 + * @default true + */ + showOverlay?: boolean; + /** + * 用于自定义当前步骤引导框的跳过按钮的内容 + */ + skipButtonProps?: ButtonProps; + /** + * 覆盖引导框的类名 + * @default '' + */ + stepOverlayClass?: string; + /** + * 当前步骤的标题内容 + * @default '' + */ + title?: string; +} + +export type StepPopupPlacement = + | 'top' + | 'left' + | 'right' + | 'bottom' + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right' + | 'left-top' + | 'left-bottom' + | 'right-top' + | 'right-bottom'; + +export type StepDialogPlacement = 'top' | 'center'; + +export type GuideCrossProps = Pick< + TdGuideStepProps, + 'mode' | 'skipButtonProps' | 'prevButtonProps' | 'nextButtonProps' | 'showOverlay' | 'highlightPadding' +>; diff --git a/src/guide/utils/getRelativePosition.ts b/src/guide/utils/getRelativePosition.ts new file mode 100644 index 0000000000..8d9e0f0815 --- /dev/null +++ b/src/guide/utils/getRelativePosition.ts @@ -0,0 +1,34 @@ +import { getElmCssPropValue, isFixed, getWindowScroll } from '../../utils/dom'; + +/** + * 获取元素相对于另一个元素的位置(或者说相对于 body) + * 感谢 `meouw`: http://stackoverflow.com/a/442474/375966 + */ +export default function getRelativePosition(elm: HTMLElement, relativeElm: HTMLElement = document.body) { + const { scrollTop, scrollLeft } = getWindowScroll(); + const { top: elmTop, left: elmLeft } = elm.getBoundingClientRect(); + const { top: relElmTop, left: relElmLeft } = relativeElm.getBoundingClientRect(); + const relativeElmPosition = getElmCssPropValue(relativeElm, 'position'); + + if ( + (relativeElm.tagName.toLowerCase() !== 'body' && relativeElmPosition === 'relative') || + relativeElmPosition === 'sticky' + ) { + return { + top: elmTop - relElmTop, + left: elmLeft - relElmLeft, + }; + } + + if (isFixed(elm)) { + return { + top: elmTop, + left: elmLeft, + }; + } + + return { + top: elmTop + scrollTop, + left: elmLeft + scrollLeft, + }; +} diff --git a/src/guide/utils/getScrollParent.ts b/src/guide/utils/getScrollParent.ts new file mode 100644 index 0000000000..7513d124fb --- /dev/null +++ b/src/guide/utils/getScrollParent.ts @@ -0,0 +1,28 @@ +import { elementInViewport } from '../../utils/dom'; + +export function getScrollParent(element: HTMLElement) { + let style = window.getComputedStyle(element); + const excludeStaticParent = style.position === 'absolute'; + const overflowRegex = /(auto|scroll)/; + + if (style.position === 'fixed') return document.body; + + for (let parent = element; parent.parentElement; ) { + parent = parent.parentElement; + style = window.getComputedStyle(parent); + if (excludeStaticParent && style.position === 'static') { + continue; + } + if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent; + } + + return document.body; +} + +export function scrollToParentVisibleArea(element: HTMLElement) { + const parent = getScrollParent(element); + if (parent === document.body) return; + // !todo 逻辑待验证 + if (elementInViewport(element, parent)) return; + parent.scrollTop = element.offsetTop - parent.offsetTop; +} diff --git a/src/guide/utils/getTargetElm.ts b/src/guide/utils/getTargetElm.ts new file mode 100644 index 0000000000..cd7379892a --- /dev/null +++ b/src/guide/utils/getTargetElm.ts @@ -0,0 +1,20 @@ +import { AttachNode } from '../../common'; + +export default function getTargetElm(elm: AttachNode): HTMLElement { + if (elm) { + let targetElement: HTMLElement = null; + if (typeof elm === 'string') { + targetElement = document.querySelector(elm); + } else if (typeof elm === 'function') { + targetElement = elm() as HTMLElement; + } else { + throw new Error('elm should be string or function'); + } + if (targetElement) { + return targetElement as HTMLElement; + } + throw new Error('There is no element with given.'); + } else { + return document.body; + } +} diff --git a/src/guide/utils/index.ts b/src/guide/utils/index.ts new file mode 100644 index 0000000000..f79a227893 --- /dev/null +++ b/src/guide/utils/index.ts @@ -0,0 +1,6 @@ +import { scrollToParentVisibleArea } from './getScrollParent'; +import getRelativePosition from './getRelativePosition'; +import getTargetElm from './getTargetElm'; +import scrollToElm from './scrollToElm'; + +export { scrollToParentVisibleArea, getRelativePosition, getTargetElm, scrollToElm }; diff --git a/src/guide/utils/scrollToElm.ts b/src/guide/utils/scrollToElm.ts new file mode 100644 index 0000000000..ba48a94d23 --- /dev/null +++ b/src/guide/utils/scrollToElm.ts @@ -0,0 +1,18 @@ +import { getWindowSize, elementInViewport, scrollTo } from '../../utils/dom'; + +export default function scrollToElm(elm: HTMLElement) { + const rect = elm.getBoundingClientRect(); + + if (!elementInViewport(elm)) { + const winHeight = getWindowSize().height; + const top = rect.bottom - (rect.bottom - rect.top); + scrollTo(rect.top - (winHeight / 2 - rect.height / 2), {}); + + // todo 先暂时保留这里的逻辑 + // if (top < 0 || element.clientHeight > winHeight) { + // window.scrollBy(0, rect.top - (winHeight / 2 - rect.height / 2)); + // } else { + // window.scrollBy(0, rect.top - (winHeight / 2 - rect.height / 2)); + // } + } +} diff --git a/src/utils/dom.ts b/src/utils/dom.ts index f6088d4d49..3d0fcf2004 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -287,3 +287,88 @@ export const requestSubmit = (target: HTMLFormElement) => { submitter.click(); target.removeChild(submitter); }; + +/** + * 检查元素是否在父元素视图 + * http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport + * @param elm 元素 + * @param parent + * @returns boolean + */ +export function elementInViewport(elm: HTMLElement, parent?: HTMLElement): boolean { + const rect = elm.getBoundingClientRect(); + if (parent) { + const parentRect = parent.getBoundingClientRect(); + return ( + rect.top >= parentRect.top && + rect.left >= parentRect.left && + rect.bottom <= parentRect.bottom && + rect.right <= parentRect.right + ); + } + return rect.top >= 0 && rect.left >= 0 && rect.bottom + 80 <= window.innerHeight && rect.right <= window.innerWidth; +} + +/** + * 获取元素某个 css 对应的值 + * @param element 元素 + * @param propName css 名 + * @returns string + */ +export function getElmCssPropValue(element: HTMLElement, propName: string): string { + let propValue = ''; + + if (document.defaultView && document.defaultView.getComputedStyle) { + propValue = document.defaultView.getComputedStyle(element, null).getPropertyValue(propName); + } + + if (propValue && propValue.toLowerCase) { + return propValue.toLowerCase(); + } + + return propValue; +} + +/** + * 判断元素是否处在 position fixed 中 + * @param element 元素 + * @returns boolean + */ +export function isFixed(element: HTMLElement): boolean { + const p = element.parentNode as HTMLElement; + + if (!p || p.nodeName === 'HTML') { + return false; + } + + if (getElmCssPropValue(element, 'position') === 'fixed') { + return true; + } + + return isFixed(p); +} + +/** + * 获取当前视图滑动的距离 + * @returns { scrollTop: number, scrollLeft: number } + */ +export function getWindowScroll(): { scrollTop: number; scrollLeft: number } { + const { body } = document; + const docElm = document.documentElement; + const scrollTop = window.pageYOffset || docElm.scrollTop || body.scrollTop; + const scrollLeft = window.pageXOffset || docElm.scrollLeft || body.scrollLeft; + + return { scrollTop, scrollLeft }; +} + +/** + * 获取当前视图的大小 + * @returns { width: number, height: number } + */ +export function getWindowSize(): { width: number; height: number } { + if (window.innerWidth !== undefined) { + return { width: window.innerWidth, height: window.innerHeight }; + } + const doc = document.documentElement; + return { width: doc.clientWidth, height: doc.clientHeight }; +} diff --git a/test/e2e/guide/guide.spec.js b/test/e2e/guide/guide.spec.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/snap/__snapshots__/csr.test.js.snap b/test/snap/__snapshots__/csr.test.js.snap index e18542e36b..1c85a8ddff 100644 --- a/test/snap/__snapshots__/csr.test.js.snap +++ b/test/snap/__snapshots__/csr.test.js.snap @@ -62606,6 +62606,1051 @@ exports[`csr snapshot test > csr test ./src/grid/_example/valign.vue 1`] = `
`; +exports[`csr snapshot test > csr test ./src/guide/_example/base.vue 1`] = ` +
+ + +
+ +
+
+ 演示新手引导 +
+ +
+ +
+
+
+ Guide 用户引导 +
+
+ 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 +
+
+
+
+ Label +
+
+
+ + + + + + + +
+ +
+
+
+
+ Label +
+
+
+ + + + + + + +
+ +
+
+
+ + + + +
+
+ + + + +
+ + +
+
+ +
+`; + +exports[`csr snapshot test > csr test ./src/guide/_example/custom-popup.vue 1`] = ` +
+ + +
+ +
+
+ 演示新手引导 +
+ +
+ +
+
+
+ Guide 用户引导 +
+
+ 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 +
+
+
+
+ Label +
+
+
+ + + + + + + +
+ +
+
+
+
+ Label +
+
+
+ + + + + + + +
+ +
+
+
+ + + + +
+
+ + + + +
+ + +
+
+ +
+`; + +exports[`csr snapshot test > csr test ./src/guide/_example/dialog.vue 1`] = ` +
+ + +
+ +
+
+ 演示新手引导 +
+ +
+ +
+
+
+ Guide 用户引导 +
+
+ 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 +
+
+
+
+ Label +
+
+
+ + + + + + + +
+ +
+
+
+
+ Label +
+
+
+ + + + + + + +
+ +
+
+
+ + + + +
+
+ + + + +
+ + +
+
+ +
+`; + +exports[`csr snapshot test > csr test ./src/guide/_example/dialog-body.vue 1`] = ` +
+ demo +

+ 此处显示本页引导的说明文案,可按需要撰写,如内容过多可折行显示。图文也可按需自由设计。 +

+
+`; + +exports[`csr snapshot test > csr test ./src/guide/_example/highlightContent.vue 1`] = ` + +`; + +exports[`csr snapshot test > csr test ./src/guide/_example/my-popup.vue 1`] = ` +
+ + + + + +
+`; + +exports[`csr snapshot test > csr test ./src/guide/_example/no-mask.vue 1`] = ` +
+ + +
+ +
+
+ 演示新手引导 +
+ +
+ +
+
+
+ Guide 用户引导 +
+
+ 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 +
+
+
+
+ Label +
+
+
+ + + + + + + +
+ +
+
+
+
+ Label +
+
+
+ + + + + + + +
+ +
+
+
+ + + + +
+
+ + + + +
+ + +
+
+ +
+`; + +exports[`csr snapshot test > csr test ./src/guide/_example/popup-dialog.vue 1`] = ` +
+ + +
+ +
+
+ 演示新手引导 +
+ +
+ +
+
+
+ Guide 用户引导 +
+
+ 按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。 +
+
+
+
+ Label +
+
+
+ + + + + + + +
+ +
+
+
+
+ Label +
+
+
+ + + + + + + +
+ +
+
+
+ + + + +
+
+ + + + +
+ + +
+
+ +
+`; + exports[`csr snapshot test > csr test ./src/icon/_example/base.vue 1`] = `
ssr test ./src/grid/_example/sort.vue 1`] = `"
ssr test ./src/grid/_example/valign.vue 1`] = `"

align top

col-3
col-3
col-3
col-3

align middle

col-3
col-3
col-3
col-3

align bottom

col-3
col-3
col-3
col-3
"`; +exports[`ssr snapshot test > ssr test ./src/guide/_example/base.vue 1`] = `"
演示新手引导
Guide 用户引导
按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。
Label
Label
"`; + +exports[`ssr snapshot test > ssr test ./src/guide/_example/custom-popup.vue 1`] = `"
演示新手引导
Guide 用户引导
按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。
Label
Label
"`; + +exports[`ssr snapshot test > ssr test ./src/guide/_example/dialog.vue 1`] = `"
演示新手引导
Guide 用户引导
按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。
Label
Label
"`; + +exports[`ssr snapshot test > ssr test ./src/guide/_example/dialog-body.vue 1`] = `"
\\"demo\\"

此处显示本页引导的说明文案,可按需要撰写,如内容过多可折行显示。图文也可按需自由设计。

"`; + +exports[`ssr snapshot test > ssr test ./src/guide/_example/highlightContent.vue 1`] = `""`; + +exports[`ssr snapshot test > ssr test ./src/guide/_example/my-popup.vue 1`] = `"

自定义的图形或说明文案,用来解释或指导该功能使用。

"`; + +exports[`ssr snapshot test > ssr test ./src/guide/_example/no-mask.vue 1`] = `"
演示新手引导
Guide 用户引导
按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。
Label
Label
"`; + +exports[`ssr snapshot test > ssr test ./src/guide/_example/popup-dialog.vue 1`] = `"
演示新手引导
Guide 用户引导
按钮用于开启一个闭环的操作任务,如“删除”对象、“购买”商品等。
Label
Label
"`; + exports[`ssr snapshot test > ssr test ./src/icon/_example/base.vue 1`] = `"
"`; exports[`ssr snapshot test > ssr test ./src/icon/_example/enhanced.vue 1`] = `"
"`;