Skip to main content

Vue常用组件开发指南

Vue组件是Vue应用的核心构建块,通过组件化开发可以提高代码复用性、可维护性和开发效率。本文将介绍Vue常用组件的开发与最佳实践。

核心价值

组件化开发优势

  • 🧩 代码复用:将UI和逻辑封装为可重用组件
  • 🛠️ 关注点分离:每个组件负责特定功能
  • 🔄 可维护性:独立开发、测试和维护
  • 📦 共享生态:利用社区组件库加速开发

1. 表单组件

表单组件是前端应用中最常用的组件类型之一,下面介绍几个常用表单组件的实现。

1.1 高级输入框组件

一个带验证、清除按钮和标签的增强输入框组件。

vue
1<template>
2 <div class="advanced-input">
3 <label v-if="label" :for="id" class="input-label">
4 {{ label }}
5 <span v-if="required" class="required">*</span>
6 </label>
7
8 <div class="input-wrapper" :class="{ 'has-error': errorMessage }">
9 <input
10 :id="id"
11 :type="type"
12 :value="modelValue"
13 @input="onInput"
14 :placeholder="placeholder"
15 :disabled="disabled"
16 :required="required"
17 :class="{ 'has-clear': modelValue && clearable }"
18 />
19
20 <button
21 v-if="modelValue && clearable"
22 type="button"
23 class="clear-button"
24 @click="clear"
25 aria-label="Clear input"
26 >
27
28 </button>
29 </div>
30
31 <p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
32 </div>
33</template>
34
35<script setup>
36import { computed, ref } from 'vue';
37import { v4 as uuidv4 } from 'uuid';
38
39const props = defineProps({
40 modelValue: {
41 type: [String, Number],
42 default: ''
43 },
44 label: {
45 type: String,
46 default: ''
47 },
48 placeholder: {
49 type: String,
50 default: ''
51 },
52 type: {
53 type: String,
54 default: 'text'
55 },
56 required: {
57 type: Boolean,
58 default: false
59 },
60 disabled: {
61 type: Boolean,
62 default: false
63 },
64 clearable: {
65 type: Boolean,
66 default: true
67 },
68 validator: {
69 type: Function,
70 default: null
71 }
72});
73
74const emit = defineEmits(['update:modelValue']);
75const id = ref(`input-${uuidv4()}`);
76const errorMessage = ref('');
77
78const onInput = (event) => {
79 const value = event.target.value;
80 emit('update:modelValue', value);
81
82 if (props.validator) {
83 const validationResult = props.validator(value);
84 errorMessage.value = typeof validationResult === 'string' ? validationResult : '';
85 }
86};
87
88const clear = () => {
89 emit('update:modelValue', '');
90 errorMessage.value = '';
91};
92</script>
93
94<style scoped>
95.advanced-input {
96 margin-bottom: 1rem;
97}
98
99.input-label {
100 display: block;
101 margin-bottom: 0.5rem;
102 font-weight: 500;
103}
104
105.input-wrapper {
106 position: relative;
107 display: flex;
108 align-items: center;
109}
110
111.input-wrapper input {
112 width: 100%;
113 padding: 0.75rem 1rem;
114 border: 1px solid #ddd;
115 border-radius: 4px;
116 font-size: 1rem;
117 transition: border-color 0.2s;
118}
119
120.input-wrapper.has-error input {
121 border-color: #dc3545;
122}
123
124.input-wrapper input:focus {
125 border-color: #4c9aff;
126 outline: none;
127 box-shadow: 0 0 0 2px rgba(76, 154, 255, 0.2);
128}
129
130.input-wrapper input.has-clear {
131 padding-right: 2.5rem;
132}
133
134.clear-button {
135 position: absolute;
136 right: 0.75rem;
137 background: none;
138 border: none;
139 cursor: pointer;
140 color: #999;
141 padding: 0.25rem;
142}
143
144.clear-button:hover {
145 color: #333;
146}
147
148.required {
149 color: #dc3545;
150 margin-left: 0.25rem;
151}
152
153.error-message {
154 color: #dc3545;
155 font-size: 0.875rem;
156 margin-top: 0.25rem;
157}
158</style>

