Skip to content

03 计算属性和侦听器

计算属性

  • 基于 现有的数据,计算出来的 新属性
  • 依赖 的数据变化,自动 重新计算。

语法

  1. 声明在 computed 配置项中,一个计算属性对应一个函数
  2. 使用起来和普通属性一样使用

注意

  1. computed 配置项和 data 配置项是 同级
  2. computed 中的计算属性 虽然是函数的写法,但他 依然是个属性
  3. computed 中的计算属性 不能data 中的属性 同名
  4. 使用 computed 中的计算属性和使用 data 中的属性是一样的用法
  5. computed 中计算属性内部的 this 依然 指向的是 Vue 实例

案例 小黑的礼物清单

4b9444a4-0f0b-4d9c-b9a4-bb1090129731

html
<div id="app">
  <h3>小黑的礼物清单</h3>
  <table>
    <tr>
      <th>名字</th>
      <th>数量</th>
    </tr>
    <tr v-for="(item, index) in list" :key="item.id">
      <td>{{ item.name }}</td>
      <td>{{ item.num }}个</td>
    </tr>
  </table>

  <!-- 目标:统计求和,求得礼物总数 -->
  <p>礼物总数:? 个</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      // 现有的数据
      list: [
        { id: 1, name: "篮球", num: 1 },
        { id: 2, name: "玩具", num: 2 },
        { id: 3, name: "铅笔", num: 5 },
      ],
    },
  });
</script>
html
<div id="app">
  <h3>小黑的礼物清单</h3>
  <table>
    <tr>
      <th>名字</th>
      <th>数量</th>
    </tr>
    <tr v-for="(item, index) in list" :key="item.id">
      <td>{{ item.name }}</td>
      <td>{{ item.num }}个</td>
    </tr>
  </table>

  <!-- 目标:统计求和,求得礼物总数 -->
  <p>礼物总数:{{totalCount}} 个</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      // 现有的数据
      list: [
        { id: 1, name: "篮球", num: 1 },
        { id: 2, name: "玩具", num: 2 },
        { id: 3, name: "铅笔", num: 6 },
      ],
    },
    computed: {
      totalCount() {
        let sum = this.list.reduce((sum, item) => sum + item.num, 0)
        return sum
      }
    }
  });
</script>

computed VS methods

  • computed 计算属性 VS methods 方法

computed 计算属性

  • 作用:封装了一段对于数据的处理,求得一个结果

  • 语法:

    1. 写在 computed 配置项中
    2. 作为属性,直接使用
      • js 中使用计算属性:this.计算属性
      • 模板中使用计算属性:
computed 计算属性
html
<div id="app">
  <h3>小黑的礼物清单🛒<span>{{ totalCount }}</span></h3>
  <table>
    <tr>
      <th>名字</th>
      <th>数量</th>
    </tr>
    <tr v-for="(item, index) in list" :key="item.id">
      <td>{{ item.name }}</td>
      <td>{{ item.num }} 个</td>
    </tr>
  </table>
  <p>礼物总数:{{ totalCount }} 个</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      list: [
        { id: 1, name: "篮球", num: 3 },
        { id: 2, name: "玩具", num: 2 },
        { id: 3, name: "铅笔", num: 5 },
      ],
    },
    computed: {
      // 计算属性:有缓存的,一旦计算出来结果,就会立刻缓存
      // 下一次读取 → 直接读缓存就行 → 性能特别高
      totalCount() {
        console.log('计算属性执行了')
        let total = this.list.reduce((sum, item) => sum + item.num, 0)
        return total
      }
    }
  });
</script>

methods 方法

  • 作用:给 Vue 实例提供一个方法,调用以处理业务逻辑

  • 语法:

    1. 写在 methods 配置项中

    2. 作为方法调用

      • js 中调用:this.方法名()
      • 模板中调用 {{方法名 ()}} 或者 @事件名="方法名"
methods 方法
html
<div id="app">
  <h3>小黑的礼物清单🛒<span>{{ totalCount() }}</span></h3>
  <table>
    <tr>
      <th>名字</th>
      <th>数量</th>
    </tr>
    <tr v-for="(item, index) in list" :key="item.id">
      <td>{{ item.name }}</td>
      <td>{{ item.num }} 个</td>
    </tr>
  </table>
  <p>礼物总数:{{ totalCount() }} 个</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      list: [
        { id: 1, name: "篮球", num: 3 },
        { id: 2, name: "玩具", num: 2 },
        { id: 3, name: "铅笔", num: 5 },
      ],
    },
    methods: {
      totalCount() {
        console.log('methods 方法执行了')
        let sum = this.list.reduce((sum, item) => sum + item.num, 0);
        return sum;
      },
    },
  });
</script>

计算属性的优势

  1. 缓存特性(提升性能)

    • 计算属性会对计算出来的结果缓存,再次使用直接读取缓存,
    • 依赖项变化了,会自动重新计算 → 并再次缓存
  2. methods 没有缓存特性

  3. 通过代码比较

计算属性和方法
html
<div id="app">
  <h3>小黑的礼物清单🛒<span>{{ totalCountMethods() }}</span></h3>
  <h3>小黑的礼物清单🛒<span>{{ totalCountMethods() }}</span></h3>
  <h3>小黑的礼物清单🛒<span>{{ totalCountMethods() }}</span></h3>
  <!-- totalCountMethods() 是方法,因为是方法,所以要加括号 -->
  <!-- 方法没有缓存,所以每次执行都会重新计算 -->
  <table>
    <tr>
      <th>名字</th>
      <th>数量</th>
    </tr>
    <tr v-for="(item, index) in list" :key="item.id">
      <td>{{ item.name }}</td>
      <td>{{ item.num }} 个</td>
    </tr>
  </table>
  <p>礼物总数:{{ totalCountComputed }} 个</p>
  <p>礼物总数:{{ totalCountComputed }} 个</p>
  <p>礼物总数:{{ totalCountComputed }} 个</p>
  <!-- totalCountComputed 是计算属性,因为是计算属性,所以不需要加括号 -->
  <!-- 计算属性有缓存,所以只会执行一次 -->
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      list: [
        { id: 1, name: "篮球", num: 3 },
        { id: 2, name: "玩具", num: 2 },
        { id: 3, name: "铅笔", num: 5 },
      ],
    },
    methods: {
      totalCountMethods() {
        console.log('methods 方法执行了')
        let sum = this.list.reduce((sum, item) => sum + item.num, 0);
        return sum;
      },
    },
    computed: {
      totalCountComputed() {
        console.log('computed 计算属性执行了')
        let sum = this.list.reduce((sum, item) => sum + item.num, 0);
        return sum;
      },
    },
  });
