前言
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架。它的设计目标是通过采用易于上手的结构和强大的功能,使前端开发变得更加简便和高效。以下是 Vue.js 的一些关键特性和优点:
核心特性
- 声明式渲染
- Vue.js 使用声明式语法来描述用户界面,数据和视图是双向绑定的。当数据变化时,视图会自动更新。
- 组件系统
- Vue.js 提供了一个灵活的组件系统,允许开发者将 UI 分解成可复用的组件,每个组件都有自己的逻辑和样式。
- 单文件组件 (SFC)
- Vue.js 允许开发者将 HTML、CSS 和 JavaScript 放在同一个
.vue
文件中,这样可以更容易地管理组件。
- 虚拟 DOM
- Vue.js 使用虚拟 DOM 来优化更新过程,通过最小化实际 DOM 操作来提高性能。
- 反应式数据绑定
- Vue.js 提供了响应式的数据绑定系统,使得数据与 DOM 之间的同步变得简单和高效。
- 指令
- Vue.js 提供了一组内置指令(如
v-bind
、v-model
和v-for
),帮助开发者轻松地操作 DOM。
优点
- 易于上手
- Vue.js 的学习曲线较低,适合新手入门,并且文档详细、社区活跃。
- 灵活性
- Vue.js 可以与现有项目集成,也可以用于构建复杂的单页面应用(SPA)。
- 性能高效
- 得益于虚拟 DOM 和响应式系统,Vue.js 在处理大量数据更新时表现出色。
- 生态系统
- Vue.js 拥有丰富的生态系统,包括 Vue Router、Vuex、Vue CLI 等工具和库,支持开发者在不同场景下使用。
- 强大的社区支持
- Vue.js 有一个全球活跃的社区,提供了大量的插件、教程和支持资源。
知识点
1. 创建 Vue 实例
2. v-bind 数据绑定
3. @click 点击事件绑定
4. v-show 显示或隐藏元素
项目效果
原始效果
添加水果
全选按钮
删除水果
本地缓存
源代码
index.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
.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
<!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">总价 : ¥ <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>