案例:购物车功能
静态页面的样式如下:
静态页面模板:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <style type="text/css"> .container .cart { width: 300px; margin: auto; } .container .title { background-color: lightblue; height: 40px; line-height: 40px; text-align: center; /*color: #fff;*/ } .container .total { background-color: #FFCE46; height: 50px; line-height: 50px; text-align: right; } .container .total button { margin: 0 10px; background-color: #DC4C40; height: 35px; width: 80px; border: 0; } .container .total span { color: red; font-weight: bold; } .container .item { height: 55px; line-height: 55px; position: relative; border-top: 1px solid #ADD8E6; } .container .item img { width: 45px; height: 45px; margin: 5px; } .container .item .name { position: absolute; width: 90px; top: 0; left: 55px; font-size: 16px; } .container .item .change { width: 100px; position: absolute; top: 0; right: 50px; } .container .item .change a { font-size: 20px; width: 30px; text-decoration: none; background-color: lightgray; vertical-align: middle; } .container .item .change .num { width: 40px; height: 25px; } .container .item .del { position: absolute; top: 0; right: 0px; width: 40px; text-align: center; font-size: 40px; cursor: pointer; color: red; } .container .item .del:hover { background-color: orange; } </style> <body> <div id="app"> <div class="container"> <div class="cart"> <div class="title">我的商品</div> <div> <div class="item"> <img src="img/a.jpg" /> <div class="name"></div> <div class="change"> <a href="">-</a> <input type="text" class="num" /> <a href="">+</a> </div> <div class="del">×</div> </div> <div class="item"> <img src="img/b.jpg" /> <div class="name"></div> <div class="change"> <a href="">-</a> <input type="text" class="num" /> <a href="">+</a> </div> <div class="del">×</div> </div> <div class="item"> <img src="img/c.jpg" /> <div class="name"></div> <div class="change"> <a href="">-</a> <input type="text" class="num" /> <a href="">+</a> </div> <div class="del">×</div> </div> <div class="item"> <img src="img/d.jpg" /> <div class="name"></div> <div class="change"> <a href="">-</a> <input type="text" class="num" /> <a href="">+</a> </div> <div class="del">×</div> </div> <div class="item"> <img src="img/e.jpg" /> <div class="name"></div> <div class="change"> <a href="">-</a> <input type="text" class="num" /> <a href="">+</a> </div> <div class="del">×</div> </div> </div> <div class="total"> <span>总价:123</span> <button>结算</button> </div> </div> </div> </div> <script type="text/javascript" src="js/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { } }); </script> </body> </html>
重构为vue模板后:
需求分析 :
1. 按照组件化方式实现业务需求
- 根据业务功能进行组件化划分
① 标题组件(展示文本)
② 列表组件(列表展示、商品数量变更、商品删除)
③ 结算组件(计算商品总额)
2. 功能实现步骤
- 实现整体布局和样式效果
- 划分独立的功能组件
- 组合所有的子组件形成整体结构
- 逐个实现各个组件功能
- 标题组件
- 列表组件
- 结算组件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <style type="text/css"> .container .cart { width: 300px; margin: auto; } .container .title { background-color: lightblue; height: 40px; line-height: 40px; text-align: center; /*color: #fff;*/ } .container .total { background-color: #FFCE46; height: 50px; line-height: 50px; text-align: right; } .container .total button { margin: 0 10px; background-color: #DC4C40; height: 35px; width: 80px; border: 0; } .container .total span { color: red; font-weight: bold; } .container .item { height: 55px; line-height: 55px; position: relative; border-top: 1px solid #ADD8E6; } .container .item img { width: 45px; height: 45px; margin: 5px; } .container .item .name { position: absolute; width: 90px; top: 0; left: 55px; font-size: 16px; } .container .item .change { width: 100px; position: absolute; top: 0; right: 50px; } .container .item .change a { font-size: 20px; width: 30px; text-decoration: none; background-color: lightgray; vertical-align: middle; } .container .item .change .num { width: 40px; height: 25px; } .container .item .del { position: absolute; top: 0; right: 0px; width: 40px; text-align: center; font-size: 40px; cursor: pointer; color: red; } .container .item .del:hover { background-color: orange; } </style> <body> <div id="app"> <div class="container"> <my-cart></my-cart> </div> </div> <script type="text/javascript" src="js/vue.js"></script> <script type="text/javascript"> var CartTitle = { props: ['uname'], template: ` <div class="title">{{uname}}的商品</div> ` } var CartList = { props: ['list'], template: ` <div> <div :key='item.id' v-for='item in list' class="item"> <img :src="item.img"/> <div class="name">{{item.name}}</div> <div class="change"> <a href="" @click.prevent='sub(item.id)'>-</a> <input type="text" class="num" :value='item.num' @blur='changeNum(item.id, $event)'/> <a href="" @click.prevent='add(item.id)'>+</a> </div> <div class="del" @click='del(item.id)'>×</div> </div> </div> `, methods: { // 下面的所有方法都是要将数据传递给父组件,然后在父组件中处理 changeNum: function (id, event) { this.$emit('change-num', { id: id, type: 'change', num: event.target.value }); }, sub: function (id) { this.$emit('change-num', { id: id, type: 'sub' }); }, add: function (id) { this.$emit('change-num', { id: id, type: 'add' }); }, del: function (id) { // 把id传递给父组件 this.$emit('cart-del', id); } } } var CartTotal = { props: ['list'], template: ` <div class="total"> <span>总价:{{total}}</span> <button>结算</button> </div> `, computed: { total: function () { // 计算商品的总价 var t = 0; this.list.forEach(item => { t += item.price * item.num; }); return t; } } } Vue.component('my-cart', { data: function () { return { uname: '张三', list: [{ id: 1, name: 'TCL彩电', price: 1000, num: 1, img: 'img/a.jpg' }, { id: 2, name: '机顶盒', price: 1000, num: 1, img: 'img/b.jpg' }, { id: 3, name: '海尔冰箱', price: 1000, num: 1, img: 'img/c.jpg' }, { id: 4, name: '小米手机', price: 1000, num: 1, img: 'img/d.jpg' }, { id: 5, name: 'PPTV电视', price: 1000, num: 2, img: 'img/e.jpg' }] } }, template: ` <div class='cart'> <cart-title :uname='uname'></cart-title> <cart-list :list='list' @change-num='changeNum($event)' @cart-del='delCart($event)'></cart-list> <cart-total :list='list'></cart-total> </div> `, components: { 'cart-title': CartTitle, 'cart-list': CartList, 'cart-total': CartTotal }, methods: { changeNum: function (val) { // 分为三种情况:输入域变更、加号变更、减号变更 if (val.type == 'change') { // 根据子组件传递过来的数据,跟新list中对应的数据 this.list.some(item => { if (item.id == val.id) { item.num = val.num; // 终止遍历 return true; } }); } else if (val.type == 'sub') { // 减一操作 this.list.some(item => { if (item.id == val.id) { item.num -= 1; // 终止遍历 return true; } }); } else if (val.type == 'add') { // 加一操作 this.list.some(item => { if (item.id == val.id) { item.num += 1; // 终止遍历 return true; } }); } }, delCart: function (id) { // 根据id删除list中对应的数据 // 1、找到id所对应数据的索引 var index = this.list.findIndex(item => { return item.id == id; }); // 2、根据索引删除对应数据 this.list.splice(index, 1); } } }); var vm = new Vue({ el: '#app', data: { } }); </script> </body> </html>
注意:
① 修改文本框数量操作和删除操作等操作都应该在父组件中进行处理,因为不推荐在子组件中直接操作props中的数据,而应该通知父组件去处理。+1和-1的操作也不能在子组件中完成,必须要在父组件中处理。
② 不能使用v-model来绑定数量的文本框,因为v-model绑定的数据是双向的,而我们在子组件中不能改变props中的数据。所以我们直接把数量显示出来即可
<input type="text" class="num" :value='item.num' @blur='changeNum(item.id, $event)'/>
那什么时候修改这个值呢?需要我们手动去绑定对应的事件。用blur事件,失去焦点的时候更新文本框里面的数量。这个处理也不是在子组件中完成的,而是在父组件中进行处理。
③ some方法的规则:终止遍历使用的是return true。
尚未解决的问题:持续点击减号按钮,会出现负值