【Vue.js】Vue.js中的Vuex、Vue-Ajax和京东购物车项目实战

时间:2022-07-26
本文章向大家介绍【Vue.js】Vue.js中的Vuex、Vue-Ajax和京东购物车项目实战,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1、Vuex

1. 单向数据流理念

组成部分

  • state:驱动应用的数据源
  • view:以声明方式将 state 映射到视图
  • actions:响应在 view 上的用户输入导致的状态变化

图示

传统数据通信存在的问题

当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:(1)多个视图依赖于同一状态,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力;(2)来自不同视图的行为需要变更同一状态,经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。

解决问题的钥匙

把组件的共享状态抽取出来,以一个全局单例模式管理,在这种模式下,项目中的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为。Vuex就是这样一个状态管理模式。

2. Vuex是什么

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
  • 它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
  • 一句话:Vuex相当于一个数据银行,对 vue 应用中多个组件的共享状态进行集中式的管理(读/写)

3. Vuex的组成

图示如下:

state

  • vuex 管理的状态对象
  • 它应该是唯一的:
const state = {
    name: 'zhangsan'
}

mutations

  • 包含多个直接更新 state 的方法(回调函数)的对象
  • 在action中通过commit(‘mutation 名称’)触发mutations中更新state的方法
  • 只能包含同步的代码, 不能写异步代码
const mutations = {
   aaaa (state, {data}) {
       // 更新 state 的 data 属性
   }
}

actions

  • 包含多个事件回调函数的对象
  • Mutation必须是同步的,Action是异步的Mutation
  • 组件中通过 $store.dispatch(‘action 名称’, data)触发
  • 可以包含异步代码(定时器, ajax)
const actions = {
   bbbb({commit, state}, data1) {
         commit('aaaa', {data1})
   }
}

getters

  • 有时候我们需要从 store 中的 state 中派生出一些状态,我们可以理解为vuex中数据的computed功能
## store.js

getters:{
  	money: state => `¥${state.count*1000}`
},
## page.vue

computed: {
  	money() {
    	return this.$store.getters.money;
 	}
}

mapState

  • 更方便的使用api,当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余
  • 为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性
...mapState({
    count:state=>state.count
}),

mapActions

  • 方便快捷的使用action
methods:{
    ...mapActions(['dealCount']),
    ...mapMutations(['count'])
},
  • this.$store.dispatch可以变为
this.dealCount({
    amount: 10
})

mapMutions

...mapMutations(['add'])
this.add()

Modules

  • 面对复杂的应用程序,当管理的状态比较多时, 我们需要将Vuex的store对象分割成多个模块(modules)
const  moduleA = {
    state: { ... },
    mutations: { ... },
    actions: { ... },
    getters: { ... }
}

const  moduleB = {
    state: { ... },
    mutations: { ... },
    actions: { ... },
    getters: { ... }
}

const store = new Vuex.Store({
     modules: {
           a:  moduleA,
           b:  moduleB
     }
});

4. 代码示例

初始化项目

>> vue create lk-vuex-demo
>> vue add vuex
>> npm run serve
## store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

export default new Vuex.Store({
    state: {
        count: 0 // 初始化数据
    },
    mutations: {
        INCREMENT(state){
            state.count++;
        },
        DECREMENT(state){
            state.count--;
        }
    },
    actions: {
        increment({commit}){
            commit('INCREMENT');
        },
        decrement({commit}){
            commit('DECREMENT');
        },
        incrementIfEven({commit, state}){
           if(state.count % 2 === 0){
               commit('INCREMENT');
           }
        },
        incrementAsync({commit}){
            setTimeout(()=>{
                commit('INCREMENT');
            }, 1000);
        }
    },
    getters: {
        evenOrOdd(state){
            return state.count % 2 === 0 ? '偶数': '奇数'
        }
    }
})
## main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount('#app');
## App.vue

<template>
    <div id="app">
         <Counter />
    </div>
</template>

<script>
    import Counter from './components/Counter'
    export default {
        name: 'app',
        components: {
            Counter
        }
    }
</script>

<style>
    #app {
        font-family: 'Avenir', Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
    }
</style>
## Counter01.vue

<template>
    <div>
        <p>点击了{{count}}次</p>
        <button @click="increment">增加+1</button>
        <button @click="decrement">减少-1</button>
    </div>
