为什么在这个项目要自己构建组件库
本项目是前台系统,前台讲究个性化,后台偏同质化,这就导致了市面上的组件库都是针对后台的,而前台系统大多不适用,所以要自己构建一个组件库。
当然,在实际工作中,许多组件非常复杂,比如日期组件,自己写的成本极高,我们还是会用到一些UI框架基础组件,配置他们一定的定制能力,改造成项目所需要的。我们要自己写的,通常更多的是,项目中的业务组件,在本项目的业务通用。
组件库架构搭建
组件库一般建立一个libs文件夹
├── libs // 通用组件,可用于构建中台物料库或通用组件库
│ ├── svg-icon // 每一个组件建立一个文件夹
│ └── index.js // 把所有组件导出和注册
在libs/index.js中注册所有组件
import svgIcon from './svg-icon/index.vue'
export default {
install(app) {
app.component('m-svg-icon', svgIcon)
...
}
}
svg-icon组件
组件功能
- 通过传入name就能显示svg图片
- 期望有一个color参数,能直接配置颜色
- 期望能传入一个自定义的class,实现定制化
实现代码
<template>
<svg aria-hidden="true">
<use :class="fillClass" :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
// 显示的 svg 图标名称(剔除 icon-)
name: {
type: String,
required: true
},
// 直接指定 svg 图标的颜色
color: {
type: String
},
// 通过 tailwind 指定 svg 颜色的类名
fillClass: {
type: String
}
})
// 真实显示的 svg 图标名(拼接 #icon-)
const symbolId = computed(() => `#icon-${props.name}`)
</script>
vite要解析svg图片还需要引入插件createSvgIconsPlugin
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
plugins: [
vue(),
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
// 指定symbolId格式
symbolId: 'icon-[name]'
})
],
以及在mian.js中引入
import 'virtual:svg-icons-register'
弹出窗口-popup组件
popup组件能力分析
- 当
popup展开时,内容视图应该不属于任何一个 组件内部 ,而应该直接被插入到body下面 popup应该包含两部分内容,一部分为背景蒙版,一部分为内容的包裹容器popup应该通过一个双向绑定进行控制展示和隐藏popup展示时,滚动应该被锁定- 内容区域应该接收所有的
attrs,并且应该通过插槽让调用方指定其内容
前置知识
Vue3内置组件Teleport
该组件可以把一段dom append到某个元素内
使用方式示范
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p> <button @click="open = false">Close</button>
</div>
</Teleport>
vue过度动画组件Transition
cn.vuejs.org/guide/built…
vue3的组件双向数据绑定
// 父组件
<Child v-model="count" />
// 子组件
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
从vue3.4开始,推荐使用defineModel方法实现双向数据绑定
cn.vuejs.org/guide/compo…
实现代码
<template>
<div class="">
<!-- teleport -->
<teleport to="body">
<!-- 蒙版 -->
<transition name="fade">
<div
v-if="modelValue"
class="w-screen h-screen bg-zinc-900/80 z-40 fixed top-0 left-0"
@click="emits('update:modelValue', false)"
></div>
</transition>
<!-- 内容 -->
<transition name="popup-down-up">
<div
v-if="modelValue"
v-bind="$attrs"
class="w-screen bg-white z-50 fixed bottom-0"
>
<slot />
</div>
</transition>
</teleport>
</div>
</template>
<script setup>
import { useScrollLock } from '@vueuse/core'
import { watch } from 'vue'
const props = defineProps({
modelValue: {
required: true,
type: Boolean
}
})
const emits = defineEmits(['update:modelValue'])
// ------ 滚动锁定 ------
const isLocked = useScrollLock(document.body)
watch(
() => props.modelValue,
(val) => {
isLocked.value = val
},
{
immediate: true
}
)
</script>
<style lang="scss" scoped>
// fade 展示动画
.fade-enter-active {
transition: all 0.3s;
}
.fade-leave-active {
transition: all 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
// popup-down-up 展示动画
.popup-down-up-enter-active {
transition: all 0.3s;
}
.popup-down-up-leave-active {
transition: all 0.3s;
}
.popup-down-up-enter-from,
.popup-down-up-leave-to {
transform: translateY(100%);
}
</style>
button组件
能力分析
- 可以显示文字按钮,并提供
loading功能 - 可以显示
icon按钮,并可以任意指定icon颜色 - 可开关的点击动画
- 可以指定各种风格和大小
- 当指定的风格或大小不符合预设时,需要给开发者以提示消息
实现思路
html结构
只需要一个button,button配置有icon,或者文字就行,不需要别的
<button>
// icon 组件
<icon v-if="显示icon">
// loading
<icon v-if="显示loading">
// 配置按钮文字
<slot>
</button>
分析需要设置哪些props
type:需要指定风格,该字段要校验只能输入指定的字符串size:需要指定大小,该字段要校验只能输入指定的字符串icon:是否显示iconiconColor:指定icon的颜色iconClass:给icon类型,方面定制样式isActiveAnim:按钮点击时候,是否需要动画loading:是否需要loading- 有一个默认slot,用来配置按钮的文字的
type按钮有几种风格,每一种风格是什么样式,应该预设定义好
// type 可选项:表示按钮风格
const typeEnum = {
primary: 'text-white bg-zinc-800 hover:bg-zinc-900 active:bg-zinc-800 ',
main: 'text-white bg-main hover:bg-hover-main active:bg-main ',
info: 'text-zinc-800 bg-zinc-200 hover:bg-zinc-300 active:bg-zinc-200 '
}
size按钮大小一共有大小,每一种是多大,预设好
// 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'
}
}
search搜索框组件
能力分析
- 输入内容实现双向数据绑定
- 鼠标移入与获取焦点时的动画
- 一键清空文本功能
- 搜索触发功能
- 可控制,可填充的下拉展示区
- 监听到以下事件列表:
clear:删除所有文本事件input:输入事件focus:获取焦点事件blur:失去焦点事件search:触发搜索(点击或回车)事件
html基本结构
布局细节,输入框左边有icon,右边也有icon,好的做法是,左边一个元素(icon),右边一个元素(div),然后中间才是input输入框,而不是用什么css before之类的
<template>
<div ref="containerTarget" class="group relative p-0.5 rounded-xl border-white duration-500 hover:bg-red-100/40">
<div>
<!-- 搜索图标 -->
<m-svg-icon class="w-1.5 h-1.5 absolute translate-y-[-50%] top-[50%] left-2" name="search" color="#707070" />
<!-- 输入框 -->
<input v-model="inputValue"
class="block w-full h-[44px] pl-4 text-sm outline-0 bg-zinc-100 caret-zinc-400 rounded-xl text-zinc-900 tracking-wide font-semibold border border-zinc-100 duration-500 group-hover:bg-white group-hover:border-zinc-200 focus:border-red-300"
type="text" placeholder="搜索" @keyup.enter="onSearchHandler" @focus="onFocusHandler" @blur="onBlurHandler" />
<!-- 删除按钮 -->
<m-svg-icon name="input-delete" v-show="inputValue" @click="onClearClick"
class="h-1.5 w-1.5 absolute translate-y-[-50%] top-[50%] right-9 duration-500 cursor-pointer"></m-svg-icon>
<!-- 分割线 -->
<div
class="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-button @click="onSearchHandler"
class="group-hover:opacity-100 opacity-0 duration-500 absolute translate-y-[-50%] top-[50%] right-1 rounded-full"
icon="search" iconColor="#ffffff"></m-button>
</div>
<!-- 下拉区 -->
<transition name="slide">
<div v-if="$slots.dropdown" v-show="isFocus"
class="max-h-[368px] w-full text-base overflow-auto bg-white absolute z-20 left-0 top-[56px] p-2 rounded border border-zinc-200 duration-200 hover:shadow-3xl">
<slot name="dropdown" />
</div>
</transition>
</div>
</template>
实现思路
- 输入内容实现双向数据绑定
- 搜索按钮在
hover时展示,并为圆角: - 一键清空文本的功能,存在文本的时候才会展示
- 点击搜索按钮,触发搜索
- 可控制,可填充的下拉展示区
- 处理所有事件通知
<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 { defineEmits, watch, ref } from 'vue'
import { useVModel, onClickOutside } from '@vueuse/core'
const props = defineProps({
modelValue: {
require: true,
type: String
}
})
const inputValue = useVModel(props)
// 监听用户输入行为
watch(inputValue, (val) => {
emits(EMIT_INPUT, val)
})
const emits = defineEmits([EMIT_UPDATE_MODELVALUE, EMIT_SEARCH, EMIT_CLEAR, EMIT_INPUT, EMIT_FOCUS, EMIT_BLUR])
// 清空
const onClearClick = () => {
inputValue.value = ''
emits(EMIT_CLEAR)
}
// 触发搜索
const onSearchHandler = () => {
emits(EMIT_SEARCH, inputValue.value)
}
// 聚焦出现下拉
const isFocus = ref(false)
const onFocusHandler = () => {
emits(EMIT_FOCUS)
isFocus.value = true
}
// 失去焦点
const onBlurHandler = () => {
emits(EMIT_BLUR)
}
// 点击区域外隐藏
const containerTarget = ref()
onClickOutside(containerTarget, () => {
isFocus.value = false
})
</script>
popover组件
能力分析
- 具备两个插槽:
- 具名插槽:用于表示触发弹出层的视图
- 匿名插槽:用来表示弹出层视图中展示的内容
- 控制弹出层的位置,我们期望可以具备以下位置弹出:左上,右上,左下,右下
需要的props 1.
实现思路
如何知道弹出框应该显示在什么位置?
该组件最外层外层设置为relative,气泡元素设置为absolute,所以默认位置如图:
一共有四个位置需要显示:左上,右上,左下,右下
根据两张图可知
- 左上位置:left:-气泡元素的width;top:0
- 右上位置:left:触发元素的width;top:0
- 左下位置:left:-气泡元素的width;top:-触发元素的height
- 左上位置:left:触发元素的width;top:-触发元素的height
所以实现思路是:监听气泡是否显示,每一次气泡显示的时候,根据以上公式计算出气泡的位置。
实现代码
<template>
<div class="relative" @mouseleave="onMouseleave" @mouseenter="onMouseenter">
<div ref="referenceTarget">
<!-- 具名插槽 -->
<slot name="reference" />
</div>
<!-- 气泡展示动画 -->
<transition name="slide">
<div ref="contentTarget" :style="contentStyle" v-if="isVisable"
class="absolute p-1 z-20 bg-white border rounded-md">
<!-- 匿名插槽 -->
<slot />
</div>
</transition>
</div>
</template>
<script>
const PROP_TOP_LEFT = 'top-left'
const PROP_TOP_RIGHT = 'top-right'
const PROP_BOTTOM_LEFT = 'bottom-left'
const PROP_BOTTOM_RIGHT = 'bottom-right'
// 延迟关闭时长
const DELAY_TIME = 100
// 定义指定位置的 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 = false
timeout = null
}, DELAY_TIME)
}
/**
* 计算弹层位置
*/
const contentStyle = ref({
top: 0,
left: 0
})
const referenceTarget = ref()
const contentTarget = ref()
const useElementSize = (el) => {
if (!el) {
return {}
}
return {
width: el.offsetWidth,
height: el.offsetHeight,
}
}
/**
* 监听展示的变化,在展示时计算气泡位置
*/
watch(isVisable, (val) => {
if (!val) {
return
}
// 等待渲染成功之后
nextTick(() => {
switch (props.placement) {
// 左上
case PROP_TOP_LEFT:
contentStyle.value.top = 0
contentStyle.value.left =
-useElementSize(contentTarget.value).width + 'px'
break
// 右上
case PROP_TOP_RIGHT:
contentStyle.value.top = 0
contentStyle.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>
倒计时组件
能力分析
我们希望,倒计时组件,除了倒计时这个功能,其他一切都是可以配置的,所以要有以下设计
- 界面显示用一个slot,把显示什么东西完全交给用户,如果给个时间就行。
- 倒计时结束需要通知用户,有一个finish事件
- 倒计时每更新一秒,都通知一次用户,有一个change事件
- 在组件销毁的时候,要停止记时,防止内存泄漏
- 需要传两个参数
- 时间(毫秒)
- 时间格式
核心代码
<template>
<div>
<slot>
<p class="text-sm">
{{ showTime }}
</p>
</slot>
</div>
</template>
<script>
// 倒计时结束
const EMITS_FINISH = 'finish'
// 倒计时改变
const EMITS_CHANGE = 'change'
const INTERVAL_COUNT = 1000
</script>
<script setup>
import { ref, watch, onUnmounted, computed } from 'vue'
import dayjs from './utils'
const emits = defineEmits([EMITS_FINISH, EMITS_CHANGE])
const props = defineProps({
// 毫秒
time: {
type: Number,
required: true
},
// 遵循 dayjs format 标准:https://day.js.org/docs/zh-CN/parse/string-format
format: {
type: String,
default: 'HH:mm:ss'
}
})
/**
* 开始倒计时
*/
const start = () => {
close()
interval = setInterval(() => {
durationFn()
}, INTERVAL_COUNT)
}
// 倒计时时长
const duration = ref(0)
/**
* 倒计时行为
*/
const durationFn = () => {
duration.value -= INTERVAL_COUNT
emits(EMITS_CHANGE)
// 监听结束行为
if (duration.value <= 0) {
duration.value = 0
emits(EMITS_FINISH)
close()
}
}
/**
* 清理倒计时
*/
let interval = null
const close = () => {
if (interval) {
clearInterval(interval)
}
}
/**
* 开始倒计时
*/
watch(
() => props.time,
(val) => {
duration.value = val
start()
},
{
immediate: true
}
)
/**
* 组件销毁时,清理倒计时
*/
onUnmounted(() => {
close()
})
/**
* 处理显示时间
*/
const showTime = computed(() => {
return dayjs.duration(duration.value).format(props.format)
})
</script>
<style></style>
confirm组件
这样一个确认弹窗组件,核心功能是要实现通过方法调用,而不是通过标签调用。
confirm('要删除所有历史记录吗?').then(() => {
// TODO...
})
前置知识-h()函数和render()函数
h函数本质是createVnode函数,用于创建vnode,接受三个参数 (要渲染的 dom,attrs 对象,子元素)。
render函数的本质是把vnode转化为真实的dom,并append到指定的某个元素中。
所以实现通过函数调用组件的思路是:
- 创建一个comfirm.vue组件
- 创建comfirm函数,
- 在comfirm函数内:
- 把comfirm.vue组件当h函数的参数,执行h函数生成vnode,
- 然后再vnode当成参数,执行render函数,这样就把comfirm.vue组件渲染到页面中了
confirm组件能力分析
我们希望这个组件以下内容可以定制,通过传参传入实现配置:
- 标题
- 内容
- 取消按钮文本
- 确定按钮文本
- 取消按钮事件
- 确定按钮事件
- 关闭弹窗的回调
const props = defineProps({
// 标题
title: {
type: String
},
// 描述
content: {
type: String,
required: true
},
// 取消按钮文本
cancelText: {
type: String,
default: '取消'
},
// 确定按钮文本
confirmText: {
type: String,
default: '确定'
},
// 取消按钮事件
cancelHandler: {
type: Function
},
// 确定按钮事件
confirmHandler: {
type: Function
},
// 关闭 confirm 的回调
close: {
type: Function
}
})
代码实现confirm组件
<template>
<div>
<!-- 蒙版 -->
<transition name="fade">
<div v-if="isVisable" @click="close" class="w-screen h-screen bg-zinc-900/80 z-40 fixed top-0 left-0"></div>
</transition>
<!-- 内容 -->
<transition name="up">
<div v-if="isVisable"
class="w-[80%] fixed top-1/3 left-[50%] translate-x-[-50%] z-50 px-2 py-1.5 rounded-sm border dark:border-zinc-600 cursor-pointer bg-white dark:bg-zinc-800 xl:w-[35%]">
<!-- 标题 -->
<div class="text-lg font-bold text-zinc-900 dark:text-zinc-200 mb-2">
{{ title }}
</div>
<!-- 内容 -->
<div class="text-base text-zinc-900 dark:text-zinc-200 mb-2">
{{ content }}
</div>
<!-- 按钮 -->
<div class="flex justify-end">
<m-button type="info" class="mr-2" @click="onCancelClick">{{
cancelText
}}</m-button>
<m-button type="primary" @click="onConfirmClick">{{
confirmText
}}</m-button>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import mButton from '../button/index.vue'
import { ref, onMounted } from 'vue'
const props = defineProps({
// ....
})
// 控制显示处理
const isVisable = ref(false)
/**
* confirm 展示
*/
const show = () => {
isVisable.value = true
}
/**
* 页面构建完成之后,执行。保留动画
*/
onMounted(() => {
show()
})
// 关闭动画执行时间
const duration = '0.5s'
/**
* confirm 关闭,保留动画执行时长
*/
const close = () => {
isVisable.value = false
setTimeout(() => {
if (props.close) {
props.close()
}
}, parseInt(duration.replace('0.', '').replace('s', '')) * 100)
}
/**
* 取消按钮点击事件
*/
const onCancelClick = () => {
if (props.cancelHandler) {
props.cancelHandler()
}
close()
}
/**
* 确定按钮点击事件
*/
const onConfirmClick = () => {
if (props.confirmHandler) {
props.confirmHandler()
}
close()
}
</script>
注意细节:
因为需要保留执行动画,所以我们在 mounted 时,再让内容展示,否则没有动画。
代码实现confirm函数
import { h, render } from 'vue'
import confirmComponent from './index.vue'
/**
*
* @param {*} title 标题
* @param {*} content 文本
* @param {*} cancelText 取消按钮文本
* @param {*} confirmText 确定按钮文本
* @returns
*/
export const confirm = (
title,
content,
cancelText = '取消',
confirmText = '确定'
) => {
return new Promise((resolve, reject) => {
// 允许只传递 content
if (title && !content) {
content = title
title = ''
}
// 关闭弹层事件
const close = () => {
render(null, document.body)
}
// 取消按钮事件
const cancelHandler = () => {
// reject(new Error('取消按钮点击'))
}
// 确定按钮事件
const confirmHandler = () => {
resolve()
}
// 1. vnode
const vnode = h(confirmComponent, {
title,
content,
cancelText,
confirmText,
confirmHandler,
cancelHandler,
close
})
// 2. render
render(vnode, document.body)
})
}
message 组件
能力分析
我们希望组件的以下内容,可以通过传入参数实现定制
- 文案内容
- type类型:success,warn,fail
- 展示的时长
- 关闭时的回调事件
const props = defineProps({
/**
* message 的消息类型
*/
type: {
type: String,
required: true,
validator(val) {
const result = typeEnum.includes(val)
if (!result) {
throw new Error(`你的 type 必须是 ${typeEnum.join('、')} 中的一个`)
}
return result
}
},
/**
* 描述文本
*/
content: {
type: String,
required: true
},
/**
* 展示时长
*/
duration: {
type: Number
},
/**
* 关闭时的回调
*/
destroy: {
type: Function
}
})
代码实现
<template>
<transition name="down" @after-leave="destroy">
<div v-show="isVisable"
class="min-w-[420px] fixed top-[20px] left-[50%] translate-x-[-50%] z-50 flex items-center px-3 py-1.5 rounded-sm border cursor-pointer"
:class="styles[type].containerClass">
<m-svg-icon :name="styles[type].icon" :fillClass="styles[type].fillClass"
class="h-1.5 w-1.5 mr-1.5"></m-svg-icon>
<span class="text-sm" :class="styles[type].textClass">
{{ content }}
</span>
</div>
</transition>
</template>
<script>
const SUCCESS = 'success'
const WARN = 'WARN'
const ERROR = 'ERROR'
const typeEnum = [SUCCESS, WARN, ERROR]
</script>
<script setup>
import { ref, onMounted } from 'vue'
import mSvgIcon from '../svg-icon/index.vue'
const props = defineProps({
// ....
})
// 样式表数据
const styles = {
// 警告
warn: {
icon: 'warn',
fillClass: 'fill-warn-300',
textClass: 'text-warn-300',
containerClass:
'bg-warn-100 border-warn-200 hover:shadow-lg hover:shadow-warn-100'
},
// 错误
error: {
icon: 'error',
fillClass: 'fill-error-300',
textClass: 'text-error-300',
containerClass:
'bg-error-100 border-error-200 hover:shadow-lg hover:shadow-error-100'
},
// 成功
success: {
icon: 'success',
fillClass: 'fill-success-300',
textClass: 'text-success-300',
containerClass:
'bg-success-100 border-success-200 hover:shadow-lg hover:shadow-success-100'
}
}
// 控制显示处理
const isVisable = ref(false)
/**
* 保证动画展示,需要在 mounted 之后进行展示
*/
onMounted(() => {
isVisable.value = true
/**
* 延迟时间关闭
*/
setTimeout(() => {
isVisable.value = false
}, props.duration)
})
</script>
<style lang="scss" scoped>
.down-enter-active,
.down-leave-active {
transition: all 0.5s;
}
.down-enter-from,
.down-leave-to {
opacity: 0;
transform: translate3d(-50%, -100px, 0);
}
</style>
message组件我们仍然希望通过函数的方式来调用,和confirm函数的实现方式一样,这里不再记录。
移动端的navbar组件
能力分析
对于这样一个navbar,分为左中右三块,我们希望
- 左边的图标和点击事件可以定制,有一个默认图标
- 中间的标题可以定制,通过slot传入
- 右边的图标和点击事件可以定制,有一个默认图标
代码实现
<template>
<div
class="w-full h-5 border-b flex items-center z-10 bg-white dark:bg-zinc-800 border-b-zinc-200 dark:border-b-zinc-700"
:class="[sticky ? 'sticky top-0 left-0' : 'relative']"
>
<!-- 左 -->
<div
class="h-full w-5 absolute left-0 flex items-center justify-center"
@click="onClickLeft"
>
<slot name="left">
<m-svg-icon
name="back"
class="w-2 h-2"
fillClass="fill-zinc-900 dark:fill-zinc-200"
/>
</slot>
</div>
<!-- 中 -->
<div
class="h-full flex items-center justify-center m-auto font-bold text-base text-zinc-900 dark:text-zinc-200"
>
<slot></slot>
</div>
<!-- 右 -->
<div
class="h-full w-5 absolute right-0 flex items-center justify-center"
@click="onClickRight"
>
<slot name="right" />
</div>
</div>
</template>
const router = useRouter()
/**
* 左侧按钮点击事件
*/
const onClickLeft = () => {
if (props.clickLeft) {
props.clickLeft()
return
}
router.back()
}
/**
* 右侧按钮点击事件
*/
const onClickRight = () => {
if (props.clickRight) {
props.clickRight()
}
}
input组件
能力分析
我们希望input组件支持
- 单行输入
- 多行输入
- 最大字数限制
- input事件,change事件等冒泡出去
所以需要参数为:
- type:表示单行还是多行
- max:最大字数限制,不传没有限制
- modelValue:输入字符双向绑定
代码实现
<template>
<div class="relative">
<input v-if="type === TYPE_TEXT"
class="border-gray-200 dark:border-zinc-600 dark:bg-zinc-800 duration-100 dark:text-zinc-400 border-[1px] outline-0 py-0.5 px-1 text-sm rounded-sm focus:border-blue-400 w-full"
type="text" v-model="text" :maxlength="max" />
<textarea v-if="type === TYPE_TEXTAREA" v-model="text" :maxlength="max" rows="5"
class="border-gray-200 dark:border-zinc-600 dark:bg-zinc-800 duration-100 dark:text-zinc-400 border-[1px] outline-0 py-0.5 px-1 text-sm rounded-sm focus:border-blue-400 w-full"></textarea>
<span v-if="max" class="absolute right-1 bottom-0.5 text-zinc-400 text-xs"
:class="{ 'text-red-700': currentNumber === parseInt(max) }">{{ currentNumber }} / {{ max }}</span>
</div>
</template>
<script>
const TYPE_TEXT = 'text'
const TYPE_TEXTAREA = 'textarea'
</script>
<script setup>
const props = defineProps({
modelValue: {
required: true,
type: String
},
type: {
type: String,
default: TYPE_TEXT,
validator(value) {
const arr = [TYPE_TEXT, TYPE_TEXTAREA]
const result = arr.includes(value)
if (!result) {
throw new Error(`type 的值必须在可选范围内 [${arr.join('、')}]`)
}
return result
}
},
max: {
type: [String, Number]
}
})
// 事件声明
defineEmits(['update:modelValue'])
// 输入的字符
const text = useVModel(props)
// 输入的字符数
const currentNumber = computed(() => {
return text.value.length
})
</script>
dialog组件
dialog和comfirm组件非常类似,一共只有两个区别:
- dialog组件不需要通过函数调用,只需要通过标签调用
- dialog的内容不一定是字符串,还可以是各种标签,comfirm是字符串
能力分析
我们希望dialog组件可以通过props进行以下配置
- 控制组件显示和隐藏的开关
- 标题
- 内容
- 取消按钮文本
- 确定按钮文本
- 取消按钮事件
- 确定按钮事件
- 关闭弹窗的回调
const props = defineProps({
// 控制开关
modelValue: {
type: Boolean,
required: true
},
// 标题
title: {
type: String
},
// 取消按钮文本
cancelText: {
type: String,
default: '取消'
},
// 确定按钮文本
confirmText: {
type: String,
default: '确定'
},
// 取消按钮点击事件
cancelHandler: {
type: Function
},
// 确定按钮点击事件
confirmHandler: {
type: Function
},
// 关闭的回调
close: {
type: Function
}
})
代码实现
<template>
<div>
<!-- 蒙版 -->
<transition name="fade">
<div v-if="isVisable" @click="close" class="w-screen h-screen bg-zinc-900/80 z-40 fixed top-0 left-0"></div>
</transition>
<!-- 内容 -->
<transition name="up">
<div v-if="isVisable"
class="max-w-[80%] max-h-[80%] overflow-auto fixed top-[10%] left-[50%] translate-x-[-50%] z-50 px-2 py-1.5 rounded-sm border dark:border-zinc-600 cursor-pointer bg-white dark:bg-zinc-800 xl:min-w-[35%]">
<!-- 标题 -->
<div class="text-lg font-bold text-zinc-900 dark:text-zinc-200 mb-2" v-if="title">
{{ title }}
</div>
<!-- 内容 -->
<div class="text-base text-zinc-900 dark:text-zinc-200 mb-2">
<slot />
</div>
<!-- 按钮 -->
<div class="flex justify-end" v-if="cancelHandler || confirmHandler">
<m-button type="info" class="mr-2" @click="onCancelClick">{{
cancelText
}}</m-button>
<m-button type="primary" @click="onConfirmClick">{{
confirmText
}}</m-button>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import { useVModel } from '@vueuse/core'
const props = defineProps({
// todo...
})
defineEmits(['update:modelValue'])
// 控制显示处理
const isVisable = useVModel(props)
/**
* 取消按钮点击事件
*/
const onCancelClick = () => {
if (props.cancelHandler) {
props.cancelHandler()
}
close()
}
/**
* 确定按钮点击事件
*/
const onConfirmClick = () => {
if (props.confirmHandler) {
props.confirmHandler()
}
close()
}
const close = () => {
isVisable.value = false
if (props.close) {
props.close()
}
}
</script>