Vue3指令知识点及使用方法详细介绍
一、条件渲染
v-if 指令
语法:
v-if="表达式"
功能: 根据表达式值的真假,动态地渲染元素。
示例代码
<template> <div> <!-- v-if 控制元素的显示与隐藏 --> <p v-if="isVisible">这段文字只有在 isVisible 为 true 时才会显示</p> <button @click="toggleVisibility">切换显示状态</button> </div> </template> <script> export default { data() { return { isVisible: true }; }, methods: { toggleVisibility() { this.isVisible = !this.isVisible; // 点击按钮切换 isVisible 的值 } } }; </script>
说明:
v-if="isVisible"
根据isVisible
的布尔值决定是否渲染<p>
标签。- 点击按钮调用
toggleVisibility
方法,切换isVisible
的值,从而实现元素的显示与隐藏。
2.v-if / v-else / v-else-if
<template> <div> <!-- v-if 根据条件渲染元素 --> <p v-if="score >= 90">优秀</p> <!-- v-else-if 提供额外条件分支 --> <p v-else-if="score >= 60">及格</p> <!-- v-else 必须紧跟在 v-if 或 v-else-if 后面 --> <p v-else>不及格</p> <!-- 使用 template 包裹多个元素 --> <template v-if="isLoggedIn"> <h1>欢迎回来</h1> <p>您有3条新消息</p> </template> </div> </template> <script setup> import { ref } from 'vue'; const score = ref(85); const isLoggedIn = ref(true); </script>
3.v-show
<template> <div> <!-- v-show 只是切换 display 属性 --> <p v-show="isVisible">你可以看到我</p> <button @click="isVisible = !isVisible">切换显示</button> </div> </template> <script setup> import { ref } from 'vue'; const isVisible = ref(true); </script>
v-if vs v-show:
v-if
是"真正"的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。v-if
是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时才会开始渲染条件块。v-show
不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。- 一般来说,
v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show
较好;如果在运行时条件很少改变,则使用v-if
较好。
v-else 指令
语法:
v-else
功能: 与
v-if
配合使用,表示v-if
条件不满足时的替代内容。示例代码
<template> <div> <p v-if="isLoggedIn">欢迎回来,{{ username }}</p> <p v-else>请先登录</p> <button @click="toggleLoginStatus">切换登录状态</button> </div> </template> <script> export default { data() { return { isLoggedIn: false, username: '张三' }; }, methods: { toggleLoginStatus() { this.isLoggedIn = !this.isLoggedIn; // 切换登录状态 } } }; </script>
- 说明:
v-if="isLoggedIn"
判断用户是否登录。- 如果
isLoggedIn
为false
,则显示v-else
的内容。
- 说明:
v-else-if 指令
语法:
v-else-if="表达式"
功能: 提供额外的条件判断。
示例代码
<template> <div> <p v-if="score >= 90">优秀</p> <p v-else-if="score >= 80">良好</p> <p v-else-if="score >= 60">及格</p> <p v-else>不及格</p> <button @click="changeScore(95)">设置分数为 95</button> <button @click="changeScore(85)">设置分数为 85</button> <button @click="changeScore(70)">设置分数为 70</button> <button @click="changeScore(45)">设置分数为 45</button> </div> </template> <script> export default { data() { return { score: 0 }; }, methods: { changeScore(newScore) { this.score = newScore; // 更新分数 } } }; </script>
二、循环渲染
v-for 指令
- 语法:
v-for="(item, index) in 可迭代对象" :key="唯一标识"
- 功能: 遍历数组、对象或字符串,动态渲染多个元素。
语法知识点
v-for
用于遍历数组或对象,渲染列表。- 支持多种语法,如
(item, index) in items
或(value, key, index) in object
。
使用方法
通常用于渲染列表、表格等重复性内容。
可以结合
:key
属性提高渲染性能。示例代码1
<template> <div> <!-- 遍历数组 --> <ul> <li v-for="(item, index) in items" :key="index"> {{ item }} - 索引: {{ index }} </li> </ul> <!-- 遍历对象 --> <div v-for="(value, key, index) in person" :key="index"> {{ key }}: {{ value }} - 索引: {{ index }} </div> <!-- 遍历字符串 --> <div v-for="(char, index) in message" :key="index"> 字符: {{ char }} - 索引: {{ index }} </div> <button @click="addItem">添加项目</button> </div> </template> <script> export default { data() { return { items: ['苹果', '香蕉', '橙子'], person: { name: '张三', age: 25, job: '前端工程师' }, message: 'Hello Vue3' }; }, methods: { addItem() { this.items.push('葡萄'); // 向数组添加新项目 } } }; </script>
说明:
v-for="(item, index) in items"
遍历数组items
,item
表示当前元素,index
表示索引。v-for="(value, key, index) in person"
遍历对象person
,key
表示对象的键,value
表示对应的值,index
表示索引。v-for="(char, index) in message"
遍历字符串message
,char
表示当前字符,index
表示索引。
示例代码2
<template> <div> <h2>用户列表</h2> <ul> <li v-for="(user, index) in users" :key="user.id"> {{ index + 1 }}. {{ user.name }} - {{ user.email }} </li> </ul> <!-- 遍历对象 --> <h2>用户信息</h2> <ul> <li v-for="(value, key) in userInfo" :key="key"> {{ key }}: {{ value }} </li> </ul> </div> </template> <script> export default { data() { return { users: [ { id: 1, name: '张三', email: 'zhangsan@example.com' }, { id: 2, name: '李四', email: 'lisi@example.com' }, ], userInfo: { age: 25, occupation: '工程师', location: '北京', }, }; }, }; </script>
- 语法:
三、数据插入(v-text 和 v-html)
文本插值
语法:
{{ 表达式 }}
功能: 动态插入数据到模板中。
示例代码
<template> <div> <p>用户名: {{ username }}</p> <p>欢迎信息: {{ getWelcomeMessage() }}</p> <button @click="updateUsername">更新用户名</button> </div> </template> <script> export default { data() { return { username: '张三' }; }, methods: { getWelcomeMessage() { return `欢迎回来,${this.username}!`; }, updateUsername() { this.username = '李四'; // 更新用户名 } } }; </script>
- 说明:
{{ username }}
动态插入username
的值。{{ getWelcomeMessage() }}
动态插入方法返回的值。
- 说明:
HTML 插值
语法:
v-html="表达式"
功能: 动态插入 HTML 内容。
示例代码
<template> <div> <div v-html="htmlContent"></div> <button @click="updateHtmlContent">更新 HTML 内容</button> </div> </template> <script> export default { data() { return { htmlContent: '<h3 style="color: blue;">这是动态 HTML 内容</h3>' }; }, methods: { updateHtmlContent() { this.htmlContent = '<p style="color: red;">HTML 内容已更新</p>'; // 更新 HTML 内容 } } }; </script>
说明:
v-html="htmlContent"
将htmlContent
的值作为 HTML 渲染到页面上。
案例代码
<template> <div> <!-- v-text 会覆盖元素原有内容 --> <p v-text="message"></p> <!-- 双大括号插值,不会覆盖原有内容 --> <p>消息: {{ message }}</p> <!-- v-html 会解析 HTML 内容 --> <div v-html="htmlContent"></div> <!-- 双大括号中的 JavaScript 表达式 --> <p>{{ message.split('').reverse().join('') }}</p> </div> </template> <script setup> import { ref } from 'vue'; const message = ref('Hello Vue3!'); const htmlContent = ref('<strong style="color: red;">红色加粗文本</strong>'); </script>
四、属性绑定
v-bind 指令
语法:
:属性名="表达式"
或v-bind:属性名="表达式"
功能: 动态绑定 HTML 属性。
示例代码
<template> <div> <!-- 动态绑定 class --> <div :class="classObject">动态绑定 class</div> <!-- 动态绑定 style --> <div :style="styleObject">动态绑定 style</div> <!-- 动态绑定属性 --> <img :src="imageSrc" :alt="imageAlt"> <button @click="toggleClass">切换 class</button> </div> </template> <script> export default { data() { return { classObject: { 'active-class': true, 'disabled-class': false }, styleObject: { color: 'blue', fontSize: '18px' }, imageSrc: 'https://via.placeholder.com/150', imageAlt: '示例图片' }; }, methods: { toggleClass() { this.classObject['active-class'] = !this.classObject['active-class']; // 切换 active-class 的状态 } } }; </script>
- 说明:
:class="classObject"
动态绑定class
属性,classObject
是一个对象,包含多个类名及其布尔值。:style="styleObject"
动态绑定style
属性,styleObject
是一个对象,包含多个样式属性及其值。:src="imageSrc"
和:alt="imageAlt"
动态绑定<img>
标签的src
和alt
属性。
- 说明:
动态绑定多个属性
语法:
v-bind="对象"
功能: 动态绑定多个属性。
示例代码
<template> <div> <button :disabled="isDisabled" :class="buttonClass">按钮</button> <button :"{disabled: isDisabled, class: buttonClass}">按钮(对象语法)</button> </div> </template> <script> export default { data() { return { isDisabled: false, buttonClass: 'primary-button' }; } }; </script>
五、事件绑定
v-on 指令
语法:
@事件名="处理函数"
或v-on:事件名="处理函数"
功能: 绑定事件处理函数。
语法知识点
v-on
用于绑定事件监听器。- 简写为
@
。
使用方法
- 绑定常见事件,如
click
,input
,change
,submit
等。 - 支持事件修饰符,如
.prevent
,.stop
,.once
等。
示例代码1
<template> <div> <!-- 简单事件绑定 --> <button @click="handleClick">点击我</button> <!-- 事件修饰符 --> <input @keydown.enter="handleEnter" placeholder="按回车键触发"> <input @input="handleInput" placeholder="输入内容"> <p>输入内容: {{ inputText }}</p> </div> </template> <script> export default { data() { return { inputText: '' }; }, methods: { handleClick() { alert('按钮被点击了!'); }, handleEnter() { alert('按下回车键了!'); }, handleInput(event) { this.inputText = event.target.value; // 获取输入框的值并更新到 inputText } } }; </script>
说明:
@click="handleClick"
绑定点击事件到handleClick
方法。@keydown.enter="handleEnter"
使用事件修饰符.enter
,表示当按下回车键时触发handleEnter
方法。@input="handleInput"
绑定输入事件到handleInput
方法,每当输入框内容变化时触发。
案例代码2
<template> <div> <!-- 绑定点击事件 --> <button @click="handleClick">点击我</button> <!-- 绑定提交事件并阻止默认行为 --> <form @submit.prevent="handleSubmit"> <input v-model="inputValue" placeholder="输入内容" /> <button type="submit">提交</button> </form> <!-- 事件修饰符 --> <div @click="handleDivClick"> <button @click.stop="handleButtonClick">点击按钮不触发 div 点击事件</button> </div> </div> </template> <script> export default { data() { return { inputValue: '', }; }, methods: { handleClick() { alert('按钮被点击了!'); }, handleSubmit() { alert(`提交的内容是: ${this.inputValue}`); }, handleDivClick() { console.log('div 被点击了'); }, handleButtonClick() { console.log('按钮被点击了'); }, }, }; </script>
事件对象
语法:
@事件名="处理函数($event)"
功能: 在事件处理函数中获取原生事件对象。
示例代码
<template> <div> <button @click="handleClickWithEvent($event)">点击获取事件对象</button> </div> </template> <script> export default { methods: { handleClickWithEvent(event) { console.log('事件对象:', event); event.target.style.color = 'red'; // 修改按钮文本颜色 } } }; </script>
六、双向数据绑定
v-model 指令
语法:
v-model="数据属性"
功能: 创建双向数据绑定(常见于表单输入)。
示例代码
<template> <div> <!-- 文本输入框 --> <input type="text" v-model="textValue" placeholder="输入文本"> <p>输入的文本: {{ textValue }}</p> <!-- 复选框 --> <input type="checkbox" v-model="isChecked"> <label :for="'checkbox' + Math.random().toString(36).substr(2, 9)">是否同意</label> <p>复选框状态: {{ isChecked }}</p> <!-- 单选按钮 --> <input type="radio" value="选项1" v-model="radioValue" name="radioGroup"> <label :for="'radio1' + Math.random().toString(36).substr(2, 9)">选项1</label> <input type="radio" value="选项2" v-model="radioValue" name="radioGroup"> <label :for="'radio2' + Math.random().toString(36).substr(2, 9)">选项2</label> <p>选中的选项: {{ radioValue }}</p> <!-- 下拉框 --> <select v-model="selectedOption"> <option value="苹果">苹果</option> <option value="香蕉">香蕉</option> <option value="橙子">橙子</option> </select> <p>选中的水果: {{ selectedOption }}</p> </div> </template> <script> export default { data() { return { textValue: '', isChecked: false, radioValue: '', selectedOption: '' }; } }; </script>
- 说明:
v-model="textValue"
在文本输入框中创建双向绑定,输入内容会实时更新到textValue
。v-model="isChecked"
在复选框中创建双向绑定,勾选状态会更新到isChecked
。v-model="radioValue"
在单选按钮中创建双向绑定,选中的值会更新到radioValue
。v-model="selectedOption"
在下拉框中创建双向绑定,选中的选项会更新到selectedOption
。
- 说明:
七、插槽
默认插槽
语法:
<slot></slot>
功能: 定义组件的插槽,允许父组件向子组件传递内容。
示例代码
<!-- 子组件: MyComponent.vue --> <template> <div class="my-component"> <h3>自定义组件</h3> <slot></slot> </div> </template> <!-- 父组件 --> <template> <div> <my-component> <p>这是插入到子组件的内容</p> </my-component> </div> </template> <script> import MyComponent from './MyComponent.vue'; export default { components: { MyComponent } }; </script>
具名插槽
语法:
- 子组件:
<slot name="插槽名"></slot>
- 父组件:
<template v-slot:插槽名>
或<template #插槽名>
- 子组件:
功能: 为插槽指定名称,允许在子组件中定义多个具名插槽。
示例代码
<!-- 子组件: MyCard.vue --> <template> <div class="card"> <h3>卡片标题</h3> <slot name="header"></slot> <div class="card-body"> <slot name="body"></slot> </div> <div class="card-footer"> <slot name="footer"></slot> </div> </div> </template> <!-- 父组件 --> <template> <div> <my-card> <template v-slot:header> <h4>自定义标题</h4> </template> <template #body> <p>这是卡片的正文内容</p> </template> <template v-slot:footer> <button>操作按钮</button> </template> </my-card> </div> </template> <script> import MyCard from './MyCard.vue'; export default { components: { MyCard } }; </script>
作用域插槽
语法:
- 子组件:
<slot name="插槽名" :数据="值"></slot>
- 父组件:
<template v-slot:插槽名="插槽数据"></template>
- 子组件:
功能: 通过插槽向父组件传递数据,父组件可以访问这些数据并自定义内容。
示例代码
<!-- 子组件: TodoItem.vue --> <template> <div class="todo-item"> <slot name="todo" :todo="todo" :index="index"></slot> </div> </template> <script> export default { props: { todo: Object, index: Number } }; </script> <!-- 父组件 --> <template> <div> <todo-item v-for="(todo, index) in todos" :key="index" :todo="todo" :index="index"> <template v-slot:todo="slotProps"> <div> <span>{{ slotProps.index + 1 }}. {{ slotProps.todo.text }}</span> <span v-if="slotProps.todo.completed">(已完成)</span> </div> </template> </todo-item> </div> </template> <script> import TodoItem from './TodoItem.vue'; export default { components: { TodoItem }, data() { return { todos: [ { text: '学习 Vue3', completed: true }, { text: '完成项目', completed: false } ] }; } }; </script>
八、性能提升相关指令
v-once 指令
语法:
v-once
功能: 表示元素和组件只渲染一次。后续数据变化不会影响渲染结果。
示例代码
<template> <div> <p v-once>这个内容只渲染一次: {{ message }}</p> <p>这个内容会更新: {{ message }}</p> <button @click="updateMessage">更新消息</button> </div> </template> <script> export default { data() { return { message: '初始消息' }; }, methods: { updateMessage() { this.message = '消息已更新'; // 更新消息 } } }; </script>
- 说明:
v-once
修饰的<p>
标签内容只在初次渲染时更新,后续数据变化不会影响它。- 没有
v-once
的<p>
标签内容会随着message
的变化而更新。
- 说明:
v-memo 指令 (Vue 3.3+)
- 语法:
v-memo
- 功能: 当指令绑定的值变化时,重新渲染元素或组件。可以减少不必要的渲染。
语法知识点
v-memo
用于优化渲染性能,指示 Vue 仅在依赖项变化时才重新渲染元素。
使用方法
接受一个依赖数组,类似于
computed
的依赖。仅在依赖项变化时重新渲染。
示例代码
<template> <div> <div v-memo="[name, age]"> <p>姓名: {{ name }}</p> <p>年龄: {{ age }}</p> <p>职业: {{ job }}</p> <!-- 职业不会触发重新渲染 --> </div> <button @click="updateName">更新姓名</button> <button @click="updateAge">更新年龄</button> <button @click="updateJob">更新职业</button> </div> </template> <script> export default { data() { return { name: '张三', age: 25, job: '前端工程师' }; }, methods: { updateName() { this.name = '李四'; // 会触发重新渲染 }, updateAge() { this.age = 26; // 会触发重新渲染 }, updateJob() { this.job = '全栈工程师'; // 不会触发重新渲染 } } }; </script>
- 说明:
v-memo="[name, age]"
表示只有当name
或age
发生变化时,才会重新渲染<div>
及其子元素。job
发生变化不会触发重新渲染,因为v-memo
没有监听它。
- 说明:
- 语法:
九、自定义指令
创建与使用自定义指令
语法:
- 全局注册:
app.directive('指令名', 指令配置)
- 局部注册:
directives: { '指令名': 指令配置 }
- 全局注册:
功能: 创建自定义指令,扩展 Vue 的功能。
示例代码
<template> <div> <input v-focus ref="inputRef" placeholder="自动聚焦的输入框"> <button @click="logInput">日志输出输入框</button> </div> </template> <script> // 全局注册自定义指令 const app = Vue.createApp({}); app.directive('focus', { mounted(el) { el.focus(); // 聚焦元素 } }); export default { name: 'App', methods: { logInput() { console.log(this.$refs.inputRef.value); // 输出输入框的值 } } }; </script>
- 说明:
v-focus
是一个全局注册的自定义指令。mounted
钩子函数在指令绑定的元素插入到 DOM 后调用,这里实现自动聚焦。
- 说明:
自定义指令使用场景
场景 1: 实现点击按钮高亮效果
<template> <div> <button v-highlight:yellow>黄色高亮按钮</button> <button v-highlight>默认颜色高亮按钮</button> </div> </template> <script> export default { name: 'App', // 局部注册自定义指令 directives: { highlight: { mounted(el, binding) { // 默认背景色为蓝色 const color = binding.arg || 'blue'; el.style.backgroundColor = color; el.style.padding = '10px 20px'; el.style.border = 'none'; el.style.borderRadius = '4px'; el.style.cursor = 'pointer'; // 点击事件 el.addEventListener('click', () => { el.style.transform = 'scale(1.05)'; setTimeout(() => { el.style.transform = 'scale(1)'; }, 200); }); } } } }; </script>
- 说明:
v-highlight:yellow
使用参数yellow
指定背景颜色。v-highlight
使用默认背景颜色。
- 说明:
自定义指令应用示例
场景 2: 实现长按事件
<template> <div> <button v-longpress="handleLongPress">长按我 2 秒</button> <p v-if="isLongPressed">已长按 2 秒!</p> </div> </template> <script> export default { name: 'App', data() { return { isLongPressed: false, pressTimer: null }; }, methods: { handleLongPress() { this.isLongPressed = true; setTimeout(() => { this.isLongPressed = false; }, 3000); // 3 秒后重置状态 } }, directives: { longpress: { mounted(el, binding, vnode) { let pressTimer; el.addEventListener('mousedown', () => { pressTimer = setTimeout(() => { binding.value(); // 执行传递的方法 }, 2000); // 长按 2 秒触发 }); el.addEventListener('mouseup', () => { clearTimeout(pressTimer); }); el.addEventListener('mouseleave', () => { clearTimeout(pressTimer); }); // 触摸设备支持 el.addEventListener('touchstart', () => { pressTimer = setTimeout(() => { binding.value(); }, 2000); }); el.addEventListener('touchend', () => { clearTimeout(pressTimer); }); el.addEventListener('touchcancel', () => { clearTimeout(pressTimer); }); } } } }; </script>
- 说明:
v-longpress="handleLongPress"
使用自定义指令实现长按事件。- 通过
mousedown
、mouseup
、mouseleave
以及触摸事件实现长按检测。
4. 生命周期钩子函数
mounted
:指令首次绑定到元素时调用。updated
:指令绑定的数据更新时调用。unmounted
:指令从元素上解绑时调用。
案例代码
<!-- 自定义指令 v-auto-resize --> <template> <div> <textarea v-auto-resize></textarea> </div> </template> <script> export default { directives: { autoResize: { mounted(el) { el.style.resize = 'none'; el.addEventListener('input', () => { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; }); }, updated(el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; }, }, }, }; </script>
注释:
v-auto-resize
指令自动调整textarea
的高度以适应内容。- 使用
mounted
和updated
钩子确保在初始化和更新时都调整高度。
- 说明:
十、综合性案例
综合示例 1: 创建一个带有过滤和排序功能的待办事项列表
<template>
<div class="todo-app">
<h2>待办事项列表</h2>
<!-- 输入新待办事项 -->
<div class="input-group">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="输入新的待办事项"
>
<button @click="addTodo">添加</button>
</div>
<!-- 过滤选项 -->
<div class="filter-options">
<button
v-for="(filter, index) in filters"
:key="index"
:class="{ active: currentFilter === filter }"
@click="currentFilter = filter"
>
{{ filter }}
</button>
</div>
<!-- 待办事项列表 -->
<ul class="todo-list">
<li
v-for="(todo, index) in filteredTodos"
:key="index"
:class="{ completed: todo.completed }"
>
<input type="checkbox" v-model="todo.completed">
<span>{{ todo.text }}</span>
<button @click="deleteTodo(index)">删除</button>
</li>
</ul>
<!-- 统计信息 -->
<div class="todo-stats">
<p>总待办事项: {{ todos.length }}</p>
<p>已完成: {{ completedCount }}</p>
<p>未完成: {{ pendingCount }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'TodoApp',
data() {
return {
newTodo: '',
todos: [
{ text: '学习 Vue3', completed: true },
{ text: '完成项目', completed: false },
{ text: '阅读文档', completed: false }
],
currentFilter: '全部',
filters: ['全部', '已完成', '未完成']
};
},
computed: {
// 计算过滤后的待办事项列表
filteredTodos() {
if (this.currentFilter === '全部') {
return this.todos;
} else if (this.currentFilter === '已完成') {
return this.todos.filter(todo => todo.completed);
} else if (this.currentFilter === '未完成') {
return this.todos.filter(todo => !todo.completed);
}
return this.todos;
},
// 计算已完成的待办事项数量
completedCount() {
return this.todos.filter(todo => todo.completed).length;
},
// 计算未完成的待办事项数量
pendingCount() {
return this.todos.filter(todo => !todo.completed).length;
}
},
methods: {
// 添加新的待办事项
addTodo() {
if (this.newTodo.trim() !== '') {
this.todos.push({
text: this.newTodo.trim(),
completed: false
});
this.newTodo = ''; // 清空输入框
}
},
// 删除待办事项
deleteTodo(index) {
this.todos.splice(index, 1);
}
}
};
</script>
<style scoped>
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.input-group {
display: flex;
margin-bottom: 20px;
}
.input-group input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
outline: none;
}
.input-group button {
padding: 10px 15px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
.filter-options {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filter-options button {
padding: 8px 12px;
background-color: #f1f1f1;
border: none;
border-radius: 4px;
cursor: pointer;
}
.filter-options button.active {
background-color: #4CAF50;
color: white;
}
.todo-list {
list-style-type: none;
padding: 0;
margin-bottom: 20px;
}
.todo-list li {
display: flex;
align-items: center;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 8px;
background-color: white;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #888;
}
.todo-list input[type="checkbox"] {
margin-right: 10px;
}
.todo-list button {
margin-left: auto;
padding: 5px 10px;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.todo-stats {
display: flex;
justify-content: space-between;
background-color: #f9f9f9;
padding: 12px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
</style>
综合示例 2: 创建一个带有主题切换和响应式布局的用户卡片组件
<template>
<div :class="['user-card', { 'dark-theme': isDarkTheme }]" :style="cardStyle">
<div class="card-header">
<h3 v-if="showTitle">用户信息卡片</h3>
<div class="theme-toggle">
<label>
<input type="checkbox" v-model="isDarkTheme">
<span class="slider"></span>
</label>
<span>暗色模式</span>
</div>
</div>
<div class="card-content">
<div class="user-avatar">
<img :src="user.avatar" :alt="user.name" v-if="user.avatar">
<div class="default-avatar" v-else>
{{ user.name.charAt(0).toUpperCase() }}
</div>
</div>
<div class="user-details">
<h4>{{ user.name }}</h4>
<p v-if="user.email">邮箱: {{ user.email }}</p>
<p v-if="user.phone">电话: {{ user.phone }}</p>
<p v-if="user.address">地址: {{ user.address }}</p>
</div>
</div>
<div class="card-footer">
<button @click="showDetails = !showDetails">
{{ showDetails ? '隐藏详情' : '显示详情' }}
</button>
<div class="user-more-details" v-if="showDetails">
<p v-if="user.bio">个人简介: {{ user.bio }}</p>
<div v-else class="no-bio">
<p>暂无个人简介</p>
</div>
<p v-if="user.skills && user.skills.length > 0">
技能:
<span
v-for="(skill, index) in user.skills"
:key="index"
class="skill-tag"
>
{{ skill }}
</span>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'UserCard',
props: {
user: {
type: Object,
required: true,
default: () => ({
name: '未知用户',
avatar: null,
email: null,
phone: null,
address: null,
bio: null,
skills: []
})
},
showTitle: {
type: Boolean,
default: true
}
},
data() {
return {
isDarkTheme: false,
showDetails: false
};
},
computed: {
// 计算卡片样式
cardStyle() {
return {
'--card-bg-color': this.isDarkTheme ? '#2c3e50' : '#ffffff',
'--card-text-color': this.isDarkTheme ? '#ecf0f1' : '#34495e',
'--card-border-color': this.isDarkTheme ? '#34495e' : '#bdc3c7',
'--card-shadow-color': this.isDarkTheme ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.1)'
};
}
}
};
</script>
<style scoped>
:root {
--card-bg-color: #ffffff;
--card-text-color: #34495e;
--card-border-color: #bdc3c7;
--card-shadow-color: rgba(0, 0, 0, 0.1);
}
.user-card {
background-color: var(--card-bg-color);
border-radius: 10px;
box-shadow: 0 4px 12px var(--card-shadow-color);
border: 1px solid var(--card-border-color);
overflow: hidden;
transition: all 0.3s ease;
color: var(--card-text-color);
width: 100%;
max-width: 400px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--card-border-color);
}
.card-header h3 {
margin: 0;
font-size: 18px;
}
.theme-toggle {
display: flex;
align-items: center;
gap: 8px;
}
.theme-toggle label {
position: relative;
display: inline-block;
width: 40px;
height: 22px;
}
.theme-toggle input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 22px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4CAF50;
}
input:checked + .slider:before {
transform: translateX(18px);
}
.card-content {
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.user-avatar {
display: flex;
justify-content: center;
margin-bottom: 10px;
}
.user-avatar img {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
}
.default-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #3498db;
color: white;
font-size: 24px;
font-weight: bold;
}
.user-details h4 {
margin-top: 0;
margin-bottom: 10px;
font-size: 18px;
}
.user-details p {
margin: 5px 0;
line-height: 1.5;
}
.card-footer {
padding: 16px 20px;
border-top: 1px solid var(--card-border-color);
}
.card-footer button {
padding: 8px 16px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-bottom: 10px;
}
.card-footer button:hover {
background-color: #2980b9;
}
.user-more-details {
margin-top: 10px;
padding-top: 10px;
border-top: 1px dashed var(--card-border-color);
}
.skill-tag {
display: inline-block;
padding: 4px 8px;
background-color: #e0f7fa;
color: #00838f;
border-radius: 4px;
font-size: 12px;
margin-right: 5px;
margin-bottom: 5px;
}
.no-bio {
padding: 10px;
border-radius: 4px;
background-color: var(--card-border-color);
color: var(--card-text-color);
}
@media (max-width: 480px) {
.user-card {
border-radius: 8px;
}
.user-details h4 {
font-size: 16px;
}
.user-avatar img, .default-avatar {
width: 80px;
height: 80px;
}
}
</style>
综合案例3:动态表单验证
<!-- DynamicForm.vue -->
<template>
<div>
<form @submit.prevent="handleSubmit">
<div>
<label>用户名:</label>
<input v-model="form.username" v-validate="validations.username" type="text" />
<p v-if="errors.username">{{ errors.username }}</p>
</div>
<div>
<label>邮箱:</label>
<input v-model="form.email" v-validate="validations.email" type="text" />
<p v-if="errors.email">{{ errors.email }}</p>
</div>
<button type="submit">提交</button>
</form>
</div>
</template>
<script>
export default {
data() {
return {
form: {
username: '',
email: '',
},
errors: {},
validations: {
username: {
required: true,
minLength: 3,
},
email: {
required: true,
regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
},
},
};
},
directives: {
validate: {
mounted(el, binding, vnode) {
const validateField = () => {
const value = el.value;
const rules = binding.value;
const errors = [];
if (rules.required && !value) {
errors.push('字段是必填的');
}
if (rules.minLength && value.length < rules.minLength) {
errors.push(`字段长度必须至少为 ${rules.minLength} 个字符`);
}
if (rules.regex && !rules.regex.test(value)) {
errors.push('字段格式不正确');
}
vnode.context.errors[vnode.key] = errors;
};
el.addEventListener('input', validateField);
validateField();
},
},
},
methods: {
handleSubmit() {
if (Object.keys(this.errors).length === 0) {
alert('表单提交成功');
} else {
alert('表单验证失败');
}
},
},
};
</script>
注释:
- 使用自定义指令
v-validate
对每个表单字段进行验证。 v-validate
指令根据validations
数据定义验证规则。- 动态更新
errors
对象,显示相应的错误信息。
综合案例4:动态指令参数
<!-- DynamicDirective.vue -->
<template>
<div>
<p v-highlight="color">这是一个高亮显示的文本</p>
<button @click="changeColor">更改颜色</button>
</div>
</template>
<script>
export default {
data() {
return {
color: 'yellow',
};
},
directives: {
highlight: {
mounted(el, binding) {
el.style.backgroundColor = binding.value;
},
updated(el, binding) {
el.style.backgroundColor = binding.value;
},
},
},
methods: {
changeColor() {
this.color = this.color === 'yellow' ? 'lightblue' : 'yellow';
},
},
};
</script>
注释:
v-highlight
指令接受一个参数color
,用于设置元素的背景色。- 通过按钮点击更改
color
的值,动态更新背景色。
综合案例5:无限滚动列表
<template>
<div>
<h1>无限滚动列表</h1>
<ul
v-infinite-scroll="loadMore"
style="height: 400px; overflow-y: auto; border: 1px solid #ccc;"
>
<li
v-for="item in items"
:key="item.id"
style="padding: 10px; border-bottom: 1px solid #eee;"
>
{{ item.content }}
</li>
<li v-if="loading" style="text-align: center; padding: 10px;">
加载中...
</li>
<li v-if="noMore" style="text-align: center; padding: 10px; color: #999;">
没有更多数据了
</li>
</ul>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const items = ref([]);
const loading = ref(false);
const noMore = ref(false);
let page = 1;
// 模拟获取数据
function fetchData() {
return new Promise(resolve => {
setTimeout(() => {
const newItems = Array.from({ length: 10 }, (_, i) => ({
id: (page - 1) * 10 + i + 1,
content: `项目 ${(page - 1) * 10 + i + 1} - 这是第${page}页的内容`
}));
resolve(newItems);
}, 1000);
});
}
// 加载更多数据
async function loadMore() {
if (loading.value || noMore.value) return;
loading.value = true;
const newItems = await fetchData();
if (newItems.length === 0) {
noMore.value = true;
} else {
items.value = [...items.value, ...newItems];
page++;
}
loading.value = false;
}
// 初始化数据
onMounted(() => {
loadMore();
});
// 无限滚动指令
const vInfiniteScroll = {
mounted(el, binding) {
const callback = binding.value;
el.addEventListener('scroll', () => {
// 检查是否滚动到底部
if (el.scrollHeight - el.scrollTop <= el.clientHeight + 50) {
callback();
}
});
// 组件卸载时移除事件监听
onUnmounted(() => {
el.removeEventListener('scroll');
});
}
};
</script>
综合案例6:图片预览指令
<template>
<div>
<h1>图片预览</h1>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<img
v-for="img in images"
:key="img.id"
v-preview="img.large"
:src="img.thumbnail"
style="width: 200px; height: 150px; object-fit: cover; cursor: pointer;"
>
</div>
<!-- 预览模态框 -->
<div v-if="previewVisible" class="preview-modal" @click="previewVisible = false">
<img :src="previewImage" class="preview-image">
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const images = ref([
{ id: 1, thumbnail: 'https://picsum.photos/200/150?random=1', large: 'https://picsum.photos/800/600?random=1' },
{ id: 2, thumbnail: 'https://picsum.photos/200/150?random=2', large: 'https://picsum.photos/800/600?random=2' },
{ id: 3, thumbnail: 'https://picsum.photos/200/150?random=3', large: 'https://picsum.photos/800/600?random=3' },
{ id: 4, thumbnail: 'https://picsum.photos/200/150?random=4', large: 'https://picsum.photos/800/600?random=4' },
{ id: 5, thumbnail: 'https://picsum.photos/200/150?random=5', large: 'https://picsum.photos/800/600?random=5' },
{ id: 6, thumbnail: 'https://picsum.photos/200/150?random=6', large: 'https://picsum.photos/800/600?random=6' },
]);
const previewVisible = ref(false);
const previewImage = ref('');
// 图片预览指令
const vPreview = {
mounted(el, binding) {
el.addEventListener('click', () => {
previewImage.value = binding.value;
previewVisible.value = true;
});
// 组件卸载时移除事件监听
onUnmounted(() => {
el.removeEventListener('click');
});
}
};
</script>
<style>
.preview-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.preview-image {
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
</style>
十一、本章小结
本章节详细介绍了 Vue3 中的指令系统,包括内置指令和自定义指令,以及它们的使用方法和具体案例。主要内容包括:
- 条件渲染: 使用
v-if
、v-else
和v-else-if
动态控制元素的显示与隐藏。 - 循环渲染: 使用
v-for
遍历数组、对象或字符串,动态渲染多个元素。 - 数据插入: 使用文本插值
{{ }}
和 HTML 插值v-html
将数据插入模板。 - 属性绑定: 使用
v-bind
动态绑定 HTML 属性。 - 事件绑定: 使用
v-on
绑定事件处理函数,支持事件修饰符。 - 双向数据绑定: 使用
v-model
创建表单输入的双向绑定。 - 插槽: 使用默认插槽、具名插槽和作用域插槽实现组件的内容分发。
- 性能提升相关指令: 使用
v-once
和v-memo
提升应用性能。 - 自定义指令: 创建和使用自定义指令扩展 Vue 的功能,并通过生命周期钩子函数控制指令行为。
- 综合性案例: 通过两个综合案例(待办事项列表和用户卡片组件)展示如何在实际项目中综合运用 Vue3 指令。
通过本章的学习,读者应能够熟练掌握 Vue3 指令的使用方法,并在实际项目中灵活应用,构建高效、可维护的 Vue3 应用程序。