Vue 條件渲染

條件渲染(Conditional Rendering)是根據條件來決定是否渲染某個元素或元件。Vue 提供了 v-ifv-else-ifv-elsev-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-ifv-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>

isVisiblefalse 時,渲染結果會是:

<p style="display: none;">這段文字可以顯示/隱藏</p>

v-if vs v-show

特性v-ifv-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-ifv-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>