Skip to content

条件渲染

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

v-if

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

vue
<template>
  <div>
    <h1 v-if="awesome">Vue is awesome!</h1>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const awesome = ref(true)
</script>

v-else

你也可以使用 v-elsev-if 添加一个"else 块":

vue
<template>
  <div>
    <button @click="awesome = !awesome">Toggle</button>
    
    <h1 v-if="awesome">Vue is awesome!</h1>
    <h1 v-else>Oh no 😢</h1>
  </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>
</template>

<script setup>
import { ref } from 'vue'

const type = ref('A')
</script>

v-else 类似,一个使用 v-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>
  </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>
  </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="dashboard">
    <nav>
      <ul>
        <li><a href="#" @click="currentView = 'home'">首页</a></li>
        <li v-if="user.role === 'admin'">
          <a href="#" @click="currentView = 'admin'">管理面板</a>
        </li>
        <li v-if="user.role === 'admin' || user.role === 'moderator'">
          <a href="#" @click="currentView = 'moderation'">内容审核</a>
        </li>
        <li><a href="#" @click="currentView = 'profile'">个人资料</a></li>
      </ul>
    </nav>
    
    <main>
      <div v-if="currentView === 'home'">
        <h1>欢迎回来,{{ user.name }}!</h1>
        <p>这是您的个人仪表板。</p>
      </div>
      
      <div v-else-if="currentView === 'admin' && user.role === 'admin'">
        <h1>管理面板</h1>
        <div class="admin-controls">
          <button @click="showUserManagement = !showUserManagement">
            用户管理
          </button>
          <button @click="showSystemSettings = !showSystemSettings">
            系统设置
          </button>
        </div>
        
        <div v-show="showUserManagement" class="panel">
          <h2>用户管理</h2>
          <p>管理系统用户...</p>
        </div>
        
        <div v-show="showSystemSettings" class="panel">
          <h2>系统设置</h2>
          <p>配置系统参数...</p>
        </div>
      </div>
      
      <div v-else-if="currentView === 'moderation'">
        <h1>内容审核</h1>
        <p v-if="pendingReviews.length === 0">
          暂无待审核内容
        </p>
        <div v-else>
          <p>待审核内容:{{ pendingReviews.length }} 项</p>
          <!-- 审核内容列表 -->
        </div>
      </div>
      
      <div v-else-if="currentView === 'profile'">
        <h1>个人资料</h1>
        <p>编辑您的个人信息...</p>
      </div>
      
      <div v-else>
        <h1>页面未找到</h1>
        <p>您访问的页面不存在或您没有权限访问。</p>
      </div>
    </main>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'

const user = reactive({
  name: '张三',
  role: 'admin' // 'admin', 'moderator', 'user'
})

const currentView = ref('home')
const showUserManagement = ref(false)
const showSystemSettings = ref(false)
const pendingReviews = ref([])
</script>

<style scoped>
.dashboard {
  display: flex;
  min-height: 100vh;
}

nav {
  width: 200px;
  background-color: #f8f9fa;
  padding: 1rem;
}

nav ul {
  list-style: none;
  padding: 0;
}

nav li {
  margin-bottom: 0.5rem;
}

nav a {
  color: #007bff;
  text-decoration: none;
  padding: 0.5rem;
  display: block;
  border-radius: 4px;
}

nav a:hover {
  background-color: #e9ecef;
}

main {
  flex: 1;
  padding: 2rem;
}

.admin-controls {
  margin: 1rem 0;
}

