Vue 模板语法完全指南

内容分享4小时前发布
0 0 0

Vue 模板语法完全指南

一、什么是模板语法?

比喻理解

想象你在写一封信:

  • 普通HTML:信的固定内容
  • Vue模板语法:可以插入变量、条件判断、循环等动态内容
<!-- 普通HTML -->
<p>你好,张三!</p>

<!-- Vue模板 -->
<p>你好,{{ name }}!</p>
<!-- 可以动态显示:你好,李四! -->

二、插值语法(显示数据)

1. 文本插值 – 基本用法

<!-- 双大括号语法(Mustache语法) -->
<template>
  <div>
    <p>消息:{{ message }}</p>
    <p>价格:¥{{ price }}</p>
    <p>总数:{{ count + 1 }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello Vue!',
      price: 100,
      count: 5
    }
  }
}
</script>

注意:双大括号内是JavaScript表达式

<!-- 可以执行简单的表达式 -->
<p>计算:{{ 5 + 3 }}</p>           <!-- 显示:8 -->
<p>三元运算:{{ isOk ? '是' : '否' }}</p>
<p>反转:{{ message.split('').reverse().join('') }}</p>

2. 一次性插值 – v-once

<!-- 只渲染一次,之后不再更新 -->
<p v-once>这个值不会变:{{ initialValue }}</p>

<!-- 实际应用场景 -->
<div v-once>
  <h1>{{ title }}</h1>  <!-- 只在初始化时渲染 -->
  <p>创建时间:{{ createTime }}</p>
</div>

三、原始HTML – v-html

对比文本插值

<template>
  <div>
    <!-- 文本插值:显示原始文本 -->
    <p>使用文本插值:{{ rawHtml }}</p>
    <!-- 显示为:<span>红色文字</span> -->
    
    <!-- v-html:渲染为HTML -->
    <p>使用v-html:<span v-html="rawHtml"></span></p>
    <!-- 显示为:红色文字(实际是红色) -->
  </div>
</template>

<script>
export default {
  data() {
    return {
      rawHtml: '<span>红色文字</span>'
    }
  }
}
</script>

⚠️ 安全警告

<!-- ❌ 危险!永远不要这样做! -->
<div v-html="userInput"></div>

<!-- 缘由:用户可能输入恶意脚本 -->
<!-- 用户输入可能是:<script>恶意代码</script> -->

最佳实践

data() {
  return {
    // 只信任服务器返回的、已消毒的内容
    safeHtml: '<strong>安全内容</strong>',
    
    // 从外部获取的内容要处理
    externalContent: this.sanitize(apiResponse.html)
  }
},
methods: {
  sanitize(html) {
    // 使用第三方库如DOMPurify清理HTML
    return DOMPurify.sanitize(html)
  }
}

四、属性绑定 – v-bind

1. 基本用法

<!-- 绑定普通属性 -->
<img v-bind:src="imageSrc">
<a v-bind:href="url">链接</a>

<!-- 简写(最常用) -->
<img :src="imageSrc">
<a :href="url">链接</a>

2. 绑定布尔属性

<!-- disabled属性,当isDisabled为true时禁用 -->
<button :disabled="isDisabled">按钮</button>

<!-- 多个复选框 -->
<input type="checkbox" :checked="isChecked">
<input type="radio" :checked="selected === 'A'">

3. 动态绑定属性名

<!-- 根据数据决定绑定哪个属性 -->
<template>
  <div>
    <!-- 绑定动态属性名 -->
    <div :[attributeName]="value">内容</div>
    
    <!-- 实际应用场景 -->
    <button :[btnType]="btnValue">动态按钮</button>
    
    <!-- 表单动态name -->
    <input :name="inputName" :placeholder="placeholder">
  </div>
</template>

<script>
export default {
  data() {
    return {
      attributeName: 'class',  // 可以是 'id', 'data-xxx' 等
      value: 'my-class',
      btnType: 'type',  // 可以是 'disabled', 'title' 等
      btnValue: 'submit',
      inputName: 'username',
      placeholder: '请输入用户名'
    }
  }
}
</script>

4. 绑定对象

