Vue 條件渲染
條件渲染(Conditional Rendering)是根據條件來決定是否渲染某個元素或元件。Vue 提供了 v-if、v-else-if、v-else 和 v-show 等指令來實現條件渲染。
v-if
v-if 指令用於條件性地渲染一個元素。當條件為真時渲染,為假時不渲染(DOM 中不存在):
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>
<template>
<p v-if="isVisible">這段文字會顯示</p>
<button @click="isVisible = !isVisible">切換</button>
</template>
v-else
v-else 表示「否則」,必須緊跟在 v-if 或 v-else-if 後面:
<script setup>
import { ref } from 'vue'
const isLoggedIn = ref(false)
</script>
<template>
<div v-if="isLoggedIn">
<p>歡迎回來!</p>
<button @click="isLoggedIn = false">登出</button>
</div>
<div v-else>
<p>請先登入</p>
<button @click="isLoggedIn = true">登入</button>
</div>
</template>
v-else-if
v-else-if 用於多重條件判斷:
<script setup>
import { ref } from 'vue'
const score = ref(85)
</script>
<template>
<div>
<input v-model.number="score" type="number" min="0" max="100">
<p v-if="score >= 90">等級:A(優秀)</p>
<p v-else-if="score >= 80">等級:B(良好)</p>
<p v-else-if="score >= 70">等級:C(中等)</p>
<p v-else-if="score >= 60">等級:D(及格)</p>
<p v-else>等級:F(不及格)</p>
</div>
</template>
在 template 上使用 v-if
如果想要同時切換多個元素,可以在 <template> 上使用 v-if。<template> 元素不會被渲染到最終的 DOM 中:
<script setup>
import { ref } from 'vue'
const showDetails = ref(false)
</script>
<template>
<button @click="showDetails = !showDetails">
{{ showDetails ? '隱藏' : '顯示' }}詳情
</button>
<template v-if="showDetails">
<h2>商品詳情</h2>
<p>這是商品的詳細描述...</p>
<ul>
<li>特色 1</li>
<li>特色 2</li>
<li>特色 3</li>
</ul>
</template>
</template>
v-show
v-show 也是根據條件顯示元素,但它是透過 CSS 的 display 屬性來切換,元素始終會被渲染到 DOM 中:
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>
<template>
<p v-show="isVisible">這段文字可以顯示/隱藏</p>
<button @click="isVisible = !isVisible">切換</button>
</template>
當 isVisible 為 false 時,渲染結果會是:
<p style="display: none;">這段文字可以顯示/隱藏</p>
v-if vs v-show
| 特性 | v-if | v-show |
|---|---|---|
| 渲染方式 | 條件為假時不渲染 | 始終渲染,用 CSS 隱藏 |
| 切換成本 | 較高(銷毀/重建元素) | 較低(只改 CSS) |
| 初始渲染成本 | 條件為假時較低 | 始終渲染,成本固定 |
支援 <template> | ✅ 支援 | ❌ 不支援 |
支援 v-else | ✅ 支援 | ❌ 不支援 |
選擇建議
- v-if:適合條件很少改變的情況,或初始條件為假且可能永遠不會變為真的情況
- v-show:適合需要頻繁切換的情況
<script setup>
import { ref } from 'vue'
const showTooltip = ref(false) // 頻繁切換,用 v-show
const isAdmin = ref(false) // 很少改變,用 v-if
</script>
<template>
<!-- 頻繁切換:使用 v-show -->
<div
class="tooltip"
v-show="showTooltip"
@mouseenter="showTooltip = true"
@mouseleave="showTooltip = false"
>
提示訊息
</div>
<!-- 很少改變:使用 v-if -->
<div v-if="isAdmin" class="admin-panel">
管理員面板
</div>
</template>
v-if 與 v-for
不建議在同一個元素上同時使用
v-if 和 v-for。當它們同時存在時,v-if 的優先級更高,這意味著 v-if 無法存取 v-for 中的變數。錯誤示範
<!-- ❌ 不好:v-if 無法存取 item -->
<li v-for="item in items" v-if="item.isActive" :key="item.id">
{{ item.name }}
</li>
正確做法 1:使用 computed 過濾
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, name: 'Apple', isActive: true },
{ id: 2, name: 'Banana', isActive: false },
{ id: 3, name: 'Orange', isActive: true }
])
// 使用 computed 過濾
const activeItems = computed(() => items.value.filter(item => item.isActive))
</script>
<template>
<ul>
<li v-for="item in activeItems" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
正確做法 2:用 template 包裝
<template>
<ul>
<template v-for="item in items" :key="item.id">
<li v-if="item.isActive">
{{ item.name }}
</li>
</template>
</ul>
</template>
正確做法 3:判斷是否顯示整個列表
如果是要根據條件決定是否渲染整個列表,把 v-if 放在外層:
<template>
<ul v-if="items.length > 0">
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<p v-else>沒有項目</p>
</template>
實際範例
登入狀態切換
<script setup>
import { ref, reactive } from 'vue'
const isLoggedIn = ref(false)
const user = reactive({
name: '',
email: ''
})
function login() {
// 模擬登入
user.name = 'John'
user.email = 'john@example.com'
isLoggedIn.value = true
}
function logout() {
user.name = ''
user.email = ''
isLoggedIn.value = false
}
</script>
<template>
<header>
<template v-if="isLoggedIn">
<span>歡迎,{{ user.name }}</span>
<button @click="logout">登出</button>
</template>
<template v-else>
<button @click="login">登入</button>
<button>註冊</button>
</template>
</header>
</template>
載入狀態
<script setup>
import { ref, onMounted } from 'vue'
const isLoading = ref(true)
const error = ref(null)
const data = ref(null)
onMounted(async () => {
try {
const response = await fetch('/api/data')
if (!response.ok) throw new Error('載入失敗')
data.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
isLoading.value = false
}
})
</script>
<template>
<div class="container">
<!-- 載入中 -->
<div v-if="isLoading" class="loading">
<span class="spinner"></span>
載入中...
</div>
<!-- 錯誤狀態 -->
<div v-else-if="error" class="error">
<p>發生錯誤:{{ error }}</p>
<button @click="retry">重試</button>
</div>
<!-- 正常顯示資料 -->
<div v-else class="content">
<h1>{{ data.title }}</h1>
<p>{{ data.description }}</p>
</div>
</div>
</template>
<style scoped>
.loading {
display: flex;
align-items: center;
gap: 10px;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #ddd;
border-top-color: #42b883;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error {
color: #dc3545;
}
</style>
步驟嚮導
<script setup>
import { ref } from 'vue'
const currentStep = ref(1)
const totalSteps = 3
function nextStep() {
if (currentStep.value < totalSteps) {
currentStep.value++
}
}
function prevStep() {
if (currentStep.value > 1) {
currentStep.value--
}
}
</script>
<template>
<div class="wizard">
<!-- 步驟指示器 -->
<div class="steps">
<span
v-for="step in totalSteps"
:key="step"
:class="{ active: step === currentStep, completed: step < currentStep }"
>
{{ step }}
</span>
</div>
<!-- 步驟內容 -->
<div class="content">
<div v-if="currentStep === 1">
<h2>步驟 1:基本資料</h2>
<input type="text" placeholder="姓名">
<input type="email" placeholder="Email">
</div>
<div v-else-if="currentStep === 2">
<h2>步驟 2:選擇方案</h2>
<label><input type="radio" name="plan" value="basic"> 基本方案</label>
<label><input type="radio" name="plan" value="pro"> 專業方案</label>
</div>
<div v-else-if="currentStep === 3">
<h2>步驟 3:確認</h2>
<p>請確認您的資料...</p>
</div>
</div>
<!-- 導航按鈕 -->
<div class="navigation">
<button @click="prevStep" :disabled="currentStep === 1">上一步</button>
<button v-if="currentStep < totalSteps" @click="nextStep">下一步</button>
<button v-else @click="submit">送出</button>
</div>
</div>
</template>
權限控制
<script setup>
import { ref, computed } from 'vue'
const user = ref({
name: 'John',
role: 'editor' // admin, editor, viewer
})
const isAdmin = computed(() => user.value.role === 'admin')
const canEdit = computed(() => ['admin', 'editor'].includes(user.value.role))
</script>
<template>
<div class="dashboard">
<h1>儀表板</h1>
<!-- 所有人都能看到 -->
<section class="overview">
<h2>總覽</h2>
<p>歡迎,{{ user.name }}</p>
</section>
<!-- 只有可編輯者能看到 -->
<section v-if="canEdit" class="editor">
<h2>編輯區</h2>
<button>新增文章</button>
<button>編輯文章</button>
</section>
<!-- 只有管理員能看到 -->
<section v-if="isAdmin" class="admin">
<h2>管理區</h2>
<button>使用者管理</button>
<button>系統設定</button>
</section>
</div>
</template>
空狀態處理
<script setup>
import { ref } from 'vue'
const todos = ref([])
function addTodo() {
const text = prompt('輸入待辦事項')
if (text) {
todos.value.push({ id: Date.now(), text, done: false })
}
}
</script>
<template>
<div class="todo-app">
<h1>待辦事項</h1>
<!-- 有資料時顯示列表 -->
<ul v-if="todos.length > 0">
<li v-for="todo in todos" :key="todo.id">
<input type="checkbox" v-model="todo.done">
<span :class="{ done: todo.done }">{{ todo.text }}</span>
</li>
</ul>
<!-- 無資料時顯示空狀態 -->
<div v-else class="empty-state">
<p>🎉 太棒了!沒有待辦事項</p>
<button @click="addTodo">新增第一個待辦事項</button>
</div>
<button @click="addTodo" v-show="todos.length > 0">新增</button>
</div>
</template>
<style scoped>
.done {
text-decoration: line-through;
color: #999;
}
.empty-state {
text-align: center;
padding: 40px;
background-color: #f5f5f5;
border-radius: 8px;
}
</style>