.admin-controls button {
  margin-right: 1rem;
  padding: 0.5rem 1rem;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.panel {
  background-color: #f8f9fa;
  padding: 1rem;
  margin: 1rem 0;
  border-radius: 4px;
}
</style>

表单状态管理

vue
<template>
  <div class="form-container">
    <form @submit.prevent="handleSubmit">
      <h2>用户注册</h2>
      
      <!-- 步骤指示器 -->
      <div class="step-indicator">
        <div 
          v-for="(step, index) in steps" 
          :key="index"
          :class="stepClass(index)"
        >
          {{ step }}
        </div>
      </div>
      
      <!-- 第一步:基本信息 -->
      <div v-if="currentStep === 1" class="step">
        <h3>基本信息</h3>
        
        <div class="form-group">
          <label for="username">用户名</label>
          <input 
            id="username"
            v-model="form.username" 
            type="text" 
            required
          />
          <div v-if="errors.username" class="error">
            {{ errors.username }}
          </div>
        </div>
        
        <div class="form-group">
          <label for="email">邮箱</label>
          <input 
            id="email"
            v-model="form.email" 
            type="email" 
            required
          />
          <div v-if="errors.email" class="error">
            {{ errors.email }}
          </div>
        </div>
        
        <div class="form-group">
          <label for="password">密码</label>
          <input 
            id="password"
            v-model="form.password" 
            type="password" 
            required
          />
          <div v-if="errors.password" class="error">
            {{ errors.password }}
          </div>
        </div>
      </div>
      
      <!-- 第二步:个人信息 -->
      <div v-else-if="currentStep === 2" class="step">
        <h3>个人信息</h3>
        
        <div class="form-group">
          <label for="firstName">姓名</label>
          <input 
            id="firstName"
            v-model="form.firstName" 
            type="text" 
            required
          />
        </div>
        
        <div class="form-group">
          <label for="lastName">姓氏</label>
          <input 
            id="lastName"
            v-model="form.lastName" 
            type="text" 
            required
          />
        </div>
        
        <div class="form-group">
          <label for="birthDate">出生日期</label>
          <input 
            id="birthDate"
            v-model="form.birthDate" 
            type="date" 
          />
        </div>
        
        <div class="form-group">
          <label for="phone">电话号码</label>
          <input 
            id="phone"
            v-model="form.phone" 
            type="tel" 
          />
        </div>
      </div>
      
      <!-- 第三步:确认信息 -->
      <div v-else-if="currentStep === 3" class="step">
        <h3>确认信息</h3>
        
        <div class="confirmation">
          <h4>基本信息</h4>
          <p><strong>用户名:</strong>{{ form.username }}</p>
          <p><strong>邮箱:</strong>{{ form.email }}</p>
          
          <h4>个人信息</h4>
          <p><strong>姓名:</strong>{{ form.firstName }} {{ form.lastName }}</p>
          <p v-if="form.birthDate">
            <strong>出生日期:</strong>{{ form.birthDate }}
          </p>
          <p v-if="form.phone">
            <strong>电话:</strong>{{ form.phone }}
          </p>
        </div>
        
        <div class="form-group">
          <label>
            <input 
              v-model="form.agreeToTerms" 
              type="checkbox" 
              required
            />
            我同意<a href="#" @click.prevent="showTerms = true">服务条款</a>
          </label>
        </div>
        
        <div class="form-group">
          <label>
            <input 
              v-model="form.subscribeNewsletter" 
              type="checkbox"
            />
            订阅我们的新闻通讯
          </label>
        </div>
      </div>
      
      <!-- 提交成功 -->
      <div v-else-if="currentStep === 4" class="step success">
        <h3>注册成功!</h3>
        <p>感谢您的注册,我们已向您的邮箱发送了确认邮件。</p>
        <button type="button" @click="resetForm">注册新用户</button>
      </div>
      
      <!-- 按钮组 -->
      <div v-if="currentStep < 4" class="button-group">
        <button 
          v-if="currentStep > 1" 
          type="button" 
          @click="previousStep"
          class="btn-secondary"
        >
          上一步
        </button>
        
        <button 
          v-if="currentStep < 3" 
          type="button" 
          @click="nextStep"
          :disabled="!canProceed"
          class="btn-primary"
        >
          下一步
        </button>
        
        <button 
          v-if="currentStep === 3" 
          type="submit"
          :disabled="!form.agreeToTerms || isSubmitting"
          class="btn-primary"
        >
          <span v-if="isSubmitting">提交中...</span>
          <span v-else>完成注册</span>
        </button>
      </div>
    </form>
    
    <!-- 服务条款模态框 -->
    <div v-if="showTerms" class="modal-overlay" @click="showTerms = false">
      <div class="modal" @click.stop>
        <h3>服务条款</h3>
        <p>这里是服务条款的内容...</p>
        <button @click="showTerms = false">关闭</button>
      </div>
    </div>
    
    <!-- 加载指示器 -->
    <div v-if="isSubmitting" class="loading-overlay">
      <div class="spinner"></div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed } from 'vue'