<!-- 绑定多个属性 -->
<template>
  <div>
    <!-- 传统写法 -->
    <input 
      :id="inputId"
      :name="inputName"
      :placeholder="placeholder"
      :disabled="isDisabled">
    
    <!-- 使用对象语法(更简洁) -->
    <input v-bind="inputAttrs">
    
    <!-- 动态生成属性对象 -->
    <img v-bind="imgProps">
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 对象语法
      inputAttrs: {
        id: 'username',
        name: 'username',
        placeholder: '请输入',
        disabled: false
      },
      imgProps: {
        src: 'image.jpg',
        alt: '描述',
        title: '图片标题',
        loading: 'lazy'
      }
    }
  }
}
</script>

五、条件渲染

1. v-if / v-else-if / v-else

<template>
  <div>
    <!-- 基本条件判断 -->
    <p v-if="score >= 90">优秀!</p>
    <p v-else-if="score >= 60">及格 </p>
    <p v-else>不及格 </p>
    
    <!-- 用户角色判断 -->
    <div v-if="user.role === 'admin'">
      <button>删除用户</button>
      <button>修改权限</button>
    </div>
    <div v-else-if="user.role === 'editor'">
      <button>编辑文章</button>
    </div>
    <div v-else>
      <button>查看内容</button>
    </div>
    
    <!-- 配合template使用 -->
    <template v-if="isLoading">
      <p>加载中...</p>
      <div class="spinner"></div>
    </template>
  </div>
</template>

2. v-show

<template>
  <div>
    <!-- v-show 只是切换 display: none -->
    <div v-show="isVisible">可见内容</div>
    
    <!-- 实际效果等同于 -->
    <div style="display: none">隐藏内容</div>
  </div>
</template>

3. v-if vs v-show

特性

v-if

v-show

首次渲染

惰性(条件为真才渲染)

始终渲染

切换开销

高(销毁/重建DOM)

低(只是CSS切换)

适用场景

不常切换的场景

频繁切换的场景

生命周期

切换时会触发生命周期

只是显示/隐藏

<!-- 何时用哪个? -->
<template>
  <div>
    <!-- 用 v-if:不常切换 -->
    <div v-if="user.isAdmin">
      管理员面板(复杂组件,不常切换)
    </div>
    
    <!-- 用 v-show:频繁切换 -->
    <div v-show="showToolbar">
      工具栏(需要快速显示/隐藏)
    </div>
    
    <!-- 用 v-if:初始不需要 -->
    <div v-if="dataLoaded">
      数据表格(初始不加载)
    </div>
  </div>
</template>

六、列表渲染 – v-for

1. 遍历数组

<template>
  <div>
    <!-- 基本用法 -->
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.name }}
      </li>
    </ul>
    
    <!-- 带索引 -->
    <ul>
      <li v-for="(item, index) in items" :key="item.id">
        {{ index + 1 }}. {{ item.name }}
      </li>
    </ul>
    
    <!-- 实际应用:商品列表 -->
    <div class="products">
      <div v-for="product in products" :key="product.id" class="product-card">
        <img :src="product.image" :alt="product.name">
        <h3>{{ product.name }}</h3>
        <p>价格:¥{{ product.price }}</p>
        <button @click="addToCart(product)">加入购物车</button>
      </div>
    </div>
  </div>
</template>

2. 遍历对象

<template>
  <div>
    <!-- 遍历对象 -->
    <ul>
      <li v-for="(value, key, index) in userInfo" :key="key">
        {{ index + 1 }}. {{ key }}: {{ value }}
      </li>
    </ul>
    
    <!-- 实际应用:显示用户信息 -->
    <dl>
      <div v-for="(value, key) in user" :key="key">
        <dt>{{ formatKey(key) }}:</dt>
        <dd>{{ value }}</dd>
      </div>
    </dl>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userInfo: {
        name: '张三',
        age: 25,
        city: '北京',
        email: 'zhangsan@example.com'
      }
    }
  },
  methods: {
    formatKey(key) {
      const map = {
        name: '姓名',
        age: '年龄',
        city: '城市',
        email: '邮箱'
      }
      return map[key] || key
    }
  }
}
</script>

3. 遍历数字范围

<template>
  <div>
    <!-- 生成1-10 -->
    <span v-for="n in 10" :key="n">{{ n }} </span>
    <!-- 显示:1 2 3 4 5 6 7 8 9 10 -->
    
    <!-- 生成星星评分 -->
    <div class="rating">
      <span v-for="star in 5" :key="star">
        {{ star <= rating ? '★' : '☆' }}
      </span>
    </div>
  </div>