</script>

总结

  1. computed 有缓存特性methods 没有缓存
  2. 当一个结果依赖其他多个值时,推荐使用计算属性
  3. 当处理业务逻辑时,推荐使用 methods 方法,比如事件的处理函数

计算属性的完整写法

既然计算属性也是属性,能访问,应该也能修改了?

  1. 计算属性默认的简写,只能读取访问,不能 "修改"
  2. 如果要 "修改" → 需要写计算属性的完整写法
js
computed: {
  计算属性名 () {
    一段代码逻辑 ()
    return 结果
  }
}
js
computed: {
  计算属性名: {
    get() {
      一段代码逻辑 (计算逻辑)
      return 结果
    },
    set (修改的值) {
      一段代码逻辑 (修改逻辑)
    }
  }
}

案例 改名卡

改名卡
html
<div id="app">
  姓:<input type="text" /><br />
  名:<input type="text" /><br />
  <p>姓名:</p>
  <button>修改姓名</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {},
    computed: {},
    methods: {},
  });
</script>
html
<div id="app">
  姓:<input type="text" placeholder="请输入姓" v-model="lastName" /><br />
  名:<input type="text" placeholder="请输入名" v-model="firstName" /><br />
  <p>读取姓名:<span>{{fullName}}</span></p>
  <button @click="changeName">设置姓名</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      lastName: "",
      firstName: "",
    },
    computed: {
      // fullName() {
      //   return this.lastName + this.firstName;
      // },
      fullName: {
        get() {
          return this.lastName + this.firstName;
        },
        set(value) {
          this.lastName = value.substr(0, 1);
          this.firstName = value.substr(1);
        },
      },
    },
    methods: {
      changeName() {
        // this.lastName = "冯";
        // this.firstName = "小刚";
        this.fullName = '冯小刚'
      },
    },
  });
</script>

案例 成绩案例

9a1e4351-30b8-47d0-9f40-a0efdcd5c793

功能描述

  1. 渲染功能
  2. 删除功能
  3. 添加功能
  4. 统计总分,求平均分

思路分析

  1. 渲染功能

    • v-for :key
    • v-bind: 动态绑定 class 的样式
    • v-if v-else 判断是否有数据,没有数据显示提示信息
  2. 删除功能

    • v-on 绑定事件,阻止 a 标签的默认行为 @click.prevent
    • 获取当前行的索引,使用 splice 删除对应的项 (索引,1)
    • 或者使用 filter 过滤出不等于当前行索引的项
  3. 添加功能

    • v-model 绑定数据
    • 使用 .trim.number 判断数据是否为空后 再添加、添加后清空文本框的数据
    • 使用 push 修改数组(添加到数组尾部)更新视图
    • 或者使用 unshift 修改数组(添加到数组头部)更新视图
  4. 统计总分,求平均分

    • 使用计算属性 computed 计算总分和平均分
    • reduce 累加求和
    • toFixed 保留两位小数
html
<div id="app" class="score-case">
  <div class="table">
    <table>
      <thead>
        <tr>
          <th>编号</th>
          <th>科目</th>
          <th>成绩</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>1</td>
          <td>语文</td>
          <td class="red">46</td>
          <td><a href="#">删除</a></td>
        </tr>
        <tr>
          <td>2</td>
          <td>英语</td>
          <td>80</td>
          <td><a href="#">删除</a></td>
        </tr>
        <tr>
          <td>3</td>
          <td>数学</td>
          <td>100</td>
          <td><a href="#">删除</a></td>
        </tr>
      </tbody>
      <tbody>
        <tr>
          <td colspan="5">
            <span class="none">暂无数据</span>
          </td>
        </tr>
      </tbody>

      <tfoot>
        <tr>
          <td colspan="5">
            <span>总分:246</span>
            <span style="margin-left: 50px">平均分:79</span>
          </td>
        </tr>
      </tfoot>
    </table>
  </div>
  <div class="form">
    <div class="form-item">
      <div class="label">科目:</div>
      <div class="input">
        <input type="text" placeholder="请输入科目" />
      </div>
    </div>
    <div class="form-item">
      <div class="label">分数:</div>
      <div class="input">
        <input type="text" placeholder="请输入分数" />
      </div>
    </div>
    <div class="form-item">
      <div class="label"></div>
      <div class="input">
        <button class="submit">添加</button>
      </div>
    </div>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<script>
  const app = new Vue({
    el: "#app",
    data: {
      list: [
        { id: 1, subject: "语文", score: 20 },
        { id: 7, subject: "数学", score: 99 },
        { id: 12, subject: "英语", score: 70 },
      ],
      subject: "",
      score: "",
    },
  });
</script>
html
<div id="app" class="score-case">
  <div class="table">
    <table>
      <thead>
        <tr>
          <th>编号</th>
          <th>科目</th>
          <th>成绩</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody v-if="list.length > 0">
        <tr v-for="(item, index) in list" :key="item.id">
          <td>{{index+1}}</td>
          <td>{{item.subject}}</td>
          <td :class="{red: item.score < 60}">{{item.score}}</td>
          <td><a href="#">删除</a></td>
        </tr>
      </tbody>
      <tbody v-else>
        <tr>
          <td colspan="5">
            <span class="none">暂无数据</span>
          </td>
        </tr>
      </tbody>

      <tfoot>
        <tr>
          <td colspan="5">
            <span>总分:246</span>
            <span style="margin-left: 50px">平均分:79</span>
          </td>
        </tr>
      </tfoot>
    </table>
  </div>
  <div class="form">
    <div class="form-item">
      <div class="label">科目:</div>
      <div class="input">
        <input type="text" placeholder="请输入科目" />
      </div>
    </div>
    <div class="form-item">
      <div class="label">分数:</div>
      <div class="input">
        <input type="text" placeholder="请输入分数" />
      </div>
    </div>
    <div class="form-item">
      <div class="label"></div>
      <div class="input">
        <button class="submit">添加</button>
      </div>
    </div>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<script>
  const app = new Vue({
    el: "#app",
    data: {
      list: [
        { id: 1, subject: "语文", score: 20 },
        { id: 7, subject: "数学", score: 99 },
        { id: 12, subject: "英语", score: 70 },
      ],
      subject: "",
      score: "",
    },
  });