</template>

<script>
    // import {mapMutations} from 'vuex'
    export default {
        name: "Counter",
        computed: {
            count(){
                return this.$store.state.count
            }
        },
        methods:{
           //  ...mapMutations(['INCREMENT', 'DECREMENT']),
            increment(){
                // this.INCREMENT();
                // this.$store.commit('INCREMENT');
                this.$store.dispatch('increment');
            },
            decrement(){
                // this.DECREMENT();
                // this.$store.commit('DECREMENT');
                this.$store.dispatch('decrement');
            }
        }
    }
</script>

<style scoped>

</style>
## Counter02.vue

<template>
    <div>
        <p>点击了{{count}}次, count是{{evenOrOdd}}</p>
        <button @click="increment">增加+1</button>
        <button @click="decrement">减少-1</button>
        <button @click="incrementIfEven">偶数+1</button>
        <button @click="incrementAsync">异步+1</button>
    </div>
</template>

<script>
    import {mapState, mapGetters, mapActions} from 'vuex'
    export default {
        name: "Counter",
        computed: {
            ...mapState(['count']),
            ...mapGetters(['evenOrOdd'])
        },
        methods:{
           ...mapActions(['increment', 'decrement', 'incrementIfEven', 'incrementAsync'])
        }
    }
</script>

<style scoped>

</style>
## Counter03.vue

<template>
    <div>
        <p>点击了{{count}}次, count是{{evenOrOdd}}</p>
        <button @click="increment">增加+1</button>
        <button @click="decrement">减少-1</button>
        <button @click="incrementIfEven">偶数+1</button>
        <button @click="incrementAsync">异步+1</button>
    </div>
</template>

<script>
    export default {
        name: "Counter",
        computed: {
            count(){
                return this.$store.state.count
            },
            evenOrOdd(){
                return this.$store.getters.evenOrOdd
            }
        },
        methods:{
            increment(){
                this.$store.dispatch('increment');
            },
            decrement(){
                this.$store.dispatch('decrement');
            },
            incrementIfEven(){
                this.$store.dispatch('incrementIfEven');
            },
            incrementAsync(){
                this.$store.dispatch('incrementAsync');
            }
        }
    }
</script>

<style scoped>

</style>

2、Vue-Ajax

Vue 项目中常用的 2 个 Ajax

  • vue-resource:vue 插件,非官方库,vue1.x 使用广泛
  • axios:通用的 ajax 请求库,官方推荐,vue2.x 使用广泛

axios 的使用

  • 官方文档:https://github.com/pagekit/vue-resource/blob/develop/docs/http.md
  • 安装:
>> npm install axios --save
// 引入模块
import axios from 'axios'
// 发送 ajax 请求
axios.get(url)
.then(response => {
      console.log(response.data) ; 		// 得到返回结果数据
}).catch(error => {
      console.log(error.message);
}

测试接口

https://www.easy-mock.com/mock/5d40032d6a3ae527e747fea9/example/itlike/p_list

GET请求实操

axios.get('https://www.easy-mock.com/mock/5d40032d6a3ae527e747fea9/example/itlike/p_list').then((response) => {
    console.log(response);
}).catch(function (error) {
    console.log(error);
});

3、Vuex版本TodoList

代码结构:

## main.js

import Vue from 'vue'
import App from './App.vue'
import './assets/index.css'
import store from './store/index'

Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount('#app');
## index.js

/*
  Vuex核心管理模块 - Store对象
*/

import Vue from 'vue'
import Vuex from 'vuex'

import state from './state'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'

Vue.use(Vuex);

export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters
});
## state.js

/*
  状态对象模块
*/
import localStorageUtil from './../utils/localStorageUtil'

export default {
   todos: localStorageUtil.readTodos()
}
## mutations.js

/*
  多个可以直接同步更新状态的方法 对象模块
*/

import {ADD_TODO, DELETE_TODO, SELECT_ALL_TODO, DELETE_FINISHED_TODO} from './mutations-type'

export default {
    [ADD_TODO](state, {todo}){ 				// ADD_TODO并不是方法名, add_to
        state.todos.unshift(todo);
    },
    [DELETE_TODO](state, {index}){
        state.todos.splice(index, 1);
    },
    [SELECT_ALL_TODO](state, {isCheck}){
        state.todos.forEach(todo => {
            todo.finished = isCheck
        })
    },
    [DELETE_FINISHED_TODO](state){
        state.todos = state.todos.filter(todo=> !todo.finished)
    },
}
## mutations-type.js