</template>

4. 配合 v-if

<template>
  <div>
    <!-- ❌ 错误:不要在同一元素上用v-for和v-if -->
    <li v-for="user in users" v-if="user.isActive" :key="user.id">
      {{ user.name }}
    </li>
    
    <!-- ✅ 正确:使用template或计算属性 -->
    
    <!-- 方法1:使用template -->
    <template v-for="user in users" :key="user.id">
      <li v-if="user.isActive">
        {{ user.name }}
      </li>
    </template>
    
    <!-- 方法2:使用计算属性 -->
    <li v-for="user in activeUsers" :key="user.id">
      {{ user.name }}
    </li>
  </div>
</template>

<script>
export default {
  computed: {
    activeUsers() {
      return this.users.filter(user => user.isActive)
    }
  }
}
</script>

5. 为什么需要 :key

<template>
  <div>
    <!-- 没有key(不推荐) -->
    <li v-for="item in items">{{ item }}</li>
    
    <!-- 有key(推荐) -->
    <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    
    <!-- key的用途 -->
    <ul>
      <!-- Vue通过key识别节点,高效更新DOM -->
      <!-- 当数组顺序变化时,Vue能重用现有元素 -->
      <!-- 使用唯一标识,不要用index! -->
      
      <!-- ❌ 不好:index会变 -->
      <li v-for="(item, index) in items" :key="index">{{ item.name }}</li>
      
      <!-- ✅ 好:用唯一id -->
      <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

七、事件处理 – v-on

1. 基本用法

<template>
  <div>
    <!-- 内联语句 -->
    <button v-on:click="counter++">增加 {{ counter }}</button>
    
    <!-- 方法名 -->
    <button v-on:click="sayHello">打招呼</button>
    
    <!-- 简写(最常用) -->
    <button @click="sayHello">打招呼</button>
    
    <!-- 调用方法并传参 -->
    <button @click="say('你好', $event)">带参数</button>
    
    <!-- 实际应用 -->
    <form @submit="handleSubmit">
      <input type="text" v-model="message">
      <button type="submit">提交</button>
      <button type="button" @click="resetForm">重置</button>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      counter: 0,
      message: ''
    }
  },
  methods: {
    sayHello() {
      alert('Hello!')
    },
    say(message, event) {
      console.log(message)
      console.log('事件对象:', event)
    },
    handleSubmit(event) {
      event.preventDefault() // 阻止默认提交行为
      console.log('提交的内容:', this.message)
    },
    resetForm() {
      this.message = ''
    }
  }
}
</script>

2. 事件修饰符

<template>
  <div>
    <!-- 阻止默认行为 -->
    <a href="/about" @click.prevent="goToAbout">关于我们</a>
    
    <!-- 阻止事件冒泡 -->
    <div @click="parentClick">
      <button @click.stop="childClick">点击我</button>
    </div>
    
    <!-- 只触发一次 -->
    <button @click.once="init">只初始化一次</button>
    
    <!-- 串联修饰符 -->
    <form @submit.prevent.once="submitForm">
      <!-- 只提交一次 -->
    </form>
    
    <!-- 实际应用:右键菜单 -->
    <div @contextmenu.prevent="showContextMenu">
      右键点击
    </div>
    
    <!-- 按键修饰符 -->
    <input 
      @keyup.enter="submit"
      @keyup.esc="cancel"
      @keyup.ctrl.enter="specialSubmit">
  </div>
</template>

3. 常用事件修饰符

修饰符

作用

示例

.stop

阻止事件冒泡

@click.stop

.prevent

阻止默认行为

@submit.prevent

.capture

使用捕获模式

@click.capture

.self

只当事件是自身触发

@click.self

.once

只触发一次

@click.once

.passive

不阻止默认行为

@scroll.passive

4. 按键修饰符

<template>
  <div>
    <!-- 常用按键 -->
    <input @keyup.enter="submit">
    <input @keyup.tab="nextField">
    <input @keyup.delete="deleteItem">
    <input @keyup.esc="cancel">
    <input @keyup.space="toggle">
    
    <!-- 组合键 -->
    <input @keyup.ctrl.enter="quickSubmit">
    <input @keyup.alt.delete="forceDelete">
    
    <!-- 方向键 -->
    <div @keyup.left="moveLeft" @keyup.right="moveRight">
      用方向键移动
    </div>
    
    <!-- 自定义按键 -->
    <input @keyup.f1="showHelp">
  </div>
