Vue购物车案例
需求分析
1.按照组件方式实现业务需求
- 根据业务功能进行组件划分
- 标题组件(展示文本)
- 列表组件(列表展示、商品数量变更、商品删除)
- 结算组件(计算商品总额)
实现步骤
1.功能实现步骤
- 实现整体布局和样式效果
- 划分独立的功能组件
- 组合所有的子组件形成整体结构
- 逐个实现各个组件功能
- 标题组件
- 列表组件
- 结算组件
CSS 样式
<style type="text/css">
.container {
}
.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>
祖传 app 组件
<div id="app">
<div class="container">
<my-cart></my-cart>
</div>
</div>
实现基本布局
var CartTitle = {
template: `
<div class="title">张三的商品</div>
`
}
var CartList = {
template: `
<div>
<div class="item">
<img src="img/a.png"/>
<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>
`
}
var CartTotal = {
template: `
<div class="total">
<span>总价:{{ total }}</span>
<button>结算</button>
</div>
`
}
Vue.component("my-cart",{
template: `
<div class="cart">
<cart-title :uname="uname"></cart-title>
<cart-list :list="list"></cart-list>
<cart-total :list="list"></cart-total>
</div>
`,
components: {
'cart-title': CartTitle,
'cart-list': CartList,
'cart-total': CartTotal
}
})
const app = new Vue({
el: "#app",
data: {},
})
接下来要使用
my-cart
给各个子组件传递需要的内容
var CartTitle = {
props: ["uname"],
template: `
<div class="title">{{ uname }}的商品</div>
`
}
var CartList = {
props: ["list"],
template: `
<div>
<div class="item" :key="item.id" v-for="item in list">
<img :src="item.img"/>
<div class="name">{{ item.name }}</div>
<div class="change">
<a href="">-</a>
<input type="text" class="num" />
<a href="">+</a>
</div>
<div class="del">×</div>
</div>
</div>
`
}
var CartTotal = {
props: ["list"],
template: `
<div class="total">
<span>总价:{{ total }}</span>
<button>结算</button>
</div>
`,
computed: {
total: function(){
// 计算商品的总价
var t = 0
this.list.filter((item)=>{
t += item.num * item.price
})
return t
}
}
}
Vue.component("my-cart",{
data: function(){
return {
uname: "张三",
list: [{
id: 1,
name: 'TCL彩电',
price: 1000,
num: 1,
img: 'https://cdn.colorhub.me/imgsrv/d69c1836dbc86d7b0deb9256d6817d71ce8baeb1'
},{
id: 2,
name: '机顶盒',
price: 1000,
num: 1,
img: 'https://cdn.colorhub.me/imgsrv/0459b5ebcc9c7fd76c320f77dfc657c3305e6c71'
},{
id: 3,
name: '海尔冰箱',
price: 1000,
num: 1,
img: 'https://cdn.colorhub.me/imgsrv/670693a1f124395e9d3f0ca1f6993eac845cb6e1'
},{
id: 4,
name: '小米手机',
price: 1000,
num: 1,
img: 'https://cdn.colorhub.me/imgsrv/0e3eee5ee122361806bc95daa3a3c0951337c16b'
},{
id: 5,
name: 'PPTV电视',
price: 1000,
num: 2,
img: 'https://cdn.colorhub.me/imgsrv/6ab9f09d1b12a199084addd8b5b5253da6b0b861'
}]
}
},
template: `
<div class="cart">
<cart-title :uname="uname"></cart-title>
<cart-list :list="list"></cart-list>
<cart-total :list="list"></cart-total>
</div>
`,
components: {
'cart-title': CartTitle,
'cart-list': CartList,
'cart-total': CartTotal
}
})
const app = new Vue({
el: "#app",
data: {},
})
接下来要实现删除按钮的功能
在项目开发中,在Vue中的父组件向子组件传递数据是通过 **v-bind:属性=“属性值”动态赋值,子组件通过props:{属性1,属性2,…}**接收传递过来的数据。
值得注意的是,Vue中的props传递是单向的,也就是说父子组件的prop之间形成的是一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。
这么做的原因是:防止在子组件中更改父级组件的状态
组件是可复用的,假设该子组件被复用了n次,当某一个组件意外地更改了其接受到的props数据 ----> 意味着,同时更改了父组件中相对应的属性值 ----> 意外地更改其它(n-1)个子组件中的 props数据,引起混乱
所以我们不应该在子组件中直接更改其props数据。
但有存在某些情况:必须对父组件传递过来的props进行转换处理,利用computed计算属性对数据进行处理。
为
<cart-list>
组件中的删除按钮添加删除事件,把对应的商品 id 传进去
<div class="del" @click="del(item.id)">×</div>
根据上面的原则,我们不能直接修改 props 传递过来的 list
然后把 id 传递给父组件,cart-del 是自定义的事件名称
methods: {
del: function(id){
// 把 id 传递给父组件
this.$emit("cart-del", id)
}
}
然后在父组件中的
<cart-list>
子组件中使用 cart-del 事件,同样是自定义一个事件名,通过$event
来接收子组件传递过来的 id
<div class="cart">
<cart-title :uname="uname"></cart-title>
<cart-list :list="list" @cart-del="delCart($event)"></cart-list>
<cart-total :list="list"></cart-total>
</div>
然后通过父组件来删除对应的商品
methods: {
delCart: function(id){
var index = this.list.findIndex((item)=>{
return item.id == id
})
this.list.splice(index, 1)
}
}
在 input 中显示相应的商品数量
同样,我们会想用
v-model
来实现数量变化,同时去修改 list 列表,但是这样不行,因为根据以上的原则,不应该在子组件中直接更改其props数据,所以我直接去使用 :value 来获取商品的数量进行显示,然后通过绑定失去焦点的事件去父组件中动态修改 list 数据,如下
@blur - 失去焦点事件,里面传了两个参数
- item.id - 触发该事件的商品 id
- $event - 事件对象,固定名称
<input type="text" class="num" :value="item.num" @blur="changeNum(item.id, $event)" />
在methods 中定义该事件,把 id 和 变化之后的值传递给父组件,让父组件去真正的定义该事件
- id - 商品id
- num - 变化之后的值
changeNum: function(id, event){
// console.log(id, event.target.value);
this.$emit("change-num", {
id: id,
num: event.target.value
})
}
在父组件中使用该自定义事件
<cart-list :list="list" @cart-del="delCart($event)" @change-num="changeNum($event)"></cart-list>
在父组件中定义该事件
changeNum: function(val){
this.list.some((item)=>{
if(item.id == val.id){
item.num = val.num
// 终止遍历
return true
}
})
}
最后写一下加减商品数量的逻辑
<a href="" @click.prevent="sub(item.id)">-</a>
<a href="" @click.prevent="add(item.id)">+</a>
同样,不能在子组件中直接修改父组件中的数据
为了方便,我使用了相同名称的自定义组件,但是我为™添加了不同了 type 类型
changeNum: function(id, event){
// console.log(id, event.target.value);
this.$emit("change-num", {
id: id,
type: "change",
num: event.target.value
})
},
sub: function(id){
this.$emit("change-num", {
id: id,
type: "sub",
num: event.target.value
})
},
add: function(id){
this.$emit("change-num", {
id: id,
type: "add",
num: event.target.value
})
}
在父组件中使用
function(val){
if(val.type == "change"){
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
}
})
}
}
实现效果图