条件渲染
在 Vue 中,我们可以使用条件渲染指令来根据条件显示或隐藏元素。Vue 提供了几种不同的条件渲染指令,每种都有其特定的用途和性能特点。
v-if
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
<template>
<div>
<h1 v-if="awesome">Vue is awesome!</h1>
<button @click="awesome = !awesome">Toggle</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const awesome = ref(true)
</script>
v-else
你也可以用 v-else
为 v-if
添加一个"else 块":
<template>
<div>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
<button @click="awesome = !awesome">Toggle</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const awesome = ref(true)
</script>
v-else
元素必须紧跟在带 v-if
或者 v-else-if
的元素的后面,否则它将不会被识别。
v-else-if
v-else-if
,顾名思义,充当 v-if
的"else-if 块"。可以连续使用:
<template>
<div>
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
<div class="controls">
<button @click="type = 'A'">Set A</button>
<button @click="type = 'B'">Set B</button>
<button @click="type = 'C'">Set C</button>
<button @click="type = 'D'">Set D</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const type = ref('A')
</script>
<style scoped>
.controls {
margin-top: 20px;
}
.controls button {
margin-right: 10px;
padding: 8px 16px;
cursor: pointer;
}
</style>
类似于 v-else
,v-else-if
也必须紧跟在带 v-if
或者 v-else-if
的元素之后。
在 <template>
上使用 v-if
因为 v-if
是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template>
元素当做不可见的包裹元素,并在上面使用 v-if
。最终的渲染结果将不包含 <template>
元素。
<template>
<div>
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
<button @click="ok = !ok">Toggle</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const ok = ref(true)
</script>
v-else
和 v-else-if
也可以在 <template>
上使用。
v-show
另一个用于根据条件展示元素的选项是 v-show
指令。用法大致一样:
<template>
<div>
<h1 v-show="ok">Hello!</h1>
<button @click="ok = !ok">Toggle</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const ok = ref(true)
</script>
不同之处在于 v-show
会在 DOM 渲染中保留该元素;v-show
仅切换了该元素上名为 display
的 CSS 属性。
v-show
不支持在 <template>
元素上使用,也不能和 v-else
搭配使用。
v-if vs v-show
v-if
是"真实的"按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
v-if
也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
相比之下,v-show
简单许多——元素无论初始条件如何,始终会被渲染,只有 CSS display
属性会被切换。
总的来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show
较好;如果在运行时绑定条件很少改变,则 v-if
会更合适。
实际应用示例
用户权限控制
<template>
<div class="permission-demo">
<h2>用户权限控制演示</h2>
<!-- 用户选择器 -->
<div class="user-selector">
<label>当前用户:</label>
<select v-model="currentUser">
<option v-for="user in users" :key="user.id" :value="user">
{{ user.name }} ({{ user.role }})
</option>
</select>
</div>
<!-- 导航菜单 -->
<nav class="navigation">
<ul>
<li><a href="#">首页</a></li>
<li v-if="canViewProfile"><a href="#">个人资料</a></li>
<li v-if="canManageUsers"><a href="#">用户管理</a></li>
<li v-if="canViewReports"><a href="#">报表中心</a></li>
<li v-if="canManageSystem"><a href="#">系统设置</a></li>
</ul>
</nav>
<!-- 主要内容区域 -->
<main class="main-content">
<!-- 管理员面板 -->
<div v-if="isAdmin" class="admin-panel">
<h3>管理员面板</h3>
<div class="admin-actions">
<button class="btn btn-primary">创建用户</button>
<button class="btn btn-warning">系统维护</button>
<button class="btn btn-danger">清理日志</button>
</div>
</div>
<!-- 编辑器面板 -->
<div v-else-if="isEditor" class="editor-panel">
<h3>编辑器面板</h3>
<div class="editor-actions">
<button class="btn btn-primary">创建文章</button>
<button class="btn btn-secondary">管理文章</button>
<button class="btn btn-info">查看统计</button>
</div>
</div>
<!-- 普通用户面板 -->
<div v-else-if="isUser" class="user-panel">
<h3>用户面板</h3>
<div class="user-actions">
<button class="btn btn-primary">查看文章</button>
<button class="btn btn-secondary">个人设置</button>
</div>
</div>
<!-- 访客面板 -->
<div v-else class="guest-panel">
<h3>访客模式</h3>
<p>请登录以获取更多功能</p>
<button class="btn btn-primary">登录</button>
<button class="btn btn-secondary">注册</button>
</div>
<!-- 功能区域 -->
<div class="feature-sections">
<!-- 数据统计 - 仅管理员和编辑可见 -->
<section v-if="canViewReports" class="stats-section">
<h4>数据统计</h4>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">1,234</div>
<div class="stat-label">总用户数</div>
</div>
<div class="stat-card">
<div class="stat-number">567</div>
<div class="stat-label">活跃用户</div>
</div>
<div class="stat-card" v-if="isAdmin">
<div class="stat-number">89</div>
<div class="stat-label">系统错误</div>
</div>
</div>
</section>
<!-- 最近活动 -->
<section v-if="currentUser" class="activity-section">
<h4>最近活动</h4>
<div class="activity-list">
<div v-for="activity in filteredActivities" :key="activity.id" class="activity-item">
<span class="activity-time">{{ activity.time }}</span>
<span class="activity-text">{{ activity.text }}</span>
</div>
</div>
</section>
<!-- 系统通知 -->
<section class="notification-section">
<h4>系统通知</h4>
<div class="notifications">
<!-- 所有用户都能看到的通知 -->
<div class="notification info">
<strong>系统维护通知:</strong> 系统将于今晚进行例行维护
</div>
<!-- 仅登录用户可见 -->
<div v-if="currentUser" class="notification warning">
<strong>安全提醒:</strong> 请定期更新您的密码
</div>
<!-- 仅管理员可见 -->
<div v-if="isAdmin" class="notification error">
<strong>紧急通知:</strong> 检测到异常登录尝试
</div>
</div>
</section>
</div>
</main>
<!-- 调试信息 -->
<div class="debug-info">
<h4>调试信息</h4>
<p><strong>当前用户:</strong> {{ currentUser?.name || '未登录' }}</p>
<p><strong>用户角色:</strong> {{ currentUser?.role || 'guest' }}</p>
<p><strong>权限列表:</strong></p>
<ul>
<li>查看个人资料: {{ canViewProfile ? '✓' : '✗' }}</li>
<li>管理用户: {{ canManageUsers ? '✓' : '✗' }}</li>
<li>查看报表: {{ canViewReports ? '✓' : '✗' }}</li>
<li>系统管理: {{ canManageSystem ? '✓' : '✗' }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const users = [
{ id: 1, name: '张三', role: 'admin' },
{ id: 2, name: '李四', role: 'editor' },
{ id: 3, name: '王五', role: 'user' },
{ id: 4, name: '赵六', role: 'user' }
]
const currentUser = ref(users[0])
// 角色判断
const isAdmin = computed(() => currentUser.value?.role === 'admin')
const isEditor = computed(() => currentUser.value?.role === 'editor')
const isUser = computed(() => currentUser.value?.role === 'user')
// 权限判断
const canViewProfile = computed(() => !!currentUser.value)
const canManageUsers = computed(() => isAdmin.value)
const canViewReports = computed(() => isAdmin.value || isEditor.value)
const canManageSystem = computed(() => isAdmin.value)
// 活动数据
const activities = [
{ id: 1, time: '10:30', text: '用户张三登录系统', level: 'admin' },
{ id: 2, time: '10:25', text: '发布了新文章', level: 'editor' },
{ id: 3, time: '10:20', text: '更新了个人资料', level: 'user' },
{ id: 4, time: '10:15', text: '系统自动备份完成', level: 'admin' }
]
// 根据用户权限过滤活动
const filteredActivities = computed(() => {
if (isAdmin.value) {
return activities // 管理员可以看到所有活动
} else if (isEditor.value) {
return activities.filter(a => a.level !== 'admin') // 编辑看不到管理员活动
} else {
return activities.filter(a => a.level === 'user') // 普通用户只能看到用户活动
}
})
</script>
<style scoped>
.permission-demo {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.user-selector {
margin-bottom: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 6px;
}
.user-selector label {
font-weight: bold;
margin-right: 10px;
}
.user-selector select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.navigation {
background-color: #343a40;
border-radius: 6px;
margin-bottom: 20px;
}
.navigation ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
}
.navigation li {
border-right: 1px solid #495057;
}
.navigation li:last-child {
border-right: none;
}
.navigation a {
display: block;
padding: 15px 20px;
color: #ffffff;
text-decoration: none;
transition: background-color 0.3s;
}
.navigation a:hover {
background-color: #495057;
}
.main-content {
background-color: #ffffff;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 20px;
margin-bottom: 20px;
}
.admin-panel,
.editor-panel,
.user-panel,
.guest-panel {
margin-bottom: 30px;
padding: 20px;
border-radius: 6px;
}
.admin-panel {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
}
.editor-panel {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
}
.user-panel {
background-color: #d4edda;
border: 1px solid #c3e6cb;
}
.guest-panel {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
}
.admin-actions,
.editor-actions,
.user-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-warning {
background-color: #ffc107;
color: #212529;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-info {
background-color: #17a2b8;
color: white;
}
.btn:hover {
opacity: 0.8;
transform: translateY(-1px);
}
.feature-sections {
display: grid;
gap: 20px;
}
.stats-section,
.activity-section,
.notification-section {
padding: 20px;
background-color: #f8f9fa;
border-radius: 6px;
}
.stats-section h4,
.activity-section h4,
.notification-section h4 {
margin: 0 0 15px 0;
color: #495057;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.stat-card {
background-color: white;
padding: 20px;
border-radius: 6px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: #007bff;
margin-bottom: 5px;
}
.stat-label {
color: #6c757d;
font-size: 0.9em;
}
.activity-list {
max-height: 200px;
overflow-y: auto;
}
.activity-item {
display: flex;
padding: 10px;
border-bottom: 1px solid #dee2e6;
background-color: white;
margin-bottom: 5px;
border-radius: 4px;
}
.activity-time {
font-weight: bold;
color: #007bff;
margin-right: 15px;
min-width: 60px;
}
.activity-text {
flex: 1;
}
.notifications {
display: grid;
gap: 10px;
}
.notification {
padding: 12px;
border-radius: 4px;
border-left: 4px solid;
}
.notification.info {
background-color: #d1ecf1;
border-left-color: #17a2b8;
}
.notification.warning {
background-color: #fff3cd;
border-left-color: #ffc107;
}
.notification.error {
background-color: #f8d7da;
border-left-color: #dc3545;
}
.debug-info {
background-color: #e9ecef;
padding: 20px;
border-radius: 6px;
font-family: monospace;
}
.debug-info h4 {
margin: 0 0 15px 0;
}
.debug-info ul {
list-style: none;
padding: 0;
}
.debug-info li {
padding: 5px 0;
}
@media (max-width: 768px) {
.navigation ul {
flex-direction: column;
}
.navigation li {
border-right: none;
border-bottom: 1px solid #495057;
}
.admin-actions,
.editor-actions,
.user-actions {
flex-direction: column;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>
表单步骤控制
<template>
<div class="multi-step-form">
<h2>多步骤表单</h2>
<!-- 步骤指示器 -->
<div class="step-indicator">
<div
v-for="(step, index) in steps"
:key="index"
class="step"
:class="{
active: currentStep === index + 1,
completed: currentStep > index + 1
}"
>
<div class="step-number">{{ index + 1 }}</div>
<div class="step-title">{{ step.title }}</div>
</div>
</div>
<!-- 表单内容 -->
<div class="form-container">
<!-- 第一步:基本信息 -->
<div v-if="currentStep === 1" class="form-step">
<h3>基本信息</h3>
<div class="form-group">
<label for="name">姓名:</label>
<input
id="name"
v-model="formData.name"
type="text"
:class="{ error: errors.name }"
@blur="validateName"
>
<span v-if="errors.name" class="error-message">{{ errors.name }}</span>
</div>
<div class="form-group">
<label for="email">邮箱:</label>
<input
id="email"
v-model="formData.email"
type="email"
:class="{ error: errors.email }"
@blur="validateEmail"
>
<span v-if="errors.email" class="error-message">{{ errors.email }}</span>
</div>
<div class="form-group">
<label for="phone">电话:</label>
<input
id="phone"
v-model="formData.phone"
type="tel"
:class="{ error: errors.phone }"
@blur="validatePhone"
>
<span v-if="errors.phone" class="error-message">{{ errors.phone }}</span>
</div>
</div>
<!-- 第二步:地址信息 -->
<div v-else-if="currentStep === 2" class="form-step">
<h3>地址信息</h3>
<div class="form-group">
<label for="country">国家:</label>
<select id="country" v-model="formData.country">
<option value="">请选择国家</option>
<option value="china">中国</option>
<option value="usa">美国</option>
<option value="japan">日本</option>
</select>
</div>
<div class="form-group">
<label for="city">城市:</label>
<input
id="city"
v-model="formData.city"
type="text"
:class="{ error: errors.city }"
@blur="validateCity"
>
<span v-if="errors.city" class="error-message">{{ errors.city }}</span>
</div>
<div class="form-group">
<label for="address">详细地址:</label>
<textarea
id="address"
v-model="formData.address"
rows="3"
:class="{ error: errors.address }"
@blur="validateAddress"
></textarea>
<span v-if="errors.address" class="error-message">{{ errors.address }}</span>
</div>
<div class="form-group">
<label for="zipCode">邮政编码:</label>
<input
id="zipCode"
v-model="formData.zipCode"
type="text"
:class="{ error: errors.zipCode }"
@blur="validateZipCode"
>
<span v-if="errors.zipCode" class="error-message">{{ errors.zipCode }}</span>
</div>
</div>
<!-- 第三步:偏好设置 -->
<div v-else-if="currentStep === 3" class="form-step">
<h3>偏好设置</h3>
<div class="form-group">
<label>兴趣爱好:</label>
<div class="checkbox-group">
<label v-for="hobby in hobbies" :key="hobby.value" class="checkbox-label">
<input
v-model="formData.interests"
type="checkbox"
:value="hobby.value"
>
{{ hobby.label }}
</label>
</div>
</div>
<div class="form-group">
<label>通知方式:</label>
<div class="radio-group">
<label v-for="method in notificationMethods" :key="method.value" class="radio-label">
<input
v-model="formData.notificationMethod"
type="radio"
:value="method.value"
>
{{ method.label }}
</label>
</div>
</div>
<div class="form-group">
<label>
<input
v-model="formData.newsletter"
type="checkbox"
>
订阅邮件通讯
</label>
</div>
<div class="form-group">
<label>
<input
v-model="formData.terms"
type="checkbox"
>
我同意<a href="#" @click.prevent>服务条款</a>
</label>
</div>
</div>
<!-- 第四步:确认信息 -->
<div v-else-if="currentStep === 4" class="form-step">
<h3>确认信息</h3>
<div class="confirmation-section">
<h4>基本信息</h4>
<div class="info-grid">
<div class="info-item">
<span class="label">姓名:</span>
<span class="value">{{ formData.name }}</span>
</div>
<div class="info-item">
<span class="label">邮箱:</span>
<span class="value">{{ formData.email }}</span>
</div>
<div class="info-item">
<span class="label">电话:</span>
<span class="value">{{ formData.phone }}</span>
</div>
</div>
</div>
<div class="confirmation-section">
<h4>地址信息</h4>
<div class="info-grid">
<div class="info-item">
<span class="label">国家:</span>
<span class="value">{{ getCountryLabel(formData.country) }}</span>
</div>
<div class="info-item">
<span class="label">城市:</span>
<span class="value">{{ formData.city }}</span>
</div>
<div class="info-item">
<span class="label">地址:</span>
<span class="value">{{ formData.address }}</span>
</div>
<div class="info-item">
<span class="label">邮编:</span>
<span class="value">{{ formData.zipCode }}</span>
</div>
</div>
</div>
<div class="confirmation-section">
<h4>偏好设置</h4>
<div class="info-grid">
<div class="info-item">
<span class="label">兴趣爱好:</span>
<span class="value">{{ getInterestsLabel() }}</span>
</div>
<div class="info-item">
<span class="label">通知方式:</span>
<span class="value">{{ getNotificationLabel(formData.notificationMethod) }}</span>
</div>
<div class="info-item">
<span class="label">邮件通讯:</span>
<span class="value">{{ formData.newsletter ? '是' : '否' }}</span>
</div>
<div class="info-item">
<span class="label">同意条款:</span>
<span class="value">{{ formData.terms ? '是' : '否' }}</span>
</div>
</div>
</div>
</div>
<!-- 第五步:完成 -->
<div v-else-if="currentStep === 5" class="form-step">
<div class="success-message">
<div class="success-icon">✓</div>
<h3>注册成功!</h3>
<p>感谢您的注册,我们已经向您的邮箱发送了确认邮件。</p>
<button @click="resetForm" class="btn btn-primary">重新开始</button>
</div>
</div>
</div>
<!-- 导航按钮 -->
<div class="form-navigation">
<button
v-if="currentStep > 1 && currentStep < 5"
@click="previousStep"
class="btn btn-secondary"
>
上一步
</button>
<button
v-if="currentStep < 4"
@click="nextStep"
:disabled="!canProceed"
class="btn btn-primary"
>
下一步
</button>
<button
v-if="currentStep === 4"
@click="submitForm"
:disabled="!formData.terms"
class="btn btn-success"
>
提交
</button>
</div>
<!-- 进度条 -->
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: progressPercentage + '%' }"
></div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
const currentStep = ref(1)
const steps = [
{ title: '基本信息' },
{ title: '地址信息' },
{ title: '偏好设置' },
{ title: '确认信息' }
]
const formData = reactive({
name: '',
email: '',
phone: '',
country: '',
city: '',
address: '',
zipCode: '',
interests: [],
notificationMethod: 'email',
newsletter: false,
terms: false
})
const errors = reactive({
name: '',
email: '',
phone: '',
city: '',
address: '',
zipCode: ''
})
const hobbies = [
{ value: 'reading', label: '阅读' },
{ value: 'sports', label: '运动' },
{ value: 'music', label: '音乐' },
{ value: 'travel', label: '旅行' },
{ value: 'cooking', label: '烹饪' },
{ value: 'gaming', label: '游戏' }
]
const notificationMethods = [
{ value: 'email', label: '邮件' },
{ value: 'sms', label: '短信' },
{ value: 'push', label: '推送' }
]
const progressPercentage = computed(() => {
return (currentStep.value / 5) * 100
})
const canProceed = computed(() => {
switch (currentStep.value) {
case 1:
return formData.name && formData.email && formData.phone &&
!errors.name && !errors.email && !errors.phone
case 2:
return formData.country && formData.city && formData.address && formData.zipCode &&
!errors.city && !errors.address && !errors.zipCode
case 3:
return true // 偏好设置都是可选的
default:
return true
}
})
// 验证函数
function validateName() {
if (!formData.name.trim()) {
errors.name = '姓名不能为空'
} else if (formData.name.length < 2) {
errors.name = '姓名至少2个字符'
} else {
errors.name = ''
}
}
function validateEmail() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!formData.email) {
errors.email = '邮箱不能为空'
} else if (!emailRegex.test(formData.email)) {
errors.email = '请输入有效的邮箱地址'
} else {
errors.email = ''
}
}
function validatePhone() {
const phoneRegex = /^1[3-9]\d{9}$/
if (!formData.phone) {
errors.phone = '电话不能为空'
} else if (!phoneRegex.test(formData.phone)) {
errors.phone = '请输入有效的手机号码'
} else {
errors.phone = ''
}
}
function validateCity() {
if (!formData.city.trim()) {
errors.city = '城市不能为空'
} else {
errors.city = ''
}
}
function validateAddress() {
if (!formData.address.trim()) {
errors.address = '地址不能为空'
} else if (formData.address.length < 5) {
errors.address = '地址至少5个字符'
} else {
errors.address = ''
}
}
function validateZipCode() {
const zipRegex = /^\d{6}$/
if (!formData.zipCode) {
errors.zipCode = '邮政编码不能为空'
} else if (!zipRegex.test(formData.zipCode)) {
errors.zipCode = '请输入6位数字邮政编码'
} else {
errors.zipCode = ''
}
}
// 导航函数
function nextStep() {
if (canProceed.value && currentStep.value < 5) {
currentStep.value++
}
}
function previousStep() {
if (currentStep.value > 1) {
currentStep.value--
}
}
function submitForm() {
if (formData.terms) {
currentStep.value = 5
}
}
function resetForm() {
currentStep.value = 1
Object.assign(formData, {
name: '',
email: '',
phone: '',
country: '',
city: '',
address: '',
zipCode: '',
interests: [],
notificationMethod: 'email',
newsletter: false,
terms: false
})
Object.keys(errors).forEach(key => {
errors[key] = ''
})
}
// 辅助函数
function getCountryLabel(value) {
const countries = {
china: '中国',
usa: '美国',
japan: '日本'
}
return countries[value] || value
}
function getInterestsLabel() {
return formData.interests.map(interest => {
const hobby = hobbies.find(h => h.value === interest)
return hobby ? hobby.label : interest
}).join(', ') || '无'
}
function getNotificationLabel(value) {
const method = notificationMethods.find(m => m.value === value)
return method ? method.label : value
}
</script>
<style scoped>
.multi-step-form {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.step-indicator {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
position: relative;
}
.step-indicator::before {
content: '';
position: absolute;
top: 20px;
left: 0;
right: 0;
height: 2px;
background-color: #e9ecef;
z-index: 1;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 2;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #e9ecef;
color: #6c757d;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-bottom: 8px;
transition: all 0.3s;
}
.step.active .step-number {
background-color: #007bff;
color: white;
}
.step.completed .step-number {
background-color: #28a745;
color: white;
}
.step-title {
font-size: 12px;
color: #6c757d;
text-align: center;
}
.step.active .step-title {
color: #007bff;
font-weight: bold;
}
.form-container {
background-color: #f8f9fa;
padding: 30px;
border-radius: 8px;
margin-bottom: 20px;
min-height: 400px;
}
.form-step h3 {
margin: 0 0 20px 0;
color: #333;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
transition: border-color 0.3s;
box-sizing: border-box;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #007bff;
}
.form-group input.error,
.form-group select.error,
.form-group textarea.error {
border-color: #dc3545;
}
.error-message {
display: block;
color: #dc3545;
font-size: 14px;
margin-top: 5px;
}
.checkbox-group,
.radio-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-top: 10px;
}
.checkbox-label,
.radio-label {
display: flex;
align-items: center;
font-weight: normal;
cursor: pointer;
}
.checkbox-label input,
.radio-label input {
width: auto;
margin-right: 8px;
}
.confirmation-section {
margin-bottom: 25px;
padding: 20px;
background-color: white;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.confirmation-section h4 {
margin: 0 0 15px 0;
color: #495057;
border-bottom: 1px solid #dee2e6;
padding-bottom: 10px;
}
.info-grid {
display: grid;
gap: 10px;
}
.info-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #f8f9fa;
}
.info-item .label {
font-weight: bold;
color: #6c757d;
}
.info-item .value {
color: #333;
}
.success-message {
text-align: center;
padding: 40px 20px;
}
.success-icon {
width: 80px;
height: 80px;
border-radius: 50%;
background-color: #28a745;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
margin: 0 auto 20px;
}
.success-message h3 {
color: #28a745;
margin-bottom: 15px;
}
.success-message p {
color: #6c757d;
margin-bottom: 25px;
line-height: 1.6;
}
.form-navigation {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.3s;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn:hover:not(:disabled) {
opacity: 0.8;
transform: translateY(-1px);
}
.btn:disabled {
background-color: #e9ecef;
color: #6c757d;
cursor: not-allowed;
transform: none;
}
.progress-bar {
height: 6px;
background-color: #e9ecef;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007bff, #0056b3);
transition: width 0.3s ease;
}
@media (max-width: 768px) {
.step-indicator {
flex-direction: column;
gap: 20px;
}
.step-indicator::before {
display: none;
}
.checkbox-group,
.radio-group {
grid-template-columns: 1fr;
}
.form-navigation {
flex-direction: column;
gap: 10px;
}
}
</style>
最佳实践
选择合适的指令:
- 使用
v-if
进行真正的条件渲染 - 使用
v-show
进行简单的显示/隐藏切换 - 频繁切换时优先考虑
v-show
- 使用
避免不必要的渲染:
- 合理使用
v-if
来避免渲染不需要的内容 - 在列表渲染中结合条件渲染时要注意性能
- 合理使用
保持逻辑清晰:
- 复杂的条件逻辑应该提取到计算属性中
- 避免在模板中写过于复杂的表达式
考虑用户体验:
- 在条件切换时提供适当的过渡效果
- 确保条件变化时界面的连贯性
性能优化:
- 对于大型列表,考虑使用虚拟滚动
- 避免在
v-if
中使用昂贵的计算
下一步
现在你已经掌握了 Vue 的条件渲染,让我们继续学习:
条件渲染是构建动态用户界面的基础,掌握它将让你能够创建更加智能和响应式的应用!