1.2 可搜索下拉选择组件

一个支持搜索功能的下拉选择组件。

vue
1<template>
2 <div class="searchable-select" v-click-outside="closeDropdown">
3 <label v-if="label" class="select-label">
4 {{ label }}
5 <span v-if="required" class="required">*</span>
6 </label>
7
8 <div
9 class="select-input"
10 @click="toggleDropdown"
11 :class="{ 'is-open': isOpen, 'is-disabled': disabled }"
12 >
13 <template v-if="selectedOption">
14 <div class="selected-value">{{ selectedOption.label }}</div>
15 </template>
16 <template v-else>
17 <div class="placeholder">{{ placeholder }}</div>
18 </template>
19 <div class="select-arrow">▼</div>
20 </div>
21
22 <div v-if="isOpen" class="dropdown-container">
23 <div class="search-container">
24 <input
25 ref="searchInput"
26 v-model="searchQuery"
27 type="text"
28 class="search-input"
29 placeholder="搜索..."
30 @click.stop
31 />
32 </div>
33
34 <div class="options-list">
35 <div
36 v-for="option in filteredOptions"
37 :key="option.value"
38 class="option-item"
39 :class="{ 'is-selected': modelValue === option.value }"
40 @click="selectOption(option)"
41 >
42 {{ option.label }}
43 </div>
44
45 <div v-if="filteredOptions.length === 0" class="no-results">
46 无匹配结果
47 </div>
48 </div>
49 </div>
50 </div>
51</template>
52
53<script setup>
54import { ref, computed, nextTick, onMounted } from 'vue';
55
56// 自定义 v-click-outside 指令
57const vClickOutside = {
58 mounted(el, binding) {
59 el._clickOutside = (event) => {
60 if (!(el === event.target || el.contains(event.target))) {
61 binding.value(event);
62 }
63 };
64 document.addEventListener('click', el._clickOutside);
65 },
66 unmounted(el) {
67 document.removeEventListener('click', el._clickOutside);
68 }
69};
70
71const props = defineProps({
72 modelValue: {
73 type: [String, Number],
74 default: null
75 },
76 options: {
77 type: Array,
78 default: () => []
79 },
80 label: {
81 type: String,
82 default: ''
83 },
84 placeholder: {
85 type: String,
86 default: '请选择'
87 },
88 required: {
89 type: Boolean,
90 default: false
91 },
92 disabled: {
93 type: Boolean,
94 default: false
95 }
96});
97
98const emit = defineEmits(['update:modelValue', 'change']);
99const isOpen = ref(false);
100const searchQuery = ref('');
101const searchInput = ref(null);
102
103const selectedOption = computed(() => {
104 return props.options.find(option => option.value === props.modelValue);
105});
106
107const filteredOptions = computed(() => {
108 if (!searchQuery.value) return props.options;
109 return props.options.filter(option =>
110 option.label.toLowerCase().includes(searchQuery.value.toLowerCase())
111 );
112});
113
114const toggleDropdown = () => {
115 if (props.disabled) return;
116
117 isOpen.value = !isOpen.value;
118 if (isOpen.value) {
119 searchQuery.value = '';
120 nextTick(() => {
121 searchInput.value?.focus();
122 });
123 }
124};
125
126const closeDropdown = () => {
127 isOpen.value = false;
128 searchQuery.value = '';
129};
130
131const selectOption = (option) => {
132 emit('update:modelValue', option.value);
133 emit('change', option);
134 closeDropdown();
135};
136</script>
137
138<style scoped>
139.searchable-select {
140 position: relative;
141 width: 100%;
142 font-size: 1rem;
143}
144
145.select-label {
146 display: block;
147 margin-bottom: 0.5rem;
148 font-weight: 500;
149}
150
151.select-input {
152 display: flex;
153 justify-content: space-between;
154 align-items: center;
155 padding: 0.75rem 1rem;
156 border: 1px solid #ddd;
157 border-radius: 4px;
158 cursor: pointer;
159 background-color: #fff;
160 transition: all 0.2s;
161}
162
163.select-input:hover {
164 border-color: #bbb;
165}
166
167.select-input.is-open {
168 border-color: #4c9aff;
169 box-shadow: 0 0 0 2px rgba(76, 154, 255, 0.2);
170}
171
172.select-input.is-disabled {
173 background-color: #f5f5f5;
174 cursor: not-allowed;
175 color: #999;
176}
177
178.selected-value {
179 flex-grow: 1;
180}
181
182.placeholder {
183 color: #999;
184 flex-grow: 1;
185}
186
187.select-arrow {
188 font-size: 0.75rem;
189 color: #666;
190 transition: transform 0.2s;
191}
192
193.is-open .select-arrow {
194 transform: rotate(180deg);
195}
196
197.dropdown-container {
198 position: absolute;
199 top: 100%;
200 left: 0;
201 width: 100%;
202 background-color: #fff;
203 border: 1px solid #ddd;
204 border-radius: 4px;
205 margin-top: 0.25rem;
206 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
207 z-index: 100;
208 max-height: 300px;
209 display: flex;
210 flex-direction: column;
211}
212
213.search-container {
214 padding: 0.5rem;
215 border-bottom: 1px solid #eee;
216}
217
218.search-input {
219 width: 100%;
220 padding: 0.5rem;
221 border: 1px solid #ddd;
222 border-radius: 4px;
223 font-size: 0.875rem;
224}
225
226.options-list {
227 overflow-y: auto;
228 max-height: 240px;
229}
230
231.option-item {
232 padding: 0.75rem 1rem;
233 cursor: pointer;
234 transition: background-color 0.2s;
235}
236
237.option-item:hover {
238 background-color: #f5f9ff;
239}
240
241.option-item.is-selected {
242 background-color: #e6f1ff;
243 color: #1a73e8;
244 font-weight: 500;
245}
246
247.no-results {
248 padding: 0.75rem 1rem;
249 color: #999;
250 text-align: center;
251 font-style: italic;
252}
253
254.required {
255 color: #dc3545;
256 margin-left: 0.25rem;
257}
258</style>

