diff --git a/devui/steps-guide/index.ts b/devui/steps-guide/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..62154caf49af04525878b52f2407535eb107a6d3 --- /dev/null +++ b/devui/steps-guide/index.ts @@ -0,0 +1,18 @@ +import type { App } from 'vue' +import StepsGuide from './src/steps-guide' + +StepsGuide.install = function(app: App): void { + app.component(StepsGuide.name, StepsGuide) +} + +export { StepsGuide } + +export default { + title: 'StepsGuide 操作指引', + category: '导航', + status: '开发中', // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + + app.use(StepsGuide as any) + } +} diff --git a/devui/steps-guide/src/steps-guide-types.ts b/devui/steps-guide/src/steps-guide-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..c162691a8b5a76dcc0a54be49824e8a031313e57 --- /dev/null +++ b/devui/steps-guide/src/steps-guide-types.ts @@ -0,0 +1,19 @@ +import type { ExtractPropTypes, PropType } from 'vue' +type positionType = 'top-left' | 'top' | 'top-right' | 'left' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right' +export type positionConf = { + left: number + top: number + type: string +} +export type Step = { + title: string + content: string + trigger: string + target?: string + position?: positionType | positionConf +} +export const stepsGuideProps = { + steps: Array as PropType +} as const + +export type StepsGuideProps = ExtractPropTypes diff --git a/devui/steps-guide/src/steps-guide.scss b/devui/steps-guide/src/steps-guide.scss new file mode 100644 index 0000000000000000000000000000000000000000..a9f035a8588e43cccd53fdb9a4fb6dd77ac3d940 --- /dev/null +++ b/devui/steps-guide/src/steps-guide.scss @@ -0,0 +1,249 @@ +$devui-font-size: 12px; +$devui-border-radius: 4px; +$devui-light-text: #ffffff; +$devui-brand:#5e7ce0; +$devui-shadow-length-feedback-overlay: var(--devui-shadow-length-feedback-overlay, 0 4px 16px 0); +$devui-border-radius-feedback: var(--devui-border-radius-feedback, 4px); +$devui-font-size-page-title: 12px; + +.d-steps-guide { + width: 400px; + min-height: 160px; + background: $devui-brand; + box-shadow: $devui-shadow-length-feedback-overlay rgba(81, 112, 255, 0.3); + border-radius: $devui-border-radius-feedback; + font-size: $devui-font-size; + color: $devui-light-text; + padding: 20px; + position: absolute; + + .devui-title { + font-size: $devui-font-size-page-title; + opacity: 1; + margin: 0 0 20px 0; + padding: 0; + } + + > .devui-arrow, + > .devui-arrow::after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + } + + > .devui-arrow { + border-width: 8px; + } + + &.left > .devui-arrow { + top: 23px; + right: -6px; + margin-top: -3px; + border-right-width: 0; + transform: rotate(-135deg); + border-left-color: $devui-brand; + } + + &.top > .devui-arrow, + &.top-left > .devui-arrow, + &.top-right > .devui-arrow { + bottom: -6px; + border-bottom-width: 0; + border-top-color: $devui-brand; + transform: rotate(135deg); + } + + &.top > .devui-arrow { + left: calc(50% - 4px); + } + + &.top-left > .devui-arrow { + left: 23px; + } + + &.top-right > .devui-arrow { + right: 23px; + transform: rotate(-135deg); + } + + &.right > .devui-arrow { + top: 23px; + left: -6px; + margin-top: -3px; + border-left-width: 0; + transform: rotate(135deg); + border-right-color: $devui-brand; + } + + &.bottom > .devui-arrow, + &.bottom-left > .devui-arrow, + &.bottom-right > .devui-arrow { + top: -6px; + margin-left: 3px; + border-top-width: 0; + border-bottom-color: $devui-brand; + } + + &.bottom > .devui-arrow { + left: calc(50% - 4px); + transform: rotate(-135deg); + } + + &.bottom-right > .devui-arrow { + right: 23px; + transform: rotate(135deg); + } + + &.bottom-left > .devui-arrow { + left: 23px; + transform: rotate(-135deg); + } + + > .devui-shining-dot, + .devui-shining-plus { + position: absolute; + background: $devui-brand; + width: 6px; + height: 6px; + border-radius: $devui-border-radius-feedback; + } + + &.left > .devui-shining-dot, + &.left > .devui-shining-plus { + top: 21px; + right: -30px; + } + + &.right > .devui-shining-dot, + &.right > .devui-shining-plus { + top: 21px; + left: -30px; + } + + &.top > .devui-shining-dot, + &.top > .devui-shining-plus { + left: calc(50% - 3px); + bottom: -30px; + } + + &.top-left > .devui-shining-dot, + &.top-left > .devui-shining-plus { + left: 21px; + bottom: -30px; + } + + &.top-right > .devui-shining-dot, + &.top-right > .devui-shining-plus { + right: 21px; + bottom: -30px; + } + + &.bottom > .devui-shining-dot, + &.bottom > .devui-shining-plus { + left: calc(50% - 3px); + top: -30px; + } + + &.bottom-right > .devui-shining-dot, + &.bottom-right > .devui-shining-plus { + top: -30px; + right: 21px; + } + + &.bottom-left > .devui-shining-dot, + &.bottom-left > .devui-shining-plus { + top: -30px; + left: 21px; + } + + .devui-shining-plus { + animation: devui-glow 2s 0s infinite; + } + + .devui-guide-container { + position: relative; + + > .icon-close { + position: absolute; + top: 0; + right: 0; + cursor: pointer; + } + + .devui-ctrl { + display: flex; + flex-wrap: wrap; + + .devui-dots { + color: $devui-light-text; + position: relative; + top: 25px; + font-size: $devui-font-size; + height: 30px; + + > em { + opacity: 0.2; + margin: 0 5px 0 2px; + + &.devui-active { + opacity: 1; + } + } + } + + .devui-guide-btn { + display: flex; + flex-flow: row nowrap; + flex-grow: 1; + justify-content: flex-end; + padding: 20px 0 0 0; + white-space: nowrap; + + > div { + color: $devui-light-text; + background: rgba(255, 255, 255, 0.1); + border-radius: $devui-border-radius; + padding: 5px 15px; + cursor: pointer; + margin-left: 10px; + + &.devui-prev-step { + background: none; + border: solid 1px rgba(255, 255, 255, 0.1); + } + } + } + } + } +} + +/* 以下定义动画帧 */ +@keyframes devui-glow { + 0% { + transform: scale(1); + opacity: 0.5; + } + + 25% { + transform: scale(2); + opacity: 0.3; + } + + 50% { + transform: scale(3); + opacity: 0.1; + } + + 75% { + transform: scale(2); + opacity: 0.3; + } + + 100% { + transform: scale(1); + opacity: 0.5; + } +} diff --git a/devui/steps-guide/src/steps-guide.tsx b/devui/steps-guide/src/steps-guide.tsx new file mode 100644 index 0000000000000000000000000000000000000000..afaf90760176d1ccc7dbc46144b5014f455eeca6 --- /dev/null +++ b/devui/steps-guide/src/steps-guide.tsx @@ -0,0 +1,118 @@ +import './steps-guide.scss' + +import { defineComponent, Teleport, ref, onMounted, reactive, computed } from 'vue' +import { stepsGuideProps, StepsGuideProps, Step, positionConf } from './steps-guide-types' + +export default defineComponent({ + name: 'DStepsGuide', + props: stepsGuideProps, + emits: [], + setup(props: StepsGuideProps, ctx) { + const teleport = Teleport + const stepsRef = ref(null) + const stepsCount = computed(() => props.steps.length - 1) + const stepIndex = ref(0), showSteps = ref(true) + const currentStep = computed(() => { + const _step = props.steps[stepIndex.value] + _step.position = _step.position || 'top' + return _step + }) + const guideClassName = ['d-steps-guide'] + const position = reactive({ + left: '', + top: '', + zIndex: 1100 + }) + onMounted(() => { + updateGuidePosition() + }) + function updateGuidePosition(){ + const baseTop = window.pageYOffset - document.documentElement.clientTop + const baseLeft = window.pageXOffset - document.documentElement.clientLeft + const currentStepPosition = currentStep.value.position + let left, top + // 自定义 position位置 + if(typeof currentStepPosition !== 'string'){ + guideClassName[1] = currentStepPosition.type + + left = currentStepPosition.left + top = currentStepPosition.top + } else { + guideClassName[1] = currentStepPosition + const stepGuideElement = stepsRef.value + const triggerSelector = currentStep.value.target || currentStep.value.trigger + const triggerElement = document.querySelector(triggerSelector) + const targetRect = triggerElement.getBoundingClientRect() + left = targetRect.left + triggerElement.clientWidth / 2 - stepGuideElement.clientWidth / 2 + baseLeft + top = targetRect.top + triggerElement.clientHeight / 2 - stepGuideElement.clientHeight / 2 + baseTop + const positionTypes = currentStepPosition.split('-') + switch(positionTypes[0]){ + case 'top': + top += (-stepGuideElement.clientHeight / 2 - triggerElement.clientHeight) + break + case 'bottom': + top += (stepGuideElement.clientHeight / 2 + triggerElement.clientHeight) + break + case 'left': + top += (stepGuideElement.clientHeight / 2 - triggerElement.clientHeight) + left += (-stepGuideElement.clientWidth / 2 - triggerElement.clientWidth / 2) + break + case 'right': + top += (stepGuideElement.clientHeight / 2 - triggerElement.clientHeight) + left += (stepGuideElement.clientWidth / 2 + triggerElement.clientWidth / 2) + break + } + switch(positionTypes[1]){ + case 'left': + left += (stepGuideElement.clientWidth / 2 - triggerElement.clientWidth / 2) + break + case 'right': + left += (-stepGuideElement.clientWidth / 2 + triggerElement.clientWidth / 2) + break + } + } + position.left = left + 'px' + position.top = top + 'px' + } + function renderPrev(){ + return stepIndex.value > 0 ?
{ '上一步' }
: null + } + function renderNext(){ + return stepIndex.value === stepsCount.value ? +
{ '我知道啦' }
: +
{ '我知道啦,继续' }
+ } + function prevStep(){ + stepIndex.value-- + updateGuidePosition() + } + function nextStep(){ + stepIndex.value++ + updateGuidePosition() + } + function closeSteps(){ + showSteps.value = false + } + return () => showSteps.value ? + ( +
+
+
+
+
+

{ currentStep.value.title }

+
{ currentStep.value.content }
+
+
+ +
+
+ { renderPrev() } + { renderNext() } +
+
+
+
+
) : null + } +}) diff --git a/docs/components/steps-guide/index.md b/docs/components/steps-guide/index.md new file mode 100644 index 0000000000000000000000000000000000000000..d62c463c692e0fccd1c73663a44e40c1d457306a --- /dev/null +++ b/docs/components/steps-guide/index.md @@ -0,0 +1,174 @@ +# steps-guide 操作指引 + +引导用户了解业务使用逻辑组件。 + +### 何时使用 + +业务推出新特性,或复杂的业务逻辑需要指引用户时使用。 + +### 基本用法 +设定一组操作指引信息顺序显示。 +:::demo +```vue + + + +``` +::: + +### 弹出位置 +总共支持 8 个弹出位置。 +:::demo +```vue + + + +``` +::: + +### 自定义 +自定义操作指引信息弹出的位置和元素。 +:::demo +```vue + + +``` +::: + +### API + +d-steps-guide 参数 + +| 参数 | 类型 | 默认 | 说明 | 跳转 | +| ----- | ----- | ---- | ---------------------- | --------------------- | +| steps | array | Step[] | 必选,操作指引步骤数组 | [基本用法](#基本用法) | +