</script>
html
<div id="app" class="score-case">
  <div class="table">
    <table>
      <thead>
        <tr>
          <th>编号</th>
          <th>科目</th>
          <th>成绩</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody v-if="list.length > 0">
        <tr v-for="(item, index) in list" :key="item.id">
          <td>{{index+1}}</td>
          <td>{{item.subject}}</td>
          <td :class="{red: item.score < 60}">{{item.score}}</td>
          <td><a href="#" @click.prevent="remove(item.id)">删除</a></td>
        </tr>
      </tbody>
      <tbody v-else>
        <tr>
          <td colspan="5">
            <span class="none">暂无数据</span>
          </td>
        </tr>
      </tbody>

      <tfoot>
        <tr>
          <td colspan="5">
            <span>总分:246</span>
            <span style="margin-left: 50px">平均分:79</span>
          </td>
        </tr>
      </tfoot>
    </table>
  </div>
  <div class="form">
    <div class="form-item">
      <div class="label">科目:</div>
      <div class="input">
        <input type="text" placeholder="请输入科目" />
      </div>
    </div>
    <div class="form-item">
      <div class="label">分数:</div>
      <div class="input">
        <input type="text" placeholder="请输入分数" />
      </div>
    </div>
    <div class="form-item">
      <div class="label"></div>
      <div class="input">
        <button class="submit">添加</button>
      </div>
    </div>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<script>
  const app = new Vue({
    el: "#app",
    data: {
      list: [
        { id: 1, subject: "语文", score: 20 },
        { id: 7, subject: "数学", score: 99 },
        { id: 12, subject: "英语", score: 70 },
      ],
      subject: "",
      score: "",
    },
    methods: {
      remove(id) {
        this.list = this.list.filter((item) => item.id !== id);
      },
    },
  });
</script>
html
<div id="app" class="score-case">
  <div class="table">
    <table>
      <thead>
        <tr>
          <th>编号</th>
          <th>科目</th>
          <th>成绩</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody v-if="list.length > 0">
        <tr v-for="(item, index) in list" :key="item.id">
          <td>{{index+1}}</td>
          <td>{{item.subject}}</td>
          <td :class="{red: item.score < 60}">{{item.score}}</td>
          <td><a href="#" @click.prevent="remove(item.id)">删除</a></td>
        </tr>
      </tbody>
      <tbody v-else>
        <tr>
          <td colspan="5">
            <span class="none">暂无数据</span>
          </td>
        </tr>
      </tbody>

      <tfoot>
        <tr>
          <td colspan="5">
            <span>总分:246</span>
            <span style="margin-left: 50px">平均分:79</span>
          </td>
        </tr>
      </tfoot>
    </table>
  </div>
  <div class="form">
    <div class="form-item">
      <div class="label">科目:</div>
      <div class="input">
        <input type="text" placeholder="请输入科目" v-model.trim="subject" />
      </div>
    </div>
    <div class="form-item">
      <div class="label">分数:</div>
      <div class="input">
        <input type="text" placeholder="请输入分数" v-model.number="score" />
      </div>
    </div>
    <div class="form-item">
      <div class="label"></div>
      <div class="input">
        <button class="submit" @click="add">添加</button>
      </div>
    </div>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<script>
  const app = new Vue({
    el: "#app",
    data: {
      list: [
        { id: 1, subject: "语文", score: 20 },
        { id: 7, subject: "数学", score: 99 },
        { id: 12, subject: "英语", score: 70 },
      ],
      subject: "",
      score: "",
    },
    methods: {
      remove(id) {
        this.list = this.list.filter((item) => item.id !== id);
      },
      add() {
        if (!this.subject || !this.score) return;
        this.list.push({
          id: +new Date(),
          subject: this.subject,
          score: this.score,
        });
        this.subject = "";
        this.score = "";
      },
    },
  });
</script>
html
<div id="app" class="score-case">
  <div class="table">
    <table>
      <thead>
        <tr>
          <th>编号</th>
          <th>科目</th>
          <th>成绩</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody v-if="list.length > 0">
        <tr v-for="(item, index) in list" :key="item.id">
          <td>{{index+1}}</td>
          <td>{{item.subject}}</td>
          <td :class="{red: item.score < 60}">{{item.score}}</td>
          <td><a href="#" @click.prevent="remove(item.id)">删除</a></td>
        </tr>
      </tbody>
      <tbody v-else>
        <tr>
          <td colspan="5">
            <span class="none">暂无数据</span>
          </td>
        </tr>
      </tbody>

      <tfoot>
        <tr>
          <td colspan="5">
            <span>总分:{{totalScore}}</span>
            <span style="margin-left: 50px">平均分:{{avagerScore}}</span>
          </td>
        </tr>
      </tfoot>
    </table>
  </div>
  <div class="form">
    <div class="form-item">
      <div class="label">科目:</div>
      <div class="input">
        <input type="text" placeholder="请输入科目" v-model.trim="subject" />
      </div>
    </div>
    <div class="form-item">
      <div class="label">分数:</div>
      <div class="input">
        <input type="text" placeholder="请输入分数" v-model.number="score" />
      </div>
    </div>
    <div class="form-item">
      <div class="label"></div>
      <div class="input">
        <button class="submit" @click="add">添加</button>
      </div>
    </div>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

<script>
  const app = new Vue({
    el: "#app",
    data: {
      list: [
        { id: 1, subject: "语文", score: 20 },
        { id: 7, subject: "数学", score: 99 },
        { id: 12, subject: "英语", score: 70 },
      ],
      subject: "",
      score: "",
    },
    methods: {
      remove(id) {
        this.list = this.list.filter((item) => item.id !== id);
      },
      add() {
        if (!this.subject || !this.score) return;
        this.list.push({
          id: +new Date(),
          subject: this.subject,
          score: this.score,
        });
        this.subject = "";
        this.score = "";
      },
    },
    computed: {
      totalScore() {
        return this.list.reduce((total, item) => total + item.score, 0);
      },
      avagerScore() {
        if (this.list.length === 0) return 0;
        // return Math.round(this.totalScore / this.list.length);
        return (this.totalScore / this.list.length).toFixed(2);
      }
    }
  });