/*
  包含多个mutations中方法名称的常量
 */
export const ADD_TODO = 'add_todo'; 							// 添加todo
export const DELETE_TODO = 'delete_todo'; 						// 删除todo
export const SELECT_ALL_TODO = 'select_all_todo'; 				// 全选/取消全选的todo
export const DELETE_FINISHED_TODO = 'delete_finished_todo'; 	// 清除已经完成的todo
## actions.js

/*
  包含多个间接更新state的方法  对象模块
*/
import {ADD_TODO, DELETE_TODO, SELECT_ALL_TODO, DELETE_FINISHED_TODO} from './mutations-type'

export default {
    addTodo({commit}, todo){
        commit(ADD_TODO, {todo});
    },
    delTodo({commit}, index){
        commit(DELETE_TODO, {index});
    },
    selectedAllTodo({commit}, isCheck){
        commit(SELECT_ALL_TODO, {isCheck});
    },
    delFinishedTodos({commit}){
        commit(DELETE_FINISHED_TODO);
    }
}
## getters.js

/*
   服务于 state
*/

export default {
    // 任务总数量
    todosCount(state){
        return state.todos.length;
    },
    // 已经完成的任务数量
    finishedCount(state) {
        return state.todos.reduce((total, todo) => total + (todo.finished ? 1 : 0), 0);
    },
    // 判断是否是全选
    isCheck(state, getters){
        return getters.finishedCount === getters.todosCount && getters.todosCount > 0
    }
}
## localStorageUtil.js

const LK_TODO = 'lk_todo';
export default {
    readTodos(){
        return JSON.parse(localStorage.getItem(LK_TODO) || '[]');
    },
    saveTodos(todos){
        console.log(todos);
        localStorage.setItem(LK_TODO, JSON.stringify(todos));
    }
}
## App.vue

<template>
    <div class="todo-container">
        <div class="todo-wrap">
            <Header/>
            <List/>
            <Footer/>
            <button @click="reqData">获取网络数据</button>
        </div>
    </div>
</template>

<script>
    // 引入组件
    import Header from './components/Header'
    import List from './components/List'
    import Footer from './components/Footer'

    import axios from 'axios'

    export default {
        name: 'app',
        components: {
            Header,
            List,
            Footer
        },
        methods: {
            reqData(){
                axios.get('https://www.easy-mock.com/mock/5d40032d6a3ae527e747fea9/example/itlike/p_list').then((response)=>{
                    console.log(response);
                }).catch((error)=>{
                    console.log(error);
                })
            }
        }
    }
</script>

<style>
    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-wrap {
        padding: 10px;
        border: 1px solid #ddd;
        border-radius: 5px;
    }
</style>
## Header.vue

<template>
    <div class="todo-header">
        <input
            type="text"
            placeholder="请输入今天的任务清单,按回车键确认"
            v-model="title"
            @keyup.enter="addItem"
        />
    </div>
</template>

<script>
    export default {
        name: "Header",
        data(){
            return {
                title: ''
            }
        },
        methods: {
            addItem(){
                // 1. 判断是否为空
                const title = this.title.trim();
                if(!title){
                    alert('输入的任务不能为空!');
                    return;
                }
                // 2. 生成一个todo对象
                let todo = {title, finished: false};
                // 3. 调用父组件的插入方法
                this.$store.dispatch('addTodo', todo);
                // 4. 清空输入框
                this.title = '';
            }
        }
    }
</script>

<style scoped>
    .todo-header input {
        width: 560px;
        height: 28px;
        font-size: 14px;
        border: 1px solid #ccc;
        border-radius: 4px;
        padding: 4px 7px;
        outline: none;
    }

    .todo-header input:focus {
        outline: none;
        border-color: rgba(255, 0, 0, 0.8);
        box-shadow: inset 0 1px 1px rgba(255, 0, 0, 0.075), 0 0 8px rgba(255, 0, 0, 0.6);
    }
</style>
## List.vue

<template>
    <ul class="todo-main">
        <Item
          v-for="(todo, index) in todos"
          :todo="todo"
          :index ="index"
        />
    </ul>
</template>

<script>
    import localStorageUtil from './../utils/localStorageUtil'
    import Item from './Item'
    import {mapState} from 'vuex'
    export default {
        name: "List",
        computed:{
          ...mapState(['todos'])
        },
        components: {
            Item
        },
        watch: {
            todos: {
                deep: true,
                handler: localStorageUtil.saveTodos
            }
        }
    }
</script>

<style scoped>
    .todo-main {
        margin-left: 0;
        border: 1px solid #ddd;
        border-radius: 2px;
        padding: 0;
    }
</style>
## Item.vue

<template>
    <li
      @mouseenter="dealShow(true)"
      @mouseleave="dealShow(false)"
      :style="{backgroundColor: bgColor}"
    >
        <label>
            <input type="checkbox" v-model="todo.finished"/>
            <span>{{todo.title}}</span>
        </label>
        <button v-show="isShowDelButton"  class="btn btn-warning" @click="delItem">删除</button>
    </li>
</template>

<script>
    export default {
        name: "Item",
        props: {
            todo: Object,
            index: Number 				// 当前任务在总任务数组中的下标位置
        },
        data(){
          return{
              isShowDelButton: false,  // false 隐藏 true 显示
              bgColor: '#fff'
          }
        },
        methods: {
            dealShow(isShow){
                // 控制按钮的显示和隐藏
                this.isShowDelButton =  isShow;
                // 控制背景颜色
                this.bgColor = isShow ? '#ddd' : '#fff';
            },

            delItem(){
                if(window.confirm(`您确定删除 ${this.todo.title} 吗?`)){
                     this.$store.dispatch('delTodo', this.index);
                }
            }
        }
    }
</script>

<style scoped>
    li {
        list-style: none;
        height: 36px;
        line-height: 36px;
        padding: 0 5px;
        border-bottom: 1px solid #ddd;
    }

    li label {
        float: left;
        cursor: pointer;
    }

    li label li input {
        vertical-align: middle;
        margin-right: 6px;
        position: relative;
        top: -1px;
    }

    li button {
        padding: 4px 10px;
        float: right;
        margin-top: 3px;
    }

    li:before {
        content: initial;
    }

    li:last-child {
        border-bottom: none;
    }
</style>
## Footer.vue

<template>
    <div class="todo-footer">
        <label>
            <input slot="isCheck" type="checkbox" v-model="selectedAllOrNot"/>
        </label>
        <span>
            <span slot="finish">已完成{{finishedCount}}件 / 总计{{todosCount}}件</span>
        </span>
        <button  slot="delete" class="btn btn-warning" @click="delFinishedTodos">清除已完成任务</button>
    </div>
</template>

<script>
    import {mapGetters, mapActions} from 'vuex'
    export default {
        name: "Footer",
        computed:{
            ...mapGetters(['todosCount', 'finishedCount', 'isCheck']),
            selectedAllOrNot: {
                get(){ // 决定是否勾选
                    return this.isCheck;
                },
                set(value){
                    this.selectedAllTodo(value)
                }
            }
        },
        methods: {
            ...mapActions(['selectedAllTodo', 'delFinishedTodos'])
        }
    }
</script>

<style scoped>
    .todo-footer {
        height: 40px;
        line-height: 40px;
        padding-left: 6px;
        margin-top: 5px;
    }

    .todo-footer label {
        display: inline-block;
        margin-right: 20px;
        cursor: pointer;
    }

    .todo-footer label input {
        position: relative;
        top: -1px;
        vertical-align: middle;
    }

    .todo-footer button {
        float: right;
        margin-top: 5px;
    }
</style>

4、京东购物车项目实战

代码结构:

## main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false;

new Vue({
  store,
  render: h => h(App)
}).$mount('#app');
## store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {

  },
  mutations: {

  },
  actions: {

  }
})
App.vue

<template>
    <div id="app">
        <Cart />
    </div>
</template>

<script>
    import Cart from './components/Cart'

    export default {
        name: 'app',
        components: {
            Cart
        }
    }
</script>

<style>

</style>
## Cart.vue

<template>
    <div>
        <!--头部区域-->
        <header class="header">
            <a href="index.html" class="icon-back"></a>
            <h3>购物车</h3>
            <a href="" class="icon-menu"></a>
        </header>
        <!--安全提示-->
        <section class="jd-safe-tip">
            <p class="tip-word">
                您正在安全购物环境中,请放心购物
            </p>
        </section>
        <!--中间内容-->
        <main class="jd-shop-cart-list">
            <section>
                <div class="shop-cart-list-title">
                    <div class="left">
                        <span class="cart-title">撩课自营</span>
                    </div>
                    <span class="right">您享受满100元免运费服务</span>
                </div>
                <div class="shop-cart-list-con" v-for="(shop, index) in shopListArr" :key="shop.shopId">
                    <div class="left">
                        <a
                             href="javascript:;"
                             class="cart-check-box"
                             :checked="shop.checked"
                             @click="singerShopSelected(shop)"
                        >
                        </a>
                    </div>
                    <div class="center">
                        <img :src="shop.shopImage" :alt="shop.shopName">
                    </div>
                    <div class="right">
                        <a href="#">{{shop.shopName}}</a>
                        <div class="shop-price">
                            <div class="singer-price">{{shop.shopPrice | moneyFormat}}</div>
                            <div class="total-price">总价:{{ shop.shopPrice * shop.shopNumber | moneyFormat}}</div>
                        </div>
                        <div class="shop-deal">
                            <span @click="singerShopPrice(shop, false)">-</span>
                            <input disabled="flase" type="number" v-model="shop.shopNumber">
                            <span @click="singerShopPrice(shop, true)">+</span>
                        </div>
                        <div class="shop-deal-right" @click="clickTrash(shop, $event)">
                            <span></span>
                            <span></span>
                        </div>
                    </div>
                </div>
            </section>
        </main>
        <!--面板-->
        <div ref="panel" class="panel" style="display: none;">
            <div ref="panelContent" class="panel-content">
                <div class="panel-title">您确认删除这个商品吗?</div>
                <div class="panel-footer">
                    <a @click.prevent="hidePanel" href="javascript:;" class="cancel">取消</a>
                    <a @click.prevent="delShop" href="javascript:;" class="submit">确定</a>
                </div>
            </div>
        </div>
        <!--底部通栏-->
        <div id="tab_bar">
            <div class="tab-bar-left">
                <a
                     href="javascript:;"
                     class="cart-check-box"
                     :checked="isSelectedAll"
                     @click="selectedAll(isSelectedAll)"
                ></a>
                <span style="font-size: 16px;">全选</span>
                <div class="select-all">
                    合计:<span class="total-price">{{totalPrice | moneyFormat}}</span>
                </div>
            </div>
            <div class="tab-bar-right">
                <a href="index.html" class="pay">去结算</a>
            </div>
        </div>
    </div>
</template>