2. 布局组件

布局组件是构建应用界面结构的基础,下面介绍几个常用布局组件。

2.1 可响应式网格系统

一个简单但功能完备的网格系统,支持响应式布局。

vue
1<!-- GridRow.vue -->
2<template>
3 <div class="grid-row" :style="rowStyle">
4 <slot></slot>
5 </div>
6</template>
7
8<script setup>
9import { computed } from 'vue';
10
11const props = defineProps({
12 gutter: {
13 type: [Number, String],
14 default: 0
15 },
16 justify: {
17 type: String,
18 default: 'start',
19 validator: value => ['start', 'end', 'center', 'space-between', 'space-around'].includes(value)
20 },
21 align: {
22 type: String,
23 default: 'top',
24 validator: value => ['top', 'middle', 'bottom'].includes(value)
25 }
26});
27
28const rowStyle = computed(() => {
29 const gutterValue = parseInt(props.gutter);
30 const marginValue = gutterValue > 0 ? -(gutterValue / 2) : 0;
31
32 const alignMap = {
33 top: 'flex-start',
34 middle: 'center',
35 bottom: 'flex-end'
36 };
37
38 const justifyMap = {
39 start: 'flex-start',
40 end: 'flex-end',
41 center: 'center',
42 'space-between': 'space-between',
43 'space-around': 'space-around'
44 };
45
46 return {
47 marginLeft: `${marginValue}px`,
48 marginRight: `${marginValue}px`,
49 justifyContent: justifyMap[props.justify],
50 alignItems: alignMap[props.align]
51 };
52});
53</script>
54
55<style scoped>
56.grid-row {
57 display: flex;
58 flex-wrap: wrap;
59}
60</style>
vue
1<!-- GridCol.vue -->
2<template>
3 <div class="grid-col" :class="colClasses" :style="colStyle">
4 <slot></slot>
5 </div>
6</template>
7
8<script setup>
9import { computed, inject } from 'vue';
10
11const props = defineProps({
12 span: {
13 type: [Number, String],
14 default: 24
15 },
16 offset: {
17 type: [Number, String],
18 default: 0
19 },
20 xs: [Number, Object],
21 sm: [Number, Object],
22 md: [Number, Object],
23 lg: [Number, Object],
24 xl: [Number, Object]
25});
26
27// 从父组件注入gutter值
28const parentGutter = inject('gutter', 0);
29
30const colStyle = computed(() => {
31 const gutterValue = parseInt(parentGutter);
32 const paddingValue = gutterValue > 0 ? gutterValue / 2 : 0;
33
34 return {
35 paddingLeft: `${paddingValue}px`,
36 paddingRight: `${paddingValue}px`
37 };
38});
39
40const colClasses = computed(() => {
41 const classes = [];
42
43 // 基本列宽
44 classes.push(`col-${props.span}`);
45
46 // 列偏移
47 if (props.offset > 0) {
48 classes.push(`col-offset-${props.offset}`);
49 }
50
51 // 响应式布局类
52 ['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
53 if (props[size]) {
54 const sizeValue = props[size];
55 if (typeof sizeValue === 'number') {
56 classes.push(`col-${size}-${sizeValue}`);
57 } else if (typeof sizeValue === 'object') {
58 if (sizeValue.span) {
59 classes.push(`col-${size}-${sizeValue.span}`);
60 }
61 if (sizeValue.offset) {
62 classes.push(`col-${size}-offset-${sizeValue.offset}`);
63 }
64 }
65 }
66 });
67
68 return classes;
69});
70</script>
71
72<style scoped>
73.grid-col {
74 box-sizing: border-box;
75}
76
77/* 基本列宽定义,24栅格 */
78.col-1 { width: 4.166667%; }
79.col-2 { width: 8.333333%; }
80.col-3 { width: 12.5%; }
81.col-4 { width: 16.666667%; }
82.col-5 { width: 20.833333%; }
83.col-6 { width: 25%; }
84.col-7 { width: 29.166667%; }
85.col-8 { width: 33.333333%; }
86.col-9 { width: 37.5%; }
87.col-10 { width: 41.666667%; }
88.col-11 { width: 45.833333%; }
89.col-12 { width: 50%; }
90.col-13 { width: 54.166667%; }
91.col-14 { width: 58.333333%; }
92.col-15 { width: 62.5%; }
93.col-16 { width: 66.666667%; }
94.col-17 { width: 70.833333%; }
95.col-18 { width: 75%; }
96.col-19 { width: 79.166667%; }
97.col-20 { width: 83.333333%; }
98.col-21 { width: 87.5%; }
99.col-22 { width: 91.666667%; }
100.col-23 { width: 95.833333%; }
101.col-24 { width: 100%; }
102
103/* 列偏移 */
104.col-offset-1 { margin-left: 4.166667%; }
105.col-offset-2 { margin-left: 8.333333%; }
106.col-offset-3 { margin-left: 12.5%; }
107/* 以此类推... */
108
109/* 响应式布局断点 */
110@media (max-width: 576px) {
111 .col-xs-1 { width: 4.166667%; }
112 .col-xs-2 { width: 8.333333%; }
113 /* 以此类推... */
114}
115
116@media (min-width: 576px) {
117 .col-sm-1 { width: 4.166667%; }
118 .col-sm-2 { width: 8.333333%; }
119 /* 以此类推... */
120}
121
122@media (min-width: 768px) {
123 .col-md-1 { width: 4.166667%; }
124 .col-md-2 { width: 8.333333%; }
125 /* 以此类推... */
126}
127
128@media (min-width: 992px) {
129 .col-lg-1 { width: 4.166667%; }
130 .col-lg-2 { width: 8.333333%; }
131 /* 以此类推... */
132}
133
134@media (min-width: 1200px) {
135 .col-xl-1 { width: 4.166667%; }
136 .col-xl-2 { width: 8.333333%; }
137 /* 以此类推... */
138}
139</style>