const currentStep = ref(1)
const isSubmitting = ref(false)
const showTerms = ref(false)

const steps = ['基本信息', '个人信息', '确认信息']

const form = reactive({
  username: '',
  email: '',
  password: '',
  firstName: '',
  lastName: '',
  birthDate: '',
  phone: '',
  agreeToTerms: false,
  subscribeNewsletter: false
})

const errors = reactive({
  username: '',
  email: '',
  password: ''
})

const stepClass = (index) => ({
  'step-item': true,
  'active': index + 1 === currentStep.value,
  'completed': index + 1 < currentStep.value
})

const canProceed = computed(() => {
  if (currentStep.value === 1) {
    return form.username && form.email && form.password && 
           !errors.username && !errors.email && !errors.password
  } else if (currentStep.value === 2) {
    return form.firstName && form.lastName
  }
  return true
})

const validateStep1 = () => {
  errors.username = form.username.length < 3 ? '用户名至少3个字符' : ''
  errors.email = !/\S+@\S+\.\S+/.test(form.email) ? '请输入有效邮箱' : ''
  errors.password = form.password.length < 6 ? '密码至少6个字符' : ''
  
  return !errors.username && !errors.email && !errors.password
}

const nextStep = () => {
  if (currentStep.value === 1 && !validateStep1()) {
    return
  }
  
  if (currentStep.value < 3) {
    currentStep.value++
  }
}

const previousStep = () => {
  if (currentStep.value > 1) {
    currentStep.value--
  }
}

const handleSubmit = async () => {
  isSubmitting.value = true
  
  try {
    // 模拟 API 调用
    await new Promise(resolve => setTimeout(resolve, 2000))
    
    currentStep.value = 4
  } catch (error) {
    console.error('注册失败:', error)
  } finally {
    isSubmitting.value = false
  }
}

const resetForm = () => {
  currentStep.value = 1
  Object.keys(form).forEach(key => {
    if (typeof form[key] === 'boolean') {
      form[key] = false
    } else {
      form[key] = ''
    }
  })
  Object.keys(errors).forEach(key => {
    errors[key] = ''
  })
}
</script>

<style scoped>
.form-container {
  max-width: 600px;
  margin: 0 auto;
  padding: 2rem;
  position: relative;
}

.step-indicator {
  display: flex;
  justify-content: space-between;
  margin-bottom: 2rem;
}

.step-item {
  flex: 1;
  text-align: center;
  padding: 0.5rem;
  background-color: #e9ecef;
  margin: 0 0.25rem;
  border-radius: 4px;
  transition: background-color 0.3s ease;
}

.step-item.active {
  background-color: #007bff;
  color: white;
}

.step-item.completed {
  background-color: #28a745;
  color: white;
}

.step {
  min-height: 400px;
}

.form-group {
  margin-bottom: 1rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: bold;
}

