Vue.js 2 项目实战(五):水果购物车

前言

Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架。它的设计目标是通过采用易于上手的结构和强大的功能,使前端开发变得更加简便和高效。以下是 Vue.js 的一些关键特性和优点:

核心特性

  1. 声明式渲染

    • Vue.js 使用声明式语法来描述用户界面,数据和视图是双向绑定的。当数据变化时,视图会自动更新。
  2. 组件系统

    • Vue.js 提供了一个灵活的组件系统,允许开发者将 UI 分解成可复用的组件,每个组件都有自己的逻辑和样式。
  3. 单文件组件 (SFC)

    • Vue.js 允许开发者将 HTML、CSS 和 JavaScript 放在同一个 .vue 文件中,这样可以更容易地管理组件。
  4. 虚拟 DOM

    • Vue.js 使用虚拟 DOM 来优化更新过程,通过最小化实际 DOM 操作来提高性能。
  5. 反应式数据绑定

    • Vue.js 提供了响应式的数据绑定系统,使得数据与 DOM 之间的同步变得简单和高效。
  6. 指令

    • Vue.js 提供了一组内置指令(如 v-bindv-modelv-for),帮助开发者轻松地操作 DOM。

优点

  1. 易于上手

    • Vue.js 的学习曲线较低,适合新手入门,并且文档详细、社区活跃。
  2. 灵活性

    • Vue.js 可以与现有项目集成,也可以用于构建复杂的单页面应用(SPA)。
  3. 性能高效

    • 得益于虚拟 DOM 和响应式系统,Vue.js 在处理大量数据更新时表现出色。
  4. 生态系统

    • Vue.js 拥有丰富的生态系统,包括 Vue Router、Vuex、Vue CLI 等工具和库,支持开发者在不同场景下使用。
  5. 强大的社区支持

    • Vue.js 有一个全球活跃的社区,提供了大量的插件、教程和支持资源。

知识点

  1. v-if v-else 条件渲染,如果没有数据则显示空车

  2. v-for 循环遍历渲染数组

  3. v-bind 绑定 class 设置条件

  4. v-model 绑定属性

  5. v-bind 绑定 src

  6. 插值语法

  7. @click 点击事件实现数量加减

  8. filter() 方法更新数组达到删除元素的效果

  9. find() 方法通过 id 找到对应的水果

  10. :disabled 动态属性绑定限制用户

  11. 计算属性完整写法实现全选的两个功能

  12. every() 方法检查每一个元素

  13. foreach() 方法设置每一个元素

  14. some() 方法实现如果有任意一个按钮不是 true,则全选,反之取消所有选择

  15. reduce() 方法计算总价与数量

  16. 判断条件实现没有选中的则不添加

项目效果

初始页面

添加水果

全选按钮

删除水果

本地缓存

源代码

index.css

css 复制代码
.app-container {
  padding-bottom: 300px;
  width: 800px;
  margin: 0 auto;
}
@media screen and (max-width: 800px) {
  .app-container {
    width: 600px;
  }
}
.app-container .banner-box {
  border-radius: 20px;
  overflow: hidden;
  margin-bottom: 10px;
}
.app-container .banner-box img {
  width: 100%;
}
.app-container .nav-box {
  background: #ddedec;
  height: 60px;
  border-radius: 10px;
  padding-left: 20px;
  display: flex;
  align-items: center;
}
.app-container .nav-box .my-nav {
  display: inline-block;
  background: #5fca71;
  border-radius: 5px;
  width: 90px;
  height: 35px;
  color: white;
  text-align: center;
  line-height: 35px;
  margin-right: 10px;
}

.breadcrumb {
  font-size: 16px;
  color: gray;
}
.table {
  width: 100%;
  text-align: left;
  border-radius: 2px 2px 0 0;
  border-collapse: separate;
  border-spacing: 0;
}
.th {
  color: rgba(0, 0, 0, 0.85);
  font-weight: 500;
  text-align: left;
  background: #fafafa;
  border-bottom: 1px solid #f0f0f0;
  transition: background 0.3s ease;
}
.th.num-th {
  flex: 1.5;
}
.th {
  text-align: center;
}
.th:nth-child(4),
.th:nth-child(5),
.th:nth-child(6),
.th:nth-child(7) {
  text-align: center;
}
.th.th-pic {
  flex: 1.3;
}
.th:nth-child(6) {
  flex: 1.3;
}

.th,
.td {
  position: relative;
  padding: 16px 16px;
  overflow-wrap: break-word;
  flex: 1;
}
.pick-td {
  font-size: 14px;
}
.main,
.empty {
  border: 1px solid #f0f0f0;
  margin-top: 10px;
}
.tr {
  display: flex;
  cursor: pointer;
  border-bottom: 1px solid #ebeef5;
}
.tr.active {
  background-color: #f5f7fa;
}
.td {
  display: flex;
  justify-content: center;
  align-items: center;
}

.table img {
  width: 100px;
  height: 100px;
}