</template>

八、双向绑定 – v-model

1. 基本表单元素

<template>
  <div>
    <!-- 文本输入 -->
    <input v-model="text" placeholder="请输入">
    <p>输入的内容:{{ text }}</p>
    
    <!-- 多行文本 -->
    <textarea v-model="message" placeholder="多行文本"></textarea>
    
    <!-- 复选框(单个) -->
    <input type="checkbox" v-model="checked">
    <label>是否同意</label>
    
    <!-- 复选框(多个) -->
    <div>
      <input type="checkbox" value="vue" v-model="skills">
      <label>Vue</label>
      <input type="checkbox" value="react" v-model="skills">
      <label>React</label>
      <input type="checkbox" value="angular" v-model="skills">
      <label>Angular</label>
    </div>
    <p>选择的技能:{{ skills }}</p>
    
    <!-- 单选按钮 -->
    <div>
      <input type="radio" value="男" v-model="gender">
      <label></label>
      <input type="radio" value="女" v-model="gender">
      <label></label>
    </div>
    
    <!-- 下拉选择 -->
    <select v-model="selected">
      <option value="">请选择</option>
      <option value="A">选项A</option>
      <option value="B">选项B</option>
    </select>
  </div>
</template>

<script>
export default {
  data() {
    return {
      text: '',
      message: '',
      checked: false,
      skills: [],  // 多选框用数组
      gender: '男',
      selected: ''
    }
  }
}
</script>

2. 修饰符

<template>
  <div>
    <!-- .lazy - 输入完成才更新 -->
    <input v-model.lazy="text">
    <!-- 输入时不会立即更新,失去焦点时才更新 -->
    
    <!-- .number - 转换为数字 -->
    <input v-model.number="age" type="number">
    <!-- 字符串转为数字 -->
    
    <!-- .trim - 去除首尾空格 -->
    <input v-model.trim="username">
    <!-- 自动去除空格 -->
    
    <!-- 实际应用:搜索框 -->
    <input 
      v-model.trim.lazy="search"
      @keyup.enter="doSearch"
      placeholder="输入关键字搜索">
    
    <!-- 实际应用:表单验证 -->
    <input 
      v-model.number="price" 
      type="number"
      min="0"
      step="0.01">
  </div>
</template>

3. 自定义组件使用 v-model

<!-- 父组件 -->
<template>
  <div>
    <CustomInput v-model="message" />
    <p>父组件中的值:{{ message }}</p>
  </div>
</template>

<script>
import CustomInput from './CustomInput.vue'

export default {
  components: { CustomInput },
  data() {
    return {
      message: ''
    }
  }
}
</script>

<!-- 子组件 CustomInput.vue -->
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)">
</template>

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

九、样式绑定

1. Class绑定

<template>
  <div>
    <!-- 对象语法(最常用) -->
    <div :class="{ active: isActive, 'text-danger': hasError }"></div>
    
    <!-- 数组语法 -->
    <div :class="[activeClass, errorClass]"></div>
    
    <!-- 混合使用 -->
    <div :class="[{ active: isActive }, 'static-class', dynamicClass]"></div>
    
    <!-- 实际应用:导航菜单 -->
    <nav>
      <a 
        v-for="item in menu" 
        :key="item.id"
        :class="{ active: currentMenu === item.id }"
        @click="currentMenu = item.id">
        {{ item.name }}
      </a>
    </nav>
    
    <!-- 实际应用:表单验证样式 -->
    <input 
      :class="{
        'form-control': true,
        'is-valid': isValid,
        'is-invalid': hasError
      }"
      v-model="username">
  </div>
</template>

<script>
export default {
  data() {
    return {
      isActive: true,
      hasError: false,
      activeClass: 'active',
      errorClass: 'text-danger',
      currentMenu: 'home',
      menu: [
        { id: 'home', name: '首页' },
        { id: 'about', name: '关于' }
      ],
      isValid: false
    }
  }
}
</script>

2. Style绑定