.form-group input {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.error {
  color: #dc3545;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

.confirmation {
  background-color: #f8f9fa;
  padding: 1rem;
  border-radius: 4px;
  margin-bottom: 1rem;
}

.confirmation h4 {
  margin-top: 1rem;
  margin-bottom: 0.5rem;
  color: #495057;
}

.confirmation h4:first-child {
  margin-top: 0;
}

.success {
  text-align: center;
  padding: 2rem;
}

.success h3 {
  color: #28a745;
  margin-bottom: 1rem;
}

.button-group {
  display: flex;
  justify-content: space-between;
  margin-top: 2rem;
}

.btn-primary, .btn-secondary {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
}

.btn-primary {
  background-color: #007bff;
  color: white;
}

.btn-primary:disabled {
  background-color: #6c757d;
  cursor: not-allowed;
}

.btn-secondary {
  background-color: #6c757d;
  color: white;
}

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.modal {
  background-color: white;
  padding: 2rem;
  border-radius: 8px;
  max-width: 500px;
  max-height: 80vh;
  overflow-y: auto;
}

.loading-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(255, 255, 255, 0.8);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1001;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #007bff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>

动态内容展示

vue
<template>
  <div class="content-display">
    <div class="filters">
      <button 
        v-for="category in categories" 
        :key="category"
        @click="selectedCategory = category"
        :class="{ active: selectedCategory === category }"
      >
        {{ category }}
      </button>
    </div>
    
    <div class="search-bar">
      <input 
        v-model="searchQuery" 
        placeholder="搜索内容..."
        @input="handleSearch"
      />
    </div>
    
    <div class="content-area">
      <!-- 加载状态 -->
      <div v-if="isLoading" class="loading">
        <p>加载中...</p>
      </div>
      
      <!-- 错误状态 -->
      <div v-else-if="error" class="error-state">
        <h3>出错了</h3>
        <p>{{ error }}</p>
        <button @click="retry">重试</button>
      </div>
      
      <!-- 空状态 -->
      <div v-else-if="filteredItems.length === 0" class="empty-state">
        <h3>没有找到内容</h3>
        <p v-if="searchQuery">
          没有找到包含 "{{ searchQuery }}" 的内容
        </p>
        <p v-else>
          当前分类下暂无内容
        </p>
      </div>
      
      <!-- 内容列表 -->
      <div v-else class="content-list">
        <div 
          v-for="item in filteredItems" 
          :key="item.id"
          class="content-item"
          @click="selectItem(item)"
        >
          <img 
            v-if="item.image" 
            :src="item.image" 
            :alt="item.title"
            class="item-image"
          />
          <div class="item-content">
            <h3>{{ item.title }}</h3>
            <p>{{ item.description }}</p>
            <div class="item-meta">
              <span class="category">{{ item.category }}</span>
              <span class="date">{{ formatDate(item.date) }}</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    
    <!-- 详情模态框 -->
    <div v-if="selectedItem" class="modal-overlay" @click="closeModal">
      <div class="modal" @click.stop>
        <button class="close-btn" @click="closeModal">&times;</button>
        
        <img 
          v-if="selectedItem.image" 
          :src="selectedItem.image" 
          :alt="selectedItem.title"
          class="modal-image"
        />
        
        <h2>{{ selectedItem.title }}</h2>
        <p class="modal-description">{{ selectedItem.description }}</p>
        
        <div v-if="selectedItem.content" class="modal-content">
          <h3>详细内容</h3>
          <p>{{ selectedItem.content }}</p>
        </div>
        
        <div class="modal-actions">
          <button @click="likeItem(selectedItem)" class="btn-like">
            {{ selectedItem.liked ? '取消点赞' : '点赞' }}
            ({{ selectedItem.likes }})
          </button>
          <button @click="shareItem(selectedItem)" class="btn-share">
            分享
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'

const categories = ['全部', '技术', '设计', '产品', '营销']
const selectedCategory = ref('全部')
const searchQuery = ref('')
const isLoading = ref(false)
const error = ref('')
const items = ref([])
const selectedItem = ref(null)

const filteredItems = computed(() => {
  let filtered = items.value

  // 按分类过滤
  if (selectedCategory.value !== '全部') {
    filtered = filtered.filter(item => item.category === selectedCategory.value)
  }

  // 按搜索关键词过滤
  if (searchQuery.value) {
    const query = searchQuery.value.toLowerCase()
    filtered = filtered.filter(item => 
      item.title.toLowerCase().includes(query) ||
      item.description.toLowerCase().includes(query)
    )
  }

  return filtered
})

const loadItems = async () => {
  isLoading.value = true
  error.value = ''
  
  try {
    // 模拟 API 调用
    await new Promise(resolve => setTimeout(resolve, 1000))
    
    items.value = [
      {
        id: 1,
        title: 'Vue.js 3.0 新特性',
        description: '了解 Vue.js 3.0 带来的激动人心的新功能',
        category: '技术',
        date: new Date('2023-01-15'),
        image: '/api/placeholder/300/200',
        content: 'Vue.js 3.0 引入了许多新特性,包括组合式 API、更好的 TypeScript 支持等...',
        likes: 42,
        liked: false
      },
      {
        id: 2,
        title: '现代 UI 设计趋势',
        description: '探索 2023 年最新的用户界面设计趋势',
        category: '设计',
        date: new Date('2023-02-20'),
        image: '/api/placeholder/300/200',
        content: '现代 UI 设计注重简洁性、可访问性和用户体验...',
        likes: 38,
        liked: true
      },
      {
        id: 3,
        title: '产品管理最佳实践',
        description: '成功产品管理的关键策略和方法',
        category: '产品',
        date: new Date('2023-03-10'),
        image: '/api/placeholder/300/200',
        content: '有效的产品管理需要平衡用户需求、技术可行性和商业目标...',
        likes: 29,
        liked: false
      }
    ]
  } catch (err) {
    error.value = '加载内容失败,请稍后重试'
  } finally {
    isLoading.value = false
  }
}

const handleSearch = () => {
  // 搜索防抖可以在这里实现
}

const selectItem = (item) => {
  selectedItem.value = item
}

const closeModal = () => {
  selectedItem.value = null
}

const likeItem = (item) => {
  item.liked = !item.liked
  item.likes += item.liked ? 1 : -1
}

const shareItem = (item) => {
  if (navigator.share) {
    navigator.share({
      title: item.title,
      text: item.description,
      url: window.location.href
    })
  } else {
    // 降级处理
    navigator.clipboard.writeText(window.location.href)
    alert('链接已复制到剪贴板')
  }
}

const formatDate = (date) => {
  return date.toLocaleDateString('zh-CN')
}

const retry = () => {
  loadItems()
}

onMounted(() => {
  loadItems()
})
</script>

<style scoped>
.content-display {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.filters {
  display: flex;
  gap: 1rem;
  margin-bottom: 1rem;
}

.filters button {
  padding: 0.5rem 1rem;
  border: 1px solid #ddd;
  background-color: white;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.filters button.active,
.filters button:hover {
  background-color: #007bff;
  color: white;
  border-color: #007bff;
}

.search-bar {
  margin-bottom: 2rem;
}

.search-bar input {
  width: 100%;
  max-width: 400px;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.loading, .error-state, .empty-state {
  text-align: center;
  padding: 3rem;
  color: #6c757d;
}

.error-state button {
  margin-top: 1rem;
  padding: 0.5rem 1rem;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.content-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1.5rem;
}

.content-item {
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
  cursor: pointer;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.content-item:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.item-image {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.item-content {
  padding: 1rem;
}

.item-content h3 {
  margin: 0 0 0.5rem 0;
  color: #333;
}

.item-content p {
  color: #666;
  margin: 0 0 1rem 0;
}

.item-meta {
  display: flex;
  justify-content: space-between;
  font-size: 0.875rem;
  color: #999;
}

.category {
  background-color: #e9ecef;
  padding: 0.25rem 0.5rem;
  border-radius: 12px;
}

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 1rem;
}

.modal {
  background-color: white;
  border-radius: 8px;
  max-width: 600px;
  max-height: 90vh;
  overflow-y: auto;
  position: relative;
}

.close-btn {
  position: absolute;
  top: 1rem;
  right: 1rem;
  background: none;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  color: #999;
}

.modal-image {
  width: 100%;
  height: 300px;
  object-fit: cover;
}

.modal h2 {
  padding: 1rem;
  margin: 0;
}

.modal-description {
  padding: 0 1rem;
  color: #666;
}

.modal-content {
  padding: 1rem;
  border-top: 1px solid #eee;
}

.modal-actions {
  padding: 1rem;
  display: flex;
  gap: 1rem;
  border-top: 1px solid #eee;
}

.btn-like, .btn-share {
  padding: 0.5rem 1rem;
  border: 1px solid #ddd;
  background-color: white;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.btn-like:hover, .btn-share:hover {
  background-color: #f8f9fa;
}
</style>

性能优化建议

1. 避免不必要的重新渲染

vue
<!-- 不好 - 每次都会重新计算 -->
<div v-if="items.filter(item => item.active).length > 0">
  有活跃项目
</div>

<!-- 好 - 使用计算属性 -->
<div v-if="hasActiveItems">
  有活跃项目
</div>

<script setup>
const hasActiveItems = computed(() => 
  items.value.some(item => item.active)
)
</script>

2. 合理使用 v-show 和 v-if

vue
<!-- 频繁切换使用 v-show -->
<div v-show="isVisible" class="frequently-toggled">
  频繁切换的内容
</div>

<!-- 条件很少改变使用 v-if -->
<div v-if="user.isAdmin" class="admin-panel">
  管理员面板
</div>

3. 使用 key 优化列表渲染

vue
<template v-for="item in items" :key="item.id">
  <div v-if="item.visible">
    {{ item.name }}
  </div>
</template>

下一步

vue study guide - 专业的 Vue.js 学习平台