button {
  outline: 0;
  box-shadow: none;
  color: #fff;
  background: #d9363e;
  border-color: #d9363e;
  color: #fff;
  background: #d9363e;
  border-color: #d9363e;
  line-height: 1.5715;
  position: relative;
  display: inline-block;
  font-weight: 400;
  white-space: nowrap;
  text-align: center;
  background-image: none;
  border: 1px solid transparent;
  box-shadow: 0 2px 0 rgb(0 0 0 / 2%);
  cursor: pointer;
  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  touch-action: manipulation;
  height: 32px;
  padding: 4px 15px;
  font-size: 14px;
  border-radius: 2px;
}
button.pay {
  background-color: #3f85ed;
  margin-left: 20px;
}

.bottom {
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-right: 20px;
  border: 1px solid #f0f0f0;
  border-top: none;
  padding-left: 20px;
}
.right-box {
  display: flex;
  align-items: center;
}
.check-all {
  cursor: pointer;
}
.price {
  color: hotpink;
  font-size: 30px;
  font-weight: 700;
}
.price-box {
  display: flex;
  align-items: center;
}
.empty {
  padding: 20px;
  text-align: center;
  font-size: 30px;
  color: #909399;
}
.my-input-number {
  display: flex;
}
.my-input-number button {
  height: 40px;
  color: #333;
  border: 1px solid #dcdfe6;
  background-color: #f5f7fa;
}
.my-input-number button:disabled {
  cursor: not-allowed!important;
}
.my-input-number .my-input__inner {
  height: 40px;
  width: 50px;
  padding: 0;
  border: none;
  border-top: 1px solid #dcdfe6;
  border-bottom: 1px solid #dcdfe6;

inputnumber.css

css 复制代码
.my-input-number {
  position: relative;
  display: inline-block;
  width: 140px;
  line-height: 38px;
}
.my-input-number span {
  -moz-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
}
.my-input-number .my-input {
  display: block;
  position: relative;
  font-size: 14px;
  width: 100%;
}
.my-input-number .my-input__inner {
  -webkit-appearance: none;
  background-color: #fff;
  background-image: none;
  border-radius: 4px;
  border: 1px solid #dcdfe6;
  box-sizing: border-box;
  color: #606266;
  display: inline-block;
  font-size: inherit;
  height: 40px;
  line-height: 40px;
  outline: none;
  padding: 0 15px;
  transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
  width: 100%;
  padding-left: 50px;
  padding-right: 50px;
  text-align: center;
}
.my-input-number .my-input-number__decrease,
.my-input-number .my-input-number__increase {
  position: absolute;
  z-index: 1;
  top: 1px;
  width: 40px;
  height: auto;
  text-align: center;
  background: #f5f7fa;
  color: #606266;
  cursor: pointer;
  font-size: 13px;
}
.my-input-number .my-input-number__decrease {
  left: 1px;
  border-radius: 4px 0 0 4px;
  border-right: 1px solid #dcdfe6;
}
.my-input-number .my-input-number__increase {
  right: 1px;
  border-radius: 0 4px 4px 0;
  border-left: 1px solid #dcdfe6;
}
.my-input-number .my-input-number__decrease.is-disabled,
.my-input-number .my-input-number__increase.is-disabled {
  color: #c0c4cc;
  cursor: not-allowed;
}

html

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./css/inputnumber.css" />
    <link rel="stylesheet" href="./css/index.css" />
    <title>购物车</title>
  </head>
  <body>
    <div class="app-container" id="app">
      <!-- 顶部banner -->
      <div class="banner-box"><img src="./img/fruit.jpg" alt="Fruit"></div>
      <!-- 面包屑 -->
      <div class="breadcrumb">
        <span>🏠</span>
        /
        <span>购物车</span>
      </div>
      <!-- 购物车主体 -->
      <!-- v-if v-else 条件渲染,如果没有数据则显示空车 -->
      <div v-if="fruitList.length > 0" 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>
          <!-- 身体 -->
          <!-- v-for 循环遍历渲染数组 -->
          <div v-for="(item, index) in fruitList" :key="item.id" class="tbody">
            <!-- v-bind 绑定 class 设置条件-->
            <div class="tr" :class="{ active: item.isChecked }">
              <!-- v-model 绑定属性 -->
              <div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
              <!-- v-bind 绑定 src -->
              <div class="td"><img :src="item.icon" alt="" /></div>
              <div class="td">{{ item.price }}</div>
              <div class="td">
                <div class="my-input-number">
                  <!-- @click 点击事件实现数量加减 -->
                  <!-- :disabled 是一个动态属性绑定,它允许你根据表达式的结果动态地设置 HTML 元素的 disabled 属性 -->
                  <button :disabled="item.num <= 1" class="decrease" @click="item.num--"> - </button>
                  <span class="my-input__inner">{{ item.num }}</span>
                  <button class="increase" @click="item.num++"> + </button>
                </div>
              </div>
              <div class="td">{{ item.price * item.num }}</div>
              <div @click="del(item.id)" class="td"><button>删除</button></div>
            </div>
          </div>
        </div>
        <!-- 底部 -->
        <div class="bottom">
          <!-- 全选 -->
          <label class="check-all">
            <!-- v-model 绑定计算属性实现全选功能 -->
            <input type="checkbox" v-model="isAll" @click=""/>
            全选
          </label>
          <div class="right-box">
            <!-- 所有商品总价 -->
            <span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">{{ totalPrice }}</span></span>
            <!-- 结算按钮 -->
            <button class="pay">结算{{ totalCount }}</button>
          </div>
        </div>
      </div>
      <!-- 空车 -->
      <div v-else class="empty">🛒空空如也</div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
    <script>
      // 初始页面给出默认数组
      const defultArr = [
        {
          id: 1,
          icon: './img/火龙果.png',
          isChecked: true,
          num: 2,
          price: 6,
        },
        {
          id: 2,
          icon: './img/荔枝.png',
          isChecked: false,
          num: 7,
          price: 20,
        },
        {
          id: 3,
          icon: './img/榴莲.png',
          isChecked: false,
          num: 3,
          price: 40,
        },
        {
          id: 4,
          icon: './img/鸭梨.png',
          isChecked: true,
          num: 10,
          price: 3,
        },
        {
          id: 5,
          icon: './img/樱桃.png',
          isChecked: false,
          num: 20,
          price: 34,
        },
      ]
      const app = new Vue({
        el: '#app',
        data: {
          // localStorage.getItem 是 Web 存储 API 的一部分,用于从本地存储中获取键名为 'list' 的数据项
          // JSON.parse 函数用于将 JSON 格式的字符串转换为 JavaScript 对象。如果从 localStorage 获取的字符串是有效的 JSON 字符串,它将被转换为对应的 JavaScript 对象
          // || 是逻辑或运算符。如果 JSON.parse(localStorage.getItem('list')) 的结果为 null 或 undefined(即本地存储中没有 'list' 键或其值为 null),则使用 defaultArr 作为默认值
          fruitList: JSON.parse(localStorage.getItem('list')) || defultArr ,
        },
        computed: {
          // 默认计算属性只能获取不能设置
          // 通过 every 检查每个水果的选中状态,如果都选中则全选按钮选中
          // isAll () {
          //   return this.fruitList.every(item => item.isChecked === true)
          // }

          // 完整写法 get + set
          isAll: {
            // get 用于获取
            get () {
              return this.fruitList.every(item => item.isChecked === true)
            },
            // set 用于设置
            set (value) {
              // 点击按钮时先判断
              // some() 方法实现如果有任意一个按钮不是 true,则全选,反之取消所有选择
              if (this.fruitList.some(item => item.isChecked !== true)) {
                this.fruitList.forEach(item => item.isChecked = true);
              } else {
                this.fruitList.forEach(item => item.isChecked = false);
              }
            }
          },
          totalCount () {
            return this.fruitList.reduce((sum, item) => {
              // 判断实现选中状态的才添加
              if (item.isChecked === true) {
                return sum + item.num
              } else {
                // 没选中则不干,返回 sum
                return sum
              }
            }, 0)
          },
          totalPrice () {
            return this.fruitList.reduce((sum, item) => {
              // 判断实现选中状态的才添加
              if (item.isChecked === true) {
                return sum + item.price * item.num
              } else {
                return sum
              }
            }, 0)
          }
        },
        methods: {
          del (id) {
            // filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
            // 这是一个箭头函数,它接收数组中的每个元素作为参数(在这里是 item)。
            // item.id !== id:这是一个比较操作,检查 item 的 id 属性是否不等于传递给 filter 方法的 id 参数。如果不等于,返回 true;如果等于,返回 false
            this.fruitList = this.fruitList.filter(item => item.id !== id)
          },
        },
        // 使用 watch 监视数据保存到本地实现持久化
        // watch 是 Vue 实例的一个选项,用于定义一个或多个观察者,观察者可以观察 Vue 实例的数据变化
        watch: {
          fruitList: {
            deep: true,
            handler (newValue) {
              // 使用 $nextTick 确保在 DOM 更新完成后保存数据
              this.$nextTick(() => {
                // 需要将变化后的数据(newValue)存储本地(转 JSON)
                // localStorage.setItem('list', JSON.stringify(newValue)) 将 fruitList 数组的新值转换为 JSON 字符串,并存储到浏览器的本地存储中。
                // localStorage 是 Web 存储 API 的一部分,允许存储键值对数据,数据在页面会话结束时会被清除
                // JSON.stringify(newValue) 将 newValue(一个数组)转换为一个 JSON 格式的字符串,以便存储在本地存储中
                localStorage.setItem('list', JSON.stringify(newValue));
              });
            }
          }
        }
      })
    </script>
  </body>
</html>
相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb5 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
沈梦研5 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
轻口味6 小时前
Vue.js 组件之间的通信模式
vue.js
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter