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 <input10 :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 <button21 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>3435<script setup>36import { computed, ref } from 'vue';37import { v4 as uuidv4 } from 'uuid';3839const 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: false59 },60 disabled: {61 type: Boolean,62 default: false63 },64 clearable: {65 type: Boolean,66 default: true67 },68 validator: {69 type: Function,70 default: null71 }72});7374const emit = defineEmits(['update:modelValue']);75const id = ref(`input-${uuidv4()}`);76const errorMessage = ref('');7778const 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};8788const clear = () => {89 emit('update:modelValue', '');90 errorMessage.value = '';91};92</script>9394<style scoped>95.advanced-input {96 margin-bottom: 1rem;97}9899.input-label {100 display: block;101 margin-bottom: 0.5rem;102 font-weight: 500;103}104105.input-wrapper {106 position: relative;107 display: flex;108 align-items: center;109}110111.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}119120.input-wrapper.has-error input {121 border-color: #dc3545;122}123124.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}129130.input-wrapper input.has-clear {131 padding-right: 2.5rem;132}133134.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}143144.clear-button:hover {145 color: #333;146}147148.required {149 color: #dc3545;150 margin-left: 0.25rem;151}152153.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 <input25 ref="searchInput"26 v-model="searchQuery"27 type="text"28 class="search-input"29 placeholder="搜索..."30 @click.stop31 />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>5253<script setup>54import { ref, computed, nextTick, onMounted } from 'vue';5556// 自定义 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};7071const props = defineProps({72 modelValue: {73 type: [String, Number],74 default: null75 },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: false91 },92 disabled: {93 type: Boolean,94 default: false95 }96});9798const emit = defineEmits(['update:modelValue', 'change']);99const isOpen = ref(false);100const searchQuery = ref('');101const searchInput = ref(null);102103const selectedOption = computed(() => {104 return props.options.find(option => option.value === props.modelValue);105});106107const 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});113114const 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};125126const closeDropdown = () => {127 isOpen.value = false;128 searchQuery.value = '';129};130131const selectOption = (option) => {132 emit('update:modelValue', option.value);133 emit('change', option);134 closeDropdown();135};136</script>137138<style scoped>139.searchable-select {140 position: relative;141 width: 100%;142 font-size: 1rem;143}144145.select-label {146 display: block;147 margin-bottom: 0.5rem;148 font-weight: 500;149}150151.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}162163.select-input:hover {164 border-color: #bbb;165}166167.select-input.is-open {168 border-color: #4c9aff;169 box-shadow: 0 0 0 2px rgba(76, 154, 255, 0.2);170}171172.select-input.is-disabled {173 background-color: #f5f5f5;174 cursor: not-allowed;175 color: #999;176}177178.selected-value {179 flex-grow: 1;180}181182.placeholder {183 color: #999;184 flex-grow: 1;185}186187.select-arrow {188 font-size: 0.75rem;189 color: #666;190 transition: transform 0.2s;191}192193.is-open .select-arrow {194 transform: rotate(180deg);195}196197.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}212213.search-container {214 padding: 0.5rem;215 border-bottom: 1px solid #eee;216}217218.search-input {219 width: 100%;220 padding: 0.5rem;221 border: 1px solid #ddd;222 border-radius: 4px;223 font-size: 0.875rem;224}225226.options-list {227 overflow-y: auto;228 max-height: 240px;229}230231.option-item {232 padding: 0.75rem 1rem;233 cursor: pointer;234 transition: background-color 0.2s;235}236237.option-item:hover {238 background-color: #f5f9ff;239}240241.option-item.is-selected {242 background-color: #e6f1ff;243 color: #1a73e8;244 font-weight: 500;245}246247.no-results {248 padding: 0.75rem 1rem;249 color: #999;250 text-align: center;251 font-style: italic;252}253254.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>78<script setup>9import { computed } from 'vue';1011const props = defineProps({12 gutter: {13 type: [Number, String],14 default: 015 },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});2728const 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>5455<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>78<script setup>9import { computed, inject } from 'vue';1011const props = defineProps({12 span: {13 type: [Number, String],14 default: 2415 },16 offset: {17 type: [Number, String],18 default: 019 },20 xs: [Number, Object],21 sm: [Number, Object],22 md: [Number, Object],23 lg: [Number, Object],24 xl: [Number, Object]25});2627// 从父组件注入gutter值28const parentGutter = inject('gutter', 0);2930const 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});3940const 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>7172<style scoped>73.grid-col {74 box-sizing: border-box;75}7677/* 基本列宽定义,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%; }102103/* 列偏移 */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/* 以此类推... */108109/* 响应式布局断点 */110@media (max-width: 576px) {111 .col-xs-1 { width: 4.166667%; }112 .col-xs-2 { width: 8.333333%; }113 /* 以此类推... */114}115116@media (min-width: 576px) {117 .col-sm-1 { width: 4.166667%; }118 .col-sm-2 { width: 8.333333%; }119 /* 以此类推... */120}121122@media (min-width: 768px) {123 .col-md-1 { width: 4.166667%; }124 .col-md-2 { width: 8.333333%; }125 /* 以此类推... */126}127128@media (min-width: 992px) {129 .col-lg-1 { width: 4.166667%; }130 .col-lg-2 { width: 8.333333%; }131 /* 以此类推... */132}133134@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>2728<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 <= 542 },43 hoverable: {44 type: Boolean,45 default: false46 }47});4849defineEmits(['click']);50</script>5152<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}6263.responsive-card.hoverable {64 cursor: pointer;65}6667.responsive-card.hoverable:hover {68 transform: translateY(-5px);69}7071/* 不同阴影级别 */72.elevation-0 {73 box-shadow: none;74 border: 1px solid #eee;75}7677.elevation-1 {78 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);79}8081.elevation-2 {82 box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);83}8485.elevation-3 {86 box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);87}8889.elevation-4 {90 box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);91}9293.elevation-5 {94 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);95}9697.card-header {98 padding: 1rem;99 border-bottom: 1px solid #f0f0f0;100}101102.card-title {103 margin: 0 0 0.25rem;104 font-size: 1.25rem;105 font-weight: 600;106}107108.card-subtitle {109 color: #6c757d;110 font-size: 0.875rem;111}112113.card-media {114 position: relative;115 overflow: hidden;116}117118.card-media img {119 width: 100%;120 display: block;121}122123.card-body {124 padding: 1rem;125 flex: 1;126}127128.card-footer {129 padding: 1rem;130 border-top: 1px solid #f0f0f0;131 background-color: #fafafa;132}133134/* 响应式调整 */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.stop9 >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>3839<script setup>40import { computed, watch } from 'vue';41import { onMounted, onUnmounted } from 'vue';4243const props = defineProps({44 modelValue: {45 type: Boolean,46 default: false47 },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: true60 },61 closable: {62 type: Boolean,63 default: true64 },65 closeOnBackdrop: {66 type: Boolean,67 default: true68 },69 closeOnEsc: {70 type: Boolean,71 default: true72 },73 confirmText: {74 type: String,75 default: '确定'76 },77 cancelText: {78 type: String,79 default: '取消'80 }81});8283const emit = defineEmits(['update:modelValue', 'confirm', 'cancel']);8485const sizeClass = computed(() => {86 return `modal-${props.size}`;87});8889// 处理ESC键关闭90const handleKeyDown = (e) => {91 if (e.key === 'Escape' && props.closeOnEsc && props.modelValue) {92 close();93 }94};9596// 阻止滚动97const lockScroll = () => {98 document.body.style.overflow = 'hidden';99 document.body.style.paddingRight = '15px'; // 防止滚动条消失导致页面抖动100};101102// 恢复滚动103const unlockScroll = () => {104 document.body.style.overflow = '';105 document.body.style.paddingRight = '';106};107108// 关闭模态框109const close = () => {110 emit('update:modelValue', false);111 emit('cancel');112};113114// 确认115const confirm = () => {116 emit('confirm');117 close();118};119120// 监听模态框状态变化121watch(() => props.modelValue, (newValue) => {122 if (newValue) {123 lockScroll();124 } else {125 unlockScroll();126 }127});128129onMounted(() => {130 document.addEventListener('keydown', handleKeyDown);131 if (props.modelValue) {132 lockScroll();133 }134});135136onUnmounted(() => {137 document.removeEventListener('keydown', handleKeyDown);138 unlockScroll();139});140</script>141142<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}154155.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}165166.modal-centered {167 align-self: center;168}169170/* 不同尺寸 */171.modal-small {172 width: 400px;173}174175.modal-medium {176 width: 600px;177}178179.modal-large {180 width: 800px;181}182183.modal-full {184 width: 90vw;185}186187.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}194195.modal-title {196 margin: 0;197 font-size: 1.25rem;198 font-weight: 600;199}200201.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}210211.modal-close:hover {212 color: #333;213}214215.modal-body {216 padding: 1.5rem;217 overflow-y: auto;218}219220.modal-body.has-footer {221 border-bottom: 1px solid #eee;222}223224.modal-footer {225 padding: 1rem 1.5rem;226 display: flex;227 justify-content: flex-end;228 gap: 0.75rem;229}230231.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}239240.modal-btn-cancel {241 background-color: #f3f4f6;242 color: #374151;243}244245.modal-btn-cancel:hover {246 background-color: #e5e7eb;247}248249.modal-btn-confirm {250 background-color: #3b82f6;251 color: white;252}253254.modal-btn-confirm:hover {255 background-color: #2563eb;256}257258/* 动画过渡 */259.modal-fade-enter-active,260.modal-fade-leave-active {261 transition: opacity 0.3s ease;262}263264.modal-fade-enter-from,265.modal-fade-leave-to {266 opacity: 0;267}268269.modal-fade-enter-active .modal-container {270 animation: modal-slide-down 0.3s ease forwards;271}272273.modal-fade-leave-active .modal-container {274 animation: modal-slide-up 0.3s ease forwards;275}276277@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}287288@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}298299/* 响应式调整 */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'89const app = createApp(App)1011app.component('AdvancedInput', AdvancedInput)12app.component('SearchableSelect', SearchableSelect)13app.component('ResponsiveCard', ResponsiveCard)14app.component('Modal', Modal)1516app.mount('#app')4.2 组件库开发最佳实践
-
组件设计原则
- 单一职责: 每个组件只做一件事
- 可配置性: 通过props提供合理的定制选项
- 支持事件: 提供完善的事件系统
- 插槽灵活性: 利用插槽实现内容定制
-
可访问性建议
- 支持键盘导航
- 适当的ARIA属性
- 提供合理的焦点管理
- 确保足够的色彩对比度
-
性能优化技巧
- 使用
v-once渲染静态内容 - 适当使用
v-memo减少更新 - 大列表使用虚拟滚动
- 组件懒加载
- 使用
4.3 组件通信方式
- Props向下传递数据
- 事件向上传递数据
- v-model双向绑定
- Provide/Inject跨层级通信
- 状态管理(Pinia/Vuex)
参与讨论