<script>
    import './../assets/css/base.css'
    import './../assets/css/cart.css'
    import axios from 'axios'

    export default {
        name: "Cart",
        data() {
            return {                
                shopListArr: [],			// 购物车中的商品数据
                totalPrice: 0,
                isSelectedAll: false, 		// 标识是否全选
                up: '', 					// 盖子
                currentDelShop: {}, 		// 要删除的商品
            }
        },
        created() {
            this.getProduct();
        },
        methods: {
            // 1. 获取网络数据
            getProduct() {
                axios.get('http://demo.itlike.com/web/jdm/api/shoplist').then((response) => {
                    if (response.data.status === 200) {
                        this.shopListArr = response.data.result.shopList;
                    }
                }).catch((error) => {
                    alert('网络出现异常!');
                })
            },
            //  2. 单个商品的加减
            singerShopPrice(shop, flag) { // true +  false -
                if (flag) { 					// 加
                    shop.shopNumber += 1;
                } else { 						// 减
                    if (shop.shopNumber <= 1) {
                        shop.shopNumber = 1;
                        alert('只有一件商品啦~');
                        return;
                    }
                    shop.shopNumber -= 1;
                }
                // 2.1 计算总价
                this.getAllShopPrice();
            },

            // 3. 全选
            selectedAll(flag) {
                // 3.1 属性控制
                this.isSelectedAll = !flag;
                // 3.2 遍历购物车中所有的商品数据
                this.shopListArr.forEach((value, index) => {
                    // 3.3 判断
                    if (typeof value.checked === 'undefined') { // 当前对象中没有该属性
                        this.$set(value, 'checked', !flag);
                    } else {
                        value.checked = !flag;
                    }
                });
                // 3.3 计算总价
                this.getAllShopPrice();
            },

            // 4. 单个商品的选中和全校选中
            singerShopSelected(shop) {
                // 4.1 判断有没有该属性
                if (typeof shop.checked === 'undefined') { 		// 当前对象中没有该属性
                    this.$set(shop, 'checked', true);
                } else {
                    shop.checked = !shop.checked;
                }
                // 4.2 判断是否全选
                this.hasSelectedAll();
                // 4.3 计算总价
                this.getAllShopPrice();
            },

            // 5. 判断是否要全选
            hasSelectedAll() {
                let flag = true;
                this.shopListArr.forEach((value, index) => {
                    if (!value.checked) {
                        flag = false;
                    }
                });
                this.isSelectedAll = flag && this.shopListArr.length > 0;
            },

            // 6. 计算商品的总价格
            getAllShopPrice() {
                let tPrice = 0;
                //  6.1 遍历所有的商品
                this.shopListArr.forEach((value, index) => {
                    // 6.2 判断是否选中
                    if (value.checked) {
                        tPrice += value.shopPrice * value.shopNumber;
                    }
                });
                // 6.3 更新总价格
                this.totalPrice = tPrice;
            },

            //  7. 点击垃圾篓
            clickTrash(shop, event) {
                // 7.1 获取父标签
                let trashes = event.target.parentNode;
                let up = trashes.firstElementChild;
                // console.log(up);

                // 7.2 加过渡
                up.style.transition = 'all .2s ease';
                up.style.webkitTransition = 'all .2s ease';

                // 7.3 实现动画
                up.style.transformOrigin = '0 0.5rem';
                up.style.webkitTransformOrigin = '0 0.5rem';
                up.style.transform = 'rotate(-45deg)';
                up.style.webkitTransform = 'rotate(-45deg)';
                this.up = up;

                // 7.4 显示面板
                this.$refs.panel.style.display = 'block';
                this.$refs.panelContent.className = 'panel-content jump';

                // 7.5 计算要被删除的商品
                this.currentDelShop = shop;
            },

            // 8. 点击取消
            hidePanel(){
                // 8.1 面板隐藏
                this.$refs.panel.style.display = 'none';
                this.$refs.panelContent.className = 'panel-content';

                // 8.2 盖子闭合
                this.up.style.transform = 'rotate(0deg)';
                this.up.style.webkitTransform = 'rotate(0deg)';
            },

            // 9. 删除当前的商品
            delShop(){
                // 9.1 隐藏面板
                this.$refs.panel.style.display = 'none';
                // 获取索引
                let index = this.shopListArr.indexOf(this.currentDelShop);
                this.shopListArr.splice(index, 1);

                // 9.2 计算总价
                this.getAllShopPrice();
                this.hasSelectedAll();
            }
        },
        filters: {
            // 格式化金钱
            moneyFormat(money) {
                return '¥' + Number(money).toFixed(2)
            }
        }
    }
</script>

<style scoped>

</style>
## base.css

*, ::before, ::after{
    margin: 0;padding: 0;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    /* 去除移动端点击产生的高亮状态 */
    -webkit-tap-highlight-color: transparent;
}