</script>

watch 侦听器

作用

监视数据变化,执行一些业务逻辑或异步操作

语法

  1. watch 同样声明在跟 data 同级的配置项中
  2. 简单写法:简单类型数据直接监视
  3. 完整写法:添加额外配置项
js
data: {
  words: '苹果',
  obj: {
    words: '苹果'
  }
},

watch: {
  // 该方法会在数据变化时,触发执行
  数据属性名 (newValue, oldValue) {
    一些业务逻辑 异步操作
  },
  '对象.属性名' (newValue, oldValue) {
    一些业务逻辑 异步操作
  }
}

深度监听

完整写法 —>添加额外的配置项

  1. deep:true 对复杂类型进行深度监听
  2. immdiate: true 初始化 立刻执行一次
js
watch: { // watch 完整写法
  对象: {
    deep: true, // 深度监视
    immdiate:true,//立即执行 handler 函数
    handler (newValue) {
      console.log(newValue)
    }
  }
}

案例 翻译

需求:

  1. 输入框输入内容,自动翻译
  2. 输入内容,修改语言,两种事件下都要翻译
html
<div id="app">
  <!-- 条件选择框 -->
  <div class="query">
    <span>翻译成的语言:</span>
    <select>
      <option value="italy">意大利</option>
      <option value="english">英语</option>
      <option value="german">德语</option>
    </select>
  </div>

  <!-- 翻译框 -->
  <div class="box">
    <div class="input-wrap">
      <textarea v-model="words"></textarea>
      <span><i>⌨️</i>文档翻译</span>
    </div>
    <div class="output-wrap">
      <div class="transbox">mela</div>
    </div>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
  // 接口地址:https://applet-base-api-t.itheima.net/api/translate
  // 请求方式:get
  // 请求参数:
  // (1)words:需要被翻译的文本(必传)
  // (2)lang:需要被翻译成的语言(可选)默认值 - 意大利
  // -----------------------------------------------

  const app = new Vue({
    el: "#app",
    data: {
      words: "",
    },
    // 具体讲解:(1) watch 语法 (2) 具体业务实现
  });
</script>
html
<div id="app">
  <!-- 条件选择框 -->
  <div class="query">
    <span>翻译成的语言:</span>
    <select>
      <option value="italy">意大利</option>
      <option value="english">英语</option>
      <option value="german">德语</option>
    </select>
  </div>

  <!-- 翻译框 -->
  <div class="box">
    <div class="input-wrap">
      <textarea v-model="obj.words"></textarea>
      <span><i>⌨️</i>文档翻译</span>
    </div>
    <div class="output-wrap">
      <div class="transbox">{{result}}</div>
    </div>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
  // 接口地址:https://applet-base-api-t.itheima.net/api/translate
  // 请求方式:get
  // 请求参数:
  // (1)words:需要被翻译的文本(必传)
  // (2)lang:需要被翻译成的语言(可选)默认值 - 意大利
  // -----------------------------------------------

  const app = new Vue({
    el: "#app",
    data: {
      obj: {
        words: "",
      },
      result: "",
    },
    // 具体讲解:(1) watch 语法 (2) 具体业务实现
    watch: {
      'obj.words'(newVal) {
        // 防抖:延迟执行 → 干啥事先等一等,延迟一会,一段时间内没有再次触发,才执行
        clearTimeout(this.timer)
        this.timer = setTimeout(async () => {
          // 发送请求
          const res = await axios.get('https://applet-base-api-t.itheima.net/api/translate', {
            params: {
              words: newVal
            }
          })
          console.log(res);
          // 获取翻译结果
          this.result = res.data.data
        }, 500);
      }
    },
  });
</script>
html
<div id="app">
  <!-- 条件选择框 -->
  <div class="query">
    <span>翻译成的语言:</span>
    <select v-model="obj.lang">
      <option value="italy">意大利</option>
      <option value="english">英语</option>
      <option value="german">德语</option>
    </select>
  </div>

  <!-- 翻译框 -->
  <div class="box">
    <div class="input-wrap">
      <textarea v-model="obj.words"></textarea>
      <span><i>⌨️</i>文档翻译</span>
    </div>
    <div class="output-wrap">
      <div class="transbox">{{result}}</div>
    </div>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
  // 接口地址:https://applet-base-api-t.itheima.net/api/translate
  // 请求方式:get
  // 请求参数:
  // (1)words:需要被翻译的文本(必传)
  // (2)lang:需要被翻译成的语言(可选)默认值 - 意大利
  // -----------------------------------------------

  const app = new Vue({
    el: "#app",
    data: {
      obj: {
        words: "刁德一",
        lang: "english"
      },
      result: "",
    },
    // 具体讲解:(1) watch 语法 (2) 具体业务实现
    watch: {
      obj: {
        handler(newVal) {
          // 防抖:延迟执行 → 干啥事先等一等,延迟一会,一段时间内没有再次触发,才执行
          clearTimeout(this.timer) // 清除定时器
          this.timer = setTimeout(async () => {
            // 发送请求
            const res = await axios.get("https://applet-base-api-t.itheima.net/api/translate", {
              params: newVal
            })
            // console.log(res)
            this.result = res.data.data
          }, 500)
        },
        deep: true, // 深度监听
        immediate: true, // 页面加载后立即执行
      }
    },
  });
</script>

总结

watch 侦听器的写法有几种?

  1. 简单写法

    js
    watch: {
      数据属性名 (newValue, oldValue) {
        一些业务逻辑 异步操作
      },
      '对象。属性名' (newValue, oldValue) {
        一些业务逻辑 异步操作
      }
    }
  2. 完整写法

    js
    watch: {// watch 完整写法
      数据属性名:{
        deep: true, // 深度监视 (针对复杂类型)
        immediate: true, // 是否立刻执行一次 handler
        handler (newValue) {
          console.log(newValue)
        }
      }
    }

综合案例 购物车

bb66ca9c-1ee2-431c-bd3f-444effb182cf

需求说明

  1. 渲染功能
  2. 删除功能
  3. 修改个数
  4. 全选反选
  5. 统计 选中的 总价 和 总数量
  6. 持久化到本地

实现思路

  1. 基本渲染:v-for 遍历、:class 动态绑定样式

  2. 删除功能:v-on 绑定事件,获取当前行的 id

  3. 修改个数:v-on 绑定事件,获取当前行的 id,进行筛选出对应的项然后增加或减少

  4. 全选反选

    • 必须所有的小选框都选中,全选按钮才选中 → every
    • 如果全选按钮选中,则所有小选框都选中
    • 如果全选取消,则所有小选框都取消选中

    声明计算属性,判断数组中的每一个 checked 属性的值,看是否需要全部选

  5. 统计 选中的 总价 和 总数量:通过计算属性来计算选中的总价和总数量

  6. 持久化到本地:在数据变化时都要更新下本地存储 watch

渲染功能

  • v-if v-else 判断是否有数据,有数据显示购物车本体,没有数据显示空购物车。
  • v-for 遍历购物车数组,渲染购物车列表。
html
<!-- 购物车主体 -->
<div class="main" v-if="fruitList.length > 0"></div>

<!-- 空车 -->
<div class="empty" v-else>🛒空空如也</div>

<div class="tbody">
  <!-- 遍历购物车数组,渲染购物车列表 -->
  <div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
    <div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
    <div class="td"><img :src="item.icon" alt="" /></div>
    <div class="td">{{item.price}}</div>
    <div class="td">
      <div class="my-input-number">
        <button class="decrease">-</button>
        <span class="my-input__inner">{{item.num}}</span>
        <button class="increase">+</button>
      </div>
    </div>
    <div class="td">{{item.num * item.price}}</div>
    <div class="td"><button>删除</button></div>
  </div>
</div>

删除功能

  • v-on button 绑定点击事件,获取当前行的 id
  • 创建删除方法,使用 reduce 遍历数组,筛选出不等于当前行 id 的项,重新赋值给购物车
jsx
<div class="td"><button @click="deleteItem(item.id)">删除</button></div>

methods: {
  deleteItem(id) {
    this.fruitList = this.fruitList.filter(item => item.id !== id)
  }
}

修改个数

  • v-on button 绑定点击事件,然后 item.num++item.num--
  • item.num 为 1 时,点击减少按钮,item.num 会变成 0。所以当 item.num <= 1 要禁用减少按钮
html
<button class="decrease" @click="item.num--" :disabled="item.num <= 1">-</button>

<button class="increase" @click="item.num++">+</button>

全选反选

  • 要求

    • 必须所有的小选框都选中,全选按钮才选中 → every
    • 如果全选按钮选中,则所有小选框都选中
    • 如果全选取消,则所有小选框都取消选中
  • 思路

    • 声明计算属性 selectAll
    • get 方法中使用 every 判断数组中的每一个 item.isChecked 属性的值,看是否全部选择,如果全部选择则返回 true,否则返回 false
    • set 方法中,遍历数组,将每一个 item.isChecked 属性的值都设置为 selectAll 的值(也就是全选按钮的值 true/false)
jsx
<label class="check-all">
  <input type="checkbox" v-model="selectAll" />全选
</label>

computed: {
  selectAll: {
    get() {
      return this.fruitList.every(item => item.isChecked)
    },
    set(val) {
      this.fruitList.forEach(item => item.isChecked = val)
    }
  }
}

统计

  • 通过计算属性来计算选中的总价和总数量
jsx
<!-- 所有商品总价 -->
<span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">{{totalPrice}}</span></span>
<!-- 结算按钮 -->
<button class="pay">结算 ( {{totalNum}} )</button>

computed: {
  // 统计选中的总数 reduce num
  totalNum() {
    return this.fruitList.reduce((total, item) => {
      if (item.isChecked) {
        return total + item.num;
      } else {
        return total;
      }
    }, 0);
  },

  // 总计选中的总价 reduce num * price
  totalPrice() {
    return this.fruitList.reduce((total, item) => {
      if (item.isChecked) {
        return total + (item.num * item.price);
      } else {
        return total;
      }
    }, 0);
  },
}

持久化到本地

  • 使用 watch 侦听器监听 fruitList 的变化,如果有变化,将变化的值存储到 localStorage 中(需要使用 JSON.stringify 将对象转换为字符串)
  • 默认数据从 localStorage 中获取(需要使用 JSON.parse 将字符串转换为对象),如果没有数据,就使用默认数据
jsx
// 监听 fruitList 的变化,如果有变化,将变化的值存储到 localStorage 中(需要使用 JSON.stringify 将对象转换为字符串)
watch: {
  fruitList: {
    handler(newVal) {
      localStorage.setItem('fruitList', JSON.stringify(newVal))
    },
    deep: true
  }
}

// 默认数据
const defaultFruitList = [
  {
    id: 1,
    icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
    isChecked: true,
    num: 2,
    price: 6,
  },
  {
    id: 2,
    icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
    isChecked: false,
    num: 7,
    price: 20,
  },
  {
    id: 3,
    icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
    isChecked: false,
    num: 3,
    price: 40,
  },
  {
    id: 4,
    icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
    isChecked: true,
    num: 10,
    price: 3,
  },
  {
    id: 5,
    icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
    isChecked: false,
    num: 20,
    price: 34,
  },
];

data: {
  // 水果列表
  // 优先从 localStorage 中读取存储的 fruitList,如果不存在(用户清空本地存储),则从 defaultFruitList 中读取
  fruitList: JSON.parse(localStorage.getItem('fruitList')) || defaultFruitList,
},

相关代码