2.2 响应式卡片布局组件

一个适用于展示内容的卡片组件。

vue
1<template>
2 <div
3 class="responsive-card"
4 :class="[`elevation-${elevation}`, { hoverable }]"
5 @click="$emit('click')"
6 >
7 <div v-if="$slots.header || title" class="card-header">
8 <slot name="header">
9 <h3 class="card-title">{{ title }}</h3>
10 <div v-if="subtitle" class="card-subtitle">{{ subtitle }}</div>
11 </slot>
12 </div>
13
14 <div v-if="$slots.media" class="card-media">
15 <slot name="media"></slot>
16 </div>
17
18 <div class="card-body">
19 <slot></slot>
20 </div>
21
22 <div v-if="$slots.footer" class="card-footer">
23 <slot name="footer"></slot>
24 </div>
25 </div>
26</template>
27
28<script setup>
29defineProps({
30 title: {
31 type: String,
32 default: ''
33 },
34 subtitle: {
35 type: String,
36 default: ''
37 },
38 elevation: {
39 type: Number,
40 default: 1,
41 validator: value => value >= 0 && value <= 5
42 },
43 hoverable: {
44 type: Boolean,
45 default: false
46 }
47});
48
49defineEmits(['click']);
50</script>
51
52<style scoped>
53.responsive-card {
54 background-color: #fff;
55 border-radius: 8px;
56 overflow: hidden;
57 transition: all 0.3s ease;
58 height: 100%;
59 display: flex;
60 flex-direction: column;
61}
62
63.responsive-card.hoverable {
64 cursor: pointer;
65}
66
67.responsive-card.hoverable:hover {
68 transform: translateY(-5px);
69}
70
71/* 不同阴影级别 */
72.elevation-0 {
73 box-shadow: none;
74 border: 1px solid #eee;
75}
76
77.elevation-1 {
78 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
79}
80
81.elevation-2 {
82 box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
83}
84
85.elevation-3 {
86 box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
87}
88
89.elevation-4 {
90 box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
91}
92
93.elevation-5 {
94 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
95}
96
97.card-header {
98 padding: 1rem;
99 border-bottom: 1px solid #f0f0f0;
100}
101
102.card-title {
103 margin: 0 0 0.25rem;
104 font-size: 1.25rem;
105 font-weight: 600;
106}
107
108.card-subtitle {
109 color: #6c757d;
110 font-size: 0.875rem;
111}
112
113.card-media {
114 position: relative;
115 overflow: hidden;
116}
117
118.card-media img {
119 width: 100%;
120 display: block;
121}
122
123.card-body {
124 padding: 1rem;
125 flex: 1;
126}
127
128.card-footer {
129 padding: 1rem;
130 border-top: 1px solid #f0f0f0;
131 background-color: #fafafa;
132}
133
134/* 响应式调整 */
135@media (max-width: 768px) {
136 .card-header {
137 padding: 0.75rem;
138 }
139
140 .card-body {
141 padding: 0.75rem;
142 }
143
144 .card-footer {
145 padding: 0.75rem;
146 }
147
148 .card-title {
149 font-size: 1.1rem;
150 }
151}
152</style>