html{font-size: 10px;font-family: 'Microsoft Yahei', sans-serif;color: #000;}
a{text-decoration: none;color: #666666;}
ul,ol{list-style: none;}
input{
    border: none;
    outline: none;
    /* 针对iOS浏览器, 清除默认非扁平化分格 */
    -webkit-appearance: none;
}

.clearfix::before,
.clearfix::after{
    content: '';
    height: 0;
    line-height: 0;
    display: block;
    visibility: hidden;
    clear: both;
}

[class^='icon-'],
[class*=' icon-']{
    background: url("../images/sprites.png") no-repeat;
    -webkit-background-size: 20rem 20rem;
    background-size: 20rem 20rem;
}
## cart.css

body{
    background-color: #f5f5f5;
    font-size: 1.4rem;
    padding-top: 4.4rem;
}

.header{
    z-index: 999;
}

/*************************导航样式***************************/
.header{
    width: 100%;
    height: 4.4rem;
    background: url("./../images/header-bg.png") repeat-x;
    -webkit-background-size: 0.1rem 4.4rem;
    background-size: 0.1rem 4.4rem;
    position: fixed;
    left: 0;
    top: 0;
}

.header .icon-back,
.header .icon-menu{
    width: 4rem;
    height: 4rem;
    position: absolute;
    top: 0;
    padding: 10px;
    /* 背景图的定位从内容开始计算 */
    -webkit-background-origin: content-box;
    background-origin: content-box;
    /* 背景图从内容开始显示 */
    background-clip: content-box;
}

.header .icon-back{
    left: 0;
    background-position: -2rem 0;
}

.header .icon-menu{
    right: 0;
    background-position: -6rem 0;
}

.header h3{
    width: 100%;
    height: 4.4rem;
    line-height: 4.4rem;
    text-align: center;
    /*background-color: red;*/
    padding-left: 4rem;
    padding-right: 4rem;
    overflow: hidden;
    font-size: 1.6rem;
    color: #666666;
}

.header form{
    width: 100%;
    height: 4.4rem;
    padding-left: 4rem;
    padding-right: 4rem;
}

.header form input{
    width: 100%;
    height: 3.4rem;
    border: 1px solid #e0e0e0;
    margin-top: 0.5rem;
    padding-left: 0.5rem;
}

/* 安全提示 */
.jd-safe-tip{
    height: 3.6rem;
    line-height: 3.6rem;
    background-color: #fff;
    border-bottom: 1px solid #e0e0e0;
    text-align: center;
}

.jd-safe-tip .tip-word{
    position: relative;
    /*background-color: red;*/
    /* 改变标签类型 */
    display: inline-block;
}

.jd-safe-tip .tip-word::before{
    content: '';
    width: 1.8rem;
    height: 1.8rem;
    background: url("./../images/safe_icon.png ") no-repeat;
    -webkit-background-size: 1.8rem 1.8rem;
    background-size: 1.8rem 1.8rem;
    position: absolute;
    left: -2.1rem;
    top: 0.9rem;
}

/* 列表内容 */
.jd-shop-cart-list{
   padding-bottom: 6rem;
}

.jd-shop-cart-list section{
    margin-top: 1.5rem;
    border-top: 0.1rem solid #e0e0e0;
    background-color: #fff;
}

.jd-shop-cart-list section .shop-cart-list-title{
    display: flex;
    justify-content: space-between;
    height: 4.4rem;
    line-height: 4.4rem;
}

.jd-shop-cart-list section  .shop-cart-list-title .left{
    flex: 1;

    /*background-color: red;*/
    padding-left: 8px;

    display: flex;
    /*justify-content: space-between;*/
    align-items: center;
}

.jd-shop-cart-list section .cart-logo{
    background: url("./../images/buy-logo.png") no-repeat;
    -webkit-background-size: 1.5rem 1.5rem;
    background-size: 1.5rem 1.5rem;
    width: 1.5rem;
    height: 1.5rem;
    margin: 0 0.5rem;
}

.cart-check-box{
    background: url("./../images/shop-icon.png ") no-repeat;
    -webkit-background-size: 5rem 10rem;
    background-size: 5rem 10rem;
    width: 2rem;
    height: 2rem;
}

.cart-check-box[checked]{
    background-position: -2.5rem 0;
}

.jd-shop-cart-list section  .shop-cart-list-title .right{
    /* background-color: red; */
    flex: 1;
    color: red;
}

.shop-cart-list-con{
    /* background-color: red; */
    /* 伸缩布局 */
    display: flex;
    height: 10rem;
    border-bottom:  0.1rem solid #e0e0e0;
    margin-bottom: 0.7rem;
}

.shop-cart-list-con .left{
    /* background: purple; */
    flex: 1;
    display: flex;
    /* justify-content: center; */
}

.shop-cart-list-con .left a{
    display: inline-block;
    margin-top: 0.5rem;
    margin-left: 0.7rem;
}


.shop-cart-list-con .center{
    /* background: blue; */
    flex: 3;
}

.shop-cart-list-con .center img{
    width: 100%;
    height: 85%;
}

.shop-cart-list-con .right{
    /* background: orangered; */
    flex: 9;
    display: flex;
    flex-direction: column;
    margin-left: 0.5rem;
    margin-right: 0.5rem;

    position: relative;
}

.shop-cart-list-con .right a{
    height: 4rem;
    line-height: 2rem;
    overflow: hidden;
    /* background-color: red; */
    margin-bottom: 0.3rem;
}

.shop-cart-list-con .right .shop-deal span{
    border: 1px solid #e0e0e0;
    display: inline-block;
    width: 3rem;
    height: 2.5rem;
    line-height: 2.5rem;
    text-align: center;
    float: left;
}

.shop-cart-list-con .right .shop-deal span:first-child{
    border-top-left-radius: 0.3rem;
    border-bottom-left-radius: 0.3rem;
}

.shop-cart-list-con .right .shop-deal span:last-child{
    border-top-right-radius: 0.3rem;
    border-bottom-right-radius: 0.3rem;
}

.shop-cart-list-con .right .shop-deal input{
    border-top: 1px solid #e0e0e0;
    border-bottom: 1px solid #e0e0e0;
    float: left;
    width: 5rem;
    height: 2.5rem;
    text-align: center;
}

.shop-cart-list-con .right .shop-deal-right{
    width: 3rem;
    height: 3rem;
    /*background-color: red;*/
    position: absolute;
    right: 0.5rem;
    bottom: 0.1rem;
}

.shop-cart-list-con .right .shop-deal-right span:first-child{
    background: url("./../images/delete_up.png") no-repeat;
    -webkit-background-size: 1.8rem 0.4rem;
    background-size: 1.8rem 0.4rem;
    width: 1.8rem;
    height: 0.4rem;
    display: block;
    margin: 0 auto;
}

.shop-cart-list-con .right .shop-deal-right span:last-child{
    background: url("./../images/delete_down.png") no-repeat;
    -webkit-background-size: 1.7rem 1.7rem;
    background-size: 1.7rem 1.7rem;
    width: 1.7rem;
    height: 1.7rem;
    display: block;
    margin: -0.3rem auto 0;
}

.shop-price{
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 0.5rem;
}

.shop-price .total-price{
    color: red;
}

/* 面板 */
.panel{
    width: 100%;
    height: 100%;
    position: fixed;
    left: 0;
    top: 0;
    background-color: rgba(0, 0, 0, .6);
    z-index: 1000;
}

.panel-content{
    width:84%;
    position: absolute;
    left:8%;
    top: 200px;
    background-color: #fff;
    border: 1px solid #e0e0e0;
    border-radius: 5px;
    padding: 15px;
}

.panel-title{
    text-align: center;
    font-size: 17px;
    padding-bottom: 30px;
    border-bottom: 1px solid #e0e0e0;
    margin-bottom: 10px;
}

.panel-footer{
    width: 100%;
    height: 50px;
    /* background-color: green; */
}

.panel-footer a{
    width: 120px;
    height: 40px;
    border: 1px solid #e0e0e0;
    margin-top: 10px;
    text-align: center;
    line-height: 40px;
    font-size: 18px;
    border-radius: 5px;
}

.panel-footer .cancel{
    float: left;

}

.panel-footer .submit{
    float: right;
    background-color: #E9232C;
    color:#fff;
    border: none;
}

.panel-is-show{
   display: none;
}

/* 实现动画效果 */
.jump{
    animation: jump 1s ease;
}

@keyframes jump {
    0%{
        opacity: 0;
        transform: translateY(-300rem);
        -webkit-transform: translateY(-300rem);
    }

    25%{
        opacity: 0.3;
        transform: translateY(1rem);
        -webkit-transform: translateY(1rem);
    }

    50%{
        opacity: 0.6;
        transform: translateY(3rem);
        -webkit-transform: translateY(3rem);
    }

    80%{
        opacity: 0.8;
        transform: translateY(-1rem);
        -webkit-transform: translateY(-1rem);
    }

    90%{
        opacity: 1;
        transform: translateY(0.5rem);
        -webkit-transform: translateY(0.5rem);
    }

    100%{
        opacity: 1;
        transform: none;
        -webkit-transform: none;
    }
}

/* 底部通栏 */
#tab_bar{
    position: fixed;
    left:0;
    bottom:0;
    width:100%;
    height: 44px;
    background-color: #fff;
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-shadow: 5px 5px 5px #000;
}

.tab-bar-left{
    display: flex;
    align-items: center;
    margin-left: 7px;
}

.tab-bar-left .select-all{
    margin-left: 8px;
    font-size: 16px;
}

.tab-bar-right .pay{
    width: 90px;
    height: 44px;
    background-color: #E9232C;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 18px;
    color: #fff;
}

运行结果: