外观风格
vue3 公共组件
可复用的组件是开发中经常会遇到的,将可能在多个地方使用的业务逻辑封装成公共的可复用组件是一个很好的开发习惯。
使用 vue3 开发公共组件,经常会涉及到在子组件上使用 v-model
,给子组件定义事件
等问题。
下面以开发一个分段选择组件为例,来讲解开发可复用组件中的要点。
教程
精选文章
你好开源
定义 props
首先确认组件的 props
,该组件需要接收一个 名为tabs
的 props,用于遍历渲染出选项。
<script setup>
const props = defineProps({
tabs: {
type: Array,
default () {
return []
}
}
})
</script>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
<script setup>
+ typescript
的写法:
<script setup lang="ts">
interface Props {
tabs: {
name: string
value: string
}[]
}
const props = defineProps<Props>()
</script>
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
下面将默认使用
<script setup>
+typescript
写法。
使用 v-model
在组件上使用 v-model
来创建双向绑定。
// 父组件调用时
<template>
<Tabs :tabs="tabs" v-model="activeTab" />
</template>
1
2
3
4
2
3
4
要达到双向绑定的效果,只需要组件内做到下面这 2 步:
- 定义
modelValue
props - 定义
update:modelValue
事件
<script setup lang="ts">
interface Props {
modelValue: string
tabs: {
name: string
value: string
}[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): string
}>()
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
在父组件内调用该组件时,就能正常使用 v-model
指令了。
v-model 修饰符
在子组件的 props
中定义一个 modelModifiers
属性,就可以获取到所有修饰符。
现在来创建一个lower
修饰符。
父组件:
<template>
<Tabs :tabs="tabs" v-model.lower="activeTab" />
</template>
1
2
3
2
3
子组件:
<script setup lang="ts">
// 定义 props 类型
interface Props {
modelValue: string
modelModifiers?: {
lower: boolean
} // 修饰符
tabs: {
name: string
value: string
}[]
}
// 使用 withDefaults 设置默认值
const props = withDefaults(defineProps<Props>(), {
modelModifiers: () => ({ lower: false })
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
下面将使用这个修饰符,在
change
事件中将返回值都转换为小写。
自定义事件
在点击切换选项时,我们还需要触发一个change
事件,来传递更改后的选项值。
<script setup lang="ts">
// ...
const emit = defineEmits<{
(e: 'change', value: string): string
(e: 'update:modelValue', value: string): string
}>()
</script>
1
2
3
4
5
6
7
2
3
4
5
6
7
到这里,开发一个公共组件的基本内容我们就确定好了,下面就是具体逻辑实现。
完整代码
<template>
<div class="tabs-rail">
<div
class="tabs-tab-wrapper"
v-for="(item, index) in tabs"
:key="item.value"
:ref="item.value"
>
<div
class="tabs-tab"
:class="{ 'tabs-tab-active': activeTab == item.value }"
@click="handleClick(item.value)"
>
<span class="tabs-tab__label">{{ item.name }}</span>
</div>
</div>
<div class="tabs-bar" :style="barStyle"></div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, nextTick, getCurrentInstance } from 'vue'
import type { CSSProperties } from 'vue'
// 定义 props 类型
interface Props {
modelValue: string
modelModifiers?: {
lower: boolean
} // 修饰符
tabs: {
name: string
value: string
}[]
}
const props = withDefaults(defineProps<Props>(), {
modelModifiers: () => ({ lower: false })
})
// 自定义事件 change 和 v-model 事件
const emit = defineEmits<{
(e: 'change', value: string): string
(e: 'update:modelValue', value: string): string
}>()
const instance = getCurrentInstance()!
const activeTab = ref(props.modelValue)
const barStyle = ref<CSSProperties>()
/**
* 处理点击切换时tabBar的跟随效果
*/
const getBarStyle = () => {
let offset = 0
let barSize = 0
props.tabs.every((tab) => {
const $refs = instance.refs?.[`${tab.value}`] as HTMLElement[]
const $el = $refs[0] as HTMLElement
if (!$el) return false
if (tab.value !== activeTab.value) {
return true
}
// dom 的样式
const tabStyles = window.getComputedStyle($el.parentElement!)
// tabBar 的宽度
barSize = $el.clientWidth
// 获取到 tabBar 的 x 轴偏移量
offset = $el.getBoundingClientRect().left -
($el.parentElement?.getBoundingClientRect().left ?? 0) -
parseFloat(tabStyles.paddingLeft)
return false
})
return {
width: `${barSize}px`,
transform: `translateX(${offset}px)`
}
}
const updateStyle = () => (barStyle.value = getBarStyle())
const setActiveTab = async (value: any) => {
let tabValue = value
if (activeTab.value === tabValue || tabValue === undefined) return
activeTab.value = tabValue
updateStyle()
// 根据修饰符,转换小写
if (props.modelModifiers.lower) {
tabValue = tabValue.toLowerCase()
}
// 点击后触发事件
emit('update:modelValue', tabValue)
emit('change', tabValue)
}
const handleClick = (v: string) => {
setActiveTab(v)
}
watch(
() => props.modelValue,
async (modelValue) => {
await nextTick()
setActiveTab(modelValue)
}
)
onMounted(() => {
updateStyle()
// 处理窗口缩放
window.onresize = () => {
updateStyle()
}
})
</script>
<style scoped lang="less">
.tabs-rail {
position: relative;
padding: 2px;
border-radius: 8px;
display: flex;
align-items: center;
background-color: #f5f5f5;
transition: background-color 0.2s ease;
.tabs-tab-wrapper {
flex-basis: 0;
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
margin: auto 0px;
.tabs-tab {
z-index: 1;
overflow: hidden;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
white-space: nowrap;
flex-wrap: nowrap;
background-clip: padding-box;
font-size: 14px;
padding: 6px 0;
font-weight: 500;
&-active {
font-weight: 700;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.tabs-tab__label {
display: flex;
align-items: center;
color: #333;
}
}
}
.tabs-bar {
position: absolute;
height: 34px;
display: inline-block;
border-radius: 8px;
background-color: #fff;
box-shadow: 0px 1px 3px 0px rgba(73, 64, 64, 0.1);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1), width 0.2s cubic-bezier(0.4, 0, 0.2, 1),
background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171