3. 功能组件

功能组件提供特定的交互或功能,例如模态框、抽屉、轮播等。

3.1 模态框组件

一个可定制的模态对话框组件。

vue
1<template>
2 <Teleport to="body">
3 <Transition name="modal-fade">
4 <div v-if="modelValue" class="modal-overlay" @click="closeOnBackdrop && close()">
5 <div
6 class="modal-container"
7 :class="[sizeClass, { 'modal-centered': centered }]"
8 @click.stop
9 >
10 <div class="modal-header">
11 <h3 class="modal-title">
12 <slot name="title">{{ title }}</slot>
13 </h3>
14 <button v-if="closable" class="modal-close" @click="close" aria-label="Close modal">
15
16 </button>
17 </div>
18
19 <div class="modal-body" :class="{ 'has-footer': $slots.footer }">
20 <slot></slot>
21 </div>
22
23 <div v-if="$slots.footer" class="modal-footer">
24 <slot name="footer">
25 <button class="modal-btn modal-btn-cancel" @click="close">
26 {{ cancelText }}
27 </button>
28 <button class="modal-btn modal-btn-confirm" @click="confirm">
29 {{ confirmText }}
30 </button>
31 </slot>
32 </div>
33 </div>
34 </div>
35 </Transition>
36 </Teleport>
37</template>
38
39<script setup>
40import { computed, watch } from 'vue';
41import { onMounted, onUnmounted } from 'vue';
42
43const props = defineProps({
44 modelValue: {
45 type: Boolean,
46 default: false
47 },
48 title: {
49 type: String,
50 default: '对话框'
51 },
52 size: {
53 type: String,
54 default: 'medium',
55 validator: value => ['small', 'medium', 'large', 'full'].includes(value)
56 },
57 centered: {
58 type: Boolean,
59 default: true
60 },
61 closable: {
62 type: Boolean,
63 default: true
64 },
65 closeOnBackdrop: {
66 type: Boolean,
67 default: true
68 },
69 closeOnEsc: {
70 type: Boolean,
71 default: true
72 },
73 confirmText: {
74 type: String,
75 default: '确定'
76 },
77 cancelText: {
78 type: String,
79 default: '取消'
80 }
81});
82
83const emit = defineEmits(['update:modelValue', 'confirm', 'cancel']);
84
85const sizeClass = computed(() => {
86 return `modal-${props.size}`;
87});
88
89// 处理ESC键关闭
90const handleKeyDown = (e) => {
91 if (e.key === 'Escape' && props.closeOnEsc && props.modelValue) {
92 close();
93 }
94};
95
96// 阻止滚动
97const lockScroll = () => {
98 document.body.style.overflow = 'hidden';
99 document.body.style.paddingRight = '15px'; // 防止滚动条消失导致页面抖动
100};
101
102// 恢复滚动
103const unlockScroll = () => {
104 document.body.style.overflow = '';
105 document.body.style.paddingRight = '';
106};
107
108// 关闭模态框
109const close = () => {
110 emit('update:modelValue', false);
111 emit('cancel');
112};
113
114// 确认
115const confirm = () => {
116 emit('confirm');
117 close();
118};
119
120// 监听模态框状态变化
121watch(() => props.modelValue, (newValue) => {
122 if (newValue) {
123 lockScroll();
124 } else {
125 unlockScroll();
126 }
127});
128
129onMounted(() => {
130 document.addEventListener('keydown', handleKeyDown);
131 if (props.modelValue) {
132 lockScroll();
133 }
134});
135
136onUnmounted(() => {
137 document.removeEventListener('keydown', handleKeyDown);
138 unlockScroll();
139});
140</script>
141
142<style scoped>
143.modal-overlay {
144 position: fixed;
145 top: 0;
146 left: 0;
147 right: 0;
148 bottom: 0;
149 background-color: rgba(0, 0, 0, 0.5);
150 display: flex;
151 justify-content: center;
152 z-index: 1000;
153}
154
155.modal-container {
156 background-color: #fff;
157 border-radius: 8px;
158 box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
159 overflow: hidden;
160 display: flex;
161 flex-direction: column;
162 max-height: 90vh;
163 margin: 1.5rem;
164}
165
166.modal-centered {
167 align-self: center;
168}
169
170/* 不同尺寸 */
171.modal-small {
172 width: 400px;
173}
174
175.modal-medium {
176 width: 600px;
177}
178
179.modal-large {
180 width: 800px;
181}
182
183.modal-full {
184 width: 90vw;
185}
186
187.modal-header {
188 display: flex;
189 justify-content: space-between;
190 align-items: center;
191 padding: 1rem 1.5rem;
192 border-bottom: 1px solid #eee;
193}
194
195.modal-title {
196 margin: 0;
197 font-size: 1.25rem;
198 font-weight: 600;
199}
200
201.modal-close {
202 background: transparent;
203 border: none;
204 font-size: 1.25rem;
205 cursor: pointer;
206 color: #999;
207 padding: 0.25rem;
208 line-height: 1;
209}
210
211.modal-close:hover {
212 color: #333;
213}
214
215.modal-body {
216 padding: 1.5rem;
217 overflow-y: auto;
218}
219
220.modal-body.has-footer {
221 border-bottom: 1px solid #eee;
222}
223
224.modal-footer {
225 padding: 1rem 1.5rem;
226 display: flex;
227 justify-content: flex-end;
228 gap: 0.75rem;
229}
230
231.modal-btn {
232 padding: 0.5rem 1.25rem;
233 border-radius: 4px;
234 font-weight: 500;
235 cursor: pointer;
236 border: none;
237 transition: all 0.2s;
238}
239
240.modal-btn-cancel {
241 background-color: #f3f4f6;
242 color: #374151;
243}
244
245.modal-btn-cancel:hover {
246 background-color: #e5e7eb;
247}
248
249.modal-btn-confirm {
250 background-color: #3b82f6;
251 color: white;
252}
253
254.modal-btn-confirm:hover {
255 background-color: #2563eb;
256}
257
258/* 动画过渡 */
259.modal-fade-enter-active,
260.modal-fade-leave-active {
261 transition: opacity 0.3s ease;
262}
263
264.modal-fade-enter-from,
265.modal-fade-leave-to {
266 opacity: 0;
267}
268
269.modal-fade-enter-active .modal-container {
270 animation: modal-slide-down 0.3s ease forwards;
271}
272
273.modal-fade-leave-active .modal-container {
274 animation: modal-slide-up 0.3s ease forwards;
275}
276
277@keyframes modal-slide-down {
278 from {
279 transform: translateY(-50px);
280 opacity: 0;
281 }
282 to {
283 transform: translateY(0);
284 opacity: 1;
285 }
286}
287
288@keyframes modal-slide-up {
289 from {
290 transform: translateY(0);
291 opacity: 1;
292 }
293 to {
294 transform: translateY(-50px);
295 opacity: 0;
296 }
297}
298
299/* 响应式调整 */
300@media (max-width: 768px) {
301 .modal-small, .modal-medium, .modal-large {
302 width: 95%;
303 max-width: none;
304 }
305}
306</style>

