This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
@import "../popover/index";
.nut-tour {
&-mask {
position: fixed;
z-index: 1002;
width: 100px;
height: 50px;
border-radius: 10px;
box-shadow: 0 0 0 150vh rgb(0 0 0 / 50%);
&-none {
box-shadow: none;
}
&-hidden {
opacity: 0;
}
}
&-content {
display: block;
min-width: 200px;
padding: 10px 12px;
&-top {
display: block;
text-align: right;
&-close {
width: 10px;
height: 10px;
}
}
&-inner {
margin: 10px 0;
font-size: 14px;
}
&-bottom {
display: flex;
justify-content: space-between;
margin-top: 10px;
&-init {
margin-left: 10px;
}
&-operate {
display: flex;
justify-content: flex-end;
&-btn {
display: inline-block;
padding: 2px 4px;
margin-left: 4px;
font-size: 12px;
color: $text-color;
border: 1px solid $disable-color;
border-radius: 4px;
&.active {
color: #fff;
background: $primary-color;
border: 0;
}
}
}
}
&-tile {
.nut-tour-content-inner {
margin: 0;
}
}
}
&-masked {
position: fixed;
top: 0;
left: 0;
z-index: 2000;
width: 100vh;
height: 100vh;
background: transparent;
}
}

View File

@@ -0,0 +1 @@
export * from './tour'

View File

