目录
01: 处理 PC 端基础架构
02: 通用组件:search 搜索框能力分析
03: 通用组件:search 搜索框样式处理
04: 通用组件:Button 按钮能力分析
05: 通用组件:Button 按钮功能实现
06: 通用组件:完善 search 基本能力
07: 通用组件:popover 气泡卡片能力分析
08: 通用组件:popover 气泡卡片基础功能实现
09: 通用组件:popover 功能延伸,控制气泡展示位置
10: 通用组件:处理慢速移动时,气泡消失问题
01: 处理 PC 端基础架构
- layout
- - components
- - - header
- - - - index.vue
- - - floating.vue
- - - main.vue
- - index.vue
// 设置 header 和 main 区域高度// tailwind.config.js
module.exports = {……theme: {extend: {……height: {header: '72px',main: 'calc(100vh - 72px)'}}}
}// 使用
// l-white => shadow-l-white
// height => h-header<header-vue class="h-header"></>
<main-vue class="h-main"></>
02: 通用组件:search 搜索框能力分析
既然是通用组件,就需要分析它的能力,它应该具备什么样的功能:
1. 输入内容实现双向数据绑定
2. 鼠标移入与获取焦点时的动画
3. 一键清空文本功能
4. 搜索触发功能
5. 可控制,可填充的下拉展示区
6. 监听到以下事件列表:
1. clear:删除所有文本事件
2. input:输入事件
3. focus:获取焦点事件
4. blur:失去焦点事件
5. search:触发搜索(点击或回车)事件
03: 通用组件:search 搜索框样式处理
- libs
- - search
- - - index.vue
<template><divref="containerTarget"class="group relative p-0.5 rounded-xl border-white duration-500 hover:bg-red-100/40"><div><!-- 搜索图标 --><m-svg-iconclass="w-1.5 h-1.5 absolute translate-y-[-50%] top-[50%] left-2"name="search"color="#707070"/><!-- 输入框 --><inputclass="block w-full h-[44px] pl-4 text-sm outline-0 bg-zinc-100 dark:bg-zinc-800 caret-zinc-400 rounded-xl text-zinc-900 dark:text-zinc-200 tracking-wide font-semibold border border-zinc-100 dark:border-zinc-700 duration-500 group-hover:bg-white dark:group-hover:bg-zinc-900 group-hover:border-zinc-200 dark:group-hover:border-zinc-700 focus:border-red-300"type="text"placeholder="搜索"v-model="inputValue"@focus="onFocusHandler"@blur="onBlurHandler"@keyup.enter="onSearchHandlder"/><!-- 删除按钮 --><m-svg-iconv-show="inputValue"name="input-delete"class="h-1.5 w-1.5 absolute translate-y-[-50%] top-[50%] right-9 duration-500 cursor-pointer"@click="onClearClick"></m-svg-icon><!-- 分割线 --><divclass="opacity-0 h-1.5 w-[1px] absolute translate-y-[-50%] top-[50%] right-[62px] duration-500 bg-zinc-200 group-hover:opacity-100"></div><!-- TODO: 搜索按钮(通用组件) --><m-buttonclass="absolute translate-y-[-50%] top-[50%] right-1 rounded-xl duration-500 opacity-0 group-hover:opacity-100"icon="search"iconColor="#ffffff"@click="onSearchHandlder"></m-button></div><!-- 下拉区 --><transition name="slide"><divv-if="$slots.dropdown"v-show="isFocus"class="max-h-[368px] w-full text-base overflow-auto bg-white dark:bg-zinc-800 absolute z-20 left-0 top-[56px] p-2 rounded border border-zinc-200 dark:border-zinc-600 duration-200 hover:shadow-3xl scrollbar-thin scrollbar-thumb-zinc-200 dark:scrollbar-thumb-zinc-900 scrollbar-track-transparent"><slot name="dropdown" /></div></transition></div>
</template><script>
// 更新事件
const EMIT_UPDATE_MODELVALUE = 'update:modelValue'
// 触发搜索(点击或回车)事件
const EMIT_SEARCH = 'search'
// 删除所有文本事件
const EMIT_CLEAR = 'clear'
// 输入事件
const EMIT_INPUT = 'input'
// 获取焦点事件
const EMIT_FOCUS = 'focus'
// 失去焦点事件
const EMIT_BLUR = 'blur'
</script><script setup>
import { watch, ref } from 'vue'
import { useVModel, onClickOutside } from '@vueuse/core'const props = defineProps({modelValue: {type: String,required: true}
})const emits = defineEmits([EMIT_UPDATE_MODELVALUE,EMIT_CLEAR,EMIT_INPUT,EMIT_FOCUS,EMIT_BLUR,EMIT_SEARCH
])// 输入文本
const inputValue = useVModel(props)/*** 清空文本*/
const onClearClick = () => {inputValue.value = ''emits(EMIT_CLEAR, '')
}/*** 触发搜索*/
const onSearchHandlder = () => {emits(EMIT_SEARCH, inputValue.value)
}/*** 监听焦点行为*/
const isFocus = ref(false)
const onFocusHandler = () => {isFocus.value = trueemits(EMIT_FOCUS)
}/*** 失去焦点*/
const onBlurHandler = () => {emits(EMIT_BLUR)
}/*** 点击区域外隐藏 dropdown*/
const containerTarget = ref(null)
onClickOutside(containerTarget, () => {isFocus.value = false
})/*** 监听输入行为*/
watch(inputValue, (val) => {emits(EMIT_INPUT, val)
})
</script><style lang="scss" scoped>
.slide-enter-active {transition: all 0.5s;
}.slide-leave-active {transition: all 0.5s;
}.slide-enter-from,
.slide-leave-to {transform: translateY(40px);opacity: 0;
}
</style>
04: 通用组件:Button 按钮能力分析
对于这个按钮来说,我们期望拥有以下能力:
1. 可以显示文字按钮,并提供 loading 功能
2. 可以显示 icon 按钮,并可以任意指定 icon 颜色
3. 可以开关的点击动画
4. 可以指定各种风格和大小
5. 当指定的风格或大小不符合预设时,需要给开发者以提示消息
05: 通用组件:Button 按钮功能实现
- libs
- - button
- - - index.vue
/*** 实现步骤:* 1. 构建 type 风格可选项 和 size 大小可选项* 2. 通过 props 让开发者控制按钮* 3. 区分 icon button 和 text button* 4. 依据当前数据,实现视图* 5. 处理点击事件*/
书写习惯:setup 是写逻辑的地方,不希望在这里写大量的常量。可以在 <script setup> 上面再去创建一个 <script>
// 定义 main 颜色
// tailwind.config.js
module.exports = {theme: {extend: {colors: {main: '#f44c58','hover-main': '#F2F9EC',}}}
}// 使用
class = "bg-main"
<script>
// type 可选项:表示按钮风格
const typeEnum = {primary:'text-white bg-zinc-800 dark:bg-zinc-900 hover:bg-zinc-900 dark:hover:bg-zinc-700 active:bg-zinc-800 dark:active:bg-zinc-700',main: 'text-white bg-main dark:bg-zinc-900 hover:bg-hover-main dark:hover:bg-zinc-700 active:bg-main dark:active:bg-zinc-700',info: 'text-zinc-800 dark:text-zinc-300 bg-zinc-200 dark:bg-zinc-700 hover:bg-zinc-300 dark:hover:bg-zinc-600 active:bg-zinc-200 dark:active:bg-zinc-700 '
}
// size 可选项:表示按钮大小。区分文字按钮和icon按钮
const sizeEnum = {default: {button: 'w-8 h-4 text-base',icon: ''},'icon-default': {button: 'w-4 h-4',icon: 'w-1.5 h-1.5'},small: {button: 'w-7 h-3 text-base',icon: ''},'icon-small': {button: 'w-3 h-3',icon: 'w-1.5 h-1.5'}
}
</script>
// 通过 props 让开发者控制按钮
<script setup>
const props = defineProps({// icon 图标名字icon: {type: String},// icon 图标颜色iconColor: {type: String},// icon 图标类名(匹配 tailwind)iconClass: {type: String},// 按钮风格type: {type: String,default: 'main',validator(val) {// 获取所有的可选的按钮风格const keys = Object.keys(typeEnum)// 开发者指定风格是否在可选风格中const result = keys.includes(val)// 如果不在则给开发者提示if (!result) {throw new Error(`你的 type 必须是 ${keys.join('、')} 中的一个`)}// 返回校验结果return result}},// 大小风格size: {type: String,default: 'default',validator(val) {// 获取所有的可选的大小(注意剔除 icon 开头的元素,因为我们期望开发者输入 size="default",但不期望开发者输入 size="icon-default")const keys = Object.keys(sizeEnum).filter((key) => !key.includes('icon'))// 开发者指定大小是否在可选大小中const result = keys.includes(val)// 如果不在则给开发者提示if (!result) {throw new Error(`你的 size 必须是 ${keys.join('、')} 中的一个`)}// 返回校验结果return result}},// 按钮在点击时是否需要动画isActiveAnim: {type: Boolean,default: true},// 加载状态loading: {type: Boolean,default: false}
})
</script>
// 区分 icon button 和 text button
// 传递了 icon props 则默认按钮类型为 icon button// 处理大小的 key 值
const sizeKey = computed(() => {return props.icon ? 'icon-' + props.size : props.size
})
// 依据当前的数据,实现视图
<template><buttonclass="text-sm text-center rounded duration-150 flex justify-center items-center":class="[typeEnum[type],sizeEnum[sizeKey].button,{ 'active:scale-105': isActiveAnim }]"@click.stop="onBtnClick"><!-- 展示 loading --><m-svg-iconv-if="loading"name="loading"class="w-2 h-2 animate-spin mr-1"></m-svg-icon><!-- icon 按钮 --><m-svg-iconv-if="icon":name="icon"class="m-auto":class="sizeEnum[sizeKey].icon":color="iconColor":fillClass="iconClass"></m-svg-icon><!-- 文字按钮 --><slot v-else /></button>
</template>
// 处理点击事件
const EMITS_CLICK = 'click'
const emits = defineEmits([EMITS_CLICK])
/*** 按钮点击事件处理*/
const onBtnClick = () => {if (props.loading) {return}emits(EMITS_CLICK)
}
06: 通用组件:完善 search 基本能力
/*** 1. 输入内容实现双向数据绑定* 2. 搜索按钮在 hover 时展示* 3. 一键清空文本功能* 4. 触发搜索* 5. 控制下拉展示区的展示* 6. 事件处理*/
// 事件处理:
// 双向绑定
// search 搜索
// 删除所有文本
// 输入事件
// 获取焦点事件
// 失去焦点事件
07: 通用组件:popover 气泡卡片能力分析
/*** 具备两个插槽。* 第一个插槽描述触发弹出层的视图。这个视图可以定为具名插槽。* 第二个插槽描述弹出层内容。这个内容可以定为匿名插槽。* 弹出层气泡可以在指定位置弹出。*/
08: 通用组件:popover 气泡卡片基础功能实现
- libs
- - popover
- - - index.vue
<template><div class="relative" @mouseleave="onMouseleave" @mouseenter="onMouseenter"><div ref="referenceTarget"><!-- 具名插槽 --><slot name="reference" /></div><!-- 气泡展示动画 --><transition name="slide"><divv-show="isVisable"ref="contentTarget"class="absolute p-1 z-20 bg-white dark:bg-zinc-900 border rounded-md dark:border-zinc-700":style="contentStyle"><!-- 匿名插槽 --><slot /></div></transition></div>
</template><script>
// 延迟关闭时长
const DELAY_TIME = 100const PROP_TOP_LEFT = 'top-left'
const PROP_TOP_RIGHT = 'top-right'
const PROP_BOTTOM_LEFT = 'bottom-left'
const PROP_BOTTOM_RIGHT = 'bottom-right'// 定义指定位置的 Enum
const placementEnum = [PROP_TOP_LEFT,PROP_TOP_RIGHT,PROP_BOTTOM_LEFT,PROP_BOTTOM_RIGHT
]
</script><script setup>
import { ref, watch, nextTick } from 'vue'const props = defineProps({// 控制气泡弹出位置,并给出开发者错误的提示placement: {type: String,default: 'bottom-left',validator(val) {const result = placementEnum.includes(val)if (!result) {throw new Error(`你的 placement 必须是 ${placementEnum.join('、')} 中的一个`)}return result}}
})// 控制 menu 展示
const isVisable = ref(false)// 控制延迟关闭
let timeout = null
/*** 鼠标移入的触发行为*/
const onMouseenter = () => {isVisable.value = true// 再次触发时,清理延时装置if (timeout) {clearTimeout(timeout)}
}
/*** 鼠标移出的触发行为*/
const onMouseleave = () => {// 延时装置timeout = setTimeout(() => {isVisable.value = falsetimeout = null}, DELAY_TIME)
}/*** 计算元素尺寸*/
const referenceTarget = ref(null)
const contentTarget = ref(null)
const useElementSize = (target) => {if (!target) return {}return {width: target.offsetWidth,height: target.offsetHeight}
}/*** 计算弹层位置*/
const contentStyle = ref({top: 0,left: 0
})/*** 监听展示的变化,在展示时计算气泡位置*/
watch(isVisable, (val) => {if (!val) {return}// 等待渲染成功之后nextTick(() => {switch (props.placement) {// 左上case PROP_TOP_LEFT:contentStyle.value.top = 0contentStyle.value.left =-useElementSize(contentTarget.value).width + 'px'break// 右上case PROP_TOP_RIGHT:contentStyle.value.top = 0contentStyle.value.left =useElementSize(referenceTarget.value).width + 'px'break// 左下case PROP_BOTTOM_LEFT:contentStyle.value.top =useElementSize(referenceTarget.value).height + 'px'contentStyle.value.left =-useElementSize(contentTarget.value).width + 'px'break// 右下case PROP_BOTTOM_RIGHT:contentStyle.value.top =useElementSize(referenceTarget.value).height + 'px'contentStyle.value.left =useElementSize(referenceTarget.value).width + 'px'break}})
})
</script><style lang="scss" scoped>
// slide 展示动画
.slide-enter-active {transition: opacity 0.3s, transform 0.3s;
}.slide-leave-active {transition: opacity 0.3s, transform 0.3s;
}.slide-enter-from,
.slide-leave-to {transform: translateY(20px);opacity: 0;
}
</style>
09: 通用组件:popover 功能延伸,控制气泡展示位置
/*** 步骤:* 1. 指定所有可选位置的常量,并生成 enum* 2. 通过 prop 控制指定位置* 3. 获取元素的 DOM;创建读取元素尺寸的方法* 4. 生成气泡的样式对象,用来控制每个位置对应的样式* 5. 根据 prop,计算样式对象*/
10: 通用组件:处理慢速移动时,气泡消失问题
想要解决这个问题,可以利用 类似于防抖(debounce)的概念。
也就是:鼠标刚离开时,不去立刻修改 isVisible,而是延迟一段时间,如果在这段时间之内,再次触发了鼠标移入事件,则不再修改 isVisible。