Skip to content

条件渲染

在 Vue 中,我们可以使用条件渲染指令来根据条件显示或隐藏元素。Vue 提供了几种不同的条件渲染指令,每种都有其特定的用途和性能特点。

v-if

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。

vue
<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-elsev-if 添加一个"else 块":

vue
<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 块"。可以连续使用:

vue
<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-elsev-else-if 也必须紧跟在带 v-if 或者 v-else-if 的元素之后。

<template> 上使用 v-if

因为 v-if 是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template> 元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 <template> 元素。

vue
<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-elsev-else-if 也可以在 <template> 上使用。

v-show

另一个用于根据条件展示元素的选项是 v-show 指令。用法大致一样:

vue
<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 会更合适。

实际应用示例

用户权限控制

vue
<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>

表单步骤控制

vue
<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>

最佳实践

  1. 选择合适的指令

    • 使用 v-if 进行真正的条件渲染
    • 使用 v-show 进行简单的显示/隐藏切换
    • 频繁切换时优先考虑 v-show
  2. 避免不必要的渲染

    • 合理使用 v-if 来避免渲染不需要的内容
    • 在列表渲染中结合条件渲染时要注意性能
  3. 保持逻辑清晰

    • 复杂的条件逻辑应该提取到计算属性中
    • 避免在模板中写过于复杂的表达式
  4. 考虑用户体验

    • 在条件切换时提供适当的过渡效果
    • 确保条件变化时界面的连贯性
  5. 性能优化

    • 对于大型列表,考虑使用虚拟滚动
    • 避免在 v-if 中使用昂贵的计算

下一步

现在你已经掌握了 Vue 的条件渲染,让我们继续学习:

条件渲染是构建动态用户界面的基础,掌握它将让你能够创建更加智能和响应式的应用!

基于 Vue.js 官方文档构建的学习宝典