init
This commit is contained in:
88
uni_modules/nutui-uni/components/tour/index.scss
Normal file
88
uni_modules/nutui-uni/components/tour/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/tour/index.ts
Normal file
1
uni_modules/nutui-uni/components/tour/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './tour'
|
||||
104
uni_modules/nutui-uni/components/tour/tour.ts
Normal file
104
uni_modules/nutui-uni/components/tour/tour.ts
Normal 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
|
||||
223
uni_modules/nutui-uni/components/tour/tour.vue
Normal file
223
uni_modules/nutui-uni/components/tour/tour.vue
Normal 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>
|
||||
Reference in New Issue
Block a user