因为公司网络问题,只得将GitHub找到的优秀源码放到帖子上,在公司不忙时来学习。
Select
<template>
<div class="x-select">
<div
class="x-select-input"
@click.prevent="toggle"
@mouseover="mouseover"
@mouseout="mouseout"
>
<div class="x-select-array">
<template v-if="multiple">
<div class="x-select-array-content" v-if="state.length">
<span class="x-con-array">{{ state[0] }}</span>
<span class="x-clearable-array" @click.stop="handelClear">
<i class="x-icon-x"></i>
</span>
</div>
<span v-if="state.length > 1">+ {{ state.length - 1 }}</span>
</template>
<div v-if="!multiple && state.length">{{ state }}</div>
</div>
<input
readonly
:placeholder="state.length ? '' : placeholder"
class="x-input"
:class="{
'is-focus': isShow,
'is-blur': !isShow,
}"
@focus="focus"
/>
<i class="x-arrow" v-show="!isClear" :class="{ 'is-active': isShow }"></i>
<div class="x-clearable" v-show="isClear" @click.stop="handelClear">
<i class="x-icon-x"></i>
</div>
</div>
<teleport to="body">
<transition name="scaleY" ref="trigger">
<div
class="x-trigger x-select-option"
@click.stop
:style="rect"
v-show="isShow"
>
<div class="x-select-search" v-if="search">
<div class="x-from-input x-input-icon-before">
<i class="x-before x-icon-search"></i>
<input
class="x-input x-input-sm"
v-model="searchValue"
@click.stop
/>
</div>
</div>
<div class="x-select-item">
<slot></slot>
</div>
</div>
</transition>
</teleport>
</div>
</template>
<script>
import {
ref,
getCurrentInstance,
reactive,
provide,
computed,
watch,
} from 'vue'
import useToggle from '../../utils/togger'
import emitter from '../../utils/emiter'
import { isArray } from '../../utils/isType'
export default {
name: 'Select',
props: {
modelValue: [Array, String],
placeholder: String,
search: [String, Boolean],
},
setup(props, { emit }) {
provide('Select', getCurrentInstance())
const { toggle, isShow, focus, rect, trigger, hide } = useToggle()
const searchValue = ref('')
watch(isShow, (value) => {
if (value) {
searchValue.value = ''
}
})
const multiple = computed(() => isArray(props.modelValue))
const state = multiple.value ? reactive([]) : ref('')
const { on, broadcast } = emitter()
on('selectOption', ({ label, value, checked }) => {
emit('update:modelValue', value)
if (multiple.value) {
const labelIndex = state.indexOf(label)
labelIndex === -1 && checked === true && state.unshift(label)
labelIndex !== -1 && checked === false && state.splice(labelIndex, 1)
} else {
state.value = label
hide()
}
})
on('selectDefault', ({ label, value, checked }) => {
if (checked) {
if (multiple.value) {
state.unshift(label)
} else {
state.value = label
}
}
})
const clear = useClear(state, multiple.value)
const handelClear = () => {
if (multiple.value) {
const modelValue = props.modelValue
modelValue.pop()
state.shift() // state 用的unshift插入 所以需要从第一个开始删
emit('update:modelValue', modelValue)
} else {
state.value = ''
emit('update:modelValue', '')
}
}
if (props.search) {
let time
watch(searchValue, (value) => {
clearTimeout(time)
time = setTimeout(() => {
broadcast('search', value)
}, 100)
})
}
return {
focus,
rect,
multiple,
state,
trigger,
searchValue,
toggle,
isShow,
handelClear,
...clear,
}
},
}
function useClear(state, multiple) {
const isClear = ref(false)
const mouseover = () => {
if (!multiple && state.value.length) {
isClear.value = true
}
}
const mouseout = () => {
if (!multiple) {
isClear.value = false
}
}
return {
isClear,
mouseover,
mouseout,
}
}
</script>
<style lang="less">
.x-select {
position: relative;
display: inline-block;
user-select: none;
.x-select-input{
position: relative;
cursor: pointer;
&.x-select-input-disabled{
cursor: not-allowed;
.x-input{
cursor: not-allowed;
}
}
&>.x-input{
cursor: pointer;
}
&>i{
right: 10px;
&.x-arrow{
position: absolute;
}
}
.x-clearable{
position: absolute;
z-index: 1;
height: 100%;
top: 0;
width: 30px;
align-items: center;
justify-content: center;
display: flex;
i{
transition: all 0.25s ease-in-out;
color: var(--line-color);
}
right: 0;
cursor: pointer;
}
.x-select-array{
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 30px;
padding-left: 8px;
flex-wrap: wrap;
display: flex;
align-items: center;
pointer-events: none;
&>span{
padding: 2px 8px;
background-color: var(--line-color);
border-radius: 4px;
}
.x-select-array-content{
max-width: 70%;
padding: 2px 8px;
background-color: var(--line-color);
border-radius: 4px;
margin-right: 5px;
display: flex;
pointer-events: all;
&>span {
float: left;
}
.x-con-array{
overflow: hidden;
flex:1;
display: block;
white-space:nowrap;flex:1;
text-overflow: ellipsis;
}
.x-clearable-array{
width: 14px;
width: auto;
display: block;
margin-left: 5px;
cursor: pointer;
}
}
}
}
}
.x-select-option{
.x-option{
white-space: nowrap;
cursor: pointer;
padding-left: 25px;
padding: 0 12px;
height: 30px;
line-height: 30px;
overflow: hidden;
color: var(--sub-text-color);
&.is-checked{
color: rgb(var(--primary));
background-color: rgb(var(--line-color), .1);
font-weight: bold;
}
&:hover{
background-color: rgb(var(--primary), .2)
}
// 禁用状态
&.is-disabled{
cursor: no-drop;
color: var(--sub-text-color);
opacity: .4;
&:hover{
background-color: initial;
}
}
}
.x-select-item{
max-height: 220px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 8px; /*高宽分别对应横竖滚动条的尺寸*/
height: 1px;
}
&::-webkit-scrollbar-thumb {
border-radius: 6px;
// -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
background: var(--line-color);
}
&::-webkit-scrollbar-track {
// -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
border-radius: 5px;
background: transparent;
}
}
.x-select-search{
padding: 10px;
}
}
</style>
Option
<template>
<CollapseTransition>
<div
class="x-option"
:class="{
'is-checked': isChecked,
'is-disabled': disabled,
}"
@click.stop="handerClick"
v-show="isShow"
>
{{ label || value }}
</div>
</CollapseTransition>
</template>
<script>
import {
inject,
computed,
watchEffect,
reactive,
ref,
getCurrentInstance,
} from 'vue'
import CollapseTransition from '../transitions/collapse-transition.vue'
import emitter from '../../utils/emiter'
import { isArray } from '../../utils/isType'
export default {
name: 'Option',
props: {
value: String,
label: String,
disabled: Boolean,
},
components: {
CollapseTransition,
},
setup(props) {
const { value, label, disabled } = props
const { dispatch, on } = emitter()
const uid = getCurrentInstance().uid
const Select = inject('Select', { props: {} })
const state = reactive({
modelValue: null,
})
watchEffect(() => {
state.modelValue = Select.props.modelValue
})
const model = computed({
get() {
return state.modelValue
},
set({ checked, value }) {
if (isArray(model.value)) {
const modelValue = model.value
const labelIndex = modelValue.indexOf(value)
labelIndex === -1 && checked === true && modelValue.push(value)
labelIndex !== -1 &&
checked === false &&
modelValue.splice(labelIndex, 1)
state.modelValue = modelValue
dispatch('selectOption', {
value: modelValue,
label,
checked,
})
} else {
state.modelValue = value
dispatch('selectOption', {
value,
label,
checked,
})
}
},
})
const isChecked = computed(() => {
if (isArray(model.value)) {
return model.value.includes(value)
} else {
return model.value === value
}
})
const handerClick = () => {
if (disabled) return
model.value = {
checked: !isChecked.value,
value,
}
}
dispatch('selectDefault', {
value,
label,
checked: isChecked.value,
})
const isShow = ref(true)
on('search', (searchValue) => {
isShow.value = props.value.search(searchValue) > -1
})
return {
handerClick,
isShow,
isChecked,
uid,
}
},
}
</script>
Slider
<template>
<div class="x-slider" @mouseenter="handleEnter" @mouseleave="handleLeave">
<tooltip v-if="range" :content="start.num" v-model="isTooltip">
<div
class="x-slider-dot"
:style="{
left: `${start.x}%`,
}"
@mousedown="handleDown($event, 'start')"
></div>
</tooltip>
<tooltip :content="end.num" v-model="isTooltip">
<div
class="x-slider-dot"
:style="{
left: `${end.x}%`,
}"
@mousedown="handleDown($event, 'end')"
></div>
</tooltip>
<div class="x-slider-bar" :style="propress"></div>
<div class="x-slider-step" v-if="steps > 0">
<i v-for="i in steps" :key="i"></i>
</div>
</div>
</template>
<script>
import {
reactive,
ref,
toRefs,
getCurrentInstance,
onMounted,
computed,
watchEffect,
} from 'vue'
import { on, off } from '../../utils/dom'
import tooltip from '../tooltip/index'
import { isArray } from '../../utils/isType'
export default {
name: 'Slider',
components: {
tooltip,
},
props: {
modelValue: [Number, Array],
max: {
type: Number,
default: 100,
},
min: {
type: Number,
default: 0,
},
step: Boolean,
showTooltip: Boolean,
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const { modelValue, max, min, step, showTooltip } = toRefs(props)
const instance = getCurrentInstance()
const propress = reactive({})
const isTooltip = ref(false)
const space = ref(0)
const range = ref(isArray(modelValue.value))
const steps = ref(0)
const isHover = ref(false)
const isDrag = ref(false)
const start = reactive({
num: range.value ? modelValue.value[0] : 0,
})
const end = reactive({
num: range.value ? modelValue.value[1] : modelValue.value,
})
const state = reactive({
modelValue: null,
})
const model = computed({
get() {
return state.modelValue
},
set({ start, end }) {
if (range.value) {
const modelValue = model.value
modelValue[0] = start
modelValue[1] = end
emit('update:modelValue', state.modelValue)
} else {
state.modelValue = end
emit('update:modelValue', state.modelValue)
}
},
})
onMounted(() => {
useSpace()
useSlider()
window.addEventListener('resize', () => {
useSpace()
useSlider()
})
})
const usePos = (dot) =>
((dot.num - min.value) / (max.value - min.value)) * 100
watchEffect(() => {
isTooltip.value = isDrag.value || isHover.value || showTooltip.value
state.modelValue = modelValue.value
if (!range.value) {
end.num = modelValue.value
end.x = usePos(end)
propress.width = end.x + '%'
}
})
const useSpace = () => {
const el = instance.vnode.el
propress.maxWidth = el.getBoundingClientRect().width
space.value = propress.maxWidth / (max.value - min.value)
step.value && (steps.value = max.value - min.value + 1)
}
const useSlider = () => {
start.x = range.value ? usePos(start) : 0
end.x = usePos(end)
let a = start
let b = end
if (end.x >= start.x) {
a = end
b = start
}
propress.width = a.x - b.x + '%'
propress.left = b.x + '%'
model.value = {
start: b.num,
end: a.num,
}
}
const handleDown = (e, type) => {
const dot = type === 'start' ? start : end
const touchX = e.screenX - dot.num * space.value + space.value * min.value
isDrag.value = true
on(document, 'mousemove', move)
on(document, 'mouseup', () => {
isDrag.value = false
off(document, 'mousemove', move)
})
function move(e) {
e.preventDefault()
let mx = e.screenX - touchX
mx < 0 && (mx = 0)
mx > propress.maxWidth && (mx = propress.maxWidth)
const num = Math.round(mx / space.value) + min.value
dot.num = num
useSlider()
}
}
function handleEnter() {
isHover.value = true
}
function handleLeave() {
isHover.value = false
}
return {
handleDown,
handleEnter,
handleLeave,
range,
end,
start,
steps,
propress,
isTooltip,
}
},
}
</script>
<style lang="less">
.x-slider{
height: 6px;
background-color: var(--hover-background-color);
border-radius: 2em;
cursor: pointer;
position: relative;
margin: 14px 6px 10px;
.x-slider-dot{
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: rgb(var(--white));
border: 2px solid rgb(var(--primary));
margin-top: -3px;
z-index: 3;
margin-left: -6px;
transition: box-shadow .3s ease;
&:active{
box-shadow: 0 0 0 4px rgb(var(--primary), .4);
}
}
.x-slider-bar{
position: absolute;
top: 0;
bottom: 0;
z-index: 2;
border-radius: 2em;
background-color: rgb(var(--primary));
}
.x-slider-step{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
display: flex;
justify-content: space-between;
i{
display: block;
width: 6px;
height: 6px;
background: var(--shadow);
border-radius: 50%;
&:nth-child(1),&:nth-last-child(1){
visibility: hidden;
}
}
}
}
</style>
Step
<template>
<div class="steps-item" :class="className">
<div class="step-icon">
<i :class="icon" v-if="icon"></i>
<template v-else>
<i class="x-icon-check" v-if="success"></i>
<i v-else>{{ num + 1 }}</i>
</template>
</div>
<div class="step-line"></div>
<div class="step-title">
<span>
{{ title }}
</span>
<p>
{{ description }}
</p>
<slot></slot>
</div>
</div>
</template>
<script>
import emitter from "../../utils/emiter";
import { computed, getCurrentInstance, inject, ref } from "vue";
export default {
name: "Step",
props: {
title: String,
description: String,
icon: String,
},
setup() {
const { dispatch } = emitter();
const instance = getCurrentInstance();
dispatch("step-item", instance.uid);
const steps = inject("steps");
const num = ref(steps.indexOf(instance.uid));
const active = inject("step-active");
const current = computed(() => active.value === num.value);
const success = computed(() => active.value > num.value);
const className = computed(() => ({
current: current.value,
success: success.value,
}));
return {
num,
success,
className,
};
},
};
</script>
Steps
<template>
<div class="x-steps">
<slot></slot>
</div>
</template>
<script>
import emitter from "../../utils/emiter";
import { reactive, watchEffect, provide, toRefs, ref } from "vue";
export default {
name: "Steps",
props: {
current: Number,
},
setup(props) {
const { on } = emitter();
const items = reactive([]);
const { current } = toRefs(props);
const active = ref(0);
watchEffect(() => {
current && (active.value = current.value);
});
on("step-item", (uid) => {
items.push(uid);
});
provide("steps", items);
provide("step-active", active);
},
};
</script>
<style lang="less">
.x-steps {
position: relative;
display: flex;
.steps-item {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
--color: rgba(var(--primary), 1);
&.current {
.step-title,
.step-icon {
color: var(--color);
}
}
&.success {
color: var(--color);
.step-icon {
color: #fff;
background-color: var(--color);
border-color: var(--color);
}
}
color: #ccc;
.step-line {
position: absolute;
height: 1px;
width: calc(100% - 10px);
left: 0;
top: 13px;
background-color: currentColor;
}
.step-icon {
overflow: hidden;
border: 1px solid currentColor;
display: flex;
width: 25px;
height: 25px;
border-radius: 50%;
align-items: center;
justify-content: center;
background-color: #fff;
position: relative;
z-index: 1;
i {
font-style: normal;
}
}
.step-title {
margin-top: 5px;
font-size: 12px;
text-align: center;
}
&:not(:first-child):not(:last-child) {
.step-icon,
.step-title {
transform: translateX(-50%);
}
}
&:first-child {
align-items: flex-start;
.step-title {
text-align: left;
}
}
&:last-child {
position: absolute;
right: 1px;
width: auto;
text-align: right;
align-items: flex-end;
.step-line {
display: none;
}
.step-title {
text-align: right;
}
}
}
}
</style>
SubMenu
<template>
<li class="x-submenu" :class="{ 'is-active': isActive }" @click="handleClick">
<div class="x-menu-title">
<slot name="title"></slot>
<i class="x-arrow" :class="{ 'is-active': isActive }"></i>
</div>
<transition name="scaleY" ref="trigger" v-if="horizontal">
<ul class="x-menu" v-show="isActive">
<slot></slot>
</ul>
</transition>
<CollapseTransition v-else>
<ul class="x-menu" v-show="isActive">
<slot></slot>
</ul>
</CollapseTransition>
</li>
</template>
<script>
import { inject, ref, toRefs, watch, getCurrentInstance, onMounted } from 'vue'
import CollapseTransition from '../transitions/collapse-transition.vue'
import emiter from '../../utils/emiter'
export default {
name: 'SubMenu',
components: {
CollapseTransition,
},
props: {
name: [String, Number],
},
setup(props) {
const { name } = toRefs(props)
const { dispatch, on } = emiter()
const menu = inject('menu', { props: {} })
const isActive = ref(false)
const isChild = ref('')
const Instance = getCurrentInstance()
const horizontal = menu.props.mode === 'horizontal'
watch(menu.currName, (value) => {
if (menu.props.uniqueOpened) {
if (value !== name.value) {
isActive.value = false
}
if (isChild.value === value) {
isActive.value = true
}
}
if (horizontal) {
if (value !== name.value) {
isActive.value = false
}
}
})
on('item-click', (item) => {
isChild.value = item
})
const handleClick = () => {
dispatch('item-click', name.value)
isActive.value = !isActive.value
}
onMounted(() => {
if (horizontal) {
document.addEventListener('click', (e) => {
const el = Instance.vnode.el
if (!el.contains(e.target)) {
isActive.value = false
}
})
}
})
return {
horizontal,
handleClick,
isActive,
}
},
}
</script>
Switch
<template>
<button
class="x-switch"
@click="handerClick"
:class="[
`x-switch-${type}`,
{
'x-switch-checked': modelValue,
'x-switch-disabled': disabled,
},
]"
>
<span class="x-switch-inner">
<slot v-if="modelValue" name="open"></slot>
<slot v-else name="close"></slot>
</span>
</button>
</template>
<script>
import { toRefs } from 'vue'
export default {
name: 'Switch',
props: {
type: {
type: String,
default: 'primary',
validator: (value) =>
['success', 'primary', 'warning', 'info', 'danger'].includes(value),
},
disabled: Boolean,
modelValue: Boolean,
},
setup(props, { emit }) {
const { modelValue, disabled } = toRefs(props)
const handerClick = () => {
if (disabled.value) {
return
}
emit('update:modelValue', !modelValue.value)
emit('change', !modelValue.value)
}
return {
handerClick,
}
},
}
</script>
<style lang="less">
.x-switch {
min-width: 44px;
height: 22px;
position: relative;
border: none;
border-radius: 20px;
display: inline-block;
cursor: pointer;
outline: none;
text-align: right;
transition: all .3s ease;
color: rgb(var(--white));
font-size: 12px;
padding-left: 25px;
padding-right: 10px;
background-color: var(--hover-background-color);
&::before{
content: "";
width: 16px;
height: 16px;
position: absolute;
top: 3px;
left: 3px;
border-radius: 20px;
overflow: hidden;
background-color: transparent;
border: 3px solid #FFF;
transform: translateX(0);
transition: .3s;
}
&.x-switch-disabled{
opacity: 0.4;
cursor: not-allowed;
}
&.x-switch-checked{
text-align: left;
padding-right: 25px;
padding-left: 10px;
&.x-switch-primary{
background-color: rgb(var(--primary));
}
&.x-switch-info{
background-color: rgb(var(--info));
}
&.x-switch-danger{
background-color: rgb(var(--danger));
}
&.x-switch-success{
background-color: rgb(var(--success));
}
&.x-switch-warning{
background-color: rgb(var(--warning));
}
&.x-switch-disabled{
&::before{
border-color: #fff;
background-color: #FFF;
}
}
&::before{
left: 100%;
margin-left: -3px;
background-color: #FFF;
transform: translateX(-100%)
}
}
&:active::before{
width: 21px;
}
}
</style>
TabPane
<template>
<div class="x-tabs-item" v-show="isShow">
<slot></slot>
</div>
</template>
<script>
import { computed, inject } from 'vue'
import emtter from '../../utils/emiter'
export default {
name: 'TabPane',
props: {
label: String,
disabled: Boolean,
},
setup(props) {
const { label } = props
const { dispatch } = emtter()
const active = inject('active')
dispatch('props', props)
const isShow = computed(() => {
return active.label === label
})
return {
isShow,
}
},
}
</script>
Tabs
<template>
<div class="x-tabs">
<div class="x-tabs-nav">
<div
class="x-tabs-menu"
:ref="setItemRef"
v-for="(item, i) in nav"
:key="i"
@click="setTabs(item, i)"
:class="[
item.disabled && 'x-tabs-disabled',
{
active: active.index === i,
},
]"
>
{{ item.label }}
</div>
<i class="x-tabs-line" :style="line"></i>
</div>
<div class="x-tabs-view">
<slot></slot>
</div>
</div>
</template>
<script>
import { provide, reactive, onBeforeUpdate, onMounted, nextTick } from 'vue'
import emtter from '../../utils/emiter'
export default {
name: 'Tabs',
setup() {
const { on } = emtter()
const nav = reactive([])
const active = reactive({})
const line = reactive({})
provide('active', active)
on('props', (value) => {
if (!active.label) {
setTabs(value, 0)
}
nav.push(value)
})
const setTabs = (item, i) => {
if (item.disabled) return
active.label = item.label
active.index = i
nextTick(() => {
const e = itemRefs[i].getBoundingClientRect()
line.width = e.width + 'px'
line.transform = ` translateX(${itemRefs[i].offsetLeft}px)`
})
}
let itemRefs = []
const setItemRef = (el) => {
itemRefs.push(el)
}
onBeforeUpdate(() => {
itemRefs = []
})
return {
nav,
active,
setTabs,
setItemRef,
line,
}
},
}
</script>
<style lang="less">
.x-tabs{
.x-tabs-nav{
display: flex;
border-bottom: 1px solid var(--hover-background-color);
position: relative;
.x-tabs-menu{
padding: 5px 10px;
margin-right: 10px;
cursor: pointer;
user-select: none;
&.active{
color: rgba(var(--primary));
}
&.x-tabs-disabled{
cursor: not-allowed;
color: var(--hover-background-color);
}
}
.x-tabs-line{
position: absolute;
bottom: -1px;
left: 0;
height: 2px;
display: block;
background-color: rgba(var(--primary));
transition: all .2s ease-in-out;
transform: translateX();
}
}
.x-tabs-view{
padding: 10px;
}
}
</style>
Tooltip
import {
getCurrentInstance,
onMounted,
onUnmounted,
toRefs,
ref,
watchEffect,
nextTick,
} from 'vue'
import { on, off, remove, add } from '../../utils/dom'
import { isNull } from '../../utils/isType'
export default {
name: 'Tooltip',
props: {
placement: {
type: String,
default: 'top',
validator: (value) =>
[
'left-start',
'left',
'left-end',
'top-start',
'top',
'top-end',
'right-start',
'right',
'right-end',
'bottom-start',
'bottom',
'bottom-end',
].includes(value),
},
modelValue: {
type: Boolean,
default: null,
},
width: String,
content: [String, Number],
},
setup(props, { slots }) {
const instance = getCurrentInstance()
const { placement, content, width, modelValue } = toRefs(props)
const isShow = ref(modelValue.value)
function getFirstElement() {
const slotsDefault = slots.default()
if (!Array.isArray(slotsDefault)) return null
let element = null
for (let index = 0; index < slotsDefault.length; index++) {
if (slotsDefault[index] && slotsDefault[index].type) {
element = slotsDefault[index]
}
}
return element
}
const tip = document.createElement('div')
tip.className = `x-tooltip x-tooltip-${placement.value}`
width && (tip.style.width = width.value)
const tid = (tip.id = `x-tooltip-${instance.uid}`)
function hide() {
const el = document.getElementById(tid)
if (el) {
remove(el, 'x-tooltip-show')
on(el, 'transitionend', none)
}
}
function update() {
const Rect = instance.proxy.$el.getBoundingClientRect()
const el = document.getElementById(tid)
if (!el) {
document.body.appendChild(tip)
}
off(tip, 'transitionend', none)
tip.style.display = 'block'
const { x, y } = calcStyle(Rect, tip, placement.value)
tip.style.top = y + 'px'
tip.style.left = x + 'px'
}
function show() {
tip && add(tip, 'x-tooltip-show')
}
const none = () => {
document.getElementById(tid).style.display = 'none'
}
onMounted(() => {
const el = instance.proxy.$el
watchEffect(() => {
tip.innerHTML = `<span>${content.value}</span>` || ''
nextTick(update)
if (!isNull(props.modelValue)) {
isShow.value = modelValue.value
}
if (isShow.value) {
show()
} else {
hide()
}
})
on(el, 'mouseenter', () => {
if (!modelValue && !modelValue.value) return
isShow.value = true
})
on(el, 'mouseleave', () => {
if (modelValue && modelValue.value) return
isShow.value = false
})
on(window, 'resize', update)
})
onUnmounted(() => {
const el = document.getElementById(tid)
el && document.body.removeChild(tip)
off(window, 'resize', update)
})
return () => {
return getFirstElement()
}
},
}
function calcStyle(Rect, tip, key) {
let y = document.documentElement.scrollTop
let x = 0
const placement = {
'top-start': () => {
x += Rect.x
y += Rect.y - tip.offsetHeight
},
top: () => {
x += Rect.x + (Rect.width - tip.offsetWidth) * 0.5
y += Rect.y - tip.offsetHeight
},
'top-end': () => {
x += Rect.x + Rect.width - tip.offsetWidth
y += Rect.y - tip.offsetHeight
},
'left-start': () => {
x += Rect.x - tip.offsetWidth
y += Rect.y
},
left: () => {
x += Rect.x - tip.offsetWidth
y += Rect.y + (Rect.height - tip.offsetHeight) * 0.5
},
'left-end': () => {
x += Rect.x - tip.offsetWidth
y += Rect.y + Rect.height - tip.offsetHeight
},
'right-start': () => {
x += Rect.x + Rect.width
y += Rect.y
},
right: () => {
x += Rect.x + Rect.width
y += Rect.y + (Rect.height - tip.offsetHeight) * 0.5
},
'right-end': () => {
x += Rect.x + Rect.width
y += Rect.y + Rect.height - tip.offsetHeight
},
'bottom-start': () => {
x += Rect.x
y += Rect.y + Rect.height
},
bottom: () => {
x += Rect.x + (Rect.width - tip.offsetWidth) * 0.5
y += Rect.y + Rect.height
},
'bottom-end': () => {
x += Rect.x + Rect.width - tip.offsetWidth
y += Rect.y + Rect.height
},
}
placement[key]()
return { x, y }
}
<style lang="less">
.x-tooltip{
position: absolute;
--color: var(--main-text-color);
pointer-events: none;
z-index: 995;
opacity: 0;
visibility: hidden;
transition-property: opacity,transform,visibility;
transition-duration: .2s;
transition-timing-function: ease;
&>span{
padding: 6px 10px;
border-radius: 3px;
background-color: var(--color);
color: var(--main-background-color);
display: block;
line-height: 1.3;
}
&.x-tooltip-show{
opacity: 1;
visibility: visible;
}
&.x-tooltip-top, &.x-tooltip-top-start, &.x-tooltip-top-end{
padding-bottom: 8px;
&::after{
content: "";
position: absolute;
bottom: 3px;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid var(--color);
}
}
&.x-tooltip-top-start, &.x-tooltip-bottom-start{
&::after{
left: 10px;
}
}
&.x-tooltip-top, &.x-tooltip-bottom{
&::after{
left: 50%;
transform: translateX(-50%);
}
}
&.x-tooltip-top-end, &.x-tooltip-bottom-end{
&::after{
right: 10px;
}
}
&.x-tooltip-bottom-start, &.x-tooltip-bottom, &.x-tooltip-bottom-end{
padding-top: 8px;
&::after{
content: "";
position: absolute;
top: 3px;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 5px solid var(--color);
}
}
&.x-tooltip-left, &.x-tooltip-left-start, &.x-tooltip-left-end{
padding-right: 8px;
transform-origin: right center;
&::after{
content: "";
position: absolute;
right: 3px;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 5px solid var(--color);
}
}
&.x-tooltip-left-start, &.x-tooltip-right-start {
&::after{
top: 10px;
}
}
&.x-tooltip-left, &.x-tooltip-right{
&::after{
top: 50%;
transform: translateY(-50%);
}
}
&.x-tooltip-left-end, &.x-tooltip-right-end{
&::after{
bottom: 10px;
}
}
&.x-tooltip-right, &.x-tooltip-right-start, &.x-tooltip-right-end{
padding-left: 8px;
&::after{
content: "";
position: absolute;
left: 3px;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-right: 5px solid var(--color);
}
}
&[class*='x-tooltip-top']{
transform: translateY(5px);
&.x-tooltip-show{
transform: translateY(0);
}
}
&[class*='x-tooltip-left']{
transform: translateX(5px);
&.x-tooltip-show{
transform: translateY(0);
}
}
&[class*='x-tooltip-right']{
transform: translateX(-5px);
&.x-tooltip-show{
transform: translateY(0);
}
}
&[class*='x-tooltip-bottom']{
transform: translateY(-5px);
&.x-tooltip-show{
transform: translateY(0);
}
}
}
</style>