<template>
  <div>
    <!-- 对象语法 -->
    <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
    
    <!-- 数组语法(多个对象) -->
    <div :style="[baseStyles, overridingStyles]"></div>
    
    <!-- 自动添加前缀 -->
    <div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
    
    <!-- 实际应用:动态进度条 -->
    <div class="progress-bar">
      <div 
        class="progress" 
        :style="{ width: progress + '%', backgroundColor: color }">
      </div>
    </div>
    
    <!-- 实际应用:元素位置 -->
    <div 
      class="draggable" 
      :style="{ 
        left: x + 'px', 
        top: y + 'px',
        transform: `rotate(${rotation}deg)`
      }">
      可拖拽元素
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      activeColor: 'red',
      fontSize: 16,
      baseStyles: {
        color: 'red',
        fontSize: '14px'
      },
      overridingStyles: {
        fontWeight: 'bold'
      },
      progress: 50,
      color: '#42b983',
      x: 100,
      y: 200,
      rotation: 45
    }
  }
}
</script>

十、特殊指令

1. v-cloak – 解决闪现问题

<!-- 在Vue编译完成前,隐藏未编译的模板 -->
<style>
  [v-cloak] {
    display: none;
  }
</style>

<div v-cloak>
  <!-- 页面加载时,这个区域会隐藏 -->
  <!-- Vue编译完成后,会自动移除v-cloak属性 -->
  {{ message }}
</div>

2. v-pre – 跳过编译

<!-- 显示原始双大括号,不进行编译 -->
<div v-pre>
  <!-- 这里的内容不会被Vue编译 -->
  {{ 这里的内容会原样显示 }}
  <!-- 显示为:{{ 这里的内容会原样显示 }} -->
</div>

<!-- 用途:显示代码示例 -->
<pre v-pre>
  {{ message }}  <!-- 显示为:{{ message }} -->
  <div v-if="show">内容</div>
</pre>

3. v-once – 一次性渲染

<!-- 只渲染一次,之后不会更新 -->
<div v-once>
  <h1>{{ title }}</h1>  <!-- 只渲染一次 -->
  <p>{{ content }}</p>   <!-- 只渲染一次 -->
</div>

<!-- 实际应用:静态内容 -->
<header v-once>
  <h1>{{ siteTitle }}</h1>
  <nav>导航菜单</nav>
</header>

十一、实战综合示例

商品列表页面