@@ -0,0 +1,104 @@
import type { ExtractPropTypes } from 'vue'
import { CHANGE_EVENT, CLOSE_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
import { commonProps, isBoolean, isNumber, makeArrayProp, makeNumberProp, makeNumericProp, makeStringProp, truthProp } from '../_utils'
import type { PopoverLocation, PopoverTheme } from '../popover/type'
export interface StepOptions {
target: string
content?: string
location?: PopoverLocation
popoverOffset?: number[]
arrowOffset?: number
}
export const tourProps = {
...commonProps,
/**
* @description 是否展示引导弹出层
*/
modelValue: Boolean,
/**
* @description 引导类型
* - 默认值为 'step'
*/
type: makeStringProp('step'),
/**
* @description 引导步骤内容
* - 类型为 `StepOptions[]`
* - 默认
*/
steps: makeArrayProp<StepOptions>([]),
/**
* @description 弹出层位置,同 Popopver 的location 属性
* - 默认值为 `'bottom'`
*/
location: makeStringProp<PopoverLocation>('bottom'),
/**
* @description 类型为 `step` 时,默认展示第几步
* - 默认值为 `0`
*/
current: makeNumberProp(0),
/**
* @description 下一步按钮文案
* - 默认值为 `'下一步'`
*/
nextStepTxt: makeStringProp('下一步'),
/**
* @description 上一步按钮文案
* - 默认值为 `'上一步'`
*/
prevStepTxt: makeStringProp('上一步'),
/**
* @description 完成按钮文案
* - 默认值为 `'完成'`
*/
completeTxt: makeStringProp('完成'),
/**
* @description 是否显示镂空遮罩
* - 默认值为 `true`
*/
mask: truthProp,
/**
* @description 镂空遮罩相对于目标元素的偏移量
* - 默认值为 `[8, 10]`
*/
offset: makeArrayProp<number>([8, 10]),
/**
* @description 自定义背景色
* - 默认值为 `''`
*/
bgColor: String,
/**
* @description 气泡遮罩层主题,同 Popopver 的theme 属性
*/
theme: makeStringProp<PopoverTheme>('light'),
/**
* @description 镂空遮罩层宽度
*/
maskWidth: makeNumericProp(''),
/**
* @description 镂空遮罩层高度
*/
maskHeight: makeNumericProp(''),
/**
* @description 是否在点击镂空遮罩层后关闭,同 Popopver 的closeOnClickOverlay 属性
*/
closeOnClickOverlay: truthProp,
/**
* @description 是否展示上一步按钮
*/
showPrevStep: truthProp,
/**
* @description 是否展示标题栏
*/
showTitleBar: truthProp,
}
export type TourProps = ExtractPropTypes<typeof tourProps>
export const tourEmits = {
[UPDATE_MODEL_EVENT]: (val: boolean) => isBoolean(val),
[CLOSE_EVENT]: (val: number) => isNumber(val),
[CHANGE_EVENT]: (val: number) => isNumber(val),
}
export type TourEmits = typeof tourEmits

View File

@@ -0,0 +1,223 @@
<script setup lang="ts">
import type { ComponentInternalInstance } from 'vue'
import { computed, defineComponent, getCurrentInstance, onMounted, reactive, ref, watch } from 'vue'
import { CHANGE_EVENT, CLOSE_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
import { useRect } from '../_hooks'
import { getMainClass, getRandomId } from '../_utils'
import NutIcon from '../icon/icon.vue'
import NutPopover from '../popover/popover.vue'
import { tourEmits, tourProps } from './tour'
const props = defineProps(tourProps)
const emit = defineEmits(tourEmits)
const instance = getCurrentInstance() as ComponentInternalInstance
const refRandomId = getRandomId()
const state = reactive({
showTour: props.modelValue,
active: 0,
})
const showPopup = ref([false])
const maskRect: any[] = []
const maskStyles = ref<any[]>([])
const classes = computed(() => {
return getMainClass(props, componentName)
})
function maskStyle(index: number) {
const { offset, maskWidth, maskHeight } = props
if (!maskRect[index])
return {}
const { width, height, left, top } = maskRect[index]
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`,
}
maskStyles.value[index] = styles
}
function changeStep(type: string) {
const current = state.active
let next = current
if (type === 'next')
next = current + 1
else
next = current - 1
showPopup.value[current] = false
setTimeout(() => {
showPopup.value[next] = true
state.active = next
}, 300)
emit(CHANGE_EVENT, state.active)
}
function getRootPosition() {
props.steps.forEach(async (item, i) => {
let rect
// #ifdef H5
rect = await useRect(item.target)
// #endif
// #ifndef H5
// TODO uniapp微信小程序无法实现获取不到组件外节点的信息
rect = await useRect(item.target, instance.root)
if (rect.left! < 0)
rect.left = -rect.left!
if (rect.top! < 0)
rect.top = -rect.top!
if (rect.right! < 0)
rect.right = -rect.right!
if (rect.bottom! < 0)
rect.bottom = -rect.bottom!
// #endif
maskRect[i] = rect
maskStyle(i)
})
}
function close() {
state.showTour = false
showPopup.value[state.active] = false
emit(CLOSE_EVENT, state.active)
emit(UPDATE_MODEL_EVENT, false)
}
function handleClickMask() {
props.closeOnClickOverlay && close()
}
onMounted(() => {
setTimeout(() => {
getRootPosition()
}, 500)
})
watch(
() => props.modelValue,
(val) => {
if (val) {
state.active = 0
getRootPosition()
}
state.showTour = val
showPopup.value[state.active] = val
},
)
</script>
<script lang="ts">
const componentName = `${PREFIX}-tour`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="classes" :style="customStyle">
<view v-if="state.showTour" class="nut-tour-masked" @click="handleClickMask" />
<view v-for="(step, i) in steps" :key="i" style="height: 0;">
<view
v-if="state.showTour"
:id="`nut-tour-popid${i}${refRandomId}`"
class="nut-tour-mask"
:class="[mask ? (showPopup[i] ? '' : 'nut-tour-mask-hidden') : 'nut-tour-mask-none']"
:style="maskStyles[i]"
/>
<NutPopover
v-if="state.showTour"
v-model:visible="showPopup[i]"
:location="step.location || location"
:target-id="`nut-tour-popid${i}${refRandomId}`"
:bg-color="bgColor"
:theme="theme"
:close-on-click-outside="false"
:offset="step.popoverOffset || [0, 12]"
:arrow-offset="step.arrowOffset || 0"
:duration="200"
>
<template #content>
<slot>
<view v-if="type === 'step'" class="nut-tour-content">
<view v-if="showTitleBar" class="nut-tour-content-top">
<view @click="close">
<NutIcon name="close" class="nut-tour-content-top-close" size="10px" />
</view>
</view>
<view class="nut-tour-content-inner">
{{ step.content }}
</view>
<view class="nut-tour-content-bottom">
<view class="nut-tour-content-bottom-init">
{{ state.active + 1 }}/{{ steps.length }}
</view>
<view class="nut-tour-content-bottom-operate">
<slot name="prev-step">
<view
v-if="state.active !== 0 && showPrevStep"
class="nut-tour-content-bottom-operate-btn"
@click="changeStep('prev')"
>
{{ prevStepTxt }}
</view>
</slot>
<view
v-if="steps.length - 1 === state.active"
class="nut-tour-content-bottom-operate-btn active"
@click="close"
>
{{ completeTxt }}
</view>
<slot name="next-step">
<view
v-if="steps.length - 1 !== state.active"
class="nut-tour-content-bottom-operate-btn active"
@click="changeStep('next')"
>
{{ nextStepTxt }}
</view>
</slot>
</view>
</view>
</view>
<view v-if="type === 'tile'" class="nut-tour-content nut-tour-content-tile">
<view class="nut-tour-content-inner">
{{ step.content }}
</view>
</view>
</slot>
</template>
</NutPopover>
</view>
</view>
</template>
<style lang="scss">
@import './index';
</style>