4. 使用方法与最佳实践

4.1 组件注册与使用

js
1// 全局注册
2import { createApp } from 'vue'
3import App from './App.vue'
4import AdvancedInput from './components/AdvancedInput.vue'
5import SearchableSelect from './components/SearchableSelect.vue'
6import ResponsiveCard from './components/ResponsiveCard.vue'
7import Modal from './components/Modal.vue'
8
9const app = createApp(App)
10
11app.component('AdvancedInput', AdvancedInput)
12app.component('SearchableSelect', SearchableSelect)
13app.component('ResponsiveCard', ResponsiveCard)
14app.component('Modal', Modal)
15
16app.mount('#app')

4.2 组件库开发最佳实践

  1. 组件设计原则

    • 单一职责: 每个组件只做一件事
    • 可配置性: 通过props提供合理的定制选项
    • 支持事件: 提供完善的事件系统
    • 插槽灵活性: 利用插槽实现内容定制
  2. 可访问性建议

    • 支持键盘导航
    • 适当的ARIA属性
    • 提供合理的焦点管理
    • 确保足够的色彩对比度
  3. 性能优化技巧

    • 使用v-once渲染静态内容
    • 适当使用v-memo减少更新
    • 大列表使用虚拟滚动
    • 组件懒加载

4.3 组件通信方式

  1. Props向下传递数据
  2. 事件向上传递数据
  3. v-model双向绑定
  4. Provide/Inject跨层级通信
  5. 状态管理(Pinia/Vuex)

参与讨论