<template>
  <div class="product-page">
    <!-- 搜索区域 -->
    <div class="search-box">
      <input 
        v-model.trim.lazy="searchKeyword"
        @keyup.enter="search"
        placeholder="搜索商品...">
      <button @click="search">搜索</button>
      <button @click="resetFilter">重置</button>
    </div>
    
    <!-- 分类筛选 -->
    <div class="filters">
      <span 
        v-for="category in categories" 
        :key="category.id"
        :class="['category-tag', { active: selectedCategory === category.id }]"
        @click="selectCategory(category.id)">
        {{ category.name }}
      </span>
    </div>
    
    <!-- 排序选项 -->
    <div class="sort-options">
      <label>
        <input 
          type="radio" 
          value="price_asc" 
          v-model="sortBy">
        价格从低到高
      </label>
      <label>
        <input 
          type="radio" 
          value="price_desc" 
          v-model="sortBy">
        价格从高到低
      </label>
    </div>
    
    <!-- 商品列表 -->
    <div class="product-list">
      <div 
        v-for="product in filteredProducts" 
        :key="product.id"
        class="product-card"
        :class="{ 'out-of-stock': product.stock === 0 }">
        <!-- 商品图片 -->
        <img 
          :src="product.image" 
          :alt="product.name"
          @error="handleImageError"
          @click="viewDetail(product)">
        
        <!-- 商品信息 -->
        <div class="product-info">
          <h3>{{ product.name }}</h3>
          <p class="price">¥{{ product.price.toFixed(2) }}</p>
          
          <!-- 库存状态 -->
          <p v-if="product.stock === 0" class="stock out">缺货</p>
          <p v-else-if="product.stock < 10" class="stock low">仅剩{{ product.stock }}件</p>
          <p v-else class="stock in">有货</p>
          
          <!-- 评分 -->
          <div class="rating">
            <span 
              v-for="star in 5" 
              :key="star"
              :class="['star', { active: star <= product.rating }]"></span>
            <span class="rating-text">({{ product.reviews }})</span>
          </div>
        </div>
        
        <!-- 操作按钮 -->
        <div class="actions">
          <button 
            :disabled="product.stock === 0"
            @click="addToCart(product)"
            :class="{ disabled: product.stock === 0 }">
            {{ product.stock === 0 ? '已售罄' : '加入购物车' }}
          </button>
          <button 
            @click="toggleFavorite(product)"
            :class="{ favorite: product.isFavorite }">
            {{ product.isFavorite ? '❤️ 已收藏' : ' 收藏' }}
          </button>
        </div>
      </div>
    </div>
    
    <!-- 空状态 -->
    <div v-if="filteredProducts.length === 0" class="empty-state">
      <p>没有找到相关商品</p>
      <button @click="resetFilter">查看所有商品</button>
    </div>
    
    <!-- 加载更多 -->
    <div v-show="hasMore" class="load-more">
      <button 
        @click="loadMore"
        :disabled="loading">
        {{ loading ? '加载中...' : '加载更多' }}
      </button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 搜索关键词
      searchKeyword: '',
      
      // 选中的分类
      selectedCategory: null,
      
      // 排序方式
      sortBy: 'price_asc',
      
      // 商品数据
      products: [],
      categories: [],
      
      // 分页
      currentPage: 1,
      pageSize: 10,
      hasMore: true,
      loading: false
    }
  },
  
  computed: {
    // 过滤和排序后的商品
    filteredProducts() {
      let filtered = this.products
      
      // 按关键词过滤
      if (this.searchKeyword) {
        const keyword = this.searchKeyword.toLowerCase()
        filtered = filtered.filter(product => 
          product.name.toLowerCase().includes(keyword) ||
          product.description.toLowerCase().includes(keyword)
        )
      }
      
      // 按分类过滤
      if (this.selectedCategory) {
        filtered = filtered.filter(
          product => product.categoryId === this.selectedCategory
        )
      }
      
      // 排序
      filtered.sort((a, b) => {
        if (this.sortBy === 'price_asc') {
          return a.price - b.price
        } else if (this.sortBy === 'price_desc') {
          return b.price - a.price
        }
        return 0
      })
      
      return filtered
    }
  },
  
  methods: {
    // 搜索
    search() {
      this.currentPage = 1
      this.loadProducts()
    },
    
    // 选择分类
    selectCategory(categoryId) {
      this.selectedCategory = 
        this.selectedCategory === categoryId ? null : categoryId
      this.currentPage = 1
      this.loadProducts()
    },
    
    // 重置筛选
    resetFilter() {
      this.searchKeyword = ''
      this.selectedCategory = null
      this.sortBy = 'price_asc'
      this.currentPage = 1
      this.loadProducts()
    },
    
    // 添加购物车
    addToCart(product) {
      if (product.stock === 0) return
      
      this.$emit('add-to-cart', {
        id: product.id,
        name: product.name,
        price: product.price,
        quantity: 1
      })
      
      // 添加动画效果
      const button = event.target
      button.classList.add('added')
      setTimeout(() => {
        button.classList.remove('added')
      }, 1000)
    },
    
    // 切换收藏
    toggleFavorite(product) {
      product.isFavorite = !product.isFavorite
      
      // 保存到本地存储
      this.saveFavorites()
    },
    
    // 图片加载失败处理
    handleImageError(event) {
      event.target.src = '/images/default-product.jpg'
    },
    
    // 查看详情
    viewDetail(product) {
      this.$router.push(`/product/${product.id}`)
    },
    
    // 加载更多
    loadMore() {
      this.currentPage++
      this.loadProducts()
    },
    
    // 加载商品
    async loadProducts() {
      this.loading = true
      try {
        const response = await this.$api.getProducts({
          page: this.currentPage,
          size: this.pageSize,
          keyword: this.searchKeyword,
          category: this.selectedCategory,
          sort: this.sortBy
        })
        
        if (this.currentPage === 1) {
          this.products = response.data
        } else {
          this.products.push(...response.data)
        }
        
        this.hasMore = response.hasMore
      } catch (error) {
        console.error('加载商品失败:', error)
      } finally {
        this.loading = false
      }
    }
  },
  
  mounted() {
    this.loadProducts()
    this.loadCategories()
  }
}
</script>

