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>