以下是一个使用 Vue 3 Composition API + TypeScript 开发组件的完整案例,我们将创建一个带自定义功能的按钮组件:
CustomButton.vue
<template>
<button
:class="buttonClasses"
:disabled="disabled || isLoading"
@click="handleClick"
>
<span v-if="isLoading" class="button__loader"></span>
<span v-else class="button__content">
<slot name="icon">
<span v-if="icon" class="button__icon">{{ icon }}</span>
</slot>
<slot></slot>
</span>
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue'
// 定义 Props 类型
interface Props {
buttonType?: 'primary' | 'secondary' | 'text'
size?: 'small' | 'medium' | 'large'
icon?: string
disabled?: boolean
isLoading?: boolean
}
// 定义 Emits 类型
interface Emits {
(e: 'click', event: MouseEvent): void
}
// 声明 Props 和 Emits
const props = withDefaults(defineProps<Props>(), {
buttonType: 'primary',
size: 'medium',
disabled: false,
isLoading: false
})
const emit = defineEmits<Emits>()
// 计算类名
const buttonClasses = computed(() => [
'button',
`button--${props.buttonType}`,
`button--${props.size}`,
{
'button--disabled': props.disabled,
'button--loading': props.isLoading
}
])
// 点击事件处理
const handleClick = (event: MouseEvent) => {
if (!props.disabled && !props.isLoading) {
emit('click', event)
}
}
</script>
<style scoped lang="scss">
.button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
&--primary {
background-color: #409eff;
color: white;
&:hover:not(.button--disabled) {
background-color: #79bbff;
}
}
&--secondary {
background-color: #e6e6e6;
color: #333;
&:hover:not(.button--disabled) {
background-color: #d4d4d4;
}
}
&--text {
background: none;
color: #409eff;
&:hover:not(.button--disabled) {
color: #79bbff;
}
}
&--small {
padding: 6px 12px;
font-size: 12px;
}
&--medium {
padding: 8px 16px;
font-size: 14px;
}
&--large {
padding: 12px 24px;
font-size: 16px;
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
&__content {
display: inline-flex;
align-items: center;
gap: 8px;
}
&__icon {
display: inline-flex;
align-items: center;
}
&__loader {
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
Props 类型定义:
buttonType
: 按钮类型(primary/secondary/text)size
: 尺寸(small/medium/large)icon
: 图标(支持 Unicode 字符或简写)disabled
: 禁用状态isLoading
: 加载状态功能特性:
<template>
<div class="container">
<CustomButton @click="handleClick">默认按钮</CustomButton>
<CustomButton
button-type="secondary"
size="large"
icon="★"
@click="handleIconClick"
>
带图标按钮
</CustomButton>
<CustomButton
:is-loading="loading"
@click="handleAsyncClick"
>
异步操作按钮
</CustomButton>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CustomButton from './CustomButton.vue'
const loading = ref(false)
const handleClick = (event: MouseEvent) => {
console.log('按钮点击', event)
}
const handleIconClick = () => {
console.log('带图标按钮点击')
}
const handleAsyncClick = async () => {
loading.value = true
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 2000))
loading.value = false
}
</script>
Props 类型校验:
Emit 事件类型安全:
更好的代码维护性:
组合式函数复用:
// useButton.ts
import { computed } from 'vue'
export function useButton(props: { buttonType: string; size: string }) {
const buttonClasses = computed(() => [
'button',
`button--${props.buttonType}`,
`button--${props.size}`
])
return { buttonClasses }
}
单元测试(使用 Vitest):
import { mount } from '@vue/test-utils'
import CustomButton from './CustomButton.vue'
describe('CustomButton', () => {
it('emits click event', async () => {
const wrapper = mount(CustomButton)
await wrapper.trigger('click')
expect(wrapper.emitted()).toHaveProperty('click')
})
})
这个案例演示了:
组件具备良好的类型安全性和可维护性,可以直接在实际项目中使用或根据需求扩展功能。