<style scoped>
.product-page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.search-box {
  margin-bottom: 20px;
}

.filters {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.category-tag {
  padding: 5px 15px;
  border: 1px solid #ddd;
  border-radius: 20px;
  cursor: pointer;
  transition: all 0.3s;
}

.category-tag.active {
  background-color: #42b983;
  color: white;
  border-color: #42b983;
}

.product-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
  margin: 20px 0;
}

.product-card {
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 15px;
  transition: transform 0.3s, box-shadow 0.3s;
}

.product-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}

.product-card.out-of-stock {
  opacity: 0.6;
}

.stock.out {
  color: #f56c6c;
}

.stock.low {
  color: #e6a23c;
}

.stock.in {
  color: #67c23a;
}

.rating {
  margin: 10px 0;
}

.star {
  color: #ddd;
  font-size: 18px;
}

.star.active {
  color: #f7ba2a;
}

.actions {
  display: flex;
  gap: 10px;
  margin-top: 15px;
}

button {
  flex: 1;
  padding: 8px 12px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}

button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

button.favorite {
  background-color: #ffe6e6;
  color: #f56c6c;
}

.empty-state {
  text-align: center;
  padding: 50px;
  color: #909399;
}

.load-more {
  text-align: center;
  margin-top: 30px;
}
</style>

十二、最佳实践和技巧

1. 保持模板简洁

<!-- ❌ 不好:模板中逻辑太多 -->
<div>
  {{ user.firstName + ' ' + user.lastName + ' (' + user.age + '岁)' }}
</div>

<!-- ✅ 好:使用计算属性 -->
<template>
  <div>
    {{ fullNameWithAge }}
  </div>
</template>

<script>
export default {
  computed: {
    fullNameWithAge() {
      return `${this.user.firstName} ${this.user.lastName} (${this.user.age}岁)`
    }
  }
}
</script>

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

<!-- 初始不需要的,用v-if -->
<div v-if="isAdmin">
  <!-- 管理员才有的复杂组件 -->
  <AdminPanel />
</div>

<!-- 频繁切换的,用v-show -->
<div v-show="showToolbar">
  <!-- 常常显示/隐藏的工具栏 -->
  <Toolbar />
</div>

<!-- 多个元素用template包裹 -->
<template v-if="isLoading">
  <div class="spinner"></div>
  <p>加载中...</p>
</template>

3. 列表渲染优化

<!-- 使用唯一的key -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>

<!-- 大数据量使用虚拟滚动 -->
<virtual-list :size="50" :remain="10">
  <div v-for="item in largeList" :key="item.id">
    {{ item.content }}
  </div>
</virtual-list>

4. 事件处理优化

<!-- 防抖处理 -->
<input @input="onSearch" placeholder="搜索...">

<script>
export default {
  methods: {
    onSearch: _.debounce(function(event) {
      this.search(event.target.value)
    }, 500)
  }
}
</script>

5. 表单处理技巧

<!-- 表单验证 -->
<input 
  v-model="email"
  :class="{ 'is-invalid': !isEmailValid }"
  @blur="validateEmail">

<!-- 文件上传 -->
<input 
  type="file"
  @change="handleFileUpload"
  multiple
  accept="image/*">

总结表格

语法

用途

示例

{{ }}

文本插值

{{ message }}

v-html

原始HTML

<span v-html=”rawHtml”></span>

v-bind/ :

属性绑定

:src=”imageUrl”

v-if

条件渲染

v-if=”isShow”

v-show

显示/隐藏

v-show=”isVisible”

v-for

列表渲染

v-for=”item in items”

v-on/ @

事件监听

@click=”handleClick”

v-model

双向绑定

v-model=”input”

:class

类绑定

:class=”{ active: isActive }”

:style

样式绑定

:style=”{ color: textColor }”

v-cloak

隐藏未编译模板

[v-cloak] { display: none }

v-pre

跳过编译

<div v-pre>{{ 不会编译 }}</div>

v-once

一次性渲染

<div v-once>{{ 只渲染一次 }}</div>

记住:Vue模板语法就像是给HTML赋予了”超能力”,让你能轻松实现数据的动态展示和交互!

© 版权声明

相关文章

暂无评论

none
暂无评论...