html
<div class="app-container" id="app">
  <!-- 顶部 banner -->
  <div class="banner-box">
    <img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" />
  </div>
  <!-- 面包屑 -->
  <div class="breadcrumb">
    <span>🏠</span>
    /
    <span>购物车</span>
  </div>
  <!-- 购物车主体 -->
  <div class="main">
    <div class="table">
      <!-- 头部 -->
      <div class="thead">
        <div class="tr">
          <div class="th">选中</div>
          <div class="th th-pic">图片</div>
          <div class="th">单价</div>
          <div class="th num-th">个数</div>
          <div class="th">小计</div>
          <div class="th">操作</div>
        </div>
      </div>
      <!-- 身体 -->
      <div class="tbody">
        <div class="tr active">
          <div class="td"><input type="checkbox" checked /></div>
          <div class="td">
            <img src="https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png" alt="" />
          </div>
          <div class="td">6</div>
          <div class="td">
            <div class="my-input-number">
              <button class="decrease">-</button>
              <span class="my-input__inner">2</span>
              <button class="increase">+</button>
            </div>
          </div>
          <div class="td">12</div>
          <div class="td"><button>删除</button></div>
        </div>

        <div class="tr">
          <div class="td"><input type="checkbox" /></div>
          <div class="td">
            <img src="https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png" alt="" />
          </div>
          <div class="td">7</div>
          <div class="td">
            <div class="my-input-number">
              <button disabled class="decrease">-</button>
              <span class="my-input__inner">1</span>
              <button class="increase">+</button>
            </div>
          </div>
          <div class="td">14</div>
          <div class="td"><button>删除</button></div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <div class="bottom">
      <!-- 全选 -->
      <label class="check-all">
        <input type="checkbox" />
        全选
      </label>
      <div class="right-box">
        <!-- 所有商品总价 -->
        <span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">24</span></span>
        <!-- 结算按钮 -->
        <button class="pay">结算 ( 6 )</button>
      </div>
    </div>
  </div>
  <!-- 空车 -->
  <div class="empty">🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      // 水果列表
      fruitList: [
        {
          id: 1,
          icon: "https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png",
          isChecked: true,
          num: 2,
          price: 6,
        },
        {
          id: 2,
          icon: "https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png",
          isChecked: false,
          num: 7,
          price: 20,
        },
        {
          id: 3,
          icon: "https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png",
          isChecked: false,
          num: 3,
          price: 40,
        },
        {
          id: 4,
          icon: "https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png",
          isChecked: true,
          num: 10,
          price: 3,
        },
        {
          id: 5,
          icon: "https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png",
          isChecked: false,
          num: 20,
          price: 34,
        },
      ],
    },
  });
</script>
html
<div class="app-container" id="app">
  <!-- 顶部 banner -->
  <div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
  <!-- 面包屑 -->
  <div class="breadcrumb">
    <span>🏠</span>
    /
    <span>购物车</span>
  </div>
  <!-- 购物车主体 -->
  <div class="main" v-if="fruitList.length > 0">
    <div class="table">
      <!-- 头部 -->
      <div class="thead">
        <div class="tr">
          <div class="th">选中</div>
          <div class="th th-pic">图片</div>
          <div class="th">单价</div>
          <div class="th num-th">个数</div>
          <div class="th">小计</div>
          <div class="th">操作</div>
        </div>
      </div>
      <!-- 身体 -->
      <div class="tbody">
        <div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
          <div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
          <div class="td"><img :src="item.icon" alt="" /></div>
          <div class="td">{{item.price}}</div>
          <div class="td">
            <div class="my-input-number">
              <button class="decrease"> - </button>
              <span class="my-input__inner">{{item.num}}</span>
              <button class="increase"> + </button>
            </div>
          </div>
          <div class="td">{{item.num * item.price}}</div>
          <div class="td"><button>删除</button></div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <div class="bottom">
      <!-- 全选 -->
      <label class="check-all">
        <input type="checkbox" />
        全选
      </label>
      <div class="right-box">
        <!-- 所有商品总价 -->
        <span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">24</span></span>
        <!-- 结算按钮 -->
        <button class="pay">结算 ( 6 )</button>
      </div>
    </div>
  </div>
  <!-- 空车 -->
  <div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      // 水果列表
      fruitList: [
        {
          id: 1,
          icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
          isChecked: true,
          num: 2,
          price: 6,
        },
        {
          id: 2,
          icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
          isChecked: false,
          num: 7,
          price: 20,
        },
        {
          id: 3,
          icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
          isChecked: false,
          num: 3,
          price: 40,
        },
        {
          id: 4,
          icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
          isChecked: true,
          num: 10,
          price: 3,
        },
        {
          id: 5,
          icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
          isChecked: false,
          num: 20,
          price: 34,
        },
      ],
    },
  })
</script>
html
<div class="app-container" id="app">
  <!-- 顶部 banner -->
  <div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
  <!-- 面包屑 -->
  <div class="breadcrumb">
    <span>🏠</span>
    /
    <span>购物车</span>
  </div>
  <!-- 购物车主体 -->
  <div class="main" v-if="fruitList.length > 0">
    <div class="table">
      <!-- 头部 -->
      <div class="thead">
        <div class="tr">
          <div class="th">选中</div>
          <div class="th th-pic">图片</div>
          <div class="th">单价</div>
          <div class="th num-th">个数</div>
          <div class="th">小计</div>
          <div class="th">操作</div>
        </div>
      </div>
      <!-- 身体 -->
      <div class="tbody">
        <div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
          <div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
          <div class="td"><img :src="item.icon" alt="" /></div>
          <div class="td">{{item.price}}</div>
          <div class="td">
            <div class="my-input-number">
              <button class="decrease"> - </button>
              <span class="my-input__inner">{{item.num}}</span>
              <button class="increase"> + </button>
            </div>
          </div>
          <div class="td">{{item.num * item.price}}</div>
          <div class="td"><button @click="deleteItem(item.id)">删除</button></div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <div class="bottom">
      <!-- 全选 -->
      <label class="check-all">
        <input type="checkbox" />
        全选
      </label>
      <div class="right-box">
        <!-- 所有商品总价 -->
        <span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">24</span></span>
        <!-- 结算按钮 -->
        <button class="pay">结算 ( 6 )</button>
      </div>
    </div>
  </div>
  <!-- 空车 -->
  <div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      // 水果列表
      fruitList: [
        {
          id: 1,
          icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
          isChecked: true,
          num: 2,
          price: 6,
        },
        {
          id: 2,
          icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
          isChecked: false,
          num: 7,
          price: 20,
        },
        {
          id: 3,
          icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
          isChecked: false,
          num: 3,
          price: 40,
        },
        {
          id: 4,
          icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
          isChecked: true,
          num: 10,
          price: 3,
        },
        {
          id: 5,
          icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
          isChecked: false,
          num: 20,
          price: 34,
        },
      ],
    },
    methods: {
      deleteItem(id) {
        this.fruitList = this.fruitList.filter(item => item.id !== id)
      }
    }
  })
