先上代码
<template>
<div v-if="visible" class="prompt" ref="s_prompt" :style="`width:${getWidth() && '100px'};
max-height:${getMaxHeight() ?? 'auto'};
left:${shifting.x}px;
top:${shifting.y}px;
display:${shifting.display};
opacity:${shifting.opacity};
`">
<!-- 接收一个插槽 如果插槽自定义?? 用内置列表进行渲染 -->
<template v-if="$slots.custom">
<slot name="custom"></slot>
</template>
<ul v-else>
<template v-if="list.length">
<li v-for="(item) in list" :key="item.key" @click="handleClick(item)">{{ item.label }}</li>
</template>
<li v-else>无数据</li>
</ul>
</div>
</template>
<script>
// 1可以点击的数据源
export default {
props: {
// 拓展菜单宽度
width: {
type: String || Number,
default: ''
},
// 最长高度
maxHeight: {
type: String || Number,
default: ''
},
// data数据 对象的key按要求转换
options: {
type: Object,
default: () => {
return {
label: 'label',
key: 'key'
}
}
},
// 菜单数据源
data: {
type: Array,
default: () => {
return [{ name: '停诊', value: '1' }, { name: '停号', value: '2' }, { name: '新增', value: '3' }, { name: '修改', value: '4' }]
}
}
},
watch: {
data: {
handler(val) {
if (val && val.length) {
// 对传入的数据进行初始化
this.list = val.map(item => {
return {
...item,
label: item[this.options['label']],
key: item[this.options['key']]
}
})
}
},
immediate: true
}
},
data() {
return {
shifting: {
x: 0,
y: 0,
display: 'none',
opacity: 0
},
visible:false,//组件在注销之后 在生命周期删除挂载的dom
item: null,
list: [],//对props 传入的数据进行二次处理
}
},
mounted() {
// 浏览器其他位置点击 会关闭拓展菜单的显示
window.addEventListener('click', (e) => {
e.preventDefault();
this.downExpand()
})
// 挂载
document.body.appendChild(this.$el);
},
beforeUnmount() {
document.body.removeChild(this.$el);
},
methods: {
downExpand() {
this.shifting.opacity = '0'
setTimeout(() => {
this.shifting.display = 'none';
this.visible = false;
}, 100);
},
openExpand(e) {
this.visible = true;
// 动画过度效果
this.shifting.display = 'block',
setTimeout(() => {
this.shifting.opacity = '1'
}, 100);
// 拓展显示位置
this.$nextTick(() => {
const box = this.$refs['s_prompt'];
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const clientWidth = box?.clientWidth;
const clientHeight = box?.clientHeight;
if (windowWidth - e.pageX < clientWidth) {
this.shifting.x = e.pageX - clientWidth;
} else {
this.shifting.x = e.pageX;
}
if (windowHeight - e.pageY < clientHeight) {
this.shifting.y = e.pageY - clientHeight;
} else {
this.shifting.y = e.pageY;
}
})
},
handleClick(item) {
this.downExpand()
this.$emit('handleClick', item)
},
// props width 参数处理
getWidth() {
if (this.width) {
let width = this.width;
if (typeof width === 'string') {
return width;
} else if (typeof width === 'number') {
return width + 'px';
}
} else {
return 'auto'
}
},
// props height 参数处理
getMaxHeight() {
if (this.maxHeight) {
let maxHeight = this.maxHeight;
if (typeof maxHeight === 'string') {
return maxHeight;
} else if (typeof maxHeight === 'number') {
return maxHeight + 'px';
}
} else {
return 'auto'
}
},
}
}
</script>
<style lang="scss" scoped>
* {
margin: 0;
padding: 0;
text-decoration: none;
list-style: none;
}
.prompt {
position: absolute;
background: #fff;
box-shadow: #ccc 0 0 3px;
transition: all .3s;
height: auto;
left: 0;
top: 0;
opacity: 1;
z-index: 20;
border-radius: 3px;
padding: 4px;
ul {
width: 100%;
height: 100%;
overflow: auto;
li {
cursor: pointer;
transition: all .1s;
padding: 7px 20px;
font-size: 14px;
color: #494848;
border-radius: 3px;
}
li:hover {
background: rgb(230, 229, 229);
}
}
}
</style>
使用方法
组件注册之后
<s-expand ref="expandom" :data="data" :options="{label:'name', key: 'value'}" />
触发方法
const handleOpen = (e) =>{
e.preventDefault();
// 组件ref实例调用内部方法
expandom.value.openExpand(e)
}
总结
设计一个组件需要有一个明确的思路, 从原型到交付的一个过程, 每一个阶段都要思考如何去实现, 如何去快捷的实现, 当所有思路完毕之后,在去敲代码, 实现的过程肯定有困难,但是一定不要停止, 也许你的晋升之路才刚开始。