</script>
html
<div class="app-container" id="app">
  <!-- 顶部 banner -->
  <div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
  <!-- 面包屑 -->
  <div class="breadcrumb">
    <span>🏠</span>
    /
    <span>购物车</span>
  </div>
  <!-- 购物车主体 -->
  <div class="main" v-if="fruitList.length > 0">
    <div class="table">
      <!-- 头部 -->
      <div class="thead">
        <div class="tr">
          <div class="th">选中</div>
          <div class="th th-pic">图片</div>
          <div class="th">单价</div>
          <div class="th num-th">个数</div>
          <div class="th">小计</div>
          <div class="th">操作</div>
        </div>
      </div>
      <!-- 身体 -->
      <div class="tbody">
        <div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
          <div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
          <div class="td"><img :src="item.icon" alt="" /></div>
          <div class="td">{{item.price}}</div>
          <div class="td">
            <div class="my-input-number">
              <button class="decrease" @click="item.num--" :disabled="item.num <= 1"> - </button>
              <span class="my-input__inner">{{item.num}}</span>
              <button class="increase" @click="item.num++"> + </button>
            </div>
          </div>
          <div class="td">{{item.num * item.price}}</div>
          <div class="td"><button @click="deleteItem(item.id)">删除</button></div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <div class="bottom">
      <!-- 全选 -->
      <label class="check-all">
        <input type="checkbox" />
        全选
      </label>
      <div class="right-box">
        <!-- 所有商品总价 -->
        <span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">24</span></span>
        <!-- 结算按钮 -->
        <button class="pay">结算 ( 6 )</button>
      </div>
    </div>
  </div>
  <!-- 空车 -->
  <div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      // 水果列表
      fruitList: [
        {
          id: 1,
          icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
          isChecked: true,
          num: 2,
          price: 6,
        },
        {
          id: 2,
          icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
          isChecked: false,
          num: 7,
          price: 20,
        },
        {
          id: 3,
          icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
          isChecked: false,
          num: 3,
          price: 40,
        },
        {
          id: 4,
          icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
          isChecked: true,
          num: 10,
          price: 3,
        },
        {
          id: 5,
          icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
          isChecked: false,
          num: 20,
          price: 34,
        },
      ],
    },
    methods: {
      deleteItem(id) {
        this.fruitList = this.fruitList.filter(item => item.id !== id)
      }
    }
  })
</script>
html
<div class="app-container" id="app">
  <!-- 顶部 banner -->
  <div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
  <!-- 面包屑 -->
  <div class="breadcrumb">
    <span>🏠</span>
    /
    <span>购物车</span>
  </div>
  <!-- 购物车主体 -->
  <div class="main" v-if="fruitList.length > 0">
    <div class="table">
      <!-- 头部 -->
      <div class="thead">
        <div class="tr">
          <div class="th">选中</div>
          <div class="th th-pic">图片</div>
          <div class="th">单价</div>
          <div class="th num-th">个数</div>
          <div class="th">小计</div>
          <div class="th">操作</div>
        </div>
      </div>
      <!-- 身体 -->
      <div class="tbody">
        <div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
          <div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
          <div class="td"><img :src="item.icon" alt="" /></div>
          <div class="td">{{item.price}}</div>
          <div class="td">
            <div class="my-input-number">
              <button class="decrease" @click="item.num--" :disabled="item.num <= 1"> - </button>
              <span class="my-input__inner">{{item.num}}</span>
              <button class="increase" @click="item.num++"> + </button>
            </div>
          </div>
          <div class="td">{{item.num * item.price}}</div>
          <div class="td"><button @click="deleteItem(item.id)">删除</button></div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <div class="bottom">
      <!-- 全选 -->
      <label class="check-all">
        <input type="checkbox" v-model="selectAll" />
        全选
      </label>
      <div class="right-box">
        <!-- 所有商品总价 -->
        <span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">24</span></span>
        <!-- 结算按钮 -->
        <button class="pay">结算 ( 6 )</button>
      </div>
    </div>
  </div>
  <!-- 空车 -->
  <div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      // 水果列表
      fruitList: [
        {
          id: 1,
          icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
          isChecked: true,
          num: 2,
          price: 6,
        },
        {
          id: 2,
          icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
          isChecked: false,
          num: 7,
          price: 20,
        },
        {
          id: 3,
          icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
          isChecked: false,
          num: 3,
          price: 40,
        },
        {
          id: 4,
          icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
          isChecked: true,
          num: 10,
          price: 3,
        },
        {
          id: 5,
          icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
          isChecked: false,
          num: 20,
          price: 34,
        },
      ],
    },
    methods: {
      deleteItem(id) {
        this.fruitList = this.fruitList.filter(item => item.id !== id)
      }
    },
    computed: {
      selectAll: {
        get() {
          return this.fruitList.every(item => item.isChecked)
        },
        set(val) {
          this.fruitList.forEach(item => item.isChecked = val)
        }
      }
    }
  })
</script>
html
<div class="app-container" id="app">
  <!-- 顶部 banner -->
  <div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
  <!-- 面包屑 -->
  <div class="breadcrumb">
    <span>🏠</span>
    /
    <span>购物车</span>
  </div>
  <!-- 购物车主体 -->
  <div class="main" v-if="fruitList.length > 0">
    <div class="table">
      <!-- 头部 -->
      <div class="thead">
        <div class="tr">
          <div class="th">选中</div>
          <div class="th th-pic">图片</div>
          <div class="th">单价</div>
          <div class="th num-th">个数</div>
          <div class="th">小计</div>
          <div class="th">操作</div>
        </div>
      </div>
      <!-- 身体 -->
      <div class="tbody">
        <div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
          <div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
          <div class="td"><img :src="item.icon" alt="" /></div>
          <div class="td">{{item.price}}</div>
          <div class="td">
            <div class="my-input-number">
              <button class="decrease" @click="item.num--" :disabled="item.num <= 1"> - </button>
              <span class="my-input__inner">{{item.num}}</span>
              <button class="increase" @click="item.num++"> + </button>
            </div>
          </div>
          <div class="td">{{item.num * item.price}}</div>
          <div class="td"><button @click="deleteItem(item.id)">删除</button></div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <div class="bottom">
      <!-- 全选 -->
      <label class="check-all">
        <input type="checkbox" v-model="selectAll" />
        全选
      </label>
      <div class="right-box">
        <!-- 所有商品总价 -->
        <span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">{{totalPrice}}</span></span>
        <!-- 结算按钮 -->
        <button class="pay">结算 ( {{totalNum}} )</button>
      </div>
    </div>
  </div>
  <!-- 空车 -->
  <div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      // 水果列表
      fruitList: [
        {
          id: 1,
          icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
          isChecked: true,
          num: 2,
          price: 6,
        },
        {
          id: 2,
          icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
          isChecked: false,
          num: 7,
          price: 20,
        },
        {
          id: 3,
          icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
          isChecked: false,
          num: 3,
          price: 40,
        },
        {
          id: 4,
          icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
          isChecked: true,
          num: 10,
          price: 3,
        },
        {
          id: 5,
          icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
          isChecked: false,
          num: 20,
          price: 34,
        },
      ],
    },
    methods: {
      deleteItem(id) {
        this.fruitList = this.fruitList.filter(item => item.id !== id)
      }
    },
    computed: {
      selectAll: {
        get() {
          return this.fruitList.every(item => item.isChecked)
        },
        set(val) {
          this.fruitList.forEach(item => item.isChecked = val)
        }
      },
      // 统计选中的总数 reduce
      totalNum() {
        return this.fruitList.reduce((total, item) => {
          if (item.isChecked) {
            return total + item.num;
          } else {
            return total;
          }
        }, 0);
      },
      // 总计选中的总价 num * price
      totalPrice() {
        return this.fruitList.reduce((total, item) => {
          if (item.isChecked) {
            return total + (item.num * item.price);
          } else {
            return total;
          }
        }, 0);
      },
    }
  })
</script>
html
<div class="app-container" id="app">
  <!-- 顶部 banner -->
  <div class="banner-box"><img src="https://s2.loli.net/2024/01/18/zdSNRkD4rwHxC3Q.jpg" alt="" /></div>
  <!-- 面包屑 -->
  <div class="breadcrumb">
    <span>🏠</span>
    /
    <span>购物车</span>
  </div>
  <!-- 购物车主体 -->
  <div class="main" v-if="fruitList.length > 0">
    <div class="table">
      <!-- 头部 -->
      <div class="thead">
        <div class="tr">
          <div class="th">选中</div>
          <div class="th th-pic">图片</div>
          <div class="th">单价</div>
          <div class="th num-th">个数</div>
          <div class="th">小计</div>
          <div class="th">操作</div>
        </div>
      </div>
      <!-- 身体 -->
      <div class="tbody">
        <div :class="{active: item.isChecked}" v-for="item in fruitList" :key="item.id" class="tr">
          <div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
          <div class="td"><img :src="item.icon" alt="" /></div>
          <div class="td">{{item.price}}</div>
          <div class="td">
            <div class="my-input-number">
              <button class="decrease" @click="item.num--" :disabled="item.num <= 1"> - </button>
              <span class="my-input__inner">{{item.num}}</span>
              <button class="increase" @click="item.num++"> + </button>
            </div>
          </div>
          <div class="td">{{item.num * item.price}}</div>
          <div class="td"><button @click="deleteItem(item.id)">删除</button></div>
        </div>
      </div>
    </div>
    <!-- 底部 -->
    <div class="bottom">
      <!-- 全选 -->
      <label class="check-all">
        <input type="checkbox" v-model="selectAll" />
        全选
      </label>
      <div class="right-box">
        <!-- 所有商品总价 -->
        <span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">{{totalPrice}}</span></span>
        <!-- 结算按钮 -->
        <button class="pay">结算 ( {{totalNum}} )</button>
      </div>
    </div>
  </div>
  <!-- 空车 -->
  <div class="empty" v-else>🛒空空如也</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
  const defaultFruitList = [
    {
      id: 1,
      icon: 'https://s2.loli.net/2024/01/18/Q4ieLnUwYhmOFN2.png',
      isChecked: true,
      num: 2,
      price: 6,
    },
    {
      id: 2,
      icon: 'https://s2.loli.net/2024/01/18/FVNT2iJ3HyB8zUg.png',
      isChecked: false,
      num: 7,
      price: 20,
    },
    {
      id: 3,
      icon: 'https://s2.loli.net/2024/01/18/7Gclpdw96YLinj1.png',
      isChecked: false,
      num: 3,
      price: 40,
    },
    {
      id: 4,
      icon: 'https://s2.loli.net/2024/01/18/iv5aHcwJ1eKZdpD.png',
      isChecked: true,
      num: 10,
      price: 3,
    },
    {
      id: 5,
      icon: 'https://s2.loli.net/2024/01/18/fOZ73nQ9F1dRhID.png',
      isChecked: false,
      num: 20,
      price: 34,
    },
  ];
  const app = new Vue({

    el: '#app',
    data: {
      // 水果列表
      // 优先从 localStorage 中读取存储的 fruitList,如果不存在(用户清空本地存储),则从 defaultFruitList 中读取
      fruitList: JSON.parse(localStorage.getItem('fruitList')) || defaultFruitList,
    },
    methods: {
      deleteItem(id) {
        this.fruitList = this.fruitList.filter(item => item.id !== id)
      }
    },
    computed: {
      selectAll: {
        get() {
          return this.fruitList.every(item => item.isChecked)
        },
        set(val) {
          this.fruitList.forEach(item => item.isChecked = val)
        }
      },
      // 统计选中的总数 reduce
      totalNum() {
        return this.fruitList.reduce((total, item) => {
          if (item.isChecked) {
            return total + item.num;
          } else {
            return total;
          }
        }, 0);
      },
      // 总计选中的总价 num * price
      totalPrice() {
        return this.fruitList.reduce((total, item) => {
          if (item.isChecked) {
            return total + (item.num * item.price);
          } else {
            return total;
          }
        }, 0);
      },
    },
    watch: {
      // 监听 fruitList 的变化,如果有变化,将变化的值存储到 localStorage 中(需要使用 JSON.stringify 将对象转换为字符串)
      fruitList: {
        handler(newVal) {
          localStorage.setItem('fruitList', JSON.stringify(newVal))
        },
        deep: true
